Files
TankIC/3DTank/tools/lib_space_codec.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

209 lines
9.3 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.
// 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<T> = varint 元素数 + 元素…
// opt<T> = 1 字节标志(非 0 = 空)+ 有值时紧跟 T
// enum<T> = 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: '<enum 不上线>', 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 };