The bind_! macro generated &mut self methods for vtable calls, giving
LLVM noalias guarantees that WA bridge calls violated. This caused
codegen-sensitive desyncs where unrelated code changes would shift
optimization decisions and expose the UB.
Fix: auto-generate _raw associated functions (taking raw pointers) for
every bind_! vtable method. Migrate all call sites in weapon dispatch,
weapon release, sound, cloud, and filter handlers to use _raw variants
and CTask::ddgame_raw() instead of the Task trait methods.
Also adds TeamMessage enum with NapalmStrike variant as the first typed
message, and CTaskTeam::on_napalm_strike handler (not yet wired up due
to a separate binary layout sensitivity issue under investigation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct CTask vtable layout — 7 slots, not 8
CTask base has exactly 7 virtual methods (confirmed by wkJellyWorm vtNum=7
and Ghidra decompilation of each slot):
Slot 0: WriteReplayState Slot 1: Free (scalar deleting dtor)
Slot 2: HandleMessage Slot 3-4: stubs (returns 0)
Slot 5: ProcessChildren Slot 6: ProcessFrame
Previous code had size=8 with ProcessFrame misplaced at slot 7. Slot 7
doesn't exist in CTask base — it's the first class-specific extension
slot for derived types (e.g., OnContactEntity for CTaskWorm,
process_frame for CTaskCloud).
Also update CTaskWormVTable doc comment with full 20-slot layout table
showing which slots are CTask base, CGameTask, and CTaskWorm-specific.
Ghidra renames: CTask__vt7_ProcessFrame → CTask__vt7_INVALID_NotInBaseVtable,
CTask__vt5 → ProcessChildren, CTask__vt6 → ProcessFrame,
CTaskWorm_ProcessFrame_Maybe → CTaskWorm__PerFrameUpdate_Maybe (not vtable slot).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: broadcast_message now works — use read_volatile for children state
The pure Rust CTask::HandleMessage port (broadcast_message) was failing
because LLVM cached children_watermark and children_data reads across
virtual dispatch calls. Child handlers can modify the parent's children
array (add/remove/realloc), so these fields must be re-read each
iteration.
Fix: use read_volatile via core::ptr::addr_of! for both fields.
This matches the original WA code which re-reads [EDI+0x10] and
[EDI+0x14] from memory each iteration.
CTaskCloud and CTaskFilter handlers now use broadcast_message directly
instead of calling the original WA CTask::HandleMessage at 0x562F30.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add CTaskVtable, broadcast_message, and retype children_data
Add base CTaskVtable (8 slots, 0x669F8C) via #[vtable] macro with
write_replay_state, free, handle_message, and process_frame.
Add Task::broadcast_message() — pure Rust port of CTask::HandleMessage
(0x562F30) that iterates the sparse children array and dispatches via
typed CTaskVtable. Currently unused (callers use original WA function
pending investigation of a subtle iteration bug).
Retype CTask.children_data from *mut u8 to *mut *mut CTask for type
safety.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add Vtable marker trait for CTask<V> type safety
Add unsafe trait Vtable as a bound on CTask<V> and CGameTask<V>,
preventing arbitrary types from being used as vtable parameters.
- *const c_void implements Vtable (the default/untyped case)
- #[vtable] proc macro auto-generates `impl Vtable for *const MyVTable`
- CTask<V: Vtable> and CGameTask<V: Vtable> enforce the bound
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add Task trait for ergonomic CTask base access
Add unsafe trait Task with default methods:
- task() / task_mut() — reference to CTask base
- as_task_ptr() / as_task_ptr_mut() — raw pointers
- ddgame() — direct DDGame pointer access
Implemented for all CTask subclasses (CTaskWorm, CTaskTeam,
CTaskMissile, CTaskArrow, CTaskMine, etc.) via unsafe impls.
Replace all .base.base chains in weapon.rs with trait methods:
- (*worm).base.base.ddgame → (*worm).ddgame()
- &(*worm).base.base as *const _ as *const CTask → (*worm).as_task_ptr()
- SharedDataTable::from_task(task as *const _ as ...) → from_task((*worm).as_task_ptr())
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make CTask generic over its vtable pointer type:
CTask<V: 'static = *const c_void>
This allows subclasses to specify their typed vtable:
CTaskTeam { base: CTask<*const CTaskTeamVTable>, ... }
Changes:
FieldRegistry derive macro (openwa-derive):
- Handle generic type params by substituting defaults in size_of
expressions. offset_of uses the bare struct name (Rust applies
defaults). Builds a generic_defaults map from TypeParam::default.
bind macro (openwa-derive):
- Accept dotted field paths ($($vtable_field:ident).+) instead of
single idents, enabling bind_VTable!(Struct, base.vtable) for
inherited vtables.
CTask (openwa-core):
- Add generic parameter V with default *const c_void. All existing
code using bare CTask continues to work unchanged.
CTaskTeam (openwa-core):
- Use CTask<*const CTaskTeamVTable> as base type
- Replace manual vtable() and handle_message() with bind macro
- bind_CTaskTeamVTable!(CTaskTeam, base.vtable) generates typed
method wrappers automati…
refactor: distribute task class addresses to their home modules
Move all CTask* class blocks from address.rs into their respective
task/ module files. address.rs re-exports via pub use so all existing
va::* call sites are unchanged.
Moved to task/base.rs: CTask, CTaskLand, CTaskDirt, CTaskSpriteAnim,
CTaskCPU, CTaskSeaBubble, + 10 ctor-only entities
Moved to task/game_task.rs: CGameTask
Moved to task/worm.rs: CTaskWorm
Moved to task/turn_game.rs: CTaskTurnGame
Moved to task/missile.rs: CTaskMissile
Moved to task/mine_oil_drum.rs: CTaskMine, CTaskOilDrum
Moved to task/supply_crate.rs: CTaskCrate
Moved to task/cloud.rs: CTaskCloud
Moved to task/filter.rs: CTaskFilter
Moved to task/fire.rs: CTaskFire
Moved to task/team.rs: CTaskTeam
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>
feat: port Napalm Strike and Select Worm to pure Rust
Add SharedDataTable::lookup() — pure Rust equivalent of FUN_004FDF90.
Looks up an entity by (key_esi, key_edi) in the 256-bucket hash table.
Port two weapon fire handlers using the new lookup:
- Napalm Strike (subtype 13): lookup CTaskTeam, send message 0x2B
- Select Worm (subtype 21): lookup CTaskTeam, send message 0x5D
with buf = [8, team_index]
Both share the fire_send_team_message() helper which handles the
SharedData lookup + HandleMessage vtable dispatch pattern.
Verified: headless PASS (byte-identical).
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: reorganize openwa-core into subdirectory modules
- CTaskMissile (0x41C bytes): fully decompiled constructor layout with
spawn_params (11 DWORDs), weapon_data (94 DWORDs), render_data (42 DWORDs).
render_data is a shifted copy of weapon_data (weapon_data[N+3] for single
shots, weapon_data[N+52] for cluster sub-pellets) — likely for frame
interpolation support in the Steam version.
- MissileType enum: Standard=2, Homing=3, Sheep=4, Cluster=5 (confirmed via
constructor switch analysis).
- CTaskBfsIter: general-purpose BFS tree walker over CTask children using
VecDeque. Replaces inline fixed-size queue arrays in validation code.
- dump_missile_tasks(): uses CTaskBfsIter, routes raw DWORD dump to
OpenWA_validation.log (not OpenWA.log).
- Validation log now cleared unconditionally on DLL load (not just when
OPENWA_VALIDATE=1).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: correct CTask children field names and children count display
Runtime analysis of CTask__AppendChild (0x4fdc70) and CTask__UnlinkChild
(0x4fdce0) reveals the true semantics of the children sub-struct:
+0x08 children_capacity — starts 0x10, doubles on overflow
+0x0C children_dirty — flag set to 1 when any child is unlinked
+0x10 children_watermark — insertion counter, never decremented on remove
+0x14 children_data — sparse pointer array; dead slots are nulled
The append-only watermark means CTaskFilter's "(N children)" label was
rising unboundedly as sea bubbles spawned. Fixed by computing live count
from non-null slots rather than reading children_watermark directly.
Also adds dump_ctask_children() to validation (F8 hotkey) for live
verification of these fields. Ghidra functions renamed accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: split task.rs into per-type modules under task/
Each task struct now lives in its own file. Tightly coupled types
are kept together (TurnGameCtx+CTaskTurnGame, CTaskMine+CTaskOilDrum,
SoundEmitter+CGameTask, CTaskWorm+CTaskWormVTable). mod.rs re-exports
all public types so external import paths are unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>