havoc-remaster/docs/FUNCTIONS.md
pyr0ball 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

19 KiB

Havoc Function Tracker

Progress tracker for reverse-engineered functions in HAVOC_NOCD.EXE, kept in git so it's diffable and gives an honest "how much of this binary do we actually understand" number for the blog post. This is a curated list of functions worth naming/understanding, not a full dump of all ~987 functions Ghidra's auto-analysis found.

Status legend:

  • unanalyzed -- known to exist (an address we care about), not yet decompiled/read
  • analyzed -- decompiled and read, behavior understood, still has a Ghidra default FUN_* name
  • named -- renamed in the Ghidra project (tools/ghidra_project/) to reflect what it does
  • documented -- named and has a writeup in one of the other docs/*.md files

Update this table whenever a function moves between these states. VA = virtual address (runtime address with relocations applied; see docs/HANDOFF.md section 5 for FO->VA formulas per section).

Boot / init / main loop

VA Name Status Notes
0x464657 CRT entry point documented See docs/HANDOFF.md section 5
0x407C90 WinMain analyzed Allocs game object, calls init (0x434040), then main loop (0x43AC40)
0x434040 main init analyzed Allocs ~30 subsystem objects, loads title/GAMIMAGE, palette fade, calls 0x418600
0x418600 DirectDraw + archives + sound init unanalyzed GRAFIX/world files load during this
0x43AC40 main_game_loop (vtable[7]) named State machine, state byte at [this+0x18]. No frame limiter -> FPS-coupled. See docs/METHODOLOGY.md section 10
0x436170 per_frame_vtable8 named Calls frame_tick_counter_while_playing then conditionally two more vtable calls gated on state/flags
0x4361d0 per_frame_vtable9_state_machine named Sets the 60Hz tick baseline (DAT_0047e56c) on first call; handles state transitions 0x47/0x49/0xa1
0x43adc0 msg_pump (vtable[17]) named Linked-list event queue drain, dispatches via [this]+0x3c
0x40C8E0 busywait_frame_limiter_60hz named ~60/sec via timeGetTime; used in palette-fade / state transitions, NOT the gameplay loop
0x40CAE0 state_transition_with_limiter named Palette-fade / state-transition context that calls the limiter
0x418870 frame_tick_counter_while_playing named Calls dsound_stream_pump then, if state==0x47, increments a counter at this+0x158

FPS-coupling investigation (docs/HANDOFF.md section 7.1)

VA Name Status Notes
0x0047da88 (data) game/window object ptr analyzed Same object as this in the main loop 0x43AC40 -- confirmed by reading its vtable at 0x4791A0 and matching slots 7/8/9/17 to the known main-loop callees. Assigned once in FUN_00426560 (a window-init routine: ShowWindow/UpdateWindow/SetWindowLongA, offset +0x943 = HWND)
offset +0x92a CORRECTED: common base "Window" class active/shown flag, not cockpit/HUD-specific analyzed Scanned all instructions program-wide for this offset (ScanForOffset.java, since it's a struct offset not a fixed global address) -- found ~29 distinct functions across a huge address range (0x415262 to 0x4624e0) reading/writing it. This is a shared base-class member used by many menu/dialog/window objects throughout the UI system, not specific to the top-level game object or to cockpit/HUD mode. Retracts the earlier "possible cockpit/HUD-mode flag" guess
0x426170 / 0x4264d0 / 0x426470 base Window class Init / Show / Hide analyzed Found clustered together (same class impl). 0x426170: constructor-like, zeroes a large field block, sets +0x92a=0. 0x4264d0 (Show): calls vtable[31] (+0x7c) then sets +0x92a=1. 0x426470 (Hide): sets +0x92a=0, calls 0x418a30, conditional sub-object cleanup. This is the base class every menu/dialog screen likely inherits -- relevant to the keyboard-reassignment graphics bug investigation below
vtable slot 0x54 (21, 0x4188c0) on the base vtable 0x4791A0 base-class default stub, likely NOT a bug on its own analyzed Still just MOV AX,1 / RET on the base class. Given +0x92a is now known to be a common "active" flag checked by many classes, this being a no-op default for the plain base/top-level window is plausible and not necessarily wrong -- the real per-menu-screen override would live on each menu's own derived vtable, not this one. Not re-litigated as the FPS-coupling mechanism (still correctly ruled out for that), but also not confirmed as unrelated to the keyboard-menu bug -- would need to find the keyboard-reassignment dialog's own object/vtable specifically, not the top-level game object's
0x418870 frame_tick_counter_while_playing named Calls dsound_stream_pump() unconditionally, then if state==0x47 (playing) increments a counter at this+0x158. Not the physics update itself
0x40a3f0 dsound_stream_pump -- dead end for physics named References "DSound Error: SoundBuf->Lock fail..." / "...BackgroundBuf->Lock..." strings and Lock/Unlock-style vtable calls. This is double-buffered audio streaming, not physics
0x40ecb0 player_turn_smoothing_substep (called only while state==0x47) named Takes DAT_0047c488 (see below) and runs a proportional-smoothing loop DAT_0047c488 times on a value at param_1+0xbd/+0x121 (steering/turn smoothing?). Confirms DAT_0047c488 is consumed as a sub-step count, i.e. treated as elapsed ticks
0x40e980 player_cooldown_timers_tick (called only while state==0x47) named Subtracts DAT_0047c488 from several countdown timers on the player/vehicle object (param_1+0x1a1/0x1a5/0x1a9/0x1ad -- cooldowns/animation timers), firing side effects (sound events via DAT_0046ea30/DAT_0047c038 vtables) when they expire
0x4092d0 compute_frame_delta_ticks_60hz_min1_clamp -- THE key find named DAT_0047c488 = FUN_004092d0() is called once per frame from 0x40e980. Converts timeGetTime() (ms) to a 60Hz tick count (ms*60/1000), diffs against the stored value from last call, returns the tick delta. Min-1 clamp: if (iVar2 == 0) iVar2 = 1; -- if the game is rendering faster than 60fps, less than one 60Hz tick elapses between calls (delta rounds to 0), but the function clamps the minimum return value to 1 tick anyway. Net effect: above 60fps, simulation time is credited faster than real time, in direct proportion to render rate -- a clean, deterministic linear speedup, not glitchy/non-deterministic breakage (confirmed by the user: "physics does not break above 60fps, it's deterministic and doesn't seem to behave any differently than 'FASTER'" -- exactly what this clamp predicts). At or below 60fps the delta is accurate (real elapsed ticks, never hits the clamp), which is exactly why the dgVoodoo FPSLimit=30 workaround works. This is a real per-frame delta-time system with one specific min-clamp causing the speedup, not a naive "no delta timing at all" -- worth correcting the docs/HANDOFF.md characterization of "no frame limiter = FPS-coupled" to the more precise "has delta timing, but a >60fps min-tick clamp makes simulation speed scale with framerate above that threshold"
0x0047c488 (data) per-frame delta value (ticks) analyzed Recomputed by 0x4092d0 each call; consumed by 0x40ecb0 (sub-step loop count) and 0x40e980 (timer decrement amount)
0x435610 (vtable slot 12) unanalyzed unanalyzed Real function (not a stub) on the same vtable, unexplored
0x437370 (vtable slot 15) unanalyzed unanalyzed Real function (not a stub) on the same vtable, unexplored
Open question, REVISED cockpit ("normal") vs. full-viewport ("maximized") speed difference unanalyzed User testing (2026-07-03) with FPSLimit=0 + [DirectX] ForceVerticalSync=true (both views now genuinely vsync-locked to the same real 60fps): "normal" now runs at exactly the correct speed; "maximized" runs at exactly 2x normal's speed -- and the 2x ratio is unchanged from before the dgVoodoo tuning (only "normal"'s absolute speed was fixed by the config change). Since both views are confirmed at the same actual framerate now, the render-cost/"naturally throttled below 60fps" theory from docs/PATCHES.md cannot explain a persisting 2x gap -- framerate parity should mean parity in the compute_frame_delta_ticks_60hz_min1_clamp clamp behavior too, since neither view exceeds 60fps anymore. This points instead to a genuine code-level difference: something in the full-viewport/"maximized" path likely calls the per-frame update/tick machinery (player_cooldown_timers_tick/player_turn_smoothing_substep, or whatever drives them) an extra time per rendered frame, independent of actual framerate. Not yet located -- the +0x92a flag and its vtable-slot-21 gate (still just a base-class stub on the object checked so far) remain a lead, but given the flag is a generic base-class field used by ~29 unrelated functions, the real mode-switch code is more likely in whatever toggles between the cockpit-dashboard render path and the full-viewport render path (bound to the + key) -- not yet found. Do not re-attempt the previous FPS patch approach (min-clamp removal) to explain this -- that was a different, already-reverted-as-unsafe mechanism (see "FPS-coupling" section in docs/PATCHES.md) and doesn't explain a framerate-independent 2x multiplier anyway

Alt-tab / focus-loss investigation

Triggered by a real symptom report: menu appears to "reset" when alt-tabbing out and back in while running via dgVoodoo2. Also connects to a second observation: the game renders as a borderless-but-resizable window, not true OS fullscreen or a titled window.

VA Name Status Notes
0x40b5b0 wndproc_dispatch named Main WndProc. Handles WM_DESTROY (2, sets state=0xf0), WM_ACTIVATEAPP (0x1c), WM_SETFOCUS-family (7/0xf), plus DirectDraw palette/mouse messages (0x201/0x202/0x203) and custom app messages 0x401-0x405
0x436070 on_deactivate_force_pause_state (vtable slot 13 / 0x34) named On WM_ACTIVATEAPP(0): skips if state==0xf0 (exiting). Otherwise backs up current state to DAT_0047e578, force-sets state to 0xB0 (pause), calls 0x40a8b0, a vtable[23] call on a secondary object, then state_transition_with_limiter
0x4360e0 on_reactivate_restore_state (vtable slot 14 / 0x38) named On WM_ACTIVATEAPP(1): correctly restores the state backed up in DAT_0047e578. Calls ddraw_full_display_reinit, then vtable[30]/[34] calls on a secondary object, conditionally 0x40a910, then dsound_stream_pump, then state_transition_with_limiter. The state machine itself is NOT the bug -- it round-trips cleanly
0x40bd30 ddraw_full_display_reinit named The actual heavy-lifter. Full DirectDraw teardown/rebuild: SetDisplayMode, CreateSurface (primary+back, with retry loop on DDERR_SURFACELOST/DDERR_WASSTILLDRAWING), Lock, CreatePalette. This is the exclusive-fullscreen "recreate everything after losing the display device" path, not a lightweight IDirectDrawSurface::Restore(). Explains both symptoms: (1) the visible "reset" flash on alt-tab-back-in is a real full surface recreation, and (2) it only makes sense if the game requested DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN -- which is consistent with dgVoodoo2 presenting it as a borderless-but-resizable window rather than a normal titled window (dgVoodoo's own rendering of a wrapped exclusive-fullscreen DirectDraw app)
Conclusion, PARTIALLY REVISED (two distinct symptoms, not one) state machine confirmed not buggy; two separate "reset" triggers exist -- Internal game state survives alt-tab correctly at the state-machine level (confirmed originally, still true). Two different real-world symptoms both got called "resets to menu" during testing, and they have different causes: (1) alt-tab while paused, staying away/paused too long -- this was the pause-loop message-queue hang (see docs/PATCHES.md "Pause-loop hang"); confirmed fixed by HAVOC_NOCD_PAUSEFIX_v1.EXE (2026-07-03 test: alt-tab away while paused, return, unpause -> continues normally). (2) alt-tab while NOT paused -- user confirmed (2026-07-03, same session, same HAVOC_NOCD_PAUSEFIX_v1.EXE binary) this STILL resets to the menu on refocus, so it is a genuinely separate, still-unresolved issue -- the pause-loop fix did not touch it. Since the WM_ACTIVATEAPP handlers (on_deactivate_force_pause_state/on_reactivate_restore_state) run unconditionally on every alt-tab regardless of pause state, and we already confirmed those two functions round-trip the state variable correctly, the cause of symptom (2) is still open -- likely either in ddraw_full_display_reinit's surface recreation actually failing to restore the correct visual content (not just the state int), or some other object beyond the one game-state variable we've traced so far. Do not assume this is resolved -- re-open investigation if picked back up
Tested mouse input in menu, fresh launch vs. post-alt-tab -- User confirmed (2026-07-03): mouse responds correctly in the menu both on fresh launch and after alt-tabbing. An earlier report of "menu doesn't respond to mouse" was transient/not reproducible -- not a real regression from the ddraw.dll permission fix, the FPSLimit/ForceVerticalSync tuning, or this investigation
Live-confirmed (2026-07-04) state transition 0x47->0xB0 on alt-tab, MCP debug agent -- Hardware watchpoint on game_object+0x18 (runtime addr 0x2361064) fired during active gameplay (state==0x47). Write at 0x4360A7 (MOV [ECX+18h], 0xB0) inside on_deactivate_force_pause_state. At stop: eax=0x47 (old state just read), esi=0x1c (WM_ACTIVATEAPP - exact match), DAT_0047E578=0x47 (prev-state global saved as expected), game_object+0x20=0x47 (target state still 0x47). State machine round-trips correctly - confirms existing static analysis. The still-unresolved symptom-2 (reset to menu on re-focus during gameplay) is not in this state write; it lies further along in on_reactivate_restore_state/ddraw_full_display_reinit.

Keyboard-reassignment menu graphics bug (first genuine bug found this session)

Symptom: on the "Change Keyboard Settings" screen, the dynamic elements (the list of game functions + their currently-assigned keys, since the static menu chrome loads fine) flash for a fraction of a second on menu load, then disappear and never redraw.

Confirmed the string table for this screen: INTRFACE.FF resource STR#7A62 (tools/out_intrface/raw/STR#7A62.dat): "Press a key to assign to selected game function." / "Cancel" / "Save Settings" / "Restore Default Settings". The parent Options menu is STR#7A44: "Change Game Settings" / "Change Viewscreen Settings" / "Change Keyboard Settings" / "Change Joystick Settings" / "Return to Main Screen".

Initial hypothesis (retracted, see the +0x92a correction above): thought the per-frame vtable[21] stub on the top-level game object was the missing dynamic-redraw call. Scanning showed +0x92a is a common base "Window" class active-flag used by ~29 different functions across the codebase -- not specific to this menu. The stub at 0x4188c0 is the base class's default and is plausibly correct/intentional there; it doesn't mean every derived window's slot 21 is a stub.

Progress: found the Options-menu construction function as a template. 0x4596c0 (scanned via the 0x7a44 resource-ID search) builds the whole "Change Game/Viewscreen/ Keyboard/Joystick Settings" screen: allocates a button widget per string ID starting at 0x7a45 (looping, each with its own vtable -- PTR_LAB_004795b0, PTR_LAB_004794e0, PTR_FUN_00479610, PTR_LAB_00479448, PTR_LAB_00479478, PTR_FUN_00479070 are all distinct widget-subclass vtables: buttons, labels, sliders), sets a title via 0x426240 (likely SetTitleString(resourceId)), then finishes with FUN_00426240(param_1, 0x7a44) followed by FUN_004264d0(param_1) -- that's the base-class Show method we identified, called on the newly-built screen object itself. Confirms the general pattern: every menu screen is its own object instance with its own per-widget vtables, constructed by a dedicated "build this screen" function, finished off by the common Show.

Also found 0x458350, a widget message handler that references 0x7a62 (computing a string-ID offset based on a selection index for what looks like a status/tooltip line, matching STR#7A44's "Move the mouse over a button for a description of its function.") -- likely a shared button/widget message handler reused across multiple screens, not specific to the keyboard-reassignment screen's own construction.

Still open, next step: find the keyboard-reassignment screen's own construction function -- the equivalent of 0x4596c0 but building buttons/labels from string IDs starting around 0x7a63 (the item after STR#7A62's header string) instead of 0x7a45. Once found, check whether its per-widget-type vtables (for whatever draws the dynamic key-binding list specifically) have a real per-frame update/redraw implementation or a stub -- that's where the actual bug should surface. Given "flash once then disappear" (not "never drawn at all"), the initial static draw works -- something about the per-frame redraw path for this screen's dynamic list specifically is what's missing or broken.

Copy-protection / file loading (docs/PATCHES.md)

VA Name Status Notes
0x0042a480 WORLDS\ data-file loader documented The copy-protection check function; patched. See docs/PATCHES.md
0x0042a880 fn_init_file_objects analyzed
0x0042db00 delete_savegame analyzed DeleteFileA wrapper
0x0042dab0 strcpy_to_obj analyzed
0x0042d920 error_display_6 analyzed
0x0042d970 show_error_dialog documented Patched to RET (Patch N3) to silence popups
0x00438d80 get_strdata_string analyzed
0x004274e0 alloc_obj analyzed
0x00444db0 GRAFIX0N.DAT loader documented Byte-swaps BE records; template for other WORLDS loaders. See docs/FORMATS.md

Not yet started (from docs/HANDOFF.md section 7)

Target Status Notes
Cheat code handler (mmm/vvv/sss/hhh/aaa) unanalyzed Find the keyboard-buffer matcher
Weapon system (6 primary + 14 secondary) unanalyzed See docs/WEAPONS.md for the string-table side
Player/vehicle physics unanalyzed
Enemy AI unanalyzed
MAP/LAND/STUF loaders unanalyzed Also finishes the asset-cracking stream

How to update this

  1. Decompile with the headless scripts in tools/ghidra_scripts/ (e.g. DecompileFPS.java, DecompileHavoc.java -- extend their TARGET_VAS arrays, or add a new script for a new investigation topic).
  2. Read the output in tools/ghidra_decompile*.txt, update the row's status here.
  3. If you rename a function in the Ghidra GUI/project, bump status to named here too.
  4. If it gets a real writeup elsewhere in docs/, bump to documented and link it.