工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
155 lines
9.4 KiB
SQL
155 lines
9.4 KiB
SQL
-- schema.sql - 3DTank 替代服务器的持久化表结构(库名见 local/db.json 的 Database,默认 tanki)
|
||
--
|
||
-- 设计口径(与协议对齐,不是凭空拟的):
|
||
-- 1. 协议里的「对象」= (ObjectId, GameTypeId) 一对 i64,来自 _loadObjectsData 的 pt.Objects
|
||
-- (tools/server/container.js 的 ReadPt)。所以 SpaceObjects 就是对象仓库主表。
|
||
-- 2. 「对象的模型数据」= pt.Items 里属于该对象的那一段(container.js 的 LtItem:
|
||
-- Id + 就地展开的字段)。按 Ord 保序存进 ObjectModels,重放时按序拼回去。
|
||
-- 3. `G.q1wd(n)` 的 n 就是 _loadDependencies 容器 zt 的首字段 X(源码依据:
|
||
-- module_29554.js 的 O.p7g 先读 i32 进 k1wc_1;module_45686.js:362 的 h1wd 用 k1wc_1 回调 q1wd)。
|
||
-- 所以 DependencyBatches / DependencyItems 就是「第 n 批要装哪些对象/模型」的表。
|
||
-- 4. Containers 存的是「按 methodId 定布局」解出来的原始容器记录(Layout + ValueJson),
|
||
-- 既能逐字节重建原帧(当审计基线),也是导入失败时唯一的真相来源。
|
||
-- 5. 所有 id 列一律 **有符号 BIGINT**:协议里确实存在负数合成对象 id
|
||
-- (实测 -2147481788 = 0xFFFFFFFF80000744 一族,见 tools/audit_space_init.js 头注)。
|
||
-- Node 侧 mysql2 按字符串取(见 db.js 的 supportBigNumbers),与现有代码的「id 一律当字符串」一致。
|
||
-- 6. zt.Items 的 `R`/`E`/`U`/`S`/`O` 五个字母**照容器布局写**,语义尚未解出,不硬起名字误导后人。
|
||
-- 7. 所有 JSON 文本列用 **LONGTEXT + JSON_VALID 检查**,不用 JSON 列。两个实测原因:
|
||
-- a) MySQL 的 JSON 列对 double 不是逐位可逆(实测同一个值往返差 1 ulp:
|
||
-- -10.321807861328125 → -10.321807861328123)。本项目的底线是「从库里能把帧逐字节重建」,
|
||
-- 一旦出现真 f64 字段就会破功;LONGTEXT 存原文本,不经过 double。
|
||
-- b) 模型 id 是 i64,可以超过 2^53(如 3074457345817168740),走 JSON 数字有精度风险。
|
||
-- 8. `zt` 的首字段 X(客户端 `G.q1wd` 回的那个数)是**客户端侧回调号、会复用**:
|
||
-- 实测空间 100073070 里 X=1 被 11 条互不相同的 zt 重发。所以依赖批次的身份是
|
||
-- 「哪一条容器记录」,主键 = `(SpaceId, SourceFile, RecordIndex, Ord)`;按 X 查只走索引。
|
||
-- 9. 同一个对象在不同空间里可以被重发且内容不同(实测本账号对象 3074457345817168740 有 3 份不同内容),
|
||
-- 所以 ObjectModels 的唯一键是 `(SpaceId, ObjectId, Ord)`。
|
||
|
||
-- 账号:登录 token(Hn.wh4 的 loginHash)→ 玩家对象 id
|
||
CREATE TABLE IF NOT EXISTS Accounts (
|
||
Uid VARCHAR(24) NOT NULL COMMENT '玩家对象 id(i64 十进制字符串)',
|
||
LoginHash VARCHAR(160) NULL COMMENT 'Hn.wh4 的登录 token',
|
||
NickName VARCHAR(64) NULL,
|
||
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (Uid),
|
||
KEY AccountsLoginHashIdx (LoginHash)
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '账号';
|
||
|
||
-- 对象仓库主表:(ObjectId, GameTypeId)
|
||
CREATE TABLE IF NOT EXISTS SpaceObjects (
|
||
ObjectId BIGINT NOT NULL COMMENT '对象 id(可负)',
|
||
GameTypeId BIGINT NOT NULL COMMENT '对象类型 id',
|
||
OwnerUid VARCHAR(24) NULL COMMENT '归属账号;空间级合成对象为 NULL',
|
||
SourceSpace VARCHAR(24) NULL COMMENT '首次出现的空间号',
|
||
PRIMARY KEY (ObjectId),
|
||
KEY SpaceObjectsGameTypeIdx (GameTypeId),
|
||
KEY SpaceObjectsOwnerIdx (OwnerUid)
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '对象仓库';
|
||
|
||
-- 对象的模型数据项(pt.Items 按对象切出来的片,按 Ord 保序)
|
||
CREATE TABLE IF NOT EXISTS ObjectModels (
|
||
Seq BIGINT NOT NULL AUTO_INCREMENT,
|
||
SpaceId VARCHAR(24) NOT NULL COMMENT '所属空间(同一对象在不同空间可以内容不同)',
|
||
ObjectId BIGINT NULL COMMENT '归属对象;NULL = 没能归属到具体对象(保留但不参与组帧)',
|
||
Ord INT NOT NULL COMMENT '该对象内的次序,从 0 开始',
|
||
ModelId BIGINT NOT NULL COMMENT '0 = 「id=0 + 紧跟一个 i64 字面量」那种项',
|
||
ValueJson LONGTEXT NOT NULL COMMENT 'container.js 的 LtItem.Value 的 JSON 文本(Id 为 0 时是 i64 字符串)',
|
||
PRIMARY KEY (Seq),
|
||
UNIQUE KEY ObjectModelsOrdIdx (SpaceId, ObjectId, Ord),
|
||
KEY ObjectModelsModelIdx (ModelId),
|
||
CONSTRAINT ObjectModelsJsonChk CHECK (JSON_VALID(ValueJson))
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '对象模型数据';
|
||
|
||
-- 依赖批次(_loadDependencies 容器 zt):BatchId 就是首字段 X,也就是客户端 G.q1wd 回的那个数
|
||
--
|
||
-- ★ 为什么主键里带 SourceFile / RecordIndex(而不是 (SpaceId, BatchId)):
|
||
-- X 是**客户端侧的回调号**,会被复用 —— 实测空间 100073070 里 X=1 被 11 条互不相同的 zt
|
||
-- 重发(s2c_0000 带 8 个 modelId、s2c_0003 带 16 个、s2c_0006 带 4 个…)。
|
||
-- 所以「一批依赖」的身份 = **哪一条容器记录**,跟 Containers 表用同一套自然键;
|
||
-- 按 X 查只是索引(DependencyBatchesLookupIdx),不是唯一性。
|
||
CREATE TABLE IF NOT EXISTS DependencyBatches (
|
||
SpaceId VARCHAR(24) NOT NULL,
|
||
SourceFile VARCHAR(24) NOT NULL,
|
||
RecordIndex INT NOT NULL,
|
||
Ord INT NOT NULL,
|
||
BatchId INT NOT NULL COMMENT 'zt 首字段 X(客户端 G.q1wd 回的就是它,会复用)',
|
||
ObjectId BIGINT NOT NULL,
|
||
ModelIds LONGTEXT NOT NULL COMMENT '该批次要装的模型 id 列表(十进制字符串数组的 JSON)',
|
||
PRIMARY KEY (SpaceId, SourceFile, RecordIndex, Ord),
|
||
KEY DependencyBatchesLookupIdx (SpaceId, BatchId),
|
||
CONSTRAINT DependencyBatchesJsonChk CHECK (JSON_VALID(ModelIds))
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '依赖批次-对象';
|
||
|
||
CREATE TABLE IF NOT EXISTS DependencyItems (
|
||
SpaceId VARCHAR(24) NOT NULL,
|
||
SourceFile VARCHAR(24) NOT NULL,
|
||
RecordIndex INT NOT NULL,
|
||
Ord INT NOT NULL,
|
||
BatchId INT NOT NULL,
|
||
`R` BIGINT NOT NULL COMMENT '字母照 zt 布局,语义未解',
|
||
`E` SMALLINT NOT NULL,
|
||
`U` SMALLINT NOT NULL,
|
||
`S` BIGINT NOT NULL,
|
||
`O` TINYINT UNSIGNED NOT NULL,
|
||
Skipped LONGTEXT NOT NULL COMMENT '该跳过的 id 列表(十进制字符串数组的 JSON)',
|
||
PRIMARY KEY (SpaceId, SourceFile, RecordIndex, Ord),
|
||
KEY DependencyItemsLookupIdx (SpaceId, BatchId),
|
||
CONSTRAINT DependencyItemsJsonChk CHECK (JSON_VALID(Skipped))
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '依赖批次-资源项';
|
||
|
||
-- 原始容器记录(审计基线 + 逐字节重建的来源)
|
||
CREATE TABLE IF NOT EXISTS Containers (
|
||
Seq BIGINT NOT NULL AUTO_INCREMENT,
|
||
SpaceId VARCHAR(24) NOT NULL,
|
||
SourceFile VARCHAR(24) NOT NULL COMMENT '如 s2c_0001.bin',
|
||
RecordIndex INT NOT NULL COMMENT '帧内第几条记录,从 0 开始',
|
||
ObjectId BIGINT NOT NULL COMMENT '记录头里的 objectId',
|
||
MethodId BIGINT NOT NULL,
|
||
Layout VARCHAR(4) NOT NULL COMMENT 'lt / pt / zt',
|
||
`X` INT NULL COMMENT '仅 zt 有:依赖批次号',
|
||
ValueJson LONGTEXT NOT NULL COMMENT 'container.js 的 Value 的 JSON 文本',
|
||
PRIMARY KEY (Seq),
|
||
UNIQUE KEY ContainersLocIdx (SpaceId, SourceFile, RecordIndex),
|
||
KEY ContainersMethodIdx (MethodId),
|
||
CONSTRAINT ContainersJsonChk CHECK (JSON_VALID(ValueJson))
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '容器记录原文';
|
||
|
||
-- 物品:仓库 / 市场 / 已挂载 三个旗标(Items 表就是 itemsOnDepot / itemsOnMarket / mountedItems 的落地)
|
||
CREATE TABLE IF NOT EXISTS Items (
|
||
ItemId BIGINT NOT NULL,
|
||
OwnerUid VARCHAR(24) NULL,
|
||
InDepot TINYINT (1) NOT NULL DEFAULT 0,
|
||
InMarket TINYINT (1) NOT NULL DEFAULT 0,
|
||
Mounted TINYINT (1) NOT NULL DEFAULT 0,
|
||
UpdatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (ItemId),
|
||
KEY ItemsOwnerIdx (OwnerUid)
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '物品';
|
||
|
||
-- 车库挂载位(对应 devicesLoaded 记录里那些 ObjectId)
|
||
CREATE TABLE IF NOT EXISTS DeviceSlots (
|
||
SlotObjectId BIGINT NOT NULL,
|
||
OwnerUid VARCHAR(24) NULL,
|
||
PRIMARY KEY (SlotObjectId)
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '挂载位';
|
||
|
||
-- 挂载位的可用设备(devicesLoaded 的 devices 列表)
|
||
CREATE TABLE IF NOT EXISTS SlotDevices (
|
||
SlotObjectId BIGINT NOT NULL,
|
||
Ord INT NOT NULL,
|
||
DeviceId BIGINT NOT NULL,
|
||
PRIMARY KEY (SlotObjectId, DeviceId),
|
||
KEY SlotDevicesOrdIdx (SlotObjectId, Ord)
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '挂载位可用设备';
|
||
|
||
-- 挂载位当前挂载的设备(devicesLoaded 的 mountedDevice)。
|
||
-- ★ 抓包里 28 条 mountedDevice 全是 null,所以这张表初始全空;
|
||
-- 「已装备标记」要显示出来,就靠这里被写进值(挂载动作落地后)。
|
||
CREATE TABLE IF NOT EXISTS Mounted (
|
||
SlotObjectId BIGINT NOT NULL,
|
||
OwnerUid VARCHAR(24) NULL,
|
||
DeviceId BIGINT NULL COMMENT 'NULL = 该挂载位当前没有装备',
|
||
UpdatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (SlotObjectId)
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '当前挂载';
|