feat: M8 查明物品类别来源并实现同类互斥(切武器时旧件自动卸下)
类别来源(完整链):挂载 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。
This commit is contained in:
1 parent
ab1a905d28
commit
4277e0ae08
6 files changed
+249
-20
No files matched your search
@@ -0,0 +1,157 @@
|
|||||||
|
// 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 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) + 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) +
|
||||||
|
'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 };
|
||||||
@@ -115,15 +115,27 @@ CREATE TABLE IF NOT EXISTS Containers (
|
|||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '容器记录原文';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '容器记录原文';
|
||||||
|
|
||||||
-- 物品:仓库 / 市场 / 已挂载 三个旗标(Items 表就是 itemsOnDepot / itemsOnMarket / mountedItems 的落地)
|
-- 物品:仓库 / 市场 / 已挂载 三个旗标(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 (
|
CREATE TABLE IF NOT EXISTS Items (
|
||||||
ItemId BIGINT NOT NULL,
|
ItemId BIGINT NOT NULL,
|
||||||
OwnerUid VARCHAR(24) NULL,
|
OwnerUid VARCHAR(24) NULL,
|
||||||
|
Category VARCHAR(16) NULL COMMENT '装备槽类别(pt 模型 6117306895090150422 的 category,如 enum#1)',
|
||||||
InDepot TINYINT (1) NOT NULL DEFAULT 0,
|
InDepot TINYINT (1) NOT NULL DEFAULT 0,
|
||||||
InMarket TINYINT (1) NOT NULL DEFAULT 0,
|
InMarket TINYINT (1) NOT NULL DEFAULT 0,
|
||||||
Mounted TINYINT (1) NOT NULL DEFAULT 0,
|
Mounted TINYINT (1) NOT NULL DEFAULT 0,
|
||||||
UpdatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
UpdatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (ItemId),
|
PRIMARY KEY (ItemId),
|
||||||
KEY ItemsOwnerIdx (OwnerUid)
|
KEY ItemsOwnerIdx (OwnerUid),
|
||||||
|
KEY ItemsCategoryIdx (Category)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '物品';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '物品';
|
||||||
|
|
||||||
-- 车库挂载位(对应 devicesLoaded 记录里那些 ObjectId)
|
-- 车库挂载位(对应 devicesLoaded 记录里那些 ObjectId)
|
||||||
|
|||||||
@@ -176,23 +176,42 @@ function MountedFlagsSnapshot() {
|
|||||||
return Cache.MountedFlags ? Cache.MountedFlags.slice() : null;
|
return Cache.MountedFlags ? Cache.MountedFlags.slice() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装备落库:把该 ItemId 置 Mounted=1。
|
// 装备落库:**先卸下同类别里的其它件,再置该件为已挂载**(M8 同类互斥)。
|
||||||
// ★ 幂等:UPDATE 带 `AND Mounted <> 1`,重复收到同一件时 affectedRows=0、不产生任何副作用。
|
// ★ 类别来源(M8 查清,逐行证据见 schema.sql 的 Items.Category 注释):物品对象在 pt 里的模型项
|
||||||
// ★ 类别替换(同类互斥)**未实现**:物品的「类别」字段来源还没解出来,不硬猜 ——
|
// modelId = 6117306895090150422(module_85196::Lo),唯一字段 `category`(prop ici_1,线型 enum<ja>),
|
||||||
// 本函数只置该件为已挂载,不清除同类别里原来挂着的那件。
|
// 已由 backfill_category.js 回填进 Items.Category。真实客户端挂载 reducer IB.yr9(module_10032.js:29845-29883)
|
||||||
|
// 走的就是这个类别:先对「入参涉及类别里已挂载的物品」置 mounted=false,再按入参把新物品置 true。
|
||||||
|
// ★ 幂等:两条 UPDATE 都带「只在值确实要变时才动」的条件 —— 重复收到同一件时(该件已 1、同类已 0)
|
||||||
|
// 两条都不改任何行、不产生副作用,日志里能看出「同时卸下 0 件同类 / 该件已是 1」。
|
||||||
|
// ★ Category 为 NULL(该项没解出类别)⇒ 只置位、不做互斥,并打一行日志说明(不硬猜类别)。
|
||||||
async function SetMounted(ItemId) {
|
async function SetMounted(ItemId) {
|
||||||
Require();
|
Require();
|
||||||
const Id = String(ItemId);
|
const Id = String(ItemId);
|
||||||
|
const Row = await Db.One(Pool, 'SELECT Category FROM Items WHERE ItemId = ?', [Id]);
|
||||||
|
const Category = Row && Row.Category !== null && Row.Category !== undefined ? String(Row.Category) : null;
|
||||||
|
let Unmounted = 0;
|
||||||
|
if (Category === null) {
|
||||||
|
Log(' ⚠ 落库:ItemId=' + Id + ' 库里没有类别(Items.Category 为空)→ 只置位,不做同类互斥');
|
||||||
|
} else {
|
||||||
|
// 同类别、当前已挂载、且不是本件 → 全部置 0(条件 Mounted=1 保证第二次调用 affectedRows=0)
|
||||||
|
const [Res2] = await Pool.query(
|
||||||
|
'UPDATE Items SET Mounted = 0 WHERE Category = ? AND Mounted = 1 AND ItemId <> ?',
|
||||||
|
[Category, Id],
|
||||||
|
);
|
||||||
|
Unmounted = Res2.affectedRows;
|
||||||
|
}
|
||||||
const [Res] = await Pool.query('UPDATE Items SET Mounted = 1 WHERE ItemId = ? AND Mounted <> 1', [Id]);
|
const [Res] = await Pool.query('UPDATE Items SET Mounted = 1 WHERE ItemId = ? AND Mounted <> 1', [Id]);
|
||||||
const Changed = Res.affectedRows > 0;
|
const Changed = Res.affectedRows > 0;
|
||||||
if (Changed) {
|
Log(
|
||||||
Log('落库:Items.Mounted=1(ItemId=' + Id + ')已写入');
|
'→ 落库:Items.Mounted=1(ItemId=' + Id + ',类别=' + (Category === null ? '未知' : Category) +
|
||||||
await LoadMountedFlags(); // 刷新快照,下一次进车库的回显覆盖就能看到它
|
',同时卸下 ' + Unmounted + ' 件同类)' + (Changed ? '' : '(该件已是 1,幂等跳过)'),
|
||||||
|
);
|
||||||
|
if (Changed || Unmounted > 0) {
|
||||||
|
await LoadMountedFlags(); // 刷新快照,下一次进车库的回显覆盖就能看到新的集合
|
||||||
Cache.Flags.clear(); // 查库路径(FlaggedItems)的旗标缓存一并失效
|
Cache.Flags.clear(); // 查库路径(FlaggedItems)的旗标缓存一并失效
|
||||||
} else {
|
} else {
|
||||||
Log('落库:Items.Mounted=1(ItemId=' + Id + ')已是 1,幂等跳过(无副作用)');
|
const Row2 = await Db.One(Pool, 'SELECT Mounted FROM Items WHERE ItemId = ?', [Id]);
|
||||||
const Row = await Db.One(Pool, 'SELECT Mounted FROM Items WHERE ItemId = ?', [Id]);
|
if (!Row2) Log(' ⚠ 库里没有 ItemId=' + Id + ' 这一行,落库未生效');
|
||||||
if (!Row) Log(' ⚠ 库里没有 ItemId=' + Id + ' 这一行,落库未生效');
|
|
||||||
}
|
}
|
||||||
return Changed;
|
return Changed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ private val App = FC<Props> {
|
|||||||
for (Id in Session.DepotItems) {
|
for (Id in Session.DepotItems) {
|
||||||
val IsMounted = Mounted.contains(Id)
|
val IsMounted = Mounted.contains(Id)
|
||||||
li {
|
li {
|
||||||
+("$Id " + (if (IsMounted) "已挂载" else "未挂载") + " ")
|
// 类别一并渲染:M8 的同类互斥就是按它判定的(来源见 ClientState.ItemCategory 头注)
|
||||||
|
+("$Id 类别 ${Session.CategoryOf(Id) ?: "?"} " + (if (IsMounted) "已挂载" else "未挂载") + " ")
|
||||||
// 「装备」按钮:点击发 M_.fcn(ItemId);已挂载的置灰(防重复点,落库本身也幂等)
|
// 「装备」按钮:点击发 M_.fcn(ItemId);已挂载的置灰(防重复点,落库本身也幂等)
|
||||||
button {
|
button {
|
||||||
disabled = IsMounted
|
disabled = IsMounted
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ private const val SLOT_MARK_MODIFICATION = "2494628067134376039"
|
|||||||
private const val SLOT_MARK_DEVICES_AVAILABLE = "7391892068905080676"
|
private const val SLOT_MARK_DEVICES_AVAILABLE = "7391892068905080676"
|
||||||
private const val SLOT_MARK_MODIFICATION_COUNT = 7
|
private const val SLOT_MARK_MODIFICATION_COUNT = 7
|
||||||
|
|
||||||
|
// 物品「装备槽类别」所在的模型项 modelId(module_85196::Lo)—— 依据见 ItemCategory 的头注
|
||||||
|
private const val CATEGORY_MODEL = "6117306895090150422"
|
||||||
|
|
||||||
internal class ClientState {
|
internal class ClientState {
|
||||||
|
|
||||||
// ---- 物品三旗标(每个只记最新一次,附来源帧)
|
// ---- 物品三旗标(每个只记最新一次,附来源帧)
|
||||||
@@ -100,11 +103,37 @@ internal class ClientState {
|
|||||||
var MountedItems: List<String> = emptyList(); private set
|
var MountedItems: List<String> = emptyList(); private set
|
||||||
var MountedItemsFrame = -1; private set
|
var MountedItemsFrame = -1; private set
|
||||||
|
|
||||||
// 装备动作的本地乐观更新(真实客户端的 jU reducer 就是这么做的):点一下就把该件标成已挂载,
|
// 装备动作的本地乐观更新(真实客户端的 jU / IB.yr9 reducer 就是这么做的):点一下就把该件标成已挂载,
|
||||||
// 界面立即可见;下一次进车库时服务端下发的 mountedItems(已按库覆盖)会把这里整体替换成权威值。
|
// 并**把同类别里原来挂着的那件标成未挂载**(M8 同类互斥);界面立即可见,不必等服务端回声。
|
||||||
|
// 下一次进车库时服务端下发的 mountedItems(已按库覆盖)会把这里整体替换成权威值。
|
||||||
fun MarkMounted(ItemId: String) {
|
fun MarkMounted(ItemId: String) {
|
||||||
if (MountedItems.contains(ItemId)) return
|
val Cat = ItemCategory(ItemId)
|
||||||
MountedItems = MountedItems + ItemId
|
var Next = MountedItems
|
||||||
|
if (Cat != null) {
|
||||||
|
// 「类别」相同的其它已挂载件先卸下 —— 与 IB.yr9 的顺序一致(先置 false 再置 true)
|
||||||
|
val SameCat = Next.filter { it != ItemId && ItemCategory(it) == Cat }
|
||||||
|
if (SameCat.isNotEmpty()) Next = Next.filter { it !in SameCat }
|
||||||
|
}
|
||||||
|
if (!Next.contains(ItemId)) Next = Next + ItemId
|
||||||
|
if (Next != MountedItems) MountedItems = Next
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 物品类别(M8 同类互斥的依据)
|
||||||
|
|
||||||
|
// 物品的「装备槽类别」 = 该物品对象在 pt 里的模型项 modelId=6117306895090150422(module_85196::Lo)
|
||||||
|
// 的唯一字段 `category`(prop ici_1)。逐行证据与「为什么是这个模型」见 3DTank/tools/server/schema.sql
|
||||||
|
// 的 Items.Category 注释:真实客户端 IB.yr9(module_10032.js:29845-29883)按物品的 qr3_1 做同类互斥,
|
||||||
|
// 而 qr3_1 就是 sr6 里的 n.yr6_1.ici_1(module_10032.js:28655 → 28746),yr6_1 来自
|
||||||
|
// module_18334.js:1760 的 `Gr(n, Z(Ij))` —— 即 pt 里的 modelId=6117306895090150422 那项。
|
||||||
|
// 没有 pt 数据 / 该项没有 category ⇒ 返回 null(此时不做互斥,不硬猜)。
|
||||||
|
fun ItemCategory(ItemId: String): String? {
|
||||||
|
val O = PtByObjectId[ItemId] ?: return null
|
||||||
|
for (M in O.Models) {
|
||||||
|
if (M.ModelId != CATEGORY_MODEL) continue
|
||||||
|
val V = M.Value as? Map<*, *> ?: return null
|
||||||
|
return V["category"]?.toString()
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 挂载位 / 设备
|
// ---- 挂载位 / 设备
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ internal class GarageSession(private val OnChanged: () -> Unit) {
|
|||||||
val Slots: List<DevicesLoadedEntry> get() = StateObj.DevicesRecords
|
val Slots: List<DevicesLoadedEntry> get() = StateObj.DevicesRecords
|
||||||
val DeviceCount: Int get() = StateObj.DeviceCount
|
val DeviceCount: Int get() = StateObj.DeviceCount
|
||||||
|
|
||||||
|
// 某件物品的「装备槽类别」(pt 模型 6117306895090150422 的 category);没有 pt 数据时返回 null
|
||||||
|
fun CategoryOf(ItemId: String): String? = StateObj.ItemCategory(ItemId)
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 连接
|
// ---------------------------------------------------------------- 连接
|
||||||
|
|
||||||
fun Connect() {
|
fun Connect() {
|
||||||
@@ -222,8 +225,10 @@ internal class GarageSession(private val OnChanged: () -> Unit) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- 装备动作(M7)
|
// ---------------------------------------------------------------- 装备动作(M7)
|
||||||
|
|
||||||
// 装备一件仓库物品:发 M_.fcn(ItemId)(靶 = 空间 id,口径同 M4/M5),并按本地乐观语义立刻标成已挂载。
|
// 装备一件仓库物品:发 M_.fcn(ItemId)(靶 = 空间 id,口径同 M4/M5),并按本地乐观语义立刻标成已挂载,
|
||||||
// 落库与持久化由服务端负责;这里只保证「点一下 → 上行入队(下一拍发出)→ 界面立即显示已挂载」。
|
// 同时**把同类别里原来挂着的那件标成未挂载**(M8 同类互斥,与真实客户端 IB.yr9 reducer 同口径)。
|
||||||
|
// 落库与持久化由服务端负责(服务端 SetMounted 也做同类互斥);这里只保证「点一下 → 上行入队(下一拍发出)
|
||||||
|
// → 界面立即正确」。
|
||||||
fun Equip(ItemId: String) {
|
fun Equip(ItemId: String) {
|
||||||
val Id = ItemId.toLongOrNull()
|
val Id = ItemId.toLongOrNull()
|
||||||
if (Id == null) {
|
if (Id == null) {
|
||||||
@@ -231,12 +236,18 @@ internal class GarageSession(private val OnChanged: () -> Unit) {
|
|||||||
OnChanged()
|
OnChanged()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val Already = StateObj.MountedItems.contains(ItemId)
|
val Cat = StateObj.ItemCategory(ItemId)
|
||||||
|
val Before = StateObj.MountedItems
|
||||||
|
val Already = Before.contains(ItemId)
|
||||||
|
// 互斥清单要在 MarkMounted 之前算(那里会改 MountedItems)
|
||||||
|
val SameCat = if (Cat == null) emptyList() else Before.filter { it != ItemId && StateObj.ItemCategory(it) == Cat }
|
||||||
val Bytes = Uplink.BuildMFcn(SpaceId, Id)
|
val Bytes = Uplink.BuildMFcn(SpaceId, Id)
|
||||||
EnqueueManual(Uplink.M_FCN, SpaceId.toString(), Bytes)
|
EnqueueManual(Uplink.M_FCN, SpaceId.toString(), Bytes)
|
||||||
StateObj.MarkMounted(ItemId)
|
StateObj.MarkMounted(ItemId)
|
||||||
AddLog(
|
AddLog(
|
||||||
"⟳ 装备 $ItemId → 已入队 M_.fcn(靶=$SpaceId,${Bytes.size} 字节${if (Already) ",本地已是已挂载" else ",本地乐观标为已挂载"})"
|
"⟳ 装备 $ItemId(类别=${Cat ?: "未知"})→ 已入队 M_.fcn(靶=$SpaceId,${Bytes.size} 字节" +
|
||||||
|
(if (Already) ",本地已是已挂载" else ",本地乐观标为已挂载") +
|
||||||
|
(if (SameCat.isNotEmpty()) ",同时把同类 ${SameCat.joinToString(",")} 标为未挂载" else "")
|
||||||
)
|
)
|
||||||
OnChanged()
|
OnChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user