// extract_space_init.js - 把某个空间的 s2c 抓包抽成「结构化初始化模板」names/space-init-.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- [--space ] [--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 + ' 字节');