Files
TankIC/3DTank/tools/server/codec.js
T
TermiNexus b6f2cb71ee 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/(含数据库口令)不进库,可用上述工具重新生成。
2026-09-17 20:00:41 +08:00

239 lines
12 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.
// codec.js - 空间层负载的字段级编码器 MakeWriter(写包用,与 lib_space_codec 的 MakeReader 严格对称)
//
// 编码规则(出处同 lib_space_codec.js 头部注释,module_24635 的序列化器实现):
// bool/i8=1B i16=2B i32/f32=4B i64/f64=8B(大端)
// string/bytes = varint 长度 + 原始字节
// list<T> = varint 元素数 + 元素…
// map<K, V> = varint 条数 + 条数 × (K, V)(读侧同源,见 lib_space_codec.js 头部 map 那段)
// opt<T> = 1 字节标志(0 = 有值,非 0 = 空)+ 有值时紧跟 T
// —— 这是「字段表」那条路径的近似口径。真实线形是 nullmap 里的 1 个 bit(见下 OptBits)。
// opt<T>(OptBits 模式)= 不写任何标志字节:空值写 0 字节,有值只写内层 T。
// 为什么写侧不用管那个 bit:位在 nullmap 里,而 nullmap 由帧级 NullmapHex 原样带上
// (见 space.js:26-32、semantic.js:267-284);写侧只要保证「空值真的一个字节都不写」
// (src/module_24635.js:412-414 的 Ht.o7g:null 时只写位、不写值)。
// opt<T>(OptBits 模式 + AttachBits)= 「空/非空」那个 bit **写回**调用方给的 nullmap 比特流
// (就地覆盖 Bits[Index],见下 PutBit)。语义层重建 nullmap 时按这条路径走:
// 位图 = 形态位 + 各 opt 位,读写必须消费同一顺序,否则位就串了。
// enum<T> = 4 字节(大端无符号;读侧按同一宽度拼出,可能为负,这里用 asUintN 还原)
// 模型类 = 就地展开该模型 codec 的字段(无长度前缀)
// 取值形状:**与 MakeReader 的输出一一对应**(i64 = 十进制字符串、enum = 'enum#N' 或数字、
// bytes = Buffer/0x 十六进制串、opt 空 = null),这样「解出来再写回去」能逐字节还原。
// 缺字段:opt 缺 = 空;非 opt 缺 → 直接抛错(不做静默兜底)。
// 未实现:容器三编码(lt/pt/zt,见 module_45686)与模块内建 codec(Ea=变长数组、nh 等)→ 抛错暴露。
'use strict';
const MAX_DEPTH = 8;
const HEX = /^0x[0-9a-fA-F]*$/;
const { SplitTypeArgs } = require('../lib_space_codec');
function MakeWriter(Tables, Options) {
const Opts = Options || {};
const EnumBytes = Opts.EnumBytes === undefined ? 4 : Opts.EnumBytes;
const OptBits = !!Opts.OptBits;
const TypeAlias = Opts.TypeAlias || null;
// 与 MakeReader 的 StringLiteralOnly 严格同源:开了就把 string 一律当字面 UTF-8 写,
// 不再把 '0x…' 当二进制转义(否则字面量 "0xff8000" 会被写成 3 个裸字节)。
const StringLiteralOnly = !!Opts.StringLiteralOnly;
const CodecByKey = Tables.CodecByKey;
const CodecByModelName = Tables.CodecByModelName || new Map();
const WireClasses = Tables.WireClasses || new Map();
// nullmap 比特流的**写**游标(与 lib_space_codec.js:130-148 的读游标同源、同顺序)。
// 由调用方(semantic.js:EncodeFrameBody / container.js:Write)AttachBits 进来:
// AttachBits(Bits, Index) → Bits 是要就地覆盖的位数组(长度固定 = 原 nullmap 的位图位数)
// DetachBits() → 断开(opt 回到「不写位」的老路径)
let BitStream = null;
let BitIndex = 0;
function AttachBits(Bits, Index) {
BitStream = Bits;
BitIndex = Index || 0;
}
function DetachBits() {
BitStream = null;
BitIndex = 0;
}
function BitCursor() {
return { Bits: BitStream, Index: BitIndex };
}
// 写 1 个 opt 位。位不够 = nullmap 比负载短 → 直接抛错(不静默丢弃,否则组帧出来是错的)
function PutBit(V) {
if (!BitStream) throw new Error('内部错误:没有 attach nullmap 比特流却要写 opt 位');
if (BitIndex >= BitStream.length) throw new Error('nullmap 位图用完(已写 ' + BitIndex + ' 位,容量 ' + BitStream.length + ')');
BitStream[BitIndex++] = V ? 1 : 0;
}
function WriteVarint(N) {
const V = Number(N);
if (!Number.isFinite(V) || V < 0) throw new Error('varint 值非法: ' + N);
if (V < 0x80) return Buffer.from([V]);
if (V < 0x4000) return Buffer.from([0x80 | (V >> 8), V & 0xff]);
if (V < 0x400000) return Buffer.from([0xc0 | (V >> 16), (V >> 8) & 0xff, V & 0xff]);
throw new Error('varint 过大: ' + V);
}
function WriteString(S) {
// 读侧对「不是合法 UTF-8 的字符串」会给 0x 十六进制形式,这里原样还原成字节
// (StringLiteralOnly 模式下读侧不会产生这种形式,所以不认它)
if (!StringLiteralOnly && typeof S === 'string' && HEX.test(S)) {
const Raw = Buffer.from(S.slice(2), 'hex');
return Buffer.concat([WriteVarint(Raw.length), Raw]);
}
const B = Buffer.from(String(S), 'utf8');
return Buffer.concat([WriteVarint(B.length), B]);
}
function ToBytes(V) {
if (Buffer.isBuffer(V)) return V;
if (V === null || V === undefined) return Buffer.alloc(0);
if (typeof V === 'string' && HEX.test(V)) return Buffer.from(V.slice(2), 'hex');
if (Array.isArray(V)) return Buffer.from(V);
throw new Error('bytes 值无法识别: ' + typeof V);
}
function WriteValue(Type, TypeCodec, Value, Depth) {
if (Depth > MAX_DEPTH) throw new Error('嵌套超过 ' + MAX_DEPTH + ' 层');
if (Type === undefined || Type === null || Type === '' || Type === '?') throw new Error('字段类型缺失');
const T = String(Type);
const Opt = /^opt<([\s\S]+)>$/.exec(T);
if (Opt) {
if (OptBits) {
// 空值 = 负载里 0 字节(bit 在 nullmap 里);有值只写内层
const Empty = Value === null || Value === undefined;
// 有写游标 ⇒ 把「空/非空」写回 nullmap 比特流(src/module_24635.js:412-414 的 Ht.o7g:
// 写侧对空值只写 1 个 bit 的 !0、不写值)。注意「写位」与「写值」的顺序必须与读侧一致。
if (BitStream) PutBit(Empty);
if (Empty) return Buffer.alloc(0);
return WriteValue(Opt[1], TypeCodec, Value, Depth + 1);
}
if (Value === null || Value === undefined) return Buffer.from([1]);
return Buffer.concat([Buffer.from([0]), WriteValue(Opt[1], TypeCodec, Value, Depth + 1)]);
}
const List = /^list<([\s\S]+)>$/.exec(T);
if (List) {
if (!Array.isArray(Value)) throw new Error('list 值必须是数组: ' + typeof Value);
const Parts = [WriteVarint(Value.length)];
for (const X of Value) Parts.push(WriteValue(List[1], TypeCodec, X, Depth + 1));
return Buffer.concat(Parts);
}
const Enum = /^enum<([\s\S]+)>$/.exec(T);
if (Enum) {
if (EnumBytes === 0) return Buffer.alloc(0);
const N = typeof Value === 'string' ? Number(String(Value).replace(/^enum#/, '')) : Number(Value);
if (!Number.isFinite(N)) throw new Error('enum 值无法识别: ' + Value);
const U = BigInt.asUintN(EnumBytes * 8, BigInt(Math.trunc(N)));
const Out = Buffer.alloc(EnumBytes);
Out.writeUIntBE(Number(U), 0, EnumBytes);
return Out;
}
const Map = /^map<([\s\S]+)>$/.exec(T);
if (Map) {
// 与读侧严格对称:varint 条数 + 逐条 (key, value)(出处见 lib_space_codec.js 头部 map 那段)。
// 值形状必须是 [[key, value], …]——不做「对象转条目」的兜底,那会在键重复/非字符串键时静默丢字节。
const KV = SplitTypeArgs(Map[1]);
if (!Array.isArray(Value)) throw new Error('map 值必须是 [[key, value], …]:' + typeof Value);
const Parts = [WriteVarint(Value.length)];
for (const Pair of Value) {
if (!Array.isArray(Pair) || Pair.length !== 2) throw new Error('map 条目必须是 [key, value]:' + JSON.stringify(Pair));
Parts.push(WriteValue(KV[0], TypeCodec, Pair[0], Depth + 1));
Parts.push(WriteValue(KV[1], TypeCodec, Pair[1], Depth + 1));
}
return Buffer.concat(Parts);
}
switch (T) {
case 'bool': {
const Out = Buffer.alloc(1);
Out[0] = Value ? 1 : 0;
return Out;
}
case 'i8': {
const Out = Buffer.alloc(1);
Out.writeInt8(Number(Value) | 0, 0);
return Out;
}
case 'i16': {
const Out = Buffer.alloc(2);
Out.writeInt16BE(Number(Value) | 0, 0);
return Out;
}
case 'i32': {
const Out = Buffer.alloc(4);
Out.writeInt32BE(Number(Value) | 0, 0);
return Out;
}
case 'i64': {
const Out = Buffer.alloc(8);
Out.writeBigInt64BE(BigInt(Value), 0);
return Out;
}
case 'f32': {
const Out = Buffer.alloc(4);
Out.writeFloatBE(Number(Value), 0);
return Out;
}
case 'f64': {
const Out = Buffer.alloc(8);
Out.writeDoubleBE(Number(Value), 0);
return Out;
}
case 'string':
return WriteString(Value);
case 'bytes':
case 'array': {
const B = ToBytes(Value);
return Buffer.concat([WriteVarint(B.length), B]);
}
default:
break;
}
// 模型类:就地展开它自己的 codec 字段
if (TypeCodec && CodecByKey.has(TypeCodec)) return WriteFields(CodecByKey.get(TypeCodec), Value, Depth + 1);
if (CodecByModelName.has(T)) return WriteFields(CodecByModelName.get(T), Value, Depth + 1);
if (WireClasses.has(T)) return WriteValue(WireClasses.get(T), '', Value, Depth + 1);
// 容器层给的「裸类名 → 线上基础类型」直译表(与 MakeReader 的 TypeAlias 严格同源)
if (TypeAlias && Object.prototype.hasOwnProperty.call(TypeAlias, T)) return WriteValue(TypeAlias[T], '', Value, Depth + 1);
throw new Error('字段类型没有对应 codec: ' + T);
}
function WriteFields(Codec, Obj, Depth) {
const Parts = [];
for (const F of (Codec && Codec.Fields) || []) {
const Key = F.Name === '(匿名)' || F.Name === '?' ? '(匿名#' + F.Order + ')' : F.Name;
const Has = Obj !== null && Obj !== undefined && Object.prototype.hasOwnProperty.call(Obj, Key);
if (!Has) {
if (!/^opt</.test(String(F.Type))) {
throw new Error('缺少必填字段 ' + Key + '(codec=' + ((Codec && Codec.ModelName) || (Codec && Codec.Codec)) + ')');
}
Parts.push(WriteValue(F.Type, F.TypeCodec, null, Depth));
continue;
}
Parts.push(WriteValue(F.Type, F.TypeCodec, Obj[Key], Depth));
}
return Buffer.concat(Parts);
}
function WriteByKey(CodecKey, Obj) {
const C = CodecByKey.get(CodecKey);
if (!C) throw new Error('表里没有这个 codec: ' + CodecKey);
return WriteFields(C, Obj, 0);
}
return { WriteValue, WriteFields, WriteByKey, WriteVarint, WriteString, EnumBytes, OptBits, AttachBits, DetachBits, BitCursor };
}
// 按 methodId 取「描述这条命令负载」的字段表(服务端与自检共用同一套解析,避免两处口径不一):
// 优先 names/codec_fields.json 按 methodId 反查到的 codec(带类型),否则回落到
// names/uplink_table.json 里命令自带的字段列表(只有名字与类型串)。
// 实测两张表的 methodId 基本不重合(上行 360 条走 uplink_table,下行 codec 432 个走 codec_fields),
// 所以两个来源都得看。
function ResolveFields(Tables, MethodId, Cmd) {
const Codec = Tables.CodecByMethod.get(String(MethodId));
if (Codec) return { Codec, Fields: Codec.Fields || [] };
const Fields = ((Cmd && Cmd.Fields) || []).map((F) => ({ Name: F.Field, Type: F.Type, TypeCodec: F.TypeCodec || '' }));
return { Codec: null, Fields };
}
module.exports = { MakeWriter, ResolveFields, MAX_DEPTH };