Files
TermiNexus 356647937e feat: M9 枚举线形序数 enum#N 解析成成员名(category 显示 ARMOR/WEAPON/PAINT…)
数据: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>> 的元素级。
2026-09-18 12:59:45 +08:00

162 lines
7.0 KiB
JavaScript
Raw Permalink 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.
// 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 };