eFootball Player.bin 逆向手记:四次翻车教会我的事

从 FF 模式丢技能、编辑器搜不到球员、到 CSV 验证假警报——记录 eFootball 球员数据逆向中踩过的四个坑,以及它们背后的教训。

· 9 min read

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 模式的逻辑是:

  1. 把 84 字节的属性区全部填 0xFF
  2. 恢复需要保护的字段(身高、体重、年龄、惯用脚)
  3. 调用 _fill_skills() 写满技能 bit
  4. 恢复球员风格(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 版本才开始使用这些位。


总结

#问题根因教训
1FF 丢 4 技能风格还原覆盖共享字节注释和代码不在同一层必出错
2编辑器搜不到假设固定记录长度先确认文件格式再写解析器
3修完还搜不到边界检测被 null 误导笨搜索比聪明解析器可靠
4CSV 验证假警报重复 PID 未去重先确认测试方法再怀疑代码

四个问题,本质上全是假设错误。假设记录是定长的、假设边界检测是对的、假设数据没有重复。

二进制逆向就是这样——你永远在用一个不完整的理解去操作一个你看不到全貌的格式。每一步都可能是对的,直到它突然不是。

所有修复已提交到 efootball-player-tool


by Jiahao Ren | github.com/Giggitycountless | jiahao.uk