Commit graph

24 commits

Author SHA1 Message Date
a77db1edcb fix: derive debug-agent client timeout from continue_execution's requested wait
DebugAgentClient.call() hardcoded a 60s socket timeout that also bounded
recv(), so any continue_execution call asking for a longer wait (e.g.
waiting for a user to alt-tab) would time out on the client side while
the debug agent was still legitimately waiting. call() now accepts an
optional socket_timeout override, and the continue_execution tool passes
timeout + 30 so the client socket outlives the agent's own timeout + 10s
of headroom.

Also adds a round-trip test for encode_message/decode_message (the wire
contract between the two independently-built sides of this protocol),
and corrects the WatchpointSlots docstring, which overstated what
Wow64SetThreadContext does -- the agent actually arms the hardware
watchpoints via a native x64 SetThreadContext call, with the Wow64
variant used only for 32-bit register readback.

Claude-Session: https://claude.ai/code/session_01E4YCRPfAE3328FcYPyKu9T
2026-07-04 14:38:54 -07:00
6311ecb69e chore: untrack debug screenshot artifact, ignore future MCP client config
tools/debug_screenshot_gameplay.png was a one-off capture from testing the
runtime-observation MCP's screenshot tool, not something that belongs in
version control. Also ignore .mcp.json (local MCP server registration) and
future tools/debug_screenshot*.png captures.
2026-07-04 14:27:12 -07:00
481b2e1dbf docs: record live runtime confirmation of alt-tab state transition (2026-07-04)
Hardware watchpoint on game_object+0x18 (runtime addr 0x2361064) fired during
active gameplay via MCP debug agent. Write at 0x4360A7 (MOV [ECX+18h], 0xB0)
inside on_deactivate_force_pause_state. Confirms existing static analysis:
- eax=0x47 (old gameplay state read just before write)
- esi=0x1c (WM_ACTIVATEAPP -- exact match to documented WM message)
- DAT_0047E578=0x47 (prev-state global saved correctly)
- game_object+0x20=0x47 (target state still 0x47, round-trip intact)
State 0xB0 (pause/deactivate) was already documented; this adds live confirmation.
The still-open symptom-2 (reset to menu on re-focus during gameplay) is not in
this state write -- it lies further along in ddraw_full_display_reinit.

Also records Item 3 watchpoint result: watchpoint on DAT_0047c488 fired at
0x40E998 (MOV [DAT_0047c488], EAX in player_cooldown_timers_tick -- the store
is in the caller, not inside compute_frame_delta_ticks_60hz_min1_clamp).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-04 14:23:48 -07:00
7d39ccddf0 fix: harden debug agent for reliable WOW64 watchpoints and multi-run stability
Key fixes applied during Task 6 end-to-end MCP integration testing:

- CONTEXT_ALL (CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS) for Wow64Get/SetThreadContext
  so DR0-DR7 are read and written alongside general-purpose registers
- Native x64 SetThreadContext path in _apply_watchpoints_to_thread: Wow64SetThreadContext
  updates the WOW64 context save area but does not propagate to hardware DR registers;
  the native path is required for hardware watchpoints to actually fire on WOW64 threads
- _REGISTER_NAMES extended to include Dr0-Dr3, Dr6, Dr7 so get_registers returns them
- launch() now terminates and drains the previously attached process before starting a
  new one, preventing stale debug events from a hung game from corrupting a new session
- continue_execution filters events by process ID so late-arriving exit events from a
  previously terminated game do not trigger a false "exited" status for the new process
- AV exception routing restored: EXCEPTION_ACCESS_VIOLATION stops and reports; callers
  skip boot-time AVs in a loop rather than silently continuing them in the agent
- launch_with_breakpoints atomic op in agent server and MCP server
- _DebugWorker dedicated thread: Windows debug API requires all WaitForDebugEvent,
  ContinueDebugEvent, and CreateProcessA calls to originate from the same OS thread;
  ThreadingTCPServer violated this -- replaced with single worker thread + queue
- Gameplay screenshot captured at watchpoint stop (272 KB real frame)

