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
12b2ea8648
commit
63be75040b
6 files changed
+249
-20
No files matched your search
@@ -70,7 +70,8 @@ private val App = FC<Props> {
|
||||
for (Id in Session.DepotItems) {
|
||||
val IsMounted = Mounted.contains(Id)
|
||||
li {
|
||||
+("$Id " + (if (IsMounted) "已挂载" else "未挂载") + " ")
|
||||
// 类别一并渲染:M8 的同类互斥就是按它判定的(来源见 ClientState.ItemCategory 头注)
|
||||
+("$Id 类别 ${Session.CategoryOf(Id) ?: "?"} " + (if (IsMounted) "已挂载" else "未挂载") + " ")
|
||||
// 「装备」按钮:点击发 M_.fcn(ItemId);已挂载的置灰(防重复点,落库本身也幂等)
|
||||
button {
|
||||
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_MODIFICATION_COUNT = 7
|
||||
|
||||
// 物品「装备槽类别」所在的模型项 modelId(module_85196::Lo)—— 依据见 ItemCategory 的头注
|
||||
private const val CATEGORY_MODEL = "6117306895090150422"
|
||||
|
||||
internal class ClientState {
|
||||
|
||||
// ---- 物品三旗标(每个只记最新一次,附来源帧)
|
||||
@@ -100,11 +103,37 @@ internal class ClientState {
|
||||
var MountedItems: List<String> = emptyList(); private set
|
||||
var MountedItemsFrame = -1; private set
|
||||
|
||||
// 装备动作的本地乐观更新(真实客户端的 jU reducer 就是这么做的):点一下就把该件标成已挂载,
|
||||
// 界面立即可见;下一次进车库时服务端下发的 mountedItems(已按库覆盖)会把这里整体替换成权威值。
|
||||
// 装备动作的本地乐观更新(真实客户端的 jU / IB.yr9 reducer 就是这么做的):点一下就把该件标成已挂载,
|
||||
// 并**把同类别里原来挂着的那件标成未挂载**(M8 同类互斥);界面立即可见,不必等服务端回声。
|
||||
// 下一次进车库时服务端下发的 mountedItems(已按库覆盖)会把这里整体替换成权威值。
|
||||
fun MarkMounted(ItemId: String) {
|
||||
if (MountedItems.contains(ItemId)) return
|
||||
MountedItems = MountedItems + ItemId
|
||||
val Cat = ItemCategory(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 DeviceCount: Int get() = StateObj.DeviceCount
|
||||
|
||||
// 某件物品的「装备槽类别」(pt 模型 6117306895090150422 的 category);没有 pt 数据时返回 null
|
||||
fun CategoryOf(ItemId: String): String? = StateObj.ItemCategory(ItemId)
|
||||
|
||||
// ---------------------------------------------------------------- 连接
|
||||
|
||||
fun Connect() {
|
||||
@@ -222,8 +225,10 @@ internal class GarageSession(private val OnChanged: () -> Unit) {
|
||||
|
||||
// ---------------------------------------------------------------- 装备动作(M7)
|
||||
|
||||
// 装备一件仓库物品:发 M_.fcn(ItemId)(靶 = 空间 id,口径同 M4/M5),并按本地乐观语义立刻标成已挂载。
|
||||
// 落库与持久化由服务端负责;这里只保证「点一下 → 上行入队(下一拍发出)→ 界面立即显示已挂载」。
|
||||
// 装备一件仓库物品:发 M_.fcn(ItemId)(靶 = 空间 id,口径同 M4/M5),并按本地乐观语义立刻标成已挂载,
|
||||
// 同时**把同类别里原来挂着的那件标成未挂载**(M8 同类互斥,与真实客户端 IB.yr9 reducer 同口径)。
|
||||
// 落库与持久化由服务端负责(服务端 SetMounted 也做同类互斥);这里只保证「点一下 → 上行入队(下一拍发出)
|
||||
// → 界面立即正确」。
|
||||
fun Equip(ItemId: String) {
|
||||
val Id = ItemId.toLongOrNull()
|
||||
if (Id == null) {
|
||||
@@ -231,12 +236,18 @@ internal class GarageSession(private val OnChanged: () -> Unit) {
|
||||
OnChanged()
|
||||
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)
|
||||
EnqueueManual(Uplink.M_FCN, SpaceId.toString(), Bytes)
|
||||
StateObj.MarkMounted(ItemId)
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user