从零读懂 eFootball 的 Player.bin:一份逆向学习笔记
最近有人问我 Player.bin 该怎么解,问题一口气列了十几个。我把回答整理成一份学习笔记:文件在哪、记录怎么切、ID 怎么用、位级字段怎么读,以及最重要的一件事——怎么证明一个偏移真的是对的。
六月我写过一篇《Player.bin 逆向手记》,讲的是四次翻车。那之后游戏换了一次数据格式,旧的记录布局整个作废,我几乎是从头又解了一遍。
最近有人来问:想用 Player.bin 做一个 eFootball 数据站,该从哪下手?问题问得很好,一口气列了十几个:文件在哪、记录定长还是变长、ID 存在哪、有没有确认过的偏移、差分法靠不靠谱……我索性把回答整理成这篇学习笔记。
它不是一张”照抄就能用”的偏移表。偏移表会过期,Konami 每次更新都可能动它。我更想讲清楚的是怎么找、怎么验,这部分不会过期。
9 月 26 日更新:发出去第二天,我拿官方库和补丁库把文中每个数字重新核对了一遍,改了几处(特殊卡的数量、ID 怎么分族、卡包名、空表数量、两个新技能的验证方式),都记在第十节。
先说边界。 这是我在自己的正版安装上做的独立格式分析,和 Konami 没有任何关系。文中不含任何游戏文件、数据导出或密钥,只讲格式和方法。只针对离线单机:联网模式由服务器说了算,这里的东西碰不到,我也没兴趣碰。修改数据库大概率违反用户协议,动手前先备份,后果自负。
一、先搞清楚它是什么,不是什么
动手之前,最该先回答的问题是:这份数据到底能回答什么问题?
Player.bin 是游戏自带的离线数据库,存的是授权俱乐部和国家队的球员,外加一部分特殊卡,离线比赛(比如 Trial Match)读的就是它。
它不是账号数据。梦幻球队那一侧的东西全在 Konami 的服务器上,本地一份都没有:
| 本地文件(离线生效) | 服务器(账号数据) |
|---|---|
| 能力值、技能、位置适性、状态 | 拥有哪些卡、卡的等级、训练点 |
| 阵容、背号、队长和主罚 | 每张卡装了什么增能 |
| 阵型、教练的名字和国籍 | GP、金币、自己的阵容和教练 |
本地倒是有一张 PlayerBooster.bin,但它只是增能的定义表,不记录谁装了哪个。
它也不是完整的卡库。我看过的官方版本里,Player.bin 大约 23,500 条记录,特殊卡只有 700 张左右(我对比过的一个社区补丁库有约 13,600 张)。Konami 并没有把梦幻球队里所有的卡都放进离线库,所以想做卡牌目录,PESDB 这类网站仍然是绕不开的数据源。
不过库里有的那些特殊卡,信息是全的:每张都能查到它出自哪个卡包(第九节),卡种(Big Time、Show Time……)直接编码在卡的 ID 里(第四节)。
Player.bin 真正值钱的,是网站上不容易看到的东西:精确的存储值、阵容和背号、青训俱乐部和租借信息、第二国籍、球队阵型、外观数据。
二、文件在哪、怎么打开
我用的是 eFootball 2027,Steam 版(PC),所有结论都是 2026 年 8 到 9 月间在几次周更数据库上做出来的。主机版和手机版我完全没碰过。
<Steam 库>\steamapps\common\eFootball\cpk\dt200_console_all.cpk
└─ common/etc/pesdb/Player.bin (这个目录里一共 48 张表)
它藏在两层包装里:
dt200_console_all.cpk是 CRI 的 CPK 归档,任何 CPK 解包工具都能列出和取出里面的文件。- 里面每一张
.bin都是一个 WESYS 容器:16 字节头,后面是 payload。从游戏 5.0.0 版起,payload 是先 zlib 压缩、再叠一层 XOR 流加密,这也是老的 PES 时代工具全部失效的原因。加密常量随版本轮换,这篇不讲解密细节,只讲拿到明文之后的事。
头部长这样:
| 偏移 | 类型 | 含义 |
|---|---|---|
0x00–0x02 | 3 × u8 | 标志位,正常表是 FF 22 83 |
0x03–0x07 | char[5] | "WESYS" |
0x08–0x0B | u32 LE | 压缩后大小(= 文件大小 − 16) |
0x0C–0x0F | u32 LE | 解压后大小 |
注意 magic 是从第 3 字节开始的,不是第 0 字节,拿文件开头去比永远比不上。
这一步最值得学的是:永远先做能自证的检查。 解出来的 payload 开头必须是合法的 zlib 头(78 01 或 78 DA),解压后的长度必须和头里写的完全一致。两条同时成立才算解对,只满足一条都不算。
几个坑:
- 有几张表(官方版本里是 9 张,补丁版是 7 张)只有一个 16 字节的头,两个大小都是 0。它们是合法的空表,不是坏文件,要在调用 zlib 之前单独处理。
- 游戏运行时会锁住 cpk,读之前先关游戏,或者先拷一份出来。
- 最贵的一个坑:
dt870_console_win.cpk里有一整套同名的表(Player.bin、Team.bin……),但那是旧格式(392 字节一条),当前游戏根本不打开它。拿 400 字节步长去遍历它,会得到一堆看起来非常像真的垃圾数据。所以任何遍历之前先断言:文件大小能被 400 整除。
三、记录结构:400 字节,一刀切
Player.bin 是定长记录:每条 400 字节,没有文件头、没有索引、没有填充。记录数 = 文件大小 ÷ 400。
名字就存在记录内部的定长槽位里,所以名字长短不一也不影响步长。
| 字节 | 类型 | 含义 |
|---|---|---|
| 0–3 | u32 | 青训俱乐部 ID |
| 4–7 | u32 | 租借母队 ID(没租借为 0) |
| 8–15 | u64 | 球员 ID(PID),全表按它升序 |
| 16–19 | u32 | 卡片所属俱乐部(历史归属) |
| 20–30 | 价格类数据,离线用不上 * | |
| 31–87 | 位压缩的属性区(见第五节) | |
| 88–392 | 名字区:5 × 61 字节 UTF-8 | |
| 393–399 | 恒为 0 |
* 例外:bit 223 落在这一段,它是一个真正的技能位。另外,16–19 是卡片的历史归属,不是当前阵容;当前阵容要看 PlayerAssignment.bin(见第九节)。
字节 0–7 我一开始认错了,当成”内部 PID”,还写进了论坛帖。后来发现这 8 个字节在 4 万多条记录里只有大约 1,500 种取值,一个索引不可能重复到这个程度。它其实是两个 u32 俱乐部 ID。按第一个值分组,出来的是教科书式的青训营:108 是巴萨(梅西、佩德罗、布斯克茨),116 是阿贾克斯,193 是葡萄牙体育(C 罗)。第二个值非零,当且仅当租借到期日非零:1,874 对 1,874,零例外。
学习点:看一个字段的取值分布,往往比看某一个取值更能说明它是什么。“只有 1,500 种取值”本身就是证据。
四、球员 ID:存在哪、怎么用
ID 就直接存在字节 8–15,u64 小端序,没有另外的索引表。整张表按它升序排列,游戏靠二分查找找人,所以如果你往里写记录,一定要保持有序,否则查找会静默失败。其他表(阵容、外观)也都用同一个 ID 指向球员。
ID 本身还有结构:
- 低 24 位是”人”,高位是”卡”。 同一个球员的所有卡,低 24 位都相同:C 罗所有的卡都是 4522,内马尔都是 40352。脸模也按这个”人 ID”查。
- ID 分成两族,中间有一段空档:2²⁸ 以下是普通卡,包括基础卡本身(24 位以内),以及一批 25–27 位的其他普通版本(官方库里约 2,950 张);2³² 以上是特殊卡,大部分在 45 位以上,但也有一些落在 32–44 位(官方库里 221 张)。
- 特殊卡的卡种就编码在 ID 里:
(ID >> 40) & 0xFF能分出 Big Time、Show Time、周最佳 / 月最佳 / 赛季最佳、Selection、传奇、俱乐部赛季卡、活动卡等。 - 这和 PESDB 上用的 ID 是同一套。我的 PESDB 抓取结果按完整 ID 直接连表,匹配上了四万多人。
- 按人去重请用低 24 位,别用名字:库里有重名的不同球员(比如有两个不同的”卡卡”)。
取一条记录的原始 400 字节,最简单的是二分查找:
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
别用”在文件里搜 ID 字节”的方式找记录。 小 ID 大部分字节是 0,会在几百个随机位置命中。实在要搜,只接受 (offset - 8) % 400 == 0 的结果。
五、位级字段:先把”怎么读”读对
属性区是位压缩的,字段不按字节对齐,好几个还跨字节。下面所有偏移都是从记录开头算起的 bit 偏移,位序 LSB-first:第 N 位在第 N // 8 字节的第 N % 8 位,多位字段的第 i 位来自第 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
26 项能力,每项 6 位,显示值 = 存储值 + 40(但请先看第六节的刻度问题):
| 能力 | bit |
|---|---|
| 速度 Speed | 434 |
| 加速 Acceleration | 486 |
| 射门 Finishing | 530 |
| 盘带 Dribbling | 492 |
| 短传 Low Pass | 524 |
| 高传 Lofted Pass | 448 |
| 防守意识 Def. Awareness | 390 |
| 身体对抗 Physical Contact | 518 |
| 进攻意识 Off. Awareness | 498 |
| 控球 Ball Control | 396 |
| 紧密控球 Tight Possession | 550 |
| 头球 Heading | 402 |
| 定位球 Set Piece Taking | 368 |
| 弧线 Curl | 428 |
| 抢断 Tackling | 454 |
| 侵略性 Aggression | 512 |
| 防守参与 Def. Engagement | 544 |
| 踢球力量 Kicking Power | 384 |
| 弹跳 Jumping | 408 |
| 平衡 Balance | 504 |
| 体力 Stamina | 480 |
| GK 扑救意识 | 472 |
| GK 接球 | 416 |
| GK 反应 | 460 |
| GK 倒地扑救 | 466 |
| GK 控制范围 | 422 |
其他字段:
| 字段 | bit | 宽度 | 换算 |
|---|---|---|---|
| 身高 | 248 | 8 | 值 + 100 = cm |
| 体重 | 280 | 7 | 值 + 30 = kg |
| 年龄 | 536 | 6 | 值 + 10 |
| 注册位置 | 556 | 4 | 编码见下 |
| 国籍 | 329 | 9 | 存 国家 ID ÷ 2 |
| 第二国籍 | 320 | 9 | 同上,0 = 没有 |
| 惯用脚 | 654 | 1 | 0 右 / 1 左 |
位置编码: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。
体重一定要读 7 位:只读 6 位的话,93 kg 以上的球员全部回绕成一个很小的数。
另外还有 67 个技能位(其中 2 个是 9 月新出的)、两个球风字段(进攻 / 防守)、12 个位置适性、状态和伤病抗性,完整位图在我的论坛帖里,这里不重复。只提一个最容易错的:位置适性看起来是 576–599 一整块,其实不是。LB 在 318,CMF 在 510,都在块外面。
学习点:给自己的读取器找一条不变量。我用的是:“球员自己的注册位置,在对应的适性槽位里永远读到 2(A 级)“。只要有一条记录不满足,就说明偏移或位序错了。
六、最容易掉的坑:显示刻度
这是我栽得最久的一个。
“存储值 + 40”只对特殊卡(ID ≥ 2³²)成立。对于普通卡(ID < 2²⁸),PESDB 显示的是:
max(40, round((存储值 + 40) × 24 / 25))
加上这一条规则,我抓的 42,369 名球员、110 万个能力格子里,只有 1 格对不上。不加的话,大约三分之二的球员会整体高出 4–5 分。后来我又拿当前的官方库和补丁库重新验了一遍,包括原来那份库里没有的 32–44 位特殊卡,规则依然成立。
有意思的是,我之前在论坛上写的是”能力值和 PESDB 的相关系数 0.95–0.997,差距来自 PESDB 和游戏文件不是同一个快照”。听起来很合理,但错了:差距的大头就是这个刻度。按 ID 分成两族分别看,差距当场消失。
要说清楚我确认了什么:这条规则和 PESDB 以及总评公式都对得上。至于游戏自己在普通卡的卡面上画的是哪一种刻度,我还没有确认。
七、总评不在文件里
Player.bin 里没有总评字段。它是游戏实时算出来的,输入是能力值、注册位置,以及一个我完全没想到的量:身高。
总评 = max(40, round( Σ 权重ᵢ × 能力ᵢ + h × 身高 + c ))
权重、h、c 都按位置不同。我用 PESDB 的数据按位置拟合,在没参与拟合的球员上:94.2% 完全命中,100% 误差在 ±1 以内。剩下的误差全是 1,是取整造成的。
身高的系数排序本身就很说明问题:门将每厘米 +0.185,中卫 +0.140,中锋 +0.099……边前卫几乎为 0。这正是懂球的人会猜的顺序,所以我相信它是真的,不是统计巧合。
八、核心:怎么证明一个偏移是对的
有人问:“差分法对常见取值可靠吗?还是罕见值、极端值更适合当锚点?”
我的答案是:**常见值完全可以,甚至更好,前提是你在整个语料库上打分,而不是盯着几个球员看。**罕见值和极端值有它们自己的用处。下面是我实际用下来有效的做法。
a)先有基准数据。 我的基准是对 PESDB 的一次全量抓取(42,369 人,抓取过程见这篇),用完整 ID 连到 Player.bin 上。没有基准数据,后面所有方法都无从谈起。
b)扫,不要猜。 对于技能这种 1 位的标志,拿记录里每一个 bit 位置去和已知的那一列比,找纯度 100% 的:零假阳性、零假阴性。对于数值,试每一个起始位和宽度,算相关。四万人的样本下,正确答案通常遥遥领先,错误的候选根本活不下来。最初的 65 个技能位我就是这么逐个定下来的;9 月新出的两个 PESDB 还没收录,是靠对比前后两版数据库找到的(见第三节的做法)。
c)罕见值会从两个方向骗你。 样本只有几个人的时候,随机的 bit 也能相关得很漂亮。有三个特殊卡专属技能,全库分别只有 1、9、22 张卡持有,它们曾经给我制造过一个假相关;而另一次,我又把它们当成噪声扔掉了,其实它们是真的。
d)给字段贴标签,要找一条”取值各不相同”的记录。 几个字段取值差不多的时候,一个球员可以”证实”错误的标签。给体型字段贴标签时,我第一个参照球员身上重复值太多,结果有 96 种标签排列全部完美吻合。换成一个 11 项里 10 项互不相同的球员,96 种一下子塌缩成 1 种。
要钉死一组标签,找的不是”验得多”,而是”信息量最大”的那一条记录。
e)极端值最适合做写入测试。 最强的验证,是把值写进文件,然后进游戏看。我把一整队球员的能力全部写到 6 位的上限(存储值 63,也就是 103,超过常规上限 99),游戏界面里全部显示 99:说明游戏确实读了文件,只是界面封顶。把体型数值推到极限,3D 模型肉眼可见地变了。这证明的是游戏实际怎么做,而不只是”和某个网站对得上”。
f)固定住混杂因素。 什么都和球员好坏相关。我曾经有一个”身价”候选字段,固定总评之后看它和年龄的关系:年龄越大它越高,和身价的规律正好相反,这个假设当场作废。
g)怪”数据漂移”之前,先看剩余误差有没有规律。 第六节的刻度问题就是例子:我把一个有结构的误差当成了随机漂移。
h)别假设字段是连续的。 位置适性就是反例。而且一个字段错一位,看起来可能 99% 都对:我的国籍字段就这样错过一次(见第十节)。
九、做一个完整的数据站,还需要哪些表
都在同一个 pesdb 目录里:
- Player.bin 本身:名字(5 个槽位:全名、中文名、球衣名、罗马音名、球衣名副本)、位置、技能、球风、国籍 ID。
- PlayerAssignment.bin:阵容。24 字节一行:球员 ID、球队 ID、背号(存的是从 0 开始)、队内顺序、6 位角色掩码(队长、点球、四个定位球)。
- Team.bin:1,600 字节一队:球队 ID、教练 ID、队名。
- Coach.bin:176 字节一个教练:ID、国籍(和球员共用一张国籍表)、日文 / 罗马音 / 中文名。
- Tactics.bin + TacticsFormation.bin:每支队的阵型,11 个位置,坐标落在固定网格上。
- PlayerAppearance.bin:64 字节一人,按球员 ID 索引:体型、脸、发型等。
- PlayerVariationDetail.bin:特殊卡 ID → 卡包名(比如 “Big Time Portugal 23 Jun ‘26”)。库里每张特殊卡都有,官方版本里一共 425 个不同的卡包名。注意名字里的日期不一定是发布日期:Big Time 带的是它纪念的那场比赛的日期,库里就有一张 “Big Time 1 Jul ‘90”。
- Playstyle.bin / PlayerSkill.bin:ID → 日文名称的文本表。
几个值得注意的细节:
- PlayerAssignment 的球队 ID 是 u32,不是 u16。 按 u16 读,前面大部分都对,然后 24 支非洲国家队(ID ≥ 81,952)会悄悄被截断成垃圾。
- 同一个球员可以合法地出现在多支队里(俱乐部 + 国家队)。
- 这一代的教练不带任何战术数据:
CoachTactics.bin和CoachTacticsFormation.bin都是空表。战术属于球队,不属于教练,这和 PES 时代不一样。 - 外观表里体型的 11 项我在游戏里读写双向确认过;其余大部分字段来自一张较早的社区图,我没有逐项验证。
十、我纠正过的错误
公开写出来的东西也会错。把错误标出来,比悄悄改掉更有用:别人可能已经照着旧版本写了代码。
- 原来说字节 0–7 是”内部 PID”。实际上是两个 u32 俱乐部 ID(青训 / 租借母队),见第三节。
- 原来说国籍在 bit 328,宽 10 位。实际上是 bit 329,宽 9 位,存 ID ÷ 2。旧读法把第二国籍的最高位也读了进来,凭空多出一批”奇数国籍 ID”,其实是有第二国籍的球员。
- 原来说年龄宽 1 位。实际上是 6 位,值 + 10(笔误)。
- 原来说存储值 63 在游戏里显示 103。实际上文件里存得下 103,但游戏界面显示 99。
- 原来说能力值的差距来自数据快照不同。实际上大头是第六节的刻度规则。
- 原来说特殊卡没有真脸,换成特殊卡就变默认脸。被游戏实测推翻:脸按低 24 位的”人 ID”查,特殊卡一样有真脸。
9 月 26 日,这篇文章自己也改了几处(发布时写错的):
- 原来说官方库里特殊卡有”几千张”。实际上是 700 张左右。多出来的那部分是 25–27 位 ID 的普通卡版本,它们一张卡包名都没有,不是特殊卡。
- 原来说特殊卡的 ID 在 45 位以上。实际上是从 2³² 起,官方库里有 221 张落在 32–44 位。
- 原来说空表是 7 张。实际上官方版本是 9 张,补丁版是 7 张。
- 原来说有 65 个技能位,并且都和 PESDB 逐个对过。实际上现在是 67 个,9 月新出的两个(Snap Strike、Power Tackle)是靠版本对比找到的,还没和 PESDB 对过。
最后一条尤其值得记住:那是我从数据里推出来的结论,推理链看着很完整,进游戏一看就是错的。分析和实测对不上的时候,以实测为准。
十一、自己动手:一份验证清单
读取器写好之后,不要直接跑全库。先挑 20 个尽量不一样的球员:很高的门将、很矮的边锋、有第二国籍的、正在租借的、一张特殊卡和它对应的基础卡……逐个字段和 PESDB 比。这 20 个全对了,再跑全库对比。
同时让这几条不变量在每条记录上都成立:
- 文件大小 % 400 == 0,否则直接报错
- 字节 8–15 的 ID 在整个文件里严格递增
- ID 为 0 的是空占位,跳过
- 名字槽位能按 UTF-8 解码
- 注册位置的适性槽位读到 2
写在最后
有人问我能不能直接给一份 Player.bin 样本,或者把工具开源。样本不行:Player.bin 和从它导出的任何数据都是 Konami 的,我不转发,哪怕是一小段。好在 eFootball 在 Steam 上免费,谁想跟着做,装上游戏就有自己的那一份。
工具我暂时也不公开。但我觉得也不需要:这篇加上论坛帖,已经足够写出一个读取器,而自己写出来的读取器,你才真的看得懂、改得动。
如果你照着做出来的结果和我对不上,欢迎告诉我。对不上的结果,比没有回音有意思得多。