保持原有行为:默认加载 3dtank.com/play/static/js/unpacked(含 4 个模块的人工补丁),启动时按需打单包,-Build 强制重打;-UseSrc 才切到 3DTank/src,-Split 作为兼容旧参数等同默认。serve.js 的 --src 能力保留但不启用。
14 KiB
3DTank Kotlin 重构计划(KtRebuild Plan)
目标:用 Kotlin 复刻游戏本体,最终产出职责等价、可启动的 JS 客户端。 基线:
main.7cfa4058.js(10,080,299 字节)/client_version = e98d02161a563e043eabb96c6e4db5fedcf165e7文档定位:计划表 —— 有哪些工具、怎么用、按什么顺序做、怎么验收。
0. 先纠正三个前提
-
dir.file不是当前版本的职责表。 它是旧版 126 模块的职责划分;当前版本的模块 id、字节数、内部字段都有漂移。 但职责划分大体一致,仍可作为"要做哪些子系统"的规划依据;正式开工前必须用当前包重建一份职责表(见 S0)。 -
e98d0216不是文件名。e98d0216…是client_version(git release hash,写死在module_61860.js):i.g3("client_version", "e98d02161a563e043eabb96c6e4db5fedcf165e7");而
main.7cfa4058.js的7cfa4058是 webpack contenthash。两者无对应关系,不能互相推导文件名。 -
真正要复刻的只有 40 个模块。
dir.file的分类计数:游戏本体 40 / Kotlin 生态库 21 / 第三方库 65(另有 index.json、resources.json)。 后 86 个是已知开源依赖,直接引上游即可,不需要反编译重写。
1. 验收标准("可启动 JS"的定义)
| 级别 | 判定 | 手段 |
|---|---|---|
| L1 可加载 | 126 模块 + 资源全部注册成功,无 404 / 无语法错误 | tools/test_loader.js 全绿 |
| L2 可进战局 | serve.js 起站 → 登录 → 车库 → 进战斗 → 控制台无未捕获异常 |
人工 + 控制台 |
| L3 行为等价 | 同场景关键状态(位置/血量/状态机/事件序列)与原包对比一致 | 双跑 golden trace |
| L4 工具兼容 | HORNET 全部开关可用(雷达/透视/后视镜/穿坦克/拒绝击退…) | build_bundle.py 产物实测 |
L1/L2 是最低门槛;L3 才是"职责等价"的真正证据。
2. 模块分层(依据 dir.file)
| 分类 | 数量 | 处理方式 | 是否重写 |
|---|---|---|---|
| 游戏本体 | 40 | 反混淆 → Kotlin 复刻 | ✅ |
| Kotlin 生态库 | 21 | 换官方依赖(stdlib / serialization / coroutines / atomicfu / kvbuffer…) | ❌ |
| 第三方库 | 65 | 换 npm 依赖(React 18.3.1 / Redux / Sentry 5.30.0 / qrcode / Underscore 1.13.6 / Emotion / fflate…) | ❌ |
| 资源/索引 | 2 | index.json、resources.json 原样托管 | ❌ |
游戏本体的体积分布(决定推进顺序):
| 模块 | 大小 | 职责 | 阶段 |
|---|---|---|---|
| 82347 | 5.50 MB | 大厅/车库/商店/部落/结算 UI | S6(最后) |
| 87072 | 3.78 MB | 战斗系统:底盘物理、模式、物品池、特效、空投、地图渲染 | S5 |
| 10032 | 1.54 MB | 核心 Redux 状态 | S3 |
| 85183 | 1.18 MB | 全量协议 Codec 注册 | S2 |
| 92980 | 0.83 MB | WebGL 渲染核心 | S4 |
| 其余 35 个 | < 5 MB | 协议/工具/音频/输入/资源/ECS/物理… | S2–S5 |
前 5 个约占 12.8 MB,是工作量的绝对主体。
3. 工具清单与用法
3.1 基线与拆分
| 工具 | 作用 | 用法 |
|---|---|---|
tools/split.js |
把 main.<hash>.js 切成 126 个模块 + resources.json,并注入 bundle 加载 |
node tools/split.js [bundle] [输出目录] --clean(不传参自动取根目录最新 main.*.js) |
3DTank/tools/extract.js |
bundle → prettier 美化源码(3DTank/src/) |
cd 3DTank/tools && node extract.js [bundle] [输出目录] |
tools/pack_unpacked.js |
把 unpacked/ 打成 unpacked.pack.json(一次请求加载全部模块) |
node tools/pack_unpacked.js |
tools/test_loader.js |
模拟浏览器加载流程,验证切割产物完整 | node tools/test_loader.js |
3.2 命名还原
| 工具 | 作用 | 用法 |
|---|---|---|
3DTank/tools/mine_evidence.js |
从源码挖命名证据(组件名/类名/枚举/闭包原名/导出表/字符串) | cd 3DTank/tools && node mine_evidence.js |
3DTank/tools/apply_names.js |
把证据回填成 src_named/(三道校验) |
node apply_names.js --aliases |
tools/map_symbols.js |
新旧 bundle 符号映射(K 元块锚点 + 投票) | node tools/map_symbols.js <旧unpacked> <新bundle> all tools/symbol_map.json |
tools/apply_symbol_map.js |
按映射批量改名(五道安全规则 + 双向校验) | 见文件头注释;默认读 3DTank/tmp_symbol_map.json |
3DTank/names/module-graph.json |
模块依赖图(DependsOn / Exports / Anchors / Kind) |
数据文件,作为 Kotlin 模块切分依据 |
3.3 字段抽象(当前版本适配的主入口)
| 工具 | 作用 | 用法 |
|---|---|---|
tools/hornet_schema.js |
匿名字段 → 语义字段总表(88 项)+ 导出键表 | 控制台 __hornetSchema__.SelfCheck() 一次列出失配项 |
build_bundle.py |
把 tools/hornet_*.js 拼成 3dtank_bundle.js 并同步到 unpacked/ |
python build_bundle.py |
换版本时只改 schema,工具代码不动;这是本计划里"漂移成本最低"的一环。
3.4 本地运行与协议取证
| 工具 | 作用 | 用法 |
|---|---|---|
3DTank/tools/serve.js |
本地静态服务器 + 反向代理(resources / config / balancer)+ 中继 config 改写 | node serve.js --port 8080 [--relay-port 9091] [--verbose] |
3DTank/tools/wss_server.js |
本地游戏通信服务器(中继 + 抓包 + 实时解密) | node wss_server.js --port 9091 --upstream wss://c1.3dtank.com:9090/ |
3DTank/tools/analyze_packets.js |
抓包产物分析 | 见文件头 |
3DTank/tools/prefetch_assets.js |
按 resources.json 预拉资源到本地镜像 |
node prefetch_assets.js |
3DTank/tools/server/ |
协议服务端骨架(codec / dispatch / session / space / world…) | 供 S3 协议层对照 |
3.5 HORNET 探针(现成的行为断言)
tools/hornet_lib.js / hornet_radar.js / hornet_3d_wireframe.js / hornet_noclip.js / hornet_noknockback.js 等,
直接读引擎状态(位置、血量、队伍、物理形状)。S5 之后可用它们做"新包是否语义一致"的活体探针。
4. 阶段计划
S0 基线重建(必须先做)
- 目标:产出当前版本的职责表与依赖图,替换
dir.file的旧数据。 - 做法:
node tools/split.js --cleannode 3DTank/tools/extract.js ../main.7cfa4058.js ../srcnode mine_evidence.js(更新evidence.json)- 以
module-graph.json的DependsOn/Exports/Anchors/Kind重新分类 126 模块
- 输出:
3DTank/names/module-graph.json+ 新版职责表(按当前模块 id) - 验收:
node tools/test_loader.js全绿;分类计数与旧表基本一致(40/21/65 量级)
S1 依赖还原(86 个库)
- 目标:把库模块映射为上游依赖,产出
deps.lock。 - 做法:用
Anchors(版本字符串、API 名)反推包名与版本;导出表对照 API 面。 - 已知:React/React-DOM 18.3.1、Sentry 5.30.0、Underscore 1.13.6。
- 待挖:Kotlin 编译器版本、kotlinx-coroutines / serialization / atomicfu 版本、kotlin-wrappers、Emotion。
- 验收:每个库模块都能指到"包名 + 版本 + 用到的导出符号"。
S2 协议层(14 个 codec 模块)
- 模块:85183 / 47243 / 85196 / 31908 / 16100 / 10283 / 25377 / 54199 / 37504 / 88693 / 89803 / 85667 / 67222 / 99527
- 目标:Kotlin 数据类 +
@Serializable,按 命令 id + 字段序号 对齐。 - 验证:用
wss_server.js抓到的帧做 golden 编解码对比(现有names/*_table.json、codec_fields.json可直接用)。 - 注意:字段顺序/默认值错一位整包不可用,必须逐帧验。
S3 状态与胶水
- 模块:10032(Redux 核心)、18334(DI 装配)、61860(连接与对象模型)、13431(启动)、52943(工具库)、26482(SW 注册)
- 目标:新工程能完成"启动 → 连接 → 进入大厅"。
- 验收:L2 的一半(登录 + 大厅)。
S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
- 目标:场景可渲染、物理可步进。
- 做法:先纯函数层(矩阵/几何/物理步进),再渲染管线;用同输入同输出做单元比对。
- 验收:车库 3D 预览可显示;进战斗不崩。
S5 战斗系统(87072,3.78 MB)
- 按子系统拆文件:底盘/悬挂、物理、战斗模式、物品池、空投、特效、地图渲染。
- 验收:L2 完整(可进战斗并驾驶/开火),HORNET 探针读数与原包一致。
S6 UI(82347 5.5 MB + 21300 HUD)
- 建议最后做,或长期保留原 JS 模块(Kotlin 与 JS 模块可混跑)。
- 验收:各界面可打开、无报错;不追求像素级一致。
5. 目录规划(拟)
KT/
settings.gradle.kts
build.gradle.kts
app/ # 入口(对应 13431)
core/ # 数学 32088、工具 52943
ecs/ # 61378
physics/ # 55088
render/ # 92980
resource/ # 36659、71105
net/ # 61860 + codec/*(S2 全部模块)
state/ # 10032、18334
battle/ # 87072(内部再按子系统分文件)
ui/ # 82347、21300(可延后)
deps/ # React/Redux/Sentry 的 external 声明
web/ # index.html、resources.json、静态资源
约定:
- 一个 webpack 模块 ≈ 一个 Kotlin 文件(巨型模块按类拆);
- 对外属性名保持不变(跨模块共享),类名可改;
- 浏览器 API 全部
external(WebGL2 / WebSocket / PointerLock / WebAudio / Storage / Worker)。
6. 依赖与版本
| 依赖 | 模块 | 版本 | 状态 |
|---|---|---|---|
| kotlin-stdlib-js | 37036 | 待挖 | — |
| kotlinx-serialization-core / json | 9539 / 61430 | 待挖 | — |
| kotlinx-coroutines | 89613 | 待挖 | — |
| atomicfu | 71426 | 待挖 | — |
| kvbuffer | 72834 | 待挖 | — |
| React / React-DOM | 78931 / 45555 | 18.3.1 | ✅ |
| Sentry Browser SDK | 30285 | 5.30.0 | ✅ |
| Underscore | 94965 | 1.13.6 | ✅ |
| Redux(Kotlin 侧) | 10849 | 自研封装 | 需评估 |
| qrcode 系列 | 59411 等 | 待挖 | — |
| Emotion / inline-style-prefixer | 14766 / 77249 等 | 待挖 | — |
| fflate | 29518 | 待挖 | — |
7. 换版与漂移处理
- 抓新
main.<hash>.js→node tools/split.js --clean node tools/map_symbols.js <旧unpacked> <新bundle> all tools/symbol_map.json__hornetSchema__.SelfCheck()列出语义字段失配 → 只改hornet_schema.js- 人工核对三类:
$_.xx导出键(队伍vd/dl、碰撞世界55088.$_.h等)- 工具 hook 的类内部方法(
o37/o3c/n3c/f3t/q9q/kz7) - 渲染管线字段语义(
d75_1/w39_1/b74_1…)
python build_bundle.py→ 实测工具
8. 产出"可启动 JS"的端到端流程
8.1 运行时模块来源
默认:3dtank.com/play/static/js/unpacked(切割产物,含 4 个模块的人工补丁)。
可选:-UseSrc 改用 3DTank/src(纯净还原;人工补丁需先移植过去)。
| 行为 | 说明 |
|---|---|
| 启动时打包 | 单包缺失、或所选目录的 module_*.js 比单包新 → 自动跑 pack_unpacked.js |
-Build |
强制重打单包 |
-UseSrc |
改用 3DTank/src(-Split 为兼容旧参数,等同默认) |
| 路由 | serve.js --src <目录>:仅 -UseSrc 时传入;unpacked/module_*.js、resources.json、index.json、unpacked.pack.json → 源码目录 |
| 仍取镜像 | 加载器 main.<hash>.js、unpacked/hornet_bundle.js(HORNET 工具链) |
打包命令(按来源目录二选一):
node tools/pack_unpacked.js 3dtank.com/play/static/js/unpacked 3dtank.com/play/static/js/unpacked.pack.json
node tools/pack_unpacked.js 3DTank/src 3DTank/src/unpacked.pack.json
8.2 完整流程
# 1) 基线
node tools/split.js --clean
node 3DTank/tools/extract.js ../main.7cfa4058.js ../src
cd 3DTank/tools && node mine_evidence.js && cd ..
# 2) Kotlin 编译(工程建好后)
./gradlew :app:build # 产物:build/dist/js/productionExecutable/
# 3) 产物接入(二选一)
# A. 用 Kotlin 产物直接替换 main.<hash>.js(需自建 webpack runtime)
# B. 保留现有 loader,把 Kotlin 产物作为新模块注册进 __modules__
node tools/pack_unpacked.js
# 4) 验收
node tools/test_loader.js # L1
node 3DTank/tools/serve.js --port 8080 # L2:浏览器进站
node 3DTank/tools/wss_server.js --port 9091 --upstream wss://c1.3dtank.com:9090/ # L3:抓包对比
9. 风险与回退
| 风险 | 影响 | 应对 |
|---|---|---|
| 协议字段序号错位 | 整包不可用 | S2 逐帧 golden 对比;先只做只读命令 |
| Kotlin 版本差异 | 输出与原包不同 | 不追求字节一致,只追求语义等价(双跑) |
| 巨型模块拆分失控 | 进度不可控 | 87072/82347 按子系统拆文件,先骨架后逻辑 |
| 渲染管线语义不明 | 画面异常 | 保留原 92980 模块,先替换非渲染层 |
| UI 体量过大 | 拖慢整体 | S6 延后,允许长期混跑原 JS 模块 |
| 换版漂移 | 工具失效 | schema + SelfCheck() 作为唯一适配面 |
回退策略:每个阶段都保留"原模块可用"的开关;Kotlin 模块按模块 id 替换,出问题单模块回退,不影响整包。
10. 待确认
32088(标注"Alternativa3D,Kotlin 编写")是自研移植还是开源库?决定 S4 是"引入"还是"重写"。10032/10849(Redux)是否必须保持 Kotlin 实现,还是可以换成原生 Redux?- S6 UI 是否允许永久保留原 JS 模块(推荐允许)。
- 是否要求最终产物能替代
main.<hash>.js,还是只要"能启动 + 职责等价"即可。