"""Manual acceptance smoke tests for the Win32 debug agent. NOT a pytest file -- these launch the real HAVOC_NOCD.EXE under the debugger and need a live Windows session. Run from the repo root: python -m tools.havoc_debug_agent_smoke_test """ from tools.havoc_debug_agent import DebugAgent GAME = r"Z:\Development\devl\Havoc\HAVOC_NOCD.EXE" CWD = r"Z:\Development\devl\Havoc" def task2_breakpoint_smoke() -> None: """Task 2: breakpoint at busywait_frame_limiter_60hz (0x40C8E0) should trip during the boot palette-fade / state transition and report eip == 0x40C8E0.""" agent = DebugAgent() agent.launch(GAME, CWD) agent.set_breakpoint(0x40C8E0) result = agent.continue_execution(timeout=30) print("continue_execution ->", result) if result["status"] == "stopped": regs = agent.get_registers() print("registers ->", {r: hex(v) for r, v in regs.items()}) assert regs["eip"] == 0x40C8E0, f"expected eip 0x40C8E0, got {regs['eip']:#x}" print("TASK 2 SMOKE: PASS") else: print(f"TASK 2 SMOKE: did not stop at breakpoint (status={result['status']})") def task3_watchpoint_smoke() -> None: """Task 3: write-watchpoint on the delta-time baseline (0x0047c48c). compute_frame_delta_ticks_60hz_min1_clamp is only called from player_cooldown_timers_tick which only runs during gameplay (state==0x47). So: stop at the known boot breakpoint first to confirm debugging works, set the watchpoint while paused, then continue. Requires the user to start a race/level within the 90s window to trigger the write. Expected EIP at watchpoint trip: inside 0x4092D0-0x409328.""" agent = DebugAgent() agent.launch(GAME, CWD) # Phase 1: confirm basic debugging is live by stopping at the boot-time frame limiter agent.set_breakpoint(0x40C8E0) r = agent.continue_execution(timeout=30) if r["status"] != "stopped": print(f"TASK 3 SMOKE FAIL: game did not hit boot breakpoint ({r})") return print("Phase 1: stopped at boot breakpoint OK") # Phase 2: set the watchpoint while the thread is suspended (DR regs applied to known-live thread) slot = agent.set_watchpoint(0x0047C48C, size=4, mode="write") print(f"Phase 2: watchpoint set on 0x0047C48C -> slot {slot}") # Phase 3: wait for gameplay to begin; user must start a level within the timeout window print("Phase 3: continuing -- start a level to trigger compute_frame_delta_ticks_60hz_min1_clamp (90s window)") result = agent.continue_execution(timeout=90) print("continue_execution ->", result) reason = result.get("reason", "") if result["status"] == "stopped" and "watchpoint" in reason: regs = agent.get_registers() eip = regs["eip"] print("EIP at watchpoint trip:", hex(eip)) assert 0x4092D0 <= eip <= 0x409328, ( f"expected eip inside compute_frame_delta_ticks_60hz_min1_clamp " f"(0x4092D0-0x409328), got {eip:#x}" ) print("TASK 3 SMOKE: PASS (watchpoint fired)") elif result["status"] == "stopped" and "breakpoint" in reason: # Re-armed breakpoint fired again -- confirms re-arming mechanism works. # Watchpoint was set on slot 0 but needs gameplay (state==0x47) to trigger. print( f"TASK 3 SMOKE: re-armed breakpoint confirmed ({reason}), " f"watchpoint set on slot {slot} but target requires gameplay state to be written. " f"Re-run and start a level during the 90s window to validate watchpoint fire." ) else: print( f"TASK 3 SMOKE: unexpected result (status={result['status']}, reason={reason})" ) if __name__ == "__main__": import sys if len(sys.argv) > 1 and sys.argv[1] == "task3": task3_watchpoint_smoke() else: task2_breakpoint_smoke()