563 lines
44 KiB
Markdown
563 lines
44 KiB
Markdown
# 3DTank Kotlin 重构计划(KtRebuild Plan)
|
||
|
||
> **目标**:用 Kotlin 复刻游戏本体,最终产出**职责等价、可启动**的 JS 客户端。
|
||
> **基线**:`main.7cfa4058.js`(10,080,299 字节)/ `client_version = e98d02161a563e043eabb96c6e4db5fedcf165e7`
|
||
> **文档定位**:计划表 —— 有哪些工具、怎么用、按什么顺序做、怎么验收。
|
||
|
||
---
|
||
|
||
## 0. 先纠正三个前提
|
||
|
||
1. **`dir.file` 不是当前版本的职责表。**
|
||
它是旧版 126 模块的职责划分;当前版本的模块 id、字节数、内部字段都有漂移。
|
||
但**职责划分大体一致**,仍可作为"要做哪些子系统"的规划依据;正式开工前必须用当前包重建一份职责表(见 S0)。
|
||
|
||
2. **`e98d0216` 不是文件名。**
|
||
`e98d0216…` 是 `client_version`(git release hash,写死在 `module_61860.js`):
|
||
```js
|
||
i.g3("client_version", "e98d02161a563e043eabb96c6e4db5fedcf165e7");
|
||
```
|
||
而 `main.7cfa4058.js` 的 `7cfa4058` 是 webpack **contenthash**。两者无对应关系,不能互相推导文件名。
|
||
|
||
3. **真正要复刻的只有 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` 的旧数据。
|
||
- **做法**:
|
||
1. `node tools/split.js --clean`
|
||
2. `node 3DTank/tools/extract.js ../main.7cfa4058.js ../src`
|
||
3. `node mine_evidence.js`(更新 `evidence.json`)
|
||
4. 以 `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 的一半(登录 + 大厅)。
|
||
- **进度(Kotlin/JS,2026-10)**:
|
||
- 根状态 / 委派表由 `3DTank/tools/gen_root_wiring.js` 从当前 `module_10032.js` 现场解析:
|
||
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
|
||
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
|
||
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
|
||
- 已迁移切片 **76/89**:
|
||
- `inviteState` → `aD` / `pD.er5`(`InviteReducer.kt`)
|
||
- `loginByPassword` → `gD` / `MD.cr6`(`LoginReducer.kt`,含 `SetLogin`/`SetPassword` 把 status 打回 `NONE` 的原语义)
|
||
- `twitchState` → `g9` / `y9.gta`(`TwitchReducer.kt`;`SetAccount` 是 Twitch 账号,不是 UID)
|
||
- `home` → `s1` / 根 reducer `(inline@home)` 分支(`HomeReducer.kt`;`ShopDiscountNotification`)
|
||
- `localBattleUserState` → `lE` / `(inline@localBattleUserState)` 分支(`LocalBattleUserReducer.kt`)
|
||
- `shop` → `j4` / `(inline@shop)` 分支(`ShopReducer.kt`):顶层 30 字段 32 分支;
|
||
嵌套 `U7`(`Categories`,7 字段)5 臂、`w3`(`ShopItems`,23 字段)32 臂,共 **69 个 instanceof 动作** 全量翻译。
|
||
无 `toString` 的 `Yut/F5/g4/X5/w5/o4/m4/k4/i4/t4/f5/v5/p5/H7/s5/o5/_5` 按效果定名
|
||
(`ShopLoaded`/`ResetShop`/…/`AddCategoriesWithBonus`/…),原名保留在 `ShopReducer.kt` 注释里。
|
||
- `screen` → `WK` / `dY().ks5`(`ScreenReducer.kt`):10 字段,13 个 `instanceof` 臂全量翻译。
|
||
`GoTo` 的新栈规则 = `isResetBackStackOnNextGoTo ? [] : history ? stack + current : stack`,
|
||
`current` 换成目标屏并清 `isResetBackStackOnNextGoTo` / `isDelayedGoto`;`screen_show` analytics 是副作用,纯 reducer 只保留状态。
|
||
屏幕身份用 `App.State.ScreenId`(`EntranceScreen` / `LoaderScreen` / `NamedScreen(name)`);
|
||
`LX` / `RX` 是屏幕标识类,已从动作表剔除并记入 `root_wiring.json` 的 `Markers`(`screen-id-class`)。
|
||
- `lootBoxes` → `aH` / `EH.wrv`(`LootBoxesReducer.kt`):18 字段,18 个 `instanceof` 臂全量翻译。
|
||
`timeToGiveLootBox` 是 Long 毫秒,`SetTimeToGiveLootBox` 按 `Date.now() + seconds * 1000` 写入;
|
||
`AddKeys` 沿用原 `Map.getValue` 语义(缺键抛错),`SetRewards` 同时把 `isReopening` 置 false;
|
||
`ClearRewards` 清空 `rewards` / `shownRewards`,`ShownRewards` 是追加(原 `List.plus`)。
|
||
补给箱类型用 `App.State.ContainerType`(`STANDARD/RARE/EPIC/LEGENDARY`,默认 `STANDARD`);
|
||
无 `toString` 的 `mH/kH/zH/gH/CH/SH` 按效果定名(`SetTimeToGiveLootBox`/`SetPeriodOfGiveLootBox`/…)。
|
||
- `garage` → `UB` / `hF`(`GarageReducer.kt`):31 字段;58 个 `instanceof` 臂,
|
||
另加 `Xd(t, LF)`(`GarageInsertDeviceClientAndServer`,`LF` 是标记接口)与 `yd(t, Nu)`(`xU` 单例)两个非 `instanceof` 臂,**共 60 分支**。
|
||
`PB`(`itemsByBase`)用 `GarageBaseItems`,`UF`(`baseItemIdToDevices`)与 `GarageDevice` 强类型;
|
||
`aB` 的 `urb_1` = `modification.vcf_1 ?: id`、`trb_1` = `hasDeviceSale && owned`。
|
||
`GarageLoaded` 按 depot/mounted/market/skins 重建物品表;`MU` 的 `krg` 是批量 `owned + mounted = true`(不是卸下);
|
||
`dU`/`wU` 写整组 `mountedSkin`/`mountedShotSkin`;`GoTo` 仅在目标屏幕为 Garage 时复位 `currentCategory`。
|
||
无 `toString` 的 25 个匿名类按效果定名(`rF`→`ResetGarage` … `FB`→`ClearWaitServerItemMount`);
|
||
`FX`(Garage 屏幕)与 `UB`(状态类)已从动作表剔除并记入 `Markers`(`screen-id-class` / `state-class`)。
|
||
- `garagePreview` → `IG` / `fs.eru`(`GaragePreviewReducer.kt`):22 字段,25 个顶层臂
|
||
(22 个 `instanceof` + 3 个单例 `yd(t, is/es/rs)`)。`ShowRequest` 按 `withDelay` 决定
|
||
`isVisible = !withDelay && 原值`、`needVisible = withDelay || 原值`;`UpdateViewStyle` 无字段,
|
||
原 `t.fru_1` 是 `undefined`,`trs` 视作省略,状态不变。
|
||
`SetHullData` / `SetWeaponData` / `SetDrone` / `SetPaint` 先查 `rrs(category, itemId)`
|
||
(`expectedItemId[category] == null || == itemId`);命中才写 3D 数据,但无论命中都缓存动作并删 `requestedItems`。
|
||
`TankPartData` / `TankPreviewSettings` / `HullEngineType` / `TurretMechanism` 见 `GaragePreviewModels.kt`;
|
||
12 个匿名动作按效果定名(`BG`→`CaptureScreenshot` … `tH`→`ClearDroneData`),
|
||
3 个单例臂定名 `ClearGaragePreviewParams` / `ShowGaragePreview` / `HideGaragePreview`,不在 `Actions` 表里。
|
||
- `entrance` → `hD` / `Bv.pth`(`EntranceReducer.kt`):8 个布尔字段,7 个 `instanceof` 臂。
|
||
`showSocialNetworks` 缺省 true,其余 false;`HideSocialNetworks` 置 false,
|
||
`SetControlsForChina` / `SetRunningInChinaDataCenter` / `SetRunningInChinaPartners` /
|
||
`SetAuthorizedByChinaSdkToken` / `SetClassic` 各置一个 true,`SetAgeWarnShow` 写入 `isAgeWarnShow`。
|
||
原 reducer 用 `ur4` 的 `cd` 参数保留其余字段;`authorizedByClassicEarlyAccessToken` 没有写入臂。
|
||
- `user` → `t8` / `M8().qtd`(`UserReducer.kt`):32 字段,28 个 `instanceof` 臂。
|
||
根委派是 `M8().qtd(t, n.ypu_1)`,全部通过 `qtb`(`t8.copy`,33 参数 = 32 字段 + 回调)写回,`cd` 表示保留原字段。
|
||
`id` / `premiumExpirationTime` / `eventPassEndTime` 是 Kotlin/JS `Long`(原构造 `Dd`),
|
||
`rankBounds` 保留原 `RankBounds` 形状(默认 `UserRankBounds(0,0)`);`disabledWithoutPremiumLocations` 收口为 `Set<Int>`。
|
||
`SetUserPropertiesAction` 只复制 id/货币/rank/分数/rating/uid/观战权限/normalizedRank/profileUrl,不清空未触及字段;
|
||
`AddCoins`/`AddRubies` 把 `spent` 收回主余额后清零,`SpendCoins`/`SpendRubies` 同步累加 `spent`。
|
||
`UpdateRank` 用 `min(rankNames.size, rank)` 重算 `normalizedRank`;`SetUserId(null)` 归零。
|
||
3 个匿名动作按效果定名:`R8`→`DisableLobbyTutorial`、`v8`→`SetInDebt`、`p8`→`ClearInDebt`。
|
||
- `settings` → `Uut` / `xw.ltg`(`SettingsReducer.kt`):2 个字段,2 个 `instanceof` 臂。
|
||
`currentCategory` 是设置页分类枚举(`ACCOUNT`…`SUPPORT`,默认 `GAME`),
|
||
`disableAccountSettings` 默认 false;`Fut`→`SetSettingsCategory`、`Gut`→`DisableAccountSettings`。
|
||
- `accountSettings` → `oet` / `Lp.mtg`(`AccountSettingsReducer.kt`):13 字段,13 个 `instanceof` 臂。
|
||
`newPasswordStatus` 用 `PasswordStatus`(`NONE/OK/SIMPLE/NOT_EQUALS`);
|
||
`ResetPasswordFields` 只清三个密码输入、状态和解锁按钮;`removalTimer` 存时间戳 `Long?`;
|
||
`locationCheckEnabled` 没有写入臂;11 个匿名动作按效果定名并登记。
|
||
- `proBattle` → `Wtt` / `uv.wth`(`ProBattleReducer.kt`):3 字段,2 个 `instanceof` 臂。
|
||
默认 `lastFightBattleId=-1`、`lastFightBattleTeam=NONE`、`battlePasswords=emptyMap()`;
|
||
`Fight` 写 id/team,`SetPassword` 空串删键、非空覆盖。登录切片的 `SetPassword` 是不同类,
|
||
这里用 `App.State.Reducers.SetPassword(battleId, password)`,避免丢 `battleId`。
|
||
- `language` → 枚举值 / `m6().jtg`(`LanguageReducer.kt`):`language` 在根状态里是枚举对象本身,
|
||
默认 `EN`;`d6`→`SetLanguage` 只写回语言,不包 StateSlice。
|
||
- `announcement` → `lC` / `PC().rq5`(`AnnouncementReducer.kt`):5 字段,4 个 `instanceof` 臂。
|
||
`isEnabled` 缺省 true,其余 false/空列表;`bC`→`DisableAnnouncement`、`qC`→`SetAnnouncementModelLoaded`、
|
||
`CC`→`SetAnnouncementItems`(生成物 `Init` 属于另一个切片,已改名)、`$C`→`SetSkipNextPromotedAnnounce`。
|
||
- `news` → `ZX` / `E_.asd`(`NewsReducer.kt`):4 字段,7 个分支(4 个 `instanceof` + 2 个单例 + `hJ`)。
|
||
`SetNewsItemAction` / `AddNewsItemAction` 都按 `compareBy { -dateInSeconds }` 排序(与原包 32 位取负一致),
|
||
后者是「追加再排序」;`RemoveNewsItemAction` 按 `id` 过滤,`SetUnreadNewsCounter` 写计数。
|
||
单例 `x_`/`j_` 定名 `OpenNews` / `CloseNews`;`hJ` 无字段、只把语义未定的 `p` 置 true,暂名 `SetNewsFlag`。
|
||
- `notifications` → `jJ` / `B_.ise`(`NotificationsReducer.kt`):2 字段,4 个 `instanceof` 臂。
|
||
缺省 `notifications=emptyList()`、`notificationSound=null`(根状态是 `new jJ()`,生成器认不出类名,迁移表里显式补 `jJ`)。
|
||
`AddNotification` 追加、`PJ`→`RemoveNotifications(predicate)` 按谓词过滤、`RJ`→`RemoveFirstNotification` 丢第一条。
|
||
- `quests` → `Frt` / `Yrt.otg`(`QuestsReducer.kt` + `QuestModels.kt`):3 字段,4 个分支。
|
||
缺省 `currentSection=SPECIAL`、`currentCategory=null`、`isNotCompletedQuests=false`。
|
||
`GoTo` 到 `tX`(`QuestCategoryScreen(section, categoryId)`)时切到目标分区,到 `nX`(`ChallengeScreen`)时切 `CHALLENGE`;
|
||
原包那条分支还会发 `Wrt(QuestsChangeCategory)` 命令,属于副作用,留给中间件层。
|
||
`Hrt` 类体为空、reducer 读的 `t.iu5_1` 是 `undefined`(`cd` 也是 `undefined`),三个 copy 参数全走默认值,
|
||
这一支实际是 no-op;Kotlin 侧保留 `SetNotCompletedQuests` 动作但归约时原样返回。
|
||
`tX` / `nX` 是屏幕标识类,已从动作表剔除并记入 `Markers`(`screen-id-class`)。
|
||
- `lobbyLoader` → `KW` / `Bs.es1`(`LobbyLoaderReducer.kt`):2 字段,3 个分支。
|
||
缺省 `isShowLoader=false`、`activeLoadersCount=0`;`FW`→`ShowLoader` 计数 +1 并显示,
|
||
`GW`→`HideLoader` 计数 `max(count-1,0)`,显示取 `count != 0`(计数夹到 0 后自动隐藏)。
|
||
- `purchaseDialogState` → `CO` / `je.ur2`(`PurchaseDialogReducer.kt`):4 字段,4 个分支。
|
||
`NO`→`ResetPurchaseDialog` 复位四字段;`ChangeEmailStatus` 只写 `emailStatus`(`PurchaseEmailStatus`:
|
||
`BUSY/FORBIDDEN/FREE`);`SuccessfullyPurchase` 写 withEmail/time,`goods` 是**追加**(`Im` = List.plus)。
|
||
- `promoCode` → `cZ` / `Q_.ysg`(`PromoCodeReducer.kt`):1 字段,2 个实例臂。
|
||
`ActivatePromoCode` 置 `WAIT`,`SetCodeValidateStatus` 写入服务端状态;
|
||
`PromoCodeStatus` = `NONE/INVALID/WAIT/NOT_ACTIVATED/ACTIVATED`,缺省 `NONE`。
|
||
- `supportUrl` → `I4` / `Yl.ct7`(`SupportUrlReducer.kt`):1 字段,2 个分支。
|
||
`SetUrl` 写链接,`R4`→`ClearSupportUrl` 清成空串。
|
||
- `rulesUpdated` → `i1` / `Ch.dsm`(`RulesUpdatedReducer.kt`):1 字段,`UpdateRules` 写正文,缺省 null。
|
||
- `notificationByCountry` → `gJ` / `R_.ssd`(`NotificationByCountryReducer.kt`):4 字段,2 个分支。
|
||
`Init` 写三条国家提示消息,`qJ`→`MarkHomeScreenDialogShown` 只把 `homeScreenDialogShown` 置 true。
|
||
- `availabilityByRank` → `tet` / `$p.xth`(`AvailabilityByRankReducer.kt`):1 字段,
|
||
`SetLoadedData` 整体替换等级可购买表,缺省空 Map。
|
||
- `icons` → `oH` / `vs.kru`(`IconsReducer.kt`):1 字段,2 个分支。
|
||
`SetIcons` 把 `List<IconData>` 收成 `icon → resource` 的 Map(原 `HashMap.put`),缺省空 Map。
|
||
- `questCategories` → `zZ` / `th.lsh`(`QuestCategoriesReducer.kt`):3 字段,5 个分支。
|
||
`SetQuestCategories` 按 `index` 升序;`RemoveQuestCategory` 按对象删除;
|
||
`RequestQuestCategoriesStatistic` 在 `isHiddenUpdate=false` 时把 `statisticLoadedTime` 推到 `Long.MAX_VALUE`;
|
||
`SetQuestCategoriesStatistic` 按 `questCategoryId` 建索引并写 `Date.now()`。
|
||
- `mainQuest` → `WZ` / `ih.bsk`(`MainQuestReducer.kt`):5 字段,7 个实例臂。
|
||
`SetQuests` 把任务按 `completedCount` 展开(`completed && count != 0` 的原始项丢弃,
|
||
再为 `1..count` 生成 `completed=true` 的副本),最后按 `completed` 降序稳定排序,并清空已领奖;
|
||
`UpdateQuests` 按 `id` 就地替换/追加;`AddRewardedQuest` 里的 `questId` 实际是任务对象;
|
||
`VZ`→`ResetQuestsLoaded`(无 `toString`,只 `isLoaded=false`),`KZ`→`QuestsLoaded` 只置 `isLoaded=true`。
|
||
- `securitySettings` → `V2` / `pc.qsv`(`SecuritySettingsReducer.kt`):1 字段,缺省 `isProfileBlocked=true`,
|
||
`SetProfileBlockStatus` 直接写布尔。
|
||
- `referralEvent` → `pQ` / `lh.osk`(`ReferralEventReducer.kt`):14 字段,4 个实例臂。
|
||
`SetReferrer` 写前 2 字段、`SetReferrals` 写人数与明细、`SetPromoCode` 写邀请码;
|
||
`SetReferralEventData` 一次写后 9 个字段(面板标题 / 弹窗图与背景 / 两个分类 id / 任务数 / 两个隐藏标志 / 公告时间)。
|
||
- `tipsState` → `dst` / `Hw.vth`(`TipsStateReducer.kt`):1 字段,`UpdateTips` 整体替换提示列表,缺省空表。
|
||
- `rankUpDialogState` → `LO` / `Se.br3`(`RankUpDialogReducer.kt`):1 字段,2 个分支。
|
||
`ShowAvailableItems` 是**追加**(`Im`:元素或整表),`FO`→`ResetRankUpDialog` 直接 `new LO()` 清空。
|
||
- `battleResult` → `sR` / `Fi.aqp`(`BattleResultReducer.kt`):5 字段,6 个实例臂。
|
||
`SetRewards` / `SetHasBattleResult` 写前两字段;`cR`/`lR`→`SetIncreasedReward`/`SetSimpleReward` 写两个奖励数值;
|
||
`fR`→`ResetBattleRewards` 只把两数值归零;`DoInit` 重建状态只带 `initialCurrentUserXP`。
|
||
- `captchaState` → `jP` / `ar.iqt`(`CaptchaStateReducer.kt`):4 字段,7 个实例臂。
|
||
三个位置集合缺省 `emptySet()`;`SetLocations` 用 `toSet()` 收口;
|
||
`SP`→`ShowFakeCaptcha` 只置 `useFake`,其余是启用 / 已验证 / 待重置集合的增删元素。
|
||
- `modalScreen` → `nV` / `Fs.js1`(`ModalScreenReducer.kt`):2 字段,4 个分支。
|
||
`AddModal` 按 `toString()` 判重后 `current=screenId` 并入队;`RemoveModal` 出队后 `current = queue.firstOrNull()`;
|
||
`XW`→`CloseCurrentModal` 关当前窗口;`RemoveCurrentByName` 名称不匹配时 no-op。
|
||
- `droppableGold` → `Vet` / `Wp.kth`(`DroppableGoldReducer.kt`):1 字段,缺省 `showDroppableGoldAuthor=true`;
|
||
`Ket`/`Yet` 两个单例 → `ShowDroppableGoldAuthor` / `HideDroppableGoldAuthor`。
|
||
- `eventGoods` → `W7` / `Wc.esx`(`EventGoodsReducer.kt`):1 字段 `Map<Any?, Any?>`,缺省空 Map;
|
||
`InitEventGoods(itemId, goods)` 在 `goods == null` 时删键,否则写键值。
|
||
- `creatorTagState` → `qO` / `ze.er2`(`CreatorTagReducer.kt`):2 字段,4 个分支。
|
||
`creatorTags` 缺省 null;`SetCreatorTags` / `SetCreatorTag` / `SetAttachedCreatorTag` 写对应字段,
|
||
`jO`→`ClearCreatorTag` 只清 `creatorTag`。
|
||
- `rewardedVideosState` → `JQ` / `xh.zsl`(`RewardedVideosReducer.kt`):1 字段,
|
||
新增枚举 `RewardVideoDisplayState { NONE, LOADING, DISPLAY }`,缺省 `NONE`;`SetDisplayState` 整体替换。
|
||
- `passwordChangeState` → `VD` / `uu.pr6`(`PasswordChangeReducer.kt`):3 个字符串字段(hash / uid / email),
|
||
缺省全为空串;`SetPasswordChangeHash` 只写 hash,`ShowEmailAndPasswordChangeDialog` 写 uid/email 并保留 hash。
|
||
- `system` → `_9` / `w9().xt9`(`SystemReducer.kt`):3 字段,2 个分支。
|
||
`DoShowMessage(message, restart)` 写前两字段,`SetFatalError` 只写 `fatalError`。
|
||
- `contextTutorial` → `Mst` / `Jst().tth`(`ContextTutorialReducer.kt`):4 字段,5 个分支。
|
||
`MarkElementAsViewed` 从 `unViewedElements` 删一个元素(`Ym`),其余为整表 / 整 Map 替换。
|
||
- `battleTutorial` → `rE` / `Q.sq8`(`BattleTutorialReducer.kt`):3 字段,4 个分支。
|
||
`oE`→`MarkMovementStarted` 置 `movementHasStarted`,`sE`→`ResetBattleTutorial` 整片复位。
|
||
- `spectator` → `HA` / `Li.jqm`(`SpectatorReducer.kt`):4 字段,6 个分支。
|
||
`JA`→`AddSpectator` 追加 `SpectatorInfo`(复用生成物里的同名模型),`ZA`→`RemoveSpectator` 按 userId 过滤,
|
||
`XA`→`SetSpectators` 整表替换,`YA`→`SetSpectatorsListVisible`。
|
||
`spectatorHudLayout` 的原模型是 15 字段的 HUD 布局对象(战斗层),按 `Any?` 收口,缺省 null。
|
||
- `twoStepVerificationState` → `x9` / `_f.atb`(`TwoStepVerificationReducer.kt`):6 字段,5 个分支。
|
||
`VerificationStatus` 三态:null 清空、false 写失败、true 直接把 `isActivated2FA` 置 true(原 `rta(cd, !0)`)。
|
||
- `antiAddiction` → `UC` / `W.iq8`(`AntiAddictionReducer.kt`):8 字段,5 个分支。
|
||
`FC`→`EnableRealNameCheck`、`XC`→`RequireUserIdVerification`(同时置 `unknownUserId`)、
|
||
`YC`→`UpdatePlayingTime`;`SetRealNameAndIdNumber` 的参数为 null 时保留原值。
|
||
- `moderation` → `NI` / `Ei.oqh`(`ModerationReducer.kt`):11 字段,13 个分支。
|
||
选中用户列表用 `ModerationUserRecord` 收口:`GI`/`WI` 先按 userId 去重再追加,`KI` 删除,`YI`/`uA` 清空;
|
||
`XI`/`JI` 与 `ZI`/`QI` 分别是战斗信息面板、系统消息的显示/收起,`tA`/`nA` 是封禁菜单。
|
||
`battleInfo` 的原模型 `MI` 有 13 字段(战斗层),按 `Any?` 收口。
|
||
- `migrationState` → `$it` / `Uv.rtg`(`MigrationStateReducer.kt`):7 字段,11 个分支。
|
||
新增 `MigrationLoginStatus { UNKNOWN, FREE, INCORRECT, BUSY, SHORT_NAME }` 与
|
||
`MigrationRegisterStatus { UNKNOWN, PASSWORD_CORRECT, PASSWORD_INCORRECT, FAILED }`(对应源码 `Eit()` / `Iit()`)。
|
||
`SetUid` / `ForceSetUid` / `SetPassword` 与注册、登录切片共用同一批动作类,因此几片 reducer 同时响应、各自动自己的字段。
|
||
- `register` → `Cit` / `cp.stg`(`RegisterReducer.kt`):16 字段,19 个分支。
|
||
新增 `UidStatus`、`RegistrationStatus` 两个枚举;`SetUid` / `ForceSetUid` 会复位 uid 校验状态,
|
||
且只有原状态不是 `INCORRECT` 时才把 `registerStatus` 一起打回 `UNKNOWN`(原 `2 === n.wtr_1.k3_1` 分支)。
|
||
- `userReportsState` → `Zst` / `qot.zth`(`UserReportsReducer.kt`):5 字段,6 个分支。
|
||
新增 `ReportKind { BATTLE_NAME, LOBBY_CHAT_MESSAGE, BATTLE_CHAT_MESSAGE }` 与 `ReportEntry(kind, targetId, expireTime)`;
|
||
三类举报都追加一条记录、有效期 +1 天。原 reducer 还把列表写回 localStorage 的 `SENT_REPORTS`,
|
||
属 storage port 职责,纯 reducer 不做 IO(Init 保留现有列表)。
|
||
- `friends` → `KD` / `NL().cr9`(`FriendsReducer.kt`):8 字段,13 个分支。
|
||
新增 `FriendSearchState { HIDDEN, SEARCHING, FOUND, NOT_FOUND }`(对应 `YD()`);
|
||
四个集合分别做集合 / 单值增删;`SetValidationStatus` 原判据是 `status.equals(pk())`,
|
||
`pk()` 是校验通过单例,离线重建按「非空即 FOUND」收口。
|
||
`AddUsersToNewAccepted` 与 clan 切片的同名动作合并成一个类(`clanId, userIds`),friends 只读 `userIds`。
|
||
- `suppliesState` → `wE` / `$t.fq9`(`SuppliesStateReducer.kt`):1 字段 `Map`,
|
||
`qE`→`SetSupplyCount(supplyId, count)` 写一个键值。
|
||
- `clanUserInfo` → `hO` / `de.gr1`(`ClanUserInfoReducer.kt`):1 字段 `Map`,
|
||
`Loaded(users)` 是**合并**(`jm` = copy + `putAll`),不是覆盖。
|
||
- `battleInviteState` → `att` / `Ttt().nth`(`BattleInviteStateReducer.kt`):3 字段,6 个分支。
|
||
`mtt` 的 `toString` 名 `InviteToBattle` 与另一个只有 `battleId` 的同名动作冲突,
|
||
改用 `AddInvitedUser(receiverId)`;`dtt`→`ClearSelectedForInvite`。
|
||
- `partner` → `UJ` / `(inline@partner)`(`PartnerReducer.kt`):15 字段,11 个内联分支。
|
||
`_Z`→`UpdatePartnerInviteAction`、`tZ`/`JJ`→`MarkAccountLinked`/`MarkAccountNotLinked`、
|
||
`hZ`→`SetPartnerItemEnabled`(`Km` = Pair,写 `itemsEnabledForPartner`);
|
||
`loginForBindingStatus` 复用登录切片的 `ByPasswordStatus`。
|
||
- `userRenameState` → `O8` / `Rf.pte`(`UserRenameReducer.kt`):10 字段,11 个分支。
|
||
新增 `RenameUidState { EMPTY, SHORT, RESERVED, FORBIDDEN, NOT_VALID, FREE }`(对应 `D8()`);
|
||
`SetValidationStatus` 按 ordinal 映射输入状态(EMPTY→SHORT、SHORT→NOT_VALID、RESERVED→RESERVED、
|
||
FORBIDDEN/NOT_VALID→FORBIDDEN、FREE→FREE)并把 `isResponseDelivered` 置 true;
|
||
`G8`→`ResetRenameDialog` 复位弹窗字段但保留 rubies / hash / email。
|
||
- `battlePauseState` → `dT` / `NT().mqc`(`BattlePauseStateReducer.kt`):3 字段,4 个分支。
|
||
`OnPauseMode` 置 `inAutoPauseMode=true` 并写剩余时长;`ResetBattlePause` 只保留
|
||
`idleKickPeriodInMsec`;`timeForAutoLeave` 原值是「当前时间 + 时长」,时钟换算留给中间件层。
|
||
- `battleViewState` → `DT` / `sI.jqd`(`BattleViewStateReducer.kt`):7 字段,14 个分支。
|
||
`SetVisibility` / `SetElementsVisibility` 按 `visible` 增删 `invisibleElements`;
|
||
其余 12 个无 `toString` 的动作两两成对,分别切换输入 / 统计 / 菜单 / 隐藏 / 后台位。
|
||
- `tankActionsLogState` → `vI` / `wi.gqe`(`TankActionsLogReducer.kt`):2 字段,4 个分支。
|
||
`UpdateLog` 在超过 10 条时先丢弃最早一条再追加(源码 `(Lf = 10)`);
|
||
`OnUserJoinTheBattle` / `OnUserLeaveTheBattle` 增删 `noticed`;`dI`→`ResetTankActionsLog` 整片复位。
|
||
- `suppliesTutorial` → `ust` / `Uw.uth`(`SuppliesTutorialReducer.kt`):2 字段,2 个分支。
|
||
生成器认不出切片类(根构造默认值不是 `new ust()`),在迁移表里显式补 `SliceClass: 'ust'`。
|
||
- `clanChatState` → `qnt` / `yv.qtg`(`ClanChatStateReducer.kt`):3 字段,5 个分支。
|
||
`$nt`/`xnt` 开关面板,`Configure` 写配置(`ynt` 的 `config` 与 clan 切片同名动作冲突,
|
||
整条动作对象按配置载荷收口),`DoAddMessages` 追加后只保留最后 100 条,`Tnt` 整片复位。
|
||
- `battleSearchState` → `Itt` / `Htt().vtg`(`BattleSearchStateReducer.kt`):5 字段,9 个分支。
|
||
`Fight` 只发副作用、状态不变;`RemoveBattle` 仅当选中 id 匹配时清空;
|
||
`selectedFormat` 默认值来自服务定位器,按 `null` 收口。
|
||
- `battleStatistics` → `jR` / `rr.fqr`(`BattleStatisticsReducer.kt`):27 字段,22 个分支。
|
||
`FR`(14 字段的 Init)与通知切片的同名类冲突,改名为 `InitBattle`(contractState 之后复用);
|
||
其余为战斗元信息 setter + 回合 / 统计面板 / 菜单 / 旗主 / 加载位;`SetRemainingTime` 用 `$m` 把时长转秒。
|
||
- `soundSettings` → `Det` / `Wet().jth`(`SoundSettingsReducer.kt`):10 字段,4 个分支。
|
||
音量缺省 100/100/50/50/50/50/50,三个开关 false/false/true;`Bet`/`Fet` 都改名为
|
||
`ApplySoundSettings` / `SetEntranceSoundSettings`(原名都是 `UpdateSettings`,与其它设置切片同名)。
|
||
storage 读写属 storage port,`_C`→`ResetSettingsFromStorage` 保留现有状态。
|
||
- `gameSettings` → `c2` / `g2().zss`(`GameSettingsReducer.kt`):6 字段,2 个分支。
|
||
五个显示开关缺省 true、`mobilePadding=null`;`v2` 同样因 `UpdateSettings` 重名改名为 `ApplyGameSettings`。
|
||
- `keyboardSettings` → `Sut` / `bw.oth`(`KeyboardSettingsReducer.kt`):1 字段 `Map<action, Map<index, keyState>>`,5 个分支。
|
||
`ChangeAction` 先按 keyState 从其它 index 解绑再写入新绑定;`RemoveAction` 解绑单个 index;
|
||
`ResetAction` 依赖外部 helper,纯 reducer 保留原状态;两个默认复位清空绑定表。
|
||
- `touchscreenHudSettings` → `j7` / `N7().xsv`(`TouchscreenHudSettingsReducer.kt`):1 字段,3 个分支。
|
||
`C7`(原名 `UpdateSettings`)改名为 `ApplyTouchscreenHudSettings`;`E7`→`ResetTouchscreenHudMapping` 恢复默认布局。
|
||
- `graphicsSettings` → `L2` / `lc.isu`(`GraphicsSettingsReducer.kt`):17 字段,1 个分支。
|
||
`B2`(原名 `UpdateSettings`)改名为 `ApplyGraphicsSettings` 整体替换;画质枚举按 `Any?` 收口,
|
||
vsync 变化触发的重启提示是副作用,留给中间件层。
|
||
- `controlSettings` → `i2` / `_2().ess`(`ControlSettingsReducer.kt`):7 字段,2 个分支。
|
||
嵌套模型收口为 `ControlMouseSettings` / `ControlGamepadSettings` / `ControlTouchscreenSettings`
|
||
(缺省 50/true、true/13/13、50/true/false/true/true/true/true);
|
||
`u2`(原名 `UpdateSettings`)改名为 `ApplyControlSettings`。
|
||
- `battleSettings` → `W0` / `X0().esq`(`BattleSettingsReducer.kt`):11 字段,2 个分支。
|
||
`K0`(原名 `UpdateSettings`)改名为 `ApplyBattleSettings`;`_C` 的旧键迁移属 storage port,保留现状态。
|
||
- `contractState` → `Ont` / `xv.bti`(`ContractStateReducer.kt`):3 字段,7 个分支。
|
||
合同模型跨模块(id / progress / rewards),收口为 `ContractEntry`;`SetContract` 是**合并**(`putAll`);
|
||
`UpdateProgress` / `GetReward` 复制单条合同再放回;`Fnt`(原名 `GetReward`,与任务切片同名)改名
|
||
`ClaimContractReward`,`Hnt`→`RemoveCompletedContracts` 只留还有奖励的合同;
|
||
`InitBattle` 只把 `lastBattleProgress` 归零。
|
||
- `tankBuilderState` → `brt` / `wp.ati`(`TankBuilderStateReducer.kt`):6 字段,4 个分支。
|
||
`SetParts` 把零件表拆成 `hulls`(hullId 列表)与 `weapons`(index → weaponIds)、并写 `paints`;
|
||
`SetPartsInfo` 按零件 / 涂装各自的 id 建索引;零件模型跨模块,收口为
|
||
`TankBuilderPart` / `TankBuilderPartInfo` / `TankBuilderPaintInfo`;`Trt`→`ResetTankBuilder` 整片复位。
|
||
- `battleMessage` → `FE` / `vT().rqb`(`BattleMessageReducer.kt`):1 字段,7 个分支。
|
||
四种消息模型按源码 `toString` 原样收口:`TextMessage(type, hideTimeMs, text, isHidden)`、
|
||
`LocalizedMessage(…, content, arguments, isHidden)`、`LocalizedUserMessage(…, content, userTeamRelation, userUid)`、
|
||
`LocalizedTeamMessage(…, content, teamRelation)`;`HideMessage` 只把 `hideTimeMs` 匹配的消息标隐藏,
|
||
`RemoveMessage` 删掉匹配项,`sT`→`ResetBattleMessages` 整片复位。
|
||
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
|
||
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
|
||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 75 个有 reducer 的切片,
|
||
`launchParams` 与其余 **13 个字段原样透传**;
|
||
迁移进度用 `RootReducer.Coverage`(`已迁移 76/89`)与 `RootWiring.Unmigrated` 暴露。
|
||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **832 项全绿**,含 89 槽位完整性、75 个归约切片一次归约互不串片、未迁移字段透传,
|
||
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
|
||
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
|
||
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
|
||
`launchParams` 是纯透传(`(pass-through)`,无 reducer),已把字段收口为 `Map<String, String>`。
|
||
- 无 `toString` 的默认动作按证据定名(`root_wiring.json` 的 `AnonymousActions`):
|
||
`o1` → `ShowShopDiscountNotification`(`$_$.gl` 单例,由 module_18334 的 `Qx.qge` 派发;
|
||
效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示);
|
||
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等);
|
||
screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、
|
||
entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、
|
||
language 的 1 个、announcement 的 3 个、news 的 3 个(含两个单例)、notifications 的 2 个、
|
||
quests 的 1 个、lobbyLoader 的 2 个、purchaseDialogState 的 1 个、supportUrl 的 1 个、
|
||
notificationByCountry 的 1 个、mainQuest 的 1 个、rankUpDialogState 的 1 个、battleResult 的 3 个、
|
||
captchaState 的 5 个、modalScreen 的 1 个、droppableGold 的 2 个、creatorTagState 的 1 个、
|
||
battleTutorial 的 2 个、spectator 的 4 个、antiAddiction 的 3 个、moderation 的 13 个、
|
||
迁移 / 注册共用的 10 个(`Mit`…`_rt`)匿名类 / 单例同样按效果定名并登记。
|
||
friends 的 2 个、suppliesState 的 1 个、battleInviteState 的 2 个、partner 的 4 个、
|
||
userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`);
|
||
battlePauseState 的 2 个、battleViewState 的 12 个、tankActionsLogState 的 1 个同样按效果定名。
|
||
clanChatState 的 3 个(`$nt`/`xnt`/`Tnt`)、battleSearchState 的 1 个也已登记。
|
||
battleStatistics 的 12 个(含改名的 `FR`→`InitBattle`)同样按效果定名。
|
||
设置类切片的 `_C`(共用重载)+ `Bet`/`Fet`/`Uet`/`v2`/`C7`/`E7`/`Iut` 也已登记,
|
||
其中原名都是 `UpdateSettings` 的类按切片改名为 `Apply*`,避免生成物同名冲突
|
||
(`Bet`/`v2`/`B2`/`u2`/`K0`/`C7` 共 6 个)。
|
||
contractState 的 `Fnt`/`Hnt` 也已登记(`Fnt` 因与任务切片同名改名为 `ClaimContractReward`);
|
||
tankBuilderState 的 `Trt`(`ResetTankBuilder`)同样按效果定名。
|
||
battleMessage 的 `sT`(`ResetBattleMessages`)也已登记。
|
||
|
||
### 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. 换版与漂移处理
|
||
|
||
1. 抓新 `main.<hash>.js` → `node tools/split.js --clean`
|
||
2. `node tools/map_symbols.js <旧unpacked> <新bundle> all tools/symbol_map.json`
|
||
3. `__hornetSchema__.SelfCheck()` 列出语义字段失配 → **只改 `hornet_schema.js`**
|
||
4. 人工核对三类:
|
||
- `$_.xx` 导出键(队伍 `vd/dl`、碰撞世界 `55088.$_.h` 等)
|
||
- 工具 hook 的类内部方法(`o37` / `o3c` / `n3c` / `f3t` / `q9q` / `kz7`)
|
||
- 渲染管线字段语义(`d75_1` / `w39_1` / `b74_1` …)
|
||
5. `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 工具链) |
|
||
|
||
打包命令(按来源目录二选一):
|
||
|
||
```bash
|
||
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 完整流程
|
||
|
||
```bash
|
||
# 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. 待确认
|
||
|
||
1. `32088`(标注"Alternativa3D,Kotlin 编写")是自研移植还是开源库?决定 S4 是"引入"还是"重写"。
|
||
2. `10032` / `10849`(Redux)是否必须保持 Kotlin 实现,还是可以换成原生 Redux?
|
||
3. S6 UI 是否允许永久保留原 JS 模块(推荐允许)。
|
||
4. 是否要求最终产物能**替代** `main.<hash>.js`,还是只要"能启动 + 职责等价"即可。
|