版本 1.0.371 开发日志

构建 1.0.371(code 371)· 2026-10-02 13:36:37 · sha256 e6eb5d738c8405e1…

下载:公网 · Tailscale · 局域网

开发日志(本轮摘要)

# 贵霜帝国 · 内网开发版 · 本轮开发记录

生成时间:2026-10-02 13:31:51 | 本包:kushan-1.0.371.apk(1.0.371 / code 371,sha256 e6eb5d738c8405e1…)

## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)

### TASK-B014-BAG-USE  (2026-10-02 13:12:06)

# TASK-B014-BAG-USE ——「背包 / 物品可用」真机验证成立(2026-10-02 · 1.0.370 · 三星 R3CR704Q72V)

## 结论
**「物品可用」成立**:真机点开背包 → 真点「使用」→ 客户端 hp **30 → 42**(+12,恰等于 `heal.food_amount`)、
干馕数量 **2 → 1**。原文(`logcat_b014_bag_use.txt`,同一会话、连续三条读数):
```
KUSHAN_BAG_QA prep drop=20 hp=30 max=50 food_id=-2 name=干馕 qty=2
KUSHAN_BAG_QA t=51 hp=30/50 food_id=-2 name=干馕 qty=2 heal=12
KUSHAN_BAG_QA t=54 hp=42/50 food_id=-2 name=干馕 qty=1 heal=12     ← 真点「使用」之后
```
真点链:轨迹球物理 `(880,2525)` 收到 `KUSHAN_TOUCH (659.9999, 1893.751)` → 菜单自报出现 →
点「🎒 背包」逻辑 `(519.5,631)`=物理 `(693,841)` → 背包面板打开(自报「生命 30/50 | 克什 300.00」)→
点「使用」逻辑 `(519.5,612.5)`=物理 `(693,817)` → hp/数量在下一拍就变了。
截图 `b014_bag_after_use.png`(目视:面板与读数)。**判据=hp 变大 且数量变小(RESULT=PASS)**。

## 为什么直到今天才验成(根因,值得记住)
`GameState.use_item()` 的治疗分支要求 `stats.hp < Params.int_val("combat.player_max_hp")`;
而**服务端战斗是权威账、不回写客户端 `stats.hp`** —— 打完架客户端血量不变,
真机自报也一直是 `生命 51/50`(hp 还大于上限)⇒ 永远落在 `combat.hp_full` 分支、不消耗物品。
即「客户端 hp」与「服务端战斗 hp」是**两本账**。

## 本轮处置=加一个 cfg 门控的自检驱动(不改玩法)
`scripts/bag_qa.gd`(由 `room_view.gd` 按 `SETTINGS_QA` 同款门控挂上,**不写 cfg 零副作用**):
把客户端 hp 压到 `max - N`(默认 20)并每 3 秒打一条 `KUSHAN_BAG_QA`(hp/件 id/名/数量/该件回血)。
⇒ 让「真点使用」这一步能落到治疗分支上**被观察到**,而不是靠改产品数值或改断言。
用法:`kushan.cfg` 里 `BAG_QA=watch`(或 `BAG_QA=watch:35` 调压血量)。

## 顺带记一条工具坑(这次踩了)
`KUSHAN_MENUITEM` 自报里菜单项**带 emoji**(`文=🎒 背包`)⇒ grep 用 `文=背包` 匹配不到(少了一层通配),
害我第一次脚本取到 `(0,0)` 点位。正确写法:`grep '文=.*背包'`(或只 grep 关键词)。

## 仍待人拍板(设计岔路,不在本轮擅自改)
**两本账要不要统一**:① 服务端战斗结果回写客户端 `stats.hp`(客户端血量跟服务端走);
② 明确「客户端 hp」只用作出征前快照 / 展示,治疗类物品的服务端权威路径另立。
两条都属玩法口径,**开发方倾向 ①(一处真源,省掉两套数值)**,但影响面涉及存档与战斗结算,故登记待人否。

### TASK-GUARD-DELETE-TOLERANT  (2026-10-02 12:55:47)

# TASK-GUARD-DELETE-TOLERANT(2026-10-02)——「参数键存在性」新护栏 + 5 个护栏的删件容忍

