Files
TermiNexus 2299b0996b fix(server): 组帧漏写帧级 nullmap 位,opt 字段改值会产出错位帧(M10 回显的真因)
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。
2026-09-18 13:38:28 +08:00

95 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };