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: split render.rs, move game logic to core, type command structs
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: move bitgrid module from display to crate root
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>
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>
Wire the address registry, field registries, typed vtables, and live
object tracker into all debug tools:
- Watchpoint stack traces now show symbolicated function names via
format_va() instead of raw hex addresses
- Validation auto-discovers vtables from the registry and validates
all named slots from #[vtable(...)] structs
- New ValueKind enum + FieldFormatter trait for typed field formatting:
scalars as decimal, Fixed as float, pointers resolved via registry,
CString as quoted strings. Extensible via inventory::submit!
- Snapshot fallback uses FieldRegistry for entity types without
hand-coded Snapshot impls (CTaskTeam, CTaskMine, etc.)
- New debug CLI commands: inspect (typed struct read), objects (list
tracked live objects)
- Symbolic address expressions: ddgame, ddgame+field_name,
ddgame->field_name work in all CLI commands
- New protocol messages: Inspect, ListObjects, ResolveAlias, ResolveField
- #[field(kind = "CString")] attribute for string fields in FieldRegist…
feat: add #[vtable(...)] attribute macro for typed vtable definitions
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>
feat: add structured address registry with runtime pointer identification
Replace flat pub const address definitions with define_addresses! macro
that generates both compile-time constants and a global queryable registry
via the inventory crate. ~200 addresses now have structured metadata
(kind, calling convention, class name).
Key additions:
- registry.rs: AddrEntry types, sorted lookup table, query API
(lookup_va, vtable_class_name, format_va, detect_pointer)
- define_addresses! macro: class blocks group vtable+ctor+vmethods,
standalone entries for functions/globals/data
- detect_pointer() in mem.rs: identifies static addresses by name,
heap objects by vtable class name
- classify_pointer() detail strings now include registry names
Eliminates three duplicated vtable→name mappings in snapshot.rs,
debugui/app.rs, and validation/mod.rs. All 488 existing va::* call
sites compile unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: resolve all clippy warnings across core, wormkit, and test-runner
Defines the core trait in openwa-core::snapshot with helpers:
- write_indent, fmt_ptr, fmt_ptr32 for formatting
- write_raw_region for hex-dumping unknown struct regions
with automatic pointer canonicalization
Trait impls for DDGame, WormEntry, CTaskWorm, CTaskMissile
and the capture/CLI infrastructure are next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add mem module with pointer classification to openwa-core
Add DATA_END constant and mem module with can_read, classify_pointer,
and classify_region functions extracted from the validation dump logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
refactor: rename MapClass → MapView, move to frontend module
MapView is a CWnd-derived class used across all game modes (local MP,
network, campaign, replay) for terrain/map data — not replay-specific.
Inheritance: CWnd → base (vtable 0x64C348) → MapView (vtable 0x64FC60).
Embedded in frontend dialogs as child windows for map preview/selection.
Loads .thm terrain files (current.thm for live, playback.thm for replay).
Move from engine/map_class.rs to new frontend/ module. Rename constants
(MAP_CLASS_* → MAP_VIEW_*). Update Ghidra labels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: remove extra loading progress ticks, cargo fmt
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>
refactor: move log_line to openwa-core for shared logging
Move log_line() from openwa-wormkit to openwa_core::log so that core
types (e.g. DSSound::load_wav) can emit debug logging. openwa-wormkit
re-exports via pub use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: reorganize openwa-core into subdirectory modules
Replace the FFI-based headless DisplayBase construction with pure Rust:
new_headless() allocates and initializes all fields (slot_table, sentinel,
sprite cache chain: 0x28 wrapper -> 0x3C buffer ctrl -> 0x80020 buffer)
without calling WA's constructor.
Must use WA's vtable pointer (not our own copy) because WA checks vtable
address identity. Patch WA's headless vtable destructor slot in-place to
a Rust version that correctly frees the Rust-allocated sprite cache.
Extract patch_vtable() utility to openwa-core::vtable for reuse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename DDDisplayBase -> DisplayBase across codebase. Define DisplayBase
struct with named fields from constructor analysis, DisplayBaseVtable
with 32 typed slots, SpriteCollection sub-object, and static
HEADLESS_VTABLE with all no-op stubs.
Patch WA.exe's primary DisplayBase vtable (0x6645F8) at init time:
replace 22 _purecall slots with safe Rust no-ops, benefiting both
headless (DisplayBase) and normal (DisplayGfx) modes.
Add wa_free() to wa_alloc module alongside wa_malloc().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GameStats is actually the base class of the display hierarchy —
DisplayGfx__Constructor calls it as its base ctor. Rename to
DDDisplayBase to reflect the inheritance relationship.
Rename GameSession.display_gfx → display since it holds a polymorphic
base class pointer (DisplayGfx in normal, DDDisplayBase in headless).
Update Ghidra: DDDisplayBase__Constructor, DDDisplayBase__Destructor,
DDDisplayBase__Vtable (0x6645F8), DDDisplayBase__HeadlessVtable (0x66A0F8).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add vcall! macro and typed vtable structs for Palette and InputCtrl
- Extend GameInfo to 0xF91C with named fields (display_width/height,
display_flags, headless_mode, speech_enabled, etc.)
- Add 6 placeholder structs: DisplayGfx, InputCtrl, GameTimer,
StreamingAudio, DDNetGameWrapper, GameStats
- Expand DDKeyboard with construction fields and 0x33C size assert
- Fix DSSound size to 0xBE0 and rename COM fields (direct_sound,
primary_buffer, init_success)
- Type GameSession pointers: DDGameWrapper, DDKeyboard, DSSound, Palette
- Fix DDKeyboard+0x4: must store ADDRESS of GameInfo+0xF918, not value
- Fix DSSound init: check Play() result, not outer DirectSoundCreate hr
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: replace GameEngine__InitHardware with full Rust reimplementation
Renames DDInput__Constructor_Maybe → GameEngine__InitHardware (the function
initializes all hardware subsystems; the DDInput vtable only appears in the
headless/stats-server else-branch that gave it its wrong name).
Adds:
- GameSession struct (0x120 bytes) in openwa-core with all known field offsets
mapped from decompilation of FUN_00572F50 / GameEngine__InitHardware
- G_GAME_SESSION (0x7A0884) and four new address constants
- Passthrough hook on DDGameWrapper__Constructor (0x56DEF0) that logs
wrapper and DDGame pointers once per game session start
The hook uses a naked AT&T asm trampoline to preserve EDI, which
DDGameWrapper__Constructor passes implicitly as the 9th argument to
DDGame__Constructor. A plain extern "stdcall" hook clobbered EDI via
Rust's register allocator, causing a null-pointer crash in
PCLandscape__Constructor. The trampoline tail-calls the original via
jmp (not call), leaving EDI completely untouched.
Validated: 135 PASS, 0 FAIL. OpenWA.log shows:
[GameSess…
feat: add TurnOrderWidget structs and expand DDGame field coverage
Add turn_order.rs with AnimatedItemList, TurnOrderWidget, TurnOrderAllianceGroup,
and TurnOrderTeamEntry structs for the in-game team banner health bar UI.
Retype DDGame.turn_order_widget from *mut u8 to *mut TurnOrderWidget.
DDGame fields verified via runtime DWORD dumps and Ghidra cross-references:
- team_health_ratio[6] at 0x45F0 (fixed-point, renders health bar width)
- game_speed/game_speed_target at 0x72D8/0x72DC
- rng_state_1/2 at 0x72EC/0x72F0
- viewport_width/height at 0x7380-0x738C
- camera_x/y/target at 0x73A0-0x73AC
- level_bound_min/max at 0x779C-0x77A8
- task_land (CTaskLand) at 0x54C, hud_panel at 0x534
- turn_status_text at 0x818C, turn_time_limit at 0x7EA8
Remove diagnostic dump invocations from input.rs, keep utilities for reuse.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
refactor: retype DDGame header fields based on runtime vtable analysis
The split between openwa-types (pure types/addresses) and openwa-lib
(rebase/wa_call/typed wrappers) was arbitrary and added friction when
deciding where new code belongs. Merged both into openwa-core.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>