feat(3dtank): 解出容器「模式表」头部并约束内层扫描
- ContainerSchema:容器负载头部 = [i32 a][i32 b][i64 objId][i32 N][N × i64 modelId], 这 N 个 modelId 就是容器里出现的模型(登录批次/聊天批次/商店各自对得上) - InnerScan 支持模式表:只认表里的模型且按表内顺序出现,内层「吃满边界」60% → 70%,假命中基本清掉 - 容器负载打印模式表模型名;README/注释补已核实结论与未闭合环节
This commit is contained in:
1 parent
2e5e8bd7b0
commit
8d2aa5566d
2 files changed
+83
-19
No files matched your search
+19
-6
@@ -176,15 +176,28 @@ node ingest.js --label mine-01 # 把一次抓包入库到 packets/pl
|
||||
|
||||
容器的模型恒为 `_t`(`src/module_45686.js:94`),codec 是 `lt`(同文件 483-494),只有一个字段 `idCodec`(i64):
|
||||
读 i64,`0` → 值是紧跟其后的那个 i64;否则按 modelRegister(`module_45686` 的 `at.k1we()`)里该 id 的 codec 就地展开。
|
||||
实测容器负载(`space-100045720/s2c_0000.bin`):
|
||||
|
||||
容器负载 = `[头部][内层数据]`,头部实测形状是**模式表**:
|
||||
|
||||
```
|
||||
[i64 version=1][i64 idA=100045311][i32 N=5][N × i64 modelId][i32 0][i64 idB=100045720]
|
||||
[(i64 methodId)(负载)…] ← 内层与外层帧体同形,且可无限嵌套(内层又是 _t 容器)
|
||||
[i32 a][i32 b][i64 objId][i32 N][N × i64 modelId] 共 20 + 8N 字节
|
||||
```
|
||||
|
||||
**尚未闭合**:负载首字节并不是 modelRegister 的 id(实测是 i64 `1`),说明 methodId 与模型负载之间还有一小段
|
||||
头部(可能是命令 codec 自己写的 tag/版本)。要彻底确定需要拿到容器命令自身的 codec,而不是 `_t` 的模型 codec;
|
||||
在此之前容器内层仍靠 modelId 窗口扫,统计里的"精确覆盖"只作参考。
|
||||
N 个 modelId 就是"容器里出现哪些模型",语义完全对得上:
|
||||
|
||||
| 实测容器 | 模式表 |
|
||||
|---|---|
|
||||
| `space-100045260/s2c_0000`(30 个) | CaptchaCC / RegistrationModelCC / EntranceResourcesCC / ExternalEntranceCC / ServerHaltEntranceCC … (登录+大厅初始化批次) |
|
||||
| `space-100045720/s2c_0000`(5 个) | _t / Kz / ChatCC / ClanChatCC / NewsShowingCC(聊天批次) |
|
||||
| `space-100072911`(2 个) | Jj / ShopCategoryCC(商店) |
|
||||
|
||||
`decode_space_frames.js` 会用这个模式表约束内层扫描(只认表里的模型、且按表里的先后顺序出现),
|
||||
内层记录"吃满边界"的比例从 60% 提到 **70%**,假命中基本清掉。
|
||||
|
||||
**尚未闭合**:头部前 12 字节(`a`/`b`/`objId`)与模型负载之间还没对齐 —— `_t` 的 codec 读到的首个 i64 实测是 `1`,
|
||||
而 modelRegister 里没有 id=1 的条目,说明 methodId 与模型负载之间还有一小段(可能是命令 codec 自己写的 tag/版本);
|
||||
大帧(如 41KB 那个容器)连模式表都认不出,头部形状可能还随场景变化。要彻底闭合需要拿到容器命令自身的 codec,
|
||||
而不是 `_t` 的模型 codec。
|
||||
|
||||
|
||||
|
||||
@@ -30,12 +30,19 @@
|
||||
// id = 读 i64;id == 0 → 值是紧跟其后的那个 i64;否则按 modelRegister 里该 id 对应的 codec 就地展开
|
||||
// (modelRegister = module_45686 的 at.k1we(),k7n(id) 取 codec,与 module_61860 的 or.p7g 同源:
|
||||
// 读不到 codec 会抛 CodecNotFoundForCommandException)
|
||||
// · 实测容器负载(space-100045720/s2c_0000.bin):
|
||||
// [i64 version=1][i64 idA=100045311][i32 N=5][N × i64 modelId][i32 0][i64 idB=100045720]
|
||||
// [(i64 methodId)(负载)…] ← 内层与外层帧体同形,且可无限嵌套(内层又是 _t 容器)
|
||||
// · 仍缺的一环:负载首字节并不是 modelRegister 的 id(实测是 i64 1),说明 methodId 与模型负载之间
|
||||
// 还有一小段头部(可能是命令 codec 自己写的 tag/版本),因此容器内层目前仍靠 modelId 窗口扫,
|
||||
// 不敢按上面的形状硬解。要闭合需要拿到容器命令自身的 codec(不是 _t 的模型 codec)。
|
||||
// · 容器负载 = [头部][内层数据],头部实测形状(见 ContainerSchema):
|
||||
// [i32 a][i32 b][i64 objId][i32 N][N × i64 modelId] 共 20 + 8N 字节
|
||||
// N 个 modelId 是「容器里出现哪些模型」的模式表,实测语义完全对得上,例如
|
||||
// space-100045260/s2c_0000 的 30 个 = CaptchaCC / RegistrationModelCC / EntranceResourcesCC / …(登录+大厅初始化批次)
|
||||
// space-100045720/s2c_0000 的 5 个 = _t / Kz / ChatCC / ClanChatCC / NewsShowingCC(聊天批次)
|
||||
// space-100072911 的 2 个 = Jj / ShopCategoryCC(商店)
|
||||
// 另一个 8 个 = _t / bj / PassToShopCC / uj / oj / aj / DelayMountCategoryCC / MountedResistancesCC
|
||||
// · 认出模式表后:内层只接受表里的模型、且按表里的先后顺序出现(把假命中几乎清干净,
|
||||
// 内层记录「吃满边界」的比例从 60% 提到 70%)。
|
||||
// · 仍缺的一环:负载开头那几个字节(头部前 12 字节的 a/b/objId)与「模型负载」之间还没对齐——
|
||||
// _t 的 codec 读的首个 i64 实测是 1,而 modelRegister 里没有 id=1 的条目,说明 methodId 与模型负载之间
|
||||
// 还有一小段(可能是命令 codec 自己写的 tag/版本);大帧(如 41KB 那个容器)连模式表都认不出,
|
||||
// 头部形状可能还随场景变化。要彻底闭合需要拿到容器命令自身的 codec,而不是 _t 的模型 codec。
|
||||
// 容器字节归属统计里的「精确覆盖」= 能自洽解到下一个 modelId 候选的字节占比,仅作参考。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -66,6 +73,8 @@ for (const R of Table.Rows) {
|
||||
|
||||
// modelId(hex 8B) → 模型信息(内层记录用;来源 names/regdump.json 的 u7n_1/w7n_1)
|
||||
const ByModelHex = new Map();
|
||||
// modelId(十进制串) → 模型信息(容器「模式表」校验用,含没有 codec 的模型)
|
||||
const ByModelId = new Map();
|
||||
const ModelTablePath = path.join(PROJECT, 'names', 'method_table.json');
|
||||
try {
|
||||
const Reg = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'regdump.json'), 'utf8'));
|
||||
@@ -80,6 +89,7 @@ try {
|
||||
Buf.writeBigInt64BE(BigInt(E.key));
|
||||
const Rec = { ModelId: E.key, Model: E.ctor, ModelFields: E.keys, Codec: Ww.ctor || '', CodecFields: Ww.keys || [] };
|
||||
ByModelHex.set(Buf.toString('hex'), Rec);
|
||||
ByModelId.set(String(E.key), Rec);
|
||||
Rows.push(Rec);
|
||||
}
|
||||
fs.writeFileSync(path.join(PROJECT, 'names', 'model_table.json'), JSON.stringify({ Note: 'modelId → 模型类/codec(内层记录用)', Count: Rows.length, Rows }, null, 1), 'utf8');
|
||||
@@ -135,16 +145,20 @@ function WalkRecords(Buf, Start) {
|
||||
}
|
||||
const C = CodecByMethod.get(String(Info.MethodId));
|
||||
if (!C) {
|
||||
// 没有对应 codec:负载当容器处理。容器内的元素以 modelId 开头,已解出的就用字段表展开,
|
||||
// 没解出的按「下一个命中方法表的窗口」兜底跳过去,保证还能继续往下走。
|
||||
// 没有对应 codec:负载当容器处理。容器头部带「模式表」(出现哪些模型),
|
||||
// 内层元素以 modelId 开头,已解出的就用字段表展开,没解出的按「下一个命中方法表的窗口」兜底跳过去。
|
||||
const Next = NextMethodHit(Buf, At + 8 + 1);
|
||||
const Head = ContainerSchema(Buf, At + 8);
|
||||
// 认出模式表就从表后开始扫(表里的 id 本身不是数据记录),认不出就退回从负载开头扫
|
||||
const From = Head ? Math.min(Head.End, Next) : At + 8;
|
||||
Out.push({
|
||||
Off: At,
|
||||
Model: Info.Model,
|
||||
Codec: Info.Codec,
|
||||
Container: true,
|
||||
Len: Next - At - 8,
|
||||
Inner: InnerScan(Buf, At + 8, Next),
|
||||
Schema: Head ? Head.Schema : null,
|
||||
Inner: InnerScan(Buf, From, Next, Head ? Head.Schema : null),
|
||||
});
|
||||
At = Next;
|
||||
continue;
|
||||
@@ -175,14 +189,43 @@ function WalkRecords(Buf, Start) {
|
||||
return { Records: Out, End: At, Ok: Ok && At === Buf.length };
|
||||
}
|
||||
|
||||
// 容器头部:实测形状 [i32 a][i32 b][i64 objId][i32 N][N × i64 modelId](共 20 + 8N 字节)
|
||||
// a/b 是两个小整数(实测 0/1、1/1、1/42 等),objId 是对象/空间族 id(如 100045311、920004789023)
|
||||
// N 个 modelId 是「容器里出现哪些模型」的模式表(实测 space-100045720/s2c_0000 的 5 个 id
|
||||
// = _t + ChatCC + ClanChatCC + NewsShowingCC 等,正是内层记录用的模型)。
|
||||
// 校验不过就返回 null —— 不猜,宁可按老办法窗口扫。
|
||||
// 判据只依赖「N 个 id 全部是已注册模型」:509 个已知 id 命中 64 个连号的概率可忽略。
|
||||
function ContainerSchema(Buf, Off) {
|
||||
if (Off + 20 > Buf.length) return null;
|
||||
const N = Buf.readInt32BE(Off + 16);
|
||||
if (N <= 0 || N > 64 || Off + 20 + 8 * N > Buf.length) return null;
|
||||
const Schema = [];
|
||||
for (let K = 0; K < N; K++) {
|
||||
const Id = Buf.readBigInt64BE(Off + 20 + 8 * K).toString();
|
||||
if (!ByModelId.has(Id)) return null;
|
||||
Schema.push(Id);
|
||||
}
|
||||
return { Schema, End: Off + 20 + 8 * N };
|
||||
}
|
||||
|
||||
// 在容器负载里扫 [modelId 8B] 候选,并用「解出来正好落到下一个候选处」过滤假命中
|
||||
// 容器负载实测结构: [头部 N 字节(序号/版本/spaceId/实体 id 等)] [ (modelId 8B)(负载) … ]
|
||||
// 容器负载实测结构: [头部(含模式表)] [ (modelId 8B)(负载) … ]
|
||||
// 真记录的字段一定解到下一个 modelId 开头,假命中(modelId 字节嵌在别的数据里)解不到。
|
||||
function InnerScan(Buf, From, To) {
|
||||
// 有模式表时只用表里的模型、且按表里的先后顺序出现,假命中几乎绝迹。
|
||||
function InnerScan(Buf, From, To, Schema) {
|
||||
const Allowed = Schema ? new Set(Schema) : null;
|
||||
const Hits = [];
|
||||
let LastRank = -1;
|
||||
for (let Off = From; Off + 8 <= To; Off++) {
|
||||
const C = CodecByModelId.get(Buf.readBigInt64BE(Off).toString());
|
||||
if (C) Hits.push({ Off, C });
|
||||
const Id = Buf.readBigInt64BE(Off).toString();
|
||||
const C = CodecByModelId.get(Id);
|
||||
if (!C) continue;
|
||||
if (Allowed) {
|
||||
const Rank = Schema.indexOf(Id);
|
||||
if (Rank < 0 || Rank < LastRank) continue;
|
||||
LastRank = Rank;
|
||||
}
|
||||
Hits.push({ Off, C });
|
||||
}
|
||||
const Out = [];
|
||||
let ExactCover = 0;
|
||||
@@ -386,6 +429,14 @@ for (const Dir of Dirs) {
|
||||
'记录 ' + In.Records.length + ' 条(吃满边界的 ' + In.Records.filter((X) => X.Exact).length + ' 条)' +
|
||||
' 精确覆盖 ' + In.ExactCover + '/' + In.Span + 'B' + (In.Tail ? ' 尾部余 ' + In.Tail + 'B' : ''),
|
||||
);
|
||||
if (Rec.Schema) {
|
||||
const Names = Rec.Schema.map((Id) => {
|
||||
const R = ByModelId.get(Id) || {};
|
||||
const C = CodecByModelId.get(Id);
|
||||
return (C && C.ModelName) || R.Model || Id;
|
||||
});
|
||||
Lines.push(' 模式表(' + Rec.Schema.length + '): ' + Names.join(', '));
|
||||
}
|
||||
for (const Ri of In.Records) {
|
||||
Lines.push(
|
||||
' ↳ @' + String(Ri.Off).padStart(6) + ' ' + String(Ri.ModelName || Ri.ModelId).padEnd(28) +
|
||||
|
||||
Reference in new issue
Block a user