工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。 服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。 同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。 src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
456 lines
23 KiB
JavaScript
456 lines
23 KiB
JavaScript
// container.js - 容器命令(modelService 那几条)的读/写
|
||
//
|
||
// 出处:src/module_45686.js 的三个容器 codec + src/module_29554.js 的 4 条命令封装,
|
||
// 再逐条用 `packets/plain/space-<id>/*.bin` 的真实明文帧「解出来再写回去」逐字节标定。
|
||
//
|
||
// ---------------------------------------------------------------------------
|
||
// 一、哪个方法用哪个布局:**由 methodId 决定**,不再靠「哪个布局恰好吃到帧尾」
|
||
// 依据(三处互相印证):
|
||
// src/module_29554.js:79-83 4 条 modelService 方法的长整型常量(x1wc_1/y1wd_1/…)
|
||
// src/module_29554.js:239-244 taw() 里给每条方法挂的负载 codec:
|
||
// _loadDependencies(3216143066888387731) → new _(o(K), !1) K = ObjectsDependencies
|
||
// _loadObjectsData (7640916300855664666) → new _(o(W), !1) W = ObjectsData
|
||
// _unloadObjects (9038756763225879447) → new a(new _(o(h), !1), !1)
|
||
// src/module_45686.js:600 把三个 codec 注册给三个模型类:
|
||
// m(Q)=ModelData → lt,m(tt)=ObjectsData → pt,m(nt)=ObjectsDependencies → zt
|
||
// names/downlink_table.json 的 Containers 段与方法表一致(Independent 第二证据)。
|
||
// ★ 旧实现按「三布局都试、取恰好吃到帧尾的那个」判定,这在**多记录帧**上必然失败:
|
||
// 容器记录的负载只占帧体的一段,后面还有别的记录(semantic.js 传给我们的 End 是帧尾)。
|
||
// 实例:space-920007382011/s2c_0001.bin = nullmap(2B) + pt 记录(16+286) + 第 3 条记录(16+16),
|
||
// pt 负载 286B 而帧体 336B,「吃到帧尾」永远不成立 → 旧实现把整帧当 hex 留着。
|
||
//
|
||
// 二、三个布局的负载形状(源码行号逐个给出)
|
||
// lt(_unloadObjects)= [varint 个数][个数 × i64 对象 id]
|
||
// 依据:module_29554.js:244 的 `new a(new _(o(h), !1), !1)`:a = ListCodecInfo
|
||
// (module_24635.js:877-882 的 wn,`$_$.c`),元素类型 = 类 h 的 codec;
|
||
// h = r.$_$.r 而 r = i(61860)(module_29554.js 尾部依赖表)→ module_61860 的导出 r = xr
|
||
// (module_61860.js 导出表 `t.$_$.r = xr`),xr 的 codec 只写一次 i64
|
||
// (extract_codec_fields 的 WireClasses:`"xr":"i64"`,来自 module_61860.js 的 `m7f(G(xr), new cr())`)。
|
||
// 标定:space-100073070/s2c_0002.bin 帧体 126B = nullmap(1) + 5 × (16B 头 + 9B 负载),
|
||
// 每个负载 = `01` + 8B 对象 id(如 01 ffffffff80000744)→ 5×25+1 = 126 精确吃满。
|
||
// pt(_loadObjectsData)= [i32 C][C × (i64 objId, i64 gameTypeId)][i32 D][D × LtItem]
|
||
// 依据:module_45686.js:531-556 的 pt.p7g:g1wc_1 = ft 列表(module_45686.js:111-120,
|
||
// 两次 a7g_1.q2u)、f1wc_1 = lt.p7g 列表;两处计数都是 c7g_1.p2u(= i32,
|
||
// module_72834.js:278-280 p2u → t2t → getInt32)。
|
||
// LtItem(pt 里的一项,= ModelData 的 lt codec,注意与上面 lt 布局同名但不同物)
|
||
// = [i64 id];id == 0 → 再读一个 i64 当值;否则 id 是 modelId,
|
||
// 就地展开该模型的字段。依据 module_45686.js:487-494 的 lt.p7g。
|
||
// zt(_loadDependencies)= [i32 X][i32 C][C × {i64 ObjId, i32 N, N × i64 ModelId}]
|
||
// [i32 D][D × {i64 R, i16 E, i16 U, i64 S, i8 O, i8 Skip, Skip × i64}]
|
||
// 依据:module_45686.js:563-596 的 zt.p7g(X = c7g_1.p2u;kt = module_45686.js:190-205;
|
||
// $t = module_45686.js:213-228 依次 q2u/o2u/o2u/q2u/布尔位;跳过个数是 i.n2u = 1 字节
|
||
// (module_72834.js:272-274 n2u → xy → getInt8),不是 varint)。
|
||
//
|
||
// 三、opt 字段与记录形态位共用 nullmap 比特流(这是旧实现解不动的第二个根因)
|
||
// 位图 = 「一条记录 1 个形态位」+「负载里每个 opt 字段 1 位」,按同一顺序消费:
|
||
// src/module_61860.js:2116-2119 形态位(b7g_1.xy())
|
||
// src/module_24635.js:412-417 opt 位(Ht.p7g/o7g)
|
||
// src/module_24635.js:809-845 nullmap 灌进位读取器(r7f → j7h(c, n.b7g_1))
|
||
// src/module_61860.js:1022-1038 记录链循环(有字节就一直解下一条、逐条派发)
|
||
// 所以容器负载里的叶子要按 **bit** 读 opt,且必须先把「本条记录之前」的位消费掉
|
||
// (见 ResolveCursor 的注释)。位游标拿不到时,容器负载里的 opt 一律如实报不可解,
|
||
// 绝不退回「按字节猜标志位」——那会把负载整体解错位。
|
||
//
|
||
// 四、codec_fields 里推不出嵌套 codec 的裸类名(16 个 token)里,资源类按「资源引用 = i64」解
|
||
// 依据:module_71105.js 的资源类型注册块(`r.u83(typeId, si(类), 工厂)`,typeId 4/7/8/9/10/11/17/25/26)
|
||
// + module_71105.js `i.m7f(si(Gf), i.p7f(si(pv)))`(Gf 与 pv 共用同一个 codec);
|
||
// 资源实例是 `Zu(i16 typeId, …, i64 key, …)`(module_71105.js 的 Zu 构造器 = module_45686.js:226
|
||
// 的 L,读序 i64/i16/i16/i64/bool),查资源用 i64 key(module_45686.js:209 qt() → resourceRegistry.x83)。
|
||
// 实测标定:space-920007382011/s2c_0001.bin 的 MatchmakingLayoutCC 按 pv=i64 解出的字段完全自洽
|
||
// (两个 opt<pv> 取到同一个资源 key、holidayDescription/holidayTitle 是长度精确吻合的 UTF-8、
|
||
// matchmakingModeRanks 的枚举 0..10 递增),且负载恰好止于 286B、下一条记录头紧随其后。
|
||
// 没进这张表的裸类名(Pc / Ea / nh …)**一律不猜**:解到就如实报「无 codec」。
|
||
'use strict';
|
||
const Link = require('./link');
|
||
const { MakeReader } = require('../lib_space_codec');
|
||
const { MakeWriter } = require('./codec');
|
||
|
||
const LAYOUT = { Lt: 'lt', Pt: 'pt', Zt: 'zt' };
|
||
|
||
// methodId(十进制字符串,与 names/downlink_table.json 的 Containers 完全一致)→ 负载布局
|
||
const LAYOUT_BY_METHOD = {
|
||
'3216143066888387731': LAYOUT.Zt, // ModelService.LoadDependencies
|
||
'7640916300855664666': LAYOUT.Pt, // ModelService.LoadObjectsData
|
||
'9038756763225879447': LAYOUT.Lt, // ModelService.UnloadObjects
|
||
};
|
||
|
||
// 资源类(module_71105.js 的资源类型注册块 + Gf 复用 pv 的 codec)→ 线上一律是 i64 引用。
|
||
// 只列有源码出处的那几个;表外的一律不猜。
|
||
const RESOURCE_CLASS_WIRE = {
|
||
Ea: 'i64',
|
||
nh: 'i64',
|
||
W_: 'i64',
|
||
uo: 'i64',
|
||
pv: 'i64',
|
||
B_: 'i64',
|
||
Da: 'i64',
|
||
qs: 'i64',
|
||
uh: 'i64',
|
||
Gf: 'i64',
|
||
kv: 'i64',
|
||
// Pc:**源码侧没定位到 codec**(codec_fields 里 Pc 没有 codec;module_47243.js:180 的
|
||
// `ji = u.$_$.l2` 指向 module_52943 的 Pc,而那是 `function Pc() {}` 空壳,没有 q7g/p7g)。
|
||
// 这里是**实测标定**:space-100045260/s2c_0001.bin 的 LoaderBackgroundModelCC
|
||
// 三个字段按 (Gf=i64, Pc=i64, Pc=i64) 解出的值分别是
|
||
// 1788178032155 / 1790283600000 / 1788469200000
|
||
// —— 全是 13 位毫秒时间戳,且 startDate < endDate、区间与帧采集日期自洽;同时 pt 负载
|
||
// 精确止于 448B(第 5..11 项、共 11 项全部落在合法边界上)。按 4 字节解会在下一项 id 处错位。
|
||
// 结论:Pc 是毫秒时间戳引用 → i64。这条不是源码证据,单独标出来。
|
||
Pc: 'i64',
|
||
};
|
||
|
||
// nullmap 的位图(与 semantic.js:46-58 同一口径,依据 src/module_24635.js:640-695 的 j7h):
|
||
// 长形态(首字节 bit7=1):位图 = 标签之后的字节按 MSB 先读
|
||
// 短形态:整串按 MSB 先读、跳过前 3 个 bit(那 3 位是长度标签)
|
||
function NullmapBits(Bytes) {
|
||
const Bits = [];
|
||
const Push = (B) => {
|
||
for (let K = 7; K >= 0; K--) Bits.push((B >> K) & 1);
|
||
};
|
||
if (Bytes.length && Bytes[0] & 0x80) {
|
||
for (let I = Bytes[0] & 0x40 ? 3 : 1; I < Bytes.length; I++) Push(Bytes[I]);
|
||
} else {
|
||
for (const B of Bytes) Push(B);
|
||
Bits.splice(0, 3);
|
||
}
|
||
return Bits;
|
||
}
|
||
|
||
function MakeContainerCodec(Tables, Reader, Writer, Options) {
|
||
const Opts = Options || {};
|
||
const MaxCount = Opts.MaxCount || 1 << 20; // 计数上限,超出即认定布局不成立
|
||
|
||
// 容器负载专用的读写器:opt 按 nullmap 的 bit 读/写(见文件头第三节),
|
||
// 并带上「裸类名 → 线上基础类型」直译表。语义层原来给的那两个实例保持原样不动
|
||
// (它们服务的是字段表那条路径,口径本来就不同)。
|
||
// 直译表 = codec_fields.json 的 ResourceClasses(由源码 u83 注册块导出,见 extract_codec_fields.js)
|
||
// + 上面手写的 RESOURCE_CLASS_WIRE(手写表优先:里面有 Pc 这种「实测标定」的条目)。
|
||
// 为什么要合并:原来只认手写的 10 个资源类,漏了 typeId 13 的 no(与 nh/B_/pv/kv/Da/W_/uo/Ea/qs/uh 同块注册),
|
||
// 于是 space-100072911/s2c_0001 的 DonationAlertCC.imageWithEmail 报「类型 no 没有对应 codec」。
|
||
const Alias = {};
|
||
for (const [N, R] of Object.entries(Tables.ResourceClasses || {})) Alias[N] = R.Wire;
|
||
for (const [N, W] of Object.entries(RESOURCE_CLASS_WIRE)) Alias[N] = W;
|
||
const LeafReader = MakeReader(Tables, {
|
||
EnumBytes: Reader.EnumBytes,
|
||
OptBits: true,
|
||
TypeAlias: Alias,
|
||
StringLiteralOnly: true,
|
||
});
|
||
const LeafWriter = MakeWriter(Tables, {
|
||
EnumBytes: Writer.EnumBytes,
|
||
OptBits: true,
|
||
TypeAlias: Alias,
|
||
StringLiteralOnly: true,
|
||
});
|
||
const CodecByMethod = Tables.CodecByMethod || new Map();
|
||
|
||
// ---------- nullmap 位游标 ----------
|
||
|
||
function MethodIdAt(Buf, Off) {
|
||
return Off >= 8 && Off + 8 <= Buf.length ? Buf.readBigInt64BE(Off - 8).toString() : '';
|
||
}
|
||
|
||
// 本条记录负载的起始位游标。**必须把前面的记录走一遍**,因为位图是共用的:
|
||
// 位游标(负载) = Σ前面每条记录的形态位 + Σ前面每条记录负载里的 opt 位
|
||
// 走不动(短命令形态 / 前面某条方法的字段表不在 codec_fields / 走完对不齐 Off)→ 返回 null,
|
||
// 上层就按「拿不到位」如实报不可解(不猜)。
|
||
function ResolveCursor(Buf, Off) {
|
||
const NmSize = Link.NullmapSize(Buf, 0);
|
||
if (NmSize > Buf.length || NmSize > Off) return null;
|
||
const Bits = NullmapBits(Buf.slice(0, NmSize));
|
||
let At = NmSize;
|
||
let Bit = 0;
|
||
for (;;) {
|
||
// 走到目标记录的负载 → 位游标就是「已消费的形态位 + 前面负载里的 opt 位」
|
||
if (At === Off) return { Bits, Index: Bit };
|
||
if (At > Off || Bit >= Bits.length) return null;
|
||
if (Bits[Bit++]) return null; // 短命令形态:命令号 → (objectId, methodId) 的查表数据还没挖
|
||
if (At + 16 > Buf.length) return null;
|
||
const MethodId = Buf.readBigInt64BE(At + 8).toString();
|
||
At += 16;
|
||
if (At === Off) return { Bits, Index: Bit }; // 目标负载紧跟它自己的 16 字节记录头
|
||
const Layout = LAYOUT_BY_METHOD[MethodId];
|
||
let R;
|
||
const Ctx = { Bits, Index: Bit };
|
||
if (Layout) {
|
||
R = Readers[Layout](Buf, At, Ctx);
|
||
} else {
|
||
const C = CodecByMethod.get(MethodId);
|
||
if (!C) return null; // 前面那条方法的字段表不在 codec_fields:数不出它耗掉几个位
|
||
R = ReadFieldsBits(Ctx, Buf, At, C, 0);
|
||
}
|
||
Bit = Ctx.Index;
|
||
if (R.Bad || !(R.Next > At)) return null;
|
||
At = R.Next;
|
||
}
|
||
}
|
||
|
||
// 把当前位游标临时接到 LeafReader 上跑一次字段读;读完把新游标写回 Ctx
|
||
function ReadFieldsBits(Ctx, Buf, Off, Codec, Depth) {
|
||
if (!Ctx.Bits) return { Value: null, Next: Off, Bad: true, Bit: Ctx.Index, Why: '没有 nullmap 比特流,opt 字段无法判定(不按字节猜)' };
|
||
LeafReader.AttachBits(Ctx.Bits, Ctx.Index);
|
||
let R;
|
||
try {
|
||
R = LeafReader.ReadFields(Buf, Off, Codec, Depth);
|
||
} finally {
|
||
Ctx.Index = LeafReader.BitCursor().Index;
|
||
LeafReader.DetachBits();
|
||
}
|
||
return { Value: R.Value, Next: R.Next, Bad: R.Bad, Bit: Ctx.Index, Why: R.Why };
|
||
}
|
||
|
||
// ---------- 读 ----------
|
||
|
||
// pt 里的一项 = ModelData 的 lt codec(module_45686.js:487-494)
|
||
function ReadLtItem(Buf, At, Ctx) {
|
||
const Id = Buf.readBigInt64BE(At).toString();
|
||
let Next = At + 8;
|
||
if (Id === '0') {
|
||
if (Next + 8 > Buf.length) return { Value: null, Next: At, Bad: true, Why: 'lt 项 id=0 但没有紧跟的 i64 值' };
|
||
return { Value: { Id, Value: Buf.readBigInt64BE(Next).toString() }, Next: Next + 8, Bit: Ctx.Index };
|
||
}
|
||
const Codec = Tables.CodecByModelId.get(Id);
|
||
if (!Codec) {
|
||
return { Value: null, Next: At, Bad: true, Why: 'modelId=' + Id + ' 在 codec_fields 里查不到 codec(叶子 codec 缺失)' };
|
||
}
|
||
const R = ReadFieldsBits(Ctx, Buf, Next, Codec, 1);
|
||
if (R.Bad) {
|
||
return { Value: null, Next: At, Bad: true, Why: 'modelId=' + Id + '(' + Codec.Module + '::' + Codec.Codec + ')的字段解不动:' + (R.Why || '未知类型/越界') };
|
||
}
|
||
return { Value: { Id, Value: R.Value }, Next: R.Next, Bit: Ctx.Index };
|
||
}
|
||
|
||
// lt:_unloadObjects 的负载 = list<i64 对象 id>
|
||
function ReadLt(Buf, Off, Ctx) {
|
||
const N = LeafReader.ReadVarint(Buf, Off);
|
||
if (!N || N.Bad) return { Value: null, Next: Off, Bad: true, Why: '流变量计数读不动' };
|
||
let At = N.Next;
|
||
if (N.Value > MaxCount) return { Value: null, Next: Off, Bad: true, Why: '个数 ' + N.Value + ' 超上限' };
|
||
if (At + 8 * N.Value > Buf.length) return { Value: null, Next: Off, Bad: true, Why: '个数 ' + N.Value + ' 但只剩 ' + (Buf.length - At) + ' 字节' };
|
||
const Ids = [];
|
||
for (let I = 0; I < N.Value; I++) {
|
||
Ids.push(Buf.readBigInt64BE(At).toString());
|
||
At += 8;
|
||
}
|
||
return { Value: { Ids }, Next: At, Bit: Ctx.Index };
|
||
}
|
||
|
||
// pt:对象引用表 + ModelData 表
|
||
function ReadPt(Buf, Off, Ctx) {
|
||
if (Off + 4 > Buf.length) return { Value: null, Next: Off, Bad: true, Why: '不足 4 字节读对象表个数' };
|
||
const C = Buf.readInt32BE(Off);
|
||
let At = Off + 4;
|
||
if (C < 0 || C > MaxCount) return { Value: null, Next: Off, Bad: true, Why: '对象表个数 C=' + C + ' 不合理' };
|
||
if (At + 16 * C + 4 > Buf.length) return { Value: null, Next: Off, Bad: true, Why: '对象表 C=' + C + ' 超出负载长度' };
|
||
const Objects = [];
|
||
for (let I = 0; I < C; I++) {
|
||
Objects.push({ ObjectId: Buf.readBigInt64BE(At).toString(), GameTypeId: Buf.readBigInt64BE(At + 8).toString() });
|
||
At += 16;
|
||
}
|
||
const D = Buf.readInt32BE(At);
|
||
At += 4;
|
||
if (D < 0 || D > MaxCount) return { Value: null, Next: Off, Bad: true, Why: 'ModelData 表个数 D=' + D + ' 不合理' };
|
||
const Items = [];
|
||
for (let I = 0; I < D; I++) {
|
||
if (At + 8 > Buf.length) return { Value: null, Next: At, Bad: true, Why: '第 ' + (I + 1) + '/' + D + ' 项读 id 越界' };
|
||
const R = ReadLtItem(Buf, At, Ctx);
|
||
if (R.Bad) return { Value: null, Next: At, Bad: true, Why: '第 ' + (I + 1) + '/' + D + ' 项:' + R.Why };
|
||
Items.push(R.Value);
|
||
At = R.Next;
|
||
}
|
||
return { Value: { Objects, Items }, Next: At, Bit: Ctx.Index };
|
||
}
|
||
|
||
// zt:模型归属表 + 资源引用项表
|
||
function ReadZt(Buf, Off, Ctx) {
|
||
if (Off + 8 > Buf.length) return { Value: null, Next: Off, Bad: true, Why: '不足 8 字节读 X/批次个数' };
|
||
const X = Buf.readInt32BE(Off);
|
||
const C = Buf.readInt32BE(Off + 4);
|
||
let At = Off + 8;
|
||
if (C < 0 || C > MaxCount) return { Value: null, Next: Off, Bad: true, Why: '批次个数 C=' + C + ' 不合理' };
|
||
const Batches = [];
|
||
for (let I = 0; I < C; I++) {
|
||
if (At + 12 > Buf.length) return { Value: null, Next: At, Bad: true, Why: '第 ' + (I + 1) + '/' + C + ' 批读头部越界' };
|
||
const ObjectId = Buf.readBigInt64BE(At).toString();
|
||
const N = Buf.readInt32BE(At + 8);
|
||
At += 12;
|
||
if (N < 0 || N > MaxCount) return { Value: null, Next: At - 4, Bad: true, Why: '第 ' + (I + 1) + ' 批 modelId 个数 N=' + N + ' 不合理' };
|
||
if (At + 8 * N > Buf.length) return { Value: null, Next: At, Bad: true, Why: '第 ' + (I + 1) + ' 批 N=' + N + ' 超出负载长度' };
|
||
const ModelIds = [];
|
||
for (let K = 0; K < N; K++) {
|
||
ModelIds.push(Buf.readBigInt64BE(At).toString());
|
||
At += 8;
|
||
}
|
||
Batches.push({ ObjectId, ModelIds });
|
||
}
|
||
if (At + 4 > Buf.length) return { Value: null, Next: At, Bad: true, Why: '批次之后不足 4 字节读项个数' };
|
||
const D = Buf.readInt32BE(At);
|
||
At += 4;
|
||
if (D < 0 || D > MaxCount) return { Value: null, Next: At - 4, Bad: true, Why: '项个数 D=' + D + ' 不合理' };
|
||
const Items = [];
|
||
for (let I = 0; I < D; I++) {
|
||
if (At + 22 > Buf.length) return { Value: null, Next: At, Bad: true, Why: '第 ' + (I + 1) + '/' + D + ' 项越界' };
|
||
const R = Buf.readBigInt64BE(At).toString();
|
||
const E = Buf.readInt16BE(At + 8);
|
||
const U = Buf.readInt16BE(At + 10);
|
||
const S = Buf.readBigInt64BE(At + 12).toString();
|
||
const O = Buf[At + 20];
|
||
const Skip = Buf.readInt8(At + 21); // n2u = 1 字节(module_72834.js:272-274)
|
||
At += 22;
|
||
if (Skip < 0 || Skip > MaxCount) return { Value: null, Next: At - 1, Bad: true, Why: '第 ' + (I + 1) + ' 项跳过个数 ' + Skip + ' 不合理' };
|
||
if (At + 8 * Skip > Buf.length) return { Value: null, Next: At, Bad: true, Why: '第 ' + (I + 1) + ' 项跳过 ' + Skip + ' 个 id 越界' };
|
||
const Skipped = [];
|
||
for (let K = 0; K < Skip; K++) {
|
||
Skipped.push(Buf.readBigInt64BE(At).toString());
|
||
At += 8;
|
||
}
|
||
Items.push({ R, E, U, S, O, Skipped });
|
||
}
|
||
return { Value: { X, Batches, Items }, Next: At, Bit: Ctx.Index };
|
||
}
|
||
|
||
const Readers = { lt: ReadLt, pt: ReadPt, zt: ReadZt };
|
||
|
||
// 第 4 个参数是**可选的帧级位游标** Ctx = { Bits, Index }(由 semantic.DecodeFrameBody 顺序推进后传进来):
|
||
// 传了 ⇒ 直接用它,不再自己 ResolveCursor。为什么必须这样:ResolveCursor 要把「本条记录之前」的
|
||
// 每条记录重走一遍,多记录帧上是 O(n²)(space-100073070/s2c_0001 的容器负载 766KB)。
|
||
// 没传 ⇒ 保持既有行为(按 methodId 判定要不要算游标)。selftest.js:40/51、probe_containers.js:91/95、
|
||
// extract_space_init.js:50 走的都是这条。
|
||
// (原来这里是没用的 Depth 形参:三个布局的读者都按 (Buf, Off, Ctx) 调用,Depth 从来没被读过。)
|
||
function Read(Layout, Buf, Off, Ctx) {
|
||
if (!Readers[Layout]) throw new Error('未知容器布局: ' + Layout);
|
||
// 只有「确认是 modelService 容器记录」才去算位游标:其它调用(自检把可疑帧拿来试布局)
|
||
// 不该触发走路,也不该让 opt 走位路径。
|
||
const Known = !!LAYOUT_BY_METHOD[MethodIdAt(Buf, Off)];
|
||
const Use = Ctx && Ctx.Bits ? Ctx : (Known ? ResolveCursor(Buf, Off) : null) || { Bits: null, Index: 0 };
|
||
const R = Readers[Layout](Buf, Off, Use);
|
||
if (Ctx) Ctx.Index = Use.Index; // 把本条负载消费掉的 opt 位数交回给调用方(缺口 A)
|
||
if (!R.Bad) return { Value: R.Value, Next: R.Next };
|
||
return { Value: null, Next: R.Next, Bad: true, Why: R.Why || '' };
|
||
}
|
||
|
||
// 逐个试布局,取「恰好吃到 End」的那个;都不精确则返回 null(不猜)。
|
||
// 已知的 modelService 方法直接按 methodId 定布局(多记录帧必须走这条)。
|
||
function Detect(Buf, Off, End) {
|
||
const MethodId = MethodIdAt(Buf, Off);
|
||
const Known = LAYOUT_BY_METHOD[MethodId];
|
||
if (Known) {
|
||
const R = Read(Known, Buf, Off);
|
||
return R.Bad ? null : Known;
|
||
}
|
||
const Hits = [];
|
||
for (const Layout of [LAYOUT.Zt, LAYOUT.Pt, LAYOUT.Lt]) {
|
||
const R = Read(Layout, Buf, Off);
|
||
if (!R.Bad && R.Next === End) Hits.push(Layout);
|
||
}
|
||
return Hits.length ? Hits[0] : null;
|
||
}
|
||
|
||
// ---------- 写(由读侧反推,逐字节验证过) ----------
|
||
|
||
function WriteVarint(N) {
|
||
return Writer.WriteVarint(N);
|
||
}
|
||
|
||
// pt 里的一项
|
||
function WriteLtItem(Item) {
|
||
if (!Item || Item.Id === undefined || Item.Id === null) throw new Error('lt 项缺少 Id');
|
||
const Id = Buffer.alloc(8);
|
||
Id.writeBigInt64BE(BigInt(Item.Id), 0);
|
||
if (String(Item.Id) === '0') {
|
||
const V = Buffer.alloc(8);
|
||
V.writeBigInt64BE(BigInt(Item.Value), 0);
|
||
return Buffer.concat([Id, V]);
|
||
}
|
||
const Codec = Tables.CodecByModelId.get(String(Item.Id));
|
||
if (!Codec) throw new Error('modelId=' + Item.Id + ' 在 codec_fields 里查不到 codec,无法展开');
|
||
return Buffer.concat([Id, LeafWriter.WriteFields(Codec, Item.Value, 0)]);
|
||
}
|
||
|
||
function WriteLt(Obj) {
|
||
if (!Obj || !Array.isArray(Obj.Ids)) throw new Error('lt 结构缺少 Ids');
|
||
const Parts = [WriteVarint(Obj.Ids.length)];
|
||
for (const Id of Obj.Ids) {
|
||
const B = Buffer.alloc(8);
|
||
B.writeBigInt64BE(BigInt(Id), 0);
|
||
Parts.push(B);
|
||
}
|
||
return Buffer.concat(Parts);
|
||
}
|
||
|
||
function WritePt(Obj) {
|
||
if (!Obj || !Array.isArray(Obj.Objects) || !Array.isArray(Obj.Items)) throw new Error('pt 结构缺少 Objects / Items');
|
||
const Head = Buffer.alloc(4);
|
||
Head.writeInt32BE(Obj.Objects.length, 0);
|
||
const Pairs = Obj.Objects.map((X) => {
|
||
const B = Buffer.alloc(16);
|
||
B.writeBigInt64BE(BigInt(X.ObjectId), 0);
|
||
B.writeBigInt64BE(BigInt(X.GameTypeId), 8);
|
||
return B;
|
||
});
|
||
const Count = Buffer.alloc(4);
|
||
Count.writeInt32BE(Obj.Items.length, 0);
|
||
return Buffer.concat([Head, ...Pairs, Count, ...Obj.Items.map(WriteLtItem)]);
|
||
}
|
||
|
||
function WriteZt(Obj) {
|
||
if (!Obj || !Array.isArray(Obj.Batches) || !Array.isArray(Obj.Items)) throw new Error('zt 结构缺少 Batches / Items');
|
||
const Head = Buffer.alloc(8);
|
||
Head.writeInt32BE(Obj.X, 0);
|
||
Head.writeInt32BE(Obj.Batches.length, 4);
|
||
const Batches = Obj.Batches.map((B) => {
|
||
const Id = Buffer.alloc(8);
|
||
Id.writeBigInt64BE(BigInt(B.ObjectId), 0);
|
||
const N = Buffer.alloc(4);
|
||
N.writeInt32BE(B.ModelIds.length, 0);
|
||
const Ids = B.ModelIds.map((M) => {
|
||
const Buf = Buffer.alloc(8);
|
||
Buf.writeBigInt64BE(BigInt(M), 0);
|
||
return Buf;
|
||
});
|
||
return Buffer.concat([Id, N, ...Ids]);
|
||
});
|
||
const Count = Buffer.alloc(4);
|
||
Count.writeInt32BE(Obj.Items.length, 0);
|
||
const Items = Obj.Items.map((X) => {
|
||
const Buf = Buffer.alloc(22);
|
||
Buf.writeBigInt64BE(BigInt(X.R), 0);
|
||
Buf.writeInt16BE(X.E, 8);
|
||
Buf.writeInt16BE(X.U, 10);
|
||
Buf.writeBigInt64BE(BigInt(X.S), 12);
|
||
Buf[20] = X.O;
|
||
Buf.writeInt8((X.Skipped || []).length, 21);
|
||
const Skipped = (X.Skipped || []).map((S) => {
|
||
const B = Buffer.alloc(8);
|
||
B.writeBigInt64BE(BigInt(S), 0);
|
||
return B;
|
||
});
|
||
return Buffer.concat([Buf, ...Skipped]);
|
||
});
|
||
return Buffer.concat([Head, ...Batches, Count, ...Items]);
|
||
}
|
||
|
||
const Writers = { lt: WriteLt, pt: WritePt, zt: WriteZt };
|
||
|
||
// 第 3 个参数是**可选的帧级位游标** Ctx = { Bits, Index }:语义层重建 nullmap 时传进来,
|
||
// 叶子里每个 opt 字段的「空/非空」位就按编码顺序写回该比特流(codec.js:PutBit)。
|
||
// 不传 ⇒ 保持既有行为:只保证「空值一个字节都不写」,位由帧级 NullmapHex 原样带上
|
||
// (selftest.js:53、probe_containers 走的都是这条,口径一字不变)。
|
||
function Write(Layout, Obj, Ctx) {
|
||
const F = Writers[Layout];
|
||
if (!F) throw new Error('未知容器布局: ' + Layout);
|
||
if (!(Ctx && Ctx.Bits)) return F(Obj);
|
||
LeafWriter.AttachBits(Ctx.Bits, Ctx.Index);
|
||
try {
|
||
return F(Obj);
|
||
} finally {
|
||
Ctx.Index = LeafWriter.BitCursor().Index;
|
||
LeafWriter.DetachBits();
|
||
}
|
||
}
|
||
|
||
return { Read, Write, Detect, LAYOUT, LAYOUT_BY_METHOD };
|
||
}
|
||
|
||
module.exports = { MakeContainerCodec, LAYOUT, LAYOUT_BY_METHOD };
|