Files
TankIC/3DTank/tools/structure_space_init.js
T
TermiNexus b6f2cb71ee feat(3dtank): 协议逆向工程与自研服务器入库(工具链 + 协议表 + 空间回放 + MySQL)
工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
2026-09-17 20:00:41 +08:00

175 lines
8.7 KiB
JavaScript
Raw 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.
// 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);
}