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.

· 6 min read

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:

  1. Fill the 84-byte attribute region with 0xFF
  2. Restore protected fields (height, weight, age, preferred foot)
  3. Call _fill_skills() to set all skill bits
  4. 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

#ProblemRoot CauseLesson
1FF loses 4 skillsStyle restore overwrites shared byteComments and code at different layers guarantee bugs
2Editor can’t find playersAssumed fixed record lengthConfirm file format before writing parser
3Still can’t find after fixBoundary detection misled by nullsDumb search beats clever parser
4CSV validation false alarmDuplicate PIDs not deduplicatedVerify 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