| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- """一次性: 登录指定角色, 读喝药开关状态, 为 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)
|