Files
TankIC/3DTank/tools/extract_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

121 lines
5.1 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.
// extract_space_init.js - 把某个空间的 s2c 抓包抽成「结构化初始化模板」names/space-init-<spaceId>.json
//
// 用法: node extract_space_init.js --dir ../packets/plain/space-100073070 [--space 100073070] [--max 6]
//
// 模板不是原始帧字节:
// · 容器记录 → 存「解出来的对象树」,发送时由 server/container.js 重新编码(可改、可换数据)
// · 解不动的记录 → 存 hex(如实标注,不假装结构化)
// · nullmap 原样保留(实测有 2 字节且带内容的 nullmap,含义是「哪些字段为空」,写 0 会错位)
// · DelayMs 取自同目录 timeline_s2c.txt(相对首帧),保持原节奏
//
// 容器布局判定:三个布局都试,取「恰好吃到帧尾,或吃完后剩余字节里能认出下一条记录的 methodId」的那个。
'use strict';
const fs = require('fs');
const path = require('path');
const Link = require('./server/link');
const { LoadTables, MakeReader } = require('../tools/lib_space_codec');
const { MakeWriter } = require('./server/codec');
const { MakeContainerCodec } = require('./server/container');
const ModelService = require('./server/modelservice');
const Argv = process.argv.slice(2);
const Arg = (Name, Def) => {
const I = Argv.indexOf('--' + Name);
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
};
const PROJECT = path.resolve(__dirname, '..');
const Dir = path.resolve(Arg('dir', ''));
const Max = Number(Arg('max', '8')) || 8;
if (!Dir || !fs.existsSync(Dir)) {
console.error('用法: node extract_space_init.js --dir ../packets/plain/space-<id> [--space <id>] [--max N]');
process.exit(1);
}
const Tables = LoadTables(path.join(PROJECT, 'names'));
const Reader = MakeReader(Tables, { EnumBytes: 4 });
const Writer = MakeWriter(Tables, { EnumBytes: 4 });
const Container = MakeContainerCodec(Tables, Reader, Writer);
const LAYOUTS = ['zt', 'pt', 'lt'];
// 8 字节处是不是一个「已知的 methodId」(method_table / codec_fields / 上行表任一)
function KnownMethodId(Buf, Off) {
if (Off + 8 > Buf.length) return false;
const V = Buf.readBigInt64BE(Off).toString();
return Tables.CodecByMethod.has(V) || Tables.UplinkByMethod.has(V) || ModelService.Name(V) !== null;
}
function PickLayout(Buf, Off) {
for (const L of LAYOUTS) {
const R = Container.Read(L, Buf, Off);
if (R.Bad) continue;
if (R.Next === Buf.length || KnownMethodId(Buf, R.Next)) return { Layout: L, Result: R };
}
return null;
}
const Timeline = new Map();
const Tl = path.join(Dir, 'timeline_s2c.txt');
if (fs.existsSync(Tl)) {
for (const Line of fs.readFileSync(Tl, 'utf8').split('\n')) {
const M = /^(\S+)\t(\d+)$/.exec(Line.trim());
if (M) Timeline.set(M[1], Number(M[2]));
}
}
const Files = fs.readdirSync(Dir).filter((F) => /^s2c_\d+\.bin$/.test(F)).sort().slice(0, Max);
const SpaceArg = Arg('space', '');
const Out = { SpaceId: '', Note: '结构化初始化模板:容器存对象树(发送时重新编码),解不动的记录存 hex;nullmap 原样', Frames: [] };
let Base = null;
let Structured = 0;
let RawOnly = 0;
for (const File of Files) {
const RawFile = fs.readFileSync(path.join(Dir, File));
const Info = Link.PeekFrame(RawFile);
const Body = Link.DecodeFrame(Link.ParseFrames(RawFile)[0]);
const At = Link.NullmapSize(Body, 0);
if (At + 8 > Body.length) continue; // 保活 ping 之类:帧体只有 nullmap,没有 spaceId
const SpaceId = Body.readBigInt64BE(At).toString();
Out.SpaceId = SpaceArg || SpaceId;
const Nullmap = Body.slice(0, At).toString('hex');
let Off = At + 8;
const Records = [];
while (Off + 8 <= Body.length) {
const MethodId = Body.readBigInt64BE(Off).toString();
const Start = Off;
Off += 8;
const Picked = PickLayout(Body, Off);
if (Picked) {
Records.push({ MethodId, Layout: Picked.Layout, Value: Picked.Result.Value });
Off = Picked.Result.Next;
Structured++;
} else {
Records.push({ MethodId, Hex: Body.slice(Off).toString('hex'), Note: '解不动,原样存' });
Off = Body.length;
RawOnly++;
}
if (Records[Records.length - 1].Layout === undefined) break;
if (Off === Body.length) break;
}
const Ms = Timeline.has(File) ? Timeline.get(File) : null;
if (Base === null) Base = Ms === null ? 0 : Ms;
Out.Frames.push({
File,
SpaceId,
NullmapHex: Nullmap,
DelayMs: Ms === null ? 0 : Ms - Base,
Compressed: Info.Compressed,
Records,
});
}
const OutFile = path.join(PROJECT, 'names', 'space-init-' + Out.SpaceId + '.json');
fs.writeFileSync(OutFile, JSON.stringify(Out, null, 1));
console.log('空间 ' + Out.SpaceId + ':' + Out.Frames.length + ' 帧,容器记录 ' + Structured + ' 个,原样存的记录 ' + RawOnly + ' 个');
for (const F of Out.Frames) {
console.log(' ' + F.File.padEnd(14) + ' ' + F.Records.length + ' 条记录 @+' + F.DelayMs + 'ms nullmap=' + F.NullmapHex +
(F.Compressed ? ' (原压缩)' : '') + ' ' + F.Records.map((R) => (R.Layout ? R.Layout : 'hex') + ':' + (ModelService.Name(R.MethodId) || '#' + R.MethodId)).join(' '));
}
console.log('已写 ' + OutFile + ' ' + fs.statSync(OutFile).size + ' 字节');