// probe_containers.js - 只读诊断:空间初始化模板里「容器记录」到底卡在哪 // // 为什么单独有这个工具:容器记录(modelService 的 4 条方法)过去只会被整帧留成 hex, // 而「解不动」有三种完全不同的原因,混在一起就变成了「再试一次也不会有进展」: // ① 布局/边界:负载的切分口径不对(多记录帧里容器负载不该按「吃到帧尾」定界) // ② 叶子 codec 缺失:pt/lt 项里的 modelId 在 names/codec_fields.json 查不到 codec, // 或字段类型是推不出来的裸类名(16 个 token 之一,见 codec_fields.json 的 SlotTypeStats) // ③ nullmap 位游标:opt 字段的「空/非空」位与记录形态位共用同一条比特流, // 拿不到位游标时只能如实报不可解(不能退回按字节猜标志位) // // 本工具只读模板与 packets/plain,不写任何文件。 // 用法: node probe_containers.js [--names ../names] [--plain ../packets/plain] 'use strict'; const fs = require('fs'); const path = require('path'); const Link = require('./server/link'); const Semantic = require('./server/semantic').Load(path.join(__dirname, '..', 'names')); 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 NamesDir = path.resolve(__dirname, Arg('names', path.join('..', 'names'))); const PlainDir = path.resolve(__dirname, Arg('plain', path.join('..', 'packets', 'plain'))); const LAYOUT_BY_METHOD = Semantic.Container.LAYOUT_BY_METHOD; const NAME_BY_METHOD = { '3216143066888387731': 'ModelService.LoadDependencies', '7640916300855664666': 'ModelService.LoadObjectsData', '9038756763225879447': 'ModelService.UnloadObjects', '8179162314198150518': 'ModelService.SetModelCommandAlias', }; const LAYOUTS = ['zt', 'pt', 'lt']; // model_table(modelId → 模型类)用来区分「这个 modelId 是真模型、只是 codec_fields 没抽出 codec」 // 与「这个 modelId 根本不存在 → 说明前面的项读串位了(字段表抽错/不全)」 const ModelTable = new Map( JSON.parse(fs.readFileSync(path.join(NamesDir, 'model_table.json'), 'utf8')).Rows.map((R) => [String(R.ModelId), R]), ); // 原因分类(每条记录只归一类,取「最先卡住」的那个) const Causes = new Map(); function Classify(Rec) { if (Rec.Accepted) return '✓ 已解出(布局/边界正确)'; const Why = Rec.Why || ''; if (/没有 nullmap 比特流|位游标/.test(Why)) return '③ nullmap 位游标拿不到(前一条记录的字段表不在 codec_fields)'; const M = /modelId=(\d+)/.exec(Why); if (M && !ModelTable.has(M[1])) return '⑤ 字段表抽错/不全 → 项读串位(例如 MapCodecInfo 被抽成 enum<…>)'; if (M) return '② 叶子 codec 缺失(modelId 在 model_table 里有,codec_fields 里没有 codec)'; if (/无 codec:|没有对应 codec/.test(Why)) return '② 叶子 codec 缺失(字段类型是推不出 codec 的裸类名)'; if (/抛出异常|越界|不合理|超上限/.test(Why)) return '① 布局/边界不对(负载切分口径)'; return '④ 其它:' + Why.split(':')[0]; } const MissingModelIds = new Map(); const MissingTypes = new Map(); const Rows = []; for (const File of fs.readdirSync(NamesDir).filter((X) => /^space-init-.*\.json$/.test(X)).sort()) { if (/^space-init-login-/.test(File)) continue; const T = JSON.parse(fs.readFileSync(path.join(NamesDir, File), 'utf8')); const Dir = path.join(PlainDir, 'space-' + T.SpaceId); for (const Fr of T.Frames || []) { const Plain = path.join(Dir, Fr.File); if (!fs.existsSync(Plain)) continue; const Parsed = Link.ParseFrames(fs.readFileSync(Plain)); if (!Parsed || Parsed.length !== 1) continue; const Body = Link.DecodeFrame(Parsed[0]); const Decoded = Semantic.DecodeFrameBody(Body); for (const [Idx, R] of Decoded.Records.entries()) { if (R.ObjectId === undefined || R.At === undefined) continue; const MethodId = String(R.MethodId); const Expect = LAYOUT_BY_METHOD[MethodId]; if (!Expect) continue; const Off = R.At + 16; const Limit = Body.length - Off; const Row = { File, Frame: Fr.File, Ord: Idx + 1, Method: NAME_BY_METHOD[MethodId] || MethodId, Expect, Load: Limit, Why: '', Accepted: false, Tried: [], }; // 三布局各试一遍,把「吃到哪 / 在哪中断 / 为什么」都记下来 let Hit = null; for (const L of LAYOUTS) { const B = Semantic.Container.Read(L, Body, Off); Row.Tried.push(L + (B.Bad ? ' 中断@+' + (B.Next - Off) : ' 吃到 +' + (B.Next - Off)) + (B.Why ? '(' + B.Why + ')' : '')); if (!B.Bad && L === Expect) Hit = L; } const R2 = Semantic.Container.Read(Expect, Body, Off); if (!R2.Bad) { Row.Accepted = true; Row.Consumed = R2.Next - Off; Row.Layout = Expect; } else { Row.Why = R2.Why || '(无原因)'; } // 缺 codec 统计:从容器值里扫 modelId / 从原因串里抠 modelId、类型名 const M = /modelId=(\d+)/.exec(Row.Why); if (M) MissingModelIds.set(M[1], (MissingModelIds.get(M[1]) || 0) + 1); const MT = /无 codec:([^)]+)/.exec(Row.Why); if (MT) MissingTypes.set(MT[1], (MissingTypes.get(MT[1]) || 0) + 1); Row.Cause = Classify(Row); Causes.set(Row.Cause, (Causes.get(Row.Cause) || 0) + 1); Rows.push(Row); } } } console.log('=== 容器记录诊断(只读;数据源 ' + NamesDir + ' + ' + PlainDir + ')==='); console.log(''); for (const R of Rows) { console.log(R.File.padEnd(32) + ' ' + String(R.Frame).padEnd(14) + ' 第' + String(R.Ord).padStart(2) + '条 ' + R.Method.padEnd(30) + ' 负载 ' + String(R.Load).padStart(7) + 'B 期望布局=' + R.Expect); console.log(' 三布局:' + R.Tried.join(' | ')); if (R.Accepted) console.log(' ✓ 解出 ' + R.Consumed + 'B(' + R.Layout + ');原因分类:' + R.Cause); else console.log(' ✗ ' + R.Cause + ';卡点:' + R.Why); } console.log(''); console.log('=== 汇总 ==='); console.log('容器记录 ' + Rows.length + ' 条:已解出 ' + Rows.filter((R) => R.Accepted).length + ' / 仍不可解 ' + Rows.filter((R) => !R.Accepted).length); for (const [K, V] of [...Causes].sort((A, B) => B[1] - A[1])) console.log(' ' + String(V).padStart(3) + ' ' + K); if (MissingModelIds.size) { console.log(' 缺 codec 的 modelId(去重 ' + MissingModelIds.size + ' 个):'); for (const [K, V] of [...MissingModelIds].sort((A, B) => B[1] - A[1])) console.log(' ' + K + ' ×' + V); } if (MissingTypes.size) { console.log(' 缺 codec 的裸类名(去重 ' + MissingTypes.size + ' 个):'); for (const [K, V] of [...MissingTypes].sort((A, B) => B[1] - A[1])) console.log(' ' + K + ' ×' + V); }