14 Commits
Author SHA1 Message Date
TermiNexus 2224ddc3f6 feat: M14 搬运官方车库 UI(官方应用壳 + DOM 运行时等价实现 + 车库主屏)
按用户要求改为「搬运式复刻」:版式与样式全部取自官方反编译代码 module_82347.js,逐条标注 file:line,不自行排版。壳:client/src/jsMain/resources/index.html 复刻 3DTank/web/index.html(6 个 @font-face = Rubik-*.woff / MPLUS1p-*.ttf、#preloader、#root.wrapper、#app-root、#modal-root、.bgClipText),有意差异(https 重定向脚本/analytics/isRussia 检测/manifest/favicon/官方 main.6469b966.js)已在报告标注,改为加载 TankIcClient.js。运行时 Ui/Official/UiDom.kt:等价实现 FD(parent,tag,styleFn)/iq(styleObj)->类名(注入 style[data-emotion])/WD/ip2/lp1/fp2 与单位值函数(E$=%, G$=px, O$=em, zj=rgba, rnr=em/16, cnr=rem/16, Ms 的 0 去单位),并注入官方 Emotion Global 表(module_82347.js:105411-105424 的 btr:html/body/.wrapper/portrait 媒体查询)——.wrapper 的 position:absolute;height:100% 不在 index.html 里,是车库 height:100% 成立的前提。车库主屏 Ui/Official/OfficialGarage.kt:按官方实例化路径(module_82347.js:161904-162084 的 gPn -> :166813-166828 的 hGn -> _Ln/PWn/PGn/_Wn)搬节点层级与样式函数,含 GarageCommonStyle/GarageMainScreenStyle/MountedItemsStyle 的片段;设备块按官方 PGn.tan 的 case 13 语义合并为一类一块(6 块而非 33 块)。两处纠正我任务书里的错误假设(均有证据):w$ 不是单位换算而是 Kotlin 的 Any?.toString()(w$ = Co.$_$.je -> module_37036.js:1165 Br()),单位由调用点拼字符串;fq/jq/Y$ 不是样式注册表而是 Kotlin/JS 的类/对象/伴生对象元数据注册(module_37036.js:1249/1257/1266)。浏览器实测(经由 AssetRelay 8130):官方壳与车库主屏渲染、#preloader 已移除、13 条 emotion 类规则注入、字体 BaseFontRegular/Medium 与 Fallback 全 loaded、33/33 图片加载成功、真实中文名(火焰炮/炸弹/橄榄绿/神谕者/十字军/防护×28)、布局尺寸非零(host 1311x986、positionContent 1311x826@0,64、blockParameters 1119x591@96,64、mounted 368x2186)。逐步修掉两个真问题:#app-root 缺官方 PresetContainer(position:absolute;height:100%)导致下游 height:100% 退化成 auto 而塌成 0 高;以及设备块数按官方 case 13 语义合并。回归:CompareContainerDecode 5 帧/26950/0、selftest 1108/9/90、semantic --demo 全部断言通过、compileKotlinJs/jsBrowserProductionWebpack/jsBrowserDistribution 成功。未完成(待搬,附官方出处):_Ln 3D 预览、PWn TankParametersStyle 全部片段、_Wn SuppliesComponentStyle、vGn 块内 improvementArrow/commonBlockForTurretsHulls/resistanceContainer/iconNotificator/SkinsIcon;未实现:sp1 状态选择器、Global 表的 li/a(部分)/input::placeholder;物品单件图标因数据里缺 TankInfoCC(5543162332469864498) 而退化为 defaultDevice.svg(数据缺失,非算法问题)。
2026-09-18 17:01:35 +08:00
TermiNexus 6c36c5e98d feat: M13a 导出官方「型号枚举→图标」表 + 素材中继 + 物品名称/类别中文可展示
A 图标表:从 module_36659.js:5135 的 eA.upf(底盘)与 module_57128.js:840 的 Dn.duk(炮塔)解出 case→形参→实参→i(模块id)→resources.json 的映射(严格走 case→形参名→形参声明行→实参行,不假设 case 号等于实参位置,因为该模块实参表约 1000 项而工厂形参只有 714 个、多出的被丢弃)。自验:用同文件 :907 的 Dn.euk 抗性表 21 case 全命中且本地镜像均存在(去重 20 个文件)。规模:底盘 416 case(未解 1)、炮塔 20、抗性 21、7 格道具 7。B 物品信息:名称来自服务端模型 3935107155251354407(DescriptionModelCC)的 name 字段,类别中文用 lobby.garage.<枚举名>(ARMOR=底盘/WEAPON=炮塔/PAINT=迷彩/DRONE=辅助无人机…),15 件仓库物品名称 9/9、类别中文 9/9 命中;但图标 0/9 —— 承载底盘/炮塔型号的 TankInfoCC(5543162332469864498)在全部 68 个 s2c 帧与模板里 0 次出现,属数据缺失而非算法问题,已如实标注。C 素材中继 client/tools/AssetRelay.mjs(8130):/ 服务客户端产物、/play/static/** 映射 3DTank/web/play/static 本地镜像、正确 MIME 与宽 CORS、防目录穿越(%2e%2e 返回 400)。回归:CompareContainerDecode 5 帧/26950/0、selftest 1108/9/90、semantic --demo 全部断言通过、Kotlin/JS 构建 SUCCESSFUL。
2026-09-18 15:56:00 +08:00
TermiNexus 17083ceff4 feat: M12 车库会话推满 39 帧 + 客户端可进其它空间解码(含 3 种未解上行语义)
A 会话推满:新增驱动策略 PumpDerive(每拍从 ClientState 推上行)——ls.xbl←pt 负数对象 id、ba.lcr←pt 挂载位标记、M_.hcn←devicesLoaded 记录头、Qh.ycw←mountedItems、G.q1wd←AutoResponder 对每条 zt 回 X;队列空而会话未满就 RefillPump 再补一轮;收尾判据=进空间满 12s 且连续 7s 无新下行。结果:39/39 帧全部收到并与服务端 session_002/log.txt 逐条对照 39/39 命中,重放卡住 0,全部 Bad=false(含 28 条 ka 查库应答帧)。诚实口径:触发上行的类型与靶 100% 由已收到下行推出,但「下一步该发哪条」不可从语义推出(回放器只认会话顺序里最靠前的待触发帧),必须靠派生行流轮询补发——已在报告与代码里标注。未解上行语义(file:line 证据):M_.hcn 4795202316771985007 = RequestItem3DPart(靶=空间 id,负载=对象 id,module_85196.js:6760 组帧 / module_18334.js:5767 触发);Qh.ycw 4428603232159847900 = GameObject 级选中/请求(靶=对象 id,空负载,module_85196.js:7290,对应 action sU module_10032.js:6064 无 toString 故人类名未确认);ha.gcq 8060894096836849355 = GarageUpgradeItem(靶=空间 id,负载 itemId+i32 1+期望价+货币 enum,module_85196.js:6879 / module_10032.js:30492 的 GarageUpgradeItemAction)。B 其它空间:新增 SpaceDumpDriver(TANKIC_SPACES=... 控制,默认不影响既有自检),实测 5 个空间全部进得去解得开、Bad 全 false:Entrance 3帧/3记录、Chat 1帧/2记录、Shop 3帧/11记录(zt 150批1731项 + pt 764对象)、Unknown 2帧/11记录、Queue 3帧/5记录。回归:CompareContainerDecode 26950/0、selftest 1108/9/90、semantic.js --demo 全部断言通过、三构建 exit 0。
2026-09-18 14:42:31 +08:00
TermiNexus 3065d23413 feat: M10 挂载位设备可装备(dh.hcu → Mounted.DeviceId → 重启后回显)
上行口径(源码链推出,抓包无样本):dh.hcu = 3804107380183105968,靶 = 挂载位对象 id,负载 = 设备对象 id(i64);证据见 Uplink.kt 的 DH_HCU 头注(module_85196.js:7134-7173 的 dh.hcu、module_10032.js:7692 的 TF 动作、module_18334.js:5926-5927 的 dispatch)。客户端:Uplink.BuildDeviceInsert + ClientState.MarkDeviceMounted(本地乐观)+ GarageSession.EquipDevice;Main.kt 挂载位面板每个可用设备一个「装备 <设备id>」按钮(已装备者置灰);GarageDriver 支持 TANKIC_DEVICE_EQUIP=<槽位id>:<设备id> 自检与 PrintDeviceMountDump。服务端见 2299b09。验收(9092 干净重启):帧 255848→255856B(+8B = i64 正好)、客户端解码 29 条记录 Bad=false;服务端日志「回显覆盖:devicesLoaded 的 mountedDevice 由模板…改为库中…」;MySQL SELECT 槽位 920009631046 DeviceId=920005132727;客户端 dump「槽位 920009631046 设备 23 个 当前装备:920005132727」。换装备:改为 920005770334 后库里只剩后者;重复同值 → 服务端「值未变,幂等跳过」。UI:浏览器点「装备」后该槽位行显示新设备。回归:CompareContainerDecode 5 帧/一致叶子 26950/不一致 0;selftest 逐字节一致 1108/不一致 9/解码中断 90;semantic.js --demo 全部断言通过;三构建全绿。
2026-09-18 14:19:04 +08:00
TermiNexus 356647937e feat: M9 枚举线形序数 enum#N 解析成成员名(category 显示 ARMOR/WEAPON/PAINT…)
数据:evidence.json 的 byModule.<模块>.enums = { 枚举短名: [{field, member, value}] },全局 348 类 / 2633 成员。查表键定为 (字段所属模块, 枚举短名, 序数):同模块内短名天然唯一;跨模块同名 11 个短名 / 9 个同名异义,键含模块号即可隔离。全表 102 条 enum<X> 引用的定义模块均由字段的 TypeCodec 唯一确定(无冲突、未解 0),导出后落盘 77 类 / 1343 成员(33879 字符,整表增量 33894)。两层分离(本任务核心约束):协议层原始值仍是 enum#N,一字不改,因此 CompareContainerDecode.mjs 的逐字段对照口径不变;EnumNames 只在 UI/日志/dump 里翻译,查不到返回 null 并由调用方标「未解」,不猜。category 实测(15 件仓库物品全部解出):ARMOR#1 / WEAPON#0 / PAINT#2 / BAZOOKA#13 / DRONE#8 / INVENTORY#3 / INVISIBLE#9,真实 5 件挂载物品类别互不相同 —— M8 结论不变。UI 与日志显示「成员名(enum#N)」。顺带修正 GarageDriver 接线:上一轮把 URL 硬编码成 9092 且日志 label 写死 store-02,改为环境变量 TANKIC_WS_URL(默认 9091)与 TANKIC_LOG_LABEL(默认 store-03),否则「配对表 ⇔ 服务端日志」会找错目录。验收:CompareContainerDecode 5 帧 / 一致叶子 26950 / 不一致 0(硬门槛);server selftest 逐字节一致 1108 / 不一致 9 / 解码中断 90;三构建全绿;浏览器实测 15/15 类别显示成员名。未解:枚举成员名未覆盖 list<enum<X>>/opt<enum<X>> 的元素级。
2026-09-18 12:59:45 +08:00
TermiNexus 4277e0ae08 feat: M8 查明物品类别来源并实现同类互斥(切武器时旧件自动卸下)
类别来源(完整链):挂载 reducer IB.yr9 用物品的 qr3_1 查类别表;qr3_1 = oB 构造第 4 参 = n.yr6_1.ici_1;yr6_1 来自 pt 里 model 类 Ij 那一项 = modelId 6117306895090150422(module_85196::Lo,字段名就叫 category)。交叉验证:真实 5 件 mountedItems 的类别 {enum#1,enum#0,enum#13,enum#2,enum#8} 互不相同 → 规则成立;并确认 M7 留下的第 6 件 920009631018 与 920009631039 同为 enum#1(都是底盘),正是 M7 缺口的体现。排查并排除:不是本地静态表;GarageSelectItem 的 Yy/Xy/ek/Wy 是另一套枚举(模型 ox/2114179554921095892 的 pr3_1),与挂载互斥无关。实现:schema 加 Items.Category + 索引;新增可重跑的 backfill_category.js(从 ObjectModels 回填,支持 --dry),992 件全部回填;store.SetMounted 改为「先卸同类、再置新件」且幂等,日志打印「同时卸下 N 件同类」;客户端 ClientState.MarkMounted 同步互斥(与 IB.yr9 同顺序),UI 每行显示类别。验收:连装两件 enum#1 → 库中只剩后者(日志「同时卸下 1 件同类」);浏览器 a11y 快照前后对比正确;回归 selftest 1108/9/90、CompareContainerDecode 5 帧 0 不一致、三构建全绿。未解:enum#N 只是线形序数,枚举成员名未解出,库里一律存原始令牌 enum#N。
2026-09-18 12:26:01 +08:00
TermiNexus ab1a905d28 feat: M7 点装备即落库,重启服务端后仍回显(闭合「能切换但没持久化」)
客户端:Uplink 加 M_.fcn 构造(6 条抓包样本逐字节相同)、仓库每行加「装备」按钮、Equip() 入队 + 本地乐观标记(对齐真实客户端的 jU reducer)。服务端:store.js 加 LoadMountedFlags/SetMounted(UPDATE ... WHERE Mounted<>1,幂等)/OverrideMountedRecord;session.js 加可选 RecordOverride 钩子;index.js 进车库时刷新快照、收 M_.fcn 落库。回显只在库与模板有差异时覆盖并打日志(装备前帧仍与抓包逐字节一致 775441B;装备后 775449B,+8B = 多一个 i64)。验收:浏览器点装备 → 服务端落盘 c2s_0064.bin 与抓包 c2s_0141.bin 逐字节相同;MySQL Items.Mounted=1 共 6 行;重启 9092 后启动日志显示 Items.Mounted=1 共 6 件、重进车库 UI 6 个装备按钮置灰与库一致。回归:selftest 1108/9/90、CompareContainerDecode 5 帧 0 不一致、三构建全绿。未实现并如实记录:类别互斥(物品类别来源未解出,不硬猜)、挂载位设备 mountedDevice 未处理。
2026-09-18 12:01:30 +08:00
TermiNexus 8964ea28cd feat(client): M6 车库最小界面(浏览器只读),实测显示 15/28/515/992
browser 分支改为单页界面:连接区(URL 可改)→ 仓库物品(15 件,标注已挂载/未挂载) / 挂载位(28 槽位 + 515 设备,当前装备全 null) / 市场(992 件,只渲染前 20) / 日志区(最近 50 条,人类可读名)。状态更新用「状态容器 + 手动重渲染根节点」(未用 hooks)——全页只有一份全局会话状态。新增 GarageSession.kt(浏览器侧驱动,复用 AutoResponder 与 ClientState 的靶推导);M5 GarageDriver 的 node-only 逻辑(读抓包/日志/NodeRequire)没有拖进浏览器路径。验收:三构建成功(注意 dev 与 prod 系列要分开跑,否则 Gradle 报隐式依赖);浏览器实测已进空间 100073070、面板显示 15/28/515/992、日志含 G.q1wd/LoadObjectsData/ls.xbl/ba.lcr/devicesLoaded;服务端 session_069 佐证浏览器的上行被逐条收到;浏览器 Origin(http://127.0.0.1:8123) 被接受——服务端只记录不校验 Origin(index.js:484-489),当前无需改动。
2026-09-18 11:50:19 +08:00
TermiNexus c0360810b7 feat(client): 协议层 M5 —— 车库因果闭环成立,ClientState 语义状态可 dump
闭环证据(自检 session_063,与服务端日志逐条对照):客户端 48 条上行换来服务端 5 条重放帧(s2c_0000..0004,按整帧字节数逐一匹配)+ 28 条 ba.lcr 的查库定向应答(与服务端 28 行「查库应答」吻合)。靶 id 从客户端语义状态推导、不是照抄抓包:G.q1wd 靶=空间 id;ls.xbl 靶=pt 里 ObjectId 为负的对象(与抓包 5/5 命中);ba.lcr 靶=pt 里带语义标记 modelId 2494628067134376039(自指 + modificationCount=7)与 7391892068905080676(devicesAvailable=true)的对象(与抓包 28/28 命中),且从 s2c_0001 推导出的 28 个挂载位与后到的 s2c_0004 里 28 条 devicesLoaded 记录头 ObjectId 28/28 一致(独立预测成功)。ClientState:itemsOnDepot 15 / itemsOnMarket 992 / mountedItems 5 / 挂载位 28 / 设备 515 / mountedDevice 全 null,全部与期望一致。修复自身缺陷:原先用「最新一帧 pt」推导 URL 会被 s2c_0004 的无标记版本覆盖导致靶集从 28 掉到 0,改为按帧各存对象→模型快照并扫描所有 pt 帧。无跳帧(仅 3 行正常的「重放等待」);三构建全绿;浏览器目标不回归。
2026-09-18 11:37:32 +08:00
TermiNexus 78ce1bacea feat(client): 协议层 M4 —— 上行帧构造与自动应答,形成问答闭环
新增 Uplink.kt:c2s 空间帧构造 [nullmap][靶 objectId i64][methodId i64][负载];ba.lcr(19B)/ls.xbl(19B)/G.q1wd(23B) 的构造结果与抓包逐字节相同(4 条帧全字节比对通过)。新增 AutoResponder.kt:收 LoadDependencies(zt) 自动回 G.q1wd(X),X 原样回传;防重放用「(X, 来源帧序号) 只回一次」而非「X 只回一次」——X 是回调号会被复用(3DTank/tools/server/schema.sql 头注 8),按 X 去重会吞掉合法应答;另加单连接 64 次上限兜底。Connection 加 SendBaLcr/SendLsXbl/SendGQ1wd 手动触发与 LastObjectsDataObjectIds()。口径修正:G.q1wd 的靶是「客户端进入的空间 id」,不是 s2c 记录头上的 objectId(证据:s2c_0000 记录 objectId=390801,而同会话 c2s_0001 靶=100045260)。端到端(服务端 session_055 佐证):客户端自动发出的 c2s_0002.bin 23B 与服务端落盘逐字节一致;注:100045260 的 _loadObjectsData 是模板第 2 帧(进空间即排期 +455ms),不是本次 G.q1wd 因果换来的,且查库定向应答默认 Scope.GQ1wd=false,服务端记为 Disabled/G.q1wd=1,故因果闭环需在有会话回放的 100073070 验证。
2026-09-18 11:13:33 +08:00
TermiNexus 2a0296d0bb feat(client): 协议层 M3 —— 压缩帧解压两端统一走 fflate.unzlibSync
真实抓包里的关键帧是 zlib 压缩的(space-100045260/s2c_0000.bin 整帧 915B → 解压后帧体 1799B;space-100073070/s2c_0004.bin 整帧 41590B → 解压后 255844B),M2 只在 Node 侧用 zlib 解、浏览器目标直接抛错。改为两端同一份纯 JS 实现 fflate 0.8.3,DecodeFrame 保持同步签名。关键修正:fflate 的 inflateSync 是「无包装 DEFLATE」,喂 zlib 流会报 unexpected EOF,zlib 包装要用 unzlibSync —— 官方客户端也正是用它(3DTank/src/module_24635.js:20 g = n.unzlibSync,来自 module_29518.js:1172),与我们同路。验收:Node 侧两个压缩帧的解压结果与 link.js 的 zlib.inflateSync SHA256 完全相同,且解压后完整解出记录(1 条 zt / 1 条 pt + 28 条 fields),M2 对照脚本仍 5 帧、一致叶子 26950 / 不一致 0;浏览器侧页面加载即自动跑自检、console 全项通过(已实际用浏览器打开确认);三个构建任务全绿;production 产物 524579 → 564689 字节(+40110,fflate 的 CJS 入口无法 tree-shake,后续可换 ESM 瘦身)。
2026-09-18 10:31:37 +08:00
TermiNexus 830d6408b8 feat(client): 协议层 M2 —— 容器三布局 lt/pt/zt 落定,多记录帧不再丢记录
客户端实现三种容器布局的读,与 JS 实现(3DTank/tools/server/container.js)逐字段对照:5/5 帧、一致叶子 26950 / 不一致 0,两份规范 JSON SHA256 完全相同(各 795139 B)。覆盖 space-100073070/s2c_0004.bin 的 pt(7210 项,含 515 个 Id=0 字面量与 6695 个模型展开) 与其后 28 条 fields 记录——M1 会把这 28 条全丢掉。协议表由 codec_fields.json 的 1143497 B 裁到 ProtocolTables.json 的 124270 B(削 89.1%),做法:只留解码必需属性 + 闭包裁剪(根=所有带 ModelId 的 codec ∪ 命令/容器方法,沿 TypeCodec 与模型真名取闭包,保留 559/643 个 codec),闭包外如实报「无 codec」。计划外但必需:space-100045260 的 s2c_0000/s2c_0004 是 zlib 压缩帧,M1 直接抛错,现用 Node zlib 解压(browser 侧仍显式抛错);并新增正式产物 client/tools/CompareContainerDecode.mjs。
2026-09-18 10:11:10 +08:00
TermiNexus 2deb628bf6 feat(client): 协议层 M1 —— 入口层握手 + 空间帧记录级解码 + Node 自检
在 client/ 里实现 3DTank 协议层(Kotlin/JS),并给同一工程加 nodejs() 目标做无头自检。自检跑通:Hash Request(571B) → Hash Response(启用链路加密=0)→ Auth/EnterSpace(100045260) → Open Space → 3 条初始化帧(LoadDependencies/zt、LoadObjectsData/pt、gn)→ 保活 ping。字节级核对:Hash Request 与真实抓包 server-logs/store-02/session_040/c2s_0000.bin 逐字节相同;EnterSpace 与 packets/plain/space-100045260/c2s_0000.bin 逐字节相同。M1 明确不做:容器三布局定界、短命令形态、压缩帧解压(遇到即报错,不静默)。协议表由 client/tools/ExportProtocolTables.mjs 从 3DTank/names/ 生成(下行 31 / 上行 7);本机拉不到 Kotlin 插件自带的 yarn,改用系统 npm,故 kotlin-js-store 由 yarn.lock 换成 package-lock.json。
2026-09-18 09:51:00 +08:00
TermiNexus 5644692c97 feat(client): Kotlin/JS + React 客户端脚手架(对齐游戏技术栈)
游戏本体是 Kotlin/JS + kotlin-react + kotlinx.serialization + coroutines,逆向客户端要用同一套栈重写,所以脚手架与它对齐:Kotlin 2.1.21 / kotlin-wrappers 2025.5.11-19.1.0(React 19.1.0)/ kotlinx-serialization-json 1.8.1 / kotlinx-coroutines-core 1.10.2 / kotlinx-html-js 0.12.0;Gradle 8.10.2 走腾讯镜像(services.gradle.org 与 raw.githubusercontent.com 本机不可达)。构建已验证:BUILD SUCCESSFUL,产物 build/dist/js/productionExecutable/TankIcClient.js(261KB)。注:kotlin-wrappers 自 2025 年起只发 React 19.x 的 wrapper,故 React 锁 19.1.0,与游戏的 18.3.1 不同。
2026-09-18 09:30:22 +08:00