Acceptance checklist results:
- Item 1 PASS: bp@0x40C8E0, eip/eax confirmed
- Item 2 PASS: conditional breakpoint false-skip and true-fire verified
- Item 3 PASS: watchpoint@slot0 fired at 0x40E998 (MOV [DAT_0047c488], EAX in
  player_cooldown_timers_tick -- the DAT write is in the caller, not inside
  compute_frame_delta_ticks_60hz_min1_clamp as the spec assumed)
- Item 4 PASS: screenshot PNG captured (gameplay frame 272 KB)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-04 14:05:24 -07:00
6baefe8e8a feat: add screenshot capture and local JSON-over-TCP server to debug agent
Adds DebugAgent.screenshot() using Pillow/ImageGrab -- locates the game window
via FindWindowA, grabs its rect, returns PNG bytes. Adds tools/havoc_debug_agent_server.py:
a 127.0.0.1-only ThreadingTCPServer that serializes all ops behind a single lock and
dispatches NDJSON requests to DebugAgent. Covers all debug-agent ops (launch,
breakpoints, watchpoints, continue_execution, read/write_memory, get_registers,
screenshot, get_status). Acceptance test confirmed: get_status returns
{"ok": true, "result": {"status": "not_launched"}} from a fresh server. Adds
tools/requirements-windows.txt (pillow).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-04 09:30:07 -07:00
12ec33b41a feat: add hardware watchpoints (DR0-DR3/DR7) and re-armable breakpoints to debug agent
Implements set_watchpoint/remove_watchpoint/list_watchpoints via Wow64SetThreadContext
on DR0-DR3/DR7. Skips 64-bit WOW64 helper threads (Wow64GetThreadContext returns
ERROR_ACCESS_DENIED on those -- found against the real process). Propagates active
watchpoints to new threads as they spawn. Re-armable breakpoints: on breakpoint hit,
restores original byte and sets EFlags trap flag; the resulting single-step re-plants
the 0xCC, making breakpoints persistent across multiple hits. EXCEPTION_SINGLE_STEP
handler distinguishes watchpoint trips (Dr6 bits 0-3 set) from re-arm single-steps
(Dr6 clear). Smoke test confirmed: re-armed breakpoint fires on second continue;
watchpoint on 0x0047C48C set on slot 0 -- full watchpoint trip validation requires
gameplay state==0x47 (deferred to Task 6 end-to-end run).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-04 09:26:37 -07:00
9360d1ea7a feat: add Win32 debug agent with breakpoint/register/memory support
Implements DebugAgent class (Task 2) with launch, set/remove breakpoint,
get_registers, read/write_memory, continue_execution, and get_status.
Handles WOW64 WX86 exception codes (0x4000001F/0x4000001E), keeps debuggee
suspended via _pending_event until caller resumes, and tracks stopped thread
for correct register reads. Smoke-tested against live HAVOC_NOCD.EXE: stopped
at breakpoint@0x40C8E0 with eip=0x40c8e0 confirmed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-04 09:17:36 -07:00
cf283c9bba fix: close timing-attack and empty-secret auth bypass in MCP server
check_bearer_token now uses hmac.compare_digest for constant-time comparison
and rejects a falsy expected token outright, with a startup guard that fails
loudly if HAVOC_MCP_TOKEN is set but empty. Adds Starlette TestClient coverage
of the _BearerAuthMiddleware auth-enforcement path (no header, wrong token,
correct token), and cleans up dead code found during review: unused token
param on build_mcp_server, a per-request import, and an unused json import.
2026-07-04 07:22:55 -07:00
0388488c74 feat: wire MCP tool definitions and bearer-token auth into server
Adds requirements.txt (mcp==1.28.1, pytest==9.1.1) since no Python
dependency-management convention existed in this repo yet.
2026-07-04 07:13:36 -07:00
b2887753e9 feat: add bearer token auth check for MCP server 2026-07-04 07:08:21 -07:00
d2b85fc276 feat: add DebugAgentClient for MCP-to-debug-agent translation 2026-07-04 07:07:36 -07:00
3256f4b607 feat: add watchpoint slot/DR7 bookkeeping to MCP protocol module 2026-07-04 06:58:46 -07:00
4a218cdb05 feat: add condition evaluator for runtime-observation MCP protocol 2026-07-04 06:57:59 -07:00
37571e8b8a docs: add briefing for the Caspian-side agent implementing Tasks 2-4/6 2026-07-04 06:01:55 -07:00
a06b3a28df docs: add implementation plan for runtime-observation MCP
Six tasks: protocol module (pure logic, TDD'd), debug agent core
(launch + software breakpoints, extends tools/dbg_path.py), watchpoints
(DR0-DR3/DR7), screenshot + local JSON-over-TCP server, MCP server
(tool definitions + bearer auth), and end-to-end integration against
the real game on Caspian. Tasks 1 and 5 are Windows-independent and can
run in parallel; Tasks 2/3/4/6 require ctypes.windll and must run on
Caspian against the real process.
2026-07-03 23:35:40 -07:00
70213dfa8e docs: add design spec for runtime-observation MCP
Static Ghidra decompilation hit a wall on the alt-tab-while-not-paused
reset bug - state machine confirmed correct, but the actual render
function was never located and the cause couldn't be determined from
code alone. This is the trigger for building runtime-observation
tooling: a Windows-hosted debug agent (extending tools/dbg_path.py's
proven ctypes Win32 debug-loop approach) plus a thin MCP server
translating tool calls to it, reachable over the LAN from Caspian
(the machine that actually runs the game).
2026-07-03 23:26:46 -07:00
763509e4bc feat: set up Linux Ghidra pipeline, find and patch FPS-coupling and pause-hang bugs
Ghidra decompilation environment stood up on Linux (headless, no sudo): JDK 21 +
Ghidra 12.1.2 in ~/tools, reusing the existing analyzed project at
tools/ghidra_project/ from the prior Windows session instead of starting fresh.

FPS-coupling investigation: traced the main game loop's per-frame path and found
the real root cause - compute_frame_delta_ticks_60hz_min1_clamp (0x4092D0) computes
correct delta-time from timeGetTime(), but clamps the minimum return value to 1 tick
even when zero ticks truly elapsed, causing simulation speed to scale with framerate
above 60fps. First patch attempt (NOP the clamp) crashed on level load because the
shared delta value has 203 read sites across 40+ functions and only 2 were checked
for zero-safety; reverted and archived as a documented lesson. The corrected fix
(busy-wait instead of lying) is designed but not yet built.

Pause-loop hang fixed: the pause command handler blocks the Windows message queue
in a tight GetAsyncKeyState polling loop (found while locating the cheat code
handler), which trips modern Windows' "Not Responding" hang detection if left
paused too long, permanently disconnecting input. Patched (HAVOC_NOCD_PAUSEFIX_v1.EXE)
to pump one message per loop iteration via the game's own existing message-drain
function; confirmed fixed by play-testing, including the related "alt-tab while
paused resets to menu" symptom. A separate "alt-tab while not paused" reset symptom
remains open.

New tracking docs: docs/FUNCTIONS.md (function-by-function RE progress),
docs/PATCH_VERSIONS.md (binary patch version log), docs/LEVEL_NOTES.md (level/AI
design quirks for the Remaster variant). Extended docs/PATCHES.md and
docs/METHODOLOGY.md with full discovery narratives including dead ends.
2026-07-03 22:32:28 -07:00
68cf36a3d0 docs: add self-contained session handoff for continuing on Linux
docs/HANDOFF.md bootstraps a fresh session (no prior conversation memory):
project goal + variants, current state (game boots, formats cracked), the
Windows->Linux tooling map, repo/git constraints (copyright, no game data),
the binary's section FO<->VA formulas and verified control-flow address map,
format status, the Ghidra decompilation targets (FPS-coupling/render modes,
cheats, weapons, AI, level loaders), the format-cracking methodology, and
suggested next steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-03 10:02:44 -07:00
198977ed2b feat: crack GRAFIX0N.DAT texture format + extractor
WORLDS/GRAFIX0N.DAT is a big-endian (Mac-origin) tileset: an 8-byte header
(file size at 0x08, entry count at 0x0c), 16-byte entry records (BE offset +
id), and per-graphic blocks with a BE width/height/frames/id header followed by
w*h*frames 8-bit palette indices. Confirmed against the in-game loader at
0x444db0 (16-byte record stride, BE->LE dword swap).

GRAFIX00.DAT decodes to 29 graphics: explosion (id 901, 8 frames), smoke
(id 911, 8 frames), and 27 single-frame 64x64 structure/wall textures
(ids 8001-8028). tools/grafix_extract.py renders them to PNG with any
768-byte RGB palette (the XPAL palettes from INTRFACE.FF work).

- tools/grafix_extract.py: parser + PNG renderer
- docs/FORMATS.md: GRAFIX0N.DAT spec
- README: status update

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-03 09:36:41 -07:00
27174a1c79 feat: NOCD patch boots Havoc on modern Windows (defeats CD copy protection)
Diagnosed and fixed the crash that killed HAVOC_NOCD.EXE ~1s after launch
(dgVoodoo window appears black, then the process exits). It was not a clean
exit: an ACCESS_VIOLATION (null-pointer read at VA 0x444E6A) during init.

Root cause is a copy-protection check in the WORLDS\ data-file loader
(0x42a480). It opens each file (GRAFIX/LAND/MAP/STUF) from both a local
"WORLDS\<name>" path and a CD-drive path (HAVOC.INI [SETUP] DRIVE=D:\), and
returns a valid stream only when the local open fails and the CD-drive open
succeeds. With files present locally and no D: drive it returned NULL, and the
GRAFIX loader (0x444db0) dereferenced that NULL without checking.

Fix: 14-byte patch at FO 0x29a2b rewrites the return decision to hand back the
successfully-opened local object. The game now boots to the title screen
("HAVOC(tm) by Reality Bytes"), responsive, main loop running.

- docs/PATCHES.md: full patch table (28 patches) + crash write-up
- tools/: RE + patching scripts (r2pipe disasm, minidump parser, ctypes
  debugger, PE/IAT/import analysis)
- run_probe.bat / run_and_log.bat: reliable native launch for repro
- .gitignore: exclude CD images, Ghidra install/project, dgVoodoo, *.ini

Diagnosed via WER minidumps (%LOCALAPPDATA%\CrashDumps) parsed in pure Python
(no cdb/windbg available).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN4Ytn3gdWRonNmHpisWQv
2026-07-03 09:29:59 -07:00
534c41af73 docs: add full RE methodology for blog post
Documents the process (not just the results) for each format cracked:
- Binary identification: why 32-bit PE not 16-bit, what actually broke
- .FF archive discovery: speculative parsing from file count heuristic
- MUSIC.FF playlists: big-endian payload in LE container, segment ID math
- INTRFACE.FF bitmaps: BitB/BitR/XPAL system, index-2 black patch discovery
Includes tools, dead ends, corrections (2D->3D), and what's next.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 00:29:50 -07:00
acd4057e72 feat: crack INTRFACE.FF bitmap format + render all UI assets
BitB/BitR/XPAL/STR# system fully reverse-engineered:
- BitB<id>.dat: big-endian uint16 width/height header
- BitR<id>.dat: raw 8-bit palette-indexed pixels (no header)
- XPAL<id>.dat: 256 RGB triplets, 8-bit per channel
- STR#<id>.dat: length-prefixed UI string tables
- Runtime patch: index 2 must be forced to black (0,0,0) --
  the game engine overwrites this at runtime but stored values vary

Discovery: XPAL7A12 (correct render) vs XPAL7A1C (salmon background)
differ in only 3 palette entries (0,1,2); index 2 is the canvas black.

All 32 full-screen menu backgrounds, 57 vehicle HUD icons, cockpit
windshield frames, vehicle selection screens, and button strips now
render correctly. Documented in docs/FORMATS.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 00:25:08 -07:00
aad222fd09 chore: gitignore local .claude/ and third-party reference screenshots
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:38:01 -07:00
2287f3be12 Initial commit: Havoc preservation toolkit + format docs
- ff_extract.py: .FF (FlashFile) archive extractor with music song
  assembly (WAVE+PLST), ogg/mp3 conversion, and FL Studio pack export
  (samples + per-song MIDI + mapping).
- docs/FORMATS.md: reverse-engineered file formats (.FF, WAVE/PLST music,
  world data + binary notes).
- Original game data is gitignored (copyright Reality Bytes, 1995).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:33:30 -07:00