eFootball Player.bin 逆向手记:四次翻车教会我的事
从 FF 模式丢技能、编辑器搜不到球员、到 CSV 验证假警报——记录 eFootball 球员数据逆向中踩过的四个坑,以及它们背后的教训。
eFootball Player.bin 逆向手记:四次翻车教会我的事
我一直在逆向 eFootball 的 Player.bin 文件。这个 14.7MB 的二进制文件存储了所有球员的属性、技能、风格。
过去几个月,我和 Hermes Agent 一起破解了 26 项能力、62 个技能 bit、6 个 AI 比赛风格、Playing Style、Form、Injury Resistance。还写了三个 GUI 编辑器(完整版、Lite 版、能力专用版),都打包成了 Windows exe。
听起来很顺利。但其实中间踩了四个让人抓狂的坑。每一个都是”看起来没问题,实际上完全错了”的类型。
翻车一:FF 模式丢技能
现象:用户反馈,一键 FF(全技能满能力)之后,进游戏发现 Sliding Tackle、Interception、Phenomenal Pass、Man Marking 四个技能没被激活。
排查:
FF 模式的逻辑是:
- 把 84 字节的属性区全部填
0xFF - 恢复需要保护的字段(身高、体重、年龄、惯用脚)
- 调用
_fill_skills()写满技能 bit - 恢复球员风格(Playing Style)
问题出在第 4 步。球员风格是 6-bit,位于 byte78[2:7]。但这几个技能刚好也在 byte78 里:
byte78 bit2 = Sliding Tackle
byte78 bit3 = Interception
byte78 bit4 = Phenomenal Pass
byte78 bit5 = Man Marking
_fill_skills() 内部用 |= 0xAC 设置了这些 bit。但紧接着 FF 模式调用 wb(STYLE_BIT, 6, style_val) 恢复风格——把同一字节的技能 bit 全部覆盖成了原始的风格值。
如果原始风格码是 0(比如 CF 的 Goal Poacher),byte78[2:7] 全部清零,四个技能全丢。
教训:
代码注释写了”风格还原后必须补回”,但补回操作写在 _fill_skills() 内部,风格还原在外面。注释和代码不在同一层,必出错。
修复很简单——在 maximize() 和 godmode() 的风格还原之后,加一行:
data[base + 78] |= 0xBC # 补回 byte78[2:5,7] 技能位
翻车二:能力编辑器搜不到球员
现象:eFootball_Ability_Editor.exe 的搜索功能完全失效。输入球员名称或十进制 ID,永远返回”Not Found”。
排查:
编辑器遍历记录的方式是:
for i in range(total):
off = i * 84 # 固定步进 84 字节
pid = struct.unpack_from("<Q", data, off + 8)[0]
这里假设所有记录都是固定 84 字节。但 Player.bin 的实际结构是:
[84 字节属性] + [可变长度名字区 (103~150 字节)]
名字区包含日文名 + 英文名,null 分隔,末尾 padding。
第一条记录碰巧能被读到(off=0),但第二条开始全在名字区中间——读到的”PID”只是名字字符串的字节碎片。搜索当然找不到。
然后你以为修好了,其实没有。
翻车三:修完还是搜不到
现象:把 i * 84 改成正确的变长遍历后,名字索引工作正常了(找到 40,102 条),但 十进制 ID 搜索仍然搜不到 Haaland。
排查:
新写的 iter_records() 通过跳过 null padding 来定位下一条记录:
while data[name_end] == 0:
name_end += 1
off = name_end # 下一条记录的起始
这逻辑在大多数情况下是对的。但有一条记录的名字区后面有大量连续 null padding(约 40 字节),padding 结束后正好是另一个名字区的中间(“RAUL” 文本)。iter_records 以为找到了下一条记录,实际上 偏移了 8 字节。
8 字节的偏移意味着:之后所有记录的 PID 读取都是错的,Haaland 再也找不到。
最终修复:
ID 搜索不再依赖 iter_records,改用 data.find(struct.pack("<Q", pid)) 直接搜索 PID 字节流。这是 O(n) 但 100% 准确。
教训:
“聪明的解析器”不如”笨但正确的搜索”。 变长格式的边界检测本质上是启发式的,总有意想不到的 corner case。字节搜索虽然慢,但它保证能找到——对于 14MB 二进制文件,Python 的 find() 也只需要几毫秒。
翻车四:CSV 验证的假警报
现象:用 CSV 数据验证 62 个技能 bit 映射时,多个技能的 F1 分数惨不忍睹——Chip Shot Control 只有 45.7%!
排查:
CSV 有 15,777 条球员数据,包含每个人拥有的技能列表。Binary 有约 112,000 条记录。我用 Short ID 匹配了两边的数据,然后对比每个 skill bit。
但有一个问题:同一个 Short ID 可能在 binary 中出现多次——同一球员的不同卡版本(Standard、Featured、Epic)。CSV 只有一套技能列表,但 binary 有多个版本。我把所有 binary 副本都拿去和同一套 CSV 技能对比,match 不上的自然被当成 False Negative。
去重之后,所有 62 个技能的 F1 都在 97.8% 以上。Chip Shot Control 从 45.7% 飙到 99.6%。
教训:
验证方法的 bug 比被测代码的 bug 更难发现。 你盯着 F1=45.7% 开始怀疑 bit 位置是不是搞错了,重新翻 PESDB、重新交叉验证、重新数 bit——但代码根本没毛病,是验证脚本在重复计数。
在开始修代码之前,先确认你的测试是不是在测你想测的东西。
Form & Injury Resistance:一个意外的收获
在排查过程中,我顺便用 CSV 的 Form(Standard/Unwavering/Inconsistent)和 Injury Resistance(Low/Medium/High)数据去匹配 binary,居然找到了编码位置:
- byte31[6] 是显式编码开关(0=默认/引擎决定)
- byte72[0:2] 编码 Form(01=Standard, 10=Unwavering, 00=Inconsistent)
- byte70[7] + byte71[0] 编码 Injury(00=Low, 10=Medium, 01=High)
4,626 名球员验证,99.4% 准确率。
这两个字段此前一直是”未知元数据”——旧版 Player.bin 里对应的字节全是 FF。eFootball 2026 版本才开始使用这些位。
总结
| # | 问题 | 根因 | 教训 |
|---|---|---|---|
| 1 | FF 丢 4 技能 | 风格还原覆盖共享字节 | 注释和代码不在同一层必出错 |
| 2 | 编辑器搜不到 | 假设固定记录长度 | 先确认文件格式再写解析器 |
| 3 | 修完还搜不到 | 边界检测被 null 误导 | 笨搜索比聪明解析器可靠 |
| 4 | CSV 验证假警报 | 重复 PID 未去重 | 先确认测试方法再怀疑代码 |
四个问题,本质上全是假设错误。假设记录是定长的、假设边界检测是对的、假设数据没有重复。
二进制逆向就是这样——你永远在用一个不完整的理解去操作一个你看不到全貌的格式。每一步都可能是对的,直到它突然不是。
所有修复已提交到 efootball-player-tool。
by Jiahao Ren | github.com/Giggitycountless | jiahao.uk