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: replace raw pointer arithmetic with typed field access in game_state_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>
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: 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: 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>
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>
feat: add #[derive(FieldRegistry)] proc macro for auto-generated field mapsβ
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>
refactor: add TeamIndexMap, RenderEntry, NetBridge structs to DDGameβ
Add three new typed structs replacing raw pointer arithmetic:
- TeamIndexMap (0x64 bytes): entries[i16;16] + count + gap + terminator.
Three instances at DDGame+0x7650 replace 6 fragmented byte arrays.
- RenderEntry (0x14 bytes): active flag + unknown data.
14 entries at DDGame+0x73B0 replace opaque _unknown_73b0 blob.
- NetBridge (0x2C bytes): ddgame back-pointer + net_config bytes.
Created for online games only, replaces ddgame_secondary raw pointer.
ddgame_init_render_indices now takes *mut DDGame and uses pure field
access. DDGameWrapper.ddgame_secondary renamed to net_bridge with
typed *mut NetBridge pointer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reduce from 4 to 2 DDGameWrapper__LoadingProgressTick calls, matching
the original constructor. Tick counter +0x04D8 now matches exactly
(0x334). All DDGameWrapper fields match the original.
Remaining headless checksum mismatch is likely due to headless display
handling differences (our headless display is non-null vs original's
NULL display parameter).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: pure Rust DDGameWrapper__LoadSpeechWAV, typed struct fieldsβ
Replace the naked asm bridge to DDGameWrapper__LoadSpeechWAV (0x571530)
with a pure Rust implementation that uses typed struct field access.
Add speech_name_table (360 x 0x40-byte entries at +0x14E8) and
speech_name_count (+0x6EE8) fields to DDGameWrapper, eliminating all
pointer arithmetic from the speech WAV loading path.
Rename address constant DSSOUND_LOAD_SPEECH_WAV to
DDGAMEWRAPPER_LOAD_SPEECH_WAV and update Ghidra label to match
(function operates on DDGameWrapper, not DSSound).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: reorganize openwa-core into subdirectory modulesβ