Port DDDisplay__BlitStippled (per-pixel checkerboard XOR pattern) and
DDDisplay__BlitTiled (horizontal sprite tiling) to pure Rust, eliminating
fallthrough to the original WA code for these modes.
Slot 19 (BlitSprite) now only falls through for bitmap sprites.
Slot 20 (DrawScaledSprite) has no fallthrough at all.
Adds 6 snapshot tests (4 stippled + 2 tiled) validated against WA ground
truth, and consolidates snapshot capture into a single OPENWA_CAPTURE_SNAPSHOTS
env var.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port all remaining 8bpp blend modes in the core sprite blit hook:
- Mode 1 with non-null color_table pointer now passes the LUT through
instead of falling back to native WA
- Mode 2 (additive mask): write src where both src and dst are non-zero
- Mode 3 (subtractive mask): write src where src non-zero and dst is zero
- Update color_table parameter type from u32 to *const u8 (FFI style)
- Snapshot capture now uses ORIG_BLIT to call unhooked native function
34 tests (up from 25): 5 new unit tests + 4 new snapshot tests verified
byte-exact against native WA output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port core sprite blit with snapshot tests and hook
Port BitGrid__BlitSpriteRect (0x4F6910) — the core sprite/bitmap
blitting function used for both rendering and collision mask
construction. Hooked via MinHook with fallthrough to original for
unsupported modes.
Sprite blit module (sprite_blit.rs):
- Direct copy (mode 0) and color-table/transparency (mode 1)
- 8 orientation modes (mirror X/Y/XY, 90° rotations)
- Destination clip rect support
- GIF test image loader with transparent index normalization
18 snapshot tests captured from WA's native function verify
pixel-perfect output. All 15 headless replay tests pass (simulation
checksums match), confirming correctness for collision masks.
Also ports DDDisplay::is_sprite_loaded (slot 32) and
DDDisplay::draw_via_callback (slot 21), bringing ported DDDisplay
methods to 13 of 38. Splits DisplayBase _buf_008 into named
sprite_array_0/1/2 fields.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port polygon fill with Sutherland-Hodgman clipping, 26 snapshot tests
Add PixelGrid (pure-Rust test grid) and PixelWriter trait for unit
testing line algorithms without WA runtime. Add snapshot capture in
DLL (OPENWA_CAPTURE_LINE_SNAPSHOTS=1, triggered at frame 10) that
draws test lines on a BitGrid using WA's native functions and saves
pixel data for comparison.
Also fix headful test crash: remove --minimized flag which became
incompatible with WA's DirectDraw initialization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
chore: replace multiple shotgun / longbow replays with much longer combined replay
Root cause: the launcher's hooks-ready event had a 10s timeout. Under
high concurrency (j=16), DLL loading + hook installation exceeds 10s
due to I/O contention, causing the launcher to resume WA.exe's main
thread before all hooks are installed. The FireWeapon trap then fires
because WeaponRelease (last hook in the chain) was never installed.
Fix: increase hooks-ready timeout to 120s (matching test runner timeout).
Also:
- Redirect all temp files to per-PID .openwa_tmp/{pid}/ directory
instead of individual per-PID suffixed filenames. Now covers
writetest.txt, mono.tmp, custom.dat, current.thm, playback.thm
in addition to existing land.dat, landgen.svg
- Clean up temp directory on DLL detach and in test runner
- Remove 200ms test stagger (no longer needed with proper isolation)
- Add .gitignore for replay output logs (only *_expected.log tracked)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
core_play_sound stored flags_and_slot (sound ID) in desc.flags_b instead
of priority (flags). This made all channels appear high-priority (IDs like
55, 121) so new sounds with real priority (1-8) could never evict them.
Channels accumulated until all 8 were permanently occupied, blocking all
subsequent pooled sounds (explosions, sizzle, etc.).
Also fixed stop_channel to unconditionally return pool entries to free list
even when the descriptor was already evicted by allocate_channel. WA does
this; without it, evicted pool entries leak and pool_free_count drops to 0.
Also fixed stop_channel used-list removal to use swap-remove (matching WA)
instead of shift-down.
Added sound_priority replay test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct SharedData entity type and rename all SpecialFireSubtype/WormState
Port FireWeapon__Freeze (0x51E920) — plays freeze sound at worm position,
creates landscape freeze overlay via PCLandscape vtable[5], and increments
WormEntry counters (turn_action +3, effect_counter +10).
Sound positioning uses the DDGame sound queue entry directly after
PlaySoundGlobal queues the sound effect.
Add freeze_nuke_armageddon replay test (2× Freeze, plus Nuke/Armageddon).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port weapon fire sub-functions to Rust — Drill, Prod, vtable calls
Port the core weapon firing functions from WA.exe to Rust:
- CreateWeaponProjectile (0x51E0F0): full replacement — allocates CTaskMissile
(0x40C bytes), SharedData lookup for parent CTaskTurnGame, pool capacity
check, calls original constructor. All projectile weapons flow through this.
- ProjectileFire (0x51DFB0): full replacement — advances game RNG, computes
random spread angle via Fixed16.16 trig table interpolation, applies 2D
rotation matrix to template velocity, calls ProjectileFire_Single per shot.
Used by Uzi, Handgun, Minigun.
- CreateArrow (0x51ED90): full replacement — allocates CTaskArrow (0x168 bytes),
same pool/SharedData pattern as CWP. Used by Shotgun and Longbow (which fire
physically simulated arrow entities, NOT hitscan).
Also adds:
- WeaponSpawnData struct (11 DWORDs) replacing raw [u32; 11] in CTaskMissile
- CTaskArrow struct (0x168 bytes) with vtable at 0x664198
- CTaskMissile size corrected from 0x41C to 0x40C (matching actual allocation)
- Pas…
fix: pad team names in replay log to match original WA formatting
The original WA pads quoted team names to the longest name width so
that [CPU X.XX] brackets align across all teams. Our replay loader
was missing this padding, causing log comparison failures with 6-team
replays where team names have different lengths.
Also adds bots2 replay test (6-team CPU torture test with custom scheme).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port Surrender and Mail/Mine/Mole weapon handlers to pure Rust
Port two FireWeapon sub-handlers from WA bridge calls to pure Rust:
- Surrender (0x51E600): SharedData lookup + HandleMessage(0x29) + WormEntry counter
- Mail/Mine/Mole (0x51E670): version-conditional vtable call + HandleMessage(0x28) + counter
Fix replay loader V2 scheme bug: our code was clearing super weapon data
(memset 0x4C) for both V1 and V2 schemes when header < 0, but the original
WA only clears for V1. V2 schemes already contain valid super weapon data.
Other changes:
- Name WormEntry fields: effect_counter_04_Maybe (+0x04), turn_action_counter_Maybe (+0x08)
- Add TeamArenaRef::team_worm_mut() for mutable worm entry access
- Add mail_mine_mole replay test (Mail Strike, Mine Strike, Mole Squadron, Surrender)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
test: add longbow_water and longbow_alt_theme replay tests
Two new replay files for regression testing:
- longbow_water: longbow fired at water (no terrain collision)
- longbow_alt_theme: longbow on a different theme (non-zero map_seed)
Expected logs generated from unmodded WA.exe 3.8.1 (Steam).
These replays exposed hook-level bugs (FireWeapon, ReplayLoader)
that the original longbow replay didn't trigger.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents the debug server, CLI commands, frame breakpoints,
snapshot system, and key env vars (OPENWA_DEBUG_SERVER,
OPENWA_BREAK_FRAME, OPENWA_USE_ORIG_CTOR, OPENWA_WATCH_FRAME).
Also adds openwa-debug-cli and openwa-debug-proto to crate list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
debug: add OPENWA_WATCH_FRAME for runtime hardware watchpoints
Arms DR0 on DDGame+0x50 at the specified frame via the existing VEH
watchpoint infrastructure. Set OPENWA_WATCH_FRAME=0 to arm from the
first frame.
Key finding: DDGame+0x50 is a sprite pointer at frame 1350, NOT a
small integer. The earlier raw-dump comparison was flawed (cross-
process pointer values compared without ASLR normalization). The real
desync is in sub-object content, not in DDGame field values.
Also adds shotgun replay (PASSES with both constructors, confirming
the desync is longbow-specific, not arrow-handler-specific).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recorded from unmodded WA 3.8.1. Exercises Longbow (subtype_38=4)
and Dynamite. Expected log from vanilla game serves as ground truth.
Note: currently shows checksum mismatches at frames 1350/2186/2249/2775
due to a pre-existing bug in one of the other hooks (proven by
disabling FireWeapon hook entirely — mismatches persist). Needs
bisection to identify the offending hook.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add headless mode with launcher↔DLL event synchronization
Add OPENWA_HEADLESS=1 mode that hooks user32!MessageBoxA/W via MinHook
to auto-dismiss all message boxes, enabling fully unattended /getlog
replay processing.
Add named event synchronization (OpenWA_HooksReady) between the launcher
and DLL: launcher creates the event before injection, DLL signals it
after install_all() completes, launcher waits before ResumeThread. This
eliminates the race between hook installation and main thread startup.
Add bots_expected.log as golden reference for headless replay log diff
testing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: reduce auto-capture wait to 5s, add skill and replay files