|
|
@@ -10,11 +10,18 @@ from typing import Final
|
|
|
|
|
|
import requests
|
|
|
|
|
|
+from pbot.codec.bitbuf import encode6
|
|
|
from pbot.config import Account, RoleSpec
|
|
|
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 create_role_needed, login_gate_with_retry
|
|
|
+from pbot.transport.gateway import (
|
|
|
+ GatewayConnection,
|
|
|
+ TransportError,
|
|
|
+ _rand30,
|
|
|
+ create_role_needed,
|
|
|
+ login_gate_with_retry,
|
|
|
+)
|
|
|
from pbot.types import ZoneId
|
|
|
from pbot.world.update import as_dict, to_float, to_int, typed_json
|
|
|
from pipeline.ctx import ChecklistWriter, PipelineContext
|
|
|
@@ -102,14 +109,95 @@ def _resolve_login_ports(zone: ZoneId, login_port: int) -> tuple[int, ...]:
|
|
|
return DEFAULT_PORTS
|
|
|
|
|
|
|
|
|
+def _delete_all_roles(zone: ZoneId, jwt: str, uid: int,
|
|
|
+ ports: tuple[int, ...], max_rounds: int = 6) -> list[str]:
|
|
|
+ """删该区全部角色。
|
|
|
+
|
|
|
+ 循环: 拉列表→删 last=1→重拉(backup/delete_all_roles 定案:
|
|
|
+ 服务器仅受理删除当前选中角色, 删后剩余角色自动成为新选中)。
|
|
|
+ """
|
|
|
+ deleted: list[str] = []
|
|
|
+ for rnd in range(max_rounds):
|
|
|
+ conn = None
|
|
|
+ for port in ports:
|
|
|
+ try:
|
|
|
+ conn = GatewayConnection(zone, port)
|
|
|
+ break
|
|
|
+ except TransportError as e:
|
|
|
+ _LOG.warning("端口 %d 连接失败: %s", port, e)
|
|
|
+ if conn is None:
|
|
|
+ raise RuntimeError("删角: 登录服全端口连接失败")
|
|
|
+ try:
|
|
|
+ roles = _fetch_role_list(conn, zone, jwt, uid)
|
|
|
+ lst = roles if isinstance(roles, list) else []
|
|
|
+ if not lst:
|
|
|
+ _LOG.info("[删角] 已清空(共删 %d 个: %s)",
|
|
|
+ len(deleted), deleted)
|
|
|
+ return deleted
|
|
|
+ target = next((c for c in lst if c.get("last") == 1), lst[0])
|
|
|
+ name = str(target.get("chr", ""))
|
|
|
+ tid = str(target.get("UserID", ""))
|
|
|
+ _LOG.info("[删角] round%d 目标=%s(last=%s)", rnd, name,
|
|
|
+ target.get("last"))
|
|
|
+ _ = conn.send_frame(102, 0, 0, 0, 0, tid.encode(), tail=3)
|
|
|
+ for f in conn.recv_frames(8):
|
|
|
+ if f.msg_id == 523:
|
|
|
+ deleted.append(name)
|
|
|
+ _LOG.info("[删角] %s 成功", name)
|
|
|
+ break
|
|
|
+ if f.msg_id == 524:
|
|
|
+ _LOG.error("[删角] %s 失败(524)", name)
|
|
|
+ time.sleep(2.0)
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+ return deleted
|
|
|
+
|
|
|
+
|
|
|
+def _fetch_role_list(conn: GatewayConnection, zone: ZoneId, jwt: str,
|
|
|
+ uid: int) -> list[dict[str, object]]:
|
|
|
+ """登录链拉 SC520 角色列表(2001→104→100→520, login_gate 同源精简)。"""
|
|
|
+ from pbot.codec.bitbuf import decode6
|
|
|
+ from pbot.config import CHANNEL, GAME_ID
|
|
|
+
|
|
|
+ body2001 = (f"{uid}/".encode() + encode6(jwt.encode()) + b"/"
|
|
|
+ + encode6(CHANNEL.encode()) + b"/1/0")
|
|
|
+ _ = conn.send_frame(2001, _rand30(), zone, conn.port, 0, body2001, tail=0)
|
|
|
+ key32 = None
|
|
|
+ for f in conn.recv_frames(6):
|
|
|
+ if f.msg_id == 529:
|
|
|
+ key32 = decode6(f.body).split(b"/")[0]
|
|
|
+ if not key32:
|
|
|
+ raise RuntimeError("拉列表: 无 SC529")
|
|
|
+ _ = conn.send_frame(104, 0, 0, 0, 0, encode6(key32), tail=1)
|
|
|
+ sid = 0
|
|
|
+ for f in conn.recv_frames(4):
|
|
|
+ if f.msg_id == 530:
|
|
|
+ sid = f.pre
|
|
|
+ body100 = f"{uid}/{sid}/{zone}/{CHANNEL}/{GAME_ID}/".encode() + key32
|
|
|
+ _ = conn.send_frame(100, _rand30(), zone, conn.port, sid, body100, tail=2)
|
|
|
+ for f in conn.recv_frames(6):
|
|
|
+ if f.msg_id == 520 and f.body:
|
|
|
+ try:
|
|
|
+ raw = decode6(f.body) if all(0x3C <= c <= 0x7B
|
|
|
+ for c in f.body[:200]) \
|
|
|
+ else f.body
|
|
|
+ data = typed_json(json.loads(raw))
|
|
|
+ lst = data.get("list") if isinstance(data, dict) else None
|
|
|
+ return [as_dict(i) for i in lst] if isinstance(lst, list) \
|
|
|
+ else []
|
|
|
+ except (ValueError, UnicodeDecodeError):
|
|
|
+ continue
|
|
|
+ return []
|
|
|
+
|
|
|
+
|
|
|
def bootstrap_session(user: str, password: str, woma_minutes: float,
|
|
|
login_port: int = 0, force_zone: int = 0,
|
|
|
- role_name: str = "",
|
|
|
+ role_name: str = "", reset: bool = False,
|
|
|
) -> tuple[PipelineContext, str]:
|
|
|
- """登录一次 → 选区 → 创角 → 进世界; 喝药保护在 T1 任务开启。"""
|
|
|
+ """登录一次 → 选区 → (可选删角) → 创角 → 进世界; 喝药保护在 T1 任务开启。"""
|
|
|
zone, label = _resolve_zone(force_zone)
|
|
|
role = create_role_needed(RoleSpec(name=role_name or random_cn_name()))
|
|
|
- _LOG.info("区=%s(%s) 角色=%s", label, zone, role)
|
|
|
+ _LOG.info("区=%s(%s) 角色=%s reset=%s", label, zone, role, reset)
|
|
|
|
|
|
http = requests.Session()
|
|
|
http.trust_env = False
|
|
|
@@ -125,6 +213,13 @@ def bootstrap_session(user: str, password: str, woma_minutes: float,
|
|
|
time.sleep(_ENTER_GAME_RETRY_SLEEP_S)
|
|
|
|
|
|
ports: tuple[int, ...] = _resolve_login_ports(zone, login_port)
|
|
|
+
|
|
|
+ if reset:
|
|
|
+ deleted = _delete_all_roles(zone, jwt, uid, ports)
|
|
|
+ _LOG.info("[删角·reset] 清空 %d 个: %s — 10s 落库后重创",
|
|
|
+ len(deleted), deleted)
|
|
|
+ time.sleep(10) # 服务器落库延迟(实测: 立即重创会拿不到票据)
|
|
|
+
|
|
|
_, sid_game, real_name, ticket_key, game_port = login_gate_with_retry(
|
|
|
zone, jwt, uid, role, ports)
|
|
|
if real_name != role:
|