fix(protocol): 适配 e98d0216 版编解码 API 并重生成协议表

- extract_codec_fields:字段工厂 n7f→r7g、读写方法 p7g/o7g→t7h/s7h、槽位初始化 q7g→u7h
- 资源类注册 u83→y84,核心类型按 模块::导出键 解析(Vector3/Long/实体引用)
- mine_downlink:读取器映射按当前 ByteBuffer 修正(t2u=f32、u2u=f64 等),容器槽位泛化
- 重生成 codec_fields/uplink_table/downlink_table:762 下行字段 0 未解,361 上行命令全量有类型
This commit is contained in:
TermiNexus committed 2026-10-21 00:03:07 +08:00
1 parent 78c02345c2
commit 2ef424929b
5 files changed
+42700 -43117

No files matched your search

+37965 -38489
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+99 -24
View File
@@ -136,19 +136,16 @@ const PrimTable = {
const PrimName = (K) => PrimTable[K] || 'prim:' + K;
// 读取器方法名 → 线级数值宽度(module_72834 的读取器)
// 注意:这是「读」方法。e98d0216 版里 p2u..u2u 是读、n2u/o2u 是写,旧版口径整体右移了,
// 所以这里以当前源码为准(h(F).p2u→xy、q2u→u2t、r2u→v2t、s2u→w2t、t2u→x2t、u2u→y2t)。
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',
p2u: 'i8',
q2u: 'i16',
r2u: 'i32',
s2u: 'i64',
t2u: 'f32',
u2u: 'f64',
};
// 类型描述符表达式 → {Type, Raw}
@@ -226,7 +223,9 @@ const ROLE_KEYS = {
[CODEC_INFO_MODULE + '::b']: 'Enum',
[CODEC_INFO_MODULE + '::d']: 'Map',
[KOLIB_MODULE + '::w4']: 'Prim',
// kClass 助手:旧包导出键 mf,新包(e98d0216)为 nf;两个都收,换版时不必再改
[KOLIB_MODULE + '::mf']: 'KClass',
[KOLIB_MODULE + '::nf']: 'KClass',
};
// module_03569.js → '3569'
@@ -299,8 +298,10 @@ function FindClassNames(Src) {
// 数据类工厂:function Ut() { return ((t = M(d(Ft))), Ft.call(t), t); } → Ut 其实是造 Ft
function FindFactories(Src) {
const Out = {};
// 旧包:return ((X = Y(d(Z))), Z.call(X), X);
// 新包:return ((t = L(w(Mt))), Mt.call(t), t); ← 多包了一层工厂,故放宽为「两层单参调用」
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;
/function ([A-Za-z_$][\w$]*)\(\) \{\s*return \(\(([A-Za-z_$][\w$]*) = [A-Za-z_$][\w$]*\([A-Za-z_$][\w$]*\(([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;
@@ -339,7 +340,8 @@ function FindGetters(Src) {
// q7g 块 → [{Slot, Desc, Type, Raw}]
function ParseSlots(Body, Roles) {
const Out = [];
const Re = /this\.([a-z0-9]+_\d+) = t\.n7f\(/g;
// 字段工厂:旧包 n7f,新包 r7g
const Re = /this\.([a-z0-9]+_\d+) = t\.r7g\(/g;
let M;
while ((M = Re.exec(Body))) {
const Open = Body.indexOf('(', M.index + M[0].length - 1);
@@ -355,7 +357,8 @@ function ParseSlots(Body, Roles) {
// 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;
// 读序:旧包 p7g,新包 t7h;基础类型走 <缓冲槽位>.<reader>()(旧包固定 a7g_1,新包槽位名会变,故泛化)
const Re = /([A-Za-z_$][\w$]*)\(this\)\.t7h\(t\)|t\.[a-z0-9]+_\d+\.([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 = [];
@@ -499,6 +502,56 @@ function ResolveClass(Name, ModId, ModuleInfo, Seen) {
}
// 单个别名 → { Name, Codec }(Codec = 该类的 codec 键,供递归解码用)
// 协议核心类 / 数学类在线上没有独立 codec,但会话双方对它们的字节形状是固定的。
// 键 = 模块::导出键(唯一身份,避免重名),值 = 线上形状:
// 61860::r = xr(协议实体引用)→ i64
// 37036::sj = Kotlin Long → i64
// 32088::j = Vector3 → 3 × f32(由生成器展开)
const CoreIdentities = {
'61860::r': 'i64',
'37036::sj': 'i64',
'32088::j': 'vector3',
};
// 资源类:模块::导出键;线上只出现 i64 引用(由下面的 y84 注册块填充)
const ResourceIdentities = new Set();
// 类名(模块内短名)→ 稳定身份(模块::导出键)。优先别名,其次导出反查,最后退回本地类名。
function IdentityOf(Name, ModId, ModuleInfo) {
const Info = ModuleInfo[ModId] || {};
const A = (Info.Aliases || {})[Name];
if (A) return A.Mod + '::' + A.Key;
const ExportKey = Object.keys(Info.Exports || {}).find((K) => Info.Exports[K] === Name);
return ModId + '::' + (ExportKey || Name);
}
// 源码里「共享 codec」的登记:q7g(kClass(A), codec.t7g(kClass(B))) → A 与 B 同线形。
// 实例:module_71105.js:12590 的 Jf 复用 qv 的 codec(两者都是资源引用 i64)。
function AddSharedCodecIdentities(ModuleFiles, ModuleInfo) {
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 Re =
/\.q7g\(\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)\s*,\s*[A-Za-z_$][\w$]*\.t7g\(\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)\s*\)/g;
let M;
while ((M = Re.exec(Src))) {
if ((Roles[M[1]] || '') !== 'KClass' || (Roles[M[3]] || '') !== 'KClass') continue;
const A = IdentityOf(M[2], ModId, ModuleInfo);
const B = IdentityOf(M[4], ModId, ModuleInfo);
if (ResourceIdentities.has(B)) ResourceIdentities.add(A);
}
}
}
// 源码侧没有 codec、但有实测标定出处的裸类名(详见 3DTank/tools/server/container.js 的注释)
const ManualClassWire = { Pc: 'i64' };
// 名字优先用 codec 的模型真名;拿不到就退回 toString 得到的名字
function ResolveOne(Name, ModId, ModuleInfo, ClassToCodec) {
const R = ResolveClass(Name, ModId, ModuleInfo, []);
@@ -544,6 +597,12 @@ function ResolveType(Type, ModId, ModuleInfo, ClassToCodec, Trace) {
const Hop = AliasChain(W, ModId, ModuleInfo).find((H) => PrimClassWire[H]);
// 协议核心类没有 codec 时按「线上基础类型」换算(如实体引用 xr → i64),否则保留类名
if (Hop) return PrimClassWire[Hop];
// 核心类型 / 资源类:按 模块::导出键 的稳定身份换算
const Chain = AliasChain(W, ModId, ModuleInfo);
const Core = Chain.find((H) => CoreIdentities[H]);
if (Core) return CoreIdentities[Core];
if (Chain.some((H) => ResourceIdentities.has(H))) return 'i64';
if (ManualClassWire[R.Name]) return ManualClassWire[R.Name];
if (WireClasses[R.Name]) return WireClasses[R.Name];
// 两条都没命中:这个裸类名在线上没有解码器,记进 Leftover 让调用方判「类型没推出来」
else if (Trace) Trace.Leftover.push(W);
@@ -636,7 +695,7 @@ for (const File of ModuleFiles) {
}
const ModId = ModIdOf(File);
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
const ByCodec = new Map(FindBlocks(Src, 'o7g').map((B) => [B.Codec, B.Body]));
const ByCodec = new Map(FindBlocks(Src, 's7h').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;
@@ -685,16 +744,31 @@ for (const File of ModuleFiles) {
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;
// 当前版注册方法:r.y84(<typeId>, oi(<类>), <工厂>);旧版是 .u83(
const Re = /\.y84\(\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;
// 记录稳定身份:注册处传入的可能是本模块别名,也可能是本模块本地类名;
// 后者要反查导出键(类名只是模块内短名,导出键才是跨模块身份)
const Info = ModuleInfo[ModId] || {};
const A = (Info.Aliases || {})[M[3]];
let Identity = '';
if (A) Identity = A.Mod + '::' + A.Key;
else {
const ExportKey = Object.keys(Info.Exports || {}).find((K) => Info.Exports[K] === M[3]);
Identity = ModId + '::' + (ExportKey || M[3]);
}
ResourceIdentities.add(Identity);
if (!ResourceClasses[Name])
ResourceClasses[Name] = { Wire: 'i64', TypeId: M[1].trim(), From: path.basename(File) + ':' + LineOf(M.index) };
}
}
// 资源型 codec 共享(如 Jf 复用 qv)——必须在 codec 抽取之前填好身份集
AddSharedCodecIdentities(ModuleFiles, ModuleInfo);
// ---------------------------------------------------------------- 第二遍:抽 codec
const Codecs = {};
let ModulesUsed = 0;
@@ -713,8 +787,8 @@ for (const File of ModuleFiles) {
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');
const SlotBlocks = FindBlocks(Src, 'u7h');
const OrderBlocks = FindBlocks(Src, 't7h');
if (!SlotBlocks.length && !OrderBlocks.length) continue;
ModulesUsed++;
@@ -743,8 +817,8 @@ for (const File of ModuleFiles) {
}
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);
// 模型类:优先看写方法(旧 o7g / 新 s7h)里的 instanceof,其次看读方法用的工厂函数
const O7g = FindBlocks(Src, 's7h').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] : '');
@@ -800,7 +874,7 @@ for (const C of Object.values(Codecs)) {
function ParseSlotAssignments(Src, Roles, LineOf) {
const Out = new Map();
const Conflicts = [];
const Re = /\.n7f\(/g;
const Re = /\.r7g\(/g;
let M;
while ((M = Re.exec(Src))) {
const Open = M.index + M[0].length - 1;
@@ -892,7 +966,7 @@ function FindFieldInfo(Src, Roles) {
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;
const Re = /\.r7g\(/g;
while ((M = Re.exec(Src))) {
const Open = M.index + M[0].length - 1;
const Close = MatchBracket(Src, Open, '(', ')');
@@ -966,7 +1040,7 @@ for (const File of ModuleFiles) {
const Fields = [];
if (Host) {
const Head = Src.slice(Host.BodyStart, At);
for (const G of Head.matchAll(/this\.([a-z0-9]+_\d+)\.o7g\(/g)) {
for (const G of Head.matchAll(/this\.([a-z0-9]+_\d+)\.s7h\(/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 : '' });
@@ -1008,7 +1082,8 @@ function FindModelRegistrations(Src, Roles) {
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;
// 旧包登记函数 j7n,新包 n7o;字段工厂旧 n7f / 新 r7g
const Re = /n7o\(\s*this\.([a-z0-9]+_\d+)\s*,[\s\S]{0,90}?\.r7g\(\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;
+7 -6
View File
@@ -49,8 +49,8 @@ const NamesDir = path.resolve(ToolsDir, '..', 'names');
const OutPath = path.resolve(ToolsDir, Arg('out', path.join('..', 'names', 'downlink_table.json')));
const Check = Argv.indexOf('--check') >= 0;
// ByteBuffer 的定长读取器:名字来自 src_named/module_72834.js(xi/n2u=xy=getInt8 … s2u=w2t=getFloat64)
const ReaderType = { n2u: 'i8', o2u: 'i16', p2u: 'i32', q2u: 'i64', r2u: 'f32', s2u: 'f64' };
// ByteBuffer 的定长读取器:名字来自 src/module_72834.js(p2u=xy=getInt8 … t2u=getFloat32、u2u=getFloat64)
const ReaderType = { xy: 'i8', p2u: 'i8', q2u: 'i16', r2u: 'i32', s2u: 'i64', t2u: 'f32', u2u: 'f64' };
// ---------------------------------------------------------------- 载入 codec 字段表
const CodecFields = JSON.parse(fs.readFileSync(path.join(NamesDir, 'codec_fields.json'), 'utf8'));
@@ -93,7 +93,7 @@ for (const N of NameAmbiguous) NameType.delete(N);
// 以及「模型类」——但模型类只有在能定位到 codec 时才解得出(TypeCodec 链接,或该真名全局唯一)。
// 不在这个集合里的(any/number/nothing/throwable/xxArray/prim:xx/认不出的裸类名)一律算没解出来,
// 免得把解不动的类型写进表里、到 semantic.js 那一步才炸。
const ReaderLeaf = new Set(['bool', 'i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'string', 'bytes', 'array']);
const ReaderLeaf = new Set(['bool', 'i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'string', 'bytes', 'array', 'vector3']);
function Readable(Type, TypeCodec) {
const T = String(Type || '');
if (!T || T === '?' || T === '...' || T[0] === '<') return false;
@@ -176,8 +176,9 @@ function ReadOrder(Body) {
/\(function \(t\) \{\s*var ([A-Za-z0-9_$]+) = t\.([a-z0-9]+_\d+);\s*if \(null != \1\) return \1;\s*[A-Za-z_$][\w$]*\(\s*"([^"]*)"\s*\);\s*\}\)\(this\)/g;
let M;
while ((M = ReCodec.exec(Body))) Hits.push({ At: M.index, Slot: M[2], Tag: M[3] });
const RePrim = /\.a7g_1\.([A-Za-z0-9_$]+)\(\)/g;
while ((M = RePrim.exec(Body))) Hits.push({ At: M.index, Reader: M[1] });
// 基础类型:旧包固定缓冲槽位 a7g_1,新包槽位名会变,故泛化
const RePrim = /\.([a-z0-9]+_\d+)\.([A-Za-z0-9_$]+)\(\)/g;
while ((M = RePrim.exec(Body))) Hits.push({ At: M.index, Reader: M[2] });
Hits.sort((A, B) => A.At - B.At);
return Hits;
}
@@ -208,7 +209,7 @@ for (const File of Files) {
const Longs = FindLongs(Text);
// 找每个 `.q7i = function (...) {` 的函数体。源码形态是 `(p(Pt).q7i = function (t, n) {` ——
// 类名在包装函数的括号里(包装函数逐模块不同:p / ke / ee / nt / …),所以捕获 `)<方法名>` 前那个标识符。
const ReHead = /([A-Za-z_$][\w$]*)\)\.q7i = function \(([^)]*)\) \{/g;
const ReHead = /([A-Za-z_$][\w$]*)\)\.u7j = function \(([^)]*)\) \{/g;
let H;
while ((H = ReHead.exec(Text))) {
const Owner = H[1];