类别来源(完整链):挂载 reducer IB.yr9 用物品的 qr3_1 查类别表;qr3_1 = oB 构造第 4 参 = n.yr6_1.ici_1;yr6_1 来自 pt 里 model 类 Ij 那一项 = modelId 6117306895090150422(module_85196::Lo,字段名就叫 category)。交叉验证:真实 5 件 mountedItems 的类别 {enum#1,enum#0,enum#13,enum#2,enum#8} 互不相同 → 规则成立;并确认 M7 留下的第 6 件 920009631018 与 920009631039 同为 enum#1(都是底盘),正是 M7 缺口的体现。排查并排除:不是本地静态表;GarageSelectItem 的 Yy/Xy/ek/Wy 是另一套枚举(模型 ox/2114179554921095892 的 pr3_1),与挂载互斥无关。实现:schema 加 Items.Category + 索引;新增可重跑的 backfill_category.js(从 ObjectModels 回填,支持 --dry),992 件全部回填;store.SetMounted 改为「先卸同类、再置新件」且幂等,日志打印「同时卸下 N 件同类」;客户端 ClientState.MarkMounted 同步互斥(与 IB.yr9 同顺序),UI 每行显示类别。验收:连装两件 enum#1 → 库中只剩后者(日志「同时卸下 1 件同类」);浏览器 a11y 快照前后对比正确;回归 selftest 1108/9/90、CompareContainerDecode 5 帧 0 不一致、三构建全绿。未解:enum#N 只是线形序数,枚举成员名未解出,库里一律存原始令牌 enum#N。
167 lines
11 KiB
SQL
167 lines
11 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 的落地)
|
||
--
|
||
-- Category(M8):物品的「装备槽类别」,同类互斥的依据(改 SetMounted 用它把同类旧件卸下)。
|
||
-- 来源:物品对象在 pt 里的一个模型项,modelId = 6117306895090150422(module_85196::Lo),
|
||
-- 唯一字段名就叫 category(prop ici_1,线型 enum<ja>),值形如 enum#0/1/2/3/8/9/13。
|
||
-- 客户端侧的对应关系(逐行证据):module_10032.js:5266 的 oB.qr3_1 由 sr6 的 n.yr6_1.ici_1 传入
|
||
-- (28655 → 28746),而 hB.yr6_1 就是 pt 里那个 modelId=6117306895090150422 的项(module_18334.js:1760);
|
||
-- 挂载 reducer IB.yr9(29845-29883)按 qr3_1 找同类已挂载件并置 0,再置新件为 1。
|
||
-- ★ 值一律存**原始枚举令牌**(如 'enum#1'),不转数字:协议里没解出枚举成员名,转数字等于自己发明口径。
|
||
-- NULL = 该项没解出类别(此时 SetMounted 不做同类互斥,只置位)。
|
||
-- ★ 老库不会因 CREATE TABLE IF NOT EXISTS 拿到这一列,用 backfill_category.js 补列并回填(可重跑)。
|
||
CREATE TABLE IF NOT EXISTS Items (
|
||
ItemId BIGINT NOT NULL,
|
||
OwnerUid VARCHAR(24) NULL,
|
||
Category VARCHAR(16) NULL COMMENT '装备槽类别(pt 模型 6117306895090150422 的 category,如 enum#1)',
|
||
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),
|
||
KEY ItemsCategoryIdx (Category)
|
||
) 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 = '当前挂载';
|