The project hasn't been a WormKit DLL for a long time — it requires a
custom launcher. Rename the crate to openwa-dll with output openwa.dll
to match the rest of the naming convention (openwa-core, openwa-launcher).
Updates all references across Cargo.toml, Rust sources, PowerShell scripts,
CLAUDE.md, skill files, and docs. Replaces stale "wormkit hook/shim"
comments with accurate descriptions. Three remaining "WormKit" references
are legitimate (refer to the actual WA modding framework).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both load_effect_wavs and load_speech_wav were calling DSSound::load_wav
through the vtable indirection. Since load_wav is already a Rust function,
call it directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port Music streaming audio system to pure Rust
Replace WA's Music/StreamingAudio system with a pure Rust implementation
using hound for WAV parsing and DirectSound for streaming playback.
Music system (0x354-byte object, vtable 0x66B3E0):
- Unified Music + StreamingAudio into single struct with full field map
- 6-slot vtable fully replaced via vtable_replace! macro
- Constructor hooked (0x58BC10), destructor uses wa_free
- Playlist management, track ID-to-filename mapping (1-13)
- Volume control with dB lookup table, using Fixed type
Streaming engine:
- WAV streaming via hound + BufReader<File> (replaces mmio API)
- DirectSound double-buffered ring buffer with timeSetEvent refill
- Buffer size matches original formula (timer_delay * avg_bytes/1000)
- advance_track guards against premature track changes (checks
IDirectSoundBuffer::GetStatus before advancing)
Sound hooks:
- Hook PlayWormSound, StopWormSound, LoadAndPlayStreaming
- Remove old streaming.rs stub, update GameSession to use *mut Music
Co-Authored-By: Claude Opus 4.6…
refactor: replace raw pointer arithmetic with typed struct access
- 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 WavPlayer and speech/fanfare functions to pure Rust
Replace all 3 WavPlayer asm bridges (Stop, Play, LoadAndPlay) with Rust
implementations using hound for WAV parsing and the windows crate for
DirectSound. Hook them globally to avoid CRT heap mismatch between WA's
msvcr80 and our ucrt.
Port PlayFanfare_CurrentTeam from passthrough to full replacement
(team index lookup + fanfare path building). Eliminates all remaining
WA.exe function calls from the speech/fanfare system.
Move game logic from wormkit speech.rs into openwa-core:
- audio/wav_player.rs: WavPlayer struct, stop/play/load_and_play
- audio/speech_ops.rs: PlayFeSfx, PlayFanfare, speech bank loading,
team config name lookup table
Wormkit speech.rs now contains only hook trampolines, logging shims,
and install().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: split audio modules and widen streaming sound function signatures
Move KnownSoundId to its own module (known_sound_id.rs), rename sound.rs
to sound_id.rs. Widen load_and_play_streaming to take *mut CGameTask
(needs sound_emitter) and load_and_play_streaming_positional to *mut CTask
(only reads ddgame at +0x2C). Add trap on original 0x546BB0 since its
sole caller (PlayWormSound2) is fully ported.
Co-Authored-By: Claude Opus 4.6 (1M context) <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…
refactor: extract game logic from wormkit/replacements to openwa-core
- 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>
refactor: use Fixed types and meaningful names in DSSound
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>
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>
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>
feat: add #[vtable(...)] attribute macro for typed vtable definitions
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: convert sound dispatch helpers and fix ActiveSoundEntry fields
Remove all per-sound debug log_line calls from core_play_sound and
load_wav. Keep error-path returns but without logging (matching
original's silent failure behavior).
Remove unused DSSound import from validation module.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct play_sound param order and descriptor state
Careful stack offset tracing reveals the 5 params to play_sound are:
(flags_and_slot, priority, frequency, volume, pan) — frequency and
volume are separate params, not a single dual-purpose value.
Fix descriptor state: flags_b stores flags_and_slot (identifies which
sound is playing), not -1. flags_a is -1 (marks buffer for auto-release
when finished).
Remove naked asm trampolines — pure thiscall confirmed.
All 24/24 DSSound vtable slots Rust with correct params.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement core_play_sound and allocate_channel in pure Rust:
- Channel allocator: finds free descriptor or evicts lowest-priority
- Buffer duplication via IDirectSound::DuplicateSoundBuffer
- Volume from dB lookup table with master volume scaling
- Frequency from pitch * base_freq (fixed-point 16.16)
- Pan from reversed dB table lookup
- Playback via IDirectSoundBuffer::Play
Slots 3 and 4 are pure thiscall with 5 stack params (RET 0x14), not
usercall — ESI/EBX are not implicit params (confirmed via logging that
showed garbage in those registers).
Param mapping: (flags_and_slot, priority, pitch, pan, unused).
Pitch serves dual purpose: clamped to 0x10000 for volume scaling,
unclamped for frequency multiplication.
All 24/24 DSSound vtable slots now Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: replace DSSound slot 0 (destructor) with Rust
Slot 2: frequency scaling — adjusts playback frequency on all playing
channels by multiplying channel_freq * param2 / param1. Clamped to
200000 Hz. Rename _field_0c → channel_freq.
Slot 5: stereo panning — uses VOLUME_DB_TABLE read backwards for
pan-to-dB conversion. Positive pan negates the dB value.
21/24 DSSound vtable slots now Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pan uses the same VOLUME_DB_TABLE read backwards — index 0 from the end
gives near-0 dB (no attenuation), larger indices give more attenuation.
Positive pan negates the dB value for right-channel bias.
20/24 DSSound vtable slots now Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add VOLUME_DB_TABLE (64 i16 entries from WA .rdata 0x6A6A60) and
volume_to_db() helper for fixed-point → dB attenuation conversion.
Slot 7: iterates channel descs, computes combined master*channel volume,
calls IDirectSoundBuffer::SetVolume with dB from lookup table.
Slot 8: per-channel volume with flag-dependent master scaling.
Rename _field_10 → channel_volume (per-channel volume, confirmed from
slot 8 assembly storing the clamped value there).
19/24 DSSound vtable slots now Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slot 9 was named is_channel_playing but actually implements
is_channel_finished: the original inverts DSBSTATUS_PLAYING via
NOT+AND (playing→0, stopped→1). Our code returned 1 for playing,
causing callers to think channels were free and immediately reuse
them — killing all sound playback.
Rename to is_channel_finished with correct semantics. Re-enable
slots 1 (update_channels), 10 (stop_channel), 11 (release_finished)
which all work correctly with the fixed descriptor layout.
17/24 DSSound vtable slots now Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix SpeechSlotTable::get/set to use line_id directly as array index
(1-based, slot 0 per team unused) matching WA's assembly. Previous
code subtracted 1, causing all voice lines to map to wrong slots.
Add F7 hotkey for runtime DSSound channel descriptor dumps to verify
field layouts. Runtime dumps confirmed ds_buffer at descriptor +0x14.
Add slot 14 (sub_destructor) to Rust. Slots 1, 9, 10, 11 remain
disabled — slot 9's return value semantics need caller analysis
(always returning 1 breaks sound scheduling).
13/24 DSSound vtable slots enabled in Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SpeechSlotTable::get/set subtracted 1 from line_id, but WA uses the
raw ID directly as the array index (1-based, slot 0 unused per team) —
same pattern as channel_slots. This caused voice lines to be stored and
looked up at the wrong index, playing wrong speech for each event.
Add DSSound vtable slot 14 (sub_destructor) in Rust — sets secondary
vtable and optionally frees.
Add ChannelDescriptor _field_00 at +0x00 and shift ds_buffer to +0x14
to match original code's piVar4 indexing. Slots 1, 9, 10, 11 remain
disabled pending runtime verification of the full descriptor layout.
13/24 DSSound vtable slots enabled in Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slot 14 (sub_destructor): sets secondary vtable, optionally frees.
Slots 1 and 11 implemented but disabled pending investigation —
update_channels crashes when iterating channel descriptors, likely
due to COM pointer state mismatch during frame updates.
16/24 DSSound vtable slots now Rust (14 enabled + 2 disabled).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: replace DSSound vtable slots 9 (is_channel_playing) and 10 (stop_channel)
Add typed ChannelDescriptor::buffer() and take_buffer() helpers for
safe IDirectSoundBuffer access. Remove cfg(target_os) guards — this
project is 32-bit Windows only.
Slot 9: checks IDirectSoundBuffer::GetStatus via typed helper.
Slot 10: stops buffer via take_buffer() + Drop, resets descriptor,
returns pool index to free list, removes from used list.
14/24 DSSound vtable slots now Rust.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: invert load_wav return values to match original (1=success, 0=fail)
WA's load_wav returns 1 on success and 0 on failure. Our Rust version
had them inverted, causing DSSound_LoadSpeechWAV to think every load
failed. This triggered fallback paths that assigned voice lines to
wrong slots — "ohdear" played on worm hurt instead of "oof/ow".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WA uses slot indices directly as array indices (1-based, slot 0 unused).
Our is_slot_loaded and load_wav incorrectly subtracted 1, causing all
sounds to be stored and looked up at the wrong slot — resulting in
scrambled sound effects and voice lines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: replace 12/24 DSSound vtable slots with Rust
Add typed DSSoundVtable struct with all 24 virtual method signatures,
named from decompilation analysis (update_channels, play_sound,
set_master_volume, load_wav, stop_channel, etc.).
Refine ChannelDescriptor fields from vtable method analysis: flags_a/b,
volume_num/val, ds_buffer, pool_index. Rename buffer pool fields to
reflect free-list/used-list semantics. Rename _field_bb0 to
total_bytes_loaded (incremented by load_wav).
Implement is_slot_loaded (slot 13) in Rust and patch it into WA's
DSSound vtable via patch_vtable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: pure Rust DSSound::new() and typed COM init via windows crate
Replace DSSound constructor FFI bridge (call_dssound_ctor) with pure
Rust DSSound::new() that initializes all fields: 8 channel descriptors,
500 channel slots, 64-entry buffer pool, volume, and status flags.
Replace DSSOUND_INIT_BUFFERS FFI bridge (call_dssound_init_buffers) with
typed windows crate COM calls: IDirectSound::SetCooperativeLevel,
CreateSoundBuffer(DSBCAPS_PRIMARYBUFFER), IDirectSoundBuffer::GetCaps.
Expand DSSound struct from opaque blob to named fields matching the
constructor analysis (channel_descs, channel_slots, buffer_pool, etc.).
Remove dead naked asm bridges and statics (DSSOUND_CTOR_ADDR,
DSSOUND_INIT_BUF_ADDR, DSSOUND_INIT_EAX, DSSOUND_INIT_SAVED_RET).
Trap DSSound__Constructor and DSSOUND_INIT_BUFFERS -- both had
GameEngine__InitHardware as their only caller (now Rust).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: reorganize openwa-core into subdirectory modules