// lib_space_codec.js - 空间层负载的字段级解码器(下行 s2c / 上行 c2s 共用) // // 编码规则来自客户端源码(module_24635 的序列化器实现,推导过程见 tools/extract_codec_fields.js): // bool/i8=1B i16=2B i32/f32=4B i64/f64=8B(全部大端) // string/bytes = varint 长度 + 原始字节 // list = varint 元素数 + 元素… // opt = 1 字节标志(非 0 = 空)+ 有值时紧跟 T // enum = 4 字节(库里的 EnumCodecInfo 序列化器 o7g 是空实现,但游戏自己注册的枚举写 4 字节; // 实测 ChatCC 按 4 字节解才能精确吃满 1476B 负载,按 0 字节只有 40% 的容器内层能对上边界) // 模型类 = 就地展开该模型 codec 的字段(无长度前缀) // varint: <128 → 1 字节;<16384 → 2 字节(0x80|高6位, 低8位);<4194304 → 3 字节(0xC0|高6位, 中8位, 低8位) // // 用法: // const { LoadTables, MakeReader } = require('./lib_space_codec'); // const Tables = LoadTables(path.join(__dirname, '..', 'names')); // const Reader = MakeReader(Tables, { EnumBytes: 4 }); // const R = Reader.ReadFields(Buf, Offset, Tables.CodecByKey.get(CodecKey), 0); // -> { Value: {字段名: 值}, Next: 下一个偏移, Bad: 是否中途卡住 } const fs = require('fs'); const path = require('path'); const MAX_DEPTH = 8; // 载入 names/ 下的三张表 // codec_fields.json: 下行/上行 codec 的字段表(含 ModelId / MethodId / TypeCodec) // uplink_table.json: 上行命令表(methodId → 命令类 / 发送方法 / 负载字段) function LoadTables(NamesDir) { const Out = { CodecByKey: new Map(), CodecByMethod: new Map(), CodecByModelId: new Map(), CodecByModelName: new Map(), WireClasses: new Map(), UplinkByMethod: new Map(), }; try { const CF = JSON.parse(fs.readFileSync(path.join(NamesDir, 'codec_fields.json'), 'utf8')); for (const [Key, C] of Object.entries(CF.Codecs)) { Out.CodecByKey.set(Key, C); for (const M of C.MethodId || []) if (!Out.CodecByMethod.has(M)) Out.CodecByMethod.set(M, C); if (C.ModelId && !Out.CodecByModelId.has(C.ModelId)) Out.CodecByModelId.set(C.ModelId, C); // 模型真名 → codec:字段里只写了类名、没带 TypeCodec 链接时的兜底(如 ji = module_24635::Xt 单 i64) if (C.ModelName && !Out.CodecByModelName.has(C.ModelName)) Out.CodecByModelName.set(C.ModelName, C); } // 协议核心类(module_61860 注册的「一写一读」codec)→ 线上基础类型,如实体引用 xr → i64 for (const [Name, Type] of Object.entries(CF.WireClasses || {})) Out.WireClasses.set(Name, Type); } catch (Err) { Out.CodecError = Err.message; } try { const UP = JSON.parse(fs.readFileSync(path.join(NamesDir, 'uplink_table.json'), 'utf8')); for (const C of UP.Commands || []) if (!Out.UplinkByMethod.has(C.MethodId)) Out.UplinkByMethod.set(C.MethodId, C); } catch (Err) { Out.UplinkError = Err.message; } return Out; } function ReadVarint(Buf, Off) { if (Off >= Buf.length) return { Value: 0, Next: Off, Bad: true }; const B0 = Buf[Off]; if (!(B0 & 0x80)) return { Value: B0, Next: Off + 1 }; const B1 = Buf[Off + 1] || 0; if (B0 & 0x40) return { Value: ((B0 & 0x3f) << 16) + (B1 << 8) + (Buf[Off + 2] || 0), Next: Off + 3 }; return { Value: ((B0 & 0x3f) << 8) + B1, Next: Off + 2 }; } function MakeReader(Tables, Options) { const Opts = Options || {}; const EnumBytes = Opts.EnumBytes === undefined ? 4 : Opts.EnumBytes; const CodecByKey = Tables.CodecByKey; const CodecByModelName = Tables.CodecByModelName || new Map(); const WireClasses = Tables.WireClasses || new Map(); // 读一个值:返回 { Value, Next, Bad } function ReadValue(Buf, Off, Type, TypeCodec, Depth) { if (Depth > MAX_DEPTH) return { Value: '<深度上限>', Next: Off, Bad: true }; if (Type === undefined || Type === null || Type === '' || Type === '?') return { Value: '<未知类型>', Next: Off, Bad: true }; const T = String(Type); const Opt = /^opt<([\s\S]+)>$/.exec(T); if (Opt) { if (Off >= Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; // Ht 包装:先 1 字节标志,非 0 表示 null;有值则紧跟内层 if (Buf[Off] !== 0) return { Value: null, Next: Off + 1 }; return ReadValue(Buf, Off + 1, Opt[1], TypeCodec, Depth); } const List = /^list<([\s\S]+)>$/.exec(T); if (List) { const N = ReadVarint(Buf, Off); let At = N.Next; const Arr = []; for (let I = 0; I < N.Value; I++) { if (I >= 4096) { Arr.push('<元素过多>'); break; } const R = ReadValue(Buf, At, List[1], TypeCodec, Depth + 1); Arr.push(R.Value); if (R.Bad || R.Next <= At) { Arr.push('<卡住>'); break; } At = R.Next; } return { Value: Arr, Next: At, Bad: N.Bad }; } const Enum = /^enum<([\s\S]+)>$/.exec(T); if (Enum) { if (EnumBytes === 0) return { Value: '', Next: Off }; if (Off + EnumBytes > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; let V = 0; for (let I = 0; I < EnumBytes; I++) V = (V << 8) | Buf[Off + I]; return { Value: 'enum#' + V, Next: Off + EnumBytes }; } switch (T) { case 'bool': if (Off >= Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Buf[Off] !== 0, Next: Off + 1 }; case 'i8': if (Off + 1 > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Buf.readInt8(Off), Next: Off + 1 }; case 'i16': if (Off + 2 > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Buf.readInt16BE(Off), Next: Off + 2 }; case 'i32': if (Off + 4 > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Buf.readInt32BE(Off), Next: Off + 4 }; case 'i64': if (Off + 8 > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Buf.readBigInt64BE(Off).toString(), Next: Off + 8 }; case 'f32': if (Off + 4 > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Number(Buf.readFloatBE(Off).toFixed(4)), Next: Off + 4 }; case 'f64': if (Off + 8 > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; return { Value: Buf.readDoubleBE(Off), Next: Off + 8 }; case 'string': case 'bytes': case 'array': { const N = ReadVarint(Buf, Off); const End = N.Next + N.Value; if (End > Buf.length) return { Value: '<越界>', Next: Off, Bad: true }; const Raw = Buf.slice(N.Next, End); return { Value: T === 'string' ? Raw.toString('utf8') : '0x' + Raw.toString('hex').slice(0, 64), Next: End }; } default: break; } // 模型类:就地展开它自己的 codec 字段(无长度前缀) if (TypeCodec && CodecByKey.has(TypeCodec)) { const R = ReadFields(Buf, Off, CodecByKey.get(TypeCodec), Depth + 1); return { Value: R.Value, Next: R.Next, Bad: R.Bad }; } // 字段里没带 codec 链接时:先按模型真名找 codec,再按「协议核心类 → 线上基础类型」表换算 if (CodecByModelName.has(T)) { const R = ReadFields(Buf, Off, CodecByModelName.get(T), Depth + 1); return { Value: R.Value, Next: R.Next, Bad: R.Bad }; } if (WireClasses.has(T)) return ReadValue(Buf, Off, WireClasses.get(T), '', Depth + 1); return { Value: '<无 codec:' + T + '>', Next: Off, Bad: true }; } function ReadFields(Buf, Off, Codec, Depth) { const Obj = {}; let At = Off; let Bad = false; for (const F of (Codec && Codec.Fields) || []) { const R = ReadValue(Buf, At, F.Type, F.TypeCodec, Depth); // 匿名字段(表里没有名字,只有线宽)用序号区分,否则会同名互相覆盖 const Key = F.Name === '(匿名)' || F.Name === '?' ? '(匿名#' + F.Order + ')' : F.Name; Obj[Key] = R.Value; if (R.Bad) { Bad = true; break; } At = R.Next; } return { Value: Obj, Next: At, Bad }; } // 按 codec 键解一条记录 function ReadByKey(Buf, Off, CodecKey, Depth) { const C = CodecByKey.get(CodecKey); if (!C) return null; return ReadFields(Buf, Off, C, Depth || 0); } return { ReadValue, ReadFields, ReadByKey, ReadVarint, EnumBytes }; } function FormatValue(V) { if (V === null) return 'null'; if (Array.isArray(V)) return '[' + V.length + ']' + (V.length <= 6 ? JSON.stringify(V, null, 0) : ''); if (typeof V === 'object') return JSON.stringify(V, null, 0); if (typeof V === 'string' && V.length > 90) return JSON.stringify(V.slice(0, 90) + '…'); return JSON.stringify(V); } // 把解出的对象渲染成多行(字段名左对齐 28 列) function FormatFields(Obj, Indent) { const L = []; for (const [K, V] of Object.entries(Obj || {})) L.push(Indent + String(K).padEnd(28) + ' = ' + FormatValue(V)); return L; } module.exports = { LoadTables, MakeReader, ReadVarint, FormatValue, FormatFields, MAX_DEPTH };