Gummygamer/Transformers-Forged-To-Fight-Offline-Version
GitHub: Gummygamer/Transformers-Forged-To-Fight-Offline-Version
该项目通过补丁、模拟服务器和 runtime hook,使已停服的「Transformers: Forged to Fight」手游能够离线启动并游玩部分内容,同时为后续完整重建服务端提供了逆向工程笔记与工具链。
Stars: 1 | Forks: 0
# Transformers: Forged to Fight,离线复活交接
本软件包包含了 Transformers: Forged to Fight 可运行的本地离线复活版本,
以及用于构建它的工具、补丁和逆向工程笔记。这是一次交接,
旨在继续完成一项更为庞大的任务:从零开始编写替代用的服务器端内容。
此处不包含或重建原始的在线服务数据。
在你动手修改任何东西之前,请完整阅读此文件。特别是“避坑指南”部分,
将为你节省大量时间。
## 目前实际可用的功能
游戏可以完全离线启动,并在没有 live Kabam 服务的情况下
进入真实的交互式主屏幕。基地、阵容、战斗模式选择、水晶屏幕、
弹窗和提示均可正常导航且不会崩溃。登录和首次体验门槛在本地完成。
脚本化的擎天柱对阵红蜘蛛开场战斗可以通过其轻攻击
教程进行游玩,并具有实时 3D 角色和战斗控制。
本地服务器还提供了一个完整且编写好的 STORY 1.1.1 循环:选择一个小队,
进入原始棋盘,在可到达的节点之间移动,触发最终 boss,
在原生战前屏幕上选择一个机器人,与 Sharkticon 战斗,判定胜利,
并返回棋盘。精心编写的 `Light`、`Medium`、`Heavy` 和 `Ranged` 攻击行
以及战斗护甲调整使得命中的攻击能够减少生命值。阵容、英雄详情、
团队选择和战斗模型 ID 均生成自相同的原始数据源,
因此战斗使用的是匹配的 3D 网格,而不是通用的占位符。
## 无法运行的功能及其原因
这是一个可玩的保护沙盒,并不是原游戏的完整替代品。
目前仅编写了一个小型 STORY 路径和一个 boss 遭遇战。对战结果判定目前
使用返回棋盘所需的最小成功响应;已完成的进度、
奖励和任务状态不会被持久化。每个机器人特有的能力效果、额外的任务
和敌方阵容、经济系统以及大多数进度系统仍有待编写。
Forged to Fight 是完全由服务器主导的。手机上的应用程序本质上只是
一个带有控件的屏幕。游戏几乎没有任何内容存在于应用程序中。每个任务、每场
战斗、每个敌方阵容、整个阵容的属性和能力、经济系统以及所有的
平衡性设定都存在于 Kabam 的服务器上,并在每次会话时流式传输到设备上。当
服务器在 2020 年初被关闭时,这些内容数据库也随之消失,而且它们
从未在我能接触到的任何地方发布或公开存档过。
客户端可以从操作者提供的应用程序副本中加载美术和音频资源。所
缺失的是用于选择这些资源、组建战斗和任务
以及定义属性和能力的服务器数据。本项目仅提供客户端可解析的
格式的新增、手工编写的数据;它不包含 APK、游戏资源、原始二进制文件
或捕获的游戏视听材料。扩展此项目前请参阅 `COMPLIANCE.md`。
## 离线启动的工作原理
这里有四个动态部分。它们共同让未经修改的游戏认为自己正在与
Kabam 通信。
1. 原生二进制补丁。该游戏基于 Unity IL2CPP 构建,因此逻辑存在于已编译的 ARM
库 `libil2cpp.so` 中,而不是可编辑的脚本文件中。`patches/patch_il2cpp.py` 重写了
该库中的六个函数,以绕过失效的服务器检查:它破解了两条
证书绑定路径,从而接受我们自己的 TLS 证书;强制 manager
注册块运行(即使 live config 为 null);允许通过我们的
本地设备会话成功登录;并屏蔽了子系统的致命错误,否则
这些错误会弹出“登录失败”对话框。它还重新注入了一个依赖项条目(参见
避坑指南部分),以便 runtime hook 能够真正加载。输出文件为 `libil2cpp.patched.so`。
2. 模拟 Sparx 服务器。`Server/fakeserver.py` 充当了 Kabam 后端的替代品。它在
TLS 443 和普通 HTTP 80 端口上监听并响应游戏的 API 调用。预设响应位于
`Server/responses/` 中,每个端点对应一个文件,按方法和路径命名,例如
`GET__account_data.json`。少数端点是在代码中动态响应的,而不是
从文件中读取,因为游戏期望它们回显请求中的值(教程
端点和英雄详情端点)。响应封装格式为
`{"error":null,"result": ...}`。请注意,在 Sparx 错误 payload 内部,该字段拼写为
`err`,而不是 `error`。这个细节至关重要且很容易被忽略。
3. 原生 runtime hook。`tools/nativehook/` 构建了 `libdothook.so`,这是一个小型库,
在启动时加载到游戏中,并记录游戏读取的每一个数据键,
此外还包含了几个针对性的行为引导。正是这个反馈循环使得其他一切成为可能:它
会准确地告诉你游戏正在请求什么,以便你可以合成响应并
验证它。它是在执行前安装的纯字节覆写 inline hook,因为通常用于此目的的
工具(Frida)在模拟器的 ARM 转换层下会崩溃。
4. 设备配置。模拟器必须将 Kabam 的域名发送到 PC,并信任伪造的
证书。`tools/provision_ldplayer.sh` 可以一次性完成此操作:它会推送已打补丁的库
和 hook,通过 hosts 文件将 Kabam 主机名重定向到 PC 的局域网地址,
将伪造的 CA 挂载到系统信任库中,并放宽 SELinux。在每次
模拟器重启后运行它,因为这些挂载在重启后无法保留。
运行时的数据流如下:游戏向 Kabam 域名发起 HTTPS 调用,hosts 文件
将其发送到 PC,模拟服务器使用来自 `Server/responses/` 的响应进行回复,
已打补丁的库接受该证书和响应,而 hook 会记录读取的内容。这个循环
就是这个版本中每个屏幕得以成功启动的方式。
## 本软件包包含的内容
```
README.md this file
COMPLIANCE.md copyright, trademark, and security boundaries for the project
TECHNICAL_NOTES.md the deeper technical reference: patches, recovered data shapes, findings
patches/
patch_il2cpp.py the six native patches plus the dependency re-injection
disasm_fn.py helper: disassemble a function at an offset
find_callers.py helper: find callers of a function
find_str_ref.py helper: find references to a string
Server/
fakeserver.py the fake Sparx server
gamedata.py hand-authored roster, battle balance, mission, and tuning data
test_gamedata.py verifies generated roster, combat, tuning, and mesh mappings
gen_certs.sh regenerate the TLS cert and CA (run this, see below)
run_local.py run the server on unprivileged phone-friendly ports
build_phone_apk.py create an unsigned local-server APK for a stock phone
provision_phone.sh install, launch, and configure USB or Wi-Fi phone use
setup_device.sh device side network and trust setup reference
iterate.sh quick restart and capture loop
responses/ one JSON file per endpoint the game calls
tools/
provision_ldplayer.sh one shot re-provision of the emulator to the working state
setup_arm64.sh toolchain setup notes
decompile_targets.py drive the Ghidra headless decompiler at chosen offsets
find_xrefs.py cross reference search over the binary
apply_labels.py apply IL2CPP symbol labels
light_analyze.py lightweight static analysis helpers
frida_attach.py Frida helpers (kept for reference, see the libnb note)
frida_run.py
hook_dot.js
nativehook/
hook.c source of libdothook.so, the runtime hook
libdothook.so prebuilt hook, arm64
deploy.sh build and deploy the hook
relaunch_and_capture.sh relaunch the game and capture logs
hook/dothook.c earlier hook variant, kept for reference
re_notes/
dump.cs the full IL2CPP dump: every class, method, and field in the game
decomp_out.c decompiled bodies of key functions
decompile_targets.txt the offsets worth decompiling
ASSET_INVENTORY.txt inventory of asset identifiers in an operator-supplied app
```
`re_notes/dump.cs` 是剩余工作中最有价值的文件。它是
游戏的完整类型模型:每个类、每个方法,最关键的是
客户端从服务器读取的每个数据字段。它是你整个后端 API 的地图。当你需要
知道响应应该采用什么结构时,答案就在里面。
## 本软件包未包含的内容及其获取方式
这些内容被故意排除在外,因为它们体积庞大,或受版权保护,或属于机密,或者
你应该自己生成。
- APK 本身(`com.kabam.bigrobot`,版本 9.2.0)。请仅从你有权
使用的副本中获取源文件并使用。包名和版本记录在 `TECHNICAL_NOTES.md` 中。
- 原始的 `libil2cpp.so` 和游戏资源。两者都可以直接从 APK 中提取。解压
APK,库文件位于 `lib/arm64-v8a/` 下,资源位于 `assets/` 下。
- TLS 证书和 CA。切勿发布私钥。运行 `Server/gen_certs.sh` 生成你自己的
匹配对,然后将设备信任库指向新的 CA。
- 已打补丁的库。重新生成它:针对 APK 中的原始
`libil2cpp.so` 运行 `patches/patch_il2cpp.py`。
- Frida server 和 Il2CppDumper。两者都是公开工具。Il2CppDumper 是用于从 APK 的库和 global metadata 生成
`re_notes/dump.cs` 的工具。
- Android NDK(使用的是 r26 版本)和 JDK 21,这是构建 hook 和运行 Ghidra
headless decompiler 所必需的。
- `media/` 或 `probe/` 下的任何内容。这些是本地的截图和录屏,可能包含
受版权保护的游戏视听内容,它们会被 Git 忽略,
且绝不能被添加或提交。
## 如何运行目前的版本
你需要将 APK 安装在支持 ARM 转换的模拟器上(使用的是 LDPlayer 9,
需开启 root 且系统可写),在 PC 上安装 Python,并准备上一节中提到的各项物品。
1. 生成一次证书:`bash Server/gen_certs.sh`。
2. 构建一次已打补丁的库:`python3 patches/patch_il2cpp.py path/to/original/libil2cpp.so --apply`。
3. 如果你想重新构建 hook,请构建一次;否则使用预构建的版本。参见
`tools/nativehook/deploy.sh`。
4. 在 PC 上启动模拟服务器:`python3 Server/fakeserver.py`。必须确保模拟器能够通过
443 和 80 端口访问它。
5. 配置设备:`bash tools/provision_ldplayer.sh `。在
每次模拟器重启后重新运行此命令。
6. 等待约 45 秒,然后点击标题屏幕登录。你应该能进入主
屏幕。
### 通过 Wi-Fi 在未 root 的手机上运行(游玩时无需 USB)
手机和笔记本电脑可以通过同一个 Wi-Fi/LAN 直接通信。APK 必须使用
笔记本电脑的局域网 IPv4 地址进行构建,因为普通手机无法覆盖已失效的
Kabam DNS 名称。例如,如果笔记本电脑的 IP 是 `192.168.0.139`:
1. 构建局域网版本:
`python3 Server/build_phone_apk.py --server-host 192.168.0.139 "Transformers 9.2 offline.apk" build/phone-wifi-unsigned.apk`。
2. 使用 Android build-tools 对齐并签名:
`zipalign -f -p 4 build/phone-wifi-unsigned.apk build/phone-wifi-aligned.apk`,然后
`apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android --key-pass pass:android --out build/Transformers-9.2-offline-phone-wifi.apk build/phone-wifi-aligned.apk`。
3. 在笔记本电脑上启动 `python3 Server/run_local.py`。它将在每个网络接口上的
HTTPS 端口 8443 和 HTTP 端口 8080 上监听。如果启用了
防火墙,请允许这两个 TCP 端口通过笔记本电脑的防火墙以连接专用局域网。
4. 通过 USB 安装一次:
`CONNECTION_MODE=wifi UPDATE_APK=1 APK=build/Transformers-9.2-offline-phone-wifi.apk Server/provision_phone.sh`。
如果已安装的应用使用相同的签名密钥,更新时会保留游戏数据。你也可以
通过其他可信的本地方法传输并安装已签名的 APK。
5. 断开 USB。在游玩时,请保持模拟服务器运行,并确保手机和笔记本电脑保持在同一个
局域网中。
内嵌的地址必须始终分配给该笔记本电脑。建议使用 DHCP 预留;
如果地址发生变化,请使用新地址重新构建并更新 APK。访客 Wi-Fi 网络
通常会隔离客户端之间的通信,因此无法使用。原生的验证补丁接受
本地服务器证书,因此无需在手机上安装 CA。
### 通过 USB 在未 root 的手机上运行
零售手机无法使用模拟器中专供 root 使用的 hosts 和系统 CA 绑定挂载。请构建一个
手机变体,将内嵌的后端名称重定向到 loopback 并使用 Android
所有者安装的 CA 信任,然后使用 ADB reverse 通过 USB 传输流量:
1. 运行 `python3 Server/build_phone_apk.py "Transformers 9.2 offline.apk" build/phone-unsigned.apk`。
构建器还会注入当前的 `tools/nativehook/libdothook.so`,因此手机构建版本
包含了与在模拟器上测试过的相同的游戏性修复。
2. 使用 Android `apksigner` 对结果进行签名,生成
`build/Transformers-9.2-offline-phone.apk`。生成的本地构建版本使用你的 Android
debug key。如果已经安装了由不同签名构建的同名包,则
必须先卸载它;这样做会清除该安装版本的本地的游戏数据。
3. 在笔记本电脑上启动 `python3 Server/run_local.py`。
4. 连接并授权恰好一部物理手机,然后运行 `Server/provision_phone.sh`。
如果 Play Protect 以
`INSTALL_FAILED_VERIFICATION_FAILURE` 拒绝了这个本地修改过的 APK,
请检查 APK 并重新运行首次安装,命令为
`ALLOW_UNVERIFIED_ADB=1 Server/provision_phone.sh`。这仅针对该次 ADB 安装临时禁用验证,
并在安装后立即恢复这两项设置。后续运行会检测到
已安装的包,并仅恢复端口转发/启动它。重新构建 APK 后,请使用
`UPDATE_APK=1 Server/provision_phone.sh` 原地更新它,同时保留游戏数据。
手机 APK 保留了原始的 target SDK。其捆绑的原生验证补丁接受
本地服务器证书;降低 target SDK 以继承用户安装的 CA 信任会导致
现代的 Play Protect 拒绝该 APK。
必须保持 USB 连接处于活动状态:手机的 8443 和 8080 端口被转发到笔记本电脑的
相同端口。使用明确的非特权端口是必要的,因为原生 Android 的 ADB 无法绑定设备的 443 或 80 端口。在重启或重新连接 USB 调试后,
请重新运行 `provision_phone.sh`。
如果在登录时卡住,在进行任何其他检查之前,请先查看避坑指南部分的第一项。
## 会耗费你时间的避坑指南
这些都是曾耗费了我数个小时的问题。把它们写下来,这样它们就不会再耗费你的
时间了。
- runtime hook 只能通过原始库中不存在的依赖项条目加载。补丁脚本基于
原始库构建,因此如果不重新添加该条目,
hook 就会被静默地不予加载,登录也会卡住。现在补丁脚本在每次
构建时都会重新注入它。如果 hook 似乎失效了,首先要检查的是已打补丁的
库是否实际引用了 `libdothook.so`。确切的字节和偏移量记录在
补丁脚本和 `TECHNICAL_NOTES.md` 中。
- 如果你使用 LDPlayer9/Bluestacks 进行测试,Frida 将无法工作。模拟器将 ARM 转换为 x86,
而 Frida 在这种转换下会崩溃。该项目之所以使用纯字节覆写 inline hook,整个原因就在于它在 Frida 无法生存的地方存活了下来。不要浪费时间去尝试让 Frida 正常工作。
- 设备网络挂载在模拟器重启后无法保留。hosts 重定向和 CA
信任都是绑定挂载。在模拟器进行任何重启后,你都必须重新运行
`provision_ldplayer.sh`,否则将无法连接任何设备。
- 在 Sparx 错误 payload 中,该字段是 `err`,而不是 `error`。使用错误的字段会
导致客户端静默忽略或错误处理响应。
- 交互式教程提示状态在离线时会无限循环。不要试图通过
响应教程请求来满足它。相反,应首先移除触发该教程的条件。
盾牌教程冻结问题正是通过这种方式修复的,即给予玩家
因缺失而触发该问题的资源,而不是通过响应教程请求。
- 模拟器下的实时 3D 内容渲染非常脆弱。模型确实能够渲染,但这
是最不稳定的区域,并且对模拟器的图形后端和纹理设置很敏感。
这是模拟器的图形问题,而不是数据问题。
## 如果你想真正复活它:重建后端
这是真正的工作,而且非常庞大。以下是它的整体结构以及从哪里开始。
目标是手工重建曾经流式传输到
客户端的服务器端内容:任务和关卡、地图及其敌方阵容、包含每个机器人
属性和能力的完整阵容、战斗公式以及经济系统。这些都不复存在了,
因此所有内容都必须以客户端期望的精确格式重新编写。
行之有效的方法是围绕该项目的循环。在附加 hook 的情况下运行游戏。
hook 会记录客户端读取的每个键。当客户端请求你
尚未提供的内容时,你可以准确地看到它需要什么。然后你合成
正确格式的响应,将其放入 `Server/responses/` 中,或者将其添加到
`Server/fakeserver.py` 中的动态处理程序中,重启,并验证客户端是否接受它并继续推进。重复此操作。当前
构建版本中的每个屏幕都是通过这种确切的方式启动的。`re_notes/dump.cs` 在你运行之前就会告诉你
每个结构的形状,因为它列出了客户端读取的每个字段。
合理的推进顺序:
1. 持久化任务完成状态,并为现有的 STORY 战斗编写奖励契约。
2. 添加原始的逐个机器人能力定义,并测试它们在战斗中的效果。普通攻击
和特殊伤害数据在 `Server/gamedata.py` 中已经拥有了一个可用的生成路径。
3. 每次添加一小部分路径的任务、地图和对手阵容,并使用 runtime hook
和 `re_notes/dump.cs` 来确立客户端契约。
4. 仅在使用它们(需要用到它们)的任务和奖励之后,再添加经济系统和更广泛的进度系统。
要对规模保持现实的认知。即使对于那些在关闭前由玩家保存了 live 服务器数据的游戏来说,
建立私人服务器也是一个漫长的过程。在这里没有任何已保存的数据可供
作为起点,因此每一个数字和每一个能力都必须经过研究或重新发明,然后
针对客户端进行验证。如果目标是重现真实的游戏体验,这将是一项需要多人、
耗时数年的工作。话虽如此,实现路径已不再是未解之谜。离线启动、首次 STORY 战斗
和反馈循环均已正常工作;类型模型也已导出。剩下的就是大量
细致的、原创的数据编写和验证工作。
从 `TECHNICAL_NOTES.md` 开始。它是更深层的技术参考,包含了确切的
补丁、恢复的数据结构以及具体的发现,比这份
README 更为详细。然后运行这个循环。
祝你好运。它现在是一台真正的机器了。只是需要重建它的内容。
标签:云资产清单, 单机化, 底层编程, 游戏修补, 游戏存档, 游戏私服, 逆向工具, 逆向工程