eFootball Player.bin Reverse Engineering Notes: What Four Screw-Ups Taught Me
From FF mode losing skills, an editor that couldn't find players, to CSV validation false alarms — four pitfalls in eFootball player data reverse engineering and the lessons behind them.
eFootball Player.bin Reverse Engineering Notes: What Four Screw-Ups Taught Me
I’ve been reverse engineering eFootball’s Player.bin file. This 14.7MB binary stores every player’s attributes, skills, and playing styles.
Over the past few months, Hermes Agent and I cracked 26 ability stats, 62 skill bits, 6 AI match styles, Playing Style, Form, and Injury Resistance. I also built three GUI editors (full version, Lite, ability-only), all packaged as Windows exes.
Sounds smooth. But along the way I stepped into four maddening pitfalls. Every single one was a “looks fine, actually completely wrong” kind of bug.
Screw-Up One: FF Mode Loses Skills
Symptom: Users reported that after one-click FF (max all skills and abilities), four skills — Sliding Tackle, Interception, Phenomenal Pass, Man Marking — weren’t active in-game.
Investigation:
The FF mode logic was:
- Fill the 84-byte attribute region with
0xFF - Restore protected fields (height, weight, age, preferred foot)
- Call
_fill_skills()to set all skill bits - Restore Playing Style
The problem was in step 4. Playing Style is a 6-bit field at byte78[2:7]. But those exact same skill bits also live in byte78:
byte78 bit2 = Sliding Tackle
byte78 bit3 = Interception
byte78 bit4 = Phenomenal Pass
byte78 bit5 = Man Marking
_fill_skills() internally used |= 0xAC to set those bits. But immediately afterward, FF mode called wb(STYLE_BIT, 6, style_val) to restore Playing Style — which overwrote the skill bits in the same byte with the original style value.
If the original style code was 0 (e.g., CF’s Goal Poacher), byte78[2:7] all got zeroed, and all four skills were lost.
Lesson:
A code comment said “must restore after style recovery,” but the restore logic was inside _fill_skills() while the style recovery was outside. Comments and code at different layers — bugs guaranteed.
The fix was trivial — after the style restore in maximize() and godmode(), add one line:
data[base + 78] |= 0xBC # Restore byte78[2:5,7] skill bits
Screw-Up Two: The Ability Editor Can’t Find Players
Symptom: eFootball_Ability_Editor.exe search was completely broken. Type a player name or decimal ID — always “Not Found.”
Investigation:
The editor traversed records like this:
for i in range(total):
off = i * 84 # Fixed 84-byte stride
pid = struct.unpack_from("<Q", data, off + 8)[0]
This assumed all records are fixed 84 bytes. But Player.bin’s actual structure is:
[84-byte attributes] + [variable-length name region (103~150 bytes)]
The name region contains Japanese name + English name, null-delimited, with trailing padding.
The first record happened to be readable (off=0), but from the second record onward, every read landed inside the name region — the “PID” being read was just byte fragments of name strings. Search obviously failed.
And then you think you’ve fixed it. You haven’t.
Screw-Up Three: After the Fix, Still Can’t Find Players
Symptom: After switching from i * 84 to a correct variable-length traversal, the name index worked (found 40,102 entries), but decimal ID search still couldn’t find Haaland.
Investigation:
The new iter_records() located the next record by skipping null padding:
while data[name_end] == 0:
name_end += 1
off = name_end # start of next record
This logic was correct most of the time. But one record had a large block of consecutive null padding (~40 bytes) after its name region, and right after that padding sat the middle of another name region (the text “RAUL”). iter_records thought it found the next record — but was actually off by 8 bytes.
An 8-byte offset means: every subsequent record’s PID read was wrong. Haaland was never going to be found.
The real fix:
ID search no longer depends on iter_records. Instead, use data.find(struct.pack("<Q", pid)) to directly search for the PID byte sequence. It’s O(n) but 100% accurate.
Lesson:
A “clever parser” loses to a “dumb but correct search.” Boundary detection in variable-length formats is inherently heuristic — there’s always some unforeseen corner case. Byte search is slower, but it guarantees finding what you’re looking for. For a 14MB binary, Python’s find() takes a few milliseconds anyway.
Screw-Up Four: CSV Validation False Alarms
Symptom: When validating the 62 skill bit mappings against CSV data, several skills had abysmal F1 scores — Chip Shot Control at only 45.7%!
Investigation:
The CSV had 15,777 player records, each with a list of skills owned. The binary had ~112,000 records. I matched both datasets by Short ID, then compared every skill bit.
But there was a problem: the same Short ID could appear multiple times in the binary — different card versions of the same player (Standard, Featured, Epic). The CSV had one skill list, but the binary had multiple versions. I was comparing every binary copy against the same single CSV skill set. Non-matches were naturally counted as False Negatives.
After deduplication, all 62 skills had F1 scores above 97.8%. Chip Shot Control shot from 45.7% to 99.6%.
Lesson:
Bugs in your validation method are harder to find than bugs in the code you’re testing. You stare at F1=45.7% and start doubting whether the bit positions are wrong, re-checking PESDB, re-cross-validating, re-counting bits — but the code was fine all along. The validation script was double-counting.
Before you start fixing code, make sure your tests are actually testing what you think they’re testing.
Form & Injury Resistance: An Unexpected Discovery
While investigating, I happened to match CSV Form data (Standard/Unwavering/Inconsistent) and Injury Resistance (Low/Medium/High) against the binary — and found the encoding positions:
- byte31[6] is the explicit encoding flag (0=default/engine-determined)
- byte72[0:2] encodes Form (01=Standard, 10=Unwavering, 00=Inconsistent)
- byte70[7] + byte71[0] encode Injury (00=Low, 10=Medium, 01=High)
Verified against 4,626 players — 99.4% accuracy.
These two fields were previously “unknown metadata” — in the old Player.bin, the corresponding bytes were all FF. eFootball 2026 was the first version to start using these bits.
Summary
| # | Problem | Root Cause | Lesson |
|---|---|---|---|
| 1 | FF loses 4 skills | Style restore overwrites shared byte | Comments and code at different layers guarantee bugs |
| 2 | Editor can’t find players | Assumed fixed record length | Confirm file format before writing parser |
| 3 | Still can’t find after fix | Boundary detection misled by nulls | Dumb search beats clever parser |
| 4 | CSV validation false alarm | Duplicate PIDs not deduplicated | Verify test methodology before doubting code |
All four problems boil down to one thing: wrong assumptions. Assumed fixed-length records. Assumed boundary detection was correct. Assumed no duplicate data.
That’s binary reverse engineering for you — you’re always operating on an incomplete understanding of a format you can’t fully see. Every step can be right, until suddenly it’s not.
All fixes committed to efootball-player-tool.
by Jiahao Ren | github.com/Giggitycountless | jiahao.uk