工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
175 lines
8.7 KiB
JavaScript
175 lines
8.7 KiB
JavaScript
// structure_space_init.js - 把空间初始化模板从「字节」升级成「JSON」
|
||
//
|
||
// 为什么需要它:names/space-init-*.json 里绝大多数记录存的是原始 hex(抓包字节),
|
||
// 想改一个字段(比如用户名字)就得做字节替换。semantic.js 有了字段级解码能力后,
|
||
// 这里把那些 hex 记录整段当「记录链」解一遍(旧模板的 Hex 是「该帧体从本条记录负载起剩下的全部字节」,
|
||
// 所以一段 hex 里可能含多条记录 —— 见 tools/extract_space_init.js:94 与 mine_downlink.js 头注),
|
||
// 能完整解出、且解出再编码逐字节一致的,就换成结构化记录;否则**原样保留 hex 并把原因写进 Note**。
|
||
//
|
||
// 安全阀:只有当「结构化后的模板经 space.js 的组帧路径产出与升级前完全相同的字节」时才提交,
|
||
// 所以这个脚本不可能改变任何一帧的字节(解不动的一律不动)。
|
||
//
|
||
// 用法: node tools/structure_space_init.js
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const Link = require('./server/link');
|
||
const Space = require('./server/space');
|
||
const Semantic = require('./server/semantic').Load(path.join(__dirname, '..', 'names'));
|
||
|
||
const NamesDir = path.join(__dirname, '..', 'names');
|
||
const Files = fs.readdirSync(NamesDir).filter((F) => /^space-init-.*\.json$/.test(F)).sort();
|
||
|
||
// 按模板里存的形态拼帧体(含 nullmap):Hex 原样透传 / Layout+Value 走容器 / Fields 走语义层。
|
||
// objectId 缺省回退帧级 SpaceId —— 这正是升级前模板组帧的规则,所以这就是「升级前的字节」。
|
||
function ReferenceBody(F) {
|
||
const Parts = [Buffer.from(F.NullmapHex || '00', 'hex')];
|
||
for (const R of F.Records || []) {
|
||
const Id = Buffer.alloc(8);
|
||
Id.writeBigInt64BE(BigInt(R.ObjectId === undefined || R.ObjectId === null ? F.SpaceId : R.ObjectId), 0);
|
||
const Method = Buffer.alloc(8);
|
||
Method.writeBigInt64BE(BigInt(R.MethodId), 0);
|
||
let Payload;
|
||
if (R.Hex !== undefined) Payload = Buffer.from(R.Hex || '', 'hex');
|
||
else if (R.Layout !== undefined) Payload = Semantic.Container.Write(R.Layout, R.Value);
|
||
else if (R.Fields !== undefined) Payload = Semantic.EncodeRecord(R.MethodId, R);
|
||
else return null; // 形态不认识 → 交给调用方判为不可升级
|
||
Parts.push(Id, Method, Payload);
|
||
}
|
||
return Buffer.concat(Parts);
|
||
}
|
||
|
||
function ToFrame(Body) {
|
||
const N = Link.NullmapSize(Body, 0);
|
||
return Link.EncodeFrame(Body.slice(N), Body.slice(0, N));
|
||
}
|
||
|
||
// 解出来的记录 → 模板里的形态(① 容器 {Layout,Value} ② 普通字段 {Fields});每条都带自己的 ObjectId
|
||
function ToTemplate(R) {
|
||
const Out = { ObjectId: R.ObjectId, MethodId: R.MethodId };
|
||
if (R.Kind === 'container') {
|
||
Out.Layout = R.Layout;
|
||
Out.Value = R.Value;
|
||
} else {
|
||
Out.Fields = {};
|
||
for (const F of R.Fields || []) Out.Fields[F.Name] = F.Value;
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// 保留 hex 的原因分类(不要笼统写「解不动」——分类本身是施工图)
|
||
function Classify(Decoded) {
|
||
for (const R of Decoded.Records || []) {
|
||
if (R.Short) return '短命令形态(nullmap bit=1 的 1 字节命令号):命令号 → (objectId, methodId) 的查表数据还没挖';
|
||
}
|
||
const Notes = (Decoded.Records || []).filter((R) => R.Note).map((R) => R.Note);
|
||
const Note = Notes[0] || Decoded.Note || '未知';
|
||
if (/容器三布局/.test(Note)) return '容器布局吃不到记录尾:' + Note;
|
||
if (/没有字段表|不在 downlink_table/.test(Note)) return '没有字段表:' + Note;
|
||
if (/解不动/.test(Note)) return '字段/记录解不动:' + Note;
|
||
return Note;
|
||
}
|
||
|
||
const Total = { Spaces: 0, Frames: 0, EmptyFrames: 0, Records: 0, RecordsStructured: 0, RecordsHex: 0, HexBytes: 0, Upgraded: 0, HexReplaced: 0, Kept: 0 };
|
||
const Reasons = new Map();
|
||
const Details = [];
|
||
|
||
for (const File of Files) {
|
||
const Path2 = path.join(NamesDir, File);
|
||
const T = JSON.parse(fs.readFileSync(Path2, 'utf8'));
|
||
const Rec = { File, SpaceId: T.SpaceId, Frames: (T.Frames || []).length, EmptyFrames: 0, Records: 0, Structured: 0, Hex: 0, HexBytes: 0, Upgraded: 0, HexReplaced: 0, Kept: 0 };
|
||
let Changed = false;
|
||
const ReasonsHere = new Map();
|
||
|
||
for (const F of T.Frames || []) {
|
||
const Records = F.Records || [];
|
||
Rec.Records += Records.length;
|
||
if (!Records.length) {
|
||
Rec.EmptyFrames++;
|
||
continue;
|
||
}
|
||
for (const R of Records) {
|
||
if (R.Hex !== undefined) {
|
||
Rec.Hex++;
|
||
Rec.HexBytes += (R.Hex || '').length / 2;
|
||
} else Rec.Structured++;
|
||
}
|
||
// 只处理「含未解 hex 记录」的帧:容器记录(Layout 已存在)与已结构化的帧保持不动(也保证可重复运行)
|
||
if (!Records.some((R) => R.Hex !== undefined)) continue;
|
||
|
||
const Ref = ReferenceBody(F);
|
||
let Why = '';
|
||
let Decoded = null;
|
||
if (!Ref) Why = '记录形态既不认识 Hex/Layout/Fields 中的任何一种,不敢动';
|
||
else {
|
||
try {
|
||
Decoded = Semantic.DecodeFrameBody(Ref);
|
||
} catch (Err) {
|
||
Why = '解码抛错:' + Err.message;
|
||
}
|
||
}
|
||
if (!Why && Decoded.Bad) Why = Classify(Decoded);
|
||
if (!Why && !Decoded.Records.length) Why = '帧体里没解出记录';
|
||
// 安全阀:结构化后的帧必须与升级前逐字节相同,否则一律不提交
|
||
if (!Why) {
|
||
const NewRecords = Decoded.Records.map(ToTemplate);
|
||
let Built = null;
|
||
try {
|
||
Built = Space.BuildInitFrames({ Frames: [{ SpaceId: F.SpaceId, NullmapHex: F.NullmapHex, Records: NewRecords }] }, Semantic.Container, Semantic)[0].Frame;
|
||
} catch (Err) {
|
||
Why = '重新组帧失败:' + Err.message;
|
||
}
|
||
if (!Why && !Built.equals(ToFrame(Ref))) Why = '组帧字节与升级前不同(' + Built.length + ' vs ' + ToFrame(Ref).length + ' 字节)';
|
||
if (!Why) {
|
||
F.Records = NewRecords;
|
||
Changed = true;
|
||
Rec.Upgraded += NewRecords.length;
|
||
Rec.HexReplaced += Records.filter((R) => R.Hex !== undefined).length;
|
||
}
|
||
}
|
||
if (Why) {
|
||
Rec.Kept++;
|
||
const Kind = Why.split(':')[0];
|
||
Reasons.set(Kind, (Reasons.get(Kind) || 0) + 1);
|
||
ReasonsHere.set(Kind, (ReasonsHere.get(Kind) || 0) + 1);
|
||
// 解不动就如实保留原始 hex,并把原因写进 Note(原来的 Note 会被这条更具体的原因取代)
|
||
for (const R of F.Records) if (R.Hex !== undefined) R.Note = Why;
|
||
Details.push(' ' + File + ' ' + F.File + ' ' + F.Records.map((R) => '#' + R.MethodId).join(',') + ' ' + Why);
|
||
}
|
||
}
|
||
|
||
if (Changed) fs.writeFileSync(Path2, JSON.stringify(T, null, 1), 'utf8');
|
||
Total.Spaces++;
|
||
Total.Frames += Rec.Frames;
|
||
Total.EmptyFrames += Rec.EmptyFrames;
|
||
Total.Records += Rec.Records;
|
||
Total.RecordsStructured += Rec.Structured;
|
||
Total.RecordsHex += Rec.Hex;
|
||
Total.HexBytes += Rec.HexBytes;
|
||
Total.Upgraded += Rec.Upgraded;
|
||
Total.HexReplaced += Rec.HexReplaced;
|
||
Total.Kept += Rec.Kept;
|
||
Rec.StructuredAfter = Rec.Structured + Rec.Upgraded;
|
||
Rec.HexAfter = Rec.Hex - Rec.HexReplaced;
|
||
Details.push(' ' + File.padEnd(34) + '空间 ' + String(Rec.SpaceId).padEnd(14) +
|
||
'帧 ' + String(Rec.Frames).padStart(3) + ' 记录 ' + String(Rec.Records).padStart(3) +
|
||
' 升级前:结构化 ' + String(Rec.Structured).padStart(3) + ' / hex ' + String(Rec.Hex).padStart(3) +
|
||
'(' + Rec.HexBytes + 'B)' + ' → 升级后:结构化 ' + String(Rec.StructuredAfter).padStart(3) + ' / hex ' + String(Rec.HexAfter).padStart(3) +
|
||
' 本次 ' + Rec.HexReplaced + ' 条 hex → ' + Rec.Upgraded + ' 条结构化' + (Changed ? '(已写回)' : '(无变化)') +
|
||
(Rec.EmptyFrames ? ' ⚠ 空记录帧 ' + Rec.EmptyFrames : '') +
|
||
(Rec.Kept ? ' 保留 hex 原因:' + [...ReasonsHere].map(([K, V]) => K + '×' + V).join('、') : ''));
|
||
}
|
||
|
||
console.log('空间初始化模板结构化(升级前统计 → 升级后)');
|
||
console.log('');
|
||
for (const L of Details) console.log(L);
|
||
console.log('');
|
||
console.log('合计:空间 ' + Total.Spaces + ' 帧 ' + Total.Frames + '(其中空记录帧 ' + Total.EmptyFrames + ') 记录 ' + Total.Records);
|
||
console.log(' 升级前:结构化 ' + Total.RecordsStructured + ' 条 / 原始 hex ' + Total.RecordsHex + ' 条(' + Total.HexBytes + ' 字节)');
|
||
console.log(' 本次升级 ' + Total.HexReplaced + ' 条 hex 记录 → ' + Total.Upgraded + ' 条结构化记录;保留 hex ' + Total.Kept + ' 帧');
|
||
console.log(' 升级后:结构化 ' + (Total.RecordsStructured + Total.Upgraded) + ' 条 / 原始 hex ' + (Total.RecordsHex - Total.HexReplaced) + ' 条');
|
||
if (Reasons.size) {
|
||
console.log(' 保留 hex 的原因分类(按帧计):');
|
||
for (const [K, V] of [...Reasons].sort((A, B) => B[1] - A[1])) console.log(' ' + String(V).padStart(5) + ' ' + K);
|
||
}
|