数据:evidence.json 的 byModule.<模块>.enums = { 枚举短名: [{field, member, value}] },全局 348 类 / 2633 成员。查表键定为 (字段所属模块, 枚举短名, 序数):同模块内短名天然唯一;跨模块同名 11 个短名 / 9 个同名异义,键含模块号即可隔离。全表 102 条 enum<X> 引用的定义模块均由字段的 TypeCodec 唯一确定(无冲突、未解 0),导出后落盘 77 类 / 1343 成员(33879 字符,整表增量 33894)。两层分离(本任务核心约束):协议层原始值仍是 enum#N,一字不改,因此 CompareContainerDecode.mjs 的逐字段对照口径不变;EnumNames 只在 UI/日志/dump 里翻译,查不到返回 null 并由调用方标「未解」,不猜。category 实测(15 件仓库物品全部解出):ARMOR#1 / WEAPON#0 / PAINT#2 / BAZOOKA#13 / DRONE#8 / INVENTORY#3 / INVISIBLE#9,真实 5 件挂载物品类别互不相同 —— M8 结论不变。UI 与日志显示「成员名(enum#N)」。顺带修正 GarageDriver 接线:上一轮把 URL 硬编码成 9092 且日志 label 写死 store-02,改为环境变量 TANKIC_WS_URL(默认 9091)与 TANKIC_LOG_LABEL(默认 store-03),否则「配对表 ⇔ 服务端日志」会找错目录。验收:CompareContainerDecode 5 帧 / 一致叶子 26950 / 不一致 0(硬门槛);server selftest 逐字节一致 1108 / 不一致 9 / 解码中断 90;三构建全绿;浏览器实测 15/15 类别显示成员名。未解:枚举成员名未覆盖 list<enum<X>>/opt<enum<X>> 的元素级。
162 lines
7.0 KiB
JavaScript
162 lines
7.0 KiB
JavaScript
// backfill_category.js - M8:把物品的「装备槽类别」回填进 Items.Category(可重跑)
|
||
//
|
||
// 用法: node tools/server/backfill_category.js [--space 100073070] [--dry]
|
||
//
|
||
// 为什么需要这张表这一列:M7 的 SetMounted 只会把该件置 Mounted=1,**不知道类别**,所以连装两件同槽
|
||
// 物品时库里两件都是 1。M8 查清了类别来源(见 schema.sql 的 Items.Category 注释与架构结论):
|
||
// 物品对象在 pt 里的模型项 modelId=6117306895090150422(module_85196::Lo)有一个唯一字段 `category`
|
||
// (prop ici_1,线型 enum<ja>),客户端挂载 reducer IB.yr9 就是按它做同类互斥的。
|
||
//
|
||
// 类别从哪取:ObjectModels 里 ModelId=6117306895090150422 的那些行,其 ValueJson 的 `category` 字段。
|
||
// 同一个 ItemId 在多帧/多空间可能有多行 —— 口径 = **MIN(Seq)**(首次出现那一行,与 store.js 的
|
||
// 「对象出现序」同源)。取不到的行 Category 留 NULL(宁可不填,也不猜;SetMounted 见 NULL 会跳过互斥并打日志)。
|
||
//
|
||
// 幂等 / 可重跑:
|
||
// · 列不存在才 ALTER(information_schema.COLUMNS 判定;MySQL 8 没有 ADD COLUMN IF NOT EXISTS);
|
||
// · 只对「值确实要变」的行发 UPDATE,重复跑第二次 changed=0;
|
||
// · 全程不动 Mounted/InDepot/InMarket(回填不改变装备状态)。
|
||
//
|
||
// --dry:只算不写(只打印将要发生的变更数,不碰库)。
|
||
'use strict';
|
||
const Db = require('./db');
|
||
const EnumNames = require('./enum_names');
|
||
|
||
const Argv = process.argv.slice(2);
|
||
const Arg = (Name, Def) => {
|
||
const I = Argv.indexOf('--' + Name);
|
||
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
|
||
};
|
||
const DRY = Argv.indexOf('--dry') >= 0;
|
||
const SpaceId = String(Arg('space', '100073070'));
|
||
|
||
// 物品类别所在的模型 id(module_85196::Lo)—— 出处见文件头
|
||
const CATEGORY_MODEL = '6117306895090150422';
|
||
|
||
const PRINT = (Msg) => console.log(Msg);
|
||
|
||
// 补列(幂等):information_schema 查不到 Items.Category 才 ALTER,顺带建索引
|
||
async function EnsureCategoryColumn(Pool) {
|
||
const Row = await Db.One(
|
||
Pool,
|
||
'SELECT COUNT(*) AS N FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?',
|
||
['Items', 'Category'],
|
||
);
|
||
if (Number(Row.N) > 0) {
|
||
PRINT('· Items.Category 已存在,跳过建列');
|
||
return false;
|
||
}
|
||
await Pool.query(
|
||
"ALTER TABLE Items ADD COLUMN Category VARCHAR(16) NULL COMMENT '装备槽类别(pt 模型 6117306895090150422 的 category,如 enum#1)' AFTER OwnerUid",
|
||
);
|
||
const Idx = await Db.One(
|
||
Pool,
|
||
'SELECT COUNT(*) AS N FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?',
|
||
['Items', 'ItemsCategoryIdx'],
|
||
);
|
||
if (Number(Idx.N) === 0) await Pool.query('ALTER TABLE Items ADD KEY ItemsCategoryIdx (Category)');
|
||
PRINT('· 已为 Items 建列 Category + 索引 ItemsCategoryIdx');
|
||
return true;
|
||
}
|
||
|
||
// ItemId → 类别令牌(如 'enum#1')。取不到返回 null
|
||
async function LoadCategories(Pool, ItemIds) {
|
||
const Out = new Map();
|
||
for (let I = 0; I < ItemIds.length; I += 200) {
|
||
const Chunk = ItemIds.slice(I, I + 200);
|
||
const Rows = await Db.Many(
|
||
Pool,
|
||
'SELECT ObjectId, ValueJson FROM ObjectModels WHERE SpaceId = ? AND ModelId = ? AND ObjectId IN (' +
|
||
Chunk.map(() => '?').join(',') + ') ORDER BY Seq',
|
||
[SpaceId, CATEGORY_MODEL].concat(Chunk),
|
||
);
|
||
for (const R of Rows) {
|
||
const Id = String(R.ObjectId);
|
||
if (Out.has(Id)) continue; // ORDER BY Seq ⇒ 第一次出现的那行胜出
|
||
try {
|
||
const V = JSON.parse(R.ValueJson);
|
||
if (V && typeof V.category === 'string') Out.set(Id, V.category);
|
||
} catch (Err) {
|
||
// ValueJson 不是合法 JSON:这一行不参与,继续看下一行(列本身有 JSON_VALID 约束,正常不会发生)
|
||
}
|
||
}
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
async function Main() {
|
||
PRINT('backfill_category 空间=' + SpaceId + ' 模型=' + CATEGORY_MODEL + (DRY ? ' [--dry 只算不写]' : ''));
|
||
const Pool = Db.Connect();
|
||
try {
|
||
await EnsureCategoryColumn(Pool);
|
||
const Items = await Db.Many(Pool, 'SELECT ItemId, Category FROM Items ORDER BY ItemId');
|
||
if (!Items.length) {
|
||
PRINT('✗ Items 表是空的:先跑 import_db.js 导入抓包');
|
||
return 1;
|
||
}
|
||
const Cats = await LoadCategories(Pool, Items.map((R) => String(R.ItemId)));
|
||
let Filled = 0;
|
||
let Cleared = 0;
|
||
let Unknown = 0;
|
||
const Pending = [];
|
||
for (const R of Items) {
|
||
const Id = String(R.ItemId);
|
||
const Cur = R.Category === null || R.Category === undefined ? null : String(R.Category);
|
||
const Next = Cats.has(Id) ? Cats.get(Id) : null;
|
||
if (Next === Cur) continue;
|
||
if (Next === null) Cleared++;
|
||
else Filled++;
|
||
if (Cur === null && Next === null) continue;
|
||
Pending.push({ Id, Next });
|
||
}
|
||
for (const R of Items) if (!Cats.has(String(R.ItemId))) Unknown++;
|
||
|
||
if (DRY) {
|
||
PRINT('· [--dry] 将要变更 ' + Pending.length + ' 行(填 ' + Filled + ' / 清 ' + Cleared + ');未写库');
|
||
} else {
|
||
for (const P of Pending) {
|
||
await Pool.query('UPDATE Items SET Category = ? WHERE ItemId = ?', [P.Next, P.Id]);
|
||
}
|
||
PRINT('· 已写库:变更 ' + Pending.length + ' 行(填 ' + Filled + ' / 清 ' + Cleared + ')');
|
||
}
|
||
PRINT('· Items 共 ' + Items.length + ' 行;解出类别 ' + Cats.size + ' 行(本次要变 ' + Pending.length +
|
||
');库里仍无类别 ' + Unknown + ' 行(SetMounted 见 NULL 会跳过同类互斥并打日志)');
|
||
|
||
// 分布:按类别统计(含未解出的 NULL)
|
||
const Dist = await Db.Many(
|
||
Pool,
|
||
'SELECT Category, COUNT(*) AS N FROM Items GROUP BY Category ORDER BY (Category IS NULL), Category',
|
||
);
|
||
PRINT('· Items.Category 分布(全部 992 件):');
|
||
for (const D of Dist) {
|
||
PRINT(' ' + String(D.Category === null ? '<NULL>' : D.Category).padEnd(8) +
|
||
' ' + EnumNames.CategoryLabel(D.Category === null ? null : D.Category) + ' ×' + D.N);
|
||
}
|
||
|
||
// 仓库那 15 件的明细(M8 验收口径:ItemId / Category / Mounted)
|
||
const Depot = await Db.Many(
|
||
Pool,
|
||
'SELECT ItemId, Category, Mounted FROM Items WHERE InDepot = 1 ORDER BY ItemId',
|
||
);
|
||
PRINT('· 仓库物品(InDepot=1)共 ' + Depot.length + ' 件:');
|
||
for (const D of Depot) {
|
||
PRINT(' ' + String(D.ItemId).padEnd(16) + '类别=' + String(D.Category === null ? '<NULL>' : D.Category).padEnd(8) +
|
||
EnumNames.CategoryLabel(D.Category === null ? null : D.Category).padEnd(16) + 'Mounted=' + D.Mounted);
|
||
}
|
||
return 0;
|
||
} finally {
|
||
await Pool.end();
|
||
}
|
||
}
|
||
|
||
// 只有直接运行才执行(被 require 时只导出函数,不连库)
|
||
if (require.main === module) {
|
||
Main()
|
||
.then((Code) => process.exit(Code))
|
||
.catch((Err) => {
|
||
console.error('回填失败:' + (Err && Err.stack ? Err.stack : Err));
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = { CATEGORY_MODEL, LoadCategories, EnsureCategoryColumn };
|