Просмотр исходного кода

fix(heal): 喝药读态取最新面板+最多toggle一次 — 修复旧面板缓存导致的反复误切(实机验证 off→on 一次过)

wuzilong 1 месяц назад
Родитель
Сommit
ae2a815b32

+ 58 - 56
pipeline_bot/src/pbot/game/heal.py

@@ -1,14 +1,11 @@
-"""喝药保护开关 — CS3199 UI 动作链(backup/cq2_pure_enter.enable_auto_heal 迁移)。
-
-实测流程:
-    CS3199 {"index":109,"act":"@openmianban,23","subid":1}   打开喝药面板
-    CS3199 {"index":303,"act":"@chiyao_kaiqi","subid":1}     切换开关(toggle)
-回包 SC3213 藏在 SC-1(zlib) 批量帧子帧里, `id=yd_cy1` 控件的
-`but_1.png`=on / `but_0.png`=off。开关是 toggle: 先读状态, 不符再切。
-
-状态读取双通道(2026-09-28 修): 心跳线程 pump 与本函数抢 socket,
-裸 recv 常饿死(历史全轮 None) — 优先从 world.ui_panels(pump 拆包
-归属地)读, 裸收帧作补充。
+"""喝药保护开关 — CS3199 UI 动作。
+
+2026-10-01 定案(hoe_success 抓包 + 三轮探针):
+    CS3199 {"subid":1,"act":"@chiyao_kaiqi","index":303}   toggle(翻转)
+    → 服务器 130ms 内推 SC-1 子帧面板: id=yd_cy1 的 nimg=but_1(on)/but_0(off)
+    真客户端无 openmianban 前置帧(该前置已删 — 曾致 toggle 静默)。
+toggle 非置位: 已 on 则零帧直返; off 才切一次; 读态取到达序号最新的
+面板(init 期旧 off 缓存会掩盖 toggle 后新值, 2026-10-01 误切实证)。
 """
 
 import json
@@ -19,16 +16,12 @@ from typing import Final
 
 from pbot.transport.gateway import GatewayConnection
 from pbot.world.mirror import WorldMirror
+from pbot.world.update import WorldUpdater
 
 _LOG = logging.getLogger("pbot.game")
 
-_OPEN_PANEL_INDEX: Final[int] = 109
 _TOGGLE_INDEX: Final[int] = 303
-# run16 定案: 固定 tail=900-903 在登录流程 >60s 时(端口重试/心跳推进)
-# 变成回退号, 服务器静默丢弃全部 3199 → toggle 永不生效 → 恒 off。
-# tail 纪律(README): 全链唯一递增 — 改走主链自动递增(tail=None)。
 _STATE_WAIT_S: Final[float] = 2.0
-_PANEL_KEY: Final[int] = 23  # openmianban,23 → ui_panels 键
 
 _BUT_STATE_PAT: Final[re.Pattern[bytes]] = re.compile(rb"id=yd_cy1[^}]*?but_[01]\.png", re.DOTALL)
 
@@ -41,32 +34,39 @@ def _send_action(conn: GatewayConnection, sid: int, act: str, index: int) -> Non
 
 
 def _state_from_panels(world: WorldMirror) -> str | None:
-    """从 world.ui_panels(SC3213 → pump 拆包归属)读 yd_cy1 状态。"""
-    for content in world.ui_panels.values():
+    """读 yd_cy1 状态 — 取到达序号最新的面板。
+
+    旧缓存会掩盖 toggle 后新值(2026-10-01 T1 三轮误切实证:
+    init 期 off 面板排在 dict 前面恒被读中)。
+    """
+    best_key: int | None = None
+    best_seq = -1
+    for key, content in world.ui_panels.items():
         if "yd_cy1" not in content:
             continue
-        return "on" if "but_1.png" in content else "off"
-    return None
+        seq = world.panel_seq.get(key, -1)
+        if seq > best_seq:
+            best_seq, best_key = seq, key
+    if best_key is None:
+        return None
+    content = world.ui_panels[best_key]
+    return "on" if "but_1.png" in content else "off"
 
 
-def _read_heal_state(conn: GatewayConnection, world: WorldMirror) -> str | None:
-    """收帧窗口内解析喝药开关状态(但_1=on / but_0=off); 未捕获返回 None。"""
-    state = _state_from_panels(world)
-    if state is not None:
-        return state
-    import zlib
+def _read_heal_state(conn: GatewayConnection, world: WorldMirror,
+                     refresh: bool = False) -> str | None:
+    """读喝药开关状态。
+
+    refresh=True 时先收 0.5s 新帧再读(toggle 后轮询用, 绕开可能滞后的
+    panels 缓存); False 直接读缓存。
+    """
+    if refresh:
+        from pbot.game.session import apply_batch
+        apply_batch(world, _UPDATER, conn.recv_frames(0.5))
+    return _state_from_panels(world)
 
-    for f in conn.recv_frames(_STATE_WAIT_S):
-        if f.msg_id == 60000 or f.body[:2] != b"x\x9c":
-            continue
-        try:
-            dec = zlib.decompress(f.body)
-        except zlib.error:
-            continue
-        m = _BUT_STATE_PAT.search(dec)
-        if m:
-            state = "on" if b"but_1" in m.group(0) else "off"
-    return state
+
+_UPDATER = WorldUpdater()
 
 
 def enable_auto_heal(conn: GatewayConnection, sid: int,
@@ -74,33 +74,35 @@ def enable_auto_heal(conn: GatewayConnection, sid: int,
                      ) -> str | None:
     """开启/关闭喝药保护。返回最终状态 'on'/'off'/None(未捕获)。
 
