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

155 lines
9.4 KiB
SQL
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.
-- 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 = '当前挂载';