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

139 lines
6.7 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.
// dispatch.js - 空间层:识别空间帧 → 逐条记录解码 → 落日志 → 把「未实现」记进世界状态
//
// 帧体 = [nullmap][第一个 int64][(methodId int64)(负载)…]
// 第一个 int64 的语义按方向不同(出处:module_61860 的 SpaceCommand codec,fr.o7g 依次写 f2u(第一个)、f2u(methodId)):
// c2s → objectId(目标对象:实测有空间自身 id,也有实体 id)
// s2c → spaceId
// 判据(两道门,与 relay 模式一致,实测 171/171 命中):
// ① 首字节必须是第一个 id 的最高位:0x00 = 小正数,0xff = 负数(握手/日志帧首字节是命令 ID 1/3/32)
// ② 8 字节之后必须命中 names/uplink_table.json 的 methodId
// 负载长度不靠猜:按名字/codec 表逐字段解码推进;解不动就停在原地并把剩余字节原样打出(fail-fast)。
//
// Handle 的第 5 个参数 OnRecord 是可选钩子(index.js 用它接会话重放):返回真值 = 这条上行已被处理,
// 不再记进未实现清单。
'use strict';
const { FormatValue } = require('../lib_space_codec');
const { ResolveFields } = require('./codec');
const ModelService = require('./modelservice');
function MakeDispatcher(Tables, Reader, World, Options) {
const Opts = Options || {};
const Classes = (Opts.Alias && Opts.Alias.Classes) || {};
const Methods = (Opts.Alias && Opts.Alias.Methods) || {};
const Fields = (Opts.Alias && Opts.Alias.Fields) || {};
// 别名词典(names/command_alias.json)是**派生命名**,不是客户端原始名;键 = "<moduleId>::<短名>"
const Mod2 = (M) => String(M || '').replace(/^module_/, '');
const AliasOfClass = (M, N) => {
const E = Classes[M + '::' + N];
return E ? E.Alias : null;
};
const AliasOfMethod = (M, O, F) => {
const E = Methods[M + '::' + O + '.' + F];
return E ? E.Alias : null;
};
const AliasOfField = (M, O, F) => {
const E = Fields[M + '::' + O + '.' + F];
return E ? E.Alias : null;
};
// methodId(8 字节大端 hex)→ 上行命令
const UplinkByHex = new Map();
for (const [MethodId, Cmd] of Tables.UplinkByMethod) {
const B = Buffer.alloc(8);
B.writeBigInt64BE(BigInt(MethodId));
UplinkByHex.set(B.toString('hex'), Cmd);
}
// 判定「空间层帧」的唯一可靠依据 = 偏移 At+8 处的 8 字节 methodId 在上行表里。
// 不要再加「目标 id 首字节必须是 0x00 / 0xff」这类启发式:目标 id 不一定是空间号,
// 也可能是用户对象 id(实测 3074457345817168740 = 0x2AAAAAAAAAB685CB64,首字节 0x2A)。
// 加了那条会把合法空间命令整条误判成入口命令整包丢弃(server-logs/hashlogin-01/session_010:17,19,46
// 的 "未知入口命令 ID=42" 就是它:ID=42 其实是 0x2A,目标 id 的第一个字节)。
// 反方向的误判风险可忽略:上行表 360 条,8 字节随机值撞上任一条的概率约 360/2^64。
function IsSpaceFrame(Body, At) {
if (At + 16 > Body.length) return false;
return UplinkByHex.has(Body.slice(At + 8, At + 16).toString('hex'));
}
// At = 第一个 id 的起始偏移(已跳过 nullmap)
function Describe(Body, At) {
const TargetId = Body.readBigInt64BE(At).toString();
const Records = [];
let Off = At + 8;
while (Off + 8 <= Body.length) {
const Cmd = UplinkByHex.get(Body.slice(Off, Off + 8).toString('hex'));
if (!Cmd) break;
const MethodId = Body.readBigInt64BE(Off).toString();
Off += 8;
const PayloadStart = Off;
const { Codec, Fields } = ResolveFields(Tables, MethodId, Cmd);
const Values = [];
let Bad = false;
for (const F of Fields) {
const R = Reader.ReadValue(Body, Off, F.Type, F.TypeCodec, 0);
Values.push([F.Name || '(匿名)', F.Type, R.Value]);
if (R.Bad || R.Next <= Off) {
Bad = true;
break;
}
Off = R.Next;
}
Records.push({
MethodId,
Name: (Cmd.OwnerName || Cmd.OwnerClass || '?') + '.' + (Cmd.SendFn || '?'),
Module: Cmd.Module,
CodecName: Codec ? Codec.ModelName || Codec.ModelClassReal || '' : '',
Payload: Body.slice(PayloadStart, Off),
Values,
Bad,
});
}
return { TargetId, Records, Next: Off, Tail: Body.slice(Off) };
}
// 落盘 + 记未实现;返回 Describe 的结果供调用方继续用
// OnRecord(可选)= 调用方插进来的「这条上行我自己处理」钩子,返回真值表示**不算未实现**。
// 为什么钩子要放在这里而不是调用方:NoteUnimplemented 就在本函数里记,
// 调用方(index.js)拿到 Info 时"未实现"已经记完了。会话重放(session.js)命中并发帧后
// 必须能拦住这一笔记账,否则断开时统计里还是"这些命令没人管"。
function Handle(State, Body, At, SpaceKind, OnRecord) {
const Info = Describe(Body, At);
if (!Info.Records.length) {
State.Log(' 空间帧,但 methodId 没解出记录:' + Body.slice(At).toString('hex').slice(0, 120));
return Info;
}
State.Log(' 目标 id=' + Info.TargetId + ' 命令 ' + Info.Records.length + ' 条');
for (const R of Info.Records) {
const Ms = ModelService.Name(R.MethodId);
const Ma = Mod2(R.Module);
const Parts = String(R.Name).split('.');
const Owner = Parts[0];
const Fn = Parts[1];
const ClassAlias = AliasOfClass(Ma, Owner);
const MethodAlias = AliasOfMethod(Ma, Owner, Fn);
const AliasTag =
ClassAlias || MethodAlias ? ' 【' + (ClassAlias || Owner) + '.' + (MethodAlias || Fn) + '】' : '';
State.Log(
' ' + R.Name + ' (' + R.Module + ')' + (R.CodecName ? ' [' + R.CodecName + ']' : '') + AliasTag +
(Ms ? ' ← modelService: ' + Ms : '') + ' methodId=' + R.MethodId + (R.Bad ? ' ⚠ 负载解码中断' : ''),
);
if (Ms && Ms.indexOf('G.q1wd') === 0) State.Log(' → 请求内容: ' + FormatValue(ModelService.DescribeRequest(R.Payload || Buffer.alloc(0))));
for (const [K, T, V] of R.Values) {
const Fa = AliasOfField(Ma, Owner, K);
State.Log(' ' + String(Fa ? Fa + ' (' + K + ')' : K).padEnd(28) + String(T).padEnd(14) + ' = ' + FormatValue(V));
}
if (!(typeof OnRecord === 'function' && OnRecord(R, Info.TargetId))) {
World.NoteUnimplemented(SpaceKind, R.Name, R.MethodId, Info.TargetId);
}
}
if (Info.Tail.length) {
State.Log(' ⚠ 尾部剩 ' + Info.Tail.length + 'B 没解出:' + Info.Tail.toString('hex').slice(0, 96));
}
return Info;
}
return { IsSpaceFrame, Describe, Handle, UplinkByHex };
}
module.exports = { MakeDispatcher };