| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745 |
- """run_juece32_stand_by_mine_npc.py — 裁决32区 创角 → 等待地图寻路跑 → 矿场总兵旁站立。
- 流程(全部复用仓库已实测链路):
- [1] 盒子 HTTP 登录(login: RSA+极验滑块自动破解)
- [2] EnterGameClient 选区上报(裁决32区 serverid=14594070, 2026-10-01 14:00 开服)
- [3] 登录服全链(2001→…→525) 自动创角: 名字随机纯中文(pipeline.naming),
- 重名/复用不符则换名重试(最多 3 次)
- [4] 进世界(enter_world: CS20+17帧初始化+心跳线程) — 新角色出生=等待地图
- [5] 落地静置 5s(服务器建视野, SC10 推 NPC)
- [5.5] 四处跑: BFS 可达域随机取 3 个远点, AutoMoveBehavior 逐点寻路跑
- (CS5013 START 注册 + 0139.map 权威墙 A* + CS3013 跑腿链 + FINISH)
- [5.8] 开启喝药保护: 进图 ≥62s 才受理(2026-09-26 定案时序门槛),
- enable_auto_heal 读态验证 + T1 同款 3 轮重试
- [6] 寻路跑向【矿场总兵】: 优先 world.npcs 实测坐标, 兜底坐标 (18,24)
- [6.5] T04 领锄头: CS1010{10041,43} → CS1011 @func38_get_hzc 三态判定
- (入包 Index:50001 → CS1003 穿锄; "5分钟后"弹窗 → @func_no 等 65s 重试;
- "活动已结束" → 跳过挖矿) — 开区 5-20 分钟窗口内有效
- [6.8] T05 挖矿: BFS 找 可走且邻墙 的面壁格 → CS3014{999,dir,src,dst}
- @0.68s 循环 → 产物三形态统计(自身播报/金币/入包) → 3013 走步停止
- [7] 到达 NPC 2 格内 → 原地站立(仅心跳+收帧), 默认 180s
- [8] 关闭连接, 进程退出
- 用法:
- uv run python scripts/run_juece32_stand_by_mine_npc.py \
- [-u z1098210167] [-p yang535006] [--zone 14605092] \
- [--name 某中文] [--hold-seconds 180] [--mine-seconds 600]
- """
- from __future__ import annotations
- import argparse
- import json
- import logging
- import random
- import re
- import sys
- import time
- import zlib
- from collections import deque
- from collections.abc import Iterator
- from pathlib import Path
- from typing import Final, Literal
- import requests
- from pbot.bot.automove import AutoMoveBehavior
- from pbot.bot.pathfind import load_walls_from_map
- from pbot.bot.runner import pump, run
- from pbot.codec.bitbuf import decode6
- from pbot.codec.frame import Frame, FrameSplitter
- from pbot.config import Account
- from pbot.errors import PbotError
- from pbot.game.equip import take_on
- from pbot.game.heal import enable_auto_heal
- from pbot.game.session import enter_world
- from pbot.login.box_client import enter_game_client, login
- from pbot.login.zone_ports import fetch_zone_ports
- from pbot.transport.gateway import GatewayConnection, login_gate_with_retry
- from pbot.types import ZoneId
- from pbot.world.mirror import WorldMirror
- from pbot.world.update import WorldUpdater
- ROOT: Final[Path] = Path(__file__).resolve().parents[1]
- if str(ROOT) not in sys.path:
- sys.path.insert(0, str(ROOT))
- from jiuyangfugu176.naming import random_cn_name # noqa: E402
- _LOG = logging.getLogger("juece32_stand")
- ZONE_DEFAULT: Final[int] = 14605092 # 裁决33区(2026-10-01 16:00 开服, 16:05 窗口)
- PORTS: Final[tuple[int, ...]] = (8081, 8041, 8021, 8061, 8001, 8063, 8043, 8003)
- # 2026-10-01 定案: 各区登录服端口由 modlist 下发的 serverlist/110585 提供
- # (加密格式未逆向), 不同物理机端口不同(32区=8041, 33区=8081)。脚本用
- # probe_login_port() 运行时自发现: 发 2001→104→100 探针, 收到 SC520 即真端口。
- WAIT_MAP: Final[str] = "等待地图"
- NPC_NAME: Final[str] = "矿场总兵"
- NPC_FALLBACK_POS: Final[tuple[int, int]] = (18, 24) # 用户指定坐标(2026-10-01)
- MAP_0139: Final[Path] = ROOT / "assets" / "maps" / "0139.map"
- MINE_W, MINE_H = 46, 50 # 0139.map 头部尺寸
- # T04 领锄头(2026-10-01 抓包定案, protocol_bot/docs/探索总结-20261001)
- HOE_NPC_CFG: Final[str] = "10041" # 矿场总兵 cfgID(CS1010 UserID 字段)
- HOE_NPC_MENU_INDEX: Final[int] = 43
- HOE_ACT: Final[str] = "@func38_get_hzc"
- HOE_ITEM_INDEX: Final[int] = 50001 # 鹤嘴锄 Index(入包帧判定)
- HOE_SWING_INTERVAL_S: Final[float] = 0.68 # HitTime(227 帧实测中位)
- HOE_HOE_MAX_TRIES: Final[int] = 10
- HOE_WAIT5_RETRY_S: Final[float] = 65.0
- # 全区播报: ★玩家<[名]>挖矿获得:<物品> — 2026-10-03 定案:
- # 只展示自己角色的产物(他人名过滤), 播报名=物品名来源(自身入包多为基础材料)
- # 物品名双格式(49/51区实证): 裸名"聚灵珠(小)" 或 富文本"<金刚石/FCOLOR=249>"
- _LOOT_RE: Final[re.Pattern[str]] = re.compile(
- r"玩家<\[([^\]]{1,12})\][^>]*>挖矿获得[::]\s*(?:<([^<>\",,/]{1,20})"
- r"[^<>]*>|([^<\",,]{1,20}))")
- _GOLD_RE: Final[re.Pattern[str]] = re.compile(r'"ID":1,"Value":(\d+)')
- DIR_VECS: Final[tuple[tuple[int, int], ...]] = (
- (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1))
- _ENTER_RETRIES: Final[int] = 4
- _ENTER_RETRY_SLEEP_S: Final[float] = 12.0
- _NAME_ATTEMPTS: Final[int] = 3
- _HEAL_ROUNDS: Final[int] = 3 # T1 同款: 三轮重试
- _HEAL_GATE_S: Final[float] = 62.0 # 进图后面板请求受理门槛(2026-09-26 定案)
- _SETTLE_S: Final[float] = 5.0
- _MOVE_BUDGET_S: Final[float] = 120.0 # 单段寻路时长上限
- _ROAM_MIN_DIST: Final[int] = 10 # 漫游远点最小 BFS 距离
- _ARRIVE_DIST: Final[int] = 2
- def setup_logging() -> None:
- """全量 INFO 日志到 stdout。"""
- logging.basicConfig(
- level=logging.INFO,
- format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
- stream=sys.stdout,
- )
- def banner(msg: str, *args: object) -> None:
- """阶段分隔条日志。"""
- _LOG.info("======== %s ========", msg % args if args else msg)
- def _is_pure_cn(name: str) -> bool:
- """全中文检查(每个字符都在 CJK 基本区)。"""
- return bool(name) and all("\u4e00" <= c <= "\u9fff" for c in name)
- def reachable_cells(start: tuple[int, int],
- walls: set[tuple[int, int]]) -> dict[tuple[int, int], int]:
- """0139 权威墙 BFS: 起点 reachable 域 + 每格 BFS 距离。"""
- dist = {start: 0}
- dq = deque([start])
- while dq:
- x, y = dq.popleft()
- for dx, dy in DIR_VECS:
- nxt = (x + dx, y + dy)
- if nxt in dist or not (0 <= nxt[0] < MINE_W and 0 <= nxt[1] < MINE_H):
- continue
- if nxt in walls:
- continue
- dist[nxt] = dist[(x, y)] + 1
- dq.append(nxt)
- return dist
- def pick_roam_waypoints(start: tuple[int, int], walls: set[tuple[int, int]],
- count: int) -> list[tuple[int, int]]:
- """可达域内随机取 count 个 BFS 距离 ≥ _ROAM_MIN_DIST 的远点。"""
- dist = reachable_cells(start, walls)
- far = [c for c, d in dist.items() if d >= _ROAM_MIN_DIST]
- random.shuffle(far)
- return far[:count]
- def _iter_batch_texts(frames: list[Frame]) -> Iterator[str]:
- """SC-1 zlib 批量帧 → 子帧流文本(decode6 后), 供产物/弹窗扫描。"""
- for f in frames:
- if f.msg_id != -1:
- continue
- try:
- raw = zlib.decompress(f.body)
- except zlib.error:
- continue
- for sub in FrameSplitter().feed(raw):
- yield from _sub_texts(sub)
- def _sub_texts(f: Frame) -> Iterator[str]:
- if f.msg_id == -1:
- try:
- yield zlib.decompress(f.body).decode("utf-8", "replace")
- except (zlib.error, UnicodeDecodeError):
- pass
- return
- try:
- yield decode6(f.body).decode("utf-8", "replace")
- except Exception: # noqa: BLE001 - decode6 对任意输入可能抛多种异常
- pass
- def claim_hoe(conn: GatewayConnection, sid: int, world: WorldMirror,
- updater: WorldUpdater,
- role: str) -> tuple[str, int]:
- """T04 领锄头一轮: 点击 NPC → @func38_get_hzc → 扫三态回执。
- 返回 (state, makeindex): got=入包(带物品实例号) / wait5=开区<5min
- (点 Btn2 关闭) / over=活动已结束 / fail=无定论信号。
- makeindex 是穿装备帧 CS1003.recog 的必需参数(TakeOnEquipCommand 定案)。
- """
- world.npc_dialog = None
- body1010 = json.dumps({"UserID": HOE_NPC_CFG, "index": HOE_NPC_MENU_INDEX},
- separators=(",", ":")).encode()
- _ = conn.send_frame(1010, 0, 0, 0, sid, body1010)
- time.sleep(1.2)
- pre = pump(conn, world, updater, 0.6)
- _LOG.info("[T04·诊断] CS1010 后收帧 %s", [f.msg_id for f in pre] or "零")
- if world.npc_dialog is not None:
- _LOG.info("[T04·诊断] SC643 sMsg<<%s>>",
- str(world.npc_dialog.get("sMsg", ""))[:160].replace("\n", ""))
- body1011 = json.dumps({"UserID": "999999999", "Act": HOE_ACT,
- "index": 999999999},
- separators=(",", ":")).encode()
- _ = conn.send_frame(1011, 0, 0, 0, sid, body1011)
- deadline = time.time() + 4.0
- seen_ids: set[int] = set()
- while time.time() < deadline:
- frames = pump(conn, world, updater, 0.3)
- for fr in frames:
- if fr.msg_id not in (60000, 5037):
- seen_ids.add(fr.msg_id)
- for text in _iter_batch_texts(frames):
- if f'"Index":{HOE_ITEM_INDEX}' in text and "makeindex" in text:
- mk = 0
- try:
- j = json.loads(text)
- mk = int(j.get("makeindex", 0))
- except (ValueError, TypeError):
- pass
- return "got", mk
- if "5分钟后" in text:
- _send_qfunc_no(conn, sid)
- return "wait5", 0
- if "活动已结束" in text or "挖矿活动已结束" in text:
- return "over", 0
- dialog = world.npc_dialog
- if dialog is not None:
- smsg = str(dialog.get("sMsg", ""))
- if "5分钟后" in smsg:
- _send_qfunc_no(conn, sid)
- return "wait5", 0
- if "活动已结束" in smsg:
- return "over", 0
- if seen_ids:
- _LOG.info("[T04·诊断] 本轮非心跳帧: %s", sorted(seen_ids))
- return "fail", 0
- def _send_qfunc_no(conn: GatewayConnection, sid: int) -> None:
- """弹窗 Btn2(@func_no) — 关闭"5分钟后"弹窗, 绝不点 Btn1(首充)。"""
- body = json.dumps({"UserID": "999999999", "Act": "@func_no",
- "index": 999999999},
- separators=(",", ":")).encode()
- _ = conn.send_frame(1011, 0, 0, 0, sid, body)
- def _send_exit_panel(conn: GatewayConnection, sid: int) -> None:
- """CS1011 @exit — 关闭领取面板(真客户端 t=143.54 实证, 穿锄后必发)。
- 面板未关时后续挖矿/移动指令疑似被服务器吞 — j39 穿锄无效的主疑点。
- """
- body = json.dumps({"UserID": "999999999", "Act": "@exit",
- "index": 999999999},
- separators=(",", ":")).encode()
- _ = conn.send_frame(1011, 0, 0, 0, sid, body)
- def _wear_hoe(conn: GatewayConnection, sid: int,
- world: WorldMirror, updater: WorldUpdater, # noqa: ARG001 — 保持调用方签名
- makeindex: int) -> bool:
- """穿锄头(2026-10-02 pre=makeindex 定案, 见 equip.py)。
- 领锄入包后 2s 内直接 CS1003: pre=makeindex(关键!), recog=1, body=鹤嘴锄。
- 历史版本缺 pre → 服务器按 mk 查无物品 → 恒 616(已排除 30+ 项后的真根因)。
- """
- ok = take_on(conn, sid, "鹤嘴锄", makeindex)
- _LOG.info("[穿锄] CS1003 pre=%d → %s", makeindex,
- "SC615+203 ✓已穿上" if ok else "失败(616/超时)")
- if ok:
- _LOG.info("[穿锄✓] 关闭领取面板(@exit) — 真客户端穿锄后必发")
- _send_exit_panel(conn, sid)
- return ok
- def claim_hoe_with_retry(conn: GatewayConnection, sid: int,
- world: WorldMirror, updater: WorldUpdater,
- role: str) -> tuple[str, int]:
- """T04 主循环: wait5 → 等 65s 重试; got → 立刻穿锄。返回 (状态, makeindex)。"""
- for attempt in range(1, HOE_HOE_MAX_TRIES + 1):
- state, mk = claim_hoe(conn, sid, world, updater, role)
- _LOG.info("[领锄头] 第%d轮 状态=%s makeindex=%d", attempt, state, mk)
- if state == "got":
- _LOG.info("[领锄头✓] 鹤嘴锄已入包(Index:50001, makeindex=%d)", mk)
- if not _wear_hoe(conn, sid, world, updater, mk):
- _LOG.warning("[穿锄✗] 失败 — 挖矿继续尝试(可能已穿着)")
- return "got", mk
- if state == "over":
- _LOG.warning("[领锄头✗] 挖矿活动已结束(20 分钟窗口过)")
- return "over", 0
- if state == "wait5":
- _LOG.info("[领锄头] 开区<5min — 等 %.0fs 重试", HOE_WAIT5_RETRY_S)
- deadline = time.time() + HOE_WAIT5_RETRY_S
- while time.time() < deadline:
- _ = pump(conn, world, updater, 0.5)
- continue
- time.sleep(5.0)
- return "fail", 0
- def _boundary_walls(walls: set[tuple[int, int]]) -> set[tuple[int, int]]:
- """与地图边缘连通的墙(四面矿墙带) — 从图边洪泛, 排除内部孤墙。"""
- outer: set[tuple[int, int]] = set()
- dq = deque()
- for x in range(MINE_W):
- for y in (0, MINE_H - 1):
- c = (x, y)
- if c in walls and c not in outer:
- outer.add(c)
- dq.append(c)
- for y in range(MINE_H):
- for x in (0, MINE_W - 1):
- c = (x, y)
- if c in walls and c not in outer:
- outer.add(c)
- dq.append(c)
- while dq:
- x, y = dq.popleft()
- for dx, dy in DIR_VECS:
- nxt = (x + dx, y + dy)
- if nxt in walls and nxt not in outer:
- outer.add(nxt)
- dq.append(nxt)
- return outer
- def find_mining_spot(pos: tuple[int, int],
- walls: set[tuple[int, int]],
- outer_walls: set[tuple[int, int]] | None = None,
- ) -> tuple[tuple[int, int], int] | None:
- """挖矿站位: 四面边界墙带随机选一面(用户定案 2026-10-03: 随机分散,
- 多号不挤同面墙), 在该墙上就近取「可走 且 8邻含边界墙」格。
- 等待图 = 中央菱形开阔区 + 四周矿墙带(边界墙, 与图边洪泛连通)。
- NPC 附近的内部孤墙((17,21)/(18,21) 等)不是矿, 挖不出 — j37 实证。
- """
- ow = outer_walls if outer_walls is not None else _boundary_walls(walls)
- reach = reachable_cells(pos, walls)
- # 全部可面壁格按所在墙分组(上y小/下y大/左x小/右x大 — 以格到图中心
- # 的主轴判墙), 随机挑一组后再就近取格; 判不出主轴的格归入全部池
- cx, cy = MINE_W // 2, MINE_H // 2
- groups: dict[str, list[tuple[tuple[int, int], int]]] = {}
- for cell, depth in sorted(reach.items(), key=lambda kv: kv[1]):
- ds = [i for i, (dx, dy) in enumerate(DIR_VECS)
- if (cell[0] + dx, cell[1] + dy) in ow]
- if not ds:
- continue
- dx0, dy0 = cell[0] - cx, cell[1] - cy
- side = ("n" if dy0 < 0 else "s") if abs(dy0) >= abs(dx0) \
- else ("w" if dx0 < 0 else "e")
- groups.setdefault(side, []).append((cell, ds[0]))
- if not groups:
- return None
- side = random.choice(sorted(groups)) # noqa: S311 — 四面墙随机
- return groups[side][0]
- def mine_loop(conn: GatewayConnection, sid: int, world: WorldMirror,
- updater: WorldUpdater, role: str, seconds: float,
- spot: tuple[int, int], face_dir: int) -> dict[str, int]:
- """T05 挖矿循环: CS3014{999,dir,src,dst} @0.68s + 产物三形态统计。
- src 每锄实时取 world.me(j37 实证: 规划位与服务器实际位差 1 格时,
- 每锄被 SC28 锚点纠正=全部无效); dest=面前墙格。停止后发一帧 3013
- 走步(客户端语义: 走动即停挖)。
- """
- stats = {"swings": 0, "loot_broadcasts": 0, "gold": 0, "items": 0}
- deadline = time.time() + seconds
- over = False
- raw_dump_t = 0.0 # 观察回退节流: 格式漂移时 5s 一条保现场, 不刷屏
- while time.time() < deadline and not over and not world.me.is_dead:
- me = world.me
- src_x, src_y = me.x, me.y
- dst_x = src_x + DIR_VECS[face_dir][0]
- dst_y = src_y + DIR_VECS[face_dir][1]
- payload = json.dumps({"skillID": 999, "dir": face_dir,
- "destX": dst_x, "destY": dst_y,
- "srcX": src_x, "srcY": src_y},
- separators=(",", ":")).encode()
- _ = conn.send_frame(3014, 0, 0, 0, 0, payload)
- stats["swings"] += 1
- frames = pump(conn, world, updater, HOE_SWING_INTERVAL_S)
- for text in _iter_batch_texts(frames):
- if "挖矿获得" in text:
- m = _LOOT_RE.search(text)
- if m is None:
- # 观察回退(50区实证需要): 正则失配 = 播报格式漂移,
- # 静默会掩盖格式变化 — 节流打原始前 100 字保现场
- now = time.time()
- if now - raw_dump_t > 5.0:
- raw_dump_t = now
- _LOG.warning("🎁[格式漂移] %s", text[:100])
- elif m.group(1) == role:
- stats["loot_broadcasts"] += 1
- # 双格式: 富文本包裹取组2, 裸名取组3
- _LOG.info("🎁 [产物] %s", m.group(2) or m.group(3))
- if "活动已结束" in text:
- over = True
- m_gold = _GOLD_RE.search(text)
- if m_gold:
- stats["gold"] = max(stats["gold"], int(m_gold.group(1)))
- if '"Index":' in text and "makeindex" in text:
- stats["items"] += 1
- m_name = re.search(r'"Name":"([^"]+)"', text)
- if m_name:
- _LOG.info("📦 [入包] %s", m_name.group(1))
- else:
- now = time.time()
- if now - raw_dump_t > 5.0:
- raw_dump_t = now
- _LOG.warning("📦[键名漂移] %s", text[:100])
- me = world.me
- _LOG.info("[挖矿结束] %s — 挥锄 %d 产物播报 %d 金币 %d 入包 %d 终位(%d,%d)",
- "活动结束" if over else "时长到", stats["swings"],
- stats["loot_broadcasts"], stats["gold"], stats["items"],
- me.x, me.y)
- _ = conn.send_frame(3013, 0, face_dir, (me.y << 16) | me.x, sid,
- WAIT_MAP.encode(),
- pre=((me.y + DIR_VECS[face_dir][1]) << 16)
- | (me.x + DIR_VECS[face_dir][0]))
- return stats
- def _dir8(x: int, y: int, tx: int, ty: int) -> int:
- """8 向主方向(截断 dx/dy 到 -1..1)。"""
- dx = max(-1, min(1, tx - x))
- dy = max(-1, min(1, ty - y))
- return next((i for i, (ox, oy) in enumerate(DIR_VECS) if ox == dx and oy == dy), 4)
- def _face_wall_dir(pos: tuple[int, int],
- walls: set[tuple[int, int]]) -> int | None:
- """当前位置的 8 向里找面向墙/图边界的方向; 无返回 None。"""
- for d_idx, (dx, dy) in enumerate(DIR_VECS):
- nx, ny = pos[0] + dx, pos[1] + dy
- if not (0 <= nx < MINE_W and 0 <= ny < MINE_H) or (nx, ny) in walls:
- return d_idx
- return None
- def _step_to_mine_cell(conn: GatewayConnection, sid: int,
- world: WorldMirror, updater: WorldUpdater,
- spot: tuple[int, int]) -> None:
- """AutoMove 容差 2 格, 挖矿须精确踩格 — 差 1-2 格时单步 CS3011 补位。
- j37 实证: 站(18,22)按规划位(18,21)发帧, 每锄被 SC28 纠正=全无效。
- """
- for _ in range(4):
- me = world.me
- dx, dy = spot[0] - me.x, spot[1] - me.y
- if dx == 0 and dy == 0:
- return
- d = _dir8(me.x, me.y, spot[0], spot[1])
- from pbot.game.messages import protocol_map_name
- mn = protocol_map_name(me.map_id, me.map_name)
- _ = conn.send_frame(3011, 0, d, (me.y << 16) | me.x, sid, mn.encode(),
- pre=((me.y + DIR_VECS[d][1]) << 16)
- | (me.x + DIR_VECS[d][0]))
- deadline = time.time() + 1.5
- while time.time() < deadline:
- _ = pump(conn, world, updater, 0.3)
- if (world.me.x, world.me.y) == spot:
- return
- _LOG.warning("[T05] 补位 4 次未踩到 %s(在 %d,%d)", spot,
- world.me.x, world.me.y)
- def parse_args() -> argparse.Namespace:
- """命令行。"""
- ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
- _ = ap.add_argument("-u", "--user", default="longwu988")
- _ = ap.add_argument("-p", "--password", default="Longwu1988")
- _ = ap.add_argument("--zone", type=int, default=ZONE_DEFAULT,
- help="区服 serverid(默认 裁决33区=14605092, 16:00 开)")
- _ = ap.add_argument("--name", default="",
- help="角色名(默认随机纯中文); 必须全中文")
- _ = ap.add_argument("--hold-seconds", type=float, default=180.0,
- help="NPC 旁静止时长(秒, 默认 180)")
- _ = ap.add_argument("--mine-seconds", type=float, default=600.0,
- help="挖矿时长(秒, 默认 600)")
- _ = ap.add_argument("--skip-hoe", action="store_true",
- help="跳过领锄头(已领过), 直接挖矿")
- _ = ap.add_argument("--skip-heal", action="store_true",
- help="跳过喝药保护(挖矿无需)")
- _ = ap.add_argument("--roam-points", type=int, default=3,
- help="四处跑的随机远点数(默认 3)")
- _ = ap.add_argument("--login-port", type=int, default=0)
- return ap.parse_args()
- def _create_role_with_retry(zone: ZoneId, jwt: str, uid: int, want_name: str,
- ports: tuple[int, ...]) -> tuple[int, str, str, int]:
- """创角(重名换名重试; 槽满回退复用已有角色)。
- 返回 (sid, 角色名, 票据, 游戏端口)。新区(空角色表)必走创角;
- 旧区槽满(522)时 login_gate 回退 role_list[-1] — 接受复用并警告。
- """
- name = want_name
- for attempt in range(1, _NAME_ATTEMPTS + 1):
- if not _is_pure_cn(name):
- msg = f"角色名必须全中文: {name!r}"
- raise RuntimeError(msg)
- _, sid, real, ticket, gport = login_gate_with_retry(
- zone, jwt, uid, name, ports)
- if real == name:
- return sid, real, ticket, gport
- _LOG.warning("[创角] 第%d次 请求=%s 实际=%s(重名/槽满复用旧角)",
- attempt, name, real)
- if attempt == _NAME_ATTEMPTS:
- _LOG.warning("[创角] 重试耗尽 — 接受复用角色 %s", real)
- return sid, real, ticket, gport
- name = random_cn_name()
- raise RuntimeError("unreachable")
- def probe_login_port(zone: ZoneId, jwt: str, uid: int,
- ports: tuple[int, ...]) -> int:
- """端口自发现: 逐端口发 2001→104→100 探针, 收到 SC520 = 真端口。
- 新物理机的登录服端口与旧机不同(33区 8081 vs 32区 8041), 错端口的
- 服务握手应答 529/530 但 CS100 点名该区时裸断连(2026-10-01 定案)。
- 每端口 ~10s 上限; 返回 0 = 全部失败。
- """
- from pbot.codec.bitbuf import decode6, encode6
- from pbot.transport.gateway import CHANNEL, GAME_ID, GatewayConnection
- for port in ports:
- c: GatewayConnection | None = None
- try:
- c = GatewayConnection(zone, port)
- body2001 = f"{uid}/".encode() + encode6(jwt.encode()) + b"/" \
- + encode6(CHANNEL.encode()) + b"/1/0"
- _ = c.send_frame(2001, random.randint(1, 2**30), # noqa: S311
- int(zone), port, 0, body2001, tail=0)
- key32 = None
- for f in c.recv_frames(6):
- if f.msg_id == 529:
- key32 = decode6(f.body).split(b"/")[0]
- if not key32:
- _LOG.info("[探针] %d: 无529 — 跳过", port)
- continue
- _ = c.send_frame(104, 0, 0, 0, 0, encode6(key32), tail=1)
- sid = 0
- for f in c.recv_frames(4):
- if f.msg_id == 530:
- sid = f.pre
- body100 = f"{uid}/{sid}/{int(zone)}/{CHANNEL}/{GAME_ID}/".encode() + key32
- _ = c.send_frame(100, random.randint(1, 2**30), # noqa: S311
- int(zone), port, sid, body100, tail=2)
- got = [f.msg_id for f in c.recv_frames(4)]
- if 520 in got:
- _LOG.info("[探针✓] 端口 %d 收到 SC520 — 就用这个", port)
- return port
- _LOG.info("[探针] %d: CS100 应答 %s — 非本区端口", port, got or "断连")
- except Exception as e: # noqa: BLE001 - 探针逐端口容错
- _LOG.info("[探针] %d: %s — 跳过", port, str(e)[:60])
- finally:
- if c is not None:
- c.close()
- return 0
- def main() -> int:
- """登录 → 创角 → 进世界 → 四处寻路跑 → 跑到矿场总兵 → 静止 → 退出。"""
- a = parse_args()
- setup_logging()
- hold = max(0.0, a.hold_seconds)
- banner("[0] 启动: 区=%d 角色=%s 静止=%.0fs 漫游点=%d",
- a.zone, a.name or "(随机中文)", hold, a.roam_points)
- http = requests.Session()
- http.trust_env = False
- http.verify = False
- banner("[1] 盒子登录: %s", a.user)
- jwt, uid = login(Account(username=a.user, password=a.password))
- _LOG.info("[✓] uid=%d", uid)
- banner("[2] EnterGameClient zone=%d", a.zone)
- for attempt in range(_ENTER_RETRIES):
- try:
- _ = enter_game_client(http, jwt, ZoneId(a.zone))
- _LOG.info("[✓] 上报成功")
- break
- except Exception as e: # 选区上报偶发失败, 重试兜底
- if attempt == _ENTER_RETRIES - 1:
- raise
- _LOG.warning("[!] 第%d次失败(%s), %.0fs 后重试",
- attempt + 1, e, _ENTER_RETRY_SLEEP_S)
- time.sleep(_ENTER_RETRY_SLEEP_S)
- banner("[3] 登录服端口获取")
- ports = (a.login_port,) if a.login_port else PORTS
- if not a.login_port:
- try:
- info = fetch_zone_ports().get(a.zone)
- if info and info["port"]:
- _LOG.info("[✓] serverlist 解密: %s 端口=%d state=%d",
- info["name"], info["port"], info["state"])
- ports = (info["port"],)
- except Exception as e: # noqa: BLE001 - serverlist 失败走探针兜底
- _LOG.warning("[!] serverlist 端口获取失败(%s) — 探针兜底", e)
- if len(ports) > 1:
- found = probe_login_port(ZoneId(a.zone), jwt, uid, ports)
- if found:
- ports = (found,)
- else:
- _LOG.warning("[!] 端口探测全部失败 — 用 PORTS 原序硬试")
- banner("[3.5] 登录服创角(纯中文名)")
- sid, real_name, ticket_key, game_port = _create_role_with_retry(
- ZoneId(a.zone), jwt, uid, a.name or random_cn_name(), ports)
- _LOG.info("[✓] sid=%d 角色=%s 游戏端口=%d", sid, real_name, game_port)
- banner("[4] 进游戏")
- stop: list[bool] = [False]
- conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
- sid, game_port, uid, stop=stop,
- platform=1, with_guide=False)
- _LOG.info("[✓] 出生 map=%s @(%d,%d)", world.me.map_name, world.me.x,
- world.me.y)
- try:
- if world.me.map_name != WAIT_MAP:
- _LOG.warning("[!] 出生不在%s(当前 %s) — 仍按流程站立",
- WAIT_MAP, world.me.map_name)
- born_t = time.time()
- banner("[5] 落地静置 %.0fs(建视野)", _SETTLE_S)
- deadline = time.time() + _SETTLE_S
- while time.time() < deadline:
- _ = pump(conn, world, updater, 0.5)
- npc_pos = NPC_FALLBACK_POS
- for npc in world.npcs.values():
- if NPC_NAME in npc.name:
- npc_pos = (npc.x, npc.y)
- _LOG.info("[NPC] 视野内 %s aid=%s @(%d,%d)",
- npc.name, npc.aid, npc.x, npc.y)
- break
- else:
- _LOG.info("[NPC] 视野未见 %s — 用坐标 %s", NPC_NAME, NPC_FALLBACK_POS)
- banner("[5.5] 四处跑: %d 个随机远点(AutoMove 寻路)", a.roam_points)
- walls = load_walls_from_map(str(MAP_0139))
- for i, wp in enumerate(pick_roam_waypoints(
- (world.me.x, world.me.y), walls, a.roam_points), 1):
- _LOG.info("[漫游] 第%d点 → (%d,%d)", i, wp[0], wp[1])
- bh = AutoMoveBehavior(sid=sid, dest_x=wp[0], dest_y=wp[1])
- _ = run(conn, world, updater, bh, duration_s=_MOVE_BUDGET_S)
- if world.me.map_name != WAIT_MAP:
- _LOG.warning("[漫游✗] 换图 %s — 跳过后续漫游点",
- world.me.map_name)
- break
- _ = pump(conn, world, updater, 0.5)
- _LOG.info("[漫游结束] map=%s @(%d,%d) 距 %s %d 格",
- world.me.map_name, world.me.x, world.me.y, NPC_NAME,
- max(abs(npc_pos[0] - world.me.x),
- abs(npc_pos[1] - world.me.y)))
- # 挖矿窗口(开区5-20min)时间宝贵: 领锄/挖矿优先, 喝药挪到站立期
- banner("[6] 寻路跑向 %s(%d,%d)", NPC_NAME, *npc_pos)
- bh = AutoMoveBehavior(sid=sid, dest_x=npc_pos[0], dest_y=npc_pos[1])
- r = run(conn, world, updater, bh, duration_s=_MOVE_BUDGET_S)
- dist = max(abs(npc_pos[0] - world.me.x), abs(npc_pos[1] - world.me.y))
- _LOG.info("[寻路结束] result=%s 位置=(%d,%d) 距 %s %d 格",
- r.value, world.me.x, world.me.y, NPC_NAME, dist)
- if dist > _ARRIVE_DIST:
- _LOG.warning("[!] 未精确到位(距 %d 格) — 就地站立", dist)
- # ---- T04 领锄头 ----
- hoe, hoe_mk = "got", 0 if a.skip_hoe else (None, 0)
- if a.skip_hoe:
- # 跳过领取: 从背包定位锄头真实 makeindex(equip.py pre 定案必需)
- from pbot.game.equip import bag_items, find_makeindex
- bag = bag_items(conn, sid)
- hoe_mk = find_makeindex(bag, HOE_ITEM_INDEX)
- _LOG.info("[T04] skip-hoe 模式 — 背包锄头 makeindex=%d", hoe_mk)
- if not hoe_mk:
- _LOG.warning("[T04] 背包无锄头(可能已穿着) — 继续挖矿流程")
- else:
- hoe, hoe_mk = claim_hoe_with_retry(conn, sid, world, updater,
- real_name)
- if hoe != "got":
- _LOG.warning("[T04] 结果=%s — 跳过挖矿, 进入站立", hoe)
- else:
- # ---- T05 挖矿 ----
- banner("[6.8] T05 挖矿 %.0fs", a.mine_seconds)
- spot_res = find_mining_spot((world.me.x, world.me.y), walls)
- if spot_res is None:
- _LOG.warning("[T05] 可达域无面壁格 — 跳过挖矿")
- else:
- spot, face_dir = spot_res
- if spot != (world.me.x, world.me.y):
- _LOG.info("[T05] 跑到挖矿位 %s 面向 dir=%d", spot, face_dir)
- bh = AutoMoveBehavior(sid=sid, dest_x=spot[0], dest_y=spot[1])
- _ = run(conn, world, updater, bh, duration_s=_MOVE_BUDGET_S)
- _step_to_mine_cell(conn, sid, world, updater, spot)
- me = world.me
- d = face_dir
- if (me.x, me.y) != spot:
- _LOG.warning("[T05] 未精确踩到 %s(实际 %d,%d) — 就地重找面墙向",
- spot, me.x, me.y)
- d = _face_wall_dir((me.x, me.y), walls) or face_dir
- _LOG.info("[T05] 站(%d,%d) 挖(%d,%d) dir=%d", me.x, me.y,
- me.x + DIR_VECS[d][0], me.y + DIR_VECS[d][1], d)
- _ = mine_loop(conn, sid, world, updater, real_name,
- a.mine_seconds, (me.x, me.y), d)
- if not a.skip_heal:
- banner("[7] 开启喝药保护(进图已 ≥%.0fs)", time.time() - born_t)
- heal_state: str | None = None
- for i in range(_HEAL_ROUNDS):
- heal_state = enable_auto_heal(conn, sid, want_on=True,
- world=world)
- if heal_state == "on":
- break
- _LOG.warning("[T1] 第%d轮 state=%s — 重试", i + 1, heal_state)
- time.sleep(1.0)
- _LOG.info("[%s] 喝药状态=%s", "✓" if heal_state == "on" else "!",
- heal_state)
- banner("[7.5] 原地站立 %.0fs(仅心跳+收帧)", hold)
- deadline = time.time() + hold
- while time.time() < deadline:
- _ = pump(conn, world, updater, 0.5)
- _LOG.info("[站立结束] map=%s @(%d,%d) 距 %s %d 格",
- world.me.map_name, world.me.x, world.me.y, NPC_NAME,
- max(abs(npc_pos[0] - world.me.x),
- abs(npc_pos[1] - world.me.y)))
- top = sorted(world.frame_counts.items(), key=lambda kv: -kv[1])[:12]
- _LOG.info("收帧Top12: %s", top)
- banner("[8] 退出进程")
- return 0
- finally:
- stop[0] = True
- conn.close()
- _LOG.info("连接已关闭, 角色=%s", real_name)
- if __name__ == "__main__":
- try:
- sys.exit(main())
- except PbotError as e:
- _LOG.error("失败: %s", e.detail)
- sys.exit(1)
- except RuntimeError as e:
- _LOG.error("失败: %s", e)
- sys.exit(1)
|