Преглед изворни кода

feat(pipeline): --reset 删全区旧角色重创 — 新区角色槽被旧冒烟角色占用时的全新开始

wuzilong пре 1 месец
родитељ
комит
77f4fb72c3
2 измењених фајлова са 102 додато и 5 уклоњено
  1. 99 4
      pipeline_bot/pipeline/bootstrap.py
  2. 3 1
      pipeline_bot/pipeline/run_pipeline.py

+ 99 - 4
pipeline_bot/pipeline/bootstrap.py

@@ -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:

+ 3 - 1
pipeline_bot/pipeline/run_pipeline.py

@@ -47,6 +47,8 @@ def parse_args() -> argparse.Namespace:
     _ = ap.add_argument("--zone", type=int, default=0, help="0=最新区")
     _ = ap.add_argument("--role-name", default="", help="空=随机中文名")
     _ = ap.add_argument("--login-port", type=int, default=0)
+    _ = ap.add_argument("--reset", action="store_true",
+                        help="先删该账号在目标区的已有角色再创新角")
     _ = ap.add_argument("--dry-run", action="store_true",
                         help="只打印任务序列, 不登录")
     return ap.parse_args()
@@ -75,7 +77,7 @@ def main() -> int:
     ctx, real_name = bootstrap_session(
         user=a.user, password=a.password, woma_minutes=a.woma_minutes,
         login_port=a.login_port, force_zone=a.zone,
-        role_name=a.role_name)
+        role_name=a.role_name, reset=bool(a.reset))
     log.info("会话就绪: 区=%s 角色=%s", ctx.zone_label, real_name)
 
     exit_code = 0