docs(3DTank): 电磁炮锁定蓄力改造的计划与进度(机制/门槛/改动/待办/教训)

含:z4t 三条上行(amr/o1ag/tnl)、三个时长(w1al/w187/x187)、FSM(TWt/vWt)结构、实测门槛(1.5s 最低满伤)、已落地改动与 commit 列表、离线解码流水线命令、关键行号速查。
同批提交 decode_space_frames.js 的 --side s2c/c2s 选项(MD 里的解码命令依赖它)。
This commit is contained in:
TermiNexus committed 2026-10-12 11:20:34 +08:00
1 parent 651e29f8b2
commit 12e547f32f
2 files changed
+183 -1

No files matched your search

+177
View File
@@ -0,0 +1,177 @@
# 电磁炮(RAILGUN / `z4t`)锁定蓄力「加速」改造 —— 计划与进度
> 状态:**功能已可用(锁定 3~5s → 1.5s,满伤,面板可实时调)**;**归因(离线解码)待做**。
> 本文档是「日后再说」的交接手记:机制、实测门槛、已落地改动、待办与命令、踩过的坑。
---
## 0. 一句话目标
原版:按住开火 → **锁定/蓄力 3~5s** → 才发(满伤)。
目标:缩短这段等待,**且必须保持满伤** —— 服务端会按「锁定开始 → 开火」的时间差裁决。
**当前结果**:锁定/蓄力 **1.5s**(实测满伤的最小值),比我方原版快 2~3 倍,且可在 Alt+T 面板实时调。
---
## 1. 已确证的机制(含证据)
### 1.1 玩家坦克的武器组件 = `z4t`(**不是** `xen`)
- 现场探针实测:`__gameMode__.y100_1.oyt_1.c2a_1.93`,`ctor = function z4t()`,字段含 `h1ah_1 / i1ah_1 / l1ah_1 / o1ah_1 / w1ah_1 …`
- `xen`(代码自标注 `railgunWeapon`)**不在玩家车上** —— 早期改它是空转(教训)。
### 1.2 三条相关上行命令
| 命令 | 语义 | 触发链 |
|---|---|---|
| `amr` | **普通弹**(点一下) | `J6t.z1aq → z4t.z1al → UYt → N4t.amr` |
| `o1ag` | **锁定弹**(长按/蓄力) | `K6t.z1aq → z4t.a1am(目标) → Q6t → N4t.o1ag` |
| `tnl` | **锁定开始**(无载荷、只带坦克 id) | 进 `vWt` 锁定态时 `iyz(new Mdn(n,i,b189_1))` → `N4t`:`n.h18v_1 && P4t(t).tnl(...)` |
- 走 `o1ag` 还是 `amr`,由 `V6t` 里 **「按下时刻 + 阈值」** 判定:`W6t().a1ar_1 = Hj(0.2)`(按住 ≥0.2s → `A6t → K6t`;否则 `R6t → J6t`)。
- `tnl` **只带坦克 id** ⇒ 服务端只能**用自己收到它的时刻**计时 ⇒ 这是「短于门槛就没伤害」的根因。
### 1.3 三个时长参数(都在 `k4t(this)` = `gaussParams` 组件上)
| 方法 | 原式 | 语义 | 实测 |
|---|---|---|---|
| `w1al()` | `LR(0.001 * ojs_1)` | 带锁定弹的**冷却/装填** | ~6.6s(探针采样 `o1ah_1` 从 0 涨到 1) |
| **`w187()`** | `LR(0.001 * njs_1)` | **锁定/蓄力时长** ★ 改这里 | 原版 3~5s |
| `x187()` | `LR(0.001 * mjs_1)` | 另一条 | 未仔细测 |
| (普通弹那支) | `X6t.l1ar_1 ← L6t(i1ah_1)` | 普通弹冷却 | `i1ah_1 = 1.639` |
### 1.4 状态机结构
- `z4t.y1ah` 建 FSM:`e=G6t(Idle) / u=Z6t / s=V6t(按下) / o=J6t(普通弹) / _=K6t(锁定弹) / a=X6t(累积) / h=Y6t / l=$4t(t)=TWt`
- `TWt` = `new L9("StrikerTargetSelection")`(内嵌目标选择 FSM);其**内部 `vWt` 状态用 `w187()` 当累积上限**:
`y188_1` 从 `w187()` 递减到 0 → 发 `bWt`(选中目标)→ `K6t.z1aq()` → `a1am(目标)` → `Q6t` → 上行 `o1ag`。
- `z4t.t187(y188_1 / w187())` 写出的 `o1ah_1` 就是 **HUD 上那条倒计时/进度条**。
---
## 2. 实测门槛(二分结果)
| 锁定值 | 结果 |
|---|---|
| 原版 3~5s | 满伤 |
| 2.0s | 满伤 |
| **1.5s** | **满伤(当前采用值)** |
| 1.25s | 用户实测:不行 ⇒ **1.5s 为最低可用值** |
| 1.0s / 0.1s | 无伤害(另一号**完全看不到弹药** ⇒ 服务端整发拒收) |
结论:**服务端对「`tnl` → 开火」的时间差有硬门槛,约 1.5s。**
---
## 3. 已落地改动
### 3.1 游戏侧(`3dtank.com/play/static/js/unpacked/module_87072.js`)
```js
(m$(z4t).w187 = function () {
// HORNET-TAG: 电磁炮 锁定/蓄力时长 = 面板默认 1.5s(可实时调) (z4t.w187)
var O = window.__hornetLockSec__;
if (typeof O === 'number' && O > 0) return LR(O); // 运行时覆盖
var t = k4t(this).njs_1; // 回退原版参数
return LR(0.001 * t);
}),
```
### 3.2 面板(`tools/hornet_panel.js` → 经 `python build_bundle.py` 进 bundle)
- Alt+T → **「电磁炮设置」** → **启用时长覆盖(checkbox,默认开)** + **锁定/蓄力(s) 滑块 0.1~5.0 / 步进 0.05**
- 拖动**立即生效**(`window.__hornetLockSec__`),持久化到 `localStorage`:`hornet_lock_on` / `hornet_lock_sec`
- **取消勾选 = 回原版**(3~5s)
- 控制台等价:`__hornetLockSec__ = 1.25` / 恢复原版 `__hornetLockSec__ = undefined`
### 3.3 观测/调试设施(本次会话顺带产出)
| 设施 | 位置 | 说明 |
|---|---|---|
| 版本徽标 | `3dtank.com/play/static/js/main.9f4e7a89.js` | 右下角显示「包版本 + 从 `__modules__[87072]` 源码正则读出的 `HORNET-TAG`」(防「改了没生效」);每 2s 抗 DOM 裁剪自愈 |
| 探针 | `sockets/probe_railgun.js` | 组件清单 + 300ms 采样 + **整帧** WS 上行记录;自动加载(等 `__gameMode__`),`localStorage.hornetProbe='0'` 关 |
| 转储入口 | `3DTank/tools/serve.js` | `POST /__probe__` → 追加写 `3DTank/names/probe_railgun.log`;`GET /probe_railgun.js` 提供探针 |
| 解码供给 | `3DTank/tools/probe_log_to_bins.js` | 日志 → `packets/probe/<label>/session_001/c2s_*.bin` |
### 3.4 本次会话的 commit(时间顺序)
```
09f8769 feat(mirror): 电磁炮锁定时长 3~5s → 2.0s(z4t.w187,实测满伤)
bb3d761 feat(mirror): 右下角版本徽标(包版本 + 从模块源码读出的 HORNET-TAG)
712d24e fix(mirror): 徽标抗 DOM 裁剪 + 探针块加自证日志
09f8769…(同上)
dfdd69b feat(mirror): 探针自动加载并转储(/probe_railgun.js 路由 + 加载器引入)
e154c1a fix(sockets): 探针 WS 字节读取兼容 DataView(此前 hex 恒为空)
8e13aac feat(mirror): 电磁炮锁定时长 2.0s → 1.5s(二分探门槛)
2d2a2c9 chore(mirror): 电磁炮锁定时长试 0.1s(极端对照)
1af3b3a feat(mirror): 电磁炮锁定时长支持运行时覆盖(window.__hornetLockSec__)
db8a92a feat(panel): Alt+T 面板新增「电磁炮设置」
d4c8a84 feat(panel): 电磁炮锁定时长固定默认 1.5s(面板默认开启覆盖)
651e29f feat: 探针改整帧 hex 记录 + 新增 probe_log_to_bins.js
```
---
## 4. 待办(日后接着做)
### 4.1 采一轮整帧数据(需要人工在游戏里操作一次)
1. F5(探针自动加载;徽标第二行应为「面板默认 1.5s(可实时调)」)
2. 进战场 → 打 **2~3 发蓄力弹**(最好有一次「选中坦克并打中」)
3. 跑流水线:
```bat
node 3DTank\tools\probe_log_to_bins.js
node 3DTank\tools\decrypt_frames.js --dir 3DTank\packets\probe --label probe-01
node 3DTank\tools\dump_plain.js --session probe-01 --out 3DTank\packets\plain\probe-01
node 3DTank\tools\decode_space_frames.js --dir 3DTank\packets\plain\probe-01 --side c2s --fields
```
### 4.2 归因目标(两轮对比)
- **1.5s 轮**(满伤)与 **0.1s 轮**(无伤害)的**上行命令序列**对比:
- 命令集合/顺序**一致**(只是时间被压缩)⇒ 服务端按时长裁决(目前已基本确认,只差这最后一步铁证);
- 命令**不一致** ⇒ 客户端侧问题(那一发压根没发出去 / 参数没就绪),改客户端那一步。
### 4.3 可选优化方向
- 查服务端门槛的**确切值**(1.25s 不行、1.5s 行 ⇒ 门槛在 (1.25, 1.5] 之间,可用面板滑块继续夹)。
- 若想让服务端「以为」锁更久:需要把 `tnl` **提前**发 —— 但**别用假载荷**(见 5.2 教训)。
- 冷却那一支(`w1al()` ≈ 6.6s / `x187()`)若也想缩短,需先确认服务端是否同样按它裁决(大概率是)。
---
## 5. 教训与注意事项(★ 重要)
1. **不能动冷却**:`X6t` / `l1ar_1`(普通弹 `i1ah_1=1.639`;锁定弹 `w1al()`≈6.6s)—— 服务端按它裁决;
短路它 = **出弹但无伤害**(我第一次就踩了这个)。
2. **别用假载荷预置 `tnl`**:我一度在冷却开始时提前发 `Mdn(0,0,!0)` → 那一轮**连弹药都看不到**(整发被拒),已回滚。
3. **开关只能用 `localStorage`**:`location.hash` 会被游戏当路由(**进不去战场**);查询串会被当 config 参数(**握手 1005**)。
4. **改完必须走对应构建**:
- 改 `3dtank.com/.../unpacked/module_*.js` → `node tools/pack_unpacked.js`(单包)
- 改 `tools/hornet_*.js` → `python build_bundle.py`(hornet bundle)
- `serve.js` 只有自身改动才需重启(`Start-LocalAndRelay.ps1 -KillExisting`)
5. **面板滑块实时生效的前提**:`z4t.w187()` 只在**每次开始锁定时**被读 ⇒ 改值影响**下一发**,当前这发不变。
6. **探针整帧记录是硬要求**:流密码状态跨消息连续,截断/漏帧会让后续解密**全错**。
7. **验证版本永远看徽标第二行**(`HORNET-TAG`),不要凭手感判断「改了没生效」。
---
## 6. 关键行号速查(`3dtank.com/play/static/js/unpacked/module_87072.js`)
| 内容 | 行 |
|---|---|
| `z4t.w1al/w187/x187`(三个时长) | ~92824-92839 |
| `z4t.z1al`(普通弹开火) | ~93026 |
| `z4t.a1am`(锁定弹开火) | ~93038 |
| `z4t.y1ah`(FSM 构建 + `TWt` 实例化) | ~92862-92910 |
| `V6t`(0.2s 阈值判定走哪条) | ~93601-93619 |
| `X6t`(累积/冷却) | ~93633-93652 |
| `K6t.z1aq`(锁定弹入口 → `a1am`) | ~93620 |
| `J6t.z1aq`(普通弹入口 → `z1al`) | ~93664 |
| `TWt` = `new L9("StrikerTargetSelection")` | ~23375 |
| `vWt`(用 `w187()` 累积 → `bWt`) | ~83807-83826 |
| `Mdn` 定义(`h18v_1` = 第 3 个参数) | ~34869 |
| `N4t`(`tnl`/`amr`/`o1ag` 上行分发) | ~93103-93153 |
| `PYt`(lockingServerInterface 窗口,与本案无关) | ~86175-86216 |
+6 -1
View File
@@ -360,6 +360,11 @@ function DecodeFrame(File, Raw) {
} }
const DirArg = Arg('dir', ''); const DirArg = Arg('dir', '');
// --side:both(默认)| s2c | c2s —— 上/下行帧结构相同([nullmap][记录 = objectId+methodId+负载]),
// 只是文件名区分方向;解上行(c2s = 本机客户端发出去的)时用 --side c2s。
const Side = String(Arg('side', 'both')).toLowerCase();
const FileRe =
Side === 'c2s' ? /^c2s_\d+\.bin$/ : Side === 's2c' ? /^s2c_\d+\.bin$/ : /^(c2s|s2c)_\d+\.bin$/;
const Dirs = []; const Dirs = [];
if (All || !DirArg) { if (All || !DirArg) {
const Root = path.join(PROJECT, 'packets', 'plain'); const Root = path.join(PROJECT, 'packets', 'plain');
@@ -382,7 +387,7 @@ let StringFields = 0;
const Lines = []; const Lines = [];
for (const Dir of Dirs) { for (const Dir of Dirs) {
const Files = fs.readdirSync(Dir).filter((F) => /^s2c_\d+\.bin$/.test(F)).sort(); const Files = fs.readdirSync(Dir).filter((F) => FileRe.test(F)).sort();
if (!Files.length) continue; if (!Files.length) continue;
Lines.push('===== ' + path.basename(Dir) + ' (' + Files.length + ' 帧)'); Lines.push('===== ' + path.basename(Dir) + ' (' + Files.length + ' 帧)');
for (const F of Files) { for (const F of Files) {