"""一次性: 登录指定角色, 读喝药开关状态, 为 off 时发 kaiqi 翻开, 再读确认。 协议定案(2026-09-26 v2, cap_round2 实证): - @openmianban,23 → 服务器回 SC3213(encode6 面板): id=yd_cy1 按钮 but_1.png=on / but_0.png=off - @chiyao_kaiqi = toggle, 每发一次翻转一次, 面板即时刷新 - 故必须先读状态, off 才发, 发后再读确认 用法: uv run python scripts/enable_heal.py -u <账号> -p <密码> --zone <区> --name <角色> """ from __future__ import annotations import argparse import json import logging import re import struct import sys import time import zlib import requests from pbot.codec.bitbuf import decode6 from pbot.config import Account from pbot.errors import PbotError from pbot.game.session import enter_world from pbot.login.box_client import enter_game_client, login from pbot.transport.gateway import GatewayConnection, login_gate_with_retry from pbot.types import ZoneId _LOG = logging.getLogger("enable_heal") _PORTS: tuple[int, ...] = (8021, 8061, 8001, 8041) _BUT_PAT: re.Pattern[bytes] = re.compile(rb"id=yd_cy1[^>]*?(but_[01])\.png", re.DOTALL) def _send_3199(conn: GatewayConnection, sid: int, body: dict[str, object], tail: int) -> None: """发一条 CS3199 UI 动作。""" wire = json.dumps(body, separators=(",", ":")).encode() _ = conn.send_frame(3199, 0, 0, 0, sid, wire, tail=tail) def _read_heal_state(conn: GatewayConnection, wait_s: float) -> str | None: """收帧并解析 SC3213 面板的 yd_cy1 状态('on'/'off'/None)。 面板藏在 SC-1(zlib) 子帧 SC3213, body 为 encode6 编码的 JSON。 """ deadline = time.time() + wait_s seen: list[int] = [] while time.time() < deadline: for f in conn.recv_frames(max(0.1, deadline - time.time())): seen.append(f.msg_id) if f.msg_id != -1 or f.body[:2] != b"x\x9c": continue try: raw = zlib.decompress(f.body) except zlib.error: continue pos = 0 sub_msgs: list[int] = [] while len(raw) - pos >= 48: try: h = struct.unpack("<9i", decode6(raw[pos:pos + 48])) except Exception: # noqa: BLE001 子帧错位即停 break blen = h[7] if blen < 0 or blen > 30000 or len(raw) - pos < 48 + blen: break sub_msgs.append(h[1]) if h[1] == 3213: _LOG.info("发现 SC3213 子帧!") txt = decode6(raw[pos + 48:pos + 48 + blen]) m = _BUT_PAT.search(txt) if m: _LOG.info("SC3213 面板到达!") return "on" if b"but_1" in m.group(0) else "off" else: _LOG.info("SC3213 无 yd_cy1 匹配: %s", decode6(raw[pos + 48:pos + 48 + blen])[:200]) pos += 48 + blen _LOG.info("SC-1 子帧: %s", sub_msgs) _LOG.info("状态窗口内收帧: %s", seen[-30:]) return None def ensure_heal_on(conn: GatewayConnection, sid: int) -> str | None: """kaiqi(toggle) → 读面板确认 → 未 on 再 toggle, 最多 3 轮。 实测定案: openmianban 不触发面板下发(cap_round2 + 实测双重确认), 但每次 kaiqi 后服务器必推 SC3213 刷新面板(68114/68120/68125 实证)。 故以 kaiqi 驱动面板, 读到状态即可判定并决定是否再 toggle。 """ state: str | None = None for i, tail in enumerate((901, 902, 903)): _send_3199(conn, sid, {"index": 303, "act": "@chiyao_kaiqi", "subid": 1}, tail=tail) _LOG.info("已发 kaiqi #%d(tail=%d), 等面板...", i + 1, tail) state = _read_heal_state(conn, 10.0) _LOG.info("kaiqi #%d 后状态: %s", i + 1, state) if state == "on": return "on" if state is None: _LOG.warning("面板未到, 不再盲发(防翻转)") return None return state def main() -> int: """登录目标角色, 确保自动喝药为 on, 退出码 0=on。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", stream=sys.stdout) ap = argparse.ArgumentParser() ap.add_argument("-u", "--user", required=True) ap.add_argument("-p", "--password", required=True) ap.add_argument("--zone", type=int, required=True) ap.add_argument("--name", required=True) a = ap.parse_args() http = requests.Session() http.trust_env = False http.verify = False jwt, uid = login(Account(username=a.user, password=a.password)) _LOG.info("登录成功 uid=%d", uid) _ = enter_game_client(http, jwt, ZoneId(a.zone)) _key32, sid, real_name, ticket_key, game_port = login_gate_with_retry( ZoneId(a.zone), jwt, uid, a.name, _PORTS) _LOG.info("票据 OK 角色=%s 游戏端口=%d", real_name, game_port) stop: list[bool] = [False] conn, _world, _updater = enter_world(ZoneId(a.zone), real_name, ticket_key, sid, game_port, uid, stop=stop) try: # ★ 时序门槛(cap_round2 全部 24 次 openmianban 实证: 均在进图 54~60s 后, # 更早发服务器直接忽略不回面板) — 等满 62s 再发 import time as _t _LOG.info("等待 62s(面板请求时序门槛: 进图 54~60s 内服务器忽略)...") _t.sleep(62) final = ensure_heal_on(conn, sid) if final == "on": _LOG.info("[✓] 自动喝药已确认开启") return 0 _LOG.error("[✗] 未能确认开启(最终状态: %s)", final) return 1 finally: stop[0] = True conn.close() if __name__ == "__main__": try: sys.exit(main()) except PbotError as e: _LOG.error("失败: %s", e.detail) sys.exit(1)