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>
108 lines
14 KiB
Markdown
108 lines
14 KiB
Markdown
# 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/价格 helper;live `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 格,空槽也有确定位置 | 能解析并保存非空 item,UI 补 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 可以直接修改金币、库存和 stock;live 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`。
|