- weapon_fire.rs: use DDGame.level_width/level_height instead of
.add(0x77C0/0x77C4); use new CTaskWorm accessors instead of
.add(0x34/0x48) on raw worm pointer
- sound_ops.rs: use CGameTask.pos_y instead of .add(0x88)
- sound_ops.rs + active_sound.rs: use SoundEmitter.local_ref_count
instead of .add(8) on raw emitter pointer
- game_task.rs: split SoundEmitter._unknown_04 to expose ref count
field at offset 0x08
- worm.rs: add fire_subtype_1/set_fire_subtype_1_raw/set_action_field_raw
accessors; rewrite state() to use subclass_data slice indexing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: port streaming sound chain to Rust, unify SoundId/SoundSlot
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…
feat: wire in pure Rust fire_armageddon, fix 4-byte buffer offset bug
The Rust port of fire_armageddon (0x51EA60) was complete but never
wired in due to a desync caused by a subtle buffer offset error.
Root cause: the original function writes message fields at buffer
offsets 0x04/0x08/0x0C/0x10, then passes (buffer + 4) as the data
pointer to HandleMessage. This happens because PUSH ESI (argument to
SharedData__Lookup) is cleaned by the callee's RET 4, shifting the
ESP-relative LEA by 4 bytes. The Rust port was passing buffer + 0,
putting all fields 4 bytes too high.
Also:
- Remove dead call_fire_usercall bridge (no longer needed)
- Add CTaskWorm::is_in_state() helper
- Clean up weapon_release.rs: hex→decimal constants, use is_in_state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port FUN_00515020 as play_worm_sound_2 in sound.rs — manages the secondary
sound handle at CTaskWorm+0x3B4. Includes special teleport positioning case
(worm Y < -9999 AND sound 0x36 → play at weapon target via FUN_00546BB0).
Hook installed at 0x515020 so all 23 WA callers go through Rust.
Also:
- fire_girder: replaced raw PlaySoundGlobal transmute with sound::queue_sound
- fire_teleport: replaced call_worm_play_sound bridge with play_worm_sound_2
- Fixed flags passthrough in streaming sound bridges (was hardcoded to 3)
- Added sound_handle_2 field to CTaskWorm at +0x3B4
- Removed dead call_worm_play_sound bridge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct SharedData entity type and rename all SpecialFireSubtype/WormState
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>
refactor: use Weapon enum for weapon IDs and classification
Change CTaskWorm.selected_weapon from u32 to Weapon enum. Update
is_super_weapon, is_weapon_category_a, is_weapon_category_b to take
Weapon and match on named variants instead of hex magic numbers.
Replace raw 0x22/0x24 check with Weapon::Girder/GirderPack match.
Use SpecialFireSubtype variants in type-4 sound dispatch.
Idiomatic range check: Weapon::JetPack..=Weapon::CrateShower.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: add weapon fire type enums, use Fixed for aim/scale params
Add FireType, FireMethod, and SpecialFireSubtype enums to weapon.rs
with TryFrom<i32>. All weapon dispatch matches in weapon.rs and
weapon_release.rs now use typed enum variants instead of raw integers.
Rename fire_subtype_34 → special_subtype, fire_subtype_38 → fire_method
in WeaponEntry for clarity.
Use Fixed type for aim direction params and landscape_scale field.
Add Mul<i32> for Fixed (integer scaling without shift). Remove
fixed_mul_shrd — superseded by Fixed's Mul impl. Spawn offset
calculation now reads as: aim_dir_x * scale * 0x18.
Rename param_1/param_2 → spawn_x/spawn_y, param_3/param_4 →
aim_dir_x/aim_dir_y based on caller analysis (FUN_0051cb60 computes
adjusted worm position + aim direction from angle).
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: port Air Strike, Indian Nuclear Test, and Jet Pack to Rust
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>
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>
feat: add WormState enum — typed worm state machine values
Add repr(u8) WormState enum with all known states from CTaskWorm::HandleMessage
(0x510B40) decompilation and weapon fire dispatch analysis:
- Idle/Active states (0x64-0x68)
- Weapon fire states (0x6C-0x75: Blowtorch, Bat, Kamikaze, etc.)
- Aiming/charging states (0x77-0x7E)
- Dying/dead states (0x7F-0x8B)
Update CTaskWormVTable::set_state to take WormState parameter.
Replace all hex literals in weapon.rs with enum variants.
Source: Ghidra decompilation of HandleMessage switch cases +
SetState call sites across weapon fire dispatch.
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
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>
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>
refactor: eliminate pointer arithmetic from FireWeapon impl
- Rename WeaponEntry.fire_complete → fire_params_start (u32), since
its address is passed to sub-functions as a params base pointer
- Add CTaskWorm::fire_complete() / set_fire_complete() for the
completion flag at CGameTask.subclass_data[12] (worm+0x3C)
- Rename params → fire_params, worm_u32 → w for clarity
- All field reads now go through typed struct access, no raw offsets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: identify weapon_ctx as WeaponEntry pointer, add fire fields
Runtime investigation confirmed CTaskWorm+0x36C points to the active
WeaponEntry in the WeaponTable (entry index matches selected_weapon).
Add newly discovered WeaponEntry fields:
- fire_type (+0x30): weapon dispatch type (1-4)
- fire_subtype_34 (+0x34): subtype for types 3/4
- fire_subtype_38 (+0x38): subtype for types 1/2
- fire_complete (+0x3C): completion flag / params base
Type CTaskWorm.active_weapon_entry as *mut WeaponEntry.
Remove incorrect CTaskWorm weapon accessors (they read from the worm's
subclass_data instead of the WeaponEntry).
Verified: headless PASS (byte-identical).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename CGameTask._unknown_30 to subclass_data, fix CTaskWorm weapon_ctx
Rename the polymorphic 84-byte region at CGameTask+0x30 to
subclass_data with documentation of per-subclass usage.
CTaskWorm+0x36C (weapon_ctx) is NOT a self-pointer — runtime logging
confirmed weapon_ctx != worm (differ by ~0x20000). It points to a
separate weapon context object with fire state at +0x30..+0x3C.
Renamed from weapon_self_ptr to weapon_ctx: *mut u8.
Removed incorrect CTaskWorm weapon fire accessors that read from
subclass_data (wrong object). Weapon state must be read from the
weapon_ctx pointer, as done in the working FireWeapon impl.
Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename CGameTask._unknown_30 to subclass_data, add CTaskWorm weapon accessors
Rename the polymorphic 84-byte region at CGameTask+0x30 to
subclass_data with documentation of per-subclass usage (worm weapon
state, filter subscriptions, missile params, team vtable, etc.).
Add CTaskWorm weapon fire accessor methods:
- weapon_fire_type(), weapon_fire_subtype_34/38()
- weapon_fire_complete() / weapon_fire_complete_mut()
- weapon_params_ptr(), weapon_params_34_ptr(), weapon_params_38_ptr()
- weapon_self_ptr field at +0x36C
Make task::worm and task::game_task modules public.
Update FireWeapon impl to use worm_eax (first cdecl param = EAX) for
field reads — must match original register source for determinism.
Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: name CTaskWorm weapon_self_ptr field, make game_task pub
Add weapon_self_ptr (*mut CTaskWorm) at CTaskWorm+0x36C — the
self-pointer read by WeaponRelease (MOV EAX, [EDI+0x36C]) before
calling FireWeapon. Make game_task module public for typed access.
Note: weapon fire state fields at CGameTask+0x30..+0x3C are NOT added
to CGameTask because the _unknown_30 region is polymorphic — each
subclass (worm, missile, filter, team) uses it differently.
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>
feat: add CTaskCrate struct, raw field debug UI, new task fields
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>