Add playback.thm and current.thm to file_isolation's DATA_FILES so
the CreateFileA hook handles their per-PID redirection. This removes
the need for replay hooks in baseline mode — the original WA replay
system works fine when file isolation handles the temp file conflicts.
Baseline now installs only: headless, file_isolation, frame_hook,
trace_desync. No replay hooks, no gameplay hooks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: split input.rs into frame_hook, replay_test, debug_utils
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>
docs: update CLAUDE.md with test isolation details and ERRORLOG env var
Document all concurrency isolation layers (file redirection, semaphore
renaming, batched hooks, crash detection) and add OPENWA_ERRORLOG_PATH
to the environment variables section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
perf: batch MinHook enable calls for 10x faster test startup
Replace per-hook MinHook::enable_hook() with queue_enable_hook(), then
call apply_queued() once after all hooks are installed. This batches
~90 VirtualProtect syscalls into a single pass per code page.
Results (17 tests):
j=4: ~35s → ~3s
j=16: ~25s → ~1.8s
The VirtualProtect calls were the entire startup bottleneck — each one
flushes the instruction cache and modifies page tables, and they were
serialized across all hooks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: scope MutexGuard in worker thread to not hold lock during test
The original code held the work_rx mutex for the entire recv() call.
While harmless when items are pre-buffered, this is incorrect — scope
the guard so it drops after recv() returns but before running the test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: isolate named semaphore for full test concurrency
Hook CreateSemaphoreA in headless mode to rename the "Worms Armageddon"
instance-detection semaphore to "Worms Armageddon_{pid}". Without this,
concurrent WA.exe instances detect each other via GetLastError() ==
ERROR_ALREADY_EXISTS and skip initialization, causing silent failures.
This was the last concurrency hazard — tests now pass reliably at j=16
(10/10 runs, vs 1/17 before the session's fixes).
Also:
- Expose temp_dir_path() from file_isolation for cross-module use
- Reduce test stagger from 200ms to 50ms (just enough for I/O smoothing)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: high-concurrency test crashes from premature thread resume
Root cause: the launcher's hooks-ready event had a 10s timeout. Under
high concurrency (j=16), DLL loading + hook installation exceeds 10s
due to I/O contention, causing the launcher to resume WA.exe's main
thread before all hooks are installed. The FireWeapon trap then fires
because WeaponRelease (last hook in the chain) was never installed.
Fix: increase hooks-ready timeout to 120s (matching test runner timeout).
Also:
- Redirect all temp files to per-PID .openwa_tmp/{pid}/ directory
instead of individual per-PID suffixed filenames. Now covers
writetest.txt, mono.tmp, custom.dat, current.thm, playback.thm
in addition to existing land.dat, landgen.svg
- Clean up temp directory on DLL detach and in test runner
- Remove 200ms test stagger (no longer needed with proper isolation)
- Add .gitignore for replay output logs (only *_expected.log tracked)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
feat: test runner crash detection, ERRORLOG capture, and dialog suppression
- Launcher passes /silentcrash to WA.exe in headless mode, preventing
the crash handler from relaunching with /handlecrash and showing the
"Fatal error" dialog that blocks test execution
- Hook ERRORLOG.TXT creation via OPENWA_ERRORLOG_PATH env var to
redirect crash logs to per-test paths in the run directory
- Detect crashes by NTSTATUS exit codes or ERRORLOG.TXT presence,
show CRASH status with exception details and first 20 lines of
the error log (using lossy UTF-8 for binary memory dumps)
- Strip ANSI color codes when stdout is not a terminal
- Add RE notes for Frontend__MainNavigationLoop (crash handler mechanism)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
chore: add VSCode settings for automatic formatting on save
- Add SoundSlot newtype for the packed slot+flags u32 parameter
(index(), is_looping(), is_raw_volume(), without_loop() methods)
- Rename _field_00 -> slot: SoundSlot (stores the SoundSlot used to play)
- Fix missing desc.slot = slot store in core_play_sound (was stale data)
- Rename flags_a -> pool_idx, flags_b -> priority
- Use Fixed for volume/frequency/pan params throughout DSSound vtable
- Update all stale comments referencing old field names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename ChannelDescriptor fields:
- flags_a -> pool_idx (buffer pool index, -1 = not pooled)
- flags_b -> priority (sound priority for channel eviction)
- channel_freq, channel_volume -> Fixed type (were already Fixed 16.16)
- flags_and_slot -> slot_and_flags
Use Fixed for all volume/frequency/pan parameters:
- core_play_sound, play_sound, play_sound_pooled
- set_master_volume, set_channel_volume, set_pan
- DSSoundVtable fn signatures updated to match
Fixed is #[repr(transparent)] wrapping i32 — same ABI, no behavioral change.
Updated all stale comments referencing old field names.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
core_play_sound stored flags_and_slot (sound ID) in desc.flags_b instead
of priority (flags). This made all channels appear high-priority (IDs like
55, 121) so new sounds with real priority (1-8) could never evict them.
Channels accumulated until all 8 were permanently occupied, blocking all
subsequent pooled sounds (explosions, sizzle, etc.).
Also fixed stop_channel to unconditionally return pool entries to free list
even when the descriptor was already evicted by allocate_channel. WA does
this; without it, evicted pool entries leak and pool_free_count drops to 0.
Also fixed stop_channel used-list removal to use swap-remove (matching WA)
instead of shift-down.
Added sound_priority replay test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port CTaskWorm::PlaySound2 (FUN_00515020) to Rust, hook all 23 callers
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
SpecialFireSubtype::NapalmStrike (subtype 13) is actually used by
multiple weapons (Napalm Strike, Surrender, and others) — it's a
generic "send team message 0x2B" handler, not Napalm-specific.
Renamed to StrikeFire to match the WA function name (StrikeFire at
0x51E2C0). Also renamed TeamMessage::NapalmStrike to StrikeFire and
on_napalm_strike to on_strike_fire.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: wire up typed NapalmStrike sender via TeamMessage + handle_message_raw
NapalmStrike (subtype 13) now constructs a TeamMessage::NapalmStrike,
serializes via to_raw(), and dispatches through handle_message_raw.
No manual byte buffer manipulation in the caller.
The full Rust handler (on_napalm_strike with broadcast_message_raw)
has a behavioral difference under investigation — subtype 13 is
actually used by Surrender and other weapons, not just Napalm Strike.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: eliminate noalias UB in vtable dispatch, add typed TeamMessage
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>
docs: update CLAUDE.md for WeaponRelease port, sound types, bridge patterns
Pure Rust implementation of SpawnEffect_Maybe: builds a 0x408-byte
message buffer from params, looks up CTaskSpriteAnim via SharedData
(entity type 0x1A), and sends HandleMessage(0x56).
Hooked with a naked usercall trampoline that captures EAX (constant),
ECX (speed_x), ESI (worm) + 7 stack params. Serves all 17 WA callers.
Also called directly from weapon_release_impl.
This eliminates the last naked asm bridge from weapon_release.rs —
the module now has zero bridge functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port PlayWormSound (0x5150D0) and StopWormSound (0x515180) from naked
asm bridges to typed Rust functions in the sound module:
- ActiveSoundTable::stop_sound() — ports FUN_00546490, stops a streaming
sound by handle (slot lookup + DSSound::stop_channel + emitter release)
- sound::stop_worm_sound() — reads handle from CTaskWorm.sound_handle,
dispatches to DSSound::stop_channel or ActiveSoundTable::stop_sound
based on the 0x40000000 streaming flag bit, then clears the handle
- sound::play_worm_sound() — stops current sound, then starts new via
bridge to FUN_00546c20 (streaming load-and-play), stores new handle
Removes call_play_worm_sound and call_stop_worm_sound naked asm bridges
from weapon_release.rs. One bridge remains for FUN_00546c20 (streaming
audio start) in sound.rs using addr-as-param pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: propagate WeaponReleaseContext type through fire dispatch
- fire_weapon (renamed from fire_weapon_impl) now takes typed
*const WeaponReleaseContext instead of *const u8
- Convert from extern "cdecl" to plain unsafe fn — only called
from Rust (weapon_release_impl), no longer via naked trampoline
- Replace manual FireWeapon trampoline with install_trap! — the WA
function at 0x51EE60 is fully superseded by weapon_release
- All fire sub-function bridges use typed context pointer
- Remove ORIG_FIRE_WEAPON static (no longer needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: split SoundId into newtype + KnownSoundId enum
SoundId is now an opaque newtype wrapping u32, accepting any WA sound
ID including speech/voice lines (IDs > 126). The known SFX enum is
renamed to KnownSoundId with From<KnownSoundId> for SoundId.
Fixes worm voice lines being silently dropped — the previous refactoring
made hook trampolines reject IDs outside the 1-126 enum range via
TryFrom, but WA's speech system uses higher IDs. Hooks now wrap the
raw u32 in SoundId unconditionally.
play_sound_local accepts impl Into<SoundId> so both KnownSoundId
variants and raw SoundId values work at call sites. Dynamic sound IDs
from weapon table data use SoundId(raw) directly.
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>
refactor: use SoundId and Fixed types in sound functions
Replace raw u32 sound IDs with SoundId enum and raw u32 volume/pitch
with Fixed type across queue_sound, play_sound_local, and hook
trampolines. Fixed is repr(transparent) over i32 so the hook ABIs
are unchanged.
All play_sound_local callers now use named SoundId variants
(ThrowRelease, RocketRelease, ShotgunFire, etc.) and Fixed::ONE
instead of 0x10000 magic numbers. Dynamic sound IDs from weapon
table data use SoundId::try_from() with graceful fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: eliminate raw pointer arithmetic in weapon_release.rs
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…
docs: update CLAUDE.md for weapon porting, CTask vtable, baseline rules
Jet Pack (0x51EC30): pure Rust — sends msg 0x5E to CTaskTeam, plays sound
via FUN_0053EC70 (usercall EDI=0x6CB) + FUN_005480F0 (usercall EAX=-21).
Low Gravity (0x51EA60): Rust implementation ready but kept on bridge.
Changing match arm 24 from naked-fn bridge to any other code causes a
desync in freeze_nuke_armageddon — LLVM's register allocation changes for
the entire match, exposing latent UB in the reachable code paths.
Investigation showed: not opt-level dependent, not inlining dependent,
specifically requires a naked extern "C" function call in the arm to
produce correct codegen. Plan: eliminate all usercall bridges from
fire_weapon_special to sidestep the issue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port FireWeapon__JetPack (0x51EC30) — sends message 0x5E to CTaskTeam,
then plays jet pack sound via FUN_0053EC70 (usercall EDI=0x6CB) and
FUN_005480F0 (usercall EAX=-21).
Low Gravity (0x51EA60) port attempted but deferred — changing match arm 24
causes a codegen-dependent desync in freeze_nuke_armageddon even with a
no-op body. Kept on bridge with investigation note.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The only creation site for this task type is FireWeapon__Girder (0x51E350).
The "FireBall" name was a legacy RE guess; it's actually a short-lived
terrain placement task for girder/bridge objects.
Rename in ClassType enum (26 = Girder), weapon.rs comments, task-hierarchy
docs, and Ghidra labels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
test: add girder replay test (2× Girder, 5× Girder Starter-Pack)
Port FireWeapon__Girder (0x51E350) — allocates CTaskFireBall (0xA4 bytes),
copies 7 DWORDs from fire_params, calls CTaskFireBall::Constructor via
naked asm bridge (usercall EAX=parent + 9 stack params, RET 0x24).
Same allocation+constructor pattern as CreateWeaponProjectile and CreateArrow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port FireWeapon__Freeze (0x51E920) — plays freeze sound at worm position,
creates landscape freeze overlay via PCLandscape vtable[5], and increments
WormEntry counters (turn_action +3, effect_counter +10).
Sound positioning uses the DDGame sound queue entry directly after
PlaySoundGlobal queues the sound effect.
Add freeze_nuke_armageddon replay test (2× Freeze, plus Nuke/Armageddon).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
feat: port weapon fire sub-functions to Rust — Drill, Prod, vtable calls
- 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>
docs: update CLAUDE.md for typed vtable infrastructure
Document new patterns from this session:
- Generic CTask<V: Vtable> and CGameTask<V> for typed vtable pointers
- Task trait with ddgame(), as_task_ptr(), broadcast_message()
- read_volatile requirement in broadcast_message (LLVM caching bug)
- Vtable method hooking via vtable_replace! and replacements/task/
- bind macro dotted field path support (base.vtable)
- Decompiler calling convention warning
- CTaskMissile constructor exception (thiscall, not stdcall)
- New key files: task/base.rs, team_arena.rs, ddgame_constructor.rs
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>
Replace the message subscription filter (vtable slot 2, 0x54F4A0).
Original is just 8 instructions — checks subscription_table[msg_type]
and either drops the message or tail-calls CTask::HandleMessage.
Rust replacement uses the typed subscription_table field directly:
messages with type < 98 are forwarded only if subscribed, messages
>= 98 always pass through. Unsubscribed messages are silently dropped.
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>