| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- """WanderBehavior/UnlockBehavior 走链行为验证(不发真实网络 — 桩发送)。"""
- import json
- import time
- from typing import final
- from pbot.bot.unlock import UnlockBehavior
- from pbot.bot.wander import DIRS, RUN_INTERVAL_S, STEP_INTERVAL_S, WAIT_MAP, WanderBehavior
- from pbot.codec.frame import Frame, build_frame, parse_frame
- from pbot.world.actor import FindTarget
- from pbot.world.mirror import WorldMirror
- @final
- class SpyConn:
- """记录 send_frame 参数; recv 不产生帧。"""
- def __init__(self) -> None:
- self.sent: list[tuple[int, int, int, int, int, bytes]]
- self.pres: list[int]
- self.sent = []
- self.pres = []
- # 看门狗字段(AutoFight.tick 检查): 恒"最近收帧"= 不触发死线
- self.last_recv_t: float = __import__("time").time()
- def send_frame(self, msg_id: int, recog: int, p1: int, p2: int, p3: int,
- body: bytes = b"", tail: int | None = None, pre: int = 0) -> int:
- del tail
- self.sent.append((msg_id, recog, p1, p2, p3, body))
- self.pres.append(pre)
- return 0
- def send_raw(self, wire: bytes) -> None:
- import struct
- from pbot.codec.bitbuf import decode6
- pos = 0
- while pos < len(wire):
- h = struct.unpack("<9i", decode6(wire[pos:pos + 48]))
- blen = h[7]
- self.sent.append((h[1], h[2], h[3], h[4], h[5],
- wire[pos + 48:pos + 48 + blen]))
- pos += 48 + blen
- def recv_frames(self, timeout: float) -> list[Frame]:
- del timeout
- return []
- def ready_world(x: int, y: int) -> WorldMirror:
- """SC50/SC51 已就绪的世界。"""
- world = WorldMirror()
- world.me.map_name = WAIT_MAP
- world.me.actor_key = 7
- world.me.x, world.me.y = x, y
- return world
- def fast_wander(sid: int, spy: SpyConn, world: WorldMirror) -> WanderBehavior:
- """已 on_enter 且时间门已到点的 WanderBehavior。"""
- b = WanderBehavior(sid=sid)
- b.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- b._last_step_t = time.time() - STEP_INTERVAL_S - 0.05 # pyright: ignore[reportPrivateUsage]
- return b
- def fast_walker_no_run(spy: SpyConn, world: WorldMirror) -> WanderBehavior:
- """use_run=False 且时间门已到点的 WanderBehavior。"""
- b = WanderBehavior(sid=1, use_run=False)
- b.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- b._last_step_t = time.time() - STEP_INTERVAL_S # pyright: ignore[reportPrivateUsage]
- return b
- # ---- WanderBehavior: 单帧制(cap_unlock 定案) ----
- def test_wander_sends_single_3013_frame() -> None:
- """Given: 世界就绪 When: 一拍到点 Then: 发三帧组 3013+CS1+CS28x2(cap_gold t=4985)。"""
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=159, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- kinds = [s[0] for s in spy.sent]
- assert kinds == [3011] # 单帧制(cap_live 14 步铁证: 零 CS1/CS28 上行)
- msg_id, recog, p1, p2, p3, body = spy.sent[0]
- del msg_id
- assert recog == 0
- assert p1 == 5 # 初始朝向
- assert p2 == (27 << 16) | 23 # 起点
- assert p3 == 159 # p3 = sid(cap_unlock/cap_live 双定案)
- assert body == WAIT_MAP.encode()
- def test_wander_walk_mode_uses_3011() -> None:
- """Given: use_run=False When: tick Then: 发 CS3011 走 1 格。"""
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_walker_no_run(spy, world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert spy.sent[0][0] == 3011
- def test_wander_3011_carries_landing_prediction_in_pre() -> None:
- """Given: 走一步 When: 发 3011 Then: pre=预测落点(Y<<16|X)非零。
- 回归: 2026-09-26 破案定案 — cap_round2 30 会话统计, pre=0 的 3011
- 215/215 被 SC28 拒, pre≠0 的 60/60 放行。真客户端 pre=落点预测。
- """
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- x, y = 23, 27
- dx, dy = DIRS[5] # 初始朝向 5
- expected_pre = ((y + dy) << 16) | (x + dx)
- assert spy.pres[0] == expected_pre
- assert spy.pres[0] != 0
- def test_wander_run_mode_sends_3013_two_cells() -> None:
- """Given: use_run=True When: tick Then: 发 CS3013 且落点=起点+2格。
- 协议定案(cap_round2 sid=88 放行实证): 3013 与 3011 同构,
- 跑一步 = 沿 dir 跳 2 格, 步距 0.55s, 服务器仍回 SC1。
- """
- world = ready_world(23, 27)
- spy = SpyConn()
- b = WanderBehavior(sid=1, use_run=True)
- b.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- b._last_step_t = time.time() - RUN_INTERVAL_S * 2 # pyright: ignore[reportPrivateUsage]
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert spy.sent[0][0] == 3013
- _, recog, p1, p2, p3, body = spy.sent[0]
- del recog, p3, body
- dx, dy = DIRS[5] # 初始朝向 5
- assert p1 == 5 and p2 == (27 << 16) | 23
- assert spy.pres[0] == ((27 + dy * 2) << 16) | (23 + dx * 2)
- def test_wander_holds_until_sc1_confirm() -> None:
- """Given: 上一步未获 SC1 确认 When: 到点再 tick Then: 不发下一步(链式咬合)。"""
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 1
- b._last_step_t = time.time() # pyright: ignore[reportPrivateUsage] - 步距刚走完
- world.record("self_move_confirmed") # SC1 到达(释放 pending)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 1 # 步距未到, 不发
- b._last_step_t = time.time() - STEP_INTERVAL_S - 0.05 # pyright: ignore[reportPrivateUsage]
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 2 # 确认+到点 → 放行下一步
- def test_wander_pull_back_releases_pending() -> None:
- """Given: SC28 拉回 When: tick Then: pending 释放且拒绝计数。"""
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- world.record("pull_back")
- b._last_step_t = time.time() - STEP_INTERVAL_S - 0.05 # pyright: ignore[reportPrivateUsage] - 余量防时钟抖动
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 2 # 拉回后以服务器位置重发(pending 已释放)
- del b
- def test_wander_anchor_echo_releases_pending() -> None:
- """Given: SC28 锚点回显(anchor_echo 事件) When: 到点 tick Then: 放行重发。
- 回归: 2026-09-24 冻结 bug — _absorb_events 不认 anchor_echo 导致
- pending 永挂, dir 恒 5 坐标不变(一发即停)。
- 新行为: 同向锁 3 步 — 撞回后保持同方向, 链式 3011 沿同方向发,
- SC1 一旦放行就跑通(避免 (200,30)↔(198,28) A↔B 振荡)。
- """
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 1
- world.record("anchor_echo") # update.py 对 SC28 p3=32368232 记的事件名
- b._last_step_t = time.time() - STEP_INTERVAL_S - 0.05 # pyright: ignore[reportPrivateUsage]
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 2
- assert spy.sent[1][2] == spy.sent[0][2] # p1=heading 同向锁住
- assert spy.sent[1][0] == 3011 # 仍发新帧(非冻结)
- def test_wander_pending_timeout_frees_silence() -> None:
- """Given: 帧被静默丢(无任何回包) When: 超过 PENDING_TIMEOUT_S Then: 重发。"""
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 1
- b._last_step_t = time.time() - STEP_INTERVAL_S - 0.05 # pyright: ignore[reportPrivateUsage]
- b._pending_t = time.time() - 10.0 # pyright: ignore[reportPrivateUsage] - 模拟超时
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert len(spy.sent) == 2
- def test_wander_new_heading_avoids_occupied_cells() -> None:
- """Given: 视野内 NPC/怪占满部分邻格 When: 换向 Then: 选中未占据方向。"""
- from pbot.types import ActorId
- from pbot.world.actor import ActorKey, Npc
- world = ready_world(23, 27)
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- for h, (dx, dy) in enumerate(DIRS):
- if h in (5, 2): # 5=当前朝向(排除), 2=留作唯一空闲候选
- continue
- world.npcs[100 + h] = Npc(key=ActorKey(100 + h), aid=ActorId(f"a{h}"),
- name=f"n{h}", x=23 + dx, y=27 + dy, index=h)
- b._rejected = 1 # pyright: ignore[reportPrivateUsage] - 触发换向分支
- heading = b._pick_new_heading(world) # pyright: ignore[reportPrivateUsage]
- assert heading == 2
- def test_wander_waits_until_world_known() -> None:
- """Given: SC50/SC51 未到 When: tick Then: 不发帧。"""
- world = WorldMirror()
- spy = SpyConn()
- b = fast_wander(sid=1, spy=spy, world=world)
- _ = b.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert spy.sent == []
- def test_dirs_table_matches_lua_source() -> None:
- """Given: Lua mmoConstants 方向表 When: 检查 Then: 与 backup 源码一致。"""
- assert DIRS[0] == (0, -1) # 上
- assert DIRS[2] == (1, 0) # 右
- assert DIRS[4] == (0, 1) # 下
- assert DIRS[6] == (-1, 0) # 左
- # ---- UnlockBehavior: 黄金时序状态机 ----
- def test_unlock_renwu_golden_timing() -> None:
- """Given: 已传比奇 When: renwu链 Then: 3031→[1.2s]→1011 分步发, 不连发。"""
- world = ready_world(289, 620)
- world.me.map_name = "比奇大城"
- world.me.map_id = "0"
- spy = SpyConn()
- u = UnlockBehavior(sid=159)
- u.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- u._phase_t = 0.0 # pyright: ignore[reportPrivateUsage] - 跳过 settle 等待
- _ = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- kinds = [s[0] for s in spy.sent]
- assert kinds == [3031] # 第一拍只发 3031
- u._phase = "renwu_1011" # pyright: ignore[reportPrivateUsage] - 模拟对话框已点
- u._phase_t = 0.0 # pyright: ignore[reportPrivateUsage]
- _ = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- kinds = [s[0] for s in spy.sent]
- assert kinds == [3031, 1011]
- body = spy.sent[1][5].decode()
- assert json.loads(body)["Act"] == "@renwu_mov,1"
- def test_unlock_retention_band_starts_npc_chain() -> None:
- """Given: 等待图滞留带(y<5) When: on_enter Then: 先走 NPC 视野等待, 不直发 renwu。"""
- world = ready_world(30, 0)
- spy = SpyConn()
- u = UnlockBehavior(sid=159)
- u.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- _ = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert spy.sent == [] # 等NPC视野, 不盲发
- def test_unlock_uses_full_cs3031_body() -> None:
- """Given: 无 SC55 缓存 When: 发 3031 Then: body 为 562B 完整体(≥500B)。"""
- world = ready_world(289, 620)
- world.me.map_name = "比奇大城"
- spy = SpyConn()
- u = UnlockBehavior(sid=1)
- u.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- u._phase_t = 0.0 # pyright: ignore[reportPrivateUsage]
- _ = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- body = spy.sent[0][5]
- assert len(body) >= 500 # 完整任务体, 非 200B 截断
- def test_unlock_accept_signal_enters_run() -> None:
- """Given: 1011 已发 When: SC20102 到达 Then: 进入冲刺并发 3013。"""
- world = ready_world(24, 27)
- spy = SpyConn()
- u = UnlockBehavior(sid=159)
- u.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- u._phase = "accept" # pyright: ignore[reportPrivateUsage]
- u._phase_t = time.time() # pyright: ignore[reportPrivateUsage]
- world.find_target = FindTarget(map_id=WAIT_MAP, x=16, y=34)
- world.record("auto_find")
- _ = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert u._phase == "run" # pyright: ignore[reportPrivateUsage]
- kinds = [s[0] for s in spy.sent]
- assert kinds == [3013] # 受理同拍即起跑(事件把 last_step_t 归零)
- p2 = spy.sent[0][3]
- assert p2 == (27 << 16) | 24
- def test_unlock_arrival_sends_exit() -> None:
- """Given: 冲刺中距目标≤2 When: tick Then: 发 CS1011 @exit 收尾。"""
- world = ready_world(16, 32)
- world.find_target = FindTarget(map_id=WAIT_MAP, x=16, y=34)
- spy = SpyConn()
- u = UnlockBehavior(sid=1)
- u.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- u._phase = "run" # pyright: ignore[reportPrivateUsage]
- u._phase_t = time.time() # pyright: ignore[reportPrivateUsage]
- u._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage]
- _ = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- kinds = [s[0] for s in spy.sent]
- assert kinds == [1011]
- body = spy.sent[0][5].decode()
- assert json.loads(body)["Act"] == "@exit"
- def test_unlock_done_when_confirmed_in_walkable() -> None:
- """Given: SC1 已确认且 y≥5 When: tick Then: DONE。"""
- world = ready_world(23, 27)
- spy = SpyConn()
- u = UnlockBehavior(sid=1)
- u.on_enter(spy, world) # pyright: ignore[reportArgumentType]
- u._move_confirmed = 1 # pyright: ignore[reportPrivateUsage]
- world.record("self_move_confirmed")
- result = u.tick(spy, world) # pyright: ignore[reportArgumentType]
- assert result.value == "done"
- # ---- update.py 新增 SC 帧解析 ----
- def test_sc1_confirms_position() -> None:
- """Given: SC1 确认帧 When: updater.apply Then: 位置更新+事件。"""
- from pbot.world.update import WorldUpdater
- world = WorldMirror()
- world.me.uid = "u1"
- updater = WorldUpdater()
- wire = build_frame(1, 21, 28, 5, 32368232, b"", tail=817)
- f = parse_frame(wire)
- updater.apply(world, f)
- assert world.me.x == 21 and world.me.y == 28
- assert world.events[-1].kind == "self_move_confirmed"
- def test_sc28_anchor_echo_not_reject() -> None:
- """Given: SC28 p3=32368232(锚点回显) When: apply Then: 不计拒绝。"""
- from pbot.world.update import WorldUpdater
- world = WorldMirror()
- world.me.map_name = WAIT_MAP
- updater = WorldUpdater()
- wire = build_frame(28, 0, 23, 27, 32368232, b"")
- f = parse_frame(wire)
- updater.apply(world, f)
- kinds = [e.kind for e in world.events]
- assert "pull_back" not in kinds
- assert "anchor_echo" in kinds
- def test_sc31_my_strike_feeds_hp() -> None:
- """Given: SC31 受击者是我 When: apply Then: me.hp 实时更新。
- 回归: SC53 面板帧稀疏(仅登录/死亡推), 不喂 SC31 血量则
- 低血逃跑永远只在 hp=0(已死)时触发(d401 死亡循环)。
- """
- from pbot.codec.bitbuf import encode6
- from pbot.world.update import WorldUpdater
- world = WorldMirror()
- world.me.uid = "14558825001790500354000067138"
- world.me.actor_key = 21889350
- updater = WorldUpdater()
- body = encode6(json.dumps(
- {"Hp": 80, "MaxHp": 305, "Damage": 60,
- "UserID": "21889350",
- "AttackUserID": "61526"}).encode())
- wire = build_frame(31, 0, 0, 0, 0, body)
- f = parse_frame(wire)
- updater.apply(world, f)
- assert world.me.hp == 80 and world.me.max_hp == 305
- def test_sc27_revive_restores_full_hp() -> None:
- """Given: 死亡 hp=0 收 SC27 When: apply Then: 复活+回满血。
- 回归: 回城复活后 SC53 不重推, hp 残留 0 会立即误触发低血逃跑。
- """
- from pbot.world.update import WorldUpdater
- world = WorldMirror()
- world.me.uid = "u1"
- world.me.hp, world.me.max_hp = 0, 305
- world.me.is_dead = True
- updater = WorldUpdater()
- wire = build_frame(27, 0, 0, 0, 0, b"")
- f = parse_frame(wire)
- updater.apply(world, f)
- assert world.me.is_dead is False
- assert world.me.hp == 305
- def test_sc31_my_lethal_strike_records_me_die() -> None:
- """Given: SC31 受击者是我且 Hp=0 When: apply Then: is_dead + me_die 事件。
- 回归: 我被怪打死时服务器不发 SC32, 只有 SC31(hp=0)+SC634 回城 —
- 缺 me_die 则逃跑阶段继续发移动帧("跑尸")。
- """
- from pbot.codec.bitbuf import encode6
- from pbot.world.update import WorldUpdater
- world = WorldMirror()
- world.me.uid = "14558825001790500354000067138"
- world.me.actor_key = 21889350
- updater = WorldUpdater()
- body = encode6(json.dumps(
- {"Hp": 0, "MaxHp": 407, "Damage": 72,
- "UserID": "21889350",
- "AttackUserID": "62471"}).encode())
- wire = build_frame(31, 0, 0, 0, 0, body)
- f = parse_frame(wire)
- updater.apply(world, f)
- assert world.me.is_dead is True
- assert world.me.hp == 0
- assert "me_die" in [e.kind for e in world.events]
- def test_sc20102_sets_find_target() -> None:
- """Given: SC20102(body=encode6 JSON) When: apply Then: find_target 就位。"""
- from pbot.codec.bitbuf import encode6
- from pbot.world.update import WorldUpdater
- world = WorldMirror()
- updater = WorldUpdater()
- body = encode6(json.dumps(
- {"mapID": "等待地图", "x": 16, "y": 34,
- "targetType": 100, "targetIndex": [34]}).encode())
- wire = build_frame(20102, 0, 0, 0, 0, body)
- f = parse_frame(wire)
- updater.apply(world, f)
- assert world.find_target is not None
- assert world.find_target.x == 16 and world.find_target.y == 34
- def test_cs3013_wire_layout() -> None:
- """Given: 跑帧 When: build_frame Then: 布局同 3011(cap_unlock 同构)。"""
- wire = build_frame(3013, 0, 5, (27 << 16) | 23, 159, WAIT_MAP.encode())
- f = parse_frame(wire)
- assert f.msg_id == 3013 and f.p2 == (27 << 16) | 23 and f.p3 == 159
|