space.js 的 BuildInitFrames 自己逐条调 Container.Write / Semantic.EncodeRecord 出负载,再把模板的 NullmapHex 原样带上。但帧级 nullmap 是所有记录共用的一串位(每条记录 1 个形态位 + 每个 opt 字段 1 个空/非空位,按编码顺序排),只有 semantic.EncodeFrameBody 持位游标(容器与字段两条路径都会推进 Ctx)。于是「把 opt<T> 由空改成有值」时负载多出 T 的字节数而 nullmap 位仍是 1(=空),解码侧按位读到空、把那多出来的字节当成下一条记录头 —— 实测 devicesLoaded 的 mountedDevice 由 null 改成设备 id 时帧 255848B→255856B 且 Bad(29 条记录只解出 7 条)。M7 覆盖 mountedItems 时没暴露,是因为 list<T> 改长度不占 nullmap 位(长度编在负载里)。改法:BuildInitFrames 把记录按模板形态原样交给 Semantic.EncodeFrameBody(容器 → Layout+Value、字段 → Fields、解不动 → Hex、旧模板的现成 Payload 仍透传),由它按编码顺序重建 nullmap 位;没传 semantic 实例时保留旧路径。验证:字节中性 —— names/ 下全部模板(init+session,61 帧)改动前后逐字节相同 61/61;修复有效 —— 把 s2c_0004 槽位 920009631046 的 mountedDevice 由 null 改为设备 id 后,帧 +8B、解码 29 条记录 Bad=false、nullmap 长度不变且恰好 1 个字节变化、其余 27 个槽位 0 处变化;回归 —— semantic.js --demo 全部断言通过(含「space.js 组帧 == 参考字节」那条)、selftest 逐字节一致 1108/不一致 9/解码中断 90、CompareContainerDecode 5 帧/26950/0。
95 lines
5.8 KiB
JavaScript
95 lines
5.8 KiB
JavaScript
// space.js - 空间帧组装 + 空间初始化模板
|
||
//
|
||
// s2c 空间帧 = [nullmap][(objectId int64)(methodId int64)(负载)…](出处见 dispatch.js 头注 + semantic.js 头注)
|
||
// 注意是**每条记录自带 objectId**(src/module_61860.js:2119-2120),不是「帧头一个 spaceId」;
|
||
// 单记录帧里两者恰好相同,所以旧的「帧级 spaceId」写法仍然等价(见 BuildSpaceFrame 的兼容路径)。
|
||
//
|
||
// 「初始化模板」= names/space-init-<spaceId>.json。它存的是**结构化模板**而不是原始帧字节:
|
||
// 容器记录存「解出来的对象树」(发送时由 container.js 重新编码),
|
||
// 普通字段记录存「字段 JSON」(发送时由 semantic.js 现编),
|
||
// 只有解不动的记录才存 hex。
|
||
// 这样模板里的每个 id / modelId / 项 / 字段都可以被替换成我们自己的数据(M2 的后续工作),
|
||
// 而不是把抓包字节原样回放。
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const Link = require('./link');
|
||
|
||
// Records = [{ ObjectId?, MethodId, Payload: Buffer }]
|
||
// NullmapHex:nullmap 原样字节(十六进制)。注意它不是「可有可无的 0」——
|
||
// 实测有帧的 nullmap 是 2 字节且带内容(如 `20 3c`),含义是「哪些字段为空、负载里因此没有它们」,
|
||
// 写成 0x00 会让客户端按「非空」去读,导致错位。所以能拿到原 nullmap 就必须原样带上。
|
||
// (EncodeFrame 自己会拼 nullmap,所以这里 Parts 里不能再放一个。)
|
||
// ObjectId 缺省 → 回退用帧级 SpaceId:这是**兼容路径**,只为让「每条记录 objectId 相同」这类旧调用
|
||
// (以及还没升级出 ObjectId 的老模板)继续能跑;新模板里每条记录都应带自己的 ObjectId。
|
||
function BuildSpaceFrame(SpaceId, Records, NullmapHex) {
|
||
const Nm = Buffer.from(NullmapHex || '00', 'hex');
|
||
const Parts = [];
|
||
for (const R of Records) {
|
||
const Id = Buffer.alloc(8);
|
||
Id.writeBigInt64BE(BigInt(R.ObjectId === undefined || R.ObjectId === null ? SpaceId : R.ObjectId), 0);
|
||
const Method = Buffer.alloc(8);
|
||
Method.writeBigInt64BE(BigInt(R.MethodId), 0);
|
||
Parts.push(Id, Method, R.Payload);
|
||
}
|
||
const Body = Buffer.concat(Parts);
|
||
return Link.EncodeFrame(Body, Nm);
|
||
}
|
||
|
||
function LoadInitTemplate(NamesDir, SpaceId) {
|
||
const File = path.join(NamesDir, 'space-init-' + SpaceId + '.json');
|
||
if (!fs.existsSync(File)) return null;
|
||
return JSON.parse(fs.readFileSync(File, 'utf8'));
|
||
}
|
||
|
||
// 模板 → [{ File, DelayMs, Frame }];负载由 Container / Semantic 重新编码
|
||
// 记录的三种形态:① { Layout, Value } 容器(container.js)② { Fields } 普通字段(semantic.js)③ { Hex } 未解原样透传
|
||
//
|
||
// 【为什么把记录原样交给 Semantic.EncodeFrameBody,而不是在这里逐条编成字节】
|
||
// 帧级 nullmap 是**所有记录共用的一串位**:每条记录先占 1 个「形态位」,随后它负载里每个
|
||
// opt<T> 字段再占 1 个「空/非空位」,按编码顺序排下去(见 semantic.js 头注的 gap A)。
|
||
// 所以「改一个 opt 字段的值」= 写出负载 **且** 把那位由 1 改 0 —— 两件事必须一起做。
|
||
// 只有 semantic.EncodeFrameBody 持有那个位游标(它对容器与字段两条路径都会推进 Ctx;
|
||
// container.js 的 Write/Read 都接受 Ctx)。旧写法(本函数自己 EncodeRecord 不带游标 +
|
||
// nullmap 原样带上)只写负载、不改位,产出的是**错位帧**:负载多出 T 的字节数,而解码侧按
|
||
// 位读到「空」,于是把那多出来的字节当成下一条记录的头(实测 devicesLoaded 的 mountedDevice
|
||
// 由空改成有值时,帧从 255848B 变 255856B 且 Bad)。
|
||
// 注意 list<T> 字段改长度不碰 nullmap 位(长度在负载里编),所以 M7 覆盖 mountedItems 时没暴露。
|
||
function BuildInitFrames(Template, Container, Semantic) {
|
||
const Out = [];
|
||
for (const F of Template.Frames || []) {
|
||
// 没有语义层实例时退回旧路径(逐条编字节 + nullmap 原样带上):它只适用于「记录类型全是
|
||
// 容器/Hex,或改动的只是 list 这类不占位的字段」。空模板就别在这里抛错,交给语义层报。
|
||
if (!Semantic) {
|
||
const Old = F.Records.map((R) => {
|
||
let Payload;
|
||
if (R.Layout !== undefined) Payload = Container.Write(R.Layout, R.Value);
|
||
else Payload = Buffer.from(R.Hex || '', 'hex');
|
||
return { ObjectId: R.ObjectId, MethodId: R.MethodId, Payload };
|
||
});
|
||
Out.push({ File: F.File, DelayMs: F.DelayMs || 0, Frame: BuildSpaceFrame(F.SpaceId, Old, F.NullmapHex) });
|
||
continue;
|
||
}
|
||
const Records = F.Records.map((R) => {
|
||
// objectId 缺省 → 回退帧级 SpaceId(与 BuildSpaceFrame 的兼容路径同规则)
|
||
const ObjectId = R.ObjectId === undefined || R.ObjectId === null ? F.SpaceId : R.ObjectId;
|
||
if (R.Hex !== undefined) return { ObjectId, MethodId: R.MethodId, Hex: R.Hex };
|
||
if (R.Layout !== undefined) return { ObjectId, MethodId: R.MethodId, Layout: R.Layout, Value: R.Value };
|
||
if (R.Fields !== undefined) return { ObjectId, MethodId: R.MethodId, Fields: R.Fields };
|
||
// 旧模板可能直接存了编好的负载:原样透传(语义层把它当不可重建记录,之后的位保持原值)
|
||
return { ObjectId, MethodId: R.MethodId, Payload: R.Payload };
|
||
});
|
||
// EncodeFrameBody 返回的就是完整帧体 [nullmap][记录…];再用 EncodeFrame 补长度头
|
||
const Body = Semantic.EncodeFrameBody({ Nullmap: F.NullmapHex || '00', Records });
|
||
const NmSize = Link.NullmapSize(Body, 0);
|
||
Out.push({
|
||
File: F.File,
|
||
DelayMs: F.DelayMs || 0,
|
||
Frame: Link.EncodeFrame(Body.slice(NmSize), Body.slice(0, NmSize)),
|
||
});
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
module.exports = { BuildSpaceFrame, LoadInitTemplate, BuildInitFrames };
|