-    run14 实证: state 读不到时盲 toggle 是赌博(奇偶不定, 曾把 on 切成
-    off 后洞穴无喝药死亡)。修正: 每轮 toggle 后必须**读到**状态且符合
-    期望才收工; 读不到就继续读(面板 SC3213 迟到), 3 轮读不到则最后
-    toggle 一次并再验证 — 终态以读到的为准, 未读到返回 None。
+    @chiyao_kaiqi 是 toggle(翻转非置位) — 盲切奇偶不定(run14 曾把 on
+    切 off 致死)。纪律: 最多切一次。
+    1) 读态(最新面板优先)已符 → 直接返回, 不发任何帧;
+    2) 不符 → 发一次 toggle → 8s 窗口轮询读态(toggle 后服务器 130ms
+       即推新面板, 但窗口放大防抖), 读到期望 → 返回;
+    3) 读到反值(切过头) → 再切一次纠正(此为已知翻转, 非盲切), 仍不达
+       → 返回实读值由调用方决策。
     world 非 None 时走 ui_panels 通道(免疫心跳线程抢帧)。
     """
     probe = WorldMirror() if world is None else world
     want = "on" if want_on else "off"
-    _send_action(conn, sid, "@openmianban,23", _OPEN_PANEL_INDEX)
-    time.sleep(1.0)
-    state: str | None = None
-    # run16c 定案: init 序列必发一次 chiyao_kaiqi(toggle), 上一局遗留 on
-    # 会被切成 off; 且 toggle 后 SC3213 面板刷新可达 2s — 每 toggle 后
-    # 在窗口内轮询重读(0.25s 一次), 读到期望立即收工
-    for i in range(3):
-        state = _read_heal_state(conn, probe)
-        if state == want:
-            _LOG.info("喝药开关已为 %s(读态验证)", state)
-            return state
+    state = _read_heal_state(conn, probe, refresh=True)
+    if state == want:
+        _LOG.info("喝药开关已为 %s(读态验证, 零帧)", state)
+        return state
+    for attempt in range(2):
         _send_action(conn, sid, "@chiyao_kaiqi", _TOGGLE_INDEX)
-        deadline = time.time() + (2.0 if i == 0 else 1.5)
+        deadline = time.time() + 8.0
         while time.time() < deadline:
             time.sleep(0.25)
-            after = _read_heal_state(conn, probe)
+            after = _read_heal_state(conn, probe, refresh=True)
             if after == want:
-                _LOG.info("喝药开关切至 %s(toggle后读态验证)", after)
+                _LOG.info("喝药开关切至 %s(toggle x%d 后读态验证)",
+                          after, attempt + 1)
                 return after
-        state = _read_heal_state(conn, probe)
+        state = _read_heal_state(conn, probe, refresh=True)
+        if state is not None and state != want:
+            _LOG.info("toggle x%d 读到反值 %s — 再纠正一次",
+                      attempt + 1, state)
+            continue
     _LOG.warning("喝药开关终态未验证(最后读到 %s, 期望 %s)", state, want)
     return state if state == want else (state or None)

+ 3 - 0
pipeline_bot/src/pbot/world/mirror.py

@@ -58,6 +58,9 @@ class WorldMirror:
     find_target: FindTarget | None = None  # 最近一次 SC20102 任务寻路目标
     npc_dialog: dict[str, object] | None = None  # 最近一次 SC643 对话体(含 sMsg 按钮)
     ui_panels: dict[int, str] = field(default_factory=dict)  # SC3213 index → content
+    # 面板到达序号(index → seq, 单调递增) — 同键多次推送时辨新旧
+    panel_seq: dict[int, int] = field(default_factory=dict)
+    panel_seq_next: int = 0
     events: list[ActorEvent] = field(default_factory=list)
     kills: int = 0
     frame_counts: dict[int, int] = field(default_factory=dict)  # msgId -> 收到数(观测)

+ 4 - 1
pipeline_bot/src/pbot/world/update.py

@@ -236,7 +236,10 @@ class WorldUpdater:
         # cap_gold t=345 实锤 — 喝药面板(yd_cy1 but_0/1)与引导面板载体
         j = body_json(f)
         content = str(j.get("content", ""))
-        world.ui_panels[int(_as_int(j.get("index", 0)))] = content
+        key = int(_as_int(j.get("index", 0)))
+        world.ui_panels[key] = content
+        world.panel_seq[key] = world.panel_seq_next
+        world.panel_seq_next += 1
         world.record("ui_panel", str(j.get("index", "?")))
         if "知晓" in content or "yindao" in content:
             _LOG.info("[SC3213] 引导面板 index=%s: %s",