## 一、补上真实盲区:代码引用的参数键,没人静态扫(→ 新护栏 `tools/verify_param_keys_missing.py`)

**起因(真机逮到)**:1.0.366/367 真机日志有 `ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
`verify_param_wiring` 验的是**运行时行为**「缺参即报错」(H 段),扫不到「代码引用了不存在的键」——
那条代码路径只要测试没跑到,缺参就一直潜伏(本例潜伏到玩家上报差事才可能挂住请求,因为 `HTTPRequest.timeout = 0` 表示**永不超时**)。

**判据(与 `plugins/params.gd` 同源)**:参数存在 ⇔ `data/params.json` 的 `params` 袋里有这个键
(`_raw()` 里 `_effective()` 找不到层 + `meta()` 找不到 schema ⇒ push_error 并返回 0);
世界/帝国/领地/皮/个人各层都是**运行期覆盖**,不是存在性来源。

**做法**:扫 `scripts/`+`plugins/` 全部 `.gd` 里 `Params.<getter|meta|writer>("字面键")`(动态拼接跳过并计数),
与 `data/params.json` 求差。带理由白名单 `tools/param_keys_missing_whitelist.tsv`(键<tab>理由);
`params.json` 读不到 ⇒ `RESULT=SKIP`(不假红)。已转正为 `verify_` 前缀 ⇒ 自动进 `tools/run_verify_all.sh`。

**首跑就抓到 8 个真缺键**(全在 `scripts/voice_input.gd`):`assistant.mic.{sample_rate,channels,max_ms,min_ms,auto_send,enabled,asr_path,asr_timeout_sec}`。
查证=**改名没跟到底**:真名族是 `voice.*`(`voice.sample_rate=16000`/`voice.channels=1`/`voice.max_sec=8`/`voice.enabled=1`/`voice.endpoint=/api/asr`…),
而该文件读的是旧前缀 `assistant.mic.*` ⇒ 读到的全是 0/false/"":**采样率 0、声道 0、录音时长 0、`_mic_btn.visible=false`**(麦克风按钮根本不显示)。

**处置=删件而不是补参**:`scripts/voice_input.gd` 是语音重构前的遗留件 ——
① 全仓**无人实例化它**(只有两处注释提到名字,加 `tools/verify_ui_e21.gd` 的待核清单);
② 它按**旧 4 参**签名调 `MicBridge.bind(sample_rate, channels, max_ms, min_ms)`,而现核心 `scripts/core/mic_bridge.gd` 已是
**6 参** `bind(sample_rate, channels, max_sec, silence_sec, silence_rms, poll_sec)` ⇒ 就算被实例化也会炸;
③ 现役实现是 `scripts/room/voice_bar.gd`(文件头就写着「可调项全在参数 `voice.*`/`tts.*`」)。
⇒ 删 `scripts/voice_input.gd` + `.uid`,并同步 `tools/verify_ui_e21.gd` 的待核清单。删后扫描器 **缺键 0**(扫描键 388→376)。

## 二、删件把 5 个护栏打崩/打红 ⇒ 补「删件容忍」(护栏扩展·技术债)

删掉遗留件后全量回归报 **5 红**,且**不是行号漂移**、是真崩溃(`FileNotFoundError` traceback)/计数失真:
`verify_color`(C.5 站点 + C.6 改前↔改后逐处对账的计数)、`verify_client_i18n`(C1.3 与 C1.7 两处 cache 读件)、
`verify_semantic`(S.2 迁移点计数 + S.5 `_meta.sites` 站点)、`verify_wiring`(W.1 期望调用点)、`verify_fx`(F.5 站点)。

**处置口径**(沿用本仓既有纪律「护栏取不到数据必须报错或跳过,**不能拿空表比绿**」):**跳过 + 出声计数**。
逐处改为「文件不存在 ⇒ 计入 `已删件跳过 N` 并 continue」,行里不留判红、也不静默。
**没有剪基线**:先试过剪 `evidence/TASK-E19/E20/E25/*.tsv` 与 `data/ui_config.json` 的 `_meta.sites`,
副作用是 **21 个键的站点列表被剪空**(会在别的判据上产生新的假红),且让「改前现场」基线失真 ⇒ 全部回滚,改走护栏容忍。
(另记一条坑:往 TSV 里插 `#` 说明行会让只跳空行/表头行的护栏 `int(f[1])` 崩 `IndexError` ⇒ 基线文件保持纯数据,说明写在本文档。)

**复验**(每项单跑绿 + 出声报跳过数):

### TASK-ROOM-POPUP-SHELL  (2026-10-02 12:34:57)

# TASK-ROOM-POPUP-SHELL —「战后房间整块点不动」根因与修复(2026-10-02 · 1.0.367 真机复验通过)

## 现象(真机)
自动战斗打完 → 点「**收 手**」→ **房间整块点不动**:轨迹球(逻辑 `(660,1893.75)`=物理 `(880,2525)`)
的 `input tap` **已被游戏收到**(`KUSHAN_TOUCH` 有记录)却毫无反应;方向键同样无反应、也不换房。
界面看着像正常房间(10-02 起房间卡片 `card_bg_alpha=0`,空弹窗壳**看不见**)⇒ 极易误判为「卡死/注入失效」。

## 定性链(三步,都是实测/读码,不是猜)
1. **命中探针**(cfg `HIT_X=660 HIT_Y=1893`,`room_refresh` 扫描 + `KUSHAN_HIT`):
   - 开战前:命中 2 层,**最上层=`/root/Main/Room_5/Fab (Button)`** ⇒ 轨迹球可点 ✓
   - 开战后:命中 **4 层**,**最上层=`/root/Main/Room_5/@Control@136/@MarginContainer@138/@PanelContainer@139`(全屏 rect=(0,0))** ⇒ 有东西压在轨迹球之上。
2. **读码定位**:战斗面板不是独立场景,而是**挂在房间弹窗的内容区**里
   (`room/room_battle.open_battle_view()` 把 `BattleView` 节点 `add_child` 进 `_interact_box`);
   而 `battle_view._on_close()` 只做 `queue_free()` **关自己、没关弹窗**。
3. **吃点击的是弹窗壳**:`room_popup` 的 `_popup_wrap` 是**全屏 Control**,只要 `_popup.visible`
   就吃掉房间所有点击;空壳+透明卡片让人看不出还开着。

## 修法(两处,均在 `scripts/room/room_popup.gd`)
1. **内容判定不看总数**(2026-10-02):`child_exiting_tree` 是「**即将**离开」时发出(那一刻孩子还在树上),
   而关窗走 `UI.clear_children()`(内部 `queue_free()`=延后释放)⇒ 旧写 `get_child_count() > 0`
   会把「已排队删除」的也算进去 ⇒ 遮罩永久停在 `STOP`。改为 `_has_content_now()`:
   **只数真正在树上、且没被 `is_queued_for_deletion()` 的** ⇒ 与信号/释放时序彻底解耦。
2. **内容空了连壳一起关**:`if not has_content and _popup.visible: _close_popup()`
   (延后一帧判定;正常流程「先 `_clear_interact()` 再同步加内容」同帧仍有内容,不会误关)。

## 真机复验(1.0.367 · 三星 R3CR704Q72V · sha256 `bf6036f9f47b43ea…`)
```
KUSHAN_BATTLE_QA debug_begin ok=true → round=1..4 ok=true → status=win → DONE rounds=4
(点「收 手」)
③ 关键判定:轨迹球能否开出菜单(修前:点了没反应)
  ✅ RESULT=PASS 菜单自报出现(修成立)
  KUSHAN_MENUITEM … 文=🧭 旅伴 / 🎒 背包 / 📜 任务 / 🗺 世界地图 / 🛒 商店 / ⚔ 遭遇战 / 💾 保存进度 / 设置 / 🚪 退出游戏
  KUSHAN_MENUITEM … 文=生命 51/50 | 克什 300.00 …(玩家信息卡也回来了)
④ 真点 [背包] logical=(520,631) phys=(693,841) → 背包面板照常打开(水囊 x1/干馕 x2/使用/丢弃)
```

## 顺带逮到并补登:缺参 `net.quest_timeout_sec`
真机日志里一行 `E godot: ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
查证:`scripts/game_state.gd:220` 在读它,而 `data/params.json` **没有这个键** ⇒ 运行时只报错并拿到 **0**
(`HTTPRequest.timeout = 0` = **永不超时**),差事上报请求可能一直挂着。已按同族风格补登