Reading eFootball's Player.bin From Scratch: A Reverse-Engineering Study Guide
Someone recently sent me a dozen questions about decoding Player.bin. I turned my answers into a study guide: where the file lives, how records are cut, how the IDs work, how to read bit-packed fields, and the part that matters most: how to prove an offset is actually right.
Back in June I wrote Player.bin Reverse Engineering Notes, which was about four screw-ups. Since then the game changed its data format, the old record layout became useless, and I more or less decoded the thing again from scratch.
Recently someone asked me: if I want to build an eFootball data site on top of Player.bin, where do I start? They were good questions, a dozen of them: where the file is, fixed or variable-length records, where the ID lives, which offsets are confirmed, whether differential analysis is reliable… So I turned my answers into this study guide.
It isn’t a copy-paste offset table. Offset tables go stale, and any Konami update can move things. What I want to get across is how to find things and how to verify them. That part doesn’t go stale.
Updated 26 September: the day after publishing, I re-checked every number here against the official database and a patched one, and fixed a few things (the number of special cards, how the IDs split into families, pack names, the number of empty tables, and how the two newest skills were verified). They’re all listed in section 10.
Boundaries first. This is independent format analysis done on my own legally purchased install, with no connection to Konami whatsoever. There are no game files, data dumps or keys in here, only format and method. It’s about offline single-player only: online modes are server-authoritative, nothing here touches them, and I have no interest in anything that would. Editing the database almost certainly breaches the EULA. Back up first; it’s at your own risk.
1. Know what it is, and what it isn’t
Before touching a single byte, answer this: what questions can this data actually answer?
Player.bin is the game’s offline database. It holds the licensed club and national-team players plus a subset of special cards, and it’s what offline matches (Trial Match, for example) read.
It is not account data. Everything on the Dream Team side lives on Konami’s servers; none of it is stored locally:
| Local files (used offline) | Server (account data) |
|---|---|
| Abilities, skills, aptitude, form | Cards owned, levels, training points |
| Squads, shirt numbers, roles | Boosters on each card |
| Formations, manager names | GP, coins, your squad and manager |
There is a PlayerBooster.bin locally, but it only defines the boosters. It doesn’t record who has which.
It’s not a complete card database either. In the official builds I’ve looked at, Player.bin has about 23,500 records, of which only about 700 are special cards (a community-patched database I compared has about 13,600). Konami doesn’t ship every Dream Team card in the offline database, so for a card catalogue, sites like PESDB remain unavoidable.
For the special cards it does include, though, the information is complete: each one has the pack it came from (section 9), and the card type (Big Time, Show Time…) is encoded in the card’s ID (section 4).
What makes Player.bin valuable is the data websites don’t show well: exact stored values, squads and shirt numbers, youth club and loan info, second nationality, team formations, appearance data.
2. Where the file is and how to open it
I work on eFootball 2027, Steam (PC). Everything here was established between August and September 2026, across several weekly database updates. I haven’t touched console or mobile at all.
<Steam library>\steamapps\common\eFootball\cpk\dt200_console_all.cpk
└─ common/etc/pesdb/Player.bin (48 tables live in this folder)
It sits inside two layers:
dt200_console_all.cpkis a CRI CPK archive. Any CPK extractor can list and extract its contents.- Every
.bininside is a WESYS container: a 16-byte header followed by a payload. Since game version 5.0.0 the payload is zlib-compressed and then wrapped in an XOR stream layer, which is why the old PES-era tools all stopped working. The key constants rotate between builds, and this post doesn’t cover decryption. It picks up once you have the plaintext.
The header:
| Offset | Type | Meaning |
|---|---|---|
0x00–0x02 | 3 × u8 | Flags, FF 22 83 normally |
0x03–0x07 | char[5] | "WESYS" |
0x08–0x0B | u32 LE | Compressed size (file size − 16) |
0x0C–0x0F | u32 LE | Uncompressed size |
Note that the magic starts at byte 3, not byte 0. Compare it against the start of the file and it will never match.
The lesson worth taking from this step: always build checks that prove themselves. The decoded payload must start with a valid zlib header (78 01 or 78 DA), and the inflated length must exactly match the header. You’re only done when both hold. One out of two doesn’t count.
Traps:
- A few tables (9 in the official build, 7 in a patched one) are just a 16-byte header with both sizes 0. They’re valid empty tables, not corrupt files. Handle them before calling zlib.
- The game locks the cpk while it runs. Close the game before reading it, or copy it first.
- The most expensive trap:
dt870_console_win.cpkcontains a full set of tables with the same names (Player.bin, Team.bin, …). They’re an old format (392 bytes per record) that the current game never opens. Walk them with a 400-byte stride and you get garbage that looks very convincing. So before any walk, assert that the file size divides evenly by 400.
3. Record layout: 400 bytes, flat
Player.bin uses fixed-size records: 400 bytes each, no file header, no index, no padding. Record count = file size ÷ 400.
Names live inside the record in fixed-width slots, so variable-length names don’t break the stride.
| Bytes | Type | Meaning |
|---|---|---|
| 0–3 | u32 | Youth club ID |
| 4–7 | u32 | Loan parent club ID (else 0) |
| 8–15 | u64 | Player ID (PID), table sorted by it |
| 16–19 | u32 | The card’s club (historical) |
| 20–30 | Pricing-type data, unused offline * | |
| 31–87 | Bit-packed attributes (section 5) | |
| 88–392 | Names: 5 × 61-byte UTF-8 slots | |
| 393–399 | Always zero |
* Exception: bit 223 falls in this range, and it’s a real skill flag. Also, bytes 16–19 are the card’s historical club, not the current squad; for current squads, see PlayerAssignment.bin (section 9).
I got bytes 0–7 wrong at first. I called them a “native PID” and even put that in a forum post. Then I noticed those 8 bytes have only about 1,500 distinct values across 40,000+ records, and no index repeats that much. They’re actually two u32 club IDs. Group players by the first one and you get textbook academies: 108 is Barcelona (Messi, Pedro, Busquets), 116 is Ajax, 193 is Sporting (Ronaldo). The second one is non-zero exactly when the loan-expiry date is non-zero: 1,874 out of 1,874, no exceptions.
Lesson: a field’s distribution of values often tells you more than any single value does. “Only 1,500 distinct values” is evidence in itself.
4. Player IDs: where they are and how to use them
The ID is stored directly at bytes 8–15, u64 little-endian. There’s no separate index table. The table is sorted by it and the game finds players by binary search, so if you ever write records, keep the file sorted, or lookups will silently fail. Other tables (squads, appearance) point to players by the same ID.
The ID has structure:
- The low 24 bits are the person; the high bits are the card. Every card of the same player shares the low 24 bits: all of Cristiano Ronaldo’s cards give 4522, all of Neymar’s give 40352. Face models are looked up by this person ID too.
- IDs come in two families with an empty gap between them. Below 2²⁸ are regular cards: the base card itself (up to 24 bits) plus a batch of other regular versions with 25–27-bit IDs (about 2,950 in the official build). From 2³² up are special cards: most are 45 bits and up, but some sit at 32–44 bits (221 in the official build).
- A special card’s card type is encoded in its ID:
(ID >> 40) & 0xFFtells Big Time, Show Time, POTW / POTM / POTS, Selection, Legendary, club season, event and so on apart. - They’re the same IDs PESDB uses. My PESDB scrape joined to Player.bin on the full ID for over 40,000 players.
- To group by person, use the low 24 bits, not the name. The database has different players with the same name (there are two different “Kaka”s).
The easiest way to pull a record’s raw 400 bytes is a binary search:
import struct
REC = 400
def find_record(data, pid):
lo, hi = 0, len(data) // REC
while lo < hi:
mid = (lo + hi) // 2
(p,) = struct.unpack_from("<Q", data, mid * REC + 8)
if p < pid:
lo = mid + 1
elif p > pid:
hi = mid
else:
return data[mid * REC:(mid + 1) * REC]
return None
Don’t find records by searching the file for the ID’s bytes. Small IDs are mostly zero bytes and match in hundreds of random places. If you really must search, only accept hits where (offset - 8) % 400 == 0.
5. Bit-packed fields: read them right first
The attribute area is bit-packed. Fields are not byte-aligned, and several cross byte boundaries. Every offset below is a bit offset from the start of the record, LSB-first: bit N is bit N % 8 of byte N // 8, and bit i of a multi-bit field comes from bit N + i.
def read_bits(rec, bit, width):
v = 0
for i in range(width):
p = bit + i
v |= ((rec[p >> 3] >> (p & 7)) & 1) << i
return v
The 26 abilities are 6 bits each, displayed = stored + 40 (but read section 6 on the scale first):
| Ability | Bit |
|---|---|
| Speed | 434 |
| Acceleration | 486 |
| Finishing | 530 |
| Dribbling | 492 |
| Low Pass | 524 |
| Lofted Pass | 448 |
| Defensive Awareness | 390 |
| Physical Contact | 518 |
| Offensive Awareness | 498 |
| Ball Control | 396 |
| Tight Possession | 550 |
| Heading | 402 |
| Set Piece Taking | 368 |
| Curl | 428 |
| Tackling | 454 |
| Aggression | 512 |
| Defensive Engagement | 544 |
| Kicking Power | 384 |
| Jumping | 408 |
| Balance | 504 |
| Stamina | 480 |
| GK Awareness | 472 |
| GK Catching | 416 |
| GK Reflexes | 460 |
| GK Parrying | 466 |
| GK Reach | 422 |
Other fields:
| Field | Bit | Width | Conversion |
|---|---|---|---|
| Height | 248 | 8 | value + 100 = cm |
| Weight | 280 | 7 | value + 30 = kg |
| Age | 536 | 6 | value + 10 |
| Registered position | 556 | 4 | codes below |
| Nationality | 329 | 9 | country ID ÷ 2 |
| Second nationality | 320 | 9 | same, 0 = none |
| Preferred foot | 654 | 1 | 0 right / 1 left |
Position codes: 0 GK · 1 CB · 2 LB · 3 RB · 4 DMF · 5 CMF · 6 LMF · 7 RMF · 8 AMF · 9 LWF · 10 RWF · 11 SS · 12 CF.
Weight really is 7 bits: read only 6 and every player over 93 kg wraps around to a tiny number.
There are also 67 skill flags (two of them new in September), two playing-style fields (attacking and defending), 12 position aptitudes, form and injury resistance. The full bit map is in my forum posts, so I won’t repeat it here. I’ll just flag the easiest one to get wrong: position aptitude looks like one block at bits 576–599, but it isn’t. LB is at 318 and CMF at 510, both outside the block.
Lesson: find an invariant for your reader. Mine: “a player’s registered position always reads 2 (A grade) in its own aptitude slot.” If even one record breaks it, your offsets or your bit order are wrong.
6. The easiest trap to fall into: the display scale
This one cost me the most time.
“stored + 40” only holds for special cards (ID ≥ 2³²). For regular cards (ID < 2²⁸), PESDB shows:
max(40, round((stored + 40) × 24 / 25))
With that single rule, only 1 of 1.1 million ability values across the 42,369 players I scraped fails to match. Without it, about two-thirds of players come out 4–5 points too high across the board. I later re-checked it on the current official and patched databases, including the 32–44-bit special cards the original database didn’t have, and it still holds.
The funny part: my forum post said “abilities correlate with PESDB at r = 0.95–0.997, and the gap is because PESDB and the game files are different snapshots.” Sounds reasonable. It was wrong. Most of the gap was this scale. Split the players into the two ID families and the gap vanishes on the spot.
To be precise about what I’ve confirmed: this rule agrees with PESDB and with the overall formula. Which of the two scales the game itself draws on a regular card, I haven’t confirmed yet.
7. The overall rating isn’t in the file
Player.bin has no overall field. The game computes it on the fly from the abilities, the registered position and one input I never expected: height.
overall = max(40, round( Σ weightᵢ × abilityᵢ + h × height + c ))
The weights, h and c differ per position. Fitting per position against PESDB data, on players held out from the fit: 94.2% exact, 100% within ±1. Every remaining miss is off by exactly 1, which is rounding.
The height coefficients alone are convincing: goalkeepers +0.185 per cm, centre-backs +0.140, centre-forwards +0.099… wide midfielders almost zero. That’s exactly the order a football fan would guess, which is why I believe it’s real and not a statistical accident.
8. The core skill: proving an offset is right
The question was: “Does the differential approach work for common values, or are rare or extreme values better anchors?”
My answer: common values work fine, even better, as long as you score over the whole corpus instead of eyeballing a few players. Rare and extreme values have their own uses. Here’s what actually worked for me.
a) Get ground truth first. Mine was a full PESDB scrape (42,369 players; how I scraped it is in this post), joined to Player.bin on the full ID. Without ground truth, none of what follows is possible.
b) Scan, don’t guess. For a 1-bit flag like a skill, test every bit position in the record against the known column and look for 100% purity: zero false positives, zero false negatives. For a number, try every start bit and width and correlate. With 40,000 players the right answer usually wins by a mile, and the wrong candidates simply don’t survive. That’s how I pinned down the original 65 skill bits, one by one. The two that appeared in September weren’t on PESDB yet, so I found them by comparing two database builds (the approach in section 3).
c) Rare values fool you in both directions. With only a handful of players, random bits correlate beautifully. Three special-card-only skills, held by just 1, 9 and 22 cards in the whole database, once produced a fake correlation for me. Another time I threw them out as noise when they were real.
d) To label fields, find a record whose values are all different. When several fields hold similar values, one player can “confirm” the wrong labels. When labelling the body-shape fields, my first reference player had so many repeated values that 96 different label permutations all matched perfectly. Switching to a player with 10 of 11 values distinct collapsed 96 to exactly 1.
To pin down a set of labels, don’t look for the most records. Look for the single most informative one.
e) Extreme values are best for write tests. The strongest proof is to write a value into the file and look at it in game. I set every ability of a whole squad to the 6-bit maximum (stored 63, i.e. 103, above the normal cap of 99), and the in-game screen showed 99 across the board: the game really reads the file, and only the display is capped. Pushing a body-shape value to its limit visibly changed the 3D model. That proves what the game actually does, not just that you agree with some website.
f) Hold confounders fixed. Everything correlates with how good a player is. I once had a candidate “market value” field. Holding the overall constant and plotting it against age, it went up with age, the opposite of how market value behaves, and the hypothesis died right there.
g) Before blaming “data drift”, check whether the leftover error has a pattern. Section 6 is the example: I mistook a structured error for random drift.
h) Don’t assume fields are contiguous. Position aptitude is the counter-example. And a field that’s one bit off can look 99% right; my nationality field did exactly that (see section 10).
9. The other tables a full data site needs
All in the same pesdb folder:
- Player.bin itself: names (5 slots: full name, Chinese name, shirt name, romanised name, shirt-name copy), position, skills, playing styles, nationality IDs.
- PlayerAssignment.bin: squads. 24 bytes per row: player ID, team ID, shirt number (stored 0-based), order within the squad, and a 6-bit role mask (captain, penalty taker, four set-piece takers).
- Team.bin: 1,600 bytes per team: team ID, manager ID, team names.
- Coach.bin: 176 bytes per manager: ID, nationality (same table as players), Japanese / romanised / Chinese names.
- Tactics.bin + TacticsFormation.bin: each team’s formation, 11 positions with coordinates on a fixed grid.
- PlayerAppearance.bin: 64 bytes per player, keyed by player ID: body shape, face, hair, etc.
- PlayerVariationDetail.bin: special-card ID → the pack it came from (e.g. “Big Time Portugal 23 Jun ‘26”). Every special card in the database has one; the official build has 425 different pack names. The date in the name isn’t always a release date: Big Time names carry the date of the match the card commemorates, and there’s even a “Big Time 1 Jul ‘90”.
- Playstyle.bin / PlayerSkill.bin: ID → name text tables, in Japanese.
Details worth knowing:
- The team ID in PlayerAssignment is u32, not u16. Read it as u16 and most of the file looks fine, until 24 African national teams (IDs ≥ 81,952) quietly truncate into garbage.
- The same player can legitimately appear in several teams (club + national team).
- Managers carry no tactical data in this generation:
CoachTactics.binandCoachTacticsFormation.binare both empty. Tactics belong to the team, not the manager, unlike the PES era. - I’ve confirmed the 11 body-shape values in the appearance table in both directions (read and write) in game. Most of the other fields come from an older community map that I haven’t verified field by field.
10. Mistakes I’ve corrected
Things you publish can be wrong too. Flagging mistakes is more useful than quietly fixing them, because someone may already have written code against the old version.
- I said bytes 0–7 are a “native PID”. Actually they’re two u32 club IDs (youth club / loan parent club), see section 3.
- I said nationality is at bit 328, 10 bits wide. Actually it’s bit 329, 9 bits, storing ID ÷ 2. The old reading also pulled in the top bit of the second nationality, which conjured up a batch of “odd nationality IDs” that were really players with a second nationality.
- I said age is 1 bit wide. Actually it’s 6 bits, value + 10 (a typo).
- I said a stored 63 displays as 103 in game. Actually the file holds 103, but the game screen shows 99.
- I said the ability gap comes from different data snapshots. Actually most of it is the scale rule in section 6.
- I said special cards have no real faces, so switching to a special card gives a default face. Disproved in game: faces are looked up by the low-24-bit person ID, so special cards get real faces too.
On 26 September this post itself got a few fixes (things I got wrong when publishing it):
- I said the official database has “a few thousand” special cards. Actually it’s about 700. The rest of what I’d counted were regular-card versions with 25–27-bit IDs; none of them has a pack name, and they aren’t special cards.
- I said special-card IDs are 45 bits and up. Actually they start at 2³²; 221 special cards in the official build have 32–44-bit IDs.
- I said there are 7 empty tables. Actually the official build has 9 and a patched one has 7.
- I said there are 65 skill bits, all checked one by one against PESDB. Actually there are now 67; the two new ones from September (Snap Strike, Power Tackle) were found by comparing builds and haven’t been checked against PESDB yet.
The last one is the one to remember. I derived that conclusion from the data, the reasoning looked airtight, and one look in game showed it was wrong. When analysis and the game disagree, the game wins.
11. Try it yourself: a validation checklist
Once your reader is written, don’t run it on the whole database straight away. First pick 20 players who are as different as possible: a very tall goalkeeper, a very short winger, someone with a second nationality, someone on loan, a special card together with its base card… Compare every field against PESDB. Once those 20 are perfect, run the full comparison.
Also make these invariants hold on every record:
- file size % 400 == 0, otherwise fail hard
- the IDs at bytes 8–15 are strictly ascending through the whole file
- ID 0 is an empty placeholder; skip it
- name slots decode as UTF-8
- the registered position’s aptitude slot reads 2
Closing
People have asked whether I can just send a Player.bin sample, or open-source my tool. A sample, no: Player.bin and any data extracted from it belong to Konami, and I don’t redistribute them, not even a small piece. Luckily eFootball is free on Steam, so anyone who wants to follow along gets their own copy just by installing the game.
The tool stays private for now too. I don’t think you need it: this post plus my forum posts are enough to write a reader, and a reader you wrote yourself is one you actually understand and can maintain.
If your results don’t match mine, please tell me. A mismatch is far more interesting than silence.