工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
139 lines
6.7 KiB
JavaScript
139 lines
6.7 KiB
JavaScript
// 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 };
|