Port IMG_Decode (0x4F5F80) and DisplayGfx__Constructor (0x4F5E80) to
pure Rust, eliminating the last transmute bridges in the asset loading
pipeline. img_decode returns Option<DecodedBitGrid> with Display/Collision
discrimination; img_decode_cached always returns *mut DisplayBitGrid.
- Add asset/img.rs with img_decode and img_decode_cached
- Replace transmute bridges in gfx_dir.rs, ddgame_constructor.rs
- Hook IMG_DECODE and IMG_DECODE_CACHED for remaining WA callers
- Type DDGame fields: gradient_image, gradient_image_2, arrow_sprites
as *mut BitGrid instead of *mut u8
- Rename GfxResource__Create_Maybe → IMG__LoadFromDir (Ghidra + Rust)
- Rename DisplayGfx__Constructor_Maybe → IMG__DecodeCached (Ghidra + VA)
- Propagate Option<DecodedBitGrid> through call_gfx_find_and_load
and call_gfx_load_and_wrap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: port GfxDir resource loading system to Rust
Port the entire .dir archive I/O layer — GfxDir vtable (read, seek,
load_cached, release), GfxDirStream vtable (all 6 slots), LoadImage,
cache slot management, and cleanup — eliminating all WA.exe asm bridges.
- Move gfx_dir module from render/sprite/ to new asset/ module
- Type cache slots as GfxCacheSlot struct, rename _unknown fields
- Convert GfxDirStreamVtable to #[vtable] macro with bind_! wrappers
- Replace call_gfx_load_dir/call_gfx_load_image bridges with direct calls
- Add WA_FSEEK address, GFX_DIR_STREAM_VTABLE address
- Fix GfxDirStream.gfx_dir field type from *mut u8 to *mut GfxDir
- Rename GfxDirEntry._unknown_08 to data_size
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: port BufferObject ctor, replace more bridges with typed calls
- Port DualBufferObject__Constructor (0x545FD0) to Rust: the original
function was usercall with an implicit EDI register carrying the
second buffer's capacity. Traced the disassembly to find the capacity
is 0x2DC or 0x46C depending on game_info version fields.
- Replace PaletteContext__Init bridges in ddgame_constructor.rs with
the Rust palette_context_init() ported in the previous commit.
- Replace call_display_gfx_dirty_rect raw vtable dispatch with typed
DisplayBitGrid::fill_rect_raw(), using named struct field access
instead of raw pointer offset arithmetic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: typed vtables, port PaletteContext__Init and GameStateStream__Init
Eliminate ~120 raw pointer arithmetic expressions (`ddgame_raw.add(offset)`,
`w.add(offset)`) in favor of named struct field access through typed pointers.
- Split DDGameWrapper._unknown_0f4 into 7 named team scoring arrays
- Split DDGameWrapper._unknown_2b4 to expose team_score_array_6
- Retype DDGameWrapper._zeroed_054 to team_task_ptrs: [*mut u8; 13]
- Split DDGame._unknown_77ac to add _field_77b0
- Change all function signatures from *mut u8 to *mut DDGameWrapper/*mut DDGame
- Replace wa_malloc + write_bytes with wa_malloc_zeroed (~12 sites)
- Propagate typed pointers (DisplayGfx, DSSound, PCLandscape) to vtable calls
- Update DLL trampoline shims to cast to typed pointers
- Remove unused helper parameters (_ddgame_raw, _wrapper)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: type CoordEntry fields as Fixed and eliminate pointer arithmetic in level_bounds
Retype CoordEntry fields from raw i32 to named Fixed center_x/y and
center_x_target/y_target. Extend viewport_coords array to 5 entries to
cover the init spill. Retype init_game_state_level_bounds param from
*mut u8 to *mut DDGame, removing all raw pointer arithmetic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: propagate types and named fields in game_state_init
Split _unknown blocks in DDGame (28 new fields) and GameInfo (17 new
fields) to replace raw pointer arithmetic with named struct access.
Retype game_speed, level bounds, and render_scale to Fixed. Change
game_info_sub parameters from *const u8 to *mut GameInfo across all
init sub-functions, eliminating the untyped pointer pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port DDGame__InitGameState (0x526500) to Rust
Full Rust reimplementation of the ~600-line game session initialization
function. Allocates ~20 sub-objects, initializes display layers, team
configuration, weapon tables, turn logic, random bags, and computes the
initial state serialization/checksum.
Key changes:
- game_state_init.rs: init_game_state() + 13 helper functions (~1400 LOC)
- ddgame_wrapper.rs: split _unknown blobs into ~60 named fields
- game_session.rs: replaced transmute bridge, added trap on original
- game_state_hooks.rs: removed passthrough hooks for usercall functions
(CTaskTurnGame, DisplayGfx__ConstructFull, ConstructTextbox,
DisplayObject, WeaponPanel) that drop ECX/EDX/ESI register params
- address.rs: added VectorNormalize addresses, renamed INIT_WEAPON_PANEL
Sub-constructors are called via typed bridges (transmute/fastcall/naked
asm). Already-ported functions (sprite_gfx_table_init, ring_buffer_init,
init_team_scoring, init_alliance_data, init_turn_state, etc.) are called
directly. Display layers use cor…
refactor: type set_active_layer return + propagate PaletteContext
Cascades a typed `*mut PaletteContext` return through the chain that
starts at `DisplayGfx::set_active_layer` (slot 5) and feeds the GfxDir
loaders.
- Slot 5 vtable signature retyped from `*mut u8` to
`*mut PaletteContext`. Doc-comment promoted from one line to
explain what the result is used for downstream.
- `gfx_resource_create` (`FUN_004F6300` port), `call_gfx_find_and_load`,
and `call_gfx_load_and_wrap` now take `display_ctx: *mut PaletteContext`
instead of `*mut u8`. The single internal `IMG_Decode` call still
casts back to `*mut u8` since `IMG_Decode` is WA-side.
- `compute_complex_gradient` (`render/display/gradient.rs`) takes
`layer3_ctx: *mut PaletteContext` and passes it through to
`call_gfx_find_and_load` without any casts.
- `wa_load_sprites` naked-asm bridge's `_layer_ctx` parameter retyped
to `*mut PaletteContext` to match what its only caller passes after
`set_active_layer`. The naked asm body is unchanged — pointer types
…
refactor: propagate type info through display vtable.rs and friends
A series of small type-safety improvements that compound to delete a
lot of raw byte arithmetic and ad-hoc transmutes:
- `LayerSpriteFrame.bitmap_vtable` retyped from `u32` to
`*const c_void`, and `LayerSpriteFrame.surface` from `u32` to
`*mut Surface`. The two existing consumers (the slot 0 destructor's
`layer_sprite_frame_destructor` and the `load_sprite_by_name` /
`free_layer_sprite` pair) now access these via named fields
instead of raw `elem.add(0x08)` / `elem.add(0x0C)` arithmetic.
- `SurfaceVtable::set_color_key` (slot 7) added as a typed binding,
matching the existing `lock_surface` / `unlock_surface` /
`init_surface` / `release` slots. `load_sprite_by_name`'s per-frame
surface init loop now uses the typed binding for slots 3, 4, 5, 6,
7 instead of hand-rolled raw transmutes — drops ~50 lines.
- `RenderContextVtable::lock_surface_write`'s `out_data` retyped
from `*mut u32` to `*mut *mut u8`, and `unlock_surface_write`'s
`p2: …
refactor: fix DDGameWrapper size to 0x6F10 and remove WABox
DDGameWrapper had an off-by-16 size assertion (0x6F00 vs actual allocation
0x6F10); the 0x20-byte tail is unzeroed unknown padding. Fixed struct field
and size assertion.
WABox never had a Drop implementation so it was purely a leak-by-design
wrapper with no safety value. Remove it entirely and replace all uses:
- WABox::alloc(...).leak() → wa_malloc_struct_zeroed::<T>()
- WABox::from_value(val).leak() → wa_malloc_struct::<T>() + ptr::write(p, val)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: replace inline thiscall transmutes with typed GfxDirVtable pattern
Add GfxDirVtable via #[vtable] macro (4 slots: read/seek/load_cached/release)
and GfxDirStream::total_size_raw (slot 4, formerly _slot_4). Replace all
inline transmute vtable calls across gfx_dir.rs, ddgame_constructor.rs, and
font.rs with typed _raw methods generated by bind_GfxDirVtable!. Change
call_gfx_load_image return type to *mut GfxDirStream. Rename GfxHandler and
GfxDirStream vtable functions in Ghidra accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: extract font module, type GfxDir pointers, rename DisplayVtable
Three concurrent cleanups bundled together:
- Extract the font implementation (~830 lines) from display/vtable.rs
into a new display/font.rs module: Font struct (renamed from
FontObject), font_extend, font_set_palette_impl, font_get_info_impl,
font_get_metric_impl, font_set_param_impl, font_load_from_gfx, and
GlyphEntry. vtable.rs is now 870 lines lighter and only contains
vtable definitions and slot wrappers.
- Replace `*mut u8` with typed `*mut GfxDir` across the GfxDir call
graph: LayerSprite.gfx_dir, PCLandscape.{level,water}_gfx_dir,
DDGameWrapper.{primary,secondary}_gfx_dir, slot 31 load_sprite,
slot 34 load_font, slot 37 load_sprite_by_layer,
gfx_dir_find_entry, gfx_resource_create, call_gfx_find_and_load,
call_gfx_load_and_wrap, call_gfx_load_image, plus the matching DLL
trampolines and the inner transmuted vtable[2] cached-load function
pointer in three places. Also renames the misleading local names
`land_layer`/`water_layer` to `land_dir`/`water_d…
refactor: reorganize display/ and render/ into unified render/ module
Merge the display/ and render/ sibling modules into a single render/
module with display/, sprite/, and ddraw/ sub-modules. Pure mechanical
refactor — file moves and import rewrites only, zero logic changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: eliminate DDDisplay, make DisplayBase generic, rename DDDisplayWrapper
- Make DisplayBase<V: Vtable> generic over vtable type (like CTask<V>, BitGrid<V>)
- Eliminate DDDisplay wrapper type — DisplayGfx is now the direct this type
for all display vtable methods, removing constant as_gfx() casts
- DisplayGfx.base is now DisplayBase<*const DisplayVtable> (38-slot vtable)
- bind_DisplayVtable!(DisplayGfx, base.vtable) uses nested field path
- Rename DDDisplayVtable → DisplayVtable, dd_display.rs → display_vtable.rs
- Rename DDDisplayWrapper → RenderContext (manages framebuffer + dispatches
to renderer backends — the DD prefix was misleading since WA supports
multiple graphics backends), display_wrapper.rs → render_context.rs
- Update all address constants (DDISPLAY_* → DISPLAY_GFX_*, etc.)
- Rename 51 functions in Ghidra to match
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename TeamArenaState to TeamArena, use raw pointers for soundness
BitGrid is a core engine type used for both rendering (display layers)
and simulation (collision masks, terrain geometry). Move it from
display::bitgrid to crate::bitgrid to reflect its fundamental role.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: typed BitGrid variants with alloc constructors
Move BitGrid from task::bit_grid to display::bitgrid as its canonical
home. Delete the old module. Rename fields to match known semantics:
_unused_04 → external_buffer, _unused_1c/20 → clip_left/clip_top,
width_dup/height_dup → clip_right/clip_bottom.
Convert bit_grid_init to BitGrid::init with *mut BitGrid parameter.
Replace magic vtable address with va::BIT_GRID_VTABLE constant.
Remove redundant double memset. Remove dead TaskStateMachine class.
Rename DDGame::display_gfx → display_bitgrid (it's a BitGrid, not
DisplayGfx). Rename Sprite::display_gfx → bitgrid_vtable. Clean up
tsm variable names and stale re-exports throughout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: type BitGrid display-layer vtable (0x664144, 8 pixel ops)
Add BitGridDisplayVtable with 8 typed slots: fill_rect, fill_hline,
fill_vline, destructor, get/put_pixel (clipped and unchecked variants).
Consolidate with existing task::bit_grid::BitGrid — the display module
re-exports the same struct and adds the vtable definition. Replace old
DISPLAYGFX_VTABLE / DISPLAY_GFX_VTABLE constants with the macro-generated
BIT_GRID_DISPLAY_VTABLE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename openwa-wormkit crate to openwa-dll, output openwa.dll
The project hasn't been a WormKit DLL for a long time — it requires a
custom launcher. Rename the crate to openwa-dll with output openwa.dll
to match the rest of the naming convention (openwa-core, openwa-launcher).
Updates all references across Cargo.toml, Rust sources, PowerShell scripts,
CLAUDE.md, skill files, and docs. Replaces stale "wormkit hook/shim"
comments with accurate descriptions. Three remaining "WormKit" references
are legitimate (refer to the actual WA modding framework).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port DDGame__LoadFonts and DDGameWrapper__LoadFontExtension to Rust
Replaces the usercall bridge to 0x570F30 with pure Rust that loads 28
bitmap fonts, 23 font extensions, and sets the font palette via typed
DDDisplay vtable calls. Adds three new DDDisplay vtable slots (load_font,
load_font_extension, set_font_palette) and expands gfx_color_table from
9 to 11 entries, absorbing former _unknown_7330 as font palette data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port CTaskTeam__CreateWeatherFilter to Rust, extract wa_lcg
Port the cloud spawning factory (0x552960) that creates a CTaskFilter
and spawns CTaskCloud children with deterministic LCG randomization.
- Add rng.rs with shared wa_lcg() used by advance_rng, advance_effect_rng,
and cloud spawning (eliminates duplicated LCG constants)
- Port create_weather_filter in task/team.rs with naked asm bridge for
CTaskFilter constructor (usercall ECX=0x1B)
- Add Subscribe (slot 8) and SubscribeAll (slot 7) to CTaskFilterVTable
- Add CTASK_TEAM_CREATE_WEATHER_FILTER and CTASK_FILTER_SUBSCRIBE addresses
- Hook via MinHook in wormkit cloud.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: call load_wav directly instead of through vtable
Both load_effect_wavs and load_speech_wav were calling DSSound::load_wav
through the vtable indirection. Since load_wav is already a Rust function,
call it directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port DSSound_LoadEffectWAVs to Rust, rename DDGame__LoadFonts
Port DSSound_LoadEffectWAVs (0x5714B0) to pure Rust — iterates the SFX
name table at 0x6AF378, builds "data\wav\Effects\{name}.wav" paths, and
calls the already-ported DSSound::load_wav for each entry. Removes the
last WA bridge call from the audio init path in ddgame_constructor.
Rename DDGAME_INIT_SOUND_PATHS to DDGAME_LOAD_FONTS — the function at
0x570F30 loads .fnt font resources, not sound paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port Music streaming audio system to pure Rust
Replace WA's Music/StreamingAudio system with a pure Rust implementation
using hound for WAV parsing and DirectSound for streaming playback.
Music system (0x354-byte object, vtable 0x66B3E0):
- Unified Music + StreamingAudio into single struct with full field map
- 6-slot vtable fully replaced via vtable_replace! macro
- Constructor hooked (0x58BC10), destructor uses wa_free
- Playlist management, track ID-to-filename mapping (1-13)
- Volume control with dB lookup table, using Fixed type
Streaming engine:
- WAV streaming via hound + BufReader<File> (replaces mmio API)
- DirectSound double-buffered ring buffer with timeSetEvent refill
- Buffer size matches original formula (timer_delay * avg_bytes/1000)
- advance_track guards against premature track changes (checks
IDirectSoundBuffer::GetStatus before advancing)
Sound hooks:
- Hook PlayWormSound, StopWormSound, LoadAndPlayStreaming
- Remove old streaming.rs stub, update GameSession to use *mut Music
Co-Authored-By: Claude Opus 4.6…
chore: Add new DDGame & GameInfo fields based on game state init functions
Fully port play_worm_sound/play_worm_sound_2 — the streaming sound
playback chain no longer calls into WA.exe via naked asm bridges.
New Rust implementations (sound_ops.rs):
- distance_3d_attenuation: stereo panning with i64 fixed-point math
- compute_distance_params: reads listener pos + attenuation from DDGame
- record_active_sound: ring buffer insert into ActiveSoundTable
- dispatch_local_sound: 3D audio + play_sound_pooled + record
- load_and_play_streaming / load_and_play_streaming_positional
Removed: naked asm bridges (call_load_and_play_streaming,
call_load_and_play_streaming_positional, LOAD_AND_PLAY_STREAMING_ADDR).
Unify SoundId and SoundSlot into a single SoundId type — they were
the same packed u32 format (low 16 = slot index, bit 16 = loop,
bit 17 = raw volume). Flag accessors moved to SoundId.
Other changes:
- Add Fixed Div/Div<i32> operators
- Name DDGame+0x77B8 (level_width_sound), GameInfo+0xF38C
(sound_attenuation), DDGame listener_pos() accessor
Co-Authored-By…
refactor: extract game logic from wormkit/replacements to openwa-core
The old input.rs was a grab-bag of unrelated concerns. Split into:
- frame_hook.rs: TurnManager_ProcessFrame hook, debug_sync, watchpoint
arming. Always installed (including baseline/trace-desync mode).
- replay_test.rs: Fast-forward + gameplay milestone tracking. Only for
headful replay testing — headless runs at max CPU speed already.
- debug_utils.rs: dump_region memory inspection (used by validation).
Moved get_ddgame()/get_wrapper() to openwa-core::engine::game_session
as shared #[cfg(x86)] accessors, eliminating duplicates in wormkit
and debugui.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add trace-desync tool for per-frame checksum comparison
Hook WA's GameFrameChecksumProcessor (0x5329C0) to capture the game's
own network sync checksums after each simulation frame. The test runner's
new `trace-desync` subcommand runs a replay twice — once with minimal
hooks (baseline) and once with all hooks — then diffs the per-frame
hashes to pinpoint the exact frame where simulation diverges.
New env vars: OPENWA_TRACE_DESYNC, OPENWA_TRACE_BASELINE,
OPENWA_TRACE_HASH_PATH. Baseline mode skips all gameplay hooks,
keeping only headless/file-isolation/input/replay infrastructure.
RE: identified and labeled ComputeStateChecksum (0x546140),
ComputeStateChecksum_Multi (0x546170), SerializeGameState (0x532330),
GameFrameChecksumProcessor (0x5329C0). Exposed DDGameWrapper checksum
fields at offsets 0x268/0x26C/0x270.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace DDGame raw offset accesses with named struct fields and methods:
- render_slot_count (0x72E4), render_entries, render_state_flag
- game_info, game_state_stream, version_flag_3 (direct field access)
- team_sound_id() method for 0x7768+team*0xF0 sound table
- weapon_stat_counter() method for 0x40CC-0x40D8 indexed counters
- fire_params.spread for type-4 subtype-4 sound ID
Replace raw vtable dispatch for HandleMessage with typed
lookup_team_task() + CTaskTeam::handle_message() bind macro.
Make lookup_team_task pub(crate) for cross-module reuse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full Rust reimplementation of the weapon release orchestration function
that handles ammo sync, spawn offset calculation, network timing, weapon
stat counters, sound/visual effects, and delegates to FireWeapon.
New module weapon_release.rs with:
- usercall trampoline (EAX=worm, 4 stack params, RET 0x10)
- Pure Rust: sync check, spawn offsets, delay, network timing,
SharedData HandleMessage (0x49), 4 weapon stat counters,
3 weapon category classifiers (IsSuperWeapon, CategoryA/B)
- Rust-to-Rust PlaySoundLocal via new sound::play_sound_local() helper
- Bridges for PlayWormSound/StopWormSound/SpawnEffect (addr-as-param)
- Direct call to fire_weapon_impl (made pub(crate))
- DDGame::advance_effect_rng() for secondary RNG at +0x45F0
Also adds 16 named fields to CTaskWorm (landscape_scale, shot_data,
fire_sync_frames, difficulty_level, weapon_params, sound_handle, etc.),
fills all WormState enum gaps (0x69-0x6B, 0x6F, 0x76, 0x79-0x7A,
0x84-0x85, 0x8A), and removes from_raw_unchecked ant…
feat: discover CTaskWorm poison and health display fields
Live memory debugging (debug CLI + Skunk weapon) confirmed:
- +0x144 poison_source_mask: alliance bitmask preventing double-poison
- +0x148 poison_damage: per-turn HP loss (set by PoisonWorm msg 0x51,
consumed by ApplyPoison msg 0x3E)
- +0x178/+0x17C display_health_raw/target_health_raw: animated HP
interpolation for health bar display
- +0x312 health_text: ASCII string for floating HP number ("100", "88")
- +0x31B poison_text: ASCII string for green poison number ("5")
WormEntry +0x90/+0x94 and +0x70 were initially suspected as poison
fields but not reliably correlated — reverted to unknown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: eliminate pointer arithmetic in weapon.rs — typed params throughout
- Rename DDGame.hurry_flag → version_flag_4 (confirmed via Ghidra as game
logic version byte, not a hurry flag)
- Replace raw fire_params pointer math with struct field access
(collision_radius.0, _fp_04) with polymorphism comments
- Type local_struct as *const WeaponSpawnData in projectile_fire_impl,
use named fields (initial_speed_x/y) instead of array indexing
- Type all naked asm bridge params: *mut CTaskWorm, *const WeaponFireParams,
*const WeaponSpawnData, *const u8, *const () instead of u32
- Simplify fire_weapon_special: remove params_38 u32 arg, compute
params_38_ptr inside from entry pointer
- Type CTaskArrow/CTaskMissile ctor calls with proper pointer params
- Remove duplicate section header comments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: fully type CTaskCloud handler — no raw offsets
Replace all raw pointer arithmetic in CTaskCloud::HandleMessage with
typed DDGame fields. Render path now uses RenderQueue::alloc directly
instead of delegating to original WA function.
New/renamed DDGame fields:
- render_phase (+0x764C): rendering phase, clouds render when == 5
- parallax_scale (+0x8150): renamed from crosshair_scale, also used
for cloud parallax X offset computation
- level_bound_min_x/max_x: changed from i32 to Fixed type
Cloud handler improvements:
- Uses TaskMessage enum (FrameFinish, RenderScene, SetWind)
- Uses Fixed arithmetic with from_int(128) for padding
- Calls RenderQueue::alloc::<DrawSpriteCmd> directly for rendering
- No ORIG_HANDLE_MESSAGE needed — fully self-contained
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: split ddgame.rs (2240 lines) into 3 modules
New openwa-derive crate with FieldRegistry proc macro that generates
HasFieldRegistry impls for #[repr(C)] structs. Uses offset_of!() and
size_of() for compiler-verified offsets. Fields prefixed _unknown/_pad
are automatically excluded.
Applied to 5 key structs: DDGame, DDGameWrapper, GameSession, CTask,
CTaskWorm. Each now has a field_registry() -> &'static StructFields
method for runtime offset-to-name lookups.
This lays groundwork for typed vtable derive macros using the same
proc macro infrastructure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: resolve all clippy warnings across core, wormkit, and test-runner
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>
Three constructor bugs found and fixed:
1. Allocation size 0x98B8 → 0x98D8: original malloc(0x98D8) but memset(0x98B8).
Our code allocated only 0x98B8, causing 32-byte heap overrun.
2. Wrong function after InitPaletteGradientSprites: called LoadingProgressTick
(0x5717A0) instead of DisplayGfx__InitTeamPaletteDisplayObjects (0x5703E0).
3. Arrow collision region SpriteRegion params were all wrong:
- ECX should be sprite_w/2 (was 0)
- EDX should be sprite_h - margin_h (was margin_h)
- p2/p3 should be margin values (were 0)
- p4 should be sprite_w - margin_w (was margin_w)
- p5 should be sprite_h/2 (was margin_h)
- p6 should be arrow sprite ptr (was landscape gfx_resource)
This was the root cause of the longbow replay desync — wrong collision
boxes caused different terrain collision detection and RNG divergence.
Found via sub-object hashing: comparing pointer-canonicalized hashes of
objects pointed to by DDGame between Rust and original constructors.
Co-…
Constructor correctness (match original WA behavior):
- Load debris.spr and back.spr unconditionally, not gfx_mode-gated
- Initialize PaletteContext for masks.img (word=1, word=0xFE, Init)
- Document PCLandscape param_5 as game_info+0xDAAC
Debug tooling improvements:
- Hardware watchpoints: EBP-based stack trace on every hit
- Watchpoint base selection: OPENWA_WATCH_WRAPPER, OPENWA_WATCH_DISPLAY
- Display watchpoint arming during constructor for field tracing
- Snapshot: walk parent chain to CTaskTurnGame (true entity root)
Add docs/re-notes/desync-investigation.md — writeup of the longbow
desync investigation methodology and findings.
NOTE: Longbow desync (4 checksum mismatches) is NOT yet resolved.
These changes are correctness fixes and tooling for continued work.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: pass ESI (layer_ctx) to LoadSprites usercall in DDGame constructor
GfxHandler__LoadSprites (0x570B50) is a usercall that reads ESI as the
display layer context. Our wa_load_sprites wrapper was declared as plain
stdcall, leaving ESI unset. This caused LoadSpriteFromVfs and
GfxResource__Create_Maybe to operate with garbage ESI, producing wrong
colour values (0xFFFFFFFF instead of palette indices 1-9).
Fixed by converting wa_load_sprites to a naked asm bridge that sets ESI
from a 5th parameter (layer_ctx) before calling the original function.
The gfx_color_table now matches the original constructor output.
Checksum mismatches persist — likely caused by other uncanonicalized
pointer-as-u32 fields in DDGame (like landscape_property). The snapshot
system confirms all gameplay state is identical.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: canonicalize landscape_property as pointer, remove arrow dump
landscape_property (DDGame+0x468) is a heap address stored as u32.
Show as <ptr>/null instead of raw value to avoid false diff positives.
Removed arrow_sprites/arrow_gfxdirs from snapshot — confirmed identical
between constructors, not involved in the desync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add arrow_sprites to DDGame snapshot, narrow desync further
Added arrow_sprites/arrow_gfxdirs null/ptr pattern and raw hex dump
to snapshot output. Multi-frame comparison (frames 1, 1350, 1351)
confirms:
- Arrow arrays are IDENTICAL between constructors (no diff)
- Only 2 DDGame field diffs exist, both static from construction:
1. landscape_property: misclassified pointer (heap addr as u32)
2. gfx_color_table: [1..9] vs [0xFFFFFFFF x 9]
- All gameplay state identical: entities, positions, health, ammo, RNG
- Diffs are the same at frames 1, 1350, and 1351 (no drift over time)
Root cause remains: headless LoadSprites fallback in create_ddgame
doesn't correctly populate GfxDir colour data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New `snapshot` command captures canonicalized game state for diffing:
- DDGame: frame counter, RNG, game speed, camera, team arena, ammo
- Teams: name, alliance, worm count, per-worm state/hp/name
- Entities: BFS walk of task tree, rich dump for CTaskWorm/CTaskMissile,
raw hex with pointer canonicalization for unknown entity types
Snapshot trait implemented for DDGame, WormEntry, CTaskWorm, CTaskMissile.
Unknown regions dumped as raw hex with [ptr---] replacing heap pointers.
Usage: openwa-debug snapshot > /tmp/state.txt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>