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/(含数据库口令)不进库,可用上述工具重新生成。
This commit is contained in:
1 parent
03c2fb8b69
commit
9f6dcb0eba
61 files changed
+93297
-458
No files matched your search
@@ -1,9 +1,10 @@
|
||||
// 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 片段
|
||||
// node extract_codec_fields.js // 扫 src/ 全部模块(默认)
|
||||
// node extract_codec_fields.js --scan // 同上(保留这个写法)
|
||||
// node extract_codec_fields.js --module ../src/module_54199.js // 只扫一个模块(调试用)
|
||||
// node extract_codec_fields.js --show st,ft // 打印指定 codec 的字段表
|
||||
// node extract_codec_fields.js --find-method <id> // 反查 methodId 的字段表
|
||||
//
|
||||
// 原理(Kotlin/JS 协议 codec 的三段式结构;codec 变量名只在本模块内唯一,故键 = 模块::变量名):
|
||||
// ① 字段名 getter: function G(t) { var n = t.<slot>; if (null != n) return n; v("codec_<字段名>"); }
|
||||
@@ -16,7 +17,9 @@
|
||||
// 位宽: 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
|
||||
// ⑤ 顶层 SlotTypes:全库扫 `<接收者>.<槽位> = <工厂>(<接收者>).n7f(new <描述符>)`(q7g 之外的局部 codec 实例),
|
||||
// 供下行字段表定位「q7i 分支里读的那个槽位是什么类型」
|
||||
// 产物: names/codec_fields.json(+ names/uplink_table.json)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -28,14 +31,18 @@ const Arg = (Name, 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 ModuleArg = Arg('module', '');
|
||||
// 默认就是整库扫:本脚本的产物 names/codec_fields.json 永远是「全库表」,
|
||||
// 只扫一个模块会把它覆盖成残表(下游 mine_downlink / lib_space_codec 会直接失效),
|
||||
// 所以「只扫单模块」必须显式给 --module(或 --scan 与 --module 并存时以 --scan 为准)。
|
||||
const Scan = Argv.indexOf('--scan') >= 0 || !ModuleArg;
|
||||
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')))];
|
||||
: [path.resolve(ModuleArg)];
|
||||
|
||||
// ---------------------------------------------------------------- 通用工具
|
||||
// 配平扫描:从 Open 处的开括号找到对应闭括号(跳过字符串字面量)
|
||||
@@ -84,6 +91,22 @@ function SplitArgs(Text) {
|
||||
return Out;
|
||||
}
|
||||
|
||||
// 偏移量 → 行号:SlotTypes 的 Desc 必须能指回源码(如 module_10283.js:2343),否则没法核证据
|
||||
function MakeLineIndex(Text) {
|
||||
const Starts = [0];
|
||||
for (let I = 0; I < Text.length; I++) if (Text[I] === '\n') Starts.push(I + 1);
|
||||
return (Off) => {
|
||||
let Lo = 0;
|
||||
let Hi = Starts.length - 1;
|
||||
while (Lo < Hi) {
|
||||
const Mid = (Lo + Hi + 1) >> 1;
|
||||
if (Starts[Mid] <= Off) Lo = Mid;
|
||||
else Hi = Mid - 1;
|
||||
}
|
||||
return Lo + 1;
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 类型解析
|
||||
// 基础类型注册表的访问器 → 可读类型名
|
||||
// 来源: module_37036 的 `Wr($_).<accessor> = function () { return this.<xxx>Class; }`
|
||||
@@ -132,6 +155,7 @@ const ReaderType = {
|
||||
// 描述符类只在别处定义,各模块用局部别名引用,因此先按「角色」识别别名(见 FindRoles)。
|
||||
// 角色 List → ListCodecInfo(element, optional) → list<...>
|
||||
// 角色 Enum → EnumCodecInfo(type, optional) → enum<...>
|
||||
// 角色 Map → MapCodecInfo(key, value, optional) → map<K, V>
|
||||
// 角色 Type → TypeCodecInfo(type, optional) → ...
|
||||
// 角色 KClass→ kClass(X) → X
|
||||
// 角色 Prim → KClass 注册表实例,X().pb() 取基础类型类 → bool/i32/string/...
|
||||
@@ -152,6 +176,15 @@ function TypeOfDescriptor(Expr, Roles, Depth) {
|
||||
let Base;
|
||||
if (Role === 'List') {
|
||||
Base = 'list<' + TypeOfDescriptor(First, Roles, Depth + 1).Type + '>';
|
||||
} else if (Role === 'Map') {
|
||||
// map 描述符:MapCodecInfo(keyCodec, valueCodec, optional)(src/module_24635.js:262-264 的 pn 构造函数:
|
||||
// this.t7h_1 = key、this.u7h_1 = value,第 3 参 optional 走 cn.call(this, i))。
|
||||
// 真正上线的是 src/module_24635.js:489-512 的 Jt:o7g/p7g 先写/读一个变长条数(w.s7g/w.t7g,
|
||||
// 与 list/string 同一个长度编解码,见 src/module_24635.js:589-611),再逐条写/读 key、value。
|
||||
// 所以线上形状 = map<K, V>,K/V 各自按自己的描述符编码。
|
||||
const K = TypeOfDescriptor(Args[0] || '', Roles, Depth + 1).Type;
|
||||
const V = TypeOfDescriptor(Args[1] || '', Roles, Depth + 1).Type;
|
||||
Base = 'map<' + (K || '?') + ', ' + (V || '?') + '>';
|
||||
} else {
|
||||
const Inner = TypeOfDescriptor(First, Roles, Depth + 1).Type;
|
||||
if (Role === 'Enum') Base = 'enum<' + Inner + '>';
|
||||
@@ -182,6 +215,8 @@ function JsTypeOf(Seg) {
|
||||
// $_$.e = gn = TypeCodecInfo(type, optional) 单值
|
||||
// $_$.c = wn = ListCodecInfo(element, optional) 列表
|
||||
// $_$.b = vn = EnumCodecInfo(type, optional) 枚举
|
||||
// $_$.d = pn = MapCodecInfo(key, value, optional) 映射(README 里说的「三兄弟」其实有四兄弟,
|
||||
// d 这一支以前没被认出来,于是 map 字段被当成 enum 之类按 4 字节读 → 负载错位)
|
||||
// 基础类型注册表与 kClass 助手在 module_37036:$_$.w4 = j_()、$_$.mf = x_(cls)
|
||||
const CODEC_INFO_MODULE = '24635';
|
||||
const KOLIB_MODULE = '37036';
|
||||
@@ -189,6 +224,7 @@ const ROLE_KEYS = {
|
||||
[CODEC_INFO_MODULE + '::e']: 'Type',
|
||||
[CODEC_INFO_MODULE + '::c']: 'List',
|
||||
[CODEC_INFO_MODULE + '::b']: 'Enum',
|
||||
[CODEC_INFO_MODULE + '::d']: 'Map',
|
||||
[KOLIB_MODULE + '::w4']: 'Prim',
|
||||
[KOLIB_MODULE + '::mf']: 'KClass',
|
||||
};
|
||||
@@ -439,7 +475,7 @@ function JoinRegistries(Codecs, RegPath) {
|
||||
// ③ 没有 toString 的类(非 data class)→ 用「模型类 → codec」反查,
|
||||
// 这样即使名字还原不了,也至少能指向解码该嵌套模型用的 codec
|
||||
const PRIM_WORDS = new Set(
|
||||
Object.values(PrimTable).concat(['i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'bytes', 'opt', 'list', 'enum'])
|
||||
Object.values(PrimTable).concat(['i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'bytes', 'opt', 'list', 'enum', 'map'])
|
||||
);
|
||||
|
||||
// 返回 { Name, Via, Mod, Class };Via 为空表示没还原出原名
|
||||
@@ -471,15 +507,47 @@ function ResolveOne(Name, ModId, ModuleInfo, ClassToCodec) {
|
||||
return { Name: R.Name, Codec: '' };
|
||||
}
|
||||
|
||||
// 名字在本模块里的「引用链」:每个跳点写成 模块::导出键(= PrimClassWire 的键)。
|
||||
// 为什么要这条链而不是还原后的类名:ResolveClass 把跨模块引用还原成**那个模块里的本地类名**,
|
||||
// 而这类本地名是混淆器按模块发的短名(如 15 个模块各有一个 `ji`),拿它查表会撞名;
|
||||
// 模块::导出键才是唯一身份(module_37036.js:11088 的 qj 只有一个 ji)。
|
||||
function AliasChain(Name, ModId, ModuleInfo) {
|
||||
const Hops = [];
|
||||
let Cur = Name;
|
||||
let Mod = ModId;
|
||||
for (let I = 0; I < 6; I++) {
|
||||
const Info = ModuleInfo[Mod];
|
||||
const A = Info && Info.Aliases[Cur];
|
||||
if (!A) break;
|
||||
Hops.push(A.Mod + '::' + A.Key);
|
||||
const Dep = ModuleInfo[A.Mod];
|
||||
const Cls = (Dep && Dep.Exports[A.Key]) || '';
|
||||
if (!Cls) break;
|
||||
Cur = Cls;
|
||||
Mod = A.Mod;
|
||||
}
|
||||
return Hops;
|
||||
}
|
||||
|
||||
// 类型串 → { Type, Codecs };把里面的裸类名逐个替换掉
|
||||
function ResolveType(Type, ModId, ModuleInfo, ClassToCodec) {
|
||||
// Trace(可选,调用方给就填):把「既没有 codec、也不在 WireClasses / PrimClassWire 里」的裸类名收进 Leftover。
|
||||
// SlotTypes 段靠它判「类型到底推没推出来」——残留裸名意味着线上没有解码器,只能如实留空。
|
||||
function ResolveType(Type, ModId, ModuleInfo, ClassToCodec, Trace) {
|
||||
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];
|
||||
else {
|
||||
// Kolib 原始类型类(kClass(跨模块别名的原始类型类),如 module_47243.js:181 的 xi = 37036::qj
|
||||
// = Long):按模块::导出键查表换成线上类型(见 PrimClassWire 的注释)。
|
||||
const Hop = AliasChain(W, ModId, ModuleInfo).find((H) => PrimClassWire[H]);
|
||||
// 协议核心类没有 codec 时按「线上基础类型」换算(如实体引用 xr → i64),否则保留类名
|
||||
if (Hop) return PrimClassWire[Hop];
|
||||
if (WireClasses[R.Name]) return WireClasses[R.Name];
|
||||
// 两条都没命中:这个裸类名在线上没有解码器,记进 Leftover 让调用方判「类型没推出来」
|
||||
else if (Trace) Trace.Leftover.push(W);
|
||||
}
|
||||
return R.Name;
|
||||
});
|
||||
return { Type: Out, Codecs: Refs };
|
||||
@@ -540,6 +608,25 @@ const WRITER_TYPE = {
|
||||
h2u: 'f64', m2u: 'f64',
|
||||
};
|
||||
const WireClasses = {};
|
||||
// 「Kolib 原始类型类 → 线上类型」,键 = **模块::导出键**(不是裸类名)。
|
||||
// 为什么必须带模块:这类类的名字是混淆器按模块发的短名,全库有 15 个模块各有一个 `ji`
|
||||
// (src/module_37036.js:661 才是 Kolib 的 Long,其余同名者与协议无关),裸类名当键必然串味。
|
||||
// 证据链(登记点在下面循环里,消费点在 ResolveType):
|
||||
// src/module_37036.js:661 function ji(t, n) ← Kolib 的 Long(:651-657 的 qi() 用
|
||||
// new ji(0, -2147483648) 造 MIN_VALUE)
|
||||
// src/module_37036.js:11088 t.$_$.qj = ji ← 导出键 qj
|
||||
// src/module_24635.js:36 R = i.$_$.qj ← 引入方(module_24635)的别名
|
||||
// src/module_24635.js:731-733 case "Long": i = L(R) ← an.n7f 的 primitive 分派:字符串 "Long"
|
||||
// 就映射到这个类(L = kClass 助手)
|
||||
// src/module_24635.js:247 this.m7f(L(R), new Xt()) ← 给它注册 codec Xt
|
||||
// src/module_24635.js:482-487 Xt.o7g 恰好一次写入 t.a7g_1.f2u(...)、Xt.p7g 读 t.a7g_1.u2t()
|
||||
// ⇒ 线上 8 字节 = i64(WRITER_TYPE.f2u)
|
||||
// src/module_47243.js:181 xi = n.$_$.qj ← 使用方别名
|
||||
// src/module_47243.js:15869 this.nft_1 = Mm(this).n7f(new Ei(new yi(bi(xi), !1), !1))
|
||||
// → ListCodecInfo(TypeCodecInfo(kClass(ji))) ⇒ 线上 list<i64>
|
||||
// 所以 kClass(<原始类型类>) 推不出来的根因不是角色表(bi 已是 KClass),而是「KClass 的实参是
|
||||
// 一个跨模块别名的原始类型类」时,只登记裸别名/裸类名会漏。这里按模块::导出键补齐。
|
||||
const PrimClassWire = {};
|
||||
for (const File of ModuleFiles) {
|
||||
let Src;
|
||||
try {
|
||||
@@ -547,7 +634,8 @@ for (const File of ModuleFiles) {
|
||||
} catch (Err) {
|
||||
continue;
|
||||
}
|
||||
const Roles = (ModuleInfo[ModIdOf(File)] || {}).Roles || {};
|
||||
const ModId = ModIdOf(File);
|
||||
const Roles = (ModuleInfo[ModId] || {}).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;
|
||||
@@ -563,7 +651,47 @@ for (const File of ModuleFiles) {
|
||||
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;
|
||||
if (!Type) continue;
|
||||
// 键必须用「还原后的类名」,因为消费方 ResolveType 查的就是它(见上面 ResolveType 的 WireClasses[R.Name])。
|
||||
// 只登记源码里的裸别名是不够的:别名只在本模块内有效,跨模块引用同一类时两边名字不同,
|
||||
// 本模块定义、没人再引用它的类(如 module_61860.js:439 的 `function xr() {}`)才会碰巧对上。
|
||||
// 两种键都登记:裸别名保持既有行为(如 'xr'),原始类型类另走 PrimClassWire(模块::导出键)。
|
||||
if (!WireClasses[M[2]]) WireClasses[M[2]] = Type;
|
||||
// kClass 的实参是本模块的「跨模块别名」(如 module_24635.js:247 的 R = i.$_$.qj)⇒ 记下它指向的
|
||||
// 导出身份,供 ResolveType 在**任何**模块里引用同一类时认得出来(见 PrimClassWire 的注释)。
|
||||
// 只认别名、不认本地类名:本地类名是「本模块自己定义的类」,不是原始类型类。
|
||||
const Alias = (ModuleInfo[ModId] || {}).Aliases[M[2]];
|
||||
if (Alias && !PrimClassWire[Alias.Mod + '::' + Alias.Key]) PrimClassWire[Alias.Mod + '::' + Alias.Key] = Type;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 资源类 → 线上 i64
|
||||
// 出处:src/module_71105.js:12439-12518 的资源类型注册块,形状 `r.u83(<typeId>, kClass(<类>), <工厂>)`
|
||||
// (si = module_37036 的 $_$.mf = kClass,见 src/module_71105.js:156)。
|
||||
// 被注册的类都是「资源」:资源实例在空间协议里只以 i64 key 出现(查表/构造见 src/module_45686.js:209-228 的
|
||||
// qt() → resourceRegistry.x83 与 L() 的 i64/i16/i16/i64/bool 读序)。
|
||||
// 为什么从源码导出而不是手写:容器层原来手写了 10 个(container.js 的 RESOURCE_CLASS_WIRE),
|
||||
// 漏了 typeId 13 的 no 与 zv 两兄弟——实测 space-100072911/s2c_0001 的 DonationAlertCC.imageWithEmail
|
||||
// (类型 = module_71105 的导出 d = 类 no)就是因此解不动。全库 `.u83(` 恰好只出现 12 次、全在 module_71105,
|
||||
// 不会被别处同名方法污染,所以这里按注册块全量导出,不做任何选择。
|
||||
const ResourceClasses = {};
|
||||
for (const File of ModuleFiles) {
|
||||
let Src;
|
||||
try {
|
||||
Src = fs.readFileSync(File, 'utf8');
|
||||
} catch (Err) {
|
||||
continue;
|
||||
}
|
||||
const ModId = ModIdOf(File);
|
||||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||||
const LineOf = MakeLineIndex(Src);
|
||||
const Re = /\.u83\(\s*([^,()]+),\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)/g;
|
||||
let M;
|
||||
while ((M = Re.exec(Src))) {
|
||||
if ((Roles[M[2]] || '') !== 'KClass') continue; // 第 2 参必须是 kClass(类)
|
||||
const Name = ResolveClass(M[3], ModId, ModuleInfo, []).Name;
|
||||
if (!ResourceClasses[Name])
|
||||
ResourceClasses[Name] = { Wire: 'i64', TypeId: M[1].trim(), From: path.basename(File) + ':' + LineOf(M.index) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,6 +785,91 @@ for (const C of Object.values(Codecs)) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 顶层段:SlotTypes(全模块「槽位 → 类型」)
|
||||
// 为什么需要它:上面的 Codecs 段只收「被抽成 codec 条目的类」的字段。可下行方法(模型类的 q7i 分支)
|
||||
// 读的是模型类里那些**局部 codec 实例**(chc_1 / jh4_1 / b1wd_1 …),它们由 taw() 之类的一次性初始化
|
||||
// 赋成 `<槽位> = <工厂>(<接收者>).n7f(new <描述符>)`,往往压根没进 Codecs 段(那个模型没注册 feature、
|
||||
// 或 codec 条目没被抽到)。于是「这个槽位是什么类型」在全库查不到,下行字段就只能报未解。
|
||||
// 这里对**全部模块**扫这个形状,复用与上面完全相同的 TypeOfDescriptor + ResolveType 推类型:
|
||||
// 源码证据(src/module_10283.js:2343) this.hh4_1 = Un(this).n7f(new w(v().xb(), !1))
|
||||
// 源码证据(src/module_10283.js:2727) this.chc_1 = mi(this).n7f(new $(new w(v().xb(), !1), !1))
|
||||
// 源码证据(src/module_29554.js:242) this.b1wd_1 = B(this).n7f(new _(o(W), !1))
|
||||
// 键 = 模块::槽位:槽位名本来是「模块内唯一」,但槽位一旦被别处引用(如 q7i 分支、上行发送点)
|
||||
// 就必须能定位到「哪个模块的哪个槽位」,所以把模块号写进键里,避免将来把同名槽位认错。
|
||||
// (实测本包 1073 个槽位名恰好全局唯一,但那是混淆器给的下标,不该依赖。)
|
||||
function ParseSlotAssignments(Src, Roles, LineOf) {
|
||||
const Out = new Map();
|
||||
const Conflicts = [];
|
||||
const Re = /\.n7f\(/g;
|
||||
let M;
|
||||
while ((M = Re.exec(Src))) {
|
||||
const Open = M.index + M[0].length - 1;
|
||||
const Close = MatchBracket(Src, Open, '(', ')');
|
||||
if (Close < 0) break;
|
||||
// prettier 会把实参换行,窗口放宽到 200 字符;工厂实参允许换行但不容嵌套括号
|
||||
const Win = Src.slice(Math.max(0, M.index - 200), M.index);
|
||||
const Head = /(?:([A-Za-z_$][\w$]*)\.)?([a-z0-9]+_\d+)\s*=\s*([A-Za-z_$][\w$]*)\s*\([^()]{0,60}\)\s*$/.exec(Win);
|
||||
if (Head) {
|
||||
const Slot = Head[2];
|
||||
const T = TypeOfDescriptor(Src.slice(Open + 1, Close), Roles, 0);
|
||||
const Prev = Out.get(Slot);
|
||||
// 同一槽位名在本模块被赋了不同描述符 → 记冲突、保留第一条(宁可少解,不可解错)
|
||||
if (Prev && Prev.Desc !== T.Raw) Conflicts.push(Slot + ' → ' + Prev.Desc + ' / ' + T.Raw);
|
||||
else if (!Prev) Out.set(Slot, { Slot, Desc: T.Raw, Raw: T.Type, Line: LineOf(M.index) });
|
||||
}
|
||||
Re.lastIndex = Close + 1;
|
||||
}
|
||||
return { Map: Out, Conflicts };
|
||||
}
|
||||
|
||||
const SlotTypes = {};
|
||||
const SlotTypeConflicts = [];
|
||||
let SlotTypeOk = 0;
|
||||
let SlotTypeBlank = 0;
|
||||
const SlotTypeBySlotName = new Map(); // 槽位名 → [{Mod, Type}],只用来统计「同名槽位类型不一致」
|
||||
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);
|
||||
// 键里的模块名统一用「去前导零」的 module_<数字>(与 Codecs[].ModuleId 同一套写法):
|
||||
// 文件名是 module_01510 而模块号是 1510,两套写法混用会让消费方(mine_downlink)查不到。
|
||||
const ModKey = 'module_' + ModId;
|
||||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||||
const Parsed = ParseSlotAssignments(Src, Roles, MakeLineIndex(Src));
|
||||
for (const C of Parsed.Conflicts) SlotTypeConflicts.push(Mod + ' ' + C);
|
||||
for (const [Slot, Rec] of Parsed.Map) {
|
||||
const Trace = { Leftover: [] };
|
||||
const R = ResolveType(Rec.Raw, ModId, ModuleInfo, ClassToCodec, Trace);
|
||||
// 「推不出来就留空」:类型串里还残留没有 codec 的裸类名(Trace.Leftover)或压根没解析出形状
|
||||
// ('?' / 'prim:xx')时,线上没有对应的解码器,如实留空——不要拿裸类名硬当类型。
|
||||
const Ok = !!R.Type && R.Type !== '?' && R.Type !== '...' && !/^prim:/.test(R.Type) && !Trace.Leftover.length;
|
||||
if (Ok) SlotTypeOk++;
|
||||
else SlotTypeBlank++;
|
||||
SlotTypes[ModKey + '::' + Slot] = {
|
||||
Module: ModKey,
|
||||
Slot,
|
||||
Type: Ok ? R.Type : '',
|
||||
TypeCodec: Ok ? R.Codecs[0] || '' : '',
|
||||
Desc: Rec.Desc,
|
||||
Line: Rec.Line,
|
||||
};
|
||||
if (!SlotTypeBySlotName.has(Slot)) SlotTypeBySlotName.set(Slot, []);
|
||||
SlotTypeBySlotName.get(Slot).push(SlotTypes[ModKey + '::' + Slot]);
|
||||
}
|
||||
}
|
||||
// 同名槽位在不同模块被推成不同类型 → 只按槽位名查表必然解错,这里把数量报出来当护栏
|
||||
let SlotNameMismatch = 0;
|
||||
for (const List of SlotTypeBySlotName.values()) {
|
||||
if (List.length < 2) continue;
|
||||
const First = List[0].Type;
|
||||
if (List.some((X) => X.Type !== First)) SlotNameMismatch++;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 第四遍:上行命令表
|
||||
// 上行命令在源码里的形态(见 module_85183 / 10283 / 47243 / 85667 …):
|
||||
// ① 命令类构造函数里放着「methodId 长整型字面量」和「负载 codec」:
|
||||
@@ -776,9 +989,130 @@ for (const File of ModuleFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 模型注册点 → modelId ↔ codec(源码第三证据)
|
||||
// 客户端把「modelId → codec」登记在 RPC 分发器的模型注册表里:src/module_61860.js:1664-1671 的
|
||||
// j7n(t, n) { this.w7n_1.g3(t, n) } —— 就是 names/regdump.json 的 maps['ModelReg.w7n_1']
|
||||
// 源码里的登记点形状(都是 taw() 里,三处互相印证):
|
||||
// src/module_99527.js:122 this.ide_1 = new C(-841295065, 916213531) → modelId 3935107155251354407
|
||||
// src/module_99527.js:357 Y(this).j7n(this.ide_1, K(this).n7f(new v(k(Z), !1))) → codec 取的是类 Z 的 codec(module_99527::N)
|
||||
// src/module_85196.js:1198 this.icp_1 = new Me(-769625095, 727121853) → modelId 3122964582367261689
|
||||
// src/module_85196.js:6835 na(this).j7n(this.icp_1, ta(this).n7f(new ye(ze(Q_), !1))) → codec 取类 Q_ 的 codec(module_85196::vu)
|
||||
// src/module_25377.js:274 this.ldi_1 = new Q(-2144376380, 1490830385) → modelId 6403067749608679876
|
||||
// src/module_25377.js:1413 sn(this).j7n(this.ldi_1, un(this).n7f(new L(B(en), !1))) → codec 取类 en 的 codec(module_25377::ft)
|
||||
// 判断口径:第 1 个别名必须是 TypeCodecInfo(module_24635::e = gn)、第 2 个必须是 kClass(module_37036::mf),
|
||||
// 也就是 an.n7f 那条「给某个模型类取 codec」的分支(src/module_24635.js:746-784)。长整型字面量 → modelId 用 LongOf。
|
||||
function FindModelRegistrations(Src, Roles) {
|
||||
const IdOf = {};
|
||||
const ReId = /this\.([a-z0-9]+_\d+) = new ([A-Za-z_$][\w$]*)\((-?\d+), (-?\d+)\)/g;
|
||||
let M;
|
||||
while ((M = ReId.exec(Src))) IdOf[M[1]] = LongOf(Number(M[3]), Number(M[4]));
|
||||
const Out = [];
|
||||
if (!Object.keys(IdOf).length) return Out;
|
||||
const Re = /j7n\(\s*this\.([a-z0-9]+_\d+)\s*,[\s\S]{0,90}?\.n7f\(\s*new ([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)/g;
|
||||
while ((M = Re.exec(Src))) {
|
||||
const Id = IdOf[M[1]];
|
||||
if (!Id) continue;
|
||||
if ((Roles[M[2]] || '') !== 'Type' || (Roles[M[3]] || '') !== 'KClass') continue;
|
||||
Out.push({ ModelId: Id, Class: M[4] });
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
const SrcModelCodec = new Map(); // modelId → { CodecKey, Mod, Class }
|
||||
const SrcRegConflicts = [];
|
||||
for (const File of ModuleFiles) {
|
||||
let Src;
|
||||
try {
|
||||
Src = fs.readFileSync(File, 'utf8');
|
||||
} catch (Err) {
|
||||
continue;
|
||||
}
|
||||
const ModId = ModIdOf(File);
|
||||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||||
for (const R of FindModelRegistrations(Src, Roles)) {
|
||||
const One = ResolveOne(R.Class, ModId, ModuleInfo, ClassToCodec);
|
||||
if (!One.Codec) continue;
|
||||
const Prev = SrcModelCodec.get(R.ModelId);
|
||||
if (Prev && Prev.CodecKey !== One.Codec) SrcRegConflicts.push(R.ModelId + ' 在两个模块登记了不同 codec:' + Prev.CodecKey + ' / ' + One.Codec);
|
||||
else if (!Prev) SrcModelCodec.set(R.ModelId, { CodecKey: One.Codec, Mod: ModId, Class: R.Class });
|
||||
}
|
||||
}
|
||||
|
||||
// regdump.json 的 maps['ModelReg.w7n_1'] 是同一个表的运行时 dump:modelId → codec 实例,
|
||||
// 每条带 ctor(codec 类短名)与 keys(该 codec 实例自己的槽位名)。
|
||||
// 指纹 = 「codec 类短名 + 槽位名集合」,拿它去本轮抽出的 Codecs 里找唯一匹配。
|
||||
// 为什么需要这一步:压缩后短名会跨模块重名(如 vu/N/ft 各有多个),单看名字会绑错;
|
||||
// 而槽位名(缩器按下标生成的 x_1 形式)在同一 codec 内是确定的,两者合起来才能消歧。
|
||||
// 仍然多解时用「源码登记点」(FindModelRegistrations)定音——但**只允许在候选集合里挑**,
|
||||
// 源码登记点落在候选之外就如实留空并列出,绝不硬绑。
|
||||
function BindModelIds(Codecs, RegPath) {
|
||||
const Stats = { W7nEntries: 0, ByFingerprint: 0, BySrcReg: 0, ZeroCandidate: 0, Ambiguous: 0, Bound: 0, ConflictWithJoin: 0 };
|
||||
const Unresolved = [];
|
||||
const Conflicts = [];
|
||||
if (!fs.existsSync(RegPath)) {
|
||||
Stats.Reason = '找不到 ' + RegPath;
|
||||
return { Stats, Unresolved };
|
||||
}
|
||||
const Reg = JSON.parse(fs.readFileSync(RegPath, 'utf8'));
|
||||
const Maps = Reg.maps || Reg;
|
||||
const W = (Maps['ModelReg.w7n_1'] && Maps['ModelReg.w7n_1'].entries) || [];
|
||||
const ByFingerprint = new Map(); // 短名|排序后的槽位名 → [codec 键]
|
||||
for (const [Key, C] of Object.entries(Codecs)) {
|
||||
const Sig = C.Codec + '|' + (C.Slots || []).map((S) => S.Slot).slice().sort().join(',');
|
||||
if (!ByFingerprint.has(Sig)) ByFingerprint.set(Sig, []);
|
||||
ByFingerprint.get(Sig).push(Key);
|
||||
}
|
||||
for (const E of W) {
|
||||
const ModelId = String(E.key);
|
||||
Stats.W7nEntries++;
|
||||
const Sig = String(E.ctor || '') + '|' + (E.keys || []).slice().sort().join(',');
|
||||
const Cand = ByFingerprint.get(Sig) || [];
|
||||
const Src = SrcModelCodec.get(ModelId);
|
||||
let Key = '';
|
||||
let How = '';
|
||||
if (Cand.length === 1) {
|
||||
Key = Cand[0];
|
||||
How = '唯一指纹';
|
||||
} else if (Cand.length > 1) {
|
||||
if (Src && Cand.includes(Src.CodecKey)) {
|
||||
Key = Src.CodecKey;
|
||||
How = '指纹命中 ' + Cand.length + ' 个,源码登记点(' + Src.Mod + '::' + Src.Class + ')定音';
|
||||
} else {
|
||||
Stats.Ambiguous++;
|
||||
Unresolved.push(ModelId + ':指纹 ' + Sig + ' 命中 ' + Cand.length + ' 个 [' + Cand.join(', ') + ']' +
|
||||
(Src ? ',源码登记点 ' + Src.CodecKey + ' 不在候选里' : ',源码里没有登记点'));
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (Src) {
|
||||
Key = Src.CodecKey;
|
||||
How = '指纹 0 命中,源码登记点(' + Src.Mod + '::' + Src.Class + ')';
|
||||
} else {
|
||||
Stats.ZeroCandidate++;
|
||||
Unresolved.push(ModelId + ':指纹 ' + Sig + ' 在 Codecs 里 0 命中,源码里也没有登记点');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (How === '唯一指纹') Stats.ByFingerprint++;
|
||||
else Stats.BySrcReg++;
|
||||
const C = Codecs[Key];
|
||||
if (!C.ModelIds) C.ModelIds = [];
|
||||
if (!C.ModelIds.includes(ModelId)) C.ModelIds.push(ModelId);
|
||||
if (!C.ModelHow) C.ModelHow = How;
|
||||
if (C.ModelId && C.ModelId !== ModelId && !C.ModelIds.includes(C.ModelId)) {
|
||||
// 旧 join(槽位签名 / 构造器名兜底)给的是另一个 modelId:不是错,但要如实报出来
|
||||
Stats.ConflictWithJoin++;
|
||||
Conflicts.push(Key + ' 旧 ModelId=' + C.ModelId + ',新绑定=' + ModelId + '(两个都会进 CodecByModelId)');
|
||||
}
|
||||
Stats.Bound++;
|
||||
}
|
||||
return { Stats, Unresolved, Conflicts };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 写出与统计
|
||||
const RegPath = path.resolve(Arg('reg', path.join(PROJECT, 'names', 'regdump.json')));
|
||||
const Join = JoinRegistries(Codecs, RegPath);
|
||||
const ModelBind = BindModelIds(Codecs, RegPath);
|
||||
|
||||
const AllFields = Object.values(Codecs).reduce((A, C) => A.concat(C.Fields), []);
|
||||
const Named = AllFields.filter((F) => F.Name !== '(匿名)' && F.Name !== '?');
|
||||
@@ -788,7 +1122,10 @@ 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',
|
||||
Note: '空间层 codec 字段表(键 = 模块::codec变量名)。字段名来自 v("codec_名字");类型来自 q7g 描述符(已把裸局部类名还原成 Kotlin 原名,TypeRaw 保留原名);Js= p7g 里的 JS 级线索;Prop= 数据类属性名;TypeCodec= 该嵌套模型对应的 codec 键;ModelId/MethodId 来自 regdump.json join;ModelIds= 同一个 codec 被多个 modelId 共用时的完整列表(regdump w7n_1 指纹绑定,见 ModelIdBinding)',
|
||||
SlotTypesNote:
|
||||
'SlotTypes:全模块「槽位 → 类型」表,键 = 模块::槽位(如 module_10283::jh4_1)。来源是 `<接收者>.<槽位> = <工厂>(<接收者>).n7f(new <描述符>)` 的构造赋值(q7g 之外的局部 codec 实例),' +
|
||||
'供 mine_downlink.js 解 q7i 分支里读到的槽位;Type 推不出来时留空字符串(不编),TypeCodec 为空表示该类型是基础类型/线上基础类型,不需要嵌套 codec',
|
||||
Scan,
|
||||
Modules: ModuleFiles.length,
|
||||
ModulesWithCodec: ModulesUsed,
|
||||
@@ -797,7 +1134,30 @@ const Out = {
|
||||
NamedCount: Named.length,
|
||||
AnonymousCount: Unnamed.length,
|
||||
WireClasses,
|
||||
PrimClassWire,
|
||||
ResourceClasses,
|
||||
SlotTypes,
|
||||
SlotTypeStats: {
|
||||
Count: Object.keys(SlotTypes).length,
|
||||
Typed: SlotTypeOk,
|
||||
Blank: SlotTypeBlank,
|
||||
DescConflicts: SlotTypeConflicts.length,
|
||||
NameMismatch: SlotNameMismatch,
|
||||
},
|
||||
Join,
|
||||
ModelIdBinding: {
|
||||
Note:
|
||||
'ModelId → codec 的权威绑定。主证据 = names/regdump.json 的 maps["ModelReg.w7n_1"](modelId → codec 实例:' +
|
||||
'ctor = codec 类短名、keys = 该 codec 实例的槽位名),指纹 = 「短名 + 槽位名集合」,在本表 Codecs 里唯一命中即绑定;' +
|
||||
'若指纹多解,用源码登记点(`j7n(<id 槽位>, X(this).n7f(new TypeCodecInfo(kClass(类), !1)))`,见 extract_codec_fields.js ' +
|
||||
'的 FindModelRegistrations)在候选集合内定音;多解且定不了 / 零命中且源码也没有登记点 → 如实留空并列在 Unresolved,绝不硬绑。' +
|
||||
'ByFingerprint = 靠唯一指纹绑上的条数,BySrcReg = 靠源码登记点绑上(含指纹 0 命中)的条数。',
|
||||
SrcRegistrations: SrcModelCodec.size,
|
||||
SrcRegConflicts,
|
||||
...ModelBind.Stats,
|
||||
Unresolved: ModelBind.Unresolved,
|
||||
ConflictsWithJoin: ModelBind.Conflicts || [],
|
||||
},
|
||||
Codecs,
|
||||
};
|
||||
fs.writeFileSync(OutPath, JSON.stringify(Out, null, 1), 'utf8');
|
||||
@@ -816,7 +1176,16 @@ 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(
|
||||
'slot 类型表: ' + Out.SlotTypeStats.Count + ' 个(推出 ' + SlotTypeOk + ',留空 ' + SlotTypeBlank +
|
||||
');同槽位描述符冲突 ' + SlotTypeConflicts.length + ',同名槽位类型不一致 ' + SlotNameMismatch + ' 处(故本表按 模块::槽位 索引)'
|
||||
);
|
||||
if (SlotTypeConflicts.length) for (const X of SlotTypeConflicts.slice(0, 3)) console.log(' ⚠ ' + X);
|
||||
console.log('已写出 ' + OutPath);
|
||||
console.log('协议核心类(线上基础类型): ' + JSON.stringify(WireClasses));
|
||||
console.log('Kolib 原始类型类(模块::导出键 → 线上类型): ' + JSON.stringify(PrimClassWire));
|
||||
console.log('资源类(u83 注册块导出 → 线上 i64): ' + Object.keys(ResourceClasses).length + ' 个 — ' +
|
||||
Object.entries(ResourceClasses).map(([N, R]) => N + '(typeId ' + R.TypeId + ')').join(' '));
|
||||
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(
|
||||
@@ -827,6 +1196,21 @@ if (Join.Ok)
|
||||
);
|
||||
else console.log('注册表 join: 跳过(' + Join.Reason + ')');
|
||||
|
||||
// modelId → codec 绑定(regdump w7n_1 指纹 + 源码登记点定音)报告
|
||||
const MB = Out.ModelIdBinding;
|
||||
if (MB.Reason) console.log('modelId 绑定: 跳过(' + MB.Reason + ')');
|
||||
else {
|
||||
console.log(
|
||||
'modelId → codec 绑定: w7n_1 ' + MB.W7nEntries + ' 条;绑上 ' + MB.Bound + ' 条(唯一指纹 ' + MB.ByFingerprint +
|
||||
' + 源码登记点定音 ' + MB.BySrcReg + ');仍绑不上 ' + (MB.Ambiguous + MB.ZeroCandidate) +
|
||||
'(指纹多解 ' + MB.Ambiguous + ',零命中 ' + MB.ZeroCandidate + ')'
|
||||
);
|
||||
console.log(' 源码登记点(taw 里的 j7n(id, codec))共 ' + MB.SrcRegistrations + ' 个 modelId' +
|
||||
(MB.SrcRegConflicts.length ? ';⚠ 同 id 两处登记不同 codec ' + MB.SrcRegConflicts.length + ' 处' : ';无冲突') +
|
||||
(MB.ConflictWithJoin ? ';与旧 join 给的 ModelId 不一致 ' + MB.ConflictWithJoin + ' 处' : ''));
|
||||
for (const U of MB.Unresolved.slice(0, 8)) console.log(' ⚠ 未绑定 ' + U);
|
||||
}
|
||||
|
||||
// 自检:字段的原始描述符里若还残留未识别形状(既不是基础类型也不是 X(Y) 也不是 new X(...)),列出来看
|
||||
const Suspicious = new Set();
|
||||
for (const K of Object.keys(Codecs)) {
|
||||
@@ -889,7 +1273,9 @@ for (const [Opt, Field, Label] of [
|
||||
if (!Id) continue;
|
||||
const Hit = Object.entries(Codecs).filter(([, C]) => {
|
||||
const V = C[Field];
|
||||
return Array.isArray(V) ? V.includes(Id) : V === Id;
|
||||
if (Array.isArray(V) ? V.includes(Id) : V === Id) return true;
|
||||
// modelId 可能只出现在新绑定(ModelIds)里:一个 codec 被多个 modelId 共用时 ModelId 只留了一个
|
||||
return Field === 'ModelId' && Array.isArray(C.ModelIds) && C.ModelIds.includes(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)) {
|
||||
|
||||
Reference in new issue
Block a user