- 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 等命令精确触发应答
209 lines
9.3 KiB
JavaScript
209 lines
9.3 KiB
JavaScript
// 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 };
|