Files
TankIC/3DTank/tools/extract_codec_fields.js
T
TermiNexus 2e5e8bd7b0 feat(3dtank): 协议工具链入库:上行命令接入 fake server + 字段表/WireClasses
- wss_server:识别 c2s 空间命令([nullmap][objectId i64][methodId i64][负载])并逐字段解码
- 应答签名改用 S:<methodId>(pair_flow.js 同步),fake 模式能按具体上行命令触发对应下行回放
- extract_codec_fields:新增「协议核心类 → 线上基础类型」挖掘(WireClasses,xr/R → i64)
- lib_space_codec:按模型真名/核心类换算兜底(ji → i64、xr → i64),抽成上下行共享库
- decode_space_frames:记录容器(_t/lt)已核实结构与未闭合环节
- README:补「协议逆向与离线模拟」章节(帧格式/字段编码/命令表/fake 回放/容器现状)

实测:297 条上行记录 100% 完整解出;fake 模式按 G.q1wd、ls.xbl 等命令精确触发应答
2026-09-16 16:30:38 +08:00

899 lines
38 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_codec_fields.js - 从空间层 codec 模块里抽出「字段名 + 类型 + 线上顺序」
// 用法:
// node extract_codec_fields.js --scan // 扫 src/ 全部模块(推荐)
// node extract_codec_fields.js --module ../src/module_54199.js
// node extract_codec_fields.js --scan --show st,ft // 打印指定 codec 的字段表
// node extract_codec_fields.js --scan --json // 额外打印 JSON 片段
//
// 原理(Kotlin/JS 协议 codec 的三段式结构;codec 变量名只在本模块内唯一,故键 = 模块::变量名):
// ① 字段名 getter: function G(t) { var n = t.<slot>; if (null != n) return n; v("codec_<字段名>"); }
// —— 报错字符串就是字段名(Kotlin 惰性属性名的兜底提示)
// ② (X(Codec).q7g = function (t) { this.<slot> = t.n7f(new <描述符>(...)); ... })
// —— slot → 类型描述符。首参以 "new " 开头 → 该头是「可选」包装;否则是「必填」包装;
// b(X) → 列表<X>;w().pb() → bool;w().xb() → string
// ③ (X(Codec).p7g = function (t) { ... G(this).p7g(t) ... t.a7g_1.<x>u() ... })
// —— 调用次序 = 线上字段次序;匿名 t.a7g_1.<x>u() 是无名字段的裸读
// 位宽: xy=i8 s2t=i16 t2t=i32 u2t=i64 v2t=f32 w2t=f64(见 module_72834 的读取器定义)
// ④ p7g 里给数据类赋值的属性名(如 i.xi0_1 = ...)是该模型数据类的有序字段名,
// 可与 names/regdump.json 里的模型字段列表 join 出 modelId
// 产物: names/codec_fields.json
const fs = require('fs');
const path = require('path');
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 OutPath = path.resolve(Arg('out', path.join(PROJECT, 'names', 'codec_fields.json')));
const Scan = Argv.indexOf('--scan') >= 0;
const SrcDir = path.join(PROJECT, 'src');
const ModuleFiles = Scan
? fs
.readdirSync(SrcDir)
.filter((F) => /^module_\d+\.js$/.test(F))
.map((F) => path.join(SrcDir, F))
: [path.resolve(Arg('module', path.join(SrcDir, 'module_54199.js')))];
// ---------------------------------------------------------------- 通用工具
// 配平扫描:从 Open 处的开括号找到对应闭括号(跳过字符串字面量)
function MatchBracket(Text, Open, OpenCh, CloseCh) {
let Depth = 0;
for (let I = Open; I < Text.length; I++) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C;
I++;
while (I < Text.length && Text[I] !== Q) {
if (Text[I] === '\\') I++;
I++;
}
} else if (C === OpenCh) Depth++;
else if (C === CloseCh) {
Depth--;
if (!Depth) return I;
}
}
return -1;
}
// 按顶层逗号切分实参
function SplitArgs(Text) {
const Out = [];
let Depth = 0;
let Start = 0;
for (let I = 0; I < Text.length; I++) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C;
I++;
while (I < Text.length && Text[I] !== Q) {
if (Text[I] === '\\') I++;
I++;
}
} else if (C === '(' || C === '[' || C === '{') Depth++;
else if (C === ')' || C === ']' || C === '}') Depth--;
else if (C === ',' && Depth === 0) {
Out.push(Text.slice(Start, I).trim());
Start = I + 1;
}
}
Out.push(Text.slice(Start).trim());
return Out;
}
// ---------------------------------------------------------------- 类型解析
// 基础类型注册表的访问器 → 可读类型名
// 来源: module_37036 的 `Wr($_).<accessor> = function () { return this.<xxx>Class; }`
// (旧版 tools/ 里把它误当成 codec 的 name/sid 读取器,这里以源码为准)
const PrimTable = {
mb: 'any',
nb: 'number',
ob: 'nothing',
pb: 'bool',
qb: 'i8',
rb: 'i16',
sb: 'i32',
tb: 'i64',
ub: 'f32',
vb: 'f64',
wb: 'array',
xb: 'string',
yb: 'throwable',
zb: 'boolArray',
ac: 'charArray',
bc: 'i8Array',
cc: 'i16Array',
dc: 'i32Array',
ec: 'f32Array',
fc: 'f64Array',
};
const PrimName = (K) => PrimTable[K] || 'prim:' + K;
// 读取器方法名 → 线级数值宽度(module_72834 的读取器)
const ReaderType = {
xy: 'i8',
n2u: 'i8',
s2t: 'i16',
o2u: 'i16',
t2t: 'i32',
p2u: 'i32',
u2t: 'i64',
q2u: 'i64',
v2t: 'f32',
r2u: 'f32',
w2t: 'f64',
s2u: 'f64',
};
// 类型描述符表达式 → {Type, Raw}
// 描述符类只在别处定义,各模块用局部别名引用,因此先按「角色」识别别名(见 FindRoles)。
// 角色 List → ListCodecInfo(element, optional) → list<...>
// 角色 Enum → EnumCodecInfo(type, optional) → enum<...>
// 角色 Type → TypeCodecInfo(type, optional) → ...
// 角色 KClass→ kClass(X) → X
// 角色 Prim → KClass 注册表实例,X().pb() 取基础类型类 → bool/i32/string/...
// optional 标志是构造器第 2 参:!0 = optional = true
function TypeOfDescriptor(Expr, Roles, Depth) {
const Raw = Expr.trim();
if (Depth > 6) return { Type: '...', Raw };
const E = Raw.replace(/^new\s+/, '');
// 基础类型:X().pb()
const Whole = /^([A-Za-z_$][\w$]*)\(\)\.([A-Za-z0-9_$]+)\(\)$/.exec(E);
if (Whole) return { Type: PrimName(Whole[2]), Raw };
const M = /^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/.exec(E);
if (!M) return { Type: E || '?', Raw };
const Role = Roles[M[1]] || '';
const Args = SplitArgs(M[2]);
const First = Args[0] || '';
const Optional = Args.some((A) => A === '!0');
let Base;
if (Role === 'List') {
Base = 'list<' + TypeOfDescriptor(First, Roles, Depth + 1).Type + '>';
} else {
const Inner = TypeOfDescriptor(First, Roles, Depth + 1).Type;
if (Role === 'Enum') Base = 'enum<' + Inner + '>';
else if (Role === 'KClass') Base = Inner;
else Base = Inner || '?';
}
return { Type: Optional ? 'opt<' + Base + '>' : Base, Raw };
}
// JS 级类型线索(来自 p7g 里对数据类赋值的判定)→ 可读类型名
function JsTypeOf(Seg) {
if (/"boolean" == typeof/.test(Seg)) return 'bool';
if (/"string" == typeof/.test(Seg)) return 'string';
const Inst = /instanceof ([A-Za-z_$][\w$]*)/.exec(Seg);
if (Inst) return Inst[1];
if (/[A-Za-z_$][\w$]*\([A-Za-z_$][\w$]*, [A-Za-z_$][\w$]*\)/.test(Seg)) return 'list';
return '';
}
// ---------------------------------------------------------------- 别名角色识别
// 模块结构(切割后):
// ;(window.__modules__ = window.__modules__ || {})[54199] = (t, n, i) => {
// !(function (t, n, i, r, e, u) { "use strict"; ... })
// (t.exports, i(37036), i(24635), i(88693), i(52943), i(61860));
// };
// 所以「IIFE 形参名 → 依赖模块号」可从句尾的调用实参表读出来。
// 描述符三兄弟定义在 module_24635:
// $_$.e = gn = TypeCodecInfo(type, optional) 单值
// $_$.c = wn = ListCodecInfo(element, optional) 列表
// $_$.b = vn = EnumCodecInfo(type, optional) 枚举
// 基础类型注册表与 kClass 助手在 module_37036:$_$.w4 = j_()、$_$.mf = x_(cls)
const CODEC_INFO_MODULE = '24635';
const KOLIB_MODULE = '37036';
const ROLE_KEYS = {
[CODEC_INFO_MODULE + '::e']: 'Type',
[CODEC_INFO_MODULE + '::c']: 'List',
[CODEC_INFO_MODULE + '::b']: 'Enum',
[KOLIB_MODULE + '::w4']: 'Prim',
[KOLIB_MODULE + '::mf']: 'KClass',
};
// module_03569.js → '3569'
const ModIdOf = (File) => String(Number((/module_(\d+)\.js$/.exec(File) || [, '0'])[1]));
// 形参名 → 依赖模块号
function FindDeps(Src) {
const ParamToMod = {};
const At = Src.lastIndexOf('})(');
if (At < 0) return ParamToMod;
let Close = MatchBracket(Src, At + 2, '(', ')');
if (Close < 0) Close = Src.length;
const Args = SplitArgs(Src.slice(At + 3, Close));
const Head = /\(\s*function\s*\(([^)]*)\)\s*\{/.exec(Src);
if (!Head) return ParamToMod;
const Params = SplitArgs(Head[1]);
for (let I = 0; I < Params.length && I < Args.length; I++) {
const M = /\(\s*(\d+)\s*\)/.exec(Args[I]);
if (M) ParamToMod[Params[I]] = M[1];
}
return ParamToMod;
}
// 本模块导出的类:$_$.<key> = <类名>
function FindExports(Src) {
const Out = {};
const Re = /\(\s*t\.\$_\$\.([A-Za-z0-9_$]+) = ([A-Za-z_$][\w$]*)\s*[,);]/g;
let M;
while ((M = Re.exec(Src))) if (!Out[M[1]]) Out[M[1]] = M[2];
return Out;
}
// 局部别名 → 引用的是哪个模块的哪个导出:<别名> = <形参>.$_$.<key>
// 注意:别名可以是 `$`,所以不能用 \b($ 不是单词字符),改用否定后顾
function FindAliases(Src, ParamToMod) {
const Out = {};
const Re = /(?<![A-Za-z0-9_$.])([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\.\$_\$\.([A-Za-z0-9_$]+)[,;]/g;
let M;
while ((M = Re.exec(Src))) {
const Dep = ParamToMod[M[2]];
if (Dep && !Out[M[1]]) Out[M[1]] = { Mod: Dep, Key: M[3] };
}
return Out;
}
// 局部别名 → 角色(Type/List/Enum/Prim/KClass)
function FindRoles(Src, ParamToMod) {
const Roles = {};
const Aliases = FindAliases(Src, ParamToMod);
for (const [Name, A] of Object.entries(Aliases)) {
const Role = ROLE_KEYS[A.Mod + '::' + A.Key];
if (Role) Roles[Name] = Role;
}
return Roles;
}
// 数据类真名:Kotlin 生成的 toString 里就写着原始类名
// (d(Ft).toString = function () { var t = "ChannelSidAndName ["; ...
function FindClassNames(Src) {
const Out = {};
const Re = /\(\s*[A-Za-z_$][\w$]*\(([A-Za-z_$][\w$]*)\)\.toString = function \(\) \{\s*(?:var [A-Za-z_$][\w$]* = )?"([^"]{1,90})/g;
let M;
while ((M = Re.exec(Src))) {
const N = /^(?:class\s+)?([A-Za-z_][\w$]*)/.exec(M[2]);
if (N && !Out[M[1]]) Out[M[1]] = N[1];
}
return Out;
}
// 数据类工厂:function Ut() { return ((t = M(d(Ft))), Ft.call(t), t); } → Ut 其实是造 Ft
function FindFactories(Src) {
const Out = {};
const Re =
/function ([A-Za-z_$][\w$]*)\(\) \{\s*return \(\(([A-Za-z_$][\w$]*) = [A-Za-z_$][\w$]*\(d\(([A-Za-z_$][\w$]*)\)\)\), \3\.call\(\2\), \2\);/g;
let M;
while ((M = Re.exec(Src))) if (!Out[M[1]]) Out[M[1]] = M[3];
return Out;
}
// ---------------------------------------------------------------- 各段落解析
// 找 `A(Codec).q7g = function (t) {` 形式的块体(o7g 是 (t, n) 两个参数,所以形参表放宽)
function FindBlocks(Src, Prop) {
const Out = [];
const Re = new RegExp('\\b([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*)\\)\\.' + Prop + ' = function \\([^)]*\\)\\s*\\{', 'g');
let M;
while ((M = Re.exec(Src))) {
const Open = Src.indexOf('{', M.index + M[0].length - 1);
const Close = MatchBracket(Src, Open, '{', '}');
if (Close < 0) break;
Out.push({ Codec: M[2], Body: Src.slice(Open + 1, Close) });
Re.lastIndex = Close + 1;
}
return Out;
}
// 字段名 getter: slot → 字段名(模块内唯一)
function FindGetters(Src) {
const SlotToName = {};
const GetterToSlot = {};
const Re =
/function ([A-Za-z_$][\w$]*)\(t\) \{\s*var ([A-Za-z_$][\w$]*) = t\.([a-z0-9]+_\d+);\s*if \(null != \2\) return \2;\s*([A-Za-z_$][\w$]*)\(\s*"(?:codec_)?([A-Za-z][\w]*)"\s*\)/g;
let M;
while ((M = Re.exec(Src))) {
SlotToName[M[3]] = M[5];
GetterToSlot[M[1]] = M[3];
}
return { SlotToName, GetterToSlot };
}
// q7g 块 → [{Slot, Desc, Type, Raw}]
function ParseSlots(Body, Roles) {
const Out = [];
const Re = /this\.([a-z0-9]+_\d+) = t\.n7f\(/g;
let M;
while ((M = Re.exec(Body))) {
const Open = Body.indexOf('(', M.index + M[0].length - 1);
const Close = MatchBracket(Body, Open, '(', ')');
if (Close < 0) break;
const T = TypeOfDescriptor(Body.slice(Open + 1, Close), Roles, 0);
Out.push({ Slot: M[1], Desc: T.Raw, Type: T.Type });
Re.lastIndex = Close + 1;
}
return Out;
}
// p7g 块 → [{Slot|Reader, Prop, Js, Wire}],次序即线上次序
function ParseOrder(Body) {
const Hits = [];
const Re = /([A-Za-z_$][\w$]*)\(this\)\.p7g\(t\)|t\.a7g_1\.([A-Za-z0-9_$]+)\(\)/g;
let M;
while ((M = Re.exec(Body))) Hits.push({ At: M.index, End: Re.lastIndex, Getter: M[1] || '', Reader: M[2] || '' });
const Out = [];
for (let I = 0; I < Hits.length; I++) {
const H = Hits[I];
const From = H.End;
const To = I + 1 < Hits.length ? Hits[I + 1].At : Math.min(Body.length, H.End + 300);
const Seg = Body.slice(From, To);
let Prop = '';
let Js = '';
if (H.Getter) {
// 有名字段:赋值紧跟 getter 调用之后
const A = /\b([A-Za-z_$][\w$]*)\.([a-z0-9]+_\d+) =/.exec(Seg);
if (A) Prop = A[2];
Js = JsTypeOf(Seg);
Out.push({ Slot: '', Name: '', Prop, Js, Wire: '', Getter: H.Getter, Order: I + 1 });
} else {
// 匿名字段:赋值紧贴在读取调用之前
const Before = Body.slice(Math.max(0, H.At - 120), H.At);
const A = /([A-Za-z_$][\w$]*)\.([a-z0-9]+_\d+) = $/.exec(Before);
if (A) Prop = A[2];
Out.push({ Slot: '', Name: '', Prop, Js: '', Wire: ReaderType[H.Reader] || H.Reader, Getter: '', Order: I + 1 });
}
}
return Out;
}
// ---------------------------------------------------------------- 与注册表 join
// names/regdump.json(由 tools/client_registry_dump.js 从运行中的客户端导出)里 ModelReg 的表(语义已核实):
// u7n_1: modelId → 模型类(ctor = 类名;keys 是枚举到的实例属性,不保证等于 codec 槽位)
// w7n_1: modelId → codec(ctor = codec 名;keys = 该 codec 的槽位名,只有被实例化过才有值)
// v7n_1: methodId → 命令类(ctor = 命令类名;text = 该命令负载所属的 modelId)
// 于是:
// ① 用「槽位名有序列表」在 w7n_1 里查 → 拿到 modelId(同时校验 codec 构造器名)
// ② 若该 codec 没被实例化(keys 为空)则退化为「codec 构造器名唯一匹配」
// ③ 再用 modelId 去 v7n_1 反查 methodId 列表(下行方法表)
function JoinRegistries(Codecs, RegPath) {
if (!fs.existsSync(RegPath)) return { Ok: false, Reason: '找不到 ' + RegPath };
const Reg = JSON.parse(fs.readFileSync(RegPath, 'utf8'));
const Maps = Reg.maps || Reg;
const Entries = (Name) => (Maps[Name] && Maps[Name].entries) || [];
const CodecByModel = new Map(); // modelId → codec 构造器名
const ModelBySig = new Map(); // 槽位签名 → modelId
for (const E of Entries('ModelReg.w7n_1')) {
CodecByModel.set(String(E.key), E.ctor || '');
if (E.keys && E.keys.length) ModelBySig.set(E.keys.join(','), String(E.key));
}
const ModelClass = new Map(); // modelId → 模型类名
for (const E of Entries('ModelReg.u7n_1')) ModelClass.set(String(E.key), E.ctor || '');
const MethodByModel = new Map(); // modelId → [methodId]
const CommandClass = new Map(); // methodId → 命令类名
for (const E of Entries('ModelReg.v7n_1')) {
const Id = String(E.key);
CommandClass.set(Id, E.ctor || '');
if (!E.text) continue;
const M = String(E.text);
if (!MethodByModel.has(M)) MethodByModel.set(M, []);
MethodByModel.get(M).push(Id);
}
// codec 构造器名 → [modelId](仅当全局唯一时才敢用于兜底)
const ModelsByCtor = new Map();
for (const [Id, Ctor] of CodecByModel) {
if (!Ctor) continue;
if (!ModelsByCtor.has(Ctor)) ModelsByCtor.set(Ctor, []);
ModelsByCtor.get(Ctor).push(Id);
}
// 本次扫到的 codec 里,构造器名也必须唯一(压缩后跨模块重名极常见,如 st/ht/V)
const CtorCount = {};
for (const C of Object.values(Codecs)) CtorCount[C.Codec] = (CtorCount[C.Codec] || 0) + 1;
const Conflicts = [];
let BySigHit = 0;
let ByCtorHit = 0;
for (const [Key, C] of Object.entries(Codecs)) {
const Sig = (C.Slots || []).map((S) => S.Slot).join(',');
let ModelId = Sig ? ModelBySig.get(Sig) : undefined;
if (ModelId) {
BySigHit++;
const Expect = CodecByModel.get(ModelId);
if (Expect && Expect !== C.Codec) Conflicts.push(Key + ' 签名命中 ' + ModelId + ' 但 codec 名不符(期望 ' + Expect + ')');
} else if (CtorCount[C.Codec] === 1) {
// 兜底:codec 构造器名在注册表与本轮扫描里都唯一
const Cand = ModelsByCtor.get(C.Codec);
if (Cand && Cand.length === 1) {
ModelId = Cand[0];
ByCtorHit++;
}
}
if (!ModelId) continue;
C.ModelId = ModelId;
C.ModelClass = ModelClass.get(ModelId) || '';
C.MethodId = MethodByModel.get(ModelId) || [];
}
const Count = { ModelId: 0, MethodId: 0 };
for (const C of Object.values(Codecs)) {
if (C.ModelId) Count.ModelId++;
if (C.MethodId && C.MethodId.length) Count.MethodId++;
}
return {
Ok: true,
CodecEntries: CodecByModel.size,
MethodEntries: CommandClass.size,
BySigHit,
ByCtorHit,
Count,
Conflicts,
};
}
// ---------------------------------------------------------------- 类名还原
// 字段类型里出现的裸局部类名(Ft / en / Wn / ni / $ …)需要还原成 Kotlin 原名:
// ① 本模块内定义的数据类 → 看 (d(X).toString = ...) 里的字符串
// ② 从别的模块 import 的类 → 先查别名(X = 形参.$_$.key)定位到「模块+导出键」,
// 再取那个模块的导出表拿到它本地的类名,最后同样查它自己的 toString
// ③ 没有 toString 的类(非 data class)→ 用「模型类 → codec」反查,
// 这样即使名字还原不了,也至少能指向解码该嵌套模型用的 codec
const PRIM_WORDS = new Set(
Object.values(PrimTable).concat(['i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'bytes', 'opt', 'list', 'enum'])
);
// 返回 { Name, Via, Mod, Class };Via 为空表示没还原出原名
function ResolveClass(Name, ModId, ModuleInfo, Seen) {
const Info = ModuleInfo[ModId];
const Self = { Name, Via: '', Mod: ModId, Class: Name };
if (!Info) return Self;
if (Info.Classes[Name]) return { Name: Info.Classes[Name], Via: 'local', Mod: ModId, Class: Name };
const A = Info.Aliases[Name];
if (!A) return Self;
const Dep = ModuleInfo[A.Mod];
const Cls = Dep && Dep.Exports[A.Key];
if (!Cls) return Self;
if (Dep.Classes[Cls]) return { Name: Dep.Classes[Cls], Via: 'import', Mod: A.Mod, Class: Cls };
// 导出的类自己也可能只是别处的再导出 → 顺着别名链再走一层
if (!Seen.includes(A.Mod)) {
const R = ResolveClass(Cls, A.Mod, ModuleInfo, Seen.concat(A.Mod));
if (R.Via) return R;
}
return { Name: Cls, Via: 'import-raw', Mod: A.Mod, Class: Cls };
}
// 单个别名 → { Name, Codec }(Codec = 该类的 codec 键,供递归解码用)
// 名字优先用 codec 的模型真名;拿不到就退回 toString 得到的名字
function ResolveOne(Name, ModId, ModuleInfo, ClassToCodec) {
const R = ResolveClass(Name, ModId, ModuleInfo, []);
const CodecKey = ClassToCodec[R.Mod + '::' + R.Class] || '';
if (CodecKey) return { Name: Codecs[CodecKey].ModelName || R.Name, Codec: CodecKey };
return { Name: R.Name, Codec: '' };
}
// 类型串 → { Type, Codecs };把里面的裸类名逐个替换掉
function ResolveType(Type, ModId, ModuleInfo, ClassToCodec) {
const Refs = [];
const Out = String(Type || '').replace(/[A-Za-z_$][\w$]*/g, (W) => {
if (PRIM_WORDS.has(W)) return W;
const R = ResolveOne(W, ModId, ModuleInfo, ClassToCodec);
if (R.Codec) Refs.push(R.Codec);
// 协议核心类没有 codec 时按「线上基础类型」换算(如实体引用 xr → i64),否则保留类名
else if (WireClasses[R.Name]) return WireClasses[R.Name];
return R.Name;
});
return { Type: Out, Codecs: Refs };
}
// ---------------------------------------------------------------- 第一遍:建全模块符号表
const ModuleInfo = {};
for (const File of ModuleFiles) {
let Src;
try {
Src = fs.readFileSync(File, 'utf8');
} catch (Err) {
continue;
}
const ModId = ModIdOf(File);
const Deps = FindDeps(Src);
ModuleInfo[ModId] = {
File,
Aliases: FindAliases(Src, Deps),
Roles: FindRoles(Src, Deps),
Exports: FindExports(Src),
Classes: FindClassNames(Src),
Factories: FindFactories(Src),
};
}
// --who <模块号>:<导出键>:反查哪些模块以什么别名引用了该符号,以及 `new 别名(` 的出现次数
// 例:node extract_codec_fields.js --who 61860:l → 谁在用 SpaceCommand
const Who = Arg('who', '');
if (Who) {
const [WMod, WKey] = Who.split(':');
let Total = 0;
for (const [ModId, Info] of Object.entries(ModuleInfo)) {
for (const [Name, A] of Object.entries(Info.Aliases)) {
if (A.Mod !== WMod || A.Key !== WKey) continue;
const Src2 = fs.readFileSync(Info.File, 'utf8');
const Esc = Name.replace(/\$/g, '\\$');
const News = (Src2.match(new RegExp('new ' + Esc + '\\(', 'g')) || []).length;
const Calls = (Src2.match(new RegExp('(?<![\\w$.])' + Esc + '\\(', 'g')) || []).length;
Total += News;
console.log('module_' + ModId + ' 别名 ' + Name + ' new ' + News + ' 次,调用 ' + Calls + ' 次');
}
}
console.log('合计 new ' + Total + ' 次');
}
// ---------------------------------------------------------------- 协议核心类 → 线上基础类型
// module_61860 用 `vi(t).m7f(G(类), new Codec())` 给协议核心类注册 codec。这类 codec 的 o7g 往往只调一次
// 写入器,于是「类 → 线上宽度」能直接从 o7g 的写入方法读出:
// 实体引用 xr 的 codec cr → t.a7g_1.f2u(...) → i64
// 只认「恰好一次写入」的标量 codec;向量(fi 写 3 次 l2u)、嵌套命令(or/fr)会被自动跳过。
const WRITER_TYPE = {
x2t: 'i8', i2u: 'i8',
c2u: 'i16', j2u: 'i16',
d2u: 'i32', k2u: 'i32',
f2u: 'i64', p1s: 'i64',
g2u: 'f32', l2u: 'f32',
h2u: 'f64', m2u: 'f64',
};
const WireClasses = {};
for (const File of ModuleFiles) {
let Src;
try {
Src = fs.readFileSync(File, 'utf8');
} catch (Err) {
continue;
}
const Roles = (ModuleInfo[ModIdOf(File)] || {}).Roles || {};
const ByCodec = new Map(FindBlocks(Src, 'o7g').map((B) => [B.Codec, B.Body]));
if (!ByCodec.size) continue;
const Re = /\.m7f\(\s*([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)\s*,\s*new ([A-Za-z_$][\w$]*)\(/g;
let M;
while ((M = Re.exec(Src))) {
if ((Roles[M[1]] || '') !== 'KClass') continue; // 第一参必须是 kClass(类)
const Body = ByCodec.get(M[3]);
if (!Body) continue;
// 只认「整个 o7g 里恰好调一次写入器」的标量 codec:
// 向量(fi 写 3 次 l2u)、嵌套命令(fr 写 2 次 f2u)都因次数不为 1 被跳过
const Calls = [];
for (const W of Body.matchAll(/(?<![\w$.])[A-Za-z_$][\w$]*\.[A-Za-z0-9_$]+\.([A-Za-z0-9_$]+)\(/g))
if (WRITER_TYPE[W[1]]) Calls.push(W[1]);
if (Calls.length !== 1) continue;
const Type = WRITER_TYPE[Calls[0]];
if (Type && !WireClasses[M[2]]) WireClasses[M[2]] = Type;
}
}
// ---------------------------------------------------------------- 第二遍:抽 codec
const Codecs = {};
let ModulesUsed = 0;
let TypeResolved = 0;
let TypeUnresolved = 0;
for (const File of ModuleFiles) {
let Src;
try {
Src = fs.readFileSync(File, 'utf8');
} catch (Err) {
continue;
}
const Mod = path.basename(File, '.js');
const ModId = ModIdOf(File);
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
const Factories = (ModuleInfo[ModId] || {}).Factories || {};
const { SlotToName, GetterToSlot } = FindGetters(Src);
const SlotBlocks = FindBlocks(Src, 'q7g');
const OrderBlocks = FindBlocks(Src, 'p7g');
if (!SlotBlocks.length && !OrderBlocks.length) continue;
ModulesUsed++;
const OrderByCodec = new Map(OrderBlocks.map((B) => [B.Codec, B.Body]));
const Names = new Set([...SlotBlocks.map((B) => B.Codec), ...OrderBlocks.map((B) => B.Codec)]);
for (const CodecName of Names) {
const SBs = SlotBlocks.filter((B) => B.Codec === CodecName);
const Slots = [];
for (const B of SBs) for (const S of ParseSlots(B.Body, Roles)) Slots.push(S);
const BySlot = new Map(Slots.map((S) => [S.Slot, S]));
const OrderBody = OrderByCodec.get(CodecName) || '';
const Order = ParseOrder(OrderBody);
const Fields = Order.map((O) => {
let Name = '(匿名)';
let Type = O.Wire || '?';
let Desc = '';
let Slot = '';
if (O.Getter) {
Slot = GetterToSlot[O.Getter] || '';
Name = SlotToName[Slot] || '?';
const S = BySlot.get(Slot);
if (S) {
Desc = S.Desc;
Type = S.Type;
} else if (O.Js) Type = O.Js;
}
return { Order: O.Order, Name, Type, Js: O.Js, Slot, Desc, Prop: O.Prop, Wire: O.Wire };
});
// 模型类:优先看 o7g 里的 instanceof(直接是数据类),其次看 p7g 用的工厂函数(可能是 Ut → Ft)
const O7g = FindBlocks(Src, 'o7g').find((B) => B.Codec === CodecName);
const Inst = O7g && /instanceof ([A-Za-z_$][\w$]*)/.exec(O7g.Body);
const Made = /var [A-Za-z_$][\w$]* = ([A-Za-z_$][\w$]*)\(\)/.exec(OrderBody);
const DataClass = (Inst && Inst[1]) || (Made ? Made[1] : '');
const Key = Mod + '::' + CodecName;
Codecs[Key] = {
Module: Mod,
ModuleId: ModId,
Codec: CodecName,
ModelClassLocal: DataClass,
ModelClassReal: DataClass ? Factories[DataClass] || DataClass : '',
ModelName: '',
Slots,
Fields,
ModelKeys: Fields.map((F) => F.Prop).filter(Boolean),
};
}
}
// ---------------------------------------------------------------- 第三遍:还原类名与嵌套 codec 链接
// 「模块::类名 → codec」——同一模块内类名唯一,足以定位解码该嵌套模型要用哪个 codec
const ClassToCodec = {};
for (const [Key, C] of Object.entries(Codecs)) {
if (C.ModelClassLocal) ClassToCodec[C.ModuleId + '::' + C.ModelClassLocal] = Key;
if (C.ModelClassReal) ClassToCodec[C.ModuleId + '::' + C.ModelClassReal] = Key;
}
for (const C of Object.values(Codecs)) {
const ModId = C.ModuleId;
C.ModelName = C.ModelClassReal ? ResolveClass(C.ModelClassReal, ModId, ModuleInfo, []).Name : '';
for (const F of C.Fields) {
const Raw = F.Type;
const R = ResolveType(Raw, ModId, ModuleInfo, ClassToCodec);
F.TypeRaw = Raw;
F.Type = R.Type;
if (R.Codecs.length) F.TypeCodec = R.Codecs[0];
if (F.Type !== Raw) TypeResolved++;
else if (!PRIM_WORDS.has(Raw) && !/^prim:/.test(Raw)) TypeUnresolved++;
}
}
// ---------------------------------------------------------------- 第四遍:上行命令表
// 上行命令在源码里的形态(见 module_85183 / 10283 / 47243 / 85667 …):
// ① 命令类构造函数里放着「methodId 长整型字面量」和「负载 codec」:
// function mq() {
// ((this.zl3_1 = new vi(Mn(fi), null)), (this.bl3_1 = new Xn(1724953279, 829170552)));
// } ↑ hi*2^32 + lo(无符号) = methodId
// ② 每个发送方法按顺序把负载字段写进共享缓冲,再包成 SpaceCommand 发出去:
// (p(xn).sh1 = function (t, n) {
// (this.kh1_1.o7g(this.uh0_1, t), this.lh1_1.o7g(this.uh0_1, n)); ← 负载字段(有序)
// var i = new U(spaceId, this.jh1_1, this.uh0_1), … ← SpaceCommand(objectId, methodId, buffer)
// ③ 负载字段的类型声明与下行同源:(n.kh1_1 = jn(n).n7f(new w(v().pb(), !1)))
const LongOf = (Lo, Hi) => (BigInt(Hi) * 4294967296n + BigInt(Lo >>> 0)).toString();
// 模块内「字段 → {Long, Desc}」:Long 来自两整数构造的字面量,Desc 来自 .n7f(new …)
function FindFieldInfo(Src, Roles) {
const Long = {};
const ReLong = /this\.([a-z0-9]+_\d+) = new ([A-Za-z_$][\w$]*)\((-?\d+), (-?\d+)\)/g;
let M;
while ((M = ReLong.exec(Src)))
Long[M[1]] = { Cls: M[2], Lo: Number(M[3]), Hi: Number(M[4]), Value: LongOf(Number(M[3]), Number(M[4])) };
const Desc = {};
const Re = /\.n7f\(/g;
while ((M = Re.exec(Src))) {
const Open = M.index + M[0].length - 1;
const Close = MatchBracket(Src, Open, '(', ')');
if (Close < 0) break;
const Before = Src.slice(Src.lastIndexOf('\n', M.index) + 1, M.index);
const A = /([a-z0-9]+_\d+) = [^\n]{0,60}?$/.exec(Before);
if (A && !Desc[A[1]]) Desc[A[1]] = TypeOfDescriptor(Src.slice(Open + 1, Close), Roles, 0);
Re.lastIndex = Close + 1;
}
return { Long, Desc };
}
// 找 `X(Cls).Fn = function (…) { … }` 的方法块(用于定位发送点的宿主方法与宿主类)
function FindMethods(Src) {
const Out = [];
const Re = /\(?([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)\.([A-Za-z_$][\w$]*) = function \([^)]*\)\s*\{/g;
let M;
while ((M = Re.exec(Src))) {
const Open = Src.indexOf('{', M.index + M[0].length - 1);
const Close = MatchBracket(Src, Open, '{', '}');
if (Close < 0) break;
Out.push({ Owner: M[2], Name: M[3], BodyStart: Open + 1, End: Close });
Re.lastIndex = Close + 1;
}
return Out;
}
// 命令类构造函数里登记的 codec/model:new <Codec>(<kClass>(<Model>), null)
function OwnerModels(Src, Owner, ModId) {
if (!Owner) return [];
const At = Src.search(new RegExp('\\bfunction ' + Owner.replace(/\$/g, '\\$') + '\\('));
if (At < 0) return [];
const Open = Src.indexOf('{', At);
const Close = MatchBracket(Src, Open, '{', '}');
if (Close < 0) return [];
const Body = Src.slice(Open + 1, Close);
const Out = [];
for (const G of Body.matchAll(/\(([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)/g)) {
if (!Out.includes(G[2])) Out.push(G[2]);
}
return Out.map((N) => ({ Local: N, Name: ResolveClass(N, ModId, ModuleInfo, []).Name }));
}
const UplinkCommands = [];
const UpSeen = new Set();
const UpSendRe = /new ([A-Za-z_$][\w$]*)\([^;]*?this\.([a-z0-9]+_\d+), this\.([a-z0-9]+_\d+)\)/g;
for (const File of ModuleFiles) {
let Src;
try {
Src = fs.readFileSync(File, 'utf8');
} catch (Err) {
continue;
}
const ModId = ModIdOf(File);
const Mod = path.basename(File, '.js');
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
const Classes = (ModuleInfo[ModId] || {}).Classes || {};
const { Long, Desc } = FindFieldInfo(Src, Roles);
if (!Object.keys(Long).length) continue;
const Methods = FindMethods(Src);
let M;
UpSendRe.lastIndex = 0;
while ((M = UpSendRe.exec(Src))) {
const Inf = Long[M[2]];
if (!Inf) continue; // 第二个实参必须是本模块里的长整型字面量字段,否则不是 SpaceCommand
const At = M.index;
const Host = Methods.find((X) => At > X.BodyStart && At < X.End);
// 负载字段:宿主方法里、发送点之前的所有 this.<codec>.o7g( 接收者,按出现顺序
const Fields = [];
if (Host) {
const Head = Src.slice(Host.BodyStart, At);
for (const G of Head.matchAll(/this\.([a-z0-9]+_\d+)\.o7g\(/g)) {
const D = Desc[G[1]];
const T = D ? ResolveType(D.Type, ModId, ModuleInfo, ClassToCodec) : { Type: '?', Codecs: [] };
Fields.push({ Field: G[1], Type: T.Type, TypeCodec: T.Codecs[0] || '', Desc: D ? D.Raw : '' });
}
}
const Key = Inf.Value + '|' + Mod + '|' + (Host ? Host.Owner + '.' + Host.Name : '');
if (UpSeen.has(Key)) continue;
UpSeen.add(Key);
UplinkCommands.push({
MethodId: Inf.Value,
LongLiteral: [Inf.Lo, Inf.Hi],
Module: Mod,
OwnerClass: Host ? Host.Owner : '',
OwnerName: Host ? Classes[Host.Owner] || '' : '',
SendFn: Host ? Host.Name : '',
BufferField: M[3],
Models: OwnerModels(Src, Host ? Host.Owner : '', ModId),
Fields,
});
}
}
// ---------------------------------------------------------------- 写出与统计
const RegPath = path.resolve(Arg('reg', path.join(PROJECT, 'names', 'regdump.json')));
const Join = JoinRegistries(Codecs, RegPath);
const AllFields = Object.values(Codecs).reduce((A, C) => A.concat(C.Fields), []);
const Named = AllFields.filter((F) => F.Name !== '(匿名)' && F.Name !== '?');
const Unnamed = AllFields.filter((F) => F.Name === '(匿名)');
const Unknown = Named.filter((F) => F.Name === '?');
const TypeCount = {};
for (const F of AllFields) TypeCount[F.Type] = (TypeCount[F.Type] || 0) + 1;
const Out = {
Note: '空间层 codec 字段表(键 = 模块::codec变量名)。字段名来自 v("codec_名字");类型来自 q7g 描述符(已把裸局部类名还原成 Kotlin 原名,TypeRaw 保留原名);Js= p7g 里的 JS 级线索;Prop= 数据类属性名;TypeCodec= 该嵌套模型对应的 codec 键;ModelId/MethodId 来自 regdump.json join',
Scan,
Modules: ModuleFiles.length,
ModulesWithCodec: ModulesUsed,
CodecCount: Object.keys(Codecs).length,
FieldCount: AllFields.length,
NamedCount: Named.length,
AnonymousCount: Unnamed.length,
WireClasses,
Join,
Codecs,
};
fs.writeFileSync(OutPath, JSON.stringify(Out, null, 1), 'utf8');
// 上行命令表单独落盘(names/uplink_table.json)
const UpOutPath = path.resolve(Arg('up-out', path.join(PROJECT, 'names', 'uplink_table.json')));
const UpOut = {
Note:
'上行命令表(methodId → 命令类 / 发送方法 / 负载字段)。methodId = hi*2^32 + (lo>>>0),由源码里的 Long 字面量还原。' +
'字段只有 Kotlin 局部 slot 名(如 bcq_1)+ 类型:命令类不像模型 codec 那样保留 codec_<名字> getter,' +
'555 个字段全都没有属性访问点,压缩后已无原可读名可挖(要可读名只能人工标注或靠调用点语义推断)',
Count: UplinkCommands.length,
Commands: UplinkCommands.sort((A, B) => (BigInt(A.MethodId) < BigInt(B.MethodId) ? -1 : 1)),
};
fs.writeFileSync(UpOutPath, JSON.stringify(UpOut, null, 1), 'utf8');
console.log('扫描模块 ' + ModuleFiles.length + ' 个(含 codec ' + ModulesUsed + ' 个)');
console.log('codec ' + Object.keys(Codecs).length + ' 个;字段 ' + AllFields.length + ' 条(有名 ' + Named.length + ',匿名 ' + Unnamed.length + ',未解 ' + Unknown.length + ')');
console.log('已写出 ' + OutPath);
console.log('类型分布: ' + Object.entries(TypeCount).sort((A, B) => B[1] - A[1]).slice(0, 18).map(([K, V]) => K + '=' + V).join(' '));
if (Join.Ok)
console.log(
'注册表 join: codec 表 ' + Join.CodecEntries + ' 条 / 方法表 ' + Join.MethodEntries + ' 条;对上 modelId ' +
Join.Count.ModelId + ' 个 codec(槽位签名 ' + Join.BySigHit + ' + 构造器名兜底 ' + Join.ByCtorHit +
'),其中能反查到 methodId 的 ' + Join.Count.MethodId + ' 个' +
(Join.Conflicts.length ? ' ⚠ 冲突 ' + Join.Conflicts.length + ' 处: ' + Join.Conflicts.slice(0, 3).join('; ') : ' ✓ 无冲突')
);
else console.log('注册表 join: 跳过(' + Join.Reason + ')');
// 自检:字段的原始描述符里若还残留未识别形状(既不是基础类型也不是 X(Y) 也不是 new X(...)),列出来看
const Suspicious = new Set();
for (const K of Object.keys(Codecs)) {
for (const F of Codecs[K].Fields) {
const D = (F.Desc || '').replace(/^new\s+/, '');
if (!D) continue;
if (/^[A-Za-z_$][\w$]*\(\)\.[A-Za-z0-9_$]+\(\)$/.test(D)) continue; // X().pb()
if (/^[A-Za-z_$][\w$]*\([\s\S]*\)$/.test(D)) continue; // X(...)
Suspicious.add(F.Desc);
}
}
console.log('未识别描述符形状: ' + (Suspicious.size ? [...Suspicious].slice(0, 8).join(' | ') : '无'));
console.log('类型裸类名还原: 成功 ' + TypeResolved + ' 条,仍为裸名 ' + TypeUnresolved + ' 条(多半是没有 toString 的非 data class 资源类)');
const NamedCodecs = Object.values(Codecs).filter((C) => C.ModelName).length;
const Refed = AllFields.filter((F) => F.TypeCodec).length;
console.log('codec 的模型类真名: ' + NamedCodecs + ' / ' + Object.keys(Codecs).length + ' 个;字段里带嵌套 codec 链接的 ' + Refed + ' 条');
const UpWithFields = UplinkCommands.filter((C) => C.Fields.length).length;
console.log('上行命令表: ' + UplinkCommands.length + ' 条(带负载字段的 ' + UpWithFields + ' 条)→ ' + UpOutPath);
// --find-up <methodId>:查一条上行命令的负载字段
const FindUp = Arg('find-up', '');
if (FindUp) {
const Hit = UplinkCommands.filter((C) => C.MethodId === FindUp);
if (!Hit.length) console.log('\n上行 methodId ' + FindUp + ' 未在源码里找到');
for (const C of Hit) {
console.log('\n上行 ' + C.MethodId + ' [' + C.LongLiteral.join(', ') + '] ' + C.Module + ' ' + C.OwnerClass + '(' + (C.OwnerName || '?') + ').' + C.SendFn);
if (C.Models.length) console.log(' 命令类登记的模型: ' + C.Models.map((X) => X.Local + (X.Name && X.Name !== X.Local ? '(' + X.Name + ')' : '')).join(', '));
for (const [I, F] of C.Fields.entries()) console.log(' ' + (I + 1) + '. ' + F.Type.padEnd(28) + ' ' + F.Field + ' ' + F.Desc);
if (!C.Fields.length) console.log(' (负载为空)');
}
}
const Show = Arg('show', 'st,ft,ht,dt,V').split(',');
for (const C of Show) {
const Hit = Object.keys(Codecs).filter((K) => K === C || K.endsWith('::' + C) || K.includes('::' + C));
for (const K of Hit.slice(0, 4)) {
const Cd = Codecs[K];
const Ids = [
Cd.ModelName ? '模型=' + Cd.ModelName : '',
Cd.ModelId ? 'modelId=' + Cd.ModelId : '',
Cd.ModelClass ? 'modelClass=' + Cd.ModelClass : '',
Cd.MethodId && Cd.MethodId.length ? 'methodId=' + Cd.MethodId.join(',') : '',
].filter(Boolean).join(' ');
console.log('\n=== codec ' + K + '(' + Cd.Fields.length + ' 字段)' + (Ids ? ' ' + Ids : '') + ' ===');
for (const F of Cd.Fields) {
console.log(
' ' + String(F.Order).padStart(2) + '. ' + String(F.Name).padEnd(32) + ' ' + String(F.Type).padEnd(16) +
' js=' + String(F.Js || '-').padEnd(10) + ' ' + F.Slot + ' ' + (F.Desc || '')
);
}
}
}
// --find-model <modelId> / --find-method <methodId>:反查 codec 并打印字段表
for (const [Opt, Field, Label] of [
['find-model', 'ModelId', 'modelId'],
['find-method', 'MethodId', 'methodId'],
]) {
const Id = Arg(Opt, '');
if (!Id) continue;
const Hit = Object.entries(Codecs).filter(([, C]) => {
const V = C[Field];
return Array.isArray(V) ? V.includes(Id) : V === Id;
});
console.log('\n' + Label + ' = ' + Id + ' → ' + (Hit.length ? Hit.map(([K, C]) => K + '(' + C.Codec + ', ' + C.Fields.length + ' 字段)').join(' ') : '无匹配'));
for (const [K, C] of Hit.slice(0, 2)) {
for (const F of C.Fields) console.log(' ' + String(F.Order).padStart(2) + '. ' + String(F.Name).padEnd(30) + ' ' + F.Type);
}
}