Files
mtgodot-poc/audit/contracts/npc.dialog-shop.md
T
shenleiandClaude Opus 5 a989b8f19b chore: delete 106 unreachable *_system.gd and their parity tests
These files were loaded only by their own test_*_parity.gd and never by
the runtime (transitively checked, incl. client_phase_integration_system).
Removes 76 manifest implementation/evidence references across 18
contracts; contract docs get a note that conclusions based on them are
void. Live systems kept: chest, consumable, item_attr, metin_socket,
whisper_chat.

Full headless suite: no new failures (7 failing + 5 headless-hanging
render tests are identical before the deletion).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:51:52 +09:00

108 lines
14 KiB
Markdown
Raw 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.
# npc.dialog_shop
## Scope
审计 NPC 点击与脚本对话、普通 NPC 商店、扩展多货架商店、购买/出售、商店错误和结束清理,以及个人商店招牌、点击打开、摆摊编辑和关闭流程。判断标准是当前端是否复现 40250 的服务端货架数据来源、全局槽位索引、NPC/玩家商店分支、动作门控、请求包副作用和失败清理,而不是本地 helper 是否能算出一个价格或库存结果。
## Reference call chain
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
- `RecvShopPacket` 处理 `START``START_EX``END``UPDATE_ITEM``UPDATE_PRICE` 与错误子包;普通商店固定读取 40 个槽位,多货架按 tab 读取每个货架的名称、货币类型和 40 个槽位。
- `CPythonShop::SetItemData(globalIndex, item)` 将增量更新的全局位置映射为 `tabIdx = globalIndex / SHOP_HOST_ITEM_MAX_NUM``slot = globalIndex % SHOP_HOST_ITEM_MAX_NUM`,因此多货架位置不是永远落在 tab 0。
- `SendOnClickPacket` 发送 NPC/个人商店点击请求并追加 sequence;脚本回包通过事件管理器打开对话窗口。
- `RecvShopSignPacket` 将玩家商店招牌绑定到已有角色实体;个人商店广告牌点击后由 `uiprivateshopbuilder.py` 发送 `SendOnClickPacket(vid)`
- `UserInterface/PythonNetworkStreamPhaseGameItem.cpp`
- `SendShopEndPacket``SendShopBuyPacket``SendShopSellPacket``SendShopSellPacketNew` 都先经过 `__CanActMainInstance()`,再写 `CG_SHOP` 子头、槽位/数量并 `SendSequence()`
- 购买包使用数量 1 加显示位置;出售分旧版单槽包和新版槽位+数量包。结果、金币、背包和售罄状态由服务器回包更新。
- `UserInterface/PythonShop.cpp``Client/Eternexus/root/uishop.py`
- `CPythonShop` 保存所有 tab 和全部 40 个位置,普通、NPC 扩展、多货架和个人商店由窗口状态区分。
- `ShopDialog.Open(vid)` 根据 NPC、他人玩家和主角自己的商店分别显示买卖、货架或关闭控件;自己商店不能走买卖分支。
- 左键购买有确认,右键/取消选中可直接购买;出售检查反出售标记、贵重物品确认并发送服务端请求。
- `Client/Eternexus/root/uiprivateshopbuilder.py``PythonShop.cpp``Packet.h`
- 摆摊最多 39 个商品,商品按展示位置排序,源物品位置和价格由服务端消费;标题有长度限制,禁止状态和 anti flag 在提交前检查。
- `BuildPrivateShop` 发送 `CG_MYSHOP`,关闭自己的摊位也使用相应的个人商店协议语义。
- `PythonNonPlayer.cpp``PythonPlayerInput.cpp``PythonPlayerInputMouse.cpp`
- NPC/商店点击由实体类型、可点击状态和距离/输入流程进入 `SendOnClickPacket`;个人商店广告牌是可点击的独立入口,不等同于普通 PC 攻击或选中。
## Current call chain
- `project/net_play.gd::_on_pick` 对 NPC/WARP 走 `click_npc`,有距离预约和同 VID 节流;但没有对带 `shop_sign` 的 PC 建立 40250 广告牌点击分支,当前 `project/net_world.gd::_refresh_shop_sign` 只创建显示用 `Label3D`
- `project/ui/quest_dialog.gd` 和网络事件可以承接脚本/对话显示;`extension/src/net/m2_client.cpp::click_npc``classic_session.cpp::send_click_npc` 能发送基础点击包。
- `extension/src/net/entity_store.cpp` 已解析普通 `START`、扩展 `START_EX``END`、更新和常见错误,并保存商店 VID、tab 和 item;但 `mut_shop_update_item` 拒绝 `pos >= 40`,且多 tab 时只更新 tab 0,未复现 40250 的全局位置映射。
- `project/ui/shop_ui.gd` 有买/卖模式、40 格、多货架、确认框、距离关闭和右键购买;但是 `_drop_to_sell` 在购买模式下仍继续执行出售请求,当前 `project/complex_item_drop_test.gd` 已复现该失败。
- `ShopUI.refresh` 没有根据 `shop.vid` 区分主角自己的个人商店、他人个人商店和 NPC 商店,因此当前买/卖按钮状态不等价于 `ShopDialog.Open`
- `project/npc_shop_system.gd` 使用硬编码 NPC 库存与本地 `buy_item`/`sell_item` 直接修改金币和背包;它不是 `CPythonShop` 的服务器货架回包链,且目前主要由孤立 parity 测试使用。
- `project/private_shop_system.gd` 同样是本地 stock/价格 helperlive `project/ui/private_shop_ui.gd` 能提交 `M2Client.open_private_shop`,但未在 live 提交前统一调用死亡、骑乘、战斗状态和 50200 摆摊道具校验。
- `M2Client::shop_buy``shop_sell` 只做 in-game、u8 和数量边界,缺少 40250 的 `__CanActMainInstance`、实际物品/anti flag/商店状态门控;`open_private_shop` 只验证字段 wire bounds 与最多 39 条。
- `classic_session.cpp` 的买卖、关闭和个人商店包布局及 sequence 基础存在;现有 native 测试验证布局,但没有真实 `GC_SHOP` 多阶段包和失败回包驱动的 UI 状态测试。
## Branch matrix
| 场景 | 40250 | 当前端 | 结论 |
|---|---|---|---|
| NPC 点击/脚本对话 | 实体输入、距离和 `SendOnClickPacket` 后由服务器脚本回包驱动 | NPC 点击、距离预约、节流和基础点击存在 | PARTIAL:事件/脚本结果与异常清理缺真实包覆盖 |
| 普通商店 START | 服务器提供 VID 和固定 40 格,空槽也有确定位置 | 能解析并保存非空 itemUI 补 40 格 | PARTIAL:空槽原始状态和真实 UI 回包顺序未证明 |
| START_EX 多货架 | 每 tab 40 格,更新位置是全局 `tab*40+slot` | 初始 tab 能显示;增量更新限制 `<40` 且只更新 tab 0 | GAP |
| 商品更新/售罄/错误 | `SetItemData`、价格更新、错误事件和 UI 刷新 | 基础错误字符串和刷新存在 | PARTIAL:更新索引和回包失败后 UI/金币/库存清理不足 |
| 买入 | `__CanActMainInstance`→CG_SHOP/BUY→sequence→服务器结果 | UI/native 能发买包并有确认 | PARTIAL:缺动作门控、pos 全范围和服务器结果闭环 |
| 卖出/批量卖出 | anti-sell/贵重确认后旧 SELL 或 SELL2,服务端扣物品加金币 | UI 有部分 anti flag/确认;native 只做 u8 边界 | PARTIAL:购买模式拖入仍可出售,实际库存/服务端回包未闭环 |
| 自己的个人商店 | 隐藏买卖,只显示关闭;个人商店协议关闭自己的摊位 | ShopUI 主要按普通买卖模式显示,未按 shop VID 统一区分 | GAP |
| 他人个人商店/招牌 | 广告牌点击发送 `SendOnClickPacket(vid)` 后打开玩家商店 | 招牌目前只有 `Label3D`,PC 点击未路由到点击商店 | GAP |
| 摆摊编辑/提交 | 39 条、标题/禁止状态/anti flag/源物品校验后 `CG_MYSHOP` | UI/Native 字段和数量限制存在 | PARTIAL:live 状态校验和服务端消费/拒绝反馈不足 |
| 关闭/离开/断线 | `END` 清理访问商店;个人商店关闭遵循独立协议语义 | 基础 close 和本地 UI 关闭存在 | PARTIAL:访问商店、自己的摆摊和断线/乱序清理未统一验证 |
## Implementation-equivalence matrix
| 项目 | 状态 | 证据/限制 |
|---|---|---|
| Preconditions | PARTIAL | NPC 距离和部分 UI anti flag 存在;native 买卖缺 `__CanActMainInstance`、真实物品/商店状态和动作门控,摆摊 live 缺死亡/骑乘/战斗及 bundle 校验 |
| Branch structure | PARTIAL | 普通商店、多货架、购买确认和脚本对话分支存在;自己的个人商店、玩家招牌点击和购买模式拖放分支不等价 |
| Algorithms/formulas | PARTIAL | 货架槽位和价格由协议保存;硬编码 NPC stock、本地买卖 helper 和本地出售价格不是 40250 的服务端权威实现 |
| State transition order | GAP | 参考是请求→服务端判定→item/gold/shop update/error→UI 刷新;本地 helper 可直接改库存/金币,且 UI 购买模式错误放行出售 |
| Constants/units | PARTIAL | 40 格、最多 3 tab、最多 39 条、标题/招牌和字段 wire bounds 基本存在;全局多 tab 更新范围未实现,价格/数量服务端约束未闭合 |
| Timing/event sources | PARTIAL | 点击节流、距离关闭、确认框和 shop signal 存在;真实 START→UPDATE→ERROR/END 时序、招牌点击和重复请求未覆盖 |
| Resource/data sources | GAP | 40250 商品库存来自服务器 `CPythonShop`/item data;当前 `NpcShopSystem` 额外维护硬编码本地库存,和 live authority 不是同一数据源 |
| Protocol side effects | PARTIAL | CG_SHOP、CG_ON_CLICK、CG_MYSHOP 的基础布局和 sequence 存在;动作门控、个人招牌点击和完整错误/结果副作用不完整 |
| Interruption/failure/cleanup | GAP | 购买模式拖放泄漏到出售;多 tab 更新可能落错/丢失;断线、拒绝、售罄、自己摊位关闭和未找到实体的招牌清理没有统一端到端回滚 |
## Regression evidence
- `project/test_npc_shop_parity.gd`26/26 通过,但主要验证硬编码 `NpcShopSystem` 的本地库存、金币和背包变化,不能作为 40250 服务器商店等价证明。
- `project/test_npc_shop_flow.gd`:NPC 点击距离/节流、UI 多 tab/右键购买和距离关闭通过;使用 fake client,未覆盖真实 GC shop 包。
- `project/test_npc_dialog_shop_ui_parity.gd`:窗口尺寸、格子、按钮、脚本对话与 fake client 通过;没有多 tab 全局更新和 PC 招牌点击。
- `project/private_shop_ui_test.gd``project/test_private_shop_bundle_parity.gd``project/shop_rules_test.gd``project/p8_test.gd`:本地摆摊编辑与规则通过;未证明 live UI 的状态门控、`CG_MYSHOP` 服务端拒绝/关闭和招牌打开。
- `project/complex_item_drop_test.gd`:当前有 1 项失败,购买模式仍允许把背包物品拖到商店出售目标;这是可复现的实现回归。
- `build/extension/net_classic_session_test``build/extension/net_entity_test`:基础 packet/store 行为通过,但没有 `START_EX` 位置 40..119 的更新回归,也没有完整错误/结束/重新打开序列。
## Deep audit round 2026-09-21
### Fresh regression results
- `godot --headless --path project --script test_npc_shop_parity.gd`:退出码 0,26/26 通过;覆盖的是 `NpcShopSystem` 的本地库存、价格、金币和背包 helper。
- `godot --headless --path project --script test_npc_shop_flow.gd`:退出码 0,38 项通过;覆盖 NPC 距离/预约/节流、fake shop 多货架 UI、右键购买和距离关闭,没有真实 `GC_SHOP START_EX/UPDATE/ERROR/END` 包。
- `godot --headless --path project --script test_npc_dialog_shop_ui_parity.gd`:退出码 0;窗口、40 格、买卖按钮、脚本对话和 QuestCurtain 通过,仍使用 fake client。
- `godot --headless --path project --script private_shop_ui_test.gd``test_private_shop_bundle_parity.gd``shop_rules_test.gd``p8_test.gd`:均退出码 0;本地摆摊规则、bundle、价格和社交窗口通过,不等价于真实 `CG_MYSHOP`/个人商店招牌/失败回包。
- `godot --headless --path project --script complex_item_drop_test.gd`:退出码 1;购买模式下卖出目标仍错误接受背包物品拖放。
- `build/extension/net_classic_session_test``build/extension/net_entity_test`:退出码 0;基础包/store 行为通过,但没有覆盖多货架全局位置 40..119 的增量更新。
### Static parity findings
- 40250 `START_EX` 的每个 tab 固定 40 格,`UPDATE_ITEM` 使用全局位置 `tab * 40 + slot`;当前 `EntityStore::mut_shop_update_item` 仍要求 `pos < 40`,并且只更新 `m_shop_tabs[0]`,所以 tab 1+ 的增量回包会被丢失或落错货架。
- `ShopUI::_drop_to_sell` 没有以 `_mode == 2` 作为硬门,购买模式拖拽仍能调用 `_request_sell`;这与 `ShopDialog` 的买/卖分支不一致,也是本轮失败测试的直接原因。
- 40250 `ShopDialog.Open(vid)` 按 NPC、他人个人商店和自己的个人商店分支显示;当前 `ShopUI` 主要按通用买卖窗口处理,未以 `shop_vid` 和主角 VID 做统一分支,自己的摊位与他人摊位行为仍可能混用。
- `shop_sign` 在 native/entity store 中有保存和显示数据,但 `NetPlay::_on_pick` 没有完整复刻 `SendOnClickPacket(shop_vid)` 的个人商店广告牌分支;显示招牌不等于可打开玩家商店。
- `NpcShopSystem``PrivateShopSystem` 等本地 helper 可以直接修改金币、库存和 stocklive UI 的 native 请求路径存在,但两套权威来源并存,测试通过不能证明服务端回包覆盖本地结果。
- `M2Client::shop_buy/shop_sell/shop_close/open_private_shop` 的基础 wire bounds 存在,但没有统一 `__CanActMainInstance`、商店状态、实际 item/anti-flag 和重复/失败请求门控;`SendSequence`/错误/售罄/重开顺序也未由真实 fixture 验证。
### Round conclusion
本轮仍为 `PARTIAL`:NPC 点击、普通商店 UI、基础买卖/摆摊包和局部规则测试可运行,但多货架增量索引、购买模式拖放门、个人商店招牌/自有摊位分支和服务器权威回包仍未统一;本轮未修改实现代码。
## Status
`PARTIAL`。NPC 点击、基础对话承接、普通/扩展商店协议骨架、购买出售 UI 和个人商店字段已经存在,但当前端仍有明确的多 tab 更新索引错误、购买模式错误放行出售、个人商店招牌不可点击打开、自己商店状态不分支、native 动作门控缺失以及本地硬编码库存/金币旁路。已完成审计并记录,未修改实现代码。
### 2026-09-22 死代码删除
以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`npc_shop_system.gd`, `private_shop_system.gd`, `test_npc_shop_parity.gd`, `test_private_shop_bundle_parity.gd`