diff --git a/crates/openwa-core/src/address.rs b/crates/openwa-core/src/address.rs index 19d6373512866aac48334eb3140731360a3fd279..ed9029b288636acd831e89ffe46b3833c338cb2d 100644 --- a/crates/openwa-core/src/address.rs +++ b/crates/openwa-core/src/address.rs @@ -333,9 +333,47 @@ pub mod va { pub const CONSTRUCT_OPENGL_CPU: u32 = 0x005A_0850; pub const OPENGL_INIT: u32 = 0x0059_F000; pub const DDGAME_INIT_FIELDS: u32 = 0x0052_6120; + /// DDGame__InitRenderIndices (0x526080): usercall(ESI=ddgame), plain RET. + /// Initializes render queue index arrays at DDGame+0xC4..+0x4A2. + pub const DDGAME_INIT_RENDER_INDICES: u32 = 0x0052_6080; + /// DDGame__InitVersionFlags (0x525BE0): stdcall(ddgame_wrapper). + /// Sets DDGame+0x7E2E/0x7E2F/0x7E3F flags based on game version/mode. + pub const DDGAME_INIT_VERSION_FLAGS: u32 = 0x0052_5BE0; + /// GfxResource__Create_Maybe (0x4F6300): returns GfxResource ptr or null. + pub const GFX_RESOURCE_CREATE: u32 = 0x004F_6300; + /// TaskStateMachine__Init_Maybe (0x4F6370): initializes TSM-like object. + pub const TASK_STATE_MACHINE_INIT: u32 = 0x004F_6370; + /// DDGame__InitSoundPaths_Maybe (0x570F30): called before DSSound_LoadEffectWAVs. + pub const DDGAME_INIT_SOUND_PATHS: u32 = 0x0057_0F30; + /// DDGameWrapper__LoadingProgressTick (0x5717A0): loading progress bar + message pump. + pub const DDGAME_WRAPPER_LOADING_PROGRESS_TICK: u32 = 0x0057_17A0; + /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): loads weapon icons, wind indicators, + /// girder sprites, and creates DDGame+0x37C DisplayGfx object. + /// thiscall(ECX=gfx_handler_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8. + pub const DDGAME_LOAD_HUD_AND_WEAPON_SPRITES: u32 = 0x0053_D0E0; + /// DDGame__InitDisplayFinal_Maybe (0x56A830): display finalization for non-headless. + pub const DDGAME_INIT_DISPLAY_FINAL: u32 = 0x0056_A830; + /// PCLandscape__Constructor (0x57ACB0): constructs 0xB44-byte landscape object. + pub const PC_LANDSCAPE_CONSTRUCTOR: u32 = 0x0057_ACB0; + /// SpriteRegion__Constructor (0x57DB20): constructs 0x9C-byte sprite region. + pub const SPRITE_REGION_CONSTRUCTOR: u32 = 0x0057_DB20; + /// g_GameInfo global — set to current GameInfo* during DDGame construction. + pub const G_GAME_INFO: u32 = 0x0077_49A0; + /// GfxHandler__LoadSprites_Maybe (0x570B50): loads sprites from GfxHandler. + pub const GFX_HANDLER_LOAD_SPRITES: u32 = 0x0057_0B50; + /// FUN_00570A90: called before display palette setup (non-headless only). + pub const FUN_570A90: u32 = 0x0057_0A90; + /// FUN_00570E20: called after GfxHandler initialization. + pub const FUN_570E20: u32 = 0x0057_0E20; pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0; pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520; pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0; + /// DisplayGfx__Constructor_Maybe (0x4F5E80): stdcall(raw_image) -> DisplayGfx*, RET 0x4. + pub const DISPLAYGFX_CONSTRUCTOR: u32 = 0x004F_5E80; + /// IMG_Decode (0x4F5F80): stdcall(output, raw_image, 1) -> DisplayGfx*, RET 0xC. + /// Reads IMG\x1A header, decodes image data into output buffer. + /// Ghidra mislabels this as "TaskStateMachine__Destructor_Maybe". + pub const IMG_DECODE: u32 = 0x004F_5F80; // === Higher-level drawing functions === @@ -668,6 +706,8 @@ pub mod va { pub const WA_MALLOC: u32 = 0x005C_0AE3; pub const WA_MALLOC_MEMSET: u32 = 0x0053_E910; pub const WA_FREE: u32 = 0x005D_0D2B; + /// WA's CRT _fopen — cdecl(path, mode) → FILE*. + pub const WA_FOPEN: u32 = 0x005D_3271; // === Bitmap font system === diff --git a/crates/openwa-core/src/audio/dssound.rs b/crates/openwa-core/src/audio/dssound.rs index 7be132bd2853ab756f5b1dc34e6bb4369944ba22..37f4a30d60c0d75b0fa7a808219676098abc6d07 100644 --- a/crates/openwa-core/src/audio/dssound.rs +++ b/crates/openwa-core/src/audio/dssound.rs @@ -7,14 +7,11 @@ use windows::Win32::Media::Audio::DirectSound::IDirectSoundBuffer; /// Index 0 = silence (-10000 dB), index 63 = near-unity (-22 dB). /// Used by set_master_volume, set_channel_volume, and play_sound. const VOLUME_DB_TABLE: [i16; 64] = [ - -10000, -6000, -5000, -4415, -4000, -3678, -3415, -3000, - -2744, -2522, -2326, -2150, -1991, -1845, -1712, -1589, - -1475, -1368, -1268, -1176, -1088, -1006, -928, -855, - -786, -720, -658, -599, -543, -490, -439, -390, - -344, -299, -256, -216, -177, -139, -104, -70, - -38, -7, 24, 54, 83, 111, 138, 163, - 188, 211, 234, 256, 277, 296, 315, 333, - 350, 367, 383, 398, 413, 427, 441, 454, + -10000, -6000, -5000, -4415, -4000, -3678, -3415, -3000, -2744, -2522, -2326, -2150, -1991, + -1845, -1712, -1589, -1475, -1368, -1268, -1176, -1088, -1006, -928, -855, -786, -720, -658, + -599, -543, -490, -439, -390, -344, -299, -256, -216, -177, -139, -104, -70, -38, -7, 24, 54, + 83, 111, 138, 163, 188, 211, 234, 256, 277, 296, 315, 333, 350, 367, 383, 398, 413, 427, 441, + 454, ]; // NOT: these might be negative centibels, not actual dB. Exact interpretation TBD. /// DSSound — DirectSound audio subsystem. @@ -91,7 +88,8 @@ pub struct DSSoundVtable { /// Slot 3 (0x574730): play_sound — wrapper around core play, returns bool. RET 0x14 = 5 stack params. pub play_sound: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> bool, /// Slot 4 (0x574770): play_sound_pooled — allocates from buffer pool, plays. RET 0x14 = 5 stack params. - pub play_sound_pooled: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> i32, + pub play_sound_pooled: + unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> i32, /// Slot 5 (0x574900): set_pan — sets pan on channel (dB lookup) pub set_pan: unsafe extern "thiscall" fn(*mut DSSound, u32, u32) -> u32, /// Slot 6 (0x505430): **stub** — returns 0 @@ -189,17 +187,26 @@ const _: () = assert!(core::mem::size_of::() == 0xBE0); /// Slot 13: is_slot_loaded — returns whether sound at `slot_idx` has a buffer loaded. /// Slot index is used directly as array index (1-based, slot 0 unused). pub unsafe extern "thiscall" fn is_slot_loaded(this: *mut DSSound, slot_idx: i32) -> bool { - (*this).channel_slots.get(slot_idx as usize).copied().unwrap_or(0) != 0 + (*this) + .channel_slots + .get(slot_idx as usize) + .copied() + .unwrap_or(0) + != 0 } /// Trivial noop — returns void. Used for slots 15, 16. pub unsafe extern "thiscall" fn noop(_this: *mut DSSound) {} /// Trivial stub — returns 0. Used for slots 6, 17, 18, 19, 20, 21, 22. -pub unsafe extern "thiscall" fn returns_0(_this: *mut DSSound) -> u32 { 0 } +pub unsafe extern "thiscall" fn returns_0(_this: *mut DSSound) -> u32 { + 0 +} /// Trivial stub — returns 1. Used for slot 23. -pub unsafe extern "thiscall" fn returns_1(_this: *mut DSSound) -> u32 { 1 } +pub unsafe extern "thiscall" fn returns_1(_this: *mut DSSound) -> u32 { + 1 +} /// Slot 1: update_channels — iterates 8 channel descriptors, releases finished buffers. /// Called each frame to clean up buffers that have finished playing. @@ -254,9 +261,7 @@ fn volume_to_db(volume: i32) -> i32 { /// Slot 7: set_master_volume — sets the master volume and adjusts all active channels. /// `new_volume` is Fixed 16.16 (0 = silent, 0x10000 = max). -pub unsafe extern "thiscall" fn set_master_volume( - this: *mut DSSound, new_volume: i32, -) -> u32 { +pub unsafe extern "thiscall" fn set_master_volume(this: *mut DSSound, new_volume: i32) -> u32 { let snd = &mut *this; // Clamp to [0, 0x10000]. @@ -291,9 +296,7 @@ pub unsafe extern "thiscall" fn set_master_volume( /// Slot 2: set_volume_params — sets frequency scaling params and adjusts active channels. /// `param1` is the divisor (status_1), `param2` is the multiplier (status_2). /// For each playing channel: new_freq = channel_freq * param2 / param1, clamped to 200000. -pub unsafe extern "thiscall" fn set_volume_params( - this: *mut DSSound, param1: u32, param2: i32, -) { +pub unsafe extern "thiscall" fn set_volume_params(this: *mut DSSound, param1: u32, param2: i32) { let snd = &mut *this; // No change → early return. @@ -307,7 +310,9 @@ pub unsafe extern "thiscall" fn set_volume_params( let Some(buf) = desc.buffer() else { continue }; // Check if buffer is playing. - let Ok(status) = buf.GetStatus() else { continue }; + let Ok(status) = buf.GetStatus() else { + continue; + }; if status & 1 == 0 { continue; } @@ -326,9 +331,7 @@ pub unsafe extern "thiscall" fn set_volume_params( /// Slot 5: set_pan — sets stereo panning on a specific channel. /// `pool_id` is 1-based (1..64). `pan` is Fixed 16.16 (-0x10000..0x10000). /// Negative = left, positive = right. -pub unsafe extern "thiscall" fn set_pan( - this: *mut DSSound, pool_id: i32, pan: i32, -) -> u32 { +pub unsafe extern "thiscall" fn set_pan(this: *mut DSSound, pool_id: i32, pan: i32) -> u32 { let idx = (pool_id - 1) as usize; if idx >= 64 { return 0; @@ -362,7 +365,9 @@ pub unsafe extern "thiscall" fn set_pan( /// Slot 8: set_channel_volume — sets volume on a specific channel. /// `pool_id` is 1-based (1..64). `volume` is Fixed 16.16 (0..0x10000). pub unsafe extern "thiscall" fn set_channel_volume( - this: *mut DSSound, pool_id: i32, volume: i32, + this: *mut DSSound, + pool_id: i32, + volume: i32, ) -> u32 { let idx = (pool_id - 1) as usize; if idx >= 64 { @@ -397,9 +402,7 @@ pub unsafe extern "thiscall" fn set_channel_volume( } /// Slot 0: destructor — releases all COM objects and frees memory. -pub unsafe extern "thiscall" fn destructor( - this: *mut DSSound, flags: u8, -) -> *mut DSSound { +pub unsafe extern "thiscall" fn destructor(this: *mut DSSound, flags: u8) -> *mut DSSound { let snd = &mut *this; // Reset vtable to primary (destructor chain pattern). @@ -488,8 +491,7 @@ unsafe fn core_play_sound( &*(&snd.channel_slots[slot_idx] as *const Ptr32 as *const IDirectSoundBuffer); // Duplicate the buffer via IDirectSound::DuplicateSoundBuffer. - let ds: &IDirectSound = - &*(&snd.direct_sound as *const Ptr32 as *const IDirectSound); + let ds: &IDirectSound = &*(&snd.direct_sound as *const Ptr32 as *const IDirectSound); let new_buf = match ds.DuplicateSoundBuffer(template_buf) { Ok(buf) => buf, Err(_) => { @@ -543,7 +545,9 @@ unsafe fn core_play_sound( let pan_clamped = pan.max(-0x10000).min(0x10000); let pan_idx = ((pan_clamped.unsigned_abs() >> 10) as usize).min(63); let mut pan_db = VOLUME_DB_TABLE[63 - pan_idx] as i32; - if pan_clamped > 0 { pan_db = -pan_db; } + if pan_clamped > 0 { + pan_db = -pan_db; + } let _ = buf.SetPan(pan_db); let _ = buf.Play(0, 0, if loop_flag { 1 } else { 0 }); @@ -598,17 +602,34 @@ unsafe fn allocate_channel(snd: &mut DSSound, priority: i32) -> i32 { /// Slot 3: play_sound — pure thiscall with 5 stack params, RET 0x14. /// Params: flags_and_slot, priority, frequency, volume, pan. pub unsafe extern "thiscall" fn play_sound( - this: *mut DSSound, flags_and_slot: u32, priority: i32, frequency: i32, volume: i32, pan: i32, + this: *mut DSSound, + flags_and_slot: u32, + priority: i32, + frequency: i32, + volume: i32, + pan: i32, ) -> bool { let snd = &mut *this; - let result = core_play_sound(snd, flags_and_slot & 0xFFFEFFFF, priority, frequency, volume, pan); + let result = core_play_sound( + snd, + flags_and_slot & 0xFFFEFFFF, + priority, + frequency, + volume, + pan, + ); result >= 0 } /// Slot 4: play_sound_pooled — pure thiscall with 5 stack params, RET 0x14. /// Returns pool_id (1-based) on success, 0 on failure. pub unsafe extern "thiscall" fn play_sound_pooled( - this: *mut DSSound, flags_and_slot: u32, priority: i32, frequency: i32, volume: i32, pan: i32, + this: *mut DSSound, + flags_and_slot: u32, + priority: i32, + frequency: i32, + volume: i32, + pan: i32, ) -> i32 { let snd = &mut *this; @@ -640,9 +661,7 @@ pub unsafe extern "thiscall" fn play_sound_pooled( /// Slot 14: sub_destructor — sets secondary vtable (0x66AF58), optionally frees. /// This is a base-class destructor called by the primary destructor (slot 0). -pub unsafe extern "thiscall" fn sub_destructor( - this: *mut DSSound, flags: u8, -) -> *mut DSSound { +pub unsafe extern "thiscall" fn sub_destructor(this: *mut DSSound, flags: u8) -> *mut DSSound { use crate::rebase::rb; // Set secondary vtable (base class vtable for destructor chain). (*this).vtable = rb(0x0066_AF58) as *const DSSoundVtable; @@ -661,9 +680,7 @@ pub unsafe extern "thiscall" fn sub_destructor( /// - **0** if pool_id is out of range /// /// The original inverts DSBSTATUS_PLAYING (bit 0): `NOT status; AND 1`. -pub unsafe extern "thiscall" fn is_channel_finished( - this: *mut DSSound, pool_id: i32, -) -> u8 { +pub unsafe extern "thiscall" fn is_channel_finished(this: *mut DSSound, pool_id: i32) -> u8 { let idx = (pool_id - 1) as usize; if idx >= 64 { return 0; @@ -677,7 +694,11 @@ pub unsafe extern "thiscall" fn is_channel_finished( let Some(buf) = desc.buffer() else { return 1 }; match buf.GetStatus() { Ok(status) => { - if status & 1 != 0 { 0 } else { 1 } // playing → 0, stopped → 1 + if status & 1 != 0 { + 0 + } else { + 1 + } // playing → 0, stopped → 1 } Err(_) => 1, // error → treat as finished } @@ -685,9 +706,7 @@ pub unsafe extern "thiscall" fn is_channel_finished( /// Slot 10: stop_channel — stops a buffer, releases it, and returns the pool entry. /// `pool_id` is 1-based (1..64). Returns 1 on success, 0 on invalid. -pub unsafe extern "thiscall" fn stop_channel( - this: *mut DSSound, pool_id: i32, -) -> u32 { +pub unsafe extern "thiscall" fn stop_channel(this: *mut DSSound, pool_id: i32) -> u32 { let idx = (pool_id - 1) as usize; if idx >= 64 { return 0; @@ -752,8 +771,7 @@ pub unsafe extern "thiscall" fn load_wav( path: *const u8, ) -> u32 { use windows::Win32::Media::Audio::DirectSound::{ - IDirectSound, IDirectSoundBuffer, - DSBUFFERDESC, DSBLOCK_ENTIREBUFFER, + IDirectSound, IDirectSoundBuffer, DSBLOCK_ENTIREBUFFER, DSBUFFERDESC, }; use windows::Win32::Media::Audio::{WAVEFORMATEX, WAVE_FORMAT_PCM}; @@ -821,12 +839,18 @@ pub unsafe extern "thiscall" fn load_wav( // Lock the buffer and fill with PCM data. let mut audio_ptr1: *mut core::ffi::c_void = core::ptr::null_mut(); let mut audio_len1: u32 = 0; - if buf.Lock( - 0, data_len, - &mut audio_ptr1, &mut audio_len1, - None, None, - DSBLOCK_ENTIREBUFFER, - ).is_err() { + if buf + .Lock( + 0, + data_len, + &mut audio_ptr1, + &mut audio_len1, + None, + None, + DSBLOCK_ENTIREBUFFER, + ) + .is_err() + { return 0; } @@ -843,7 +867,10 @@ pub unsafe extern "thiscall" fn load_wav( 16 => { let mut reader = match hound::WavReader::open(c_path) { Ok(r) => r, - Err(_) => { let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0); return 0; } + Err(_) => { + let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0); + return 0; + } }; let mut offset = 0usize; for sample in reader.samples::() { @@ -858,7 +885,10 @@ pub unsafe extern "thiscall" fn load_wav( 8 => { let mut reader = match hound::WavReader::open(c_path) { Ok(r) => r, - Err(_) => { let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0); return 0; } + Err(_) => { + let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0); + return 0; + } }; let mut offset = 0usize; for sample in reader.samples::() { diff --git a/crates/openwa-core/src/audio/mod.rs b/crates/openwa-core/src/audio/mod.rs index 2d4a9b5db18302c444485513a23974c6568aa240..558a19401dc312b41adc1bee044cc7b86998ed35 100644 --- a/crates/openwa-core/src/audio/mod.rs +++ b/crates/openwa-core/src/audio/mod.rs @@ -6,16 +6,14 @@ pub mod speech; pub mod streaming; pub use active_sound::{ActiveSoundEntry, ActiveSoundTable}; -pub use dssound::{DSSound, DSSoundVtable, ChannelDescriptor}; pub use dssound::{ - destructor as dssound_destructor, - update_channels, release_finished, - is_slot_loaded, is_channel_finished, stop_channel, - play_sound, play_sound_pooled, - set_volume_params, set_master_volume, set_channel_volume, set_pan, - sub_destructor as dssound_sub_destructor, load_wav, - noop as dssound_noop, returns_0 as dssound_returns_0, returns_1 as dssound_returns_1, + destructor as dssound_destructor, is_channel_finished, is_slot_loaded, load_wav, + noop as dssound_noop, play_sound, play_sound_pooled, release_finished, + returns_0 as dssound_returns_0, returns_1 as dssound_returns_1, set_channel_volume, + set_master_volume, set_pan, set_volume_params, stop_channel, + sub_destructor as dssound_sub_destructor, update_channels, }; +pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable}; pub use music::Music; pub use sound::SoundId; pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT}; diff --git a/crates/openwa-core/src/display/palette.rs b/crates/openwa-core/src/display/palette.rs index bbdb8b8dff8b982acb4dcf1ec4b232fdcc961b09..a987ef3e559a5d61d5b360ae9f2b08c5d802fe7d 100644 --- a/crates/openwa-core/src/display/palette.rs +++ b/crates/openwa-core/src/display/palette.rs @@ -49,9 +49,15 @@ impl Palette { } /// Vtable[4]: Reset palette state. - pub unsafe fn reset(&mut self) { vcall!(self, reset) } + pub unsafe fn reset(&mut self) { + vcall!(self, reset) + } /// Vtable[3]: Initialize palette. - pub unsafe fn init(&mut self) { vcall!(self, init) } + pub unsafe fn init(&mut self) { + vcall!(self, init) + } /// Vtable[2]: Set palette mode. - pub unsafe fn set_mode(&mut self, mode: u32) { vcall!(self, set_mode, mode) } + pub unsafe fn set_mode(&mut self, mode: u32) { + vcall!(self, set_mode, mode) + } } diff --git a/crates/openwa-core/src/engine/ddgame.rs b/crates/openwa-core/src/engine/ddgame.rs index 4e34a445e0825ec0d86624561e1c66a03c3050fb..f79ce60f7cc2fc01ca2e6c247be0535f48b2cb7e 100644 --- a/crates/openwa-core/src/engine/ddgame.rs +++ b/crates/openwa-core/src/engine/ddgame.rs @@ -1,14 +1,19 @@ +use crate::address::va; use crate::audio::active_sound::ActiveSoundTable; -use crate::display::dd_display::DDDisplay; -use crate::input::keyboard::DDKeyboard; use crate::audio::dssound::DSSound; -use crate::engine::game_info::GameInfo; -use crate::render::landscape::PCLandscape; use crate::audio::music::Music; +use crate::audio::speech::SpeechSlotTable; +use crate::display::dd_display::DDDisplay; use crate::display::palette::Palette; +use crate::engine::ddgame_wrapper::DDGameWrapper; +use crate::engine::game_info::GameInfo; +use crate::input::keyboard::DDKeyboard; +use crate::log::log_line; +use crate::rebase::rb; +use crate::render::landscape::PCLandscape; use crate::render::queue::RenderQueue; -use crate::audio::speech::SpeechSlotTable; use crate::render::turn_order::TurnOrderWidget; +use crate::wa_alloc::wa_malloc; /// DDGame — the main game engine object. /// @@ -346,6 +351,1769 @@ pub struct DDGame { const _: () = assert!(core::mem::size_of::() == 0x98B8); +// ============================================================ +// DDGame constructor — replaces DDGame__Constructor (0x56E220) +// ============================================================ +// +// Despite being named DDGame__Constructor, the original function +// receives DDGameWrapper* as `this` and creates DDGame internally. +// It populates fields on BOTH the wrapper and the inner DDGame. +// The Rust entry point is DDGameWrapper::create_game() in +// ddgame_wrapper.rs; the bulk of the logic lives here because +// it primarily initializes DDGame fields. + +// ============================================================ +// Pure Rust implementations of DDGame sub-functions +// ============================================================ +// These are called both by create_ddgame() and by MinHook +// trampolines in openwa-wormkit/replacements/ddgame_init.rs. + +/// Pure Rust implementation of DDGame__InitFields (0x526120). +/// +/// Zeroes stride-0x194 table entries, calls init_render_indices, +/// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx. +/// +/// # Safety +/// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98B8 bytes). +pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) { + let base = ddgame as *mut u8; + + // Zero the stride-0x194 table (10 entries) + for &off in &[ + 0x379Cusize, + 0x3930, + 0x3AC4, + 0x3C58, + 0x3DEC, + 0x3F80, + 0x4114, + 0x42A8, + 0x443C, + 0x45D0, + ] { + *(base.add(off as usize) as *mut u32) = 0; + } + + *(base.add(0x64D8) as *mut u32) = 0; + *(base.add(0x72A4) as *mut u32) = 0; + + // InitRenderIndices — original sets ESI = ddgame + 0x72D8 before calling + ddgame_init_render_indices(base.add(0x72D8)); + + // Zero 8 fields at 0x8Cxx + for &off in &[ + 0x8CBCusize, + 0x8CC0, + 0x8CCC, + 0x8CD0, + 0x8CDC, + 0x8CE0, + 0x8CEC, + 0x8CF0, + ] { + *(base.add(off as usize) as *mut u32) = 0; + } + + // Zero 8 fields at 0x98xx + for &off in &[ + 0x9850usize, + 0x9854, + 0x9860, + 0x9864, + 0x9870, + 0x9874, + 0x9880, + 0x9884, + ] { + *(base.add(off as usize) as *mut u32) = 0; + } +} + +/// Pure Rust implementation of DDGame__InitRenderIndices (0x526080). +/// +/// Convention: usercall(ESI=base_ptr), plain RET. +/// +/// **Important:** The base pointer is NOT the DDGame pointer! +/// InitFields calls this with ESI = ddgame + 0x72D8. +/// All offsets are relative to whatever ESI points to. +/// +/// Absolute DDGame offsets (base = ddgame+0x72D8): +/// - base+0xC4 = ddgame+0x739C +/// - base+0xD8 = ddgame+0x73B0 (eh_vector_constructor_iterator region) +/// - base+0x378 = ddgame+0x7650 (team_index_map_1) +/// - base+0x3DC = ddgame+0x76B4 (team_index_map_2) +/// - base+0x440 = ddgame+0x7718 (team_index_map_3) +/// +/// # Safety +/// `base` must point to a valid memory region with at least 0x4A4 bytes. +pub unsafe fn ddgame_init_render_indices(base: *mut u8) { + // Zero base+0xC4 + *(base.add(0xC4) as *mut u32) = 0; + + // eh_vector_constructor_iterator equivalent: + // FUN_525F40 is fastcall { *param_1 = 0; } + // 14 entries at stride 0x14 starting from +0xD8 + for i in 0..14usize { + *(base.add(0xD8 + i * 0x14) as *mut u32) = 0; + } + + // Identity permutation 1: base+0x378 (= ddgame+0x7650), 16 entries (i16) + for i in 0..16i16 { + *(base.add(0x378 + i as usize * 2) as *mut i16) = i; + } + *(base.add(0x398) as *mut u16) = 0x10; + *(base.add(0x3DA) as *mut u16) = 0; + + // Identity permutation 2: base+0x3DC (= ddgame+0x76B4), 16 entries (i16) + for i in 0..16i16 { + *(base.add(0x3DC + i as usize * 2) as *mut i16) = i; + } + *(base.add(0x3FC) as *mut u16) = 0x10; + *(base.add(0x43E) as *mut u16) = 0; + + // Identity permutation 3: base+0x440 (= ddgame+0x7718), 16 entries (i16) + for i in 0..16i16 { + *(base.add(0x440 + i as usize * 2) as *mut i16) = i; + } + *(base.add(0x4A2) as *mut u16) = 0; + *(base.add(0x460) as *mut u16) = 0x10; +} + +/// Pure Rust implementation of TaskStateMachine__Init (0x4F6370). +/// +/// Convention: usercall(ESI=object, ECX=param1, EDI=height) + 1 stack(width), RET 0x4. +/// +/// Allocates a bit-per-cell grid buffer. `param1` is typically 1 (cells per unit). +/// `width` and `height` are pixel dimensions. The buffer is a row-major bitfield +/// with rows aligned to 4 bytes. +/// +/// # Safety +/// `object` must point to a zero-filled allocation of at least 0x2C bytes. +pub unsafe fn task_state_machine_init(object: *mut u8, param1: u32, width: u32, height: u32) { + // Row stride: bits-to-bytes rounded up, then aligned to 4 + let bits = param1.wrapping_mul(width).wrapping_add(7) as i32; + let row_stride = ((bits >> 3) + 3) & !3; + let total_size = row_stride as u32 * height; + + // Allocate data buffer with 0x20-byte header + let alloc_size = ((total_size + 3) & !3) + 0x20; + let buffer = wa_malloc(alloc_size); + + let _ = crate::log::log_line(&format!( + "[TSM] init: obj=0x{:08X} p1={} w={} h={} stride={} total={} alloc={} buf=0x{:08X}", + object as u32, param1, width, height, row_stride, total_size, alloc_size, buffer as u32, + )); + + if buffer.is_null() { + let _ = crate::log::log_line("[TSM] WARN: wa_malloc returned null, skipping memset"); + return; + } + // Guard against integer overflow producing tiny alloc_size with huge total_size + if total_size as usize > alloc_size as usize { + let _ = crate::log::log_line(&format!( + "[TSM] OVERFLOW: total_size {} > alloc_size {}, skipping memset", + total_size, alloc_size, + )); + return; + } + + // Memset twice (matches original — likely redundant but exact match) + core::ptr::write_bytes(buffer, 0, total_size as usize); + core::ptr::write_bytes(buffer, 0, total_size as usize); + + // Set object fields (DWORD array layout) + let obj = object as *mut u32; + *obj.add(0) = rb(0x6640EC); // vtable + *obj.add(1) = 0; // unused + *obj.add(2) = buffer as u32; // data pointer + *obj.add(3) = param1; // param1 + *obj.add(4) = row_stride as u32; // row stride + *obj.add(5) = width; // width + *obj.add(6) = height; // height + *obj.add(7) = 0; // unused + *obj.add(8) = 0; // unused + *obj.add(9) = width; // width (duplicate) + *obj.add(10) = height; // height (duplicate) +} + +/// Pure Rust implementation of FUN_570E20 (display layer color init). +/// +/// Convention: usercall(ESI=wrapper), plain RET. +/// +/// Sets display layer color parameters via display->vtable[4](layer, color). +/// Layer 1 color depends on gfx_mode and game_version. +/// Layer 2 = 0x20, Layer 3 = 0x70. +/// +/// # Safety +/// `wrapper` must be a valid DDGameWrapper with initialized display and ddgame. +#[cfg(target_arch = "x86")] +pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) { + let ddgame = (*wrapper).ddgame; + let game_info = (*ddgame).game_info; + let game_version = (*game_info).game_version; + + // wrapper+0x4C8 is gfx_mode (at DDGameWrapper.gfx_mode) + let layer1_color = if (*wrapper).gfx_mode == 0 { + // (game_version > -2) - 1: yields 0 if true, -1 if false + // Then + 0x69: yields 0x69 or 0x68 + if game_version > -2 { + 0x69i32 + } else { + 0x68i32 + } + } else { + 5 + 0x69 // = 0x6E + }; + + let display = (*wrapper).display as *mut u8; + let vt = *(display as *const *const u32); + // vtable[4] (offset 0x10): thiscall(display, layer, color), RET 0x8 + let set_color: unsafe extern "thiscall" fn(*mut u8, i32, i32) = + core::mem::transmute(*vt.add(4)); + + set_color(display, 1, layer1_color); + set_color(display, 2, 0x20); + set_color(display, 3, 0x70); +} + +/// Hash function for GfxDir entry lookup (FUN_566390). +/// +/// 10-bit CRC-like hash over the global name buffer at 0x8ACF58. +/// Operates on the already-lowercased name. +/// +/// # Safety +/// `name` must be a valid null-terminated C string pointer. +unsafe fn gfx_dir_hash(name: *const u8) -> u32 { + let mut hash: u32 = 0; + let mut p = name; + while *p != 0 { + let bit9 = (hash >> 9) & 1; + hash = ((hash << 1) | bit9) & 0x3FF; + hash = hash.wrapping_add(*p as u32) & 0x3FF; + p = p.add(1); + } + hash +} + +/// Pure Rust implementation of GfxDir__FindEntry (0x566520). +/// +/// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4. +/// +/// Looks up a name in the GfxHandler's hash table. Names are case-insensitive. +/// Supports `|`-separated fallback names (e.g. "path\\file.img|fallback.img"). +/// +/// Returns entry pointer or null. Entry layout: +/// - entry+0x00: next pointer (linked list) +/// - entry+0x04: value (passed to vtable[2] for cached load) +/// - entry+0x08: unknown +/// - entry+0x0C: name string (null-terminated, lowercase) +/// +/// # Safety +/// `gfx_handler` must be a valid GfxHandler with initialized hash table at +0x04. +/// `name` must be a valid null-terminated C string. +#[cfg(target_arch = "x86")] +pub unsafe fn gfx_dir_find_entry(name: *const u8, gfx_handler: *mut u8) -> *mut u8 { + let mut current_name = name; + + loop { + // Copy name to stack buffer (max 0xFF chars + null) + let mut buf = [0u8; 0x100]; + let mut i = 0usize; + let mut src = current_name; + while *src != 0 && i < 0xFF { + buf[i] = *src; + src = src.add(1); + i += 1; + } + buf[i] = 0; + + // Find '|' separator in buffer + let mut pipe_pos: Option = None; + for j in 0..i { + if buf[j] == b'|' { + buf[j] = 0; // truncate at pipe + pipe_pos = Some(j); + break; + } + } + + // Compute next_name: pointer into original string after '|' + let next_name: *const u8 = if let Some(pos) = pipe_pos { + // Offset of '|' in buffer = pos + // Same offset in original string → current_name + pos + 1 + current_name.add(pos + 1) + } else { + core::ptr::null() + }; + + // Lowercase A-Z in buffer + for j in 0..buf.len() { + if buf[j] == 0 { + break; + } + if buf[j] >= b'A' && buf[j] <= b'Z' { + buf[j] += 0x20; + } + } + + // Hash the lowercased name + let bucket = gfx_dir_hash(buf.as_ptr()); + + // Walk linked list in hash bucket + // gfx_handler+4 = pointer to bucket array (1024 entries) + let bucket_array = *(gfx_handler.add(4) as *const *const u32); + let mut entry = *bucket_array.add(bucket as usize) as *mut u8; + + while !entry.is_null() { + // Compare entry name at entry+0x0C with our buffer + let entry_name = entry.add(0x0C); + let mut match_found = true; + let mut k = 0usize; + loop { + let a = *entry_name.add(k); + let b = buf[k]; + if a != b { + match_found = false; + break; + } + if a == 0 { + break; + } + k += 1; + } + + if match_found { + return entry; + } + + // Next in linked list: entry+0x00 + entry = *(entry as *const *mut u8); + } + + // Not found — try fallback name after '|' + if next_name.is_null() { + return core::ptr::null_mut(); + } + current_name = next_name; + } +} + +/// Pure Rust implementation of FUN_5665F0 (GfxHandler reset). +/// +/// Convention: usercall(ESI=handler), plain RET. Called at start of LoadDir. +unsafe fn gfx_handler_reset(handler: *mut u8) { + *(handler.add(0x04) as *mut u32) = 0; + *(handler.add(0x08) as *mut u32) = 0; + *(handler.add(0x194) as *mut u32) = 0; + + for i in 0..16u32 { + // handler+0x0C + i*0x10 = 0 (16 entries at stride 0x10) + *(handler.add(0x0C + i as usize * 0x10) as *mut u32) = 0; + // handler+0x10C + i*4 = i (index array) + *(handler.add(0x10C + i as usize * 4) as *mut u32) = i; + } + + *(handler.add(400) as *mut u32) = 0; // 0x190 + *(handler.add(0x18C) as *mut u32) = 0x10; +} + +/// Pure Rust implementation of GfxHandler__LoadDir (0x5663E0). +/// +/// Convention: usercall(EAX=handler), plain RET. Returns 1 on success, 0 on failure. +/// +/// Reads a .dir file through the handler's vtable I/O methods: +/// - vtable[0]: read(buf, size) → thiscall, returns bytes read +/// - vtable[1]: seek/reposition(size) → thiscall +/// - vtable[2]: allocate(size) → thiscall, returns buffer or null +/// +/// .dir file format: +/// - 4 bytes: magic "DIR\x1A" (0x1A524944) +/// - 4 bytes: total_file_size +/// - 4 bytes: data_size (hash table + entries) +/// - data: 1024-bucket hash table followed by linked list nodes +/// All pointers are relative offsets from (data_start + 4) +/// +/// # Safety +/// `handler` must be a valid GfxHandler with file handle at +0x198. +#[cfg(target_arch = "x86")] +pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 { + gfx_handler_reset(handler); + + let vt = *(handler as *const *const u32); + // vtable[0] = read: thiscall(handler, buf, size) -> bytes_read + let vt_read: unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) -> u32 = + core::mem::transmute(*vt); + // vtable[1] = seek: thiscall(handler, size) + let vt_seek: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*vt.add(1)); + // vtable[2] = allocate: thiscall(handler, size) -> *mut u8 + let vt_alloc: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 = + core::mem::transmute(*vt.add(2)); + + // Read and validate magic + let mut magic: u32 = 0; + if vt_read(handler, &mut magic as *mut u32 as *mut u8, 4) != 4 { + return 0; + } + if magic != 0x1A524944 { + // "DIR\x1A" + return 0; + } + + // Read total_file_size and data_size + let mut total_file_size: u32 = 0; + if vt_read(handler, &mut total_file_size as *mut u32 as *mut u8, 4) != 4 { + return 0; + } + let mut data_size: u32 = 0; + if vt_read(handler, &mut data_size as *mut u32 as *mut u8, 4) != 4 { + return 0; + } + + let alloc_size = data_size + 4; + + // Try fast path: vtable[2] allocate (memory-maps the data) + let data = vt_alloc(handler, alloc_size); + *(handler.add(4) as *mut *mut u8) = data; + + if data.is_null() { + // Fallback: seek past header, then malloc + read entire data block. + // Original calls vt_seek(alloc_size) then WA_MallocMemset(EDI=read_size). + // WA_MallocMemset allocates (size+3)&~3 + 0x20 and memsets size bytes. + vt_seek(handler, alloc_size); + + let read_size = total_file_size - data_size - 4; + let malloc_size = ((read_size + 3) & !3) + 0x20; + let buf = wa_malloc(malloc_size); + if buf.is_null() { + return 0; + } + core::ptr::write_bytes(buf, 0, read_size as usize); + *(handler.add(4) as *mut *mut u8) = buf; + + let bytes_read = vt_read(handler, buf, read_size); + if bytes_read != read_size { + crate::wa_alloc::wa_free(buf); + return 0; + } + + // Mark as "loaded from fallback" at handler+8 + *(handler.add(8) as *mut u32) = 1; + } + + // Fix up relative pointers in the hash table + // 1024 buckets at data[0..0x1000], each is a pointer to a linked list node + let data = *(handler.add(4) as *const *mut u8); + let base = data as u32; + + for bucket in 0..1024u32 { + let bucket_ptr = data.add(bucket as usize * 4) as *mut u32; + let entry_offset = *bucket_ptr; + if entry_offset == 0 { + continue; + } + + // Convert relative offset to absolute: offset + base - 4 + let entry_addr = entry_offset.wrapping_add(base).wrapping_sub(4); + *bucket_ptr = entry_addr; + + // Walk linked list, fix up each next pointer + let mut node = entry_addr as *mut u32; + loop { + if node.is_null() { + break; + } + let next_offset = *node; + if next_offset == 0 { + break; + } + let next_addr = next_offset.wrapping_add(base).wrapping_sub(4); + *node = next_addr; + node = next_addr as *mut u32; + } + } + + // Mark as loaded + *(handler.add(0x194) as *mut u32) = 1; + + 1 // success +} + +/// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300). +/// +/// Convention: usercall(ECX=gfx_handler, EAX=name) + 1 stack(output), RET 0x4. +/// +/// Looks up `name` in the GfxHandler's directory, tries cached load, wraps +/// as DisplayGfx. Falls back to loading the raw image and decoding it. +/// +/// # Safety +/// All pointers must be valid WA objects. +#[cfg(target_arch = "x86")] +pub unsafe fn gfx_resource_create( + gfx_handler: *mut u8, + name: *const u8, + output: *mut u8, +) -> *mut u8 { + let _ = crate::log::log_line(&format!( + "[GfxRes] create: handler=0x{:08X} name=0x{:08X} out=0x{:08X}", + gfx_handler as u32, name as u32, output as u32, + )); + // 1. Try FindEntry → cached load → DisplayGfx wrap + let entry = gfx_dir_find_entry(name, gfx_handler); + let _ = crate::log::log_line(&format!("[GfxRes] entry=0x{:08X}", entry as u32)); + if !entry.is_null() { + // gfx_handler->vtable[2](entry->field_4) — cached load + let vt = *(gfx_handler as *const *const u32); + let _ = crate::log::log_line(&format!("[GfxRes] vt=0x{:08X}", vt as u32)); + let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 = + core::mem::transmute(*vt.add(2)); + let entry_val = *(entry.add(4) as *const u32); + let cached = load_cached(gfx_handler, entry_val); + let _ = crate::log::log_line(&format!("[GfxRes] cached=0x{:08X}", cached as u32)); + if !cached.is_null() { + // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4 + let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 = + core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize); + return ctor(cached); + } + } + + // 2. Fallback: LoadImage → IMG_Decode + let _ = crate::log::log_line("[GfxRes] fallback: LoadImage"); + let raw_image = call_gfx_load_image(gfx_handler, name); + let _ = crate::log::log_line(&format!("[GfxRes] raw_image=0x{:08X}", raw_image as u32)); + if raw_image.is_null() { + return core::ptr::null_mut(); + } + + // IMG_Decode: stdcall(output, raw_image, 1), RET 0xC + let _ = crate::log::log_line("[GfxRes] calling IMG_Decode"); + let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 = + core::mem::transmute(rb(va::IMG_DECODE) as usize); + let result = decode(output, raw_image, 1); + let _ = crate::log::log_line(&format!( + "[GfxRes] IMG_Decode result=0x{:08X}", + result as u32 + )); + + // Release raw image: raw_image->vtable[0](1) + let img_vt = *(raw_image as *const *const u32); + let fn_ptr = *img_vt; + let slot_idx = *(raw_image.add(4) as *const u32); + let stored_handler = *(raw_image.add(8) as *const u32); + let _ = crate::log::log_line(&format!( + "[GfxRes] release: raw=0x{:08X} vt=0x{:08X} fn=0x{:08X} slot={} handler=0x{:08X}", + raw_image as u32, img_vt as u32, fn_ptr as u32, slot_idx, stored_handler, + )); + let _ = crate::log::log_line(&format!( + "[GfxRes] handler_usedcnt={} handler_freecnt={}", + *(gfx_handler.add(0x190) as *const u32), + *(gfx_handler.add(0x18C) as *const u32), + )); + let release: unsafe extern "thiscall" fn(*mut u8, i32) = core::mem::transmute(fn_ptr); + let _ = crate::log::log_line("[GfxRes] calling release..."); + release(raw_image, 1); + let _ = crate::log::log_line("[GfxRes] release done"); + let _ = crate::log::log_line(&format!( + "[GfxRes] heap-ok raw=0x{:08X} result=0x{:08X}", + raw_image as u32, result as u32 + )); + let _ = crate::log::log_line("[GfxRes] before return"); + + result +} + +/// Runtime address of DDGame__InitFields (set by init_constructor_addrs()). +static mut INIT_FIELDS_ADDR: u32 = 0; +static mut GFX_LOAD_DIR_ADDR: u32 = 0; + +/// Initialize runtime addresses for the constructor bridges. +/// Must be called once at DLL startup (from lib.rs or similar). +pub fn init_constructor_addrs() { + unsafe { + INIT_FIELDS_ADDR = rb(va::DDGAME_INIT_FIELDS); + SPRITE_REGION_CTOR_ADDR = rb(va::SPRITE_REGION_CONSTRUCTOR); + FUN_570E20_ADDR = rb(va::FUN_570E20); + FUN_570A90_ADDR = rb(va::FUN_570A90); + GFX_RESOURCE_CREATE_ADDR = rb(va::GFX_RESOURCE_CREATE); + TSM_INIT_ADDR = rb(va::TASK_STATE_MACHINE_INIT); + GFX_FIND_ENTRY_ADDR = rb(va::GFX_DIR_FIND_ENTRY); + GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE); + GFX_LOAD_DIR_ADDR = rb(va::GFX_HANDLER_LOAD_DIR); + FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS); + LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS); + LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK); + } +} + +/// Bridge to DDGame__InitFields (0x526120). +/// Convention: usercall(EDI=ddgame), plain RET. +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_init_fields(_ddgame: *mut DDGame) { + core::arch::naked_asm!( + "pushl %edi", + "movl 8(%esp), %edi", + "calll *({addr})", + "popl %edi", + "retl", + addr = sym INIT_FIELDS_ADDR, + options(att_syntax), + ); +} + +/// Bridge to DDGame__InitVersionFlags (0x525BE0). +/// Convention: stdcall(ddgame_wrapper). +#[cfg(target_arch = "x86")] +unsafe fn call_init_version_flags(wrapper: *mut DDGameWrapper) { + let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) = + core::mem::transmute(rb(va::DDGAME_INIT_VERSION_FLAGS) as usize); + f(wrapper); +} + +/// Create and initialize DDGame, matching DDGame__Constructor (0x56E220). +/// +/// Allocates 0x98B8 bytes from WA's heap, initializes all fields, and creates +/// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the +/// returned DDGame. +/// +/// # Safety +/// All pointer params must be valid WA objects. `wrapper` must be a +/// partially-initialized DDGameWrapper (vtable, display, sound set). +#[cfg(target_arch = "x86")] +pub unsafe fn create_ddgame( + wrapper: *mut DDGameWrapper, + keyboard: *mut DDKeyboard, + display: *mut DDDisplay, + sound: *mut DSSound, + palette: *mut Palette, + music: *mut Music, + param7: *mut u8, // timer object (0x1F4 observed) + net_game: *mut u8, // from GameSession + game_info: *mut GameInfo, + network_ecx: u32, // implicit ECX from caller +) -> *mut DDGame { + // ── 1. Allocate and zero-fill (matches: memset(piVar3, 0, 0x98B8)) ── + let ddgame = wa_malloc(0x98B8) as *mut DDGame; + if ddgame.is_null() { + return core::ptr::null_mut(); + } + core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8); + + // ── 2. InitFields — pure Rust (replaces usercall bridge) ── + ddgame_init_fields(ddgame); + + // ── 3-4. Store params BEFORE exposing DDGame via wrapper ── + // Critical: game_info must be set before wrapper->ddgame, because the + // message pump (triggered by audio loading) can cause game tasks to + // read ddgame->game_info. If game_info is null, they crash. + (*ddgame).display = display; + (*ddgame).sound = sound; + (*ddgame).keyboard = keyboard; + (*ddgame).palette = palette; + (*ddgame).music = music; + (*ddgame)._param_018 = param7; + (*ddgame)._caller = network_ecx as *mut u8; + (*ddgame).game_info = game_info; + (*ddgame)._param_028 = net_game; + + // Now safe to expose — all fields that concurrent readers check are set. + (*wrapper).ddgame = ddgame; + + // Verify game_info is set + let _ = crate::log::log_line(&format!( + "[DDGame] exposed: ddgame=0x{:08X}, game_info=0x{:08X}, net_game=0x{:08X}", + ddgame as u32, game_info as u32, net_game as u32, + )); + + // ── 5. Set g_GameInfo global ── + *(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info; + + // ── 6. Sound available + always-1 flags ── + let is_headless = (*game_info).headless_mode != 0; + // Original: sound_available = (headless_mode == 0) — set EARLY. + // This enables the loading progress bar, message pump, and sound during + // construction. Previously deferred to avoid message pump crashes, but all + // critical DDGame fields (game_info, display, sound, etc.) are now set. + (*ddgame).sound_available = if is_headless { 0 } else { 1 }; + (*ddgame)._field_7efc = 1; + + // ── 7. DDGameWrapper+0x48C init ── + (*wrapper).ddgame_secondary = core::ptr::null_mut(); + + // ── 8. Conditional network object (game_version == -2) ── + if (*game_info).game_version == -2 { + let net_obj = wa_malloc(0x2C); + core::ptr::write_bytes(net_obj, 0, 0x2C); + *(net_obj as *mut *mut DDGame) = ddgame; + *net_obj.add(0x28) = (*game_info).net_config_1; + *net_obj.add(0x29) = (*game_info).net_config_2; + (*wrapper).ddgame_secondary = net_obj; + if network_ecx != 0 { + *((network_ecx as *mut u8).add(0x18) as *mut *mut u8) = net_obj; + } + } + + // ── 9. InitVersionFlags — sets DDGame+0x7E2E/0x7E2F/0x7E3F ── + let _ = crate::log::log_line("[DDGame] calling InitVersionFlags"); + call_init_version_flags(wrapper); + let _ = crate::log::log_line("[DDGame] calling init_graphics_and_resources"); + + // ── 10. GfxHandler, landscape, sprites, audio, resources ── + init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless); + + let _ = crate::log::log_line("[DDGame] create_ddgame complete"); + ddgame +} + +/// Second half of the constructor: GfxHandler, landscape, sprites, audio, resources. +/// +/// Separated for readability. Will be broken into smaller functions as +/// individual sections are converted to pure Rust. +#[cfg(target_arch = "x86")] +unsafe fn init_graphics_and_resources( + wrapper: *mut DDGameWrapper, + game_info: *mut GameInfo, + net_game: *mut u8, + display: *mut DDDisplay, + is_headless: bool, +) { + let ddgame = (*wrapper).ddgame; + let fopen: unsafe extern "cdecl" fn(*const u8, *const u8) -> *mut u8 = + core::mem::transmute(rb(va::WA_FOPEN) as usize); + // gfx_load_dir_addr removed — using direct Rust gfx_handler_load_dir() now + let gfx_handler_vtable = rb(0x66B280) as u32; // GfxHandler__vtable (from asm: MOV [ESI], 0x66B280) + + // ── GfxHandler #1 (primary) ── + let gfx1 = wa_malloc(0x19C); + core::ptr::write_bytes(gfx1, 0, 0x19C); + *(gfx1 as *mut u32) = gfx_handler_vtable; + (*wrapper)._field_4c0 = gfx1; + (*wrapper)._field_4c4 = core::ptr::null_mut(); + + // Build path list (order depends on headless + GameInfo+0xF374) + let f914 = (*game_info).headless_mode as i32; + let f374 = (*game_info).display_flags as i32; + let paths: [&[u8]; 3] = if f914 != 0 { + if f374 == 0 { + [ + b"data\\Gfx\\Gfx.dir\0", + b"data\\Gfx\\Gfx0.dir\0", + b"data\\Gfx\\Gfx1.dir\0", + ] + } else { + [ + b"data\\Gfx\\Gfx.dir\0", + b"data\\Gfx\\Gfx1.dir\0", + b"data\\Gfx\\Gfx0.dir\0", + ] + } + } else if f374 == 0 { + [ + b"data\\Gfx\\Gfx0.dir\0", + b"data\\Gfx\\Gfx1.dir\0", + b"data\\Gfx\\Gfx.dir\0", + ] + } else { + [ + b"data\\Gfx\\Gfx1.dir\0", + b"data\\Gfx\\Gfx0.dir\0", + b"data\\Gfx\\Gfx.dir\0", + ] + }; + + let mut gfx_loaded_idx = 0u32; + for (i, path) in paths.iter().enumerate() { + let fp = fopen(path.as_ptr(), b"rb\0".as_ptr()); + *(gfx1.add(0x198) as *mut *mut u8) = fp; + if !fp.is_null() && call_gfx_load_dir(gfx1, GFX_LOAD_DIR_ADDR) != 0 { + gfx_loaded_idx = i as u32; + break; + } + if i == 2 { + panic!("DDGame: couldn't open any Gfx.dir"); + } + } + + let headless_offset = if f914 != 0 { 1u32 } else { 0u32 }; + (*wrapper).gfx_mode = if gfx_loaded_idx.wrapping_sub(headless_offset) < 2 { + 1 + } else { + 0 + }; + + // ── GfxHandler #2 (conditional) ── + let d778 = (*game_info).game_version; + // Original: (-(gfx_mode != 0) & 0x23) - 2 → gfx_mode ? 33 : -2 + let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 }; + if d778 < threshold { + let c_digit = if d778 > -3 { b'2' } else { b'1' }; + let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0"; + gfx_c_path[14] = c_digit; + + let gfx2 = wa_malloc(0x19C); + core::ptr::write_bytes(gfx2, 0, 0x19C); + *(gfx2 as *mut u32) = gfx_handler_vtable; + (*wrapper)._field_4c4 = gfx2; + + let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr()); + *(gfx2.add(0x198) as *mut *mut u8) = fp; + if fp.is_null() || call_gfx_load_dir(gfx2, GFX_LOAD_DIR_ADDR) == 0 { + let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr()); + *(gfx2.add(0x198) as *mut *mut u8) = fp2; + if fp2.is_null() || call_gfx_load_dir(gfx2, GFX_LOAD_DIR_ADDR) == 0 { + panic!("DDGame: couldn't open secondary Gfx.dir"); + } + } + } + + // ── Display palette setup (non-headless) ── + if !is_headless { + if *(rb(0x88E485) as *const u8) == 0 { + call_usercall_eax(wrapper, FUN_570A90_ADDR); + } + // DDGameWrapper+0x4D0 points to the display object used for palette calls. + // BUT: FUN_00570A90 or something else may write to wrapper in ways we + // don't expect. Read via struct field instead of raw offset. + let disp = (*wrapper).display as *mut u8; + let gfx_handler = (*wrapper)._field_4c0; + let vt = *(disp as *const *const u32); + // vtable[4] (offset 0x10): thiscall + 2 stack params (1, 0xFE), RET 0x8 + let vt_10: unsafe extern "thiscall" fn(*mut u8, i32, i32) = + core::mem::transmute(*vt.add(4)); + vt_10(disp, 1, 0xFE); + // vtable[0x1F] (offset 0x7C): thiscall + 5 stack params, RET 0x14 + let vt_7c: unsafe extern "thiscall" fn(*mut u8, i32, i32, i32, *mut u8, u32) = + core::mem::transmute(*vt.add(0x1F)); + vt_7c(disp, 1, 1, 0, gfx_handler, rb(0x66A3A8)); + // vtable[0x17] (offset 0x5C): thiscall + 2 stack params, RET 0x8 + let vt_5c: unsafe extern "thiscall" fn(*mut u8, i32, i32) = + core::mem::transmute(*vt.add(0x17)); + vt_5c(disp, 1, -100); + + // Palette slot range init + let palette_range_ptr = *(disp.add(0x3120) as *const *mut i16); + if !palette_range_ptr.is_null() && *(disp.add(0x3534) as *const i32) == 0 { + let start = *palette_range_ptr as u32; + let end = (*palette_range_ptr.add(1) as u32) + 1; + if start < end { + for i in start..end { + *(disp.add(0x312C + i as usize * 4) as *mut u32) = 1; + } + } + crate::wa_alloc::wa_free(*(disp.add(0x3120) as *const *mut u8)); + *(disp.add(0x3120) as *mut u32) = 0; + } + } + + // ── FUN_00570E20: usercall(ESI=wrapper), plain RET ── + // Runs for all modes — headless vtable[4] is 0x5231E0 (same as headful). + display_layer_color_init(wrapper); + + // ── Display vtable slot 5 (offset 0x14) ── + // Original: CALL EAX (vtable[5]), saves return value in ESI for use as + // the `output` parameter in the color-entries GfxResource__Create call below. + let layer_ctx = { + let vt = *((*ddgame).display as *const *const u32); + let f: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 = + core::mem::transmute(*vt.add(5)); + f((*ddgame).display, 1) + }; + + let _ = crate::log::log_line(&format!("[DDGame] gfx_mode={}", (*wrapper).gfx_mode)); + // ── GfxDir color entries DDGame+0x730C..0x732C ── + if (*wrapper).gfx_mode != 0 { + // Original: PUSH ESI (=layer_ctx from vtable[5] call), MOV EAX=0x66A3B4, CALL 0x4F6300 + // The layer_ctx is used as the output buffer, not a plain stack alloc. + let res = gfx_resource_create((*wrapper)._field_4c0, rb(0x66A3B4) as *const u8, layer_ctx); + if !res.is_null() { + let rvt = *(res as *const *const u32); + let _ = crate::log::log_line(&format!( + "[DDGame] color entries: res=0x{:08X} vt=0x{:08X} vt[4]=0x{:08X}", + res as u32, + rvt as u32, + *rvt.add(4), + )); + // vtable[4] = get_pixel: thiscall(this, x, y) -> color, RET 0x8. + let get_color: unsafe extern "thiscall" fn(*mut u8, u32, u32) -> u32 = + core::mem::transmute(*rvt.add(4)); + let mut off = 0x730Cu32; + let mut idx = 0u32; + while off < 0x732Du32 { + let c = get_color(res, idx, 0); + *((ddgame as *mut u8).add(off as usize) as *mut u32) = c; + if idx == 0 { + let _ = crate::log::log_line(&format!("[DDGame] color[0] = 0x{:08X}", c,)); + } + off += 4; + idx += 1; + } + // DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4. + // param_2 & 1 = free the object itself. + let release: unsafe extern "thiscall" fn(*mut u8, u8) = + core::mem::transmute(*rvt.add(3)); + release(res, 1); + } + } else { + // GFX_HANDLER_LOAD_SPRITES: SKIP for testing (gfx_mode==0 path) + } + + let _ = crate::log::log_line("[DDGame] past color entries"); + let _ = crate::log::log_line(&format!( + "[DDGame] 4c0=0x{:08X} 4c4=0x{:08X}", + (*wrapper)._field_4c0 as u32, + (*wrapper)._field_4c4 as u32 + )); + // ── Secondary GfxDir object (DDGame+0x2C, conditional) ── + if !(*wrapper)._field_4c4.is_null() { + let gfxdir2 = wa_malloc(0x70C); + core::ptr::write_bytes(gfxdir2, 0, 0x70C); + *(gfxdir2 as *mut u16) = 1; + *(gfxdir2.add(2) as *mut u16) = 0x5A; + // FUN_5411A0: usercall(EAX=gfxdir2), plain RET + call_usercall_eax(gfxdir2 as *mut DDGameWrapper, rb(0x5411A0)); + *(gfxdir2.add(0x708) as *mut u16) = 0; + (*ddgame).secondary_gfxdir = gfxdir2; + // GFX_HANDLER_LOAD_SPRITES: stdcall(wrapper, ddgame+0x7308, game_info+0xF374, 0), RET 0x10 + + let f: unsafe extern "stdcall" fn(*mut DDGameWrapper, *mut u8, u32, u32) = + core::mem::transmute(rb(va::GFX_HANDLER_LOAD_SPRITES) as usize); + f( + wrapper, + (ddgame as *mut u8).add(0x7308), + (*game_info).display_flags, + 0, + ); + } + + // ── DDGameWrapper field inits ── + (*wrapper)._field_4d8 = 0; + // Original: if (display == NULL) iVar4=0 else iVar4=*(byte*)(game_info+0x44C)*0x38+0x7E + // Ghidra mislabeled game_info as param8 — the original reads game_info, not net_game. + // Our headless display is non-null, so use is_headless as the condition. + if is_headless { + (*wrapper)._field_4dc = 0x2AD; + } else { + let byte_val = *(game_info as *const u8).add(0x44C) as u32; + (*wrapper)._field_4dc = byte_val * 0x38 + 0x7E + 0x2AD; + } + (*wrapper)._field_4e0 = 0xFFFFFF9C; // -100 + (*wrapper).speech_name_count = 0; + + // ── Audio init (non-headless + sound available) ── + let _ = crate::log::log_line("[DDGame] audio: start"); + if !is_headless { + // FUN_570F30: usercall(ESI=wrapper) + call_usercall_esi(wrapper, FUN_570F30_ADDR); + let _ = crate::log::log_line("[DDGame] audio: past 570F30"); + + if !(*ddgame).sound.is_null() { + // DSSound_LoadEffectWAVs: stdcall(wrapper) + let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) = + core::mem::transmute(rb(va::DSSOUND_LOAD_EFFECT_WAVS) as usize); + f(wrapper); + // DSSound_LoadAllSpeechBanks: the original is hooked to our Rust + // replacement (speech.rs), so the usercall bridge calls our code. + call_usercall_esi(wrapper, LOAD_SPEECH_BANKS_ADDR); + let _ = crate::log::log_line("[DDGame] audio: done"); + // Allocate ActiveSoundTable (0x608 bytes) + let ast = wa_malloc(0x608) as *mut ActiveSoundTable; + (*ast).ddgame = ddgame; + (*ast).counter = 0; + core::ptr::write_bytes(ast as *mut u8, 0, 0x600); + (*ddgame).active_sounds = ast; + } + } + + let _ = crate::log::log_line("[DDGame] past ActiveSoundTable"); + + // ── GfxResource: thiscall(ECX=gfx_handler) + EAX=name + 1 stack(output), RET 0x4 ── + // Test: call GfxResource with real params + let gfx_resource: *mut u8; + { + { + let gfx_handler = (*wrapper)._field_4c0; + let out_buf = wa_malloc(0x900); + core::ptr::write_bytes(out_buf, 0, 0x900); + // return moved to before color entries + let _ = crate::log::log_line("[DDGame] calling GfxResource_Create"); + gfx_resource = gfx_resource_create(gfx_handler, rb(0x66A3C0) as *const u8, out_buf); + let _ = crate::log::log_line("[DDGame] after gfx_resource_create"); + let _ = crate::log::log_line("[DDGame] GfxResource created (value elided)"); + } + } + + // ── PCLandscape (alloc 0xB44, stdcall 11 params) ── + // Allocate output buffers for landscape coordinate data (used later for coord_list) + let landscape_coords_buf = wa_malloc(0x1000); // 4KB for coord output (aiStack_978) + core::ptr::write_bytes(landscape_coords_buf, 0, 0x1000); + let landscape_byte_buf = wa_malloc(0x100); // generous for byte output (iStack_11f9) + core::ptr::write_bytes(landscape_byte_buf, 0, 0x100); + let stack_local_8 = wa_malloc(0x1000); // stack local for coord count + temp data + core::ptr::write_bytes(stack_local_8, 0, 0x1000); + + let landscape = { + let alloc = wa_malloc(0xB44); + core::ptr::write_bytes(alloc, 0, 0xB44); + if !alloc.is_null() { + let pc_ctor: unsafe extern "stdcall" fn( + *mut u8, + *mut DDGame, + *mut u8, // 1=this, 2=ddgame, 3=gfx_resource + *mut DDDisplay, + *const u8, // 4=display, 5=game_info+0xDAAC + *mut u8, + u32, // 6=&landscape_byte, 7=gfx_mode + *mut u8, + *mut u8, // 8=stack local, 9=coord output + *mut u8, + *mut u8, // 10=&ddgame+0x777C, 11=&ddgame+0x7780 + ) -> *mut u8 = core::mem::transmute(rb(va::PC_LANDSCAPE_CONSTRUCTOR) as usize); + + let result = pc_ctor( + alloc, + ddgame, + gfx_resource, + (*wrapper).display, // param 4: display (NOT wrapper!) + (*game_info).landscape_data_path.as_ptr(), // param 5 + landscape_byte_buf, // param 6 + (*wrapper).gfx_mode, // param 7 + stack_local_8, // param 8 + landscape_coords_buf, // param 9 + (ddgame as *mut u8).add(0x777C), // param 10 + (ddgame as *mut u8).add(0x7780), // param 11 + ); + let _ = crate::log::log_line(&format!( + "[DDGame] PCLandscape returned: 0x{:08X}, 777C=0x{:08X} 7780=0x{:08X}", + result as u32, + *((ddgame as *mut u8).add(0x777C) as *const u32), + *((ddgame as *mut u8).add(0x7780) as *const u32), + )); + // Dump landscape_coords_buf to find coordinate count and data format + let coords = landscape_coords_buf as *const u32; + let mut coord_count = 0u32; + // Count non-zero pairs (each coord is 2 u32s) + for j in 0..285 { + if *coords.add(j * 2) != 0 || *coords.add(j * 2 + 1) != 0 { + coord_count = j as u32 + 1; + } + } + let _ = crate::log::log_line(&format!( + "[DDGame] coords: first=[0x{:08X},0x{:08X}] count_est={} byte_buf[0]=0x{:02X}", + *coords, + *coords.add(1), + coord_count, + *landscape_byte_buf, + )); + // Also check stack_local_8 for count + let sl8 = stack_local_8 as *const u32; + let _ = crate::log::log_line(&format!( + "[DDGame] stack8: [0]=0x{:08X} [1]=0x{:08X} [2]=0x{:08X} [3]=0x{:08X}", + *sl8, + *sl8.add(1), + *sl8.add(2), + *sl8.add(3), + )); + // Store landscape pointer + (*wrapper).landscape = result as *mut PCLandscape; + (*ddgame).landscape = result as *mut PCLandscape; + result + } else { + (*wrapper).landscape = core::ptr::null_mut(); + (*ddgame).landscape = core::ptr::null_mut(); + core::ptr::null_mut() + } + }; + + // ── TaskStateMachine at DDGame+0x380 (alloc 0x4C, memset 0x2C) ── + // Pure Rust: allocate object, call task_state_machine_init, override vtable. + { + let tsm = wa_malloc(0x4C); + core::ptr::write_bytes(tsm, 0, 0x2C); + if !tsm.is_null() { + let width = (*ddgame).level_width; + let height = (*ddgame).level_height; + task_state_machine_init(tsm, 1, width, height); + *(tsm as *mut u32) = rb(0x664118); // Override vtable to TaskStateMachine + } + (*ddgame).task_state_machine = tsm; + } + + // ── 8× SpriteRegion at DDGame+0x46C..0x488 ── + // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18 + { + // Params verified by hooking original (see SpriteRegion param logger). + // (ddgame_offset, ECX, EDX, p2, p3, p4, p5, p6=gfx_resource) + // p6 is the GfxResource pointer returned by GfxResource__Create_Maybe. + let gr = gfx_resource as u32; + let regions: [(u32, u32, u32, u32, u32, u32, u32, u32); 8] = [ + (0x474, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, gr), + (0x46C, 0x30, 0x0C, 0x2D, 0x07, 0x34, 0x09, gr), + (0x470, 0x11, 0x1A, 0x0D, 0x0A, 0x16, 0x13, gr), + (0x478, 0x0C, 0x3D, 0x00, 0x20, 0x18, 0x33, gr), + (0x47C, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, gr), + (0x484, 0x1A2, 0x1B, 0x173, 0x09, 0x1D8, 0x03, gr), + (0x488, 0x1EF, 0x26, 0x1E5, 0x07, 0x1F9, 0x16, gr), + (0x480, 0x2D, 0x08, 0x2D, 0x07, 0x2E, 0x07, gr), + ]; + + for &(offset, ecx, edx, p2, p3, p4, p5, p6) in ®ions { + let alloc = wa_malloc(0x9C); + core::ptr::write_bytes(alloc, 0, 0x9C); + let result = if !alloc.is_null() { + call_sprite_region_ctor(alloc, ecx, edx, p2, p3, p4, p5, p6) + } else { + core::ptr::null_mut() + }; + *((ddgame as *mut u8).add(offset as usize) as *mut *mut u8) = result; + } + } + + // ── Landscape-derived value at DDGame+0x468 ── + if !landscape.is_null() { + let land_vt = *(landscape as *const *const u32); + let get_val: unsafe extern "thiscall" fn(*mut u8) -> u32 = + core::mem::transmute(*land_vt.add(0xB)); + *((ddgame as *mut u8).add(0x468) as *mut u32) = get_val(landscape); + } + + // NOTE: gfx_resource is NOT released here — arrow SpriteRegions need it. + // It's released after the arrow loop below. + + // ── Arrow sprites + collision regions (32 iterations) ── + { + let gfx_handler = (*wrapper)._field_4c0; + + for i in 0u32..32 { + // Format "arrow%02u.img\0" into stack buffer + let mut name_buf = *b"arrow00.img\0\0\0\0\0"; + name_buf[5] = b'0' + (i / 10) as u8; + name_buf[6] = b'0' + (i % 10) as u8; + + // Display vtable[5](1) — set active layer, returns context ptr + let disp_vt = *((*ddgame).display as *const *const u32); + let set_layer: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 = + core::mem::transmute(*disp_vt.add(5)); + let layer_ctx = set_layer((*ddgame).display, 1); + if i == 0 { + let _ = crate::log::log_line(&format!( + "[DDGame] arrow[0]: layer_ctx=0x{:08X}, gfx=0x{:08X}", + layer_ctx as u32, gfx_handler as u32 + )); + } + + let entry = gfx_dir_find_entry(name_buf.as_ptr(), gfx_handler); + if i == 0 { + let _ = crate::log::log_line(&format!( + "[DDGame] arrow[0]: entry=0x{:08X}", + entry as u32 + )); + } + + let sprite: *mut u8; + if !entry.is_null() { + // Try gfx_handler->vtable[2](entry->field_4) + let gfx_vt = *(gfx_handler as *const *const u32); + let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 = + core::mem::transmute(*gfx_vt.add(2)); + let entry_val = *(entry.add(4) as *const u32); + let cached = load_cached(gfx_handler, entry_val); + if i == 0 { + let _ = crate::log::log_line(&format!( + "[DDGame] arrow[0]: cached=0x{:08X}", + cached as u32 + )); + } + if !cached.is_null() { + // Wrap with DisplayGfx constructor + let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 = + core::mem::transmute(rb(0x4F5E80) as usize); + sprite = ctor(cached); + } else { + // Fallback: load from file via GfxDir__LoadImage + IMG_Decode + sprite = call_gfx_load_and_wrap(gfx_handler, name_buf.as_ptr(), layer_ctx); + } + } else { + // Entry not found — try direct file load + sprite = call_gfx_load_and_wrap(gfx_handler, name_buf.as_ptr(), layer_ctx); + } + if i == 0 { + let _ = crate::log::log_line(&format!( + "[DDGame] arrow[0]: sprite=0x{:08X}", + sprite as u32 + )); + } + + // Store arrow sprite at DDGame+0x38+i*4 + (*ddgame).arrow_sprites[i as usize] = sprite; + + // Calculate collision region dimensions from sprite + if !sprite.is_null() { + let sprite_w = *(sprite.add(0x14) as *const i32); + let sprite_h = *(sprite.add(0x18) as *const i32); + let half_w = (sprite_w / 2 - 10).max(0); + let half_h = (sprite_h / 2 - 10).max(0); + + // Create SpriteRegion for collision + let alloc = wa_malloc(0x9C); + core::ptr::write_bytes(alloc, 0, 0x9C); + let region = if !alloc.is_null() { + // SpriteRegion params: ECX, EDX, this, p2, p3, p4, p5, p6 + // this[3] = p4 - p2 (width), this[4] = EDX - p3 (height) + // For arrows: region from (0,0) to (half_w, half_h) + call_sprite_region_ctor( + alloc, + 0, // ECX (x_max for this[5]) + half_h as u32, // EDX (y_max → this[4] = EDX - p3) + 0, // p2 (x_offset) + 0, // p3 (y_offset) + half_w as u32, // p4 (x_limit → this[3] = p4 - p2) + half_h as u32, // p5 (y_limit for this[6]) + gfx_resource as u32, // p6 (gfx_resource) + ) + } else { + core::ptr::null_mut() + }; + (*ddgame).arrow_collision_regions[i as usize] = region; + } + + // Arrow GfxDir (conditional on secondary gfxdir) + if !(*ddgame).secondary_gfxdir.is_null() { + let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 = + core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize); + (*ddgame).arrow_gfxdirs[i as usize] = + gfx_resource_create(gfx_handler, core::ptr::null_mut()); + } + } + } + + let _ = crate::log::log_line("[DDGame] arrow loop done"); + + // Release gfx_resource AFTER arrow loop (arrows need it for SpriteRegions) + // vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4. + if !gfx_resource.is_null() { + let rvt = *(gfx_resource as *const *const u32); + let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*rvt.add(3)); + release(gfx_resource, 1); + } + + // ── DisplayGfx at DDGame+0x138 ── + // Original: alloc 0x4C, memset 0x2C, PUSH 0x100, MOV ECX=8, MOV EDI=0x1E0, + // CALL 0x4F6370, MOV [alloc]=0x664144 + { + let tsm = wa_malloc(0x4C); + core::ptr::write_bytes(tsm, 0, 0x2C); + if !tsm.is_null() { + // Original: TaskStateMachine__Init(ECX=8, EDI=0x1E0, stack=0x100) + // → task_state_machine_init(object, param1=8, width=0x100, height=0x1E0) + task_state_machine_init(tsm, 8, 0x100, 0x1E0); + *(tsm as *mut u32) = rb(0x664144); // Override vtable to DisplayGfx + } + (*ddgame).display_gfx = tsm; + } + + // ── CoordList at DDGame+0x50C (capacity 600, 0x12C0 buffer) ── + { + let cl = wa_malloc(12) as *mut u32; + *cl = 0; // count + *cl.add(1) = 600; // capacity + let data = wa_malloc(0x12C0); + core::ptr::write_bytes(data, 0, 0x12C0); + *cl.add(2) = data as u32; + (*ddgame).coord_list = cl as *mut u8; + + // Populate coord_list from PCLandscape's coordinate output. + // stack_local_8[0] = coordinate count, landscape_coords_buf = pairs of (x, y). + // Original packs as: coord = x * 0x10000 + y (Fixed-point). + // Each coord_list entry is 8 bytes: [coord_value, 1]. Duplicates are skipped. + let coord_count = *(stack_local_8 as *const u32); + let coords_src = landscape_coords_buf as *const u32; + let data_ptr = data as *mut u32; + for j in 0..coord_count as usize { + let x = *coords_src.add(j * 2); + let y = *coords_src.add(j * 2 + 1); + let coord_val = x.wrapping_mul(0x10000).wrapping_add(y); + let cur_count = *cl as usize; + if cur_count >= 600 { + break; + } + // Check for duplicates + let mut dup = false; + for k in 0..cur_count { + if *data_ptr.add(k * 2) == coord_val { + dup = true; + break; + } + } + if !dup { + *data_ptr.add(cur_count * 2) = coord_val; + *data_ptr.add(cur_count * 2 + 1) = 1; + *cl = (cur_count + 1) as u32; + } + } + let _ = crate::log::log_line(&format!( + "[DDGame] coord_list populated: {} entries from {} landscape coords", + *cl, coord_count, + )); + } + + // ── Loading progress ticks (2 of 4 — before load_resource_list) ── + call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR); + call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR); + + // ── Sprite resource loading via DDGameWrapper vtable[0] ── + // DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) + + // 5 stack params (layer, gfx_handler, base_path, data_table, table_size) + { + let landscape_ptr = (*wrapper).landscape as *const u8; + let water_layer = *(landscape_ptr.add(0xB38) as *const *mut u8); + let land_layer = *(landscape_ptr.add(0xB34) as *const *mut u8); + let gfx_handler = (*wrapper)._field_4c0; + + let wrapper_vt = *(wrapper as *const *const u32); + let _ = crate::log::log_line(&format!( + "[DDGame] sprite resource loading: wrapper_vt=0x{:08X}, vt[0]=0x{:08X}, landscape=0x{:08X}, water=0x{:08X}, land=0x{:08X}", + wrapper_vt as u32, *wrapper_vt, landscape_ptr as u32, water_layer as u32, land_layer as u32, + )); + let load_resource_list: unsafe extern "thiscall" fn( + *mut DDGameWrapper, + u32, + *mut u8, + *const u8, + *const u8, + u32, + ) = core::mem::transmute(*wrapper_vt); + // Load resources for layer 1 (main sprites) + let _ = crate::log::log_line("[DDGame] load_resource_list #1 (main sprites)"); + load_resource_list( + wrapper, + 1, + gfx_handler, + rb(0x643F2B) as *const u8, // base path + rb(0x6AD2C0) as *const u8, // resource table + 0x1D88, // table size + ); + let _ = crate::log::log_line(&format!( + "[DDGame] after #1: sprite_cache[144]=0x{:08X}", + (*ddgame).sprite_cache[144] as u32, + )); + + // Set global flag based on game version + let gv = (*(*ddgame).game_info).game_version; + *(rb(0x6AF050) as *mut u32) = if gv < 8 { 0 } else { 0x10 }; + + // Load resources for layer 1 with different table + let _ = crate::log::log_line("[DDGame] load_resource_list #2"); + load_resource_list( + wrapper, + 1, + gfx_handler, + rb(0x643F2B) as *const u8, + rb(0x6AF048) as *const u8, + 0x18, + ); + + // Load resources for layer 2 (water) + let _ = crate::log::log_line("[DDGame] load_resource_list #3 (water)"); + load_resource_list( + wrapper, + 2, + water_layer, + rb(0x643F2B) as *const u8, + rb(0x6AF060) as *const u8, + 0x2F4, + ); + + // Display vtable[5](3) — set active layer 3, returns context ptr + let disp_vt = *((*ddgame).display as *const *const u32); + let set_layer: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 = + core::mem::transmute(*disp_vt.add(5)); + let _layer3_ctx = set_layer((*ddgame).display, 3); + + // Load back.spr and debris.spr (conditional) + let disp_obj = (*wrapper).display as *mut u8; + let disp_obj_vt = *(disp_obj as *const *const u32); + // Check if gfx_mode high byte is set (original: uStack_123c._3_1_ != '\0') + if (*wrapper).gfx_mode != 0 { + let load_spr_94: unsafe extern "thiscall" fn(*mut u8, u32, u32, *mut u8, *const u8) = + core::mem::transmute(*disp_obj_vt.add(0x94 / 4)); + load_spr_94(disp_obj, 3, 0x26D, land_layer, b"back.spr\0".as_ptr()); + + let load_spr_7c: unsafe extern "thiscall" fn( + *mut u8, + u32, + u32, + u32, + *mut u8, + *const u8, + ) = core::mem::transmute(*disp_obj_vt.add(0x7C / 4)); + load_spr_7c(disp_obj, 3, 0x26E, 0, land_layer, b"debris.spr\0".as_ptr()); + } + + // Load layer.spr into layer 2 + let load_spr_94: unsafe extern "thiscall" fn(*mut u8, u32, u32, *mut u8, *const u8) = + core::mem::transmute(*disp_obj_vt.add(0x94 / 4)); + load_spr_94( + disp_obj, + 2, + 0x26C, + water_layer, + b"layer\\layer.spr|layer.spr\0".as_ptr(), + ); + + // gradient_image_2 = 0 + (*ddgame).gradient_image_2 = core::ptr::null_mut(); + + // ── Gradient image (0x030) ── + // The gradient is loaded from "gradient.img" via GfxDir. + // Simple path: height <= 0x60 AND level_height == 0x2B8 + let level_height = (*ddgame).level_height as i32; + // Read sVar1 from display layer 3 context + let layer3_ctx = set_layer((*ddgame).display, 3); + let s_var1 = *(layer3_ctx.add(0x606) as *const i16); + + if s_var1 < 0x61 && level_height == 0x2B8 { + // Simple gradient: load gradient.img directly + let gradient = + call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx); + (*ddgame).gradient_image = gradient; + } else { + // Complex gradient computation — ~200 lines of color interpolation. + // For now, skip this path (gradient_image remains null/0). + // This affects the sky background appearance but not gameplay. + // Set gradient_image to null (already zero from memset) + } + + // ── Fill image → fill_pixel (0x7338) ── + { + let layer2_ctx = set_layer((*ddgame).display, 2); + // In the original, fill.img uses piStack_126c which the decompiler + // shows was set from piVar3 (water_layer from landscape+0xB38). + let fill_sprite = + call_gfx_find_and_load(water_layer, b"fill.img\0".as_ptr(), layer2_ctx); + let _ = crate::log::log_line(&format!( + "[DDGame] fill.img: water=0x{:08X} sprite=0x{:08X}", + water_layer as u32, fill_sprite as u32, + )); + if !fill_sprite.is_null() { + // Get pixel value: fill_sprite->vtable[4](0, 0) + let fill_vt = *(fill_sprite as *const *const u32); + let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 = + core::mem::transmute(*fill_vt.add(4)); + (*ddgame).fill_pixel = get_pixel(fill_sprite, 0, 0); + // Release fill sprite: vtable[3] = DisplayGfx__vmethod_3(this, param_2=1) + let release: unsafe extern "thiscall" fn(*mut u8, u8) = + core::mem::transmute(*fill_vt.add(3)); + release(fill_sprite, 1); + } + } + + // ── DDGame__LoadHudAndWeaponSprites (0x53D0E0) ── + // Loads weapon icons (cow.img, pigeon.img, etc.), wind indicators, stop sign, + // girder sprites, and creates the DDGame+0x37C DisplayGfx object needed by + // CTaskLand__InitLandscape. + // Convention: thiscall(ECX=gfx_handler_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8. + { + let hud_load: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) = + core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize); + hud_load(gfx_handler, ddgame, (*wrapper)._field_4c4); + } + + // ── Loading progress ticks (2 of 4 — after LoadHudAndWeaponSprites) ── + call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR); + call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR); + } + // ── Gradient image stub (DDGame+0x30) ── + // The original constructor loads "gradient.img" here and stores the result at DDGame+0x30. + // CTaskLand__InitLandscape reads DDGame+0x30/+0x34 and calls vtable[4](0, i) for each + // pixel column. vtable[4] = CTaskOilDrum__ProcessFrame_stub (XOR EAX,EAX; RET 0x8) returns 0. + // A minimal stub with [6]=0 (zero-width) causes the loop to not execute at all. + if (*ddgame).gradient_image.is_null() { + let obj = wa_malloc(0x2C); + core::ptr::write_bytes(obj, 0, 0x2C); + if !obj.is_null() { + *(obj as *mut u32) = rb(0x6640EC); // vtable (DisplayGfx vtable, vtable[4]=ProcessFrame_stub) + // [6] = height/width = 0 → CTaskLand loop: `if (0 < 0)` → skip + (*ddgame).gradient_image = obj; + } + } + + // ── Release primary GfxHandler (vtable[3] = release, param 1 = free) ── + let gfx_handler_4c0 = (*wrapper)._field_4c0; + if !gfx_handler_4c0.is_null() { + let gfx_vt = *(gfx_handler_4c0 as *const *const u32); + let release: unsafe extern "thiscall" fn(*mut u8, u32) = + core::mem::transmute(*gfx_vt.add(3)); + release(gfx_handler_4c0, 1); + } + + // ── DDGame__InitDisplayFinal_Maybe (0x56A830): non-headless display finalization ── + if !is_headless { + let f: unsafe extern "stdcall" fn(*mut DDDisplay) = + core::mem::transmute(rb(va::DDGAME_INIT_DISPLAY_FINAL) as usize); + f((*wrapper).display); + } + + // ── FUN_00570A90 (second call, conditional) ── + if *(rb(0x88E485) as *const u8) == 0 { + call_usercall_eax(wrapper, FUN_570A90_ADDR); + } + + // ── Final display layer visibility (vtable[0x17], offset 0x5C) ── + { + let disp = (*wrapper).display as *mut u8; + let vt = *(disp as *const *const u32); + let set_vis: unsafe extern "thiscall" fn(*mut u8, i32, i32) = + core::mem::transmute(*vt.add(0x17)); + set_vis(disp, 1, 0); + set_vis(disp, 2, 0); + set_vis(disp, 3, 1); + } + + let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE"); +} + +/// Helper: find entry in GfxDir and load image, or load directly. +/// Returns a DisplayGfx/sprite pointer or null. +#[cfg(target_arch = "x86")] +unsafe fn call_gfx_find_and_load( + gfx_dir: *mut u8, + name: *const u8, + display_ctx: *mut u8, +) -> *mut u8 { + let entry = gfx_dir_find_entry(name, gfx_dir); + + if !entry.is_null() { + // Try cached load: gfx_dir->vtable[2](entry->field_4) + let dir_vt = *(gfx_dir as *const *const u32); + let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 = + core::mem::transmute(*dir_vt.add(2)); + let cached = load_cached(gfx_dir, *(entry.add(4) as *const u32)); + if !cached.is_null() { + // Wrap with DisplayGfx__Constructor_Maybe (0x4F5E80) + // This is stdcall(1 param), RET 0x4 + let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 = + core::mem::transmute(rb(0x4F5E80) as usize); + return ctor(cached); + } + } + + // Fallback: load image directly + call_gfx_load_and_wrap(gfx_dir, name, display_ctx) +} + +// Statics for usercall bridge addresses +static mut FUN_570E20_ADDR: u32 = 0; +static mut FUN_570A90_ADDR: u32 = 0; +static mut FUN_570F30_ADDR: u32 = 0; +static mut LOAD_SPEECH_BANKS_ADDR: u32 = 0; +static mut LOADING_PROGRESS_TICK_ADDR: u32 = 0; + +/// Bridge: usercall(ESI=wrapper), plain RET. Used by FUN_570E20, FUN_570F30, LoadSpeechBanks. +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_usercall_esi(_wrapper: *mut DDGameWrapper, _addr: u32) { + core::arch::naked_asm!( + "pushl %esi", + "movl 8(%esp), %esi", // ESI = wrapper + "movl 12(%esp), %eax", // EAX = target address + "calll *%eax", + "popl %esi", + "retl", + options(att_syntax), + ); +} + +/// Bridge: usercall(EAX=wrapper), plain RET. Used by FUN_570A90. +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_usercall_eax(_wrapper: *mut DDGameWrapper, _addr: u32) { + core::arch::naked_asm!( + "movl 4(%esp), %eax", // EAX = wrapper + "movl 8(%esp), %ecx", // ECX = target address (temp) + "calll *%ecx", + "retl", + options(att_syntax), + ); +} + +/// Bridge: usercall(ECX=wrapper), plain RET. Used by FUN_5717A0. +#[cfg(target_arch = "x86")] +unsafe fn call_usercall_ecx(wrapper: *mut DDGameWrapper, addr: u32) { + let f: unsafe extern "thiscall" fn(*mut DDGameWrapper) = core::mem::transmute(addr as usize); + f(wrapper); +} + +/// Runtime address of TaskStateMachine__Init_Maybe. +static mut TSM_INIT_ADDR: u32 = 0; + +/// Bridge to TaskStateMachine__Init_Maybe (0x4F6370). +/// Convention: usercall(ESI=object, ECX=1, EDI=height) + 1 stack(width), RET 0x4. +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_tsm_init(_object: *mut u8, _width: u32, _height: u32) { + core::arch::naked_asm!( + "pushl %esi", + "pushl %edi", + "movl 12(%esp), %esi", // ESI = object (1st param, shifted by 2 pushes) + "movl 20(%esp), %edi", // EDI = height (3rd param, shifted by 2 pushes) + "pushl 16(%esp)", // push width (2nd param, shifted by 2 pushes + 1 push) + "movl $1, %ecx", // ECX = 1 + "calll *({addr})", // RET 0x4 cleans the pushed width + "popl %edi", + "popl %esi", + "retl", + addr = sym TSM_INIT_ADDR, + options(att_syntax), + ); +} + +/// Runtime address of GfxResource__Create_Maybe (set by init_constructor_addrs()). +static mut GFX_RESOURCE_CREATE_ADDR: u32 = 0; + +/// Bridge to GfxResource__Create_Maybe (0x4F6300). +/// Convention: usercall(ECX=gfx_handler, EAX=data_ptr) + 1 stack(output), RET 0x4. +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_gfx_resource_create( + _gfx_handler: *mut u8, + _data_ptr: *const u8, + _output: *mut u8, +) -> *mut u8 { + core::arch::naked_asm!( + "pushl %ebp", + "pushl %ebx", + "movl 12(%esp), %ecx", // ECX = gfx_handler (0=ebx,4=ebp,8=ret,12=handler) + "movl 16(%esp), %eax", // EAX = data_ptr (16=data) + "pushl 20(%esp)", // push output (20=output, but shifts after push!) + "calll *({addr})", // RET 0x4 cleans output + "popl %ebx", + "popl %ebp", + "retl", + addr = sym GFX_RESOURCE_CREATE_ADDR, + options(att_syntax), + ); +} + +/// Runtime address of GfxDir__FindEntry (set by init_constructor_addrs()). +static mut GFX_FIND_ENTRY_ADDR: u32 = 0; + +/// Bridge to GfxDir__FindEntry (0x566520). +/// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4. +/// Returns entry pointer or null. +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_gfx_find_entry(_name: *const u8, _gfx_handler: *mut u8) -> *mut u8 { + core::arch::naked_asm!( + "movl 4(%esp), %eax", // EAX = name + "pushl 8(%esp)", // push gfx_handler (callee cleans via RET 0x4) + "calll *({addr})", + "retl", // caller cleans our 2 cdecl params + addr = sym GFX_FIND_ENTRY_ADDR, + options(att_syntax), + ); +} + +/// Bridge to GfxHandler__LoadDir (0x5663E0). +/// Convention: usercall(EAX=handler), plain RET. Returns nonzero on success. +#[cfg(target_arch = "x86")] +unsafe fn call_gfx_load_dir(handler: *mut u8, addr: u32) -> i32 { + let result: i32; + core::arch::asm!( + "call {addr}", + addr = in(reg) addr, + inlateout("eax") handler => result, + out("ecx") _, + out("edx") _, + clobber_abi("C"), + ); + result +} + +// GfxDir__LoadImage is usercall(ESI=gfx_handler) + 1 stack(name), RET 0x4. +// Returns raw image pointer or null. +static mut GFX_LOAD_IMAGE_ADDR: u32 = 0; + +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_gfx_load_image(_gfx_handler: *mut u8, _name: *const u8) -> *mut u8 { + core::arch::naked_asm!( + "pushl %esi", + "movl 8(%esp), %esi", // ESI = gfx_handler + "pushl 12(%esp)", // push name (callee cleans via RET 0x4) + "calll *({addr})", + "popl %esi", + "retl", + addr = sym GFX_LOAD_IMAGE_ADDR, + options(att_syntax), + ); +} + +/// Helper: load image via GfxDir__LoadImage + wrap as DisplayGfx. +/// Used by arrow sprite loop when GfxDir__FindEntry returns null. +#[cfg(target_arch = "x86")] +unsafe fn call_gfx_load_and_wrap( + gfx_handler: *mut u8, + name: *const u8, + display_ctx: *mut u8, +) -> *mut u8 { + let image = call_gfx_load_image(gfx_handler, name); + if image.is_null() { + return core::ptr::null_mut(); + } + // FUN_004F5F80(display_ctx, image, 1) — stdcall, RET 0xC (3 params) + let f: unsafe extern "stdcall" fn(*mut u8, *mut u8, u32) -> *mut u8 = + core::mem::transmute(rb(0x4F5F80) as usize); + let result = f(display_ctx, image, 1); + // Release the raw image + let image_vt = *(image as *const *const u32); + let destroy: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*image_vt); + destroy(image, 1); + result +} + +/// Bridge to SpriteRegion__Constructor (0x57DB20). +/// Convention: fastcall(ECX, EDX) + 6 stack params, RET 0x18. +#[cfg(target_arch = "x86")] +static mut SPRITE_REGION_CTOR_ADDR: u32 = 0; + +#[cfg(target_arch = "x86")] +#[unsafe(naked)] +unsafe extern "C" fn call_sprite_region_ctor( + _this: *mut u8, + _ecx: u32, + _edx: u32, + _p2: u32, + _p3: u32, + _p4: u32, + _p5: u32, + _p6: u32, +) -> *mut u8 { + // Stack on entry: [ret] [this] [ecx] [edx] [p2] [p3] [p4] [p5] [p6] + // Need: ECX=ecx, EDX=edx, push p6 p5 p4 p3 p2 this, call + // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack, RET 0x18 + // Params: (this, ecx, edx, p2, p3, p4, p5, p6) + core::arch::naked_asm!( + "pushl %ebp", + "pushl %ebx", + // Load ECX and EDX (shifted by 2 pushes = 8 bytes) + "movl 16(%esp), %ecx", // ecx param (offset 4+8=12? No: 0=ebx,4=ebp,8=ret,12=this,16=ecx) + "movl 20(%esp), %edx", // edx param + // Push 6 stack params in reverse: p6, p5, p4, p3, p2, this + "pushl 40(%esp)", // p6 (0=ebx,4=ebp,8=ret,...,40=p6) + "pushl 40(%esp)", // p5 (shifted by 4) + "pushl 40(%esp)", // p4 (shifted by 8) + "pushl 40(%esp)", // p3 (shifted by 12) + "pushl 40(%esp)", // p2 (shifted by 16) + "pushl 32(%esp)", // this (shifted by 20) + "calll *({addr})", // fastcall, callee cleans 6×4=24 bytes + "popl %ebx", + "popl %ebp", + "retl", + addr = sym SPRITE_REGION_CTOR_ADDR, + options(att_syntax), + ); +} + /// Well-known byte offsets into DDGame, for use with raw pointer access. /// /// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122). @@ -500,7 +2268,6 @@ const _: () = assert!(core::mem::size_of::() == 0x24); // Team arena state — sub-struct at DDGame + 0x4628 // ============================================================ - // ============================================================ // Per-worm and per-team block structs // ============================================================ @@ -673,7 +2440,6 @@ pub struct TeamArenaState { pub game_phase: i32, // === Alliance tracking (set by CountTeamsByAlliance + SetActiveWorm_Maybe) === - /// 0x2C2C: Current alliance being evaluated pub current_alliance: i32, /// 0x2C30: Count of teams with an active worm (set by SetActiveWorm_Maybe) @@ -721,7 +2487,6 @@ pub mod worm { pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484 pub const GAME_PHASE_NORMAL_MIN: i32 = -2; - impl TeamArenaState { /// Get ammo count for a weapon slot (by flat index). pub fn get_ammo(&self, index: usize) -> i32 { @@ -764,7 +2529,9 @@ impl TeamArenaRef { /// `base` must point to DDGame + TEAM_ARENA_STATE (0x4628). #[inline] pub unsafe fn from_raw(base: u32) -> Self { - Self { base: base as *const u8 } + Self { + base: base as *const u8, + } } /// Access the TeamArenaState fields (read-only). @@ -804,7 +2571,8 @@ impl TeamArenaRef { /// (0x6C+) that don't conflict with worm data (0x00-0x5F). #[inline] pub unsafe fn team_worm(&self, team_idx: usize, worm_num: usize) -> &WormEntry { - let ptr = self.base + let ptr = self + .base .add(team_idx * 0x51C) .add(worm_num * 0x9C) .sub(0x598); @@ -839,7 +2607,9 @@ impl TeamArenaRef { /// Get mutable team header for Pattern B access. #[inline] pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamBlockHeader { - &mut (*(self.blocks() as *mut FullTeamBlock).add(team_idx + 2)).header.team + &mut (*(self.blocks() as *mut FullTeamBlock).add(team_idx + 2)) + .header + .team } /// Compute the flat index for ammo/delay table access. diff --git a/crates/openwa-core/src/engine/ddgame_wrapper.rs b/crates/openwa-core/src/engine/ddgame_wrapper.rs index 7f6f9275b871d796cfb76205c301ae820c9303e1..1702cb07692e7b0b355f9b1830f888c26382c316 100644 --- a/crates/openwa-core/src/engine/ddgame_wrapper.rs +++ b/crates/openwa-core/src/engine/ddgame_wrapper.rs @@ -1,6 +1,6 @@ +use crate::audio::dssound::DSSound; use crate::display::dd_display::DDDisplay; use crate::engine::ddgame::DDGame; -use crate::audio::dssound::DSSound; use crate::render::landscape::PCLandscape; /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string). diff --git a/crates/openwa-core/src/engine/game_info.rs b/crates/openwa-core/src/engine/game_info.rs index 66e101d72ffac600105a11deded310eac902ad69..e23ae27f155e2e367db00022417b3f0e88a328a1 100644 --- a/crates/openwa-core/src/engine/game_info.rs +++ b/crates/openwa-core/src/engine/game_info.rs @@ -13,7 +13,6 @@ pub struct GameInfo { pub _unknown_0000: [u8; 0x44C], // --- Speech configuration --- - /// 0x044C: Number of teams with speech banks loaded (byte). /// Used by DSSound_LoadAllSpeechBanks to iterate teams. pub speech_team_count: u8, @@ -21,12 +20,23 @@ pub struct GameInfo { /// 0x044D-0xD777: Unknown pub _unknown_044d: [u8; 0xD778 - 0x44D], - /// 0xD778: Crosshair overflow clamp threshold (compared to 0x11E in render). - /// Also passed to timer constructor and input controller init. - pub crosshair_overflow_threshold: i32, + /// 0xD778: Game version/mode. Compared against various thresholds: + /// -2 = network game, -3..0 = different game modes, 8+ = new versions. + /// Used by DDGame constructor for conditional initialization. + pub game_version: i32, + + /// 0xD77C-0xD943: Unknown + pub _unknown_d77c: [u8; 0xD944 - 0xD77C], - /// 0xD77C-0xD9DF: Unknown - pub _unknown_d77c: [u8; 0xD9E0 - 0xD77C], + /// 0xD944: Network config byte 1 (copied to network object+0x28). + pub net_config_1: u8, + /// 0xD945: Unknown + pub _unknown_d945: u8, + /// 0xD946: Network config byte 2 (copied to network object+0x29). + pub net_config_2: u8, + + /// 0xD947-0xD9DF: Unknown + pub _unknown_d947: [u8; 0xD9E0 - 0xD947], /// 0xD9E0: Streaming audio config data (path config passed to streaming audio ctor). /// Address of this field is passed as a pointer parameter. @@ -36,11 +46,14 @@ pub struct GameInfo { /// If nonzero, streaming audio subsystem is created in InitHardware. pub speech_enabled: u8, - /// 0xDAA5-0xDAE7: Unknown - pub _unknown_daa5: [u8; 0xDAE8 - 0xDAA5], + /// 0xDAA5-0xDAAB: Unknown + pub _unknown_daa5: [u8; 0xDAAC - 0xDAA5], - // --- Cluster 1: data paths --- + /// 0xDAAC: Landscape data path (passed to PCLandscape constructor). + /// Points to a path string used for loading level terrain data. + pub landscape_data_path: [u8; 0xDAE8 - 0xDAAC], + // --- Cluster 1: data paths --- /// 0xDAE8: Config DWORD (copied from global 0x88E390) pub _config_dword_dae8: u32, /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null) @@ -68,7 +81,6 @@ pub struct GameInfo { pub _unknown_f378: [u8; 0xF3A0 - 0xF378], // --- Cluster 2: game options (populated by LoadOptions) --- - /// 0xF3A0: Unknown config byte (from global 0x7C0D38) pub _config_byte_f3a0: u8, /// 0xF3A1: Detail level (registry: DetailLevel, default 5) @@ -132,7 +144,6 @@ pub struct GameInfo { pub _unknown_f4c5: [u8; 0xF500 - 0xF4C5], // --- Extended region (beyond original 0xF500 conservative estimate) --- - /// 0xF500-0xF913: Unknown pub _unknown_f500: [u8; 0xF914 - 0xF500], @@ -153,7 +164,9 @@ impl core::fmt::Debug for HexU32s<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "[")?; for (i, v) in self.0.iter().enumerate() { - if i > 0 { write!(f, ", ")?; } + if i > 0 { + write!(f, ", ")?; + } write!(f, "0x{v:08X}")?; } write!(f, "]") @@ -163,20 +176,27 @@ impl core::fmt::Debug for HexU32s<'_> { impl core::fmt::Debug for GameInfo { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { // Extract land_dat_path as a string (null-terminated) - let land_str = self.land_dat_path.iter() + let land_str = self + .land_dat_path + .iter() .position(|&b| b == 0) .map(|end| core::str::from_utf8(&self.land_dat_path[..end]).unwrap_or("")) .unwrap_or(core::str::from_utf8(&self.land_dat_path).unwrap_or("")); // Extract speech_path as a string (null-terminated) - let speech_str = self.speech_path.iter() + let speech_str = self + .speech_path + .iter() .position(|&b| b == 0) .map(|end| core::str::from_utf8(&self.speech_path[..end]).unwrap_or("")) .unwrap_or(core::str::from_utf8(&self.speech_path).unwrap_or("")); f.debug_struct("GameInfo") // Cluster 1: data paths - .field("_config_dword_dae8", &format_args!("0x{:08X}", self._config_dword_dae8)) + .field( + "_config_dword_dae8", + &format_args!("0x{:08X}", self._config_dword_dae8), + ) .field("land_dat_path", &land_str) // Cluster 2: game options .field("_config_byte_f3a0", &self._config_byte_f3a0) @@ -186,25 +206,52 @@ impl core::fmt::Debug for GameInfo { .field("info_spy", &self.info_spy) .field("chat_pinned", &self.chat_pinned) .field("chat_lines", &self.chat_lines) - .field("pinned_chat_lines", &format_args!("0x{:08X}", self.pinned_chat_lines)) + .field( + "pinned_chat_lines", + &format_args!("0x{:08X}", self.pinned_chat_lines), + ) .field("home_lock", &self.home_lock) - .field("display_flags", &format_args!("0x{:08X}", self.display_flags)) + .field( + "display_flags", + &format_args!("0x{:08X}", self.display_flags), + ) .field("display_width", &self.display_width) .field("display_height", &self.display_height) .field("_config_dwords_f3bc", &HexU32s(&self._config_dwords_f3bc)) - .field("_config_dword_f3d4", &format_args!("0x{:08X}", self._config_dword_f3d4)) - .field("_config_dword_f3d8", &format_args!("0x{:08X}", self._config_dword_f3d8)) + .field( + "_config_dword_f3d4", + &format_args!("0x{:08X}", self._config_dword_f3d4), + ) + .field( + "_config_dword_f3d8", + &format_args!("0x{:08X}", self._config_dword_f3d8), + ) .field("capture_transparent_pngs", &self.capture_transparent_pngs) .field("camera_unlock_mouse_speed", &self.camera_unlock_mouse_speed) - .field("_config_dword_f3e4", &format_args!("0x{:08X}", self._config_dword_f3e4)) - .field("background_debris_parallax", &format_args!("0x{:08X}", self.background_debris_parallax)) - .field("topmost_explosion_onomatopoeia", &self.topmost_explosion_onomatopoeia) + .field( + "_config_dword_f3e4", + &format_args!("0x{:08X}", self._config_dword_f3e4), + ) + .field( + "background_debris_parallax", + &format_args!("0x{:08X}", self.background_debris_parallax), + ) + .field( + "topmost_explosion_onomatopoeia", + &self.topmost_explosion_onomatopoeia, + ) .field("_zeroed_f3f0", &self._zeroed_f3f0) - .field("_conditional_config_f3f4", &HexU32s(&self._conditional_config_f3f4)) + .field( + "_conditional_config_f3f4", + &HexU32s(&self._conditional_config_f3f4), + ) .field("speech_path", &speech_str) .field("speech_enabled", &self.speech_enabled) .field("headless_mode", &self.headless_mode) - .field("input_state_f918", &format_args!("0x{:08X}", self.input_state_f918)) + .field( + "input_state_f918", + &format_args!("0x{:08X}", self.input_state_f918), + ) .finish() } } diff --git a/crates/openwa-core/src/engine/game_session.rs b/crates/openwa-core/src/engine/game_session.rs index b5bfbea93d7c99fd5d92414e282436960db79032..81dc21d4bd751d8a9a5531fe7bcc74ee18f3c0af 100644 --- a/crates/openwa-core/src/engine/game_session.rs +++ b/crates/openwa-core/src/engine/game_session.rs @@ -1,7 +1,7 @@ -use crate::engine::ddgame_wrapper::DDGameWrapper; -use crate::input::keyboard::DDKeyboard; use crate::audio::dssound::DSSound; use crate::display::palette::Palette; +use crate::engine::ddgame_wrapper::DDGameWrapper; +use crate::input::keyboard::DDKeyboard; /// Top-level game session context, allocated once per game run. /// diff --git a/crates/openwa-core/src/fixed.rs b/crates/openwa-core/src/fixed.rs index a9622483bfbabf55983d77b6bb2a042937d5492b..37bd4122f799d7bb94335378dad868c510f42444 100644 --- a/crates/openwa-core/src/fixed.rs +++ b/crates/openwa-core/src/fixed.rs @@ -1,4 +1,4 @@ -use core::ops::{Add, Sub, Mul, Neg}; +use core::ops::{Add, Mul, Neg, Sub}; /// 16.16 fixed-point number used throughout WA for coordinates and velocities. /// diff --git a/crates/openwa-core/src/game/frontend.rs b/crates/openwa-core/src/game/frontend.rs index c45615b6ee458d0fc8ad61c19d4b288701b1f6ab..5cfab7494126465bdea6fb77c6e9077dd3c23f38 100644 --- a/crates/openwa-core/src/game/frontend.rs +++ b/crates/openwa-core/src/game/frontend.rs @@ -21,7 +21,6 @@ #[repr(i32)] pub enum ScreenId { // --- Dialog result codes (not main-loop dispatch targets) --- - /// Cancel / return to previous screen (EndDialog result only) Cancel = 0, /// Splash / intro trigger (result code) @@ -34,7 +33,6 @@ pub enum ScreenId { SinglePlayerConfirm = 5, // --- Valid main-loop screen IDs --- - /// Deathmatch setup dialog (constructor 0x440F40) Deathmatch = 10, /// Local multiplayer setup dialog (constructor 0x49C420) @@ -80,7 +78,6 @@ pub enum ScreenId { // 35/36 and 37/38 form offline/online pairs: // Start Game path: this+0x244 flag → 35 (no network) / 36 (network) // Multiplayer path: this+0x140 flag → 37 (no network) / 38 (network) - /// Scheme/weapon editor done — return from editor SchemeEditorDone = 32, /// Start game, network unavailable (offline/local path) @@ -101,7 +98,6 @@ pub enum ScreenId { InGameC = 58, // --- Network/lobby screen IDs (high range, 0x6A4-0x6AB) --- - /// Network provider selection dialog (constructor 0x4A7990) NetworkProviderSelect = 1700, /// Network settings dialog (constructor 0x4C23C0) diff --git a/crates/openwa-core/src/game/message.rs b/crates/openwa-core/src/game/message.rs index 8e44c7897c3361443da6bc45ad6030dcd50e889d..35efa7207b38fa617693a541418cfe6173dbe3b8 100644 --- a/crates/openwa-core/src/game/message.rs +++ b/crates/openwa-core/src/game/message.rs @@ -126,8 +126,7 @@ impl TryFrom for TaskMessage { fn try_from(value: u32) -> Result { match value { - 0..=9 | 11..=23 | 26..=41 | 43..=64 | 67..=81 | 84..=86 - | 88..=94 | 96..=114 => { + 0..=9 | 11..=23 | 26..=41 | 43..=64 | 67..=81 | 84..=86 | 88..=94 | 96..=114 => { // SAFETY: all matched values correspond to valid variants Ok(unsafe { core::mem::transmute(value) }) } diff --git a/crates/openwa-core/src/game/scheme.rs b/crates/openwa-core/src/game/scheme.rs index 2c11fac57681279031e3caf86c6f879ff102c877..4eb776a15f615c01683a233ee3950f84e585fda3 100644 --- a/crates/openwa-core/src/game/scheme.rs +++ b/crates/openwa-core/src/game/scheme.rs @@ -90,10 +90,17 @@ impl core::fmt::Display for SchemeError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { SchemeError::TooShort { len } => { - write!(f, "file too short ({len} bytes, need at least {SCHEME_HEADER_SIZE})") + write!( + f, + "file too short ({len} bytes, need at least {SCHEME_HEADER_SIZE})" + ) } SchemeError::BadMagic(m) => { - write!(f, "bad magic: {:02X} {:02X} {:02X} {:02X} (expected SCHM)", m[0], m[1], m[2], m[3]) + write!( + f, + "bad magic: {:02X} {:02X} {:02X} {:02X} (expected SCHM)", + m[0], m[1], m[2], m[3] + ) } SchemeError::UnknownVersion(v) => { write!(f, "unknown version byte: 0x{v:02X}") @@ -129,8 +136,8 @@ impl SchemeFile { return Err(SchemeError::BadMagic(magic)); } - let version = SchemeVersion::from_byte(data[4]) - .ok_or(SchemeError::UnknownVersion(data[4]))?; + let version = + SchemeVersion::from_byte(data[4]).ok_or(SchemeError::UnknownVersion(data[4]))?; let expected_payload = version.payload_size(); let actual_payload = data.len() - SCHEME_HEADER_SIZE; @@ -153,9 +160,8 @@ impl SchemeFile { // Fill remaining extended options with defaults if actual_payload < SCHEME_PAYLOAD_V3 { let defaults_start = actual_payload.saturating_sub(SCHEME_PAYLOAD_V2); - payload[actual_payload..SCHEME_PAYLOAD_V3].copy_from_slice( - &EXTENDED_OPTIONS_DEFAULTS[defaults_start..], - ); + payload[actual_payload..SCHEME_PAYLOAD_V3] + .copy_from_slice(&EXTENDED_OPTIONS_DEFAULTS[defaults_start..]); } return Ok(SchemeFile { version, payload }); } @@ -477,25 +483,25 @@ pub const SCHEME_WEAPON_ORDER: [&str; 64] = [ "Invisibility", // 43 "Damage x2", // 44 // V2 super weapons (45-63) - "Freeze", // 45 - "Super Banana Bomb", // 46 - "Mine Strike", // 47 - "Girder Starter Pack", // 48 - "Earthquake", // 49 - "Scales of Justice", // 50 - "Ming Vase", // 51 - "Mike's Carpet Bomb", // 52 + "Freeze", // 45 + "Super Banana Bomb", // 46 + "Mine Strike", // 47 + "Girder Starter Pack", // 48 + "Earthquake", // 49 + "Scales of Justice", // 50 + "Ming Vase", // 51 + "Mike's Carpet Bomb", // 52 "Patsy's Magic Bullet", // 53 - "Indian Nuclear Test", // 54 - "Select Worm", // 55 - "Salvation Army", // 56 - "Mole Squadron", // 57 - "MB Bomb", // 58 - "Concrete Donkey", // 59 - "Suicide Bomber", // 60 - "Sheep Strike", // 61 - "Mail Strike", // 62 - "Armageddon", // 63 + "Indian Nuclear Test", // 54 + "Select Worm", // 55 + "Salvation Army", // 56 + "Mole Squadron", // 57 + "MB Bomb", // 58 + "Concrete Donkey", // 59 + "Suicide Bomber", // 60 + "Sheep Strike", // 61 + "Mail Strike", // 62 + "Armageddon", // 63 ]; /// V3 extended options (110 bytes, payload offset 0x124). @@ -907,7 +913,7 @@ impl ExtendedOptions { && b[0x69] < 6 // rw_kaos_mod && b[0x6A].wrapping_sub(1) < 7 // sheep_heavens_gate [1,7] && is_bool(b[0x6B]) // conserve_instant_utilities - && is_bool(b[0x6C]) // expedite_instant_utilities + && is_bool(b[0x6C]) // expedite_instant_utilities } } @@ -931,7 +937,7 @@ pub const EXTENDED_OPTIONS_DEFAULTS: [u8; EXTENDED_OPTIONS_SIZE] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +0x50 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // +0x58 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // +0x60 - 0x01, 0x00, 0x07, 0x00, 0x00, 0x01, // +0x68 + 0x01, 0x00, 0x07, 0x00, 0x00, 0x01, // +0x68 ]; // === Typed accessors on SchemeFile === @@ -1087,7 +1093,10 @@ mod tests { assert!(matches!( SchemeFile::from_bytes(&data), - Err(SchemeError::PayloadMismatch { expected: 0xD8, got: 10 }) + Err(SchemeError::PayloadMismatch { + expected: 0xD8, + got: 10 + }) )); } @@ -1098,7 +1107,7 @@ mod tests { data.extend_from_slice(b"SCHM"); data.push(0x03); data.extend_from_slice(&[0xAA; SCHEME_PAYLOAD_V2]); // V2 portion filled with 0xAA - // No extended options bytes — should be padded with defaults + // No extended options bytes — should be padded with defaults let scheme = SchemeFile::from_bytes(&data).expect("should accept short V3"); assert_eq!(scheme.version, SchemeVersion::V3); diff --git a/crates/openwa-core/src/input/controller.rs b/crates/openwa-core/src/input/controller.rs index 8820870c04c1babeb9151e5933f32c59d310a93f..09f6ae6f2858a100bc3bb52e0140df7474ac2709 100644 --- a/crates/openwa-core/src/input/controller.rs +++ b/crates/openwa-core/src/input/controller.rs @@ -31,5 +31,7 @@ pub struct InputCtrlVtable { impl InputCtrl { /// Vtable[0]: Destroy and optionally free (flags & 1 = free). - pub unsafe fn destroy(&mut self, flags: u32) { vcall!(self, destructor, flags) } + pub unsafe fn destroy(&mut self, flags: u32) { + vcall!(self, destructor, flags) + } } diff --git a/crates/openwa-core/src/lib.rs b/crates/openwa-core/src/lib.rs index 229437ac9de351f4cb21588de61b7bfc91124c78..b3007ab73627693ef528b6b1542cbc72cac1c8ba 100644 --- a/crates/openwa-core/src/lib.rs +++ b/crates/openwa-core/src/lib.rs @@ -10,12 +10,12 @@ pub mod game; pub mod input; pub mod render; +pub mod address; pub mod fixed; pub mod log; -pub mod task; -pub mod address; pub mod rebase; +pub mod task; +pub mod vtable; pub mod wa; -pub mod wa_call; pub mod wa_alloc; -pub mod vtable; +pub mod wa_call; diff --git a/crates/openwa-core/src/render/queue.rs b/crates/openwa-core/src/render/queue.rs index 70e69b84aea40ffc637b300bc0869e7816394c29..87eb6d0655da95c07314ad805e0df744d45808f5 100644 --- a/crates/openwa-core/src/render/queue.rs +++ b/crates/openwa-core/src/render/queue.rs @@ -112,10 +112,10 @@ pub struct DrawRectCmd { pub command_type: u32, // 0 pub layer: u32, pub color: u32, - pub x1: u32, // & 0xFFFF0000 - pub y1: u32, // & 0xFFFF0000 - pub x2: u32, // & 0xFFFF0000 - pub y2: u32, // & 0xFFFF0000 + pub x1: u32, // & 0xFFFF0000 + pub y1: u32, // & 0xFFFF0000 + pub x2: u32, // & 0xFFFF0000 + pub y2: u32, // & 0xFFFF0000 pub y_clip: u32, // EDX & 0xFFFF0000 } @@ -127,8 +127,8 @@ pub struct DrawSpriteOffsetCmd { pub command_type: u32, // 6 pub layer: u32, pub sprite_id: u32, - pub x_pos: u32, // & 0xFFFF0000 - pub y_pos: u32, // & 0xFFFF0000 + pub x_pos: u32, // & 0xFFFF0000 + pub y_pos: u32, // & 0xFFFF0000 pub _reserved: u32, // hardcoded 0 pub y_clip: u32, // EDX & 0xFFFF0000 pub param_7: u32, @@ -142,8 +142,8 @@ const _: () = assert!(core::mem::size_of::() == 0x24); pub struct DrawBitmapGlobalCmd { pub command_type: u32, // 1 pub layer: u32, - pub x_pos: u32, // & 0xFFFF0000 - pub y_pos: u32, // EDX & 0xFFFF0000 + pub x_pos: u32, // & 0xFFFF0000 + pub y_pos: u32, // EDX & 0xFFFF0000 pub bitmap_ptr: u32, pub _reserved: u32, // hardcoded 0 pub param_6: u32, diff --git a/crates/openwa-core/src/render/sprite.rs b/crates/openwa-core/src/render/sprite.rs index 46f1acb520050076ad9d583f7c895ee85ff523c8..4c7d8dc0b5aee6e3f93bff8a37ba144683bcf126 100644 --- a/crates/openwa-core/src/render/sprite.rs +++ b/crates/openwa-core/src/render/sprite.rs @@ -723,7 +723,6 @@ impl TryFrom for SpriteId { // Sprite struct layout (from Ghidra + wkJellyWorm Sprites.h) // --------------------------------------------------------------------------- - /// Per-frame metadata within a Sprite (0x0C bytes). /// /// Describes the bounding box and bitmap data offset for one animation frame. diff --git a/crates/openwa-core/src/task/base.rs b/crates/openwa-core/src/task/base.rs index aa33d9126128451fb63aca547ff8d16dae4ec3c5..67bbb342e754c66666ecaac34292a042c082a4fc 100644 --- a/crates/openwa-core/src/task/base.rs +++ b/crates/openwa-core/src/task/base.rs @@ -1,5 +1,5 @@ -use crate::game::class_type::ClassType; use crate::engine::ddgame::DDGame; +use crate::game::class_type::ClassType; /// Base task class in WA's entity hierarchy. /// @@ -114,7 +114,9 @@ impl SharedDataTable { /// # Safety /// `ptr` must point to a valid shared-data region of at least 256 × 4 = 1024 bytes. pub unsafe fn from_ptr(ptr: *mut u8) -> Self { - Self { buckets: ptr as *const *mut SharedDataNode } + Self { + buckets: ptr as *const *mut SharedDataNode, + } } /// Construct from a `CTask` pointer (reads `task.shared_data`). diff --git a/crates/openwa-core/src/task/cloud.rs b/crates/openwa-core/src/task/cloud.rs index 2374c514b99311e8610d80e6edaaac8618492668..91bad6f34af11f89c725b65856d5ac9b96c76c72 100644 --- a/crates/openwa-core/src/task/cloud.rs +++ b/crates/openwa-core/src/task/cloud.rs @@ -1,5 +1,5 @@ -use crate::fixed::Fixed; use super::base::CTask; +use crate::fixed::Fixed; /// Airstrike / weather cloud task. /// diff --git a/crates/openwa-core/src/task/game_task.rs b/crates/openwa-core/src/task/game_task.rs index 95213b5b95b633152d65e67f6ce6efe29e1a1a91..b3945537fcf786514e5fc271153b3ef3ea5f3daf 100644 --- a/crates/openwa-core/src/task/game_task.rs +++ b/crates/openwa-core/src/task/game_task.rs @@ -1,5 +1,5 @@ -use crate::fixed::Fixed; use super::base::CTask; +use crate::fixed::Fixed; /// Game task - extends CTask with physics and gameplay data. /// diff --git a/crates/openwa-core/src/task/missile.rs b/crates/openwa-core/src/task/missile.rs index 6fb7cc93f84b86613dcdb2c0ab5b5b035d8ea691..14eb21deb44b4c7b42925ee37c76a7362d6dca37 100644 --- a/crates/openwa-core/src/task/missile.rs +++ b/crates/openwa-core/src/task/missile.rs @@ -1,5 +1,5 @@ -use crate::fixed::Fixed; use super::game_task::CGameTask; +use crate::fixed::Fixed; /// Projectile / missile entity task. /// @@ -24,7 +24,6 @@ pub struct CTaskMissile { pub base: CGameTask, // ---- 0xFC–0x12F: missile init fields ---- - /// 0xFC–0x10F: Unknown missile flags and state pub _unknown_fc: [u8; 0x14], /// 0x110: Unknown @@ -47,7 +46,6 @@ pub struct CTaskMissile { pub slot_id: u32, // ---- 0x130–0x15B: spawn data (11 DWORDs, from param_4) ---- - /// 0x130–0x15B: Spawn parameters (11 DWORDs copied from param_4). /// /// Runtime-confirmed indices: @@ -65,7 +63,6 @@ pub struct CTaskMissile { pub spawn_params: [u32; 0x0B], // ---- 0x15C–0x2D3: weapon/scheme data (94 DWORDs, from param_3) ---- - /// 0x15C–0x2D3: Weapon/scheme properties (94 DWORDs copied verbatim from param_3). /// /// The WGT scheme blob is split into two logical halves: @@ -85,7 +82,6 @@ pub struct CTaskMissile { pub weapon_data: [u32; 0x5E], // ---- 0x2D4–0x37B: render/physics parameters (42 DWORDs) ---- - /// 0x2D4–0x37B: Per-projectile render and physics parameters (42 DWORDs). /// /// This is NOT a separate data block — it is a shifted copy of weapon_data: @@ -107,7 +103,6 @@ pub struct CTaskMissile { pub render_data: [u32; 0x2A], // ---- 0x37C–0x41B: post-render physics and state ---- - /// 0x37C–0x39F: Post-render dynamic state. render_data[0x11] is copied to /// [0x37C] by the constructor; physics updates the values each frame. pub _unknown_37c: [u8; 0x24], diff --git a/crates/openwa-core/src/task/mod.rs b/crates/openwa-core/src/task/mod.rs index eb90e9e863ef94ce8275e081f22140907317a045..1111ca7917eb11f15a167ccbb52ba46c8a770f7f 100644 --- a/crates/openwa-core/src/task/mod.rs +++ b/crates/openwa-core/src/task/mod.rs @@ -1,25 +1,25 @@ mod base; mod cloud; -mod supply_crate; mod filter; mod fire; mod game_task; mod mine_oil_drum; mod missile; mod overlays; +mod supply_crate; mod team; mod turn_game; mod worm; pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable}; pub use cloud::CTaskCloud; -pub use supply_crate::CTaskCrate; pub use filter::CTaskFilter; pub use fire::CTaskFire; pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable}; pub use mine_oil_drum::{CTaskMine, CTaskOilDrum}; pub use missile::{CTaskMissile, MissileType}; pub use overlays::{BungeeTrailTask, WeaponAimTask}; +pub use supply_crate::CTaskCrate; pub use team::CTaskTeam; pub use turn_game::{CTaskTurnGame, TurnGameCtx}; pub use worm::{CTaskWorm, CTaskWormVTable}; diff --git a/crates/openwa-core/src/task/overlays.rs b/crates/openwa-core/src/task/overlays.rs index ca8ef1ee2c3995952e627518517ca6da46ec55ec..8e498f523e8852e0157e92d462e1da25636be598 100644 --- a/crates/openwa-core/src/task/overlays.rs +++ b/crates/openwa-core/src/task/overlays.rs @@ -1,6 +1,6 @@ -use crate::fixed::Fixed; use super::base::CTask; use super::game_task::{CGameTask, SoundEmitter}; +use crate::fixed::Fixed; /// Bungee trail rendering task fields. /// diff --git a/crates/openwa-core/src/task/team.rs b/crates/openwa-core/src/task/team.rs index 2d13ada1d8c24ae3cf579c23f4925a79ef1006f1..1a5a34f44391cb934594ac11442c892037f52297 100644 --- a/crates/openwa-core/src/task/team.rs +++ b/crates/openwa-core/src/task/team.rs @@ -1,5 +1,5 @@ -use crate::fixed::Fixed; use super::base::CTask; +use crate::fixed::Fixed; /// Per-team state-tracker task — one instance per team, child of CTaskTurnGame. /// diff --git a/crates/openwa-core/src/task/turn_game.rs b/crates/openwa-core/src/task/turn_game.rs index 3ee4678d02cc31eb07916c6a0a7348f4836457d1..47ba710207bb4b5ad71ba5a575c4e4cebcb49d35 100644 --- a/crates/openwa-core/src/task/turn_game.rs +++ b/crates/openwa-core/src/task/turn_game.rs @@ -1,5 +1,5 @@ -use crate::fixed::Fixed; use super::base::CTask; +use crate::fixed::Fixed; /// Embedded intermediate game-context sub-object within `CTaskTurnGame`. /// @@ -100,7 +100,6 @@ pub struct CTaskTurnGame { pub game_ctx: TurnGameCtx, // ---- CTaskTurnGame-specific fields (0xDC onwards) ---- - pub _unknown_dc: u32, pub _unknown_e0: u32, /// 0xE4: turn-seed: ~(DDGame+0x45EC % 9000), used for random initialisation. diff --git a/crates/openwa-core/src/task/worm.rs b/crates/openwa-core/src/task/worm.rs index bdd9dbdb62e6725f153e81543e3078190b968717..dc2d0133fca5ff4c70bd993e7d7320ede116964a 100644 --- a/crates/openwa-core/src/task/worm.rs +++ b/crates/openwa-core/src/task/worm.rs @@ -31,8 +31,7 @@ pub struct CTaskWormVTable { pub _inherited_4_to_6: [*const (); 3], /// [7] 0x0050D5D0: OnContactEntity — handles physical contact with another entity; /// worm-on-dying-worm → apply crush damage; projectile close-pass → SetState(0x80) - pub on_contact_entity: - unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32, + pub on_contact_entity: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32, /// [8] 0x0050D9A0: OnWormPush — post-contact worm-worm push impulse; /// deduplicates via a recent-collision table, then adjusts pos_x or pos_y pub on_worm_push: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32, diff --git a/crates/openwa-core/src/wa/frontend.rs b/crates/openwa-core/src/wa/frontend.rs index 11c26678d286f8711192f169369bd1603a2d8ed4..a6eb9a4f21a10fa8c8daf5f5a8e8fde2cd114c71 100644 --- a/crates/openwa-core/src/wa/frontend.rs +++ b/crates/openwa-core/src/wa/frontend.rs @@ -1,7 +1,7 @@ //! WA-internal frontend function wrappers. -use crate::rebase::rb; use crate::address::va; +use crate::rebase::rb; /// Frontend__PaletteAnimation (0x422180) /// diff --git a/crates/openwa-core/src/wa/mfc.rs b/crates/openwa-core/src/wa/mfc.rs index f1abc76b9df2d3de27fad899ce8e4db2f06854a3..937d1103c1ad13d19cd5f1cdd8280886f6604059 100644 --- a/crates/openwa-core/src/wa/mfc.rs +++ b/crates/openwa-core/src/wa/mfc.rs @@ -1,8 +1,8 @@ //! MFC library function wrappers. +use crate::address::va; use crate::rebase::rb; use crate::wa_call; -use crate::address::va; /// Zero-cost handle to a CWnd-derived MFC window (raw pointer as u32). #[derive(Clone, Copy)] @@ -84,8 +84,7 @@ impl CStringRef { /// FUN_004A39F0: EDX=resource_id, stack param=dest CString object pointer. pub unsafe fn assign_resource(&mut self, resource_id: u32) { let addr = rb(va::CSTRING_ASSIGN_RESOURCE); - let f: unsafe extern "fastcall" fn(u32, u32, u32) = - core::mem::transmute(addr); + let f: unsafe extern "fastcall" fn(u32, u32, u32) = core::mem::transmute(addr); // ECX is overwritten by the function (loads from global), so any value works. // EDX = resource_id, stack = self.ptr f(0, resource_id, self.ptr); diff --git a/crates/openwa-core/src/wa_alloc.rs b/crates/openwa-core/src/wa_alloc.rs index 53634d6cd1cad2343f96fb98c5945e8b8d33e153..3f3cf7d248a9cf5bdd27f398cfba162c103c296e 100644 --- a/crates/openwa-core/src/wa_alloc.rs +++ b/crates/openwa-core/src/wa_alloc.rs @@ -18,8 +18,7 @@ pub unsafe fn wa_malloc(size: u32) -> *mut u8 { /// # Safety /// `ptr` must have been returned by `wa_malloc` (or null, which is a no-op). pub unsafe fn wa_free(ptr: *mut u8) { - let f: unsafe extern "cdecl" fn(*mut u8) = - core::mem::transmute(rb(va::WA_FREE) as usize); + let f: unsafe extern "cdecl" fn(*mut u8) = core::mem::transmute(rb(va::WA_FREE) as usize); f(ptr); } diff --git a/crates/openwa-core/src/wa_call.rs b/crates/openwa-core/src/wa_call.rs index a06c0f080957741efee0b7b29fcbfabbda5100d3..4be24e49530763a9dabd70178f0a9c1c496319de 100644 --- a/crates/openwa-core/src/wa_call.rs +++ b/crates/openwa-core/src/wa_call.rs @@ -14,8 +14,7 @@ pub unsafe fn read_global(ghidra_addr: u32) -> u32 { /// Uses the fastcall trick: ECX = this, EDX = dummy. #[inline] pub unsafe fn thiscall_1(ghidra_addr: u32, this: u32, arg1: u32) { - let f: unsafe extern "fastcall" fn(u32, u32, u32) = - core::mem::transmute(rb(ghidra_addr)); + let f: unsafe extern "fastcall" fn(u32, u32, u32) = core::mem::transmute(rb(ghidra_addr)); f(this, 0, arg1); } @@ -32,15 +31,13 @@ pub unsafe fn thiscall_1_ret(ghidra_addr: u32, this: u32, arg1: u32) -> u32 { #[inline] pub unsafe fn thiscall_indirect_1(vtable_slot_addr: u32, this: u32, arg1: u32) { let fn_ptr = *(vtable_slot_addr as *const u32); - let f: unsafe extern "fastcall" fn(u32, u32, u32) = - core::mem::transmute(fn_ptr); + let f: unsafe extern "fastcall" fn(u32, u32, u32) = core::mem::transmute(fn_ptr); f(this, 0, arg1); } /// Call a stdcall function with 2 arguments. #[inline] pub unsafe fn stdcall_2(ghidra_addr: u32, arg1: u32, arg2: u32) { - let f: unsafe extern "stdcall" fn(u32, u32) = - core::mem::transmute(rb(ghidra_addr)); + let f: unsafe extern "stdcall" fn(u32, u32) = core::mem::transmute(rb(ghidra_addr)); f(arg1, arg2); } diff --git a/crates/openwa-core/tests/scheme_parse.rs b/crates/openwa-core/tests/scheme_parse.rs index d1c8858f7ae278bb8023479f9c2c75205be1cc11..8d0fa165a55678005baf5e7cd6548bea8fa0be3c 100644 --- a/crates/openwa-core/tests/scheme_parse.rs +++ b/crates/openwa-core/tests/scheme_parse.rs @@ -1,8 +1,8 @@ use std::path::Path; use openwa_core::game::scheme::{ - ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, - EXTENDED_OPTIONS_SIZE, SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, WEAPONS_V1_COUNT, + ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, EXTENDED_OPTIONS_SIZE, + SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, WEAPONS_V1_COUNT, }; const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"); @@ -66,7 +66,7 @@ fn beginner_options() { let opts = scheme.options(); // Verified against hex dump of beginner.wsc payload bytes - assert_eq!(opts.hot_seat_delay, 10); // 0x0A + assert_eq!(opts.hot_seat_delay, 10); // 0x0A assert_eq!(opts.retreat_time, 5); assert_eq!(opts.rope_retreat_time, 5); assert_eq!(opts.display_total_round_time, true); @@ -74,10 +74,10 @@ fn beginner_options() { assert_eq!(opts.fall_damage, 0); assert_eq!(opts.artillery_mode, false); assert_eq!(opts.bounty_mode, 0); - assert_eq!(opts.stockpiling, 1); // On - assert_eq!(opts.worm_select, 1); // Manual + assert_eq!(opts.stockpiling, 1); // On + assert_eq!(opts.worm_select, 1); // Manual assert_eq!(opts.sudden_death_event, 3); // Nothing - assert_eq!(opts.worm_energy, 150); // 0x96 + assert_eq!(opts.worm_energy, 150); // 0x96 } #[test] @@ -94,9 +94,9 @@ fn classic_options() { assert_eq!(opts.automatic_replays, true); assert_eq!(opts.fall_damage, 1); assert_eq!(opts.artillery_mode, false); - assert_eq!(opts.stockpiling, 0); // Off - assert_eq!(opts.worm_select, 1); // Manual - assert_eq!(opts.worm_energy, 100); // hex: 0x64 + assert_eq!(opts.stockpiling, 0); // Off + assert_eq!(opts.worm_select, 1); // Manual + assert_eq!(opts.worm_energy, 100); // hex: 0x64 } #[test] @@ -181,7 +181,8 @@ fn v2_super_weapons_zeroed_region() { /// Parse all .wsc files from the game directory if available. #[test] fn parse_all_game_schemes() { - let schemes_dir = Path::new("I:/games/SteamLibrary/steamapps/common/Worms Armageddon/User/Schemes"); + let schemes_dir = + Path::new("I:/games/SteamLibrary/steamapps/common/Worms Armageddon/User/Schemes"); if !schemes_dir.exists() { eprintln!("Skipping: game schemes directory not found"); return; @@ -228,7 +229,7 @@ fn make_valid_extended_options() -> [u8; EXTENDED_OPTIONS_SIZE] { b[0x34..0x38].copy_from_slice(&1i32.to_le_bytes()); // max_projectile_speed b[0x38..0x3C].copy_from_slice(&1i32.to_le_bytes()); // max_rope_speed b[0x3C..0x40].copy_from_slice(&1i32.to_le_bytes()); // max_jet_pack_speed - // game_engine_speed in [0x1000, 0x800000] + // game_engine_speed in [0x1000, 0x800000] b[0x40..0x44].copy_from_slice(&0x10000i32.to_le_bytes()); // sheep_heavens_gate in [1, 7] b[0x6A] = 1; diff --git a/crates/openwa-debugui/src/app.rs b/crates/openwa-debugui/src/app.rs index 61557976b96f7cb7e7b10a92904f874e1b4d1835..5872c4102a640a8058cda5653a6aafa14200eec4 100644 --- a/crates/openwa-debugui/src/app.rs +++ b/crates/openwa-debugui/src/app.rs @@ -2,7 +2,9 @@ use eframe::egui; use openwa_core::address::va; use openwa_core::engine::{DDGame, DDGameWrapper}; use openwa_core::rebase::rb; -use openwa_core::task::{CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx}; +use openwa_core::task::{ + CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx, +}; use crate::log; @@ -11,31 +13,30 @@ use crate::log; // --------------------------------------------------------------------------- const KNOWN_VTABLES: &[(u32, &str)] = &[ - (va::CTASK_WORM_VTABLE, "CTaskWorm"), - (va::CTASK_LAND_VTABLE, "CTaskLand"), - (va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"), - (va::CTASK_TEAM_VTABLE, "CTaskTeam"), - (va::CTASK_FILTER_VTABLE, "CTaskFilter"), - (va::CTASK_DIRT_VTABLE, "CTaskDirt"), + (va::CTASK_WORM_VTABLE, "CTaskWorm"), + (va::CTASK_LAND_VTABLE, "CTaskLand"), + (va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"), + (va::CTASK_TEAM_VTABLE, "CTaskTeam"), + (va::CTASK_FILTER_VTABLE, "CTaskFilter"), + (va::CTASK_DIRT_VTABLE, "CTaskDirt"), (va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"), - (va::CTASK_CPU_VTABLE, "CTaskCPU"), - (va::CTASK_MISSILE_VTABLE, "CTaskMissile"), - (va::CTASK_MINE_VTABLE, "CTaskMine"), - (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"), - (va::CTASK_CRATE_VTABLE, "CTaskCrate"), - (va::CTASK_CLOUD_VTABLE, "CTaskCloud"), - (va::CTASK_SEA_BUBBLE_VTABLE, "CTaskSeaBubble"), - (va::CTASK_FIRE_VTABLE, "CTaskFire"), + (va::CTASK_CPU_VTABLE, "CTaskCPU"), + (va::CTASK_MISSILE_VTABLE, "CTaskMissile"), + (va::CTASK_MINE_VTABLE, "CTaskMine"), + (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"), + (va::CTASK_CRATE_VTABLE, "CTaskCrate"), + (va::CTASK_CLOUD_VTABLE, "CTaskCloud"), + (va::CTASK_SEA_BUBBLE_VTABLE, "CTaskSeaBubble"), + (va::CTASK_FIRE_VTABLE, "CTaskFire"), ]; /// Vtables of entities that are created/destroyed every frame (particles, /// bubbles, etc.). Filtered from the census by default to reduce noise. -const TRANSIENT_VTABLES: &[u32] = &[ - va::CTASK_SEA_BUBBLE_VTABLE, -]; +const TRANSIENT_VTABLES: &[u32] = &[va::CTASK_SEA_BUBBLE_VTABLE]; fn vtable_name(runtime_vtable: u32) -> Option<&'static str> { - KNOWN_VTABLES.iter() + KNOWN_VTABLES + .iter() .find(|&&(ghidra_va, _)| rb(ghidra_va) == runtime_vtable) .map(|&(_, name)| name) } @@ -43,7 +44,9 @@ fn vtable_name(runtime_vtable: u32) -> Option<&'static str> { /// Returns a display name for the entity at `addr`. /// Tries the known-vtable map first; falls back to CTask.class_type. unsafe fn entity_type_name(addr: u32) -> String { - if addr == 0 { return "(null)".to_owned(); } + if addr == 0 { + return "(null)".to_owned(); + } let vtable = *(addr as *const u32); if let Some(name) = vtable_name(vtable) { return name.to_owned(); @@ -54,7 +57,9 @@ unsafe fn entity_type_name(addr: u32) -> String { /// One-line label for a task: "TypeName @ 0xADDR" unsafe fn entity_label(addr: u32) -> String { - if addr == 0 { return "(null)".to_owned(); } + if addr == 0 { + return "(null)".to_owned(); + } format!("{} @ {:#010X}", entity_type_name(addr), addr) } @@ -65,11 +70,17 @@ unsafe fn entity_label(addr: u32) -> String { /// Returns a pointer to DDGame, or None if not in-game. unsafe fn get_ddgame() -> Option<*const DDGame> { let session_ptr = *(rb(va::G_GAME_SESSION) as *const u32); - if session_ptr == 0 { return None; } + if session_ptr == 0 { + return None; + } let wrapper_addr = *((session_ptr + 0xA0) as *const u32); - if wrapper_addr == 0 { return None; } + if wrapper_addr == 0 { + return None; + } let ddgame_ptr = (*(wrapper_addr as *const DDGameWrapper)).ddgame; - if ddgame_ptr.is_null() { return None; } + if ddgame_ptr.is_null() { + return None; + } Some(ddgame_ptr) } @@ -81,8 +92,10 @@ unsafe fn get_ddgame() -> Option<*const DDGame> { /// that bound so the caller can filter nulls and display the live set. unsafe fn read_children(task: *const CTask) -> Vec { let slots = (*task).children_watermark as usize; - let data = (*task).children_data as *const u32; - if data.is_null() || slots == 0 { return Vec::new(); } + let data = (*task).children_data as *const u32; + if data.is_null() || slots == 0 { + return Vec::new(); + } // Hard safety cap: 4096 slots × 4 bytes = 16 KB max read let slots = slots.min(4096); (0..slots).map(|i| *data.add(i)).collect() @@ -97,11 +110,15 @@ unsafe fn read_children(task: *const CTask) -> Vec { /// MAX_DEPTH steps (guard against corrupt/circular pointers). unsafe fn find_root_task(ddgame: *const DDGame) -> Option { let task_land = (*ddgame).task_land as u32; - if task_land == 0 { return None; } + if task_land == 0 { + return None; + } let mut current = task_land; for _ in 0..64 { let parent = (*(current as *const CTask)).parent as u32; - if parent == 0 { return Some(current); } + if parent == 0 { + return Some(current); + } current = parent; } None // chain didn't terminate — corrupt data @@ -110,23 +127,31 @@ unsafe fn find_root_task(ddgame: *const DDGame) -> Option { /// DFS the task tree from `root`, returning (vtable, addr) for every node. /// A visited set prevents infinite loops from corrupt/circular pointers. unsafe fn collect_task_tree(root: u32) -> Vec<(u32, u32)> { - let mut out = Vec::new(); - let mut stack = vec![root]; + let mut out = Vec::new(); + let mut stack = vec![root]; let mut visited = std::collections::HashSet::new(); while let Some(addr) = stack.pop() { - if addr == 0 || !visited.insert(addr) { continue; } + if addr == 0 || !visited.insert(addr) { + continue; + } let vtable = *(addr as *const u32); out.push((vtable, addr)); for child in read_children(addr as *const CTask) { - if child != 0 { stack.push(child); } + if child != 0 { + stack.push(child); + } } } out } unsafe fn collect_live_entities() -> Vec<(u32, u32)> { - let Some(ddgame) = get_ddgame() else { return Vec::new(); }; - let Some(root) = find_root_task(ddgame) else { return Vec::new(); }; + let Some(ddgame) = get_ddgame() else { + return Vec::new(); + }; + let Some(root) = find_root_task(ddgame) else { + return Vec::new(); + }; collect_task_tree(root) } @@ -147,7 +172,12 @@ pub struct DebugApp { impl Default for DebugApp { fn default() -> Self { - Self { selected_entity: None, nav_history: Vec::new(), log_auto_scroll: true, show_transient: false } + Self { + selected_entity: None, + nav_history: Vec::new(), + log_auto_scroll: true, + show_transient: false, + } } } @@ -203,8 +233,12 @@ impl eframe::App for DebugApp { let mut navigate_to: Option = None; let mut go_back = false; self.show_inspector(ui, &mut navigate_to, &mut go_back); - if go_back { self.navigate_back(); } - if let Some(addr) = navigate_to { self.navigate_to(addr); } + if go_back { + self.navigate_back(); + } + if let Some(addr) = navigate_to { + self.navigate_to(addr); + } }); egui::TopBottomPanel::bottom("log_panel") @@ -217,7 +251,9 @@ impl eframe::App for DebugApp { egui::CentralPanel::default().show(ctx, |ui| { let mut navigate_to: Option = None; self.show_census(ui, &live_entities, &mut navigate_to); - if let Some(addr) = navigate_to { self.navigate_to(addr); } + if let Some(addr) = navigate_to { + self.navigate_to(addr); + } }); } } @@ -227,7 +263,12 @@ impl eframe::App for DebugApp { // --------------------------------------------------------------------------- impl DebugApp { - fn show_census(&mut self, ui: &mut egui::Ui, rows: &[(u32, u32)], navigate_to: &mut Option) { + fn show_census( + &mut self, + ui: &mut egui::Ui, + rows: &[(u32, u32)], + navigate_to: &mut Option, + ) { ui.heading("Entity Census"); if rows.is_empty() { @@ -239,44 +280,46 @@ impl DebugApp { rows.iter().collect() } else { rows.iter() - .filter(|&&(vtable, _)| { - !TRANSIENT_VTABLES.iter().any(|&t| rb(t) == vtable) - }) + .filter(|&&(vtable, _)| !TRANSIENT_VTABLES.iter().any(|&t| rb(t) == vtable)) .collect() }; if visible.len() == rows.len() { ui.label(format!("{} entities", rows.len())); } else { - ui.label(format!("{} entities ({} hidden transient)", visible.len(), rows.len() - visible.len())); + ui.label(format!( + "{} entities ({} hidden transient)", + visible.len(), + rows.len() - visible.len() + )); } ui.separator(); egui::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| { - egui::Grid::new("census_grid") - .striped(true) - .num_columns(3) - .min_col_width(60.0) - .show(ui, |ui| { - ui.strong("Type"); - ui.strong("Address"); - ui.strong("Vtable"); - ui.end_row(); - - for &&(vtable, entity) in &visible { - let name = unsafe { entity_type_name(entity) }; - let is_selected = self.selected_entity == Some(entity); - if ui.selectable_label(is_selected, name).clicked() { - *navigate_to = Some(entity); - } - ui.label(format!("{:#010X}", entity)); - ui.label(format!("{:#010X}", vtable)); + egui::Grid::new("census_grid") + .striped(true) + .num_columns(3) + .min_col_width(60.0) + .show(ui, |ui| { + ui.strong("Type"); + ui.strong("Address"); + ui.strong("Vtable"); ui.end_row(); - } - }); - }); + + for &&(vtable, entity) in &visible { + let name = unsafe { entity_type_name(entity) }; + let is_selected = self.selected_entity == Some(entity); + if ui.selectable_label(is_selected, name).clicked() { + *navigate_to = Some(entity); + } + ui.label(format!("{:#010X}", entity)); + ui.label(format!("{:#010X}", vtable)); + ui.end_row(); + } + }); + }); } } @@ -325,12 +368,16 @@ unsafe fn show_game_task_raw_fields( let base = addr as *const u32; let label_for = |off: usize| -> Option<&str> { - CGAMETASK_FIELD_LABELS.iter() + CGAMETASK_FIELD_LABELS + .iter() .find(|&&(o, _)| o == off) .map(|&(_, n)| n) - .or_else(|| extra_labels.iter() - .find(|&&(o, _)| o == off) - .map(|&(_, n)| n)) + .or_else(|| { + extra_labels + .iter() + .find(|&&(o, _)| o == off) + .map(|&(_, n)| n) + }) }; // Sections: CTask base, CGameTask unknowns, pos/speed, more unknowns, emitter, @@ -346,14 +393,19 @@ unsafe fn show_game_task_raw_fields( let mut chunk_start = 0x0FC; while chunk_start < total_size { let chunk_end = (chunk_start + 0x80).min(total_size); - sections.push((chunk_start, chunk_end, - format!("{} +0x{:03X}..0x{:03X}", type_name, chunk_start, chunk_end))); + sections.push(( + chunk_start, + chunk_end, + format!("{} +0x{:03X}..0x{:03X}", type_name, chunk_start, chunk_end), + )); chunk_start = chunk_end; } for (start, end, section_name) in §ions { let (start, end) = (*start, *end); - if start >= total_size { break; } + if start >= total_size { + break; + } let end = end.min(total_size); let header = format!("{} (0x{:03X}..0x{:03X})", section_name, start, end); let default_open = false; @@ -402,7 +454,10 @@ impl DebugApp { ui.horizontal(|ui| { ui.heading("Inspector"); ui.add_space(8.0); - if ui.add_enabled(!self.nav_history.is_empty(), egui::Button::new("← Back")).clicked() { + if ui + .add_enabled(!self.nav_history.is_empty(), egui::Button::new("← Back")) + .clicked() + { *go_back = true; } if !self.nav_history.is_empty() { @@ -422,312 +477,471 @@ impl DebugApp { egui::ScrollArea::vertical() .auto_shrink([false, false]) - .show(ui, |ui| { unsafe { - let vtable = *(addr as *const u32); - let name = entity_type_name(addr); - - ui.label(format!("Entity: {:#010X}", addr)); - ui.label(format!("Type: {}", name)); - ui.label(format!("Vtable: {:#010X}", vtable)); - ui.separator(); - - let task = addr as *const CTask; - - // --- CTask base --- - egui::CollapsingHeader::new("CTask base") - .default_open(true) - .show(ui, |ui| { - egui::Grid::new("ctask_grid").striped(true).show(ui, |ui| { - // Parent — clickable link - let parent = (*task).parent as u32; - ui.label("parent"); - if parent != 0 { - if ui.link(entity_label(parent)).clicked() { - *navigate_to = Some(parent); - } - } else { - ui.label("(none)"); - } - ui.end_row(); + .show(ui, |ui| { + unsafe { + let vtable = *(addr as *const u32); + let name = entity_type_name(addr); + + ui.label(format!("Entity: {:#010X}", addr)); + ui.label(format!("Type: {}", name)); + ui.label(format!("Vtable: {:#010X}", vtable)); + ui.separator(); + + let task = addr as *const CTask; + + // --- CTask base --- + egui::CollapsingHeader::new("CTask base") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new("ctask_grid").striped(true).show(ui, |ui| { + // Parent — clickable link + let parent = (*task).parent as u32; + ui.label("parent"); + if parent != 0 { + if ui.link(entity_label(parent)).clicked() { + *navigate_to = Some(parent); + } + } else { + ui.label("(none)"); + } + ui.end_row(); + + // children_watermark = total insertions (sparse array); live = non-null slots + ui.label("child slots"); + ui.label(format!( + "{} watermark / {} cap", + (*task).children_watermark, + (*task).children_capacity + )); + ui.end_row(); + ui.label("class_type"); + ui.label(format!("{:?}", (*task).class_type)); + ui.end_row(); + }); + }); - // children_watermark = total insertions (sparse array); live = non-null slots - ui.label("child slots"); ui.label(format!("{} watermark / {} cap", (*task).children_watermark, (*task).children_capacity)); ui.end_row(); - ui.label("class_type"); ui.label(format!("{:?}", (*task).class_type)); ui.end_row(); - }); - }); - - // --- Children tree --- - // read_children returns all slots (sparse); filter nulls for live count. - let children = read_children(task); - let live_count = children.iter().filter(|&&a| a != 0).count(); - let slot_count = (*task).children_watermark as usize; - if live_count > 0 || slot_count > 0 { - egui::CollapsingHeader::new(format!("Children ({} live / {} slots)", live_count, slot_count)) - .default_open(true) - .show(ui, |ui| { - for child_addr in &children { - let child_addr = *child_addr; - if child_addr == 0 { continue; } - let child_name = entity_type_name(child_addr); - - // Expand inline if the child itself has children - let child_task = child_addr as *const CTask; - let grandchildren = read_children(child_task); - let grandchild_count = grandchildren.iter().filter(|&&a| a != 0).count(); - - if grandchild_count > 0 { - // Show as a sub-collapsing header with its own children - let header_label = format!( - "{} @ {:#010X} ({} children)", - child_name, child_addr, grandchild_count - ); - egui::CollapsingHeader::new(&header_label) - .id_source(child_addr) - .default_open(false) - .show(ui, |ui| { - // Link to inspect this child in detail - if ui.link("→ Inspect").clicked() { - *navigate_to = Some(child_addr); - } - // Show grandchildren - for gc_addr in &grandchildren { - let gc_addr = *gc_addr; - if gc_addr == 0 { continue; } - if ui.link(entity_label(gc_addr)).clicked() { - *navigate_to = Some(gc_addr); + // --- Children tree --- + // read_children returns all slots (sparse); filter nulls for live count. + let children = read_children(task); + let live_count = children.iter().filter(|&&a| a != 0).count(); + let slot_count = (*task).children_watermark as usize; + if live_count > 0 || slot_count > 0 { + egui::CollapsingHeader::new(format!( + "Children ({} live / {} slots)", + live_count, slot_count + )) + .default_open(true) + .show(ui, |ui| { + for child_addr in &children { + let child_addr = *child_addr; + if child_addr == 0 { + continue; + } + let child_name = entity_type_name(child_addr); + + // Expand inline if the child itself has children + let child_task = child_addr as *const CTask; + let grandchildren = read_children(child_task); + let grandchild_count = + grandchildren.iter().filter(|&&a| a != 0).count(); + + if grandchild_count > 0 { + // Show as a sub-collapsing header with its own children + let header_label = format!( + "{} @ {:#010X} ({} children)", + child_name, child_addr, grandchild_count + ); + egui::CollapsingHeader::new(&header_label) + .id_source(child_addr) + .default_open(false) + .show(ui, |ui| { + // Link to inspect this child in detail + if ui.link("→ Inspect").clicked() { + *navigate_to = Some(child_addr); } - } - }); - } else { - // Leaf child — single clickable link - let label = format!(" {} @ {:#010X}", child_name, child_addr); - if ui.link(label).clicked() { - *navigate_to = Some(child_addr); + // Show grandchildren + for gc_addr in &grandchildren { + let gc_addr = *gc_addr; + if gc_addr == 0 { + continue; + } + if ui.link(entity_label(gc_addr)).clicked() { + *navigate_to = Some(gc_addr); + } + } + }); + } else { + // Leaf child — single clickable link + let label = format!(" {} @ {:#010X}", child_name, child_addr); + if ui.link(label).clicked() { + *navigate_to = Some(child_addr); + } } } - } - }); - } - - // --- CTaskMine-specific fields --- - if name == "CTaskMine" { - show_game_task_raw_fields(ui, addr, "CTaskMine", 0x128, &[ - (0x0FC, "unknown_fc[0]"), - (0x100, "unknown_fc[4]"), - (0x104, "unknown_fc[8]"), - (0x108, "unknown_fc[C]"), - (0x10C, "unknown_fc[10]"), - (0x110, "slot_id"), - (0x114, "unknown_114"), - (0x118, "fuse_timer"), - (0x11C, "unknown_11c"), - (0x120, "unknown_120"), - (0x124, "owner_team"), - ]); - } - - // --- CTaskOilDrum-specific fields --- - if name == "CTaskOilDrum" { - show_game_task_raw_fields(ui, addr, "CTaskOilDrum", 0x110, &[ - (0x0FC, "triggered"), - (0x100, "slot_id"), - (0x104, "unknown_104"), - (0x108, "health"), - (0x10C, "roll_counter"), - ]); - } + }); + } - // --- CTaskCrate-specific fields --- - if name == "CTaskCrate" { - show_game_task_raw_fields(ui, addr, "CTaskCrate", 0x4B0, &[ - (0x0FC, "unknown_fc"), - (0x100, "slot_id"), - (0x104, "unknown_104"), - (0x108, "unknown_108"), - (0x10C, "timer"), - // scheme_data starts at 0x110 (0xE5 DWORDs) - (0x124, "crate_type"), - (0x24C, "heal_amount"), - (0x264, "extra_init_flag"), - (0x4A4, "unknown_4a4"), - (0x4A8, "sequence_ref"), - (0x4AC, "unknown_4ac"), - ]); - } + // --- CTaskMine-specific fields --- + if name == "CTaskMine" { + show_game_task_raw_fields( + ui, + addr, + "CTaskMine", + 0x128, + &[ + (0x0FC, "unknown_fc[0]"), + (0x100, "unknown_fc[4]"), + (0x104, "unknown_fc[8]"), + (0x108, "unknown_fc[C]"), + (0x10C, "unknown_fc[10]"), + (0x110, "slot_id"), + (0x114, "unknown_114"), + (0x118, "fuse_timer"), + (0x11C, "unknown_11c"), + (0x120, "unknown_120"), + (0x124, "owner_team"), + ], + ); + } - // --- CTaskCloud-specific fields --- - if name == "CTaskCloud" { - let cloud = addr as *const CTaskCloud; - egui::CollapsingHeader::new("CTaskCloud") - .default_open(true) - .show(ui, |ui| { - egui::Grid::new("cloud_grid").striped(true).show(ui, |ui| { - ui.label("pos_x"); ui.label(format!("{:.1}", (*cloud).pos_x.to_f32())); ui.end_row(); - ui.label("pos_y"); ui.label(format!("{:.1}", (*cloud).pos_y.to_f32())); ui.end_row(); - ui.label("vel_x"); ui.label(format!("{:.4}", (*cloud).vel_x.to_f32())); ui.end_row(); - ui.label("vel_y"); ui.label(format!("{:.4}", (*cloud).vel_y.to_f32())); ui.end_row(); - ui.label("wind_accel"); ui.label(format!("{:.4}", (*cloud).wind_accel.to_f32())); ui.end_row(); - ui.label("wind_target"); ui.label(format!("{:.4}", (*cloud).wind_target.to_f32())); ui.end_row(); - ui.label("layer_depth"); ui.label(format!("{:.1}", (*cloud).layer_depth.to_f32())); ui.end_row(); - ui.label("sprite_id"); ui.label(format!("{:#06X}", (*cloud).sprite_id)); ui.end_row(); - }); - }); - } + // --- CTaskOilDrum-specific fields --- + if name == "CTaskOilDrum" { + show_game_task_raw_fields( + ui, + addr, + "CTaskOilDrum", + 0x110, + &[ + (0x0FC, "triggered"), + (0x100, "slot_id"), + (0x104, "unknown_104"), + (0x108, "health"), + (0x10C, "roll_counter"), + ], + ); + } - // --- CTaskTurnGame-specific fields --- - if name == "CTaskTurnGame" { - let tg = addr as *const CTaskTurnGame; - egui::CollapsingHeader::new("CTaskTurnGame") - .default_open(true) - .show(ui, |ui| { - egui::Grid::new("tg_grid").striped(true).show(ui, |ui| { - ui.label("current_team"); ui.label(format!("{}", (*tg).current_team)); ui.end_row(); - ui.label("current_worm"); ui.label(format!("{}", (*tg).current_worm)); ui.end_row(); - ui.label("arena_team"); ui.label(format!("{}", (*tg).arena_team)); ui.end_row(); - ui.label("arena_worm"); ui.label(format!("{}", (*tg).arena_worm)); ui.end_row(); - ui.label("worm_active"); ui.label(format!("{}", (*tg).worm_active != 0)); ui.end_row(); - ui.label("turn_ended"); ui.label(format!("{}", (*tg).turn_ended != 0)); ui.end_row(); - ui.label("no_time_lim"); ui.label(format!("{}", (*tg).no_time_limit != 0)); ui.end_row(); - ui.label("turn_timer"); ui.label(format!("{:.1}s", (*tg).turn_timer as f32 / 1000.0)); ui.end_row(); - ui.label("retreat"); ui.label(format!("{:.1}s", (*tg).retreat_timer as f32 / 1000.0)); ui.end_row(); - ui.label("idle_timer"); ui.label(format!("{:.1}s", (*tg).idle_timer as f32 / 1000.0)); ui.end_row(); - ui.label("num_teams"); ui.label(format!("{}", (*tg).num_teams)); ui.end_row(); - ui.label("active_frm"); ui.label(format!("{}", (*tg).active_worm_frames)); ui.end_row(); - ui.label("retreat_frm"); ui.label(format!("{}", (*tg).retreat_frames)); ui.end_row(); - }); - }); + // --- CTaskCrate-specific fields --- + if name == "CTaskCrate" { + show_game_task_raw_fields( + ui, + addr, + "CTaskCrate", + 0x4B0, + &[ + (0x0FC, "unknown_fc"), + (0x100, "slot_id"), + (0x104, "unknown_104"), + (0x108, "unknown_108"), + (0x10C, "timer"), + // scheme_data starts at 0x110 (0xE5 DWORDs) + (0x124, "crate_type"), + (0x24C, "heal_amount"), + (0x264, "extra_init_flag"), + (0x4A4, "unknown_4a4"), + (0x4A8, "sequence_ref"), + (0x4AC, "unknown_4ac"), + ], + ); + } - let ctx = &(*tg).game_ctx as *const TurnGameCtx; - egui::CollapsingHeader::new("TurnGameCtx (+0x30)") - .default_open(false) - .show(ui, |ui| { - egui::Grid::new("ctx_grid").striped(true).show(ui, |ui| { - ui.label("land_height"); ui.label(format!("{:.1}", (*ctx).land_height.to_f32())); ui.end_row(); - ui.label("land_height_2"); ui.label(format!("{:.1}", (*ctx).land_height_2.to_f32())); ui.end_row(); - ui.label("sentinel_18"); ui.label(format!("{}", (*ctx)._sentinel_18)); ui.end_row(); - ui.label("sentinel_28"); ui.label(format!("{}", (*ctx)._sentinel_28)); ui.end_row(); - ui.label("sentinel_38"); ui.label(format!("{}", (*ctx)._sentinel_38)); ui.end_row(); - ui.label("team_count"); ui.label(format!("{}", (*ctx).team_count)); ui.end_row(); - ui.label("_slot_d0"); ui.label(format!("{}", (*ctx)._slot_d0)); ui.end_row(); - let ta = (*ctx)._hud_textbox_a; - let tb = (*ctx)._hud_textbox_b; - ui.label("hud_tb_a"); ui.label(if ta == 0 { "(null)".into() } else { format!("{:#010X}", ta) }); ui.end_row(); - ui.label("hud_tb_b"); ui.label(if tb == 0 { "(null)".into() } else { format!("{:#010X}", tb) }); ui.end_row(); - }); - }); - } + // --- CTaskCloud-specific fields --- + if name == "CTaskCloud" { + let cloud = addr as *const CTaskCloud; + egui::CollapsingHeader::new("CTaskCloud") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new("cloud_grid").striped(true).show(ui, |ui| { + ui.label("pos_x"); + ui.label(format!("{:.1}", (*cloud).pos_x.to_f32())); + ui.end_row(); + ui.label("pos_y"); + ui.label(format!("{:.1}", (*cloud).pos_y.to_f32())); + ui.end_row(); + ui.label("vel_x"); + ui.label(format!("{:.4}", (*cloud).vel_x.to_f32())); + ui.end_row(); + ui.label("vel_y"); + ui.label(format!("{:.4}", (*cloud).vel_y.to_f32())); + ui.end_row(); + ui.label("wind_accel"); + ui.label(format!("{:.4}", (*cloud).wind_accel.to_f32())); + ui.end_row(); + ui.label("wind_target"); + ui.label(format!("{:.4}", (*cloud).wind_target.to_f32())); + ui.end_row(); + ui.label("layer_depth"); + ui.label(format!("{:.1}", (*cloud).layer_depth.to_f32())); + ui.end_row(); + ui.label("sprite_id"); + ui.label(format!("{:#06X}", (*cloud).sprite_id)); + ui.end_row(); + }); + }); + } - // --- CTaskTeam-specific fields --- - if name == "CTaskTeam" { - let team = addr as *const CTaskTeam; - egui::CollapsingHeader::new("CTaskTeam") - .default_open(true) - .show(ui, |ui| { - egui::Grid::new("team_grid").striped(true).show(ui, |ui| { - ui.label("team_index"); ui.label(format!("{}", (*team).team_index)); ui.end_row(); - ui.label("alive_worms"); ui.label(format!("{}", (*team).alive_worm_count)); ui.end_row(); - ui.label("worm_count"); ui.label(format!("{}", (*team).worm_count)); ui.end_row(); - ui.label("last_weapon"); ui.label(format!("{}", (*team).last_launched_weapon)); ui.end_row(); - ui.label("pos_x"); ui.label(format!("{:.2}", (*team).pos_x.to_f32())); ui.end_row(); - ui.label("pos_y"); ui.label(format!("{:.2}", (*team).pos_y.to_f32())); ui.end_row(); - }); - }); - } + // --- CTaskTurnGame-specific fields --- + if name == "CTaskTurnGame" { + let tg = addr as *const CTaskTurnGame; + egui::CollapsingHeader::new("CTaskTurnGame") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new("tg_grid").striped(true).show(ui, |ui| { + ui.label("current_team"); + ui.label(format!("{}", (*tg).current_team)); + ui.end_row(); + ui.label("current_worm"); + ui.label(format!("{}", (*tg).current_worm)); + ui.end_row(); + ui.label("arena_team"); + ui.label(format!("{}", (*tg).arena_team)); + ui.end_row(); + ui.label("arena_worm"); + ui.label(format!("{}", (*tg).arena_worm)); + ui.end_row(); + ui.label("worm_active"); + ui.label(format!("{}", (*tg).worm_active != 0)); + ui.end_row(); + ui.label("turn_ended"); + ui.label(format!("{}", (*tg).turn_ended != 0)); + ui.end_row(); + ui.label("no_time_lim"); + ui.label(format!("{}", (*tg).no_time_limit != 0)); + ui.end_row(); + ui.label("turn_timer"); + ui.label(format!("{:.1}s", (*tg).turn_timer as f32 / 1000.0)); + ui.end_row(); + ui.label("retreat"); + ui.label(format!( + "{:.1}s", + (*tg).retreat_timer as f32 / 1000.0 + )); + ui.end_row(); + ui.label("idle_timer"); + ui.label(format!("{:.1}s", (*tg).idle_timer as f32 / 1000.0)); + ui.end_row(); + ui.label("num_teams"); + ui.label(format!("{}", (*tg).num_teams)); + ui.end_row(); + ui.label("active_frm"); + ui.label(format!("{}", (*tg).active_worm_frames)); + ui.end_row(); + ui.label("retreat_frm"); + ui.label(format!("{}", (*tg).retreat_frames)); + ui.end_row(); + }); + }); + + let ctx = &(*tg).game_ctx as *const TurnGameCtx; + egui::CollapsingHeader::new("TurnGameCtx (+0x30)") + .default_open(false) + .show(ui, |ui| { + egui::Grid::new("ctx_grid").striped(true).show(ui, |ui| { + ui.label("land_height"); + ui.label(format!("{:.1}", (*ctx).land_height.to_f32())); + ui.end_row(); + ui.label("land_height_2"); + ui.label(format!("{:.1}", (*ctx).land_height_2.to_f32())); + ui.end_row(); + ui.label("sentinel_18"); + ui.label(format!("{}", (*ctx)._sentinel_18)); + ui.end_row(); + ui.label("sentinel_28"); + ui.label(format!("{}", (*ctx)._sentinel_28)); + ui.end_row(); + ui.label("sentinel_38"); + ui.label(format!("{}", (*ctx)._sentinel_38)); + ui.end_row(); + ui.label("team_count"); + ui.label(format!("{}", (*ctx).team_count)); + ui.end_row(); + ui.label("_slot_d0"); + ui.label(format!("{}", (*ctx)._slot_d0)); + ui.end_row(); + let ta = (*ctx)._hud_textbox_a; + let tb = (*ctx)._hud_textbox_b; + ui.label("hud_tb_a"); + ui.label(if ta == 0 { + "(null)".into() + } else { + format!("{:#010X}", ta) + }); + ui.end_row(); + ui.label("hud_tb_b"); + ui.label(if tb == 0 { + "(null)".into() + } else { + format!("{:#010X}", tb) + }); + ui.end_row(); + }); + }); + } - // --- CTaskFire-specific fields --- - if name == "CTaskFire" { - let fire = addr as *const CTaskFire; - egui::CollapsingHeader::new("CTaskFire") - .default_open(true) - .show(ui, |ui| { - egui::Grid::new("fire_grid").striped(true).show(ui, |ui| { - ui.label("spawn_x"); ui.label(format!("{:.1}", (*fire).spawn_x.to_f32())); ui.end_row(); - ui.label("spawn_y"); ui.label(format!("{:.1}", (*fire).spawn_y.to_f32())); ui.end_row(); - ui.label("timer"); ui.label(format!("{}", (*fire).timer)); ui.end_row(); - ui.label("burn_rate"); ui.label(format!("{}", (*fire).burn_rate)); ui.end_row(); - ui.label("spread_ctr"); ui.label(format!("{}", (*fire).spread_counter)); ui.end_row(); - ui.label("lifetime"); ui.label(format!("{}", (*fire).lifetime)); ui.end_row(); - ui.label("slot_index"); ui.label(format!("{}", (*fire).slot_index)); ui.end_row(); - }); - }); - } + // --- CTaskTeam-specific fields --- + if name == "CTaskTeam" { + let team = addr as *const CTaskTeam; + egui::CollapsingHeader::new("CTaskTeam") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new("team_grid").striped(true).show(ui, |ui| { + ui.label("team_index"); + ui.label(format!("{}", (*team).team_index)); + ui.end_row(); + ui.label("alive_worms"); + ui.label(format!("{}", (*team).alive_worm_count)); + ui.end_row(); + ui.label("worm_count"); + ui.label(format!("{}", (*team).worm_count)); + ui.end_row(); + ui.label("last_weapon"); + ui.label(format!("{}", (*team).last_launched_weapon)); + ui.end_row(); + ui.label("pos_x"); + ui.label(format!("{:.2}", (*team).pos_x.to_f32())); + ui.end_row(); + ui.label("pos_y"); + ui.label(format!("{:.2}", (*team).pos_y.to_f32())); + ui.end_row(); + }); + }); + } - // --- CTaskWorm-specific fields --- - if name == "CTaskWorm" || (*task).class_type == openwa_core::game::ClassType::Worm { - // Summary header with key info - let worm = addr as *const CTaskWorm; - let name_bytes = &(*worm).worm_name; - let nul = name_bytes.iter().position(|&b| b == 0).unwrap_or(name_bytes.len()); - let worm_name = std::str::from_utf8(&name_bytes[..nul]).unwrap_or("?"); - ui.label(format!("Worm: \"{}\" state={:#04X} team={} idx={}", - worm_name, (*worm).state(), (*worm).team_index, (*worm).worm_index)); - ui.separator(); - - show_game_task_raw_fields(ui, addr, "CTaskWorm", 0x3FC, &[ - (0x0FC, "team_index"), - (0x100, "worm_index"), - (0x104, "unknown_104"), - (0x110, "spawn_params[0]"), - (0x114, "spawn_params[1]"), - (0x118, "spawn_params[2]"), - (0x11C, "spawn_params[3]"), - (0x120, "spawn_params[4]"), - (0x124, "spawn_params[5]"), - (0x128, "spawn_params[6]"), - (0x12C, "spawn_params[7]"), - (0x130, "spawn_params[8]"), - (0x134, "spawn_params[9]"), - (0x158, "slot_id"), - (0x164, "stationary_frames"), - (0x170, "selected_weapon"), - (0x1A8, "facing_dir_2"), - (0x1AC, "facing_dir_inv"), - (0x1EC, "movement_streak"), - (0x24C, "aim_angle"), - (0x268, "show_cursor"), - (0x2F0, "worm_name[0..4]"), - (0x2F4, "worm_name[4..8]"), - (0x2F8, "worm_name[8..12]"), - (0x2FC, "worm_name[12..16]"), - (0x334, "facing_dir_3"), - (0x368, "animator"), - (0x3DC, "facing_direction"), - (0x3E4, "input_aim_up"), - (0x3E8, "input_aim_down"), - (0x3EC, "input_move_left"), - (0x3F0, "input_move_right"), - ]); - } + // --- CTaskFire-specific fields --- + if name == "CTaskFire" { + let fire = addr as *const CTaskFire; + egui::CollapsingHeader::new("CTaskFire") + .default_open(true) + .show(ui, |ui| { + egui::Grid::new("fire_grid").striped(true).show(ui, |ui| { + ui.label("spawn_x"); + ui.label(format!("{:.1}", (*fire).spawn_x.to_f32())); + ui.end_row(); + ui.label("spawn_y"); + ui.label(format!("{:.1}", (*fire).spawn_y.to_f32())); + ui.end_row(); + ui.label("timer"); + ui.label(format!("{}", (*fire).timer)); + ui.end_row(); + ui.label("burn_rate"); + ui.label(format!("{}", (*fire).burn_rate)); + ui.end_row(); + ui.label("spread_ctr"); + ui.label(format!("{}", (*fire).spread_counter)); + ui.end_row(); + ui.label("lifetime"); + ui.label(format!("{}", (*fire).lifetime)); + ui.end_row(); + ui.label("slot_index"); + ui.label(format!("{}", (*fire).slot_index)); + ui.end_row(); + }); + }); + } - // --- CTaskMissile-specific fields --- - if name == "CTaskMissile" { - use openwa_core::task::CTaskMissile; - let m = &*(addr as *const CTaskMissile); - ui.label(format!("Missile: type={:?} slot={} homing={} dir={}", - m.missile_type(), m.slot_id, m.homing_enabled, m.direction)); - ui.separator(); - - show_game_task_raw_fields(ui, addr, "CTaskMissile", 0x41C, &[ - (0x128, "launch_seed"), - (0x12C, "slot_id"), - (0x130, "spawn: owner_id"), - (0x134, "spawn: [1]"), - (0x138, "spawn: spawn_x"), - (0x13C, "spawn: spawn_y"), - (0x140, "spawn: speed_x"), - (0x144, "spawn: speed_y"), - (0x148, "spawn: cursor_x"), - (0x14C, "spawn: cursor_y"), - (0x150, "spawn: pellet_idx"), - (0x154, "spawn: fb_timer"), - (0x158, "spawn: fb_param"), - (0x3A0, "launch_speed"), - (0x3A8, "homing_enabled"), - (0x3C8, "direction"), - ]); - } + // --- CTaskWorm-specific fields --- + if name == "CTaskWorm" + || (*task).class_type == openwa_core::game::ClassType::Worm + { + // Summary header with key info + let worm = addr as *const CTaskWorm; + let name_bytes = &(*worm).worm_name; + let nul = name_bytes + .iter() + .position(|&b| b == 0) + .unwrap_or(name_bytes.len()); + let worm_name = std::str::from_utf8(&name_bytes[..nul]).unwrap_or("?"); + ui.label(format!( + "Worm: \"{}\" state={:#04X} team={} idx={}", + worm_name, + (*worm).state(), + (*worm).team_index, + (*worm).worm_index + )); + ui.separator(); + + show_game_task_raw_fields( + ui, + addr, + "CTaskWorm", + 0x3FC, + &[ + (0x0FC, "team_index"), + (0x100, "worm_index"), + (0x104, "unknown_104"), + (0x110, "spawn_params[0]"), + (0x114, "spawn_params[1]"), + (0x118, "spawn_params[2]"), + (0x11C, "spawn_params[3]"), + (0x120, "spawn_params[4]"), + (0x124, "spawn_params[5]"), + (0x128, "spawn_params[6]"), + (0x12C, "spawn_params[7]"), + (0x130, "spawn_params[8]"), + (0x134, "spawn_params[9]"), + (0x158, "slot_id"), + (0x164, "stationary_frames"), + (0x170, "selected_weapon"), + (0x1A8, "facing_dir_2"), + (0x1AC, "facing_dir_inv"), + (0x1EC, "movement_streak"), + (0x24C, "aim_angle"), + (0x268, "show_cursor"), + (0x2F0, "worm_name[0..4]"), + (0x2F4, "worm_name[4..8]"), + (0x2F8, "worm_name[8..12]"), + (0x2FC, "worm_name[12..16]"), + (0x334, "facing_dir_3"), + (0x368, "animator"), + (0x3DC, "facing_direction"), + (0x3E4, "input_aim_up"), + (0x3E8, "input_aim_down"), + (0x3EC, "input_move_left"), + (0x3F0, "input_move_right"), + ], + ); + } - }}); + // --- CTaskMissile-specific fields --- + if name == "CTaskMissile" { + use openwa_core::task::CTaskMissile; + let m = &*(addr as *const CTaskMissile); + ui.label(format!( + "Missile: type={:?} slot={} homing={} dir={}", + m.missile_type(), + m.slot_id, + m.homing_enabled, + m.direction + )); + ui.separator(); + + show_game_task_raw_fields( + ui, + addr, + "CTaskMissile", + 0x41C, + &[ + (0x128, "launch_seed"), + (0x12C, "slot_id"), + (0x130, "spawn: owner_id"), + (0x134, "spawn: [1]"), + (0x138, "spawn: spawn_x"), + (0x13C, "spawn: spawn_y"), + (0x140, "spawn: speed_x"), + (0x144, "spawn: speed_y"), + (0x148, "spawn: cursor_x"), + (0x14C, "spawn: cursor_y"), + (0x150, "spawn: pellet_idx"), + (0x154, "spawn: fb_timer"), + (0x158, "spawn: fb_param"), + (0x3A0, "launch_speed"), + (0x3A8, "homing_enabled"), + (0x3C8, "direction"), + ], + ); + } + } + }); } } diff --git a/crates/openwa-debugui/src/log.rs b/crates/openwa-debugui/src/log.rs index d4c8ed80d123b3c46335035dca2b3df26819d91c..ef8eeccab1ee7e230ac04cc85d38c9962b2c6453 100644 --- a/crates/openwa-debugui/src/log.rs +++ b/crates/openwa-debugui/src/log.rs @@ -19,13 +19,18 @@ pub fn push(msg: impl Into) { if b.len() >= 500 { b.pop_front(); } - b.push_back(LogEntry { ts: Instant::now(), text: msg.into() }); + b.push_back(LogEntry { + ts: Instant::now(), + text: msg.into(), + }); } } /// Snapshot up to `max` of the most recent entries (cloned). pub fn snapshot(max: usize) -> Vec<(Instant, String)> { - let Ok(b) = buf().lock() else { return Vec::new() }; + let Ok(b) = buf().lock() else { + return Vec::new(); + }; let start = b.len().saturating_sub(max); b.range(start..).map(|e| (e.ts, e.text.clone())).collect() } diff --git a/crates/openwa-harness/src/lib.rs b/crates/openwa-harness/src/lib.rs index 8ba96d27c793bdcf20c5f618409f28271577aa45..b4aa3cad6cf24f3fbf604ad28bf09124dfb34159 100644 --- a/crates/openwa-harness/src/lib.rs +++ b/crates/openwa-harness/src/lib.rs @@ -149,8 +149,8 @@ mod iat { original_first_thunk: u32, // RVA to INT (Import Name Table) time_date_stamp: u32, forwarder_chain: u32, - name: u32, // RVA to DLL name string - first_thunk: u32, // RVA to IAT (Import Address Table) — this is what we patch + name: u32, // RVA to DLL name string + first_thunk: u32, // RVA to IAT (Import Address Table) — this is what we patch } /// PE import-by-name entry @@ -434,8 +434,8 @@ mod tests { let img = wa(); let valid_prologues: &[u8] = &[ - 0x55, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, 0xB8, - 0x51, 0x52, 0x64, 0x85, 0x8D, 0x53, 0xC2, 0xC3, 0x33, + 0x55, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, 0xB8, 0x51, 0x52, 0x64, 0x85, 0x8D, 0x53, + 0xC2, 0xC3, 0x33, ]; // Read all CTask vtable entries and check their prologues @@ -501,7 +501,10 @@ mod tests { *heap_handle_ptr = heap as u32; *heap_type_ptr = 1; // Simple HeapAlloc mode — no SBH - eprintln!(" CRT heap initialized: handle=0x{:08X}, type=1", heap as u32); + eprintln!( + " CRT heap initialized: handle=0x{:08X}, type=1", + heap as u32 + ); } #[test] @@ -509,7 +512,9 @@ mod tests { let _lock = mutation_lock().lock().unwrap(); let img = wa(); img.patch_imports().expect("IAT patching failed"); - unsafe { init_wa_crt_heap(img); } + unsafe { + init_wa_crt_heap(img); + } // thunk_FUN_005c0ab8 is __cdecl: (size) -> ptr // This is WA's malloc wrapper — calls _malloc, checks for null @@ -540,7 +545,9 @@ mod tests { let _lock = mutation_lock().lock().unwrap(); let img = wa(); img.patch_imports().expect("IAT patching failed"); - unsafe { init_wa_crt_heap(img); } + unsafe { + init_wa_crt_heap(img); + } // Initialize security cookie (needed for stack protection in SEH functions) const SECURITY_INIT_COOKIE: u32 = 0x005E_7EAB; @@ -552,7 +559,8 @@ mod tests { } // CTask::Constructor is __stdcall: (this, parent, ddgame) -> this - type CTaskCtorFn = unsafe extern "stdcall" fn(this: *mut u8, parent: u32, ddgame: u32) -> *mut u8; + type CTaskCtorFn = + unsafe extern "stdcall" fn(this: *mut u8, parent: u32, ddgame: u32) -> *mut u8; unsafe { let ctor: CTaskCtorFn = img.func_ptr(va::CTASK_CONSTRUCTOR); @@ -571,7 +579,8 @@ mod tests { let vtable = *(task_ptr as *const u32); let vtable_ghidra = img.to_ghidra(vtable); assert_eq!( - vtable_ghidra, va::CTASK_VTABLE, + vtable_ghidra, + va::CTASK_VTABLE, "CTask vtable: expected 0x{:08X}, got 0x{vtable_ghidra:08X}", va::CTASK_VTABLE ); diff --git a/crates/openwa-launcher/src/inject.rs b/crates/openwa-launcher/src/inject.rs index f15cfebc5e1916f1789481e12843b3019515e028..4d5e38b05e50187a9be7dd383eb7d354862abc25 100644 --- a/crates/openwa-launcher/src/inject.rs +++ b/crates/openwa-launcher/src/inject.rs @@ -19,26 +19,41 @@ type FARPROC = unsafe extern "system" fn() -> isize; extern "system" { fn VirtualAllocEx( - hProcess: HANDLE, lpAddress: LPVOID, dwSize: SIZE_T, - flAllocationType: DWORD, flProtect: DWORD, + hProcess: HANDLE, + lpAddress: LPVOID, + dwSize: SIZE_T, + flAllocationType: DWORD, + flProtect: DWORD, ) -> LPVOID; - fn VirtualFreeEx(hProcess: HANDLE, lpAddress: LPVOID, dwSize: SIZE_T, dwFreeType: DWORD) -> BOOL; + fn VirtualFreeEx( + hProcess: HANDLE, + lpAddress: LPVOID, + dwSize: SIZE_T, + dwFreeType: DWORD, + ) -> BOOL; fn WriteProcessMemory( - hProcess: HANDLE, lpBaseAddress: LPVOID, lpBuffer: LPCVOID, - nSize: SIZE_T, lpNumberOfBytesWritten: *mut SIZE_T, + hProcess: HANDLE, + lpBaseAddress: LPVOID, + lpBuffer: LPCVOID, + nSize: SIZE_T, + lpNumberOfBytesWritten: *mut SIZE_T, ) -> BOOL; fn GetModuleHandleA(lpModuleName: *const u8) -> HANDLE; fn GetProcAddress(hModule: HANDLE, lpProcName: *const u8) -> Option; fn CreateRemoteThread( - hProcess: HANDLE, lpThreadAttributes: LPVOID, dwStackSize: SIZE_T, + hProcess: HANDLE, + lpThreadAttributes: LPVOID, + dwStackSize: SIZE_T, lpStartAddress: unsafe extern "system" fn(LPVOID) -> DWORD, - lpParameter: LPVOID, dwCreationFlags: DWORD, lpThreadId: *mut DWORD, + lpParameter: LPVOID, + dwCreationFlags: DWORD, + lpThreadId: *mut DWORD, ) -> HANDLE; fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD; fn CloseHandle(hObject: HANDLE) -> BOOL; } -const MEM_COMMIT: DWORD = 0x1000; +const MEM_COMMIT: DWORD = 0x1000; const MEM_RESERVE: DWORD = 0x2000; const MEM_RELEASE: DWORD = 0x8000; const PAGE_READWRITE: DWORD = 0x04; @@ -49,14 +64,16 @@ const INFINITE: DWORD = 0xFFFF_FFFF; /// Blocks until `DllMain` returns (i.e. all hooks are installed) before returning, /// so the caller can safely resume the main thread. pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String> { - let path_cstr = CString::new(dll_path) - .map_err(|e| format!("bad DLL path: {e}"))?; + let path_cstr = CString::new(dll_path).map_err(|e| format!("bad DLL path: {e}"))?; let path_bytes = path_cstr.as_bytes_with_nul(); // Allocate memory in the target process for the DLL path string. let remote_buf = VirtualAllocEx( - process, ptr::null_mut(), path_bytes.len(), - MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE, + process, + ptr::null_mut(), + path_bytes.len(), + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, ); if remote_buf.is_null() { return Err("VirtualAllocEx failed".to_string()); @@ -64,8 +81,10 @@ pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String> // Write the path into the target process. let ok = WriteProcessMemory( - process, remote_buf, - path_bytes.as_ptr().cast(), path_bytes.len(), + process, + remote_buf, + path_bytes.as_ptr().cast(), + path_bytes.len(), ptr::null_mut(), ); if ok == 0 { @@ -89,9 +108,13 @@ pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String> // Spin up a remote thread that calls LoadLibraryA(dll_path). // This triggers DllMain(DLL_PROCESS_ATTACH) which installs all our hooks. let thread = CreateRemoteThread( - process, ptr::null_mut(), 0, + process, + ptr::null_mut(), + 0, mem::transmute(load_library), - remote_buf, 0, ptr::null_mut(), + remote_buf, + 0, + ptr::null_mut(), ); if thread.is_null() { VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE); diff --git a/crates/openwa-launcher/src/main.rs b/crates/openwa-launcher/src/main.rs index 341ecb16eaa7acb5ab45029456cff9e6098c09a0..1bcd8b136c1725aea9d1ca4ee518bfac661c623b 100644 --- a/crates/openwa-launcher/src/main.rs +++ b/crates/openwa-launcher/src/main.rs @@ -6,41 +6,67 @@ use std::path::{Path, PathBuf}; use std::ptr; // Declare Win32 types and functions directly to avoid windows-sys feature-flag issues. -#[allow(non_camel_case_types)] type HANDLE = *mut core::ffi::c_void; -#[allow(non_camel_case_types)] type HMODULE = *mut core::ffi::c_void; -#[allow(non_camel_case_types)] type DWORD = u32; -#[allow(non_camel_case_types)] type BOOL = i32; -#[allow(non_camel_case_types)] type LPVOID = *mut core::ffi::c_void; -#[allow(non_camel_case_types)] type LPDWORD = *mut u32; +#[allow(non_camel_case_types)] +type HANDLE = *mut core::ffi::c_void; +#[allow(non_camel_case_types)] +type HMODULE = *mut core::ffi::c_void; +#[allow(non_camel_case_types)] +type DWORD = u32; +#[allow(non_camel_case_types)] +type BOOL = i32; +#[allow(non_camel_case_types)] +type LPVOID = *mut core::ffi::c_void; +#[allow(non_camel_case_types)] +type LPDWORD = *mut u32; #[repr(C)] #[allow(non_snake_case)] struct STARTUPINFOA { - cb: DWORD, lpReserved: *mut u8, lpDesktop: *mut u8, lpTitle: *mut u8, - dwX: DWORD, dwY: DWORD, dwXSize: DWORD, dwYSize: DWORD, - dwXCountChars: DWORD, dwYCountChars: DWORD, dwFillAttribute: DWORD, - dwFlags: DWORD, wShowWindow: u16, cbReserved2: u16, - lpReserved2: *mut u8, hStdInput: HANDLE, hStdOutput: HANDLE, hStdError: HANDLE, + cb: DWORD, + lpReserved: *mut u8, + lpDesktop: *mut u8, + lpTitle: *mut u8, + dwX: DWORD, + dwY: DWORD, + dwXSize: DWORD, + dwYSize: DWORD, + dwXCountChars: DWORD, + dwYCountChars: DWORD, + dwFillAttribute: DWORD, + dwFlags: DWORD, + wShowWindow: u16, + cbReserved2: u16, + lpReserved2: *mut u8, + hStdInput: HANDLE, + hStdOutput: HANDLE, + hStdError: HANDLE, } #[repr(C)] #[allow(non_snake_case)] struct PROCESS_INFORMATION { - hProcess: HANDLE, hThread: HANDLE, dwProcessId: DWORD, dwThreadId: DWORD, + hProcess: HANDLE, + hThread: HANDLE, + dwProcessId: DWORD, + dwThreadId: DWORD, } -const CREATE_SUSPENDED: DWORD = 0x0000_0004; -const STARTF_USESHOWWINDOW: DWORD = 0x0000_0001; -const SW_SHOWMINIMIZED: u16 = 2; -const INFINITE: DWORD = 0xFFFF_FFFF; +const CREATE_SUSPENDED: DWORD = 0x0000_0004; +const STARTF_USESHOWWINDOW: DWORD = 0x0000_0001; +const SW_SHOWMINIMIZED: u16 = 2; +const INFINITE: DWORD = 0xFFFF_FFFF; #[link(name = "kernel32")] extern "system" { fn CreateProcessA( - lpApplicationName: *const u8, lpCommandLine: *mut u8, - lpProcessAttributes: LPVOID, lpThreadAttributes: LPVOID, - bInheritHandles: BOOL, dwCreationFlags: DWORD, - lpEnvironment: LPVOID, lpCurrentDirectory: *const u8, + lpApplicationName: *const u8, + lpCommandLine: *mut u8, + lpProcessAttributes: LPVOID, + lpThreadAttributes: LPVOID, + bInheritHandles: BOOL, + dwCreationFlags: DWORD, + lpEnvironment: LPVOID, + lpCurrentDirectory: *const u8, lpStartupInfo: *const STARTUPINFOA, lpProcessInformation: *mut PROCESS_INFORMATION, ) -> BOOL; @@ -51,7 +77,9 @@ extern "system" { fn CloseHandle(hObject: HANDLE) -> BOOL; fn GetModuleFileNameA(hModule: HMODULE, lpFilename: *mut u8, nSize: DWORD) -> DWORD; fn CreateEventA( - lpEventAttributes: LPVOID, bManualReset: BOOL, bInitialState: BOOL, + lpEventAttributes: LPVOID, + bManualReset: BOOL, + bInitialState: BOOL, lpName: *const u8, ) -> HANDLE; } @@ -143,7 +171,8 @@ unsafe fn launch( let ok = CreateProcessA( exe_cstr.as_ptr().cast(), cmdline_buf.as_mut_ptr(), - ptr::null_mut(), ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), 0, // bInheritHandles = FALSE CREATE_SUSPENDED, ptr::null_mut(), // inherit environment @@ -156,9 +185,7 @@ unsafe fn launch( return Err("CreateProcessA failed — is WA.exe path correct?".to_string()); } - let dll_str = dll_path - .to_str() - .ok_or("DLL path is not valid UTF-8")?; + let dll_str = dll_path.to_str().ok_or("DLL path is not valid UTF-8")?; // Create a named event that the DLL will signal after all hooks are // installed. This ensures the main thread doesn't run any WA code @@ -168,7 +195,9 @@ unsafe fn launch( // event may be null if CreateEventA fails — we'll fall through gracefully. if let Err(e) = inject::inject_dll(pi.hProcess, dll_str) { - if !event.is_null() { CloseHandle(event); } + if !event.is_null() { + CloseHandle(event); + } TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); @@ -204,9 +233,7 @@ unsafe fn launch( fn launcher_dir() -> Result { let mut buf = vec![0u8; 32768]; - let len = unsafe { - GetModuleFileNameA(ptr::null_mut(), buf.as_mut_ptr(), buf.len() as DWORD) - }; + let len = unsafe { GetModuleFileNameA(ptr::null_mut(), buf.as_mut_ptr(), buf.len() as DWORD) }; if len == 0 { return Err("GetModuleFileNameA failed".to_string()); } @@ -251,7 +278,5 @@ fn find_wa_hardcoded() -> Option { r"C:\Program Files\Steam\steamapps\common\Worms Armageddon\WA.exe", ]; - CANDIDATES.iter() - .map(PathBuf::from) - .find(|p| p.exists()) + CANDIDATES.iter().map(PathBuf::from).find(|p| p.exists()) } diff --git a/crates/openwa-wormkit/src/hook.rs b/crates/openwa-wormkit/src/hook.rs index d83f7b65960147990df152e40e1838d69b4bd2cd..6be52869794fb33513a4f7528d41372fecfdc49f 100644 --- a/crates/openwa-wormkit/src/hook.rs +++ b/crates/openwa-wormkit/src/hook.rs @@ -326,15 +326,12 @@ macro_rules! install_trap { ($name:literal, $addr:expr) => {{ unsafe extern "C" fn trap() { panic!(concat!( - "TRAP: ", $name, + "TRAP: ", + $name, " called by WA.exe — all callers should be Rust" )); } - let _ = hook::install( - concat!($name, " [TRAP]"), - $addr, - trap as *const (), - )?; + let _ = hook::install(concat!($name, " [TRAP]"), $addr, trap as *const ())?; }}; } diff --git a/crates/openwa-wormkit/src/lib.rs b/crates/openwa-wormkit/src/lib.rs index 53256982457e64d7d49b6df0cc02e7ec133d45ce..8131f6d1e458809f7be2a11c1638c1ac0d6f939d 100644 --- a/crates/openwa-wormkit/src/lib.rs +++ b/crates/openwa-wormkit/src/lib.rs @@ -2,8 +2,8 @@ use std::ffi::c_void; -pub mod hook; mod debug_ui; +pub mod hook; mod replacements; mod validation; @@ -53,6 +53,11 @@ fn run() -> Result<(), String> { let _ = clear_log(); let _ = std::fs::write("OpenWA_validation.log", ""); + // Install panic hook that writes to our log file + std::panic::set_hook(Box::new(|info| { + let _ = log_line(&format!("[PANIC] {info}")); + })); + let delta = openwa_core::rebase::init(); let _ = log_line(&format!( "=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}" @@ -88,8 +93,8 @@ fn run() -> Result<(), String> { /// Signal the `OpenWA_HooksReady` named event so the launcher knows it's /// safe to resume WA.exe's main thread. fn signal_hooks_ready() { - use windows_sys::Win32::System::Threading::{OpenEventA, SetEvent}; use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::Threading::{OpenEventA, SetEvent}; const EVENT_MODIFY_STATE: u32 = 0x0002; diff --git a/crates/openwa-wormkit/src/replacements/config.rs b/crates/openwa-wormkit/src/replacements/config.rs index a18a2b27fac921896cd6712a7d1ecce2ee22c5aa..0b31ad64d6ac6ee01fb72be186c3fb625d19d199 100644 --- a/crates/openwa-wormkit/src/replacements/config.rs +++ b/crates/openwa-wormkit/src/replacements/config.rs @@ -9,9 +9,9 @@ //! - GameInfo__LoadOptions (0x460AC0): game options from registry //! - Options__GetCrashReportURL (0x5A63F0): crash report URL from registry -use openwa_core::{address::va, engine::GameInfo}; use crate::log_line; use openwa_core::rebase::rb; +use openwa_core::{address::va, engine::GameInfo}; const THEME_PATH: &str = "data\\current.thm"; const CLEAN_ALL_FLAG_OFFSET: usize = 0xE0; @@ -68,7 +68,12 @@ fn show_error_message(msg: &str) { let mut msg_buf: Vec = msg.bytes().collect(); msg_buf.push(0); unsafe { - MessageBoxA(core::ptr::null_mut(), msg_buf.as_ptr(), core::ptr::null(), MB_OK); + MessageBoxA( + core::ptr::null_mut(), + msg_buf.as_ptr(), + core::ptr::null(), + MB_OK, + ); } } @@ -86,10 +91,8 @@ unsafe extern "stdcall" fn hook_delete_key_recursive(hkey: u32, subkey: u32) -> let _ = log_line(&format!("[Config] DeleteKeyRecursive: {subkey_str}")); - let result = openwa_core::wa::registry::delete_key_recursive( - hkey as usize as HKEY, - &subkey_str, - ); + let result = + openwa_core::wa::registry::delete_key_recursive(hkey as usize as HKEY, &subkey_str); let _ = log_line(&format!("[Config] DeleteKeyRecursive result: {result}")); result as i32 @@ -159,25 +162,27 @@ unsafe extern "stdcall" fn hook_load_options(game_info: *mut GameInfo) { // Format streams directory and randomize stream indices (global, not GameInfo) let streams_dest = rb(va::G_STREAMS_DIR) as *mut u8; let streams_path = format!("{}\\streams\0", base_str.to_string_lossy()); - core::ptr::copy_nonoverlapping( - streams_path.as_ptr(), - streams_dest, - streams_path.len(), - ); + core::ptr::copy_nonoverlapping(streams_path.as_ptr(), streams_dest, streams_path.len()); // Randomize stream indices (16 entries, each rand() % 11 + 1) let indices = rb(va::G_STREAM_INDICES) as *mut u32; let indices_end = rb(va::G_STREAM_INDICES_END) as usize; let mut ptr = indices; while (ptr as usize) < indices_end { - extern "cdecl" { fn rand() -> i32; } + extern "cdecl" { + fn rand() -> i32; + } *ptr = (rand() % 11 + 1) as u32; ptr = ptr.add(1); } // Stream volume: 0x10 if flag set, else 0 let stream_vol_addr = rb(va::G_STREAM_INDICES_END) as *mut u8; - *stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 { 0x10 } else { 0 }; + *stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 { + 0x10 + } else { + 0 + }; *(rb(va::G_STREAM_VOLUME) as *mut u8) = 0x4B; // Copy "data\land.dat" string (14 bytes) @@ -226,7 +231,11 @@ unsafe extern "stdcall" fn hook_load_options(game_info: *mut GameInfo) { } gi.info_transparency = read_profile_int("Options", "InfoTransparency", 0) as u8; - gi.info_spy = if read_profile_int("Options", "InfoSpy", 1) != 0 { 1 } else { 0 }; + gi.info_spy = if read_profile_int("Options", "InfoSpy", 1) != 0 { + 1 + } else { + 0 + }; gi.chat_pinned = read_profile_int("Options", "ChatPinned", 0) as u8; gi.chat_lines = read_profile_int("Options", "ChatLines", 0); gi.pinned_chat_lines = read_profile_int("Options", "PinnedChatLines", 0xFFFFFFFF); @@ -244,7 +253,8 @@ unsafe extern "stdcall" fn hook_load_options(game_info: *mut GameInfo) { } gi.background_debris_parallax = parallax << 16; - gi.topmost_explosion_onomatopoeia = read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0); + gi.topmost_explosion_onomatopoeia = + read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0); gi.capture_transparent_pngs = read_profile_int("Options", "CaptureTransparentPNGs", 0); // CameraUnlockMouseSpeed: clamp to max 0xB504, then square @@ -273,11 +283,8 @@ unsafe extern "cdecl" fn hook_get_crash_report_url() -> u32 { let buf = rb(va::G_CRASH_REPORT_URL) as *mut u8; let buf_slice = core::slice::from_raw_parts_mut(buf, CRASH_REPORT_URL_BUF_SIZE); - let len = openwa_core::wa::registry::read_profile_string( - "Options", - "CrashReportURL", - buf_slice, - ); + let len = + openwa_core::wa::registry::read_profile_string("Options", "CrashReportURL", buf_slice); if len > 0 { // Null-terminate @@ -300,17 +307,9 @@ pub fn install() -> Result<(), String> { hook_theme_get_file_size as *const (), )?; - let _ = crate::hook::install( - "Theme__Load", - va::THEME_LOAD, - hook_theme_load as *const (), - )?; + let _ = crate::hook::install("Theme__Load", va::THEME_LOAD, hook_theme_load as *const ())?; - let _ = crate::hook::install( - "Theme__Save", - va::THEME_SAVE, - hook_theme_save as *const (), - )?; + let _ = crate::hook::install("Theme__Save", va::THEME_SAVE, hook_theme_save as *const ())?; let _ = crate::hook::install( "Registry__DeleteKeyRecursive", diff --git a/crates/openwa-wormkit/src/replacements/ddgame_init.rs b/crates/openwa-wormkit/src/replacements/ddgame_init.rs new file mode 100644 index 0000000000000000000000000000000000000000..16b07326eacec47a4b99ace19f4a1af767ac0a96 --- /dev/null +++ b/crates/openwa-wormkit/src/replacements/ddgame_init.rs @@ -0,0 +1,205 @@ +//! Rust replacements for DDGame constructor sub-functions. +//! +//! Each function is hooked individually so it works regardless of whether the +//! DDGame constructor itself is Rust or the original WA code. +//! +//! ## Ported functions +//! +//! - `DDGame__InitFields` (0x526120): usercall(EDI=ddgame), plain RET → EAX=ddgame +//! - `DDGame__InitRenderIndices` (0x526080): usercall(ESI=base), plain RET → EAX=base +//! - `TaskStateMachine__Init` (0x4F6370): usercall(ESI,ECX,EDI) + 1 stack, RET 0x4 +//! - `GfxResource__Create_Maybe` (0x4F6300): usercall(ECX,EAX) + 1 stack, RET 0x4 +//! - `FUN_570E20` (display layer init): usercall(ESI=wrapper), plain RET + +use crate::hook; +use openwa_core::address::va; +use openwa_core::engine::ddgame::{ + ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init, gfx_dir_find_entry, + gfx_handler_load_dir, gfx_resource_create, task_state_machine_init, DDGame, +}; +use openwa_core::engine::DDGameWrapper; + +// ─── DDGame__InitFields (0x526120) ────────────────────────────────────────── + +hook::usercall_trampoline!( + fn init_fields_trampoline; + impl_fn = impl_init_fields; + reg = edi +); + +extern "cdecl" fn impl_init_fields(ddgame: u32) -> u32 { + unsafe { ddgame_init_fields(ddgame as *mut DDGame) } + ddgame // Original: MOV EAX, EDI; RET +} + +// ─── DDGame__InitRenderIndices (0x526080) ─────────────────────────────────── + +hook::usercall_trampoline!( + fn init_render_indices_trampoline; + impl_fn = impl_init_render_indices; + reg = esi +); + +extern "cdecl" fn impl_init_render_indices(base: u32) -> u32 { + unsafe { ddgame_init_render_indices(base as *mut u8) } + base // Original: MOV EAX, ESI; RET +} + +// ─── TaskStateMachine__Init (0x4F6370) ────────────────────────────────────── + +extern "cdecl" fn impl_tsm_init(object: u32, param1: u32, height: u32, width: u32) -> u32 { + unsafe { task_state_machine_init(object as *mut u8, param1, width, height) } + object // Original: MOV EAX, ESI; RET 0x4 +} + +#[unsafe(naked)] +unsafe extern "C" fn tsm_init_trampoline() { + core::arch::naked_asm!( + "push edx", + "push [esp+8]", // width (stack param) + "push edi", // height + "push ecx", // param1 + "push esi", // object + "call {impl_fn}", + "add esp, 16", + "pop edx", + "ret 0x4", + impl_fn = sym impl_tsm_init, + ); +} + +// ─── FUN_570E20 (display layer color init) ────────────────────────────────── + +hook::usercall_trampoline!( + fn display_layer_init_trampoline; + impl_fn = impl_display_layer_init; + reg = esi +); + +extern "cdecl" fn impl_display_layer_init(wrapper: u32) -> u32 { + unsafe { display_layer_color_init(wrapper as *mut DDGameWrapper) } + wrapper +} + +// ─── GfxResource__Create_Maybe (0x4F6300) ─────────────────────────────────── + +extern "cdecl" fn impl_gfx_resource_create(gfx_handler: u32, name: u32, output: u32) -> u32 { + let result = unsafe { + gfx_resource_create(gfx_handler as *mut u8, name as *const u8, output as *mut u8) + }; + result as u32 +} + +#[unsafe(naked)] +unsafe extern "C" fn gfx_resource_create_trampoline() { + core::arch::naked_asm!( + "push edx", + "push [esp+8]", // output (stack param) + "push eax", // name + "push ecx", // gfx_handler + "call {impl_fn}", + "add esp, 12", + "pop edx", + "ret 0x4", + impl_fn = sym impl_gfx_resource_create, + ); +} + +// ─── GfxDir__FindEntry (0x566520) ──────────────────────────────────────────── +// +// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4. + +hook::usercall_trampoline!( + fn find_entry_trampoline; + impl_fn = impl_find_entry; + reg = eax; + stack_params = 1; ret_bytes = "0x4" +); + +static FIND_ENTRY_LOG_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + +extern "cdecl" fn impl_find_entry(name: u32, gfx_handler: u32) -> u32 { + let result = unsafe { gfx_dir_find_entry(name as *const u8, gfx_handler as *mut u8) }; + + // Log first 20 lookups for debugging + let count = FIND_ENTRY_LOG_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed); + if count < 20 { + let name_str = unsafe { + let p = name as *const u8; + let mut len = 0; + while *p.add(len) != 0 && len < 64 { + len += 1; + } + core::str::from_utf8_unchecked(core::slice::from_raw_parts(p, len)) + }; + let _ = crate::log_line(&format!( + "[FindEntry] #{count}: \"{name_str}\" -> 0x{:08X}", + result as u32 + )); + } + result as u32 +} + +// ─── GfxHandler__LoadDir (0x5663E0) ────────────────────────────────────────── +// +// Convention: usercall(EAX=handler), plain RET. Returns 1/0. + +hook::usercall_trampoline!( + fn load_dir_trampoline; + impl_fn = impl_load_dir; + reg = eax +); + +extern "cdecl" fn impl_load_dir(handler: u32) -> u32 { + unsafe { gfx_handler_load_dir(handler as *mut u8) as u32 } +} + +// ─── Hook installation ────────────────────────────────────────────────────── + +pub fn install() -> Result<(), String> { + unsafe { + hook::install( + "DDGame__InitFields", + va::DDGAME_INIT_FIELDS, + init_fields_trampoline as *const (), + )?; + + hook::install( + "DDGame__InitRenderIndices", + va::DDGAME_INIT_RENDER_INDICES, + init_render_indices_trampoline as *const (), + )?; + + hook::install( + "TaskStateMachine__Init", + va::TASK_STATE_MACHINE_INIT, + tsm_init_trampoline as *const (), + )?; + + hook::install( + "GfxResource__Create", + va::GFX_RESOURCE_CREATE, + gfx_resource_create_trampoline as *const (), + )?; + + hook::install( + "FUN_570E20_DisplayLayerInit", + va::FUN_570E20, + display_layer_init_trampoline as *const (), + )?; + + hook::install( + "GfxDir__FindEntry", + va::GFX_DIR_FIND_ENTRY, + find_entry_trampoline as *const (), + )?; + + hook::install( + "GfxHandler__LoadDir", + va::GFX_HANDLER_LOAD_DIR, + load_dir_trampoline as *const (), + )?; + } + + Ok(()) +} diff --git a/crates/openwa-wormkit/src/replacements/display.rs b/crates/openwa-wormkit/src/replacements/display.rs index 38ea4f09717296e5e99ad8528bf147c2d1363126..f55bc8fc3989aefbc49f1d1231ed3a2c7d7648d2 100644 --- a/crates/openwa-wormkit/src/replacements/display.rs +++ b/crates/openwa-wormkit/src/replacements/display.rs @@ -5,12 +5,12 @@ //! - Headless vtable (0x66A0F8): replaces destructor with Rust version that //! correctly frees our Rust-allocated sprite cache sub-objects +use crate::log_line; use openwa_core::address::va; -use openwa_core::display::{DisplayBase, SpriteCacheWrapper, SpriteBufferCtrl}; +use openwa_core::display::{DisplayBase, SpriteBufferCtrl, SpriteCacheWrapper}; use openwa_core::rebase::rb; use openwa_core::vtable::patch_vtable; use openwa_core::wa_alloc::wa_free; -use crate::log_line; /// The _purecall function address (calls abort). const PURECALL: u32 = 0x005D_4E16; diff --git a/crates/openwa-wormkit/src/replacements/frontend.rs b/crates/openwa-wormkit/src/replacements/frontend.rs index 9cc1f8d28b50c57babbd0fa5a7c70a06fc5277df..3726b24370149cabd1e7f9f253d8005e69f97b87 100644 --- a/crates/openwa-wormkit/src/replacements/frontend.rs +++ b/crates/openwa-wormkit/src/replacements/frontend.rs @@ -6,10 +6,10 @@ use std::sync::atomic::{AtomicU32, Ordering}; use crate::log_line; -use openwa_core::wa::mfc::{CDialogHandle, CWndHandle}; -use openwa_core::wa_call; use openwa_core::address::va; use openwa_core::game::ScreenId; +use openwa_core::wa::mfc::{CDialogHandle, CWndHandle}; +use openwa_core::wa_call; // Frontend dialog struct offsets (MFC CDialog-derived) const DIALOG_FLAGS: usize = 0x3C; @@ -67,7 +67,9 @@ unsafe extern "cdecl" fn frontend_change_screen_impl(dialog: u32, screen_id: u32 let name = ScreenId::try_from(screen_id as i32) .map(|s| format!("{s:?}")) .unwrap_or_else(|v| format!("Unknown({v})")); - let _ = log_line(&format!("[FrontendChangeScreen] screen_id={screen_id} ({name})")); + let _ = log_line(&format!( + "[FrontendChangeScreen] screen_id={screen_id} ({name})" + )); } pub fn install() -> Result<(), String> { diff --git a/crates/openwa-wormkit/src/replacements/game_session.rs b/crates/openwa-wormkit/src/replacements/game_session.rs index 2b387bdbd8fd99f848953040698353841ba99d8b..f2380577e89c03ff6e496a9a0a0bd3299db89871 100644 --- a/crates/openwa-wormkit/src/replacements/game_session.rs +++ b/crates/openwa-wormkit/src/replacements/game_session.rs @@ -10,20 +10,19 @@ //! - `DDGameWrapper__InitReplay` (0x56F860): usercall(EAX=game_info, ESI=this), //! plain RET (no stack args). Bridged via `call_init_replay`. //! - `DDGame__Constructor` (0x56E220): stdcall 9 params + implicit ECX=network. -//! Bridged via `ddgame_constructor_call` which sets ECX then **tail-jumps**, -//! preserving the exact stack so DDGame::ctor's `RET 0x24` returns to ctor_impl. +//! Bridged via `ddgame_constructor_call`. Being incrementally replaced by +//! `create_ddgame()` in openwa-core (not yet complete). //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute. -use openwa_core::address::va; -use openwa_core::engine::GameInfo; -use openwa_core::rebase::rb; -use openwa_core::engine::DDGameWrapper; -use openwa_core::display::DDDisplay; -use openwa_core::audio::DSSound; -use openwa_core::engine::GameSession; -use openwa_core::display::Palette; use crate::hook; use crate::log_line; +use openwa_core::address::va; +use openwa_core::audio::DSSound; +use openwa_core::display::{DDDisplay, Palette}; +use openwa_core::engine::ddgame::{create_ddgame, init_constructor_addrs, DDGame}; +use openwa_core::engine::{DDGameWrapper, GameInfo, GameSession}; +use openwa_core::rebase::rb; +use openwa_core::wa_alloc::{wa_free, wa_malloc}; /// Implicit EDI = game_info pointer, captured from EDI on entry. static mut GAME_INFO: *mut GameInfo = core::ptr::null_mut(); @@ -41,25 +40,15 @@ static mut DDGAME_CTOR_ADDR: u32 = 0; // ─── Bridge: DDGameWrapper__InitReplay ─────────────────────────────────────── // // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params). -// Declared as stdcall(game_info, this) so the Rust caller pushes both; the -// naked body loads EAX/ESI from the stack and calls via INIT_REPLAY_ADDR. -// -// Stack on entry (after stdcall push): -// [esp+0] = ret addr -// [esp+4] = game_info (arg 1, pushed last by stdcall) -// [esp+8] = this (arg 2, pushed first by stdcall) -// -// After `pushl %esi`: -// [esp+0] = old_esi, [esp+4] = ret addr, [esp+8] = game_info, [esp+0xC] = this #[unsafe(naked)] unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *mut DDGameWrapper) { core::arch::naked_asm!( "pushl %esi", "movl 8(%esp), %eax", // EAX = game_info "movl 0xC(%esp), %esi", // ESI = this - "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET from callee - "popl %esi", // restore ESI - "retl $8", // stdcall callee-cleanup: 2 × u32 = 8 bytes + "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET + "popl %esi", + "retl $8", // stdcall cleanup: 2 × u32 = 8 fn = sym INIT_REPLAY_ADDR, options(att_syntax), ); @@ -68,16 +57,7 @@ unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *m // ─── Bridge: DDGame__Constructor ───────────────────────────────────────────── // // Convention: stdcall 9 params + implicit ECX=network. -// -// Rust calls this as a normal stdcall(9 params). The naked body loads ECX from -// DDGAME_CTOR_ECX and then **tail-jumps** (jmp, not call) to DDGame::ctor. -// -// Tail-jump rationale: the call instruction in ctor_impl already pushed the -// return address at [esp+0]. After the tail-jump, DDGame::ctor sees exactly: -// [esp+0] = return address back into ctor_impl -// [esp+4] = this … [esp+24] = game_info (9 stdcall args) -// ECX = network (implicit) -// DDGame::ctor's `RET 0x24` pops 0x24 bytes and returns to ctor_impl. ✓ +// Tail-jumps to original; DDGame::ctor's RET 0x24 returns to caller. #[unsafe(naked)] unsafe extern "stdcall" fn ddgame_constructor_call( _this: *mut DDGameWrapper, @@ -91,8 +71,8 @@ unsafe extern "stdcall" fn ddgame_constructor_call( _game_info: *mut GameInfo, ) -> *mut u8 { core::arch::naked_asm!( - "movl {ecx_val}, %ecx", // ECX = input_ctrl (implicit param for DDGame::ctor) - "jmpl *({fn})", // tail-jump; DDGame::ctor's RET 0x24 returns to caller + "movl {ecx_val}, %ecx", // ECX = network (implicit param) + "jmpl *({fn})", // tail-jump; RET 0x24 returns to caller ecx_val = sym DDGAME_CTOR_ECX, fn = sym DDGAME_CTOR_ADDR, options(att_syntax), @@ -100,10 +80,6 @@ unsafe extern "stdcall" fn ddgame_constructor_call( } /// Called by `impl_init_hardware` to construct the DDGameWrapper in-place. -/// -/// Sets `GAME_INFO` (read by `ctor_impl`) and delegates directly to `ctor_impl`, -/// bypassing the naked entry trampoline which is designed as a hook target, not a -/// callable subroutine. pub(crate) unsafe fn construct_ddgame_wrapper( game_info: *mut GameInfo, this: *mut DDGameWrapper, @@ -116,8 +92,6 @@ pub(crate) unsafe fn construct_ddgame_wrapper( ) -> *mut DDGameWrapper { GAME_INFO = game_info; - let this = this; - // Initialize DDGameWrapper fields (order matches original decompile). (*this).ddgame = core::ptr::null_mut(); (*this).landscape = core::ptr::null_mut(); @@ -131,19 +105,112 @@ pub(crate) unsafe fn construct_ddgame_wrapper( // Read timer_obj and net_game from the live game session struct. let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession); let timer_obj = (*session).timer_obj; - let net_game = (*session).net_game; + let net_game = (*session).net_game; - // Store input_ctrl as the implicit ECX for DDGame::ctor, then tail-jump-call. - DDGAME_CTOR_ECX = input_ctrl as u32; - ddgame_constructor_call( - this, display, sound, keyboard, palette, streaming_audio, - timer_obj, net_game, game_info, - ); + // Debug: dump session fields around net_game (0xBC-0xC8) + let session_base = session as *const u8; + let _ = log_line(&format!( + "[GameSession] session=0x{:08X} +0xBC=0x{:08X} +0xC0=0x{:08X} +0xC4=0x{:08X}", + session as u32, + *(session_base.add(0xBC) as *const u32), + *(session_base.add(0xC0) as *const u32), + *(session_base.add(0xC4) as *const u32), + )); + let _ = log_line(&format!( + "[GameSession] display=0x{:08X}, net_game=0x{:08X}, timer=0x{:08X}, game_info(EDI)=0x{:08X}", + display as u32, net_game as u32, timer_obj as u32, game_info as u32, + )); + + // Toggle: use Rust constructor (true) or original (false) + // TODO: set back to true once sprite/gradient loading is ported. + const USE_RUST_CTOR: bool = true; + if USE_RUST_CTOR { + create_ddgame( + this, + keyboard as *mut openwa_core::input::DDKeyboard, + display, + sound, + palette, + streaming_audio as *mut openwa_core::audio::Music, + timer_obj, + net_game, + game_info, + input_ctrl as u32, + ); + } else { + DDGAME_CTOR_ECX = input_ctrl as u32; + ddgame_constructor_call( + this, + display, + sound, + keyboard, + palette, + streaming_audio, + timer_obj, + net_game, + game_info, + ); + } + + let _ = log_line(&format!( + "[GameSession] create_ddgame returned, ddgame=0x{:08X}", + (*this).ddgame as u32 + )); + + // Dump DDGame state BEFORE InitGameState (constructor output only) + if std::env::var("OPENWA_VALIDATE").is_ok() { + let ddgame_pre = (*this).ddgame; + let real_dwords = ddgame_pre as *const u32; + let dword_count = 0x98B8 / 4; + let mut nonzero = 0u32; + let _ = log_line("[Shadow] === DDGame after constructor (before InitGameState) ==="); + for i in 0..dword_count { + let val = *real_dwords.add(i); + if val != 0 { + nonzero += 1; + if nonzero <= 300 { + let _ = log_line(&format!("[Shadow:Pre] +0x{:04X} = 0x{:08X}", i * 4, val,)); + } + } + } + let _ = log_line(&format!( + "[Shadow:Pre] Total non-zero: {} / {}", + nonzero, dword_count, + )); + } + + // Dump DDGameWrapper state + if std::env::var("OPENWA_VALIDATE").is_ok() { + let wrapper_dwords = this as *const u32; + // DDGameWrapper is ~0x6EF0 bytes (from DDGameWrapper__Constructor at 0x56DEF0) + let w_count = 0x6EF0 / 4; + let mut w_nonzero = 0u32; + let _ = log_line("[Shadow:Wrapper] === DDGameWrapper ==="); + for i in 0..w_count { + let val = *wrapper_dwords.add(i); + if val != 0 { + w_nonzero += 1; + if w_nonzero <= 200 { + let _ = log_line(&format!( + "[Shadow:Wrapper] +0x{:04X} = 0x{:08X}", + i * 4, + val, + )); + } + } + } + let _ = log_line(&format!( + "[Shadow:Wrapper] Total non-zero: {} / {}", + w_nonzero, w_count, + )); + } // Initialize DDGame's game-state fields. + let _ = log_line("[GameSession] calling InitGameState"); let init_state: unsafe extern "stdcall" fn(*mut DDGameWrapper) = core::mem::transmute(rb(va::DDGAME_INIT_GAME_STATE) as usize); init_state(this); + let _ = log_line("[GameSession] InitGameState done"); let ddgame = (*this).ddgame; let _ = log_line(&format!( @@ -151,16 +218,270 @@ pub(crate) unsafe fn construct_ddgame_wrapper( this as u32, ddgame as u32, )); + // ── Shadow comparison: run create_ddgame() into a temp allocation ── + // Compare byte-by-byte with the original to find porting bugs. + if std::env::var("OPENWA_VALIDATE").is_ok() { + shadow_compare_ddgame( + this, + keyboard, + display, + sound, + palette, + streaming_audio, + input_ctrl, + timer_obj, + net_game, + game_info, + ddgame, + ); + } + this } +/// Shadow-construct a DDGame via create_ddgame() and compare byte-by-byte +/// with the real DDGame produced by the original constructor. +/// +/// Logs the first N differing DWORD offsets. This lets us verify each section +/// of create_ddgame() against the original without breaking the game. +#[allow(clippy::too_many_arguments)] +unsafe fn shadow_compare_ddgame( + wrapper: *mut DDGameWrapper, + keyboard: *mut u8, + display: *mut DDDisplay, + sound: *mut DSSound, + palette: *mut Palette, + streaming_audio: *mut u8, + input_ctrl: *mut u8, + timer_obj: *mut u8, + net_game: *mut u8, + game_info: *mut GameInfo, + real_ddgame: *mut DDGame, +) { + let _ = log_line("[Shadow] Running create_ddgame() for comparison..."); + + // Create a temporary wrapper clone so create_ddgame can write to it + // without corrupting the real wrapper. + let shadow_wrapper = wa_malloc(core::mem::size_of::() as u32); + core::ptr::copy_nonoverlapping( + wrapper as *const u8, + shadow_wrapper, + core::mem::size_of::(), + ); + let _sw = shadow_wrapper as *mut DDGameWrapper; + + // Instead of running create_ddgame (which has side effects from resource + // loading), compare the fields that we KNOW create_ddgame sets correctly: + // the parameter storage and simple flag fields in the first 0x30 bytes + // and scattered known offsets. + // + // We compare the real DDGame against what create_ddgame WOULD write, + // computed without side effects. + let r = real_ddgame as *const u8; + + // Check param storage (offsets 0x00-0x28) + let checks: &[(&str, usize, u32)] = &[ + ("keyboard", 0x00, keyboard as u32), + ("display", 0x04, display as u32), + ("sound", 0x08, sound as u32), + ("palette", 0x10, palette as u32), + ("music", 0x14, streaming_audio as u32), + ("param7", 0x18, timer_obj as u32), + ("caller/ECX", 0x1C, input_ctrl as u32), + ("game_info", 0x24, game_info as u32), + ("net_game", 0x28, net_game as u32), + ( + "sound_available", + 0x7EF8, + if *(game_info as *const u8).add(0xF914).cast::() == 0 { + 1 + } else { + 0 + }, + ), + ("field_7EFC", 0x7EFC, 1), + ]; + + let mut ok = 0u32; + let mut fail = 0u32; + for &(name, offset, expected) in checks { + let actual = *(r.add(offset) as *const u32); + if actual == expected { + ok += 1; + } else { + let _ = log_line(&format!( + "[Shadow] FAIL +0x{:04X} ({}): expected=0x{:08X} actual=0x{:08X}", + offset, name, expected, actual, + )); + fail += 1; + } + } + + // Dump a summary of selected DDGame regions for future comparison + // (pointer fields that would differ between original and shadow) + let _ = log_line(&format!("[Shadow] Param check: {} OK, {} FAIL", ok, fail,)); + + // Dump ALL non-zero DWORDs from the real DDGame. + // This gives us the exact map of what the constructor initializes. + let real_dwords = real_ddgame as *const u32; + let dword_count = 0x98B8 / 4; + let mut nonzero = 0u32; + for i in 0..dword_count { + let val = *real_dwords.add(i); + if val != 0 { + nonzero += 1; + // Log up to 200 non-zero fields + if nonzero <= 200 { + let _ = log_line(&format!("[Shadow] DDGame+0x{:04X} = 0x{:08X}", i * 4, val,)); + } + } + } + let _ = log_line(&format!( + "[Shadow] Total non-zero DWORDs: {} / {}", + nonzero, dword_count, + )); + + // Also dump non-zero wrapper fields + let wrapper_dwords = core::mem::size_of::() / 4; + let real_w = wrapper as *const u32; + let mut w_nonzero = 0u32; + for i in 0..wrapper_dwords { + let val = *real_w.add(i); + if val != 0 { + w_nonzero += 1; + if w_nonzero <= 50 { + let _ = log_line(&format!("[Shadow] Wrapper+0x{:04X} = 0x{:08X}", i * 4, val,)); + } + } + } + let _ = log_line(&format!( + "[Shadow] Wrapper non-zero DWORDs: {} / {}", + w_nonzero, wrapper_dwords, + )); + + wa_free(shadow_wrapper); +} + +// ── PCLandscape__Constructor param logger ────────────────────────── +static mut PC_LANDSCAPE_TRAMPOLINE: *const () = core::ptr::null(); + +/// Passthrough hook: logs all 11 params, then calls original. +unsafe extern "stdcall" fn hook_pc_landscape_ctor( + p1: u32, + p2: u32, + p3: u32, + p4: u32, + p5: u32, + p6: u32, + p7: u32, + p8: u32, + p9: u32, + p10: u32, + p11: u32, +) -> u32 { + let _ = log_line(&format!( + "[PCLandscape] p1=0x{:08X} p2=0x{:08X} p3=0x{:08X} p4=0x{:08X} p5=0x{:08X} p6=0x{:08X}", + p1, p2, p3, p4, p5, p6 + )); + let _ = log_line(&format!( + "[PCLandscape] p7=0x{:08X} p8=0x{:08X} p9=0x{:08X} p10=0x{:08X} p11=0x{:08X}", + p7, p8, p9, p10, p11 + )); + // Call original + let orig: unsafe extern "stdcall" fn( + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + u32, + ) -> u32 = core::mem::transmute(PC_LANDSCAPE_TRAMPOLINE); + orig(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) +} + +// ── HUD_LoadWeaponSprites param logger ────────────────────────────── +static mut HUD_LOAD_TRAMPOLINE: *const () = core::ptr::null(); + +unsafe extern "thiscall" fn hook_hud_load(this_ecx: u32, p1: u32, p2: u32) -> u32 { + let _ = log_line(&format!( + "[HUD] ECX=0x{:08X} p1=0x{:08X} p2=0x{:08X}", + this_ecx, p1, p2 + )); + let orig: unsafe extern "thiscall" fn(u32, u32, u32) -> u32 = + core::mem::transmute(HUD_LOAD_TRAMPOLINE); + orig(this_ecx, p1, p2) +} + +// ── SpriteRegion__Constructor param logger ────────────────────────── +static mut SPRITE_REGION_TRAMPOLINE: *const () = core::ptr::null(); + +/// Log fastcall params. Called from naked trampoline with all params on stack. +unsafe extern "cdecl" fn log_sprite_region( + ecx: u32, + edx: u32, + p1: u32, + p2: u32, + p3: u32, + p4: u32, + p5: u32, + p6: u32, +) { + let _ = log_line(&format!( + "[SpriteRgn] ECX=0x{:X} EDX=0x{:X} p1=0x{:X} p2=0x{:X} p3=0x{:X} p4=0x{:X} p5=0x{:X} p6=0x{:X}", + ecx, edx, p1, p2, p3, p4, p5, p6)); +} + +/// Naked trampoline: captures ECX/EDX, logs, then calls original fastcall. +#[unsafe(naked)] +unsafe extern "C" fn hook_sprite_region_ctor() { + core::arch::naked_asm!( + // Save all regs + "pushl %eax", + "pushl %ecx", + "pushl %edx", + // Push all 8 params for logger: ECX, EDX, then 6 stack params + // Stack after 3 pushes: [edx][ecx][eax][ret][p1][p2][p3][p4][p5][p6] + // p1 at ESP+16, p2 at ESP+20, ... + "pushl 36(%esp)", // p6 + "pushl 36(%esp)", // p5 + "pushl 36(%esp)", // p4 + "pushl 36(%esp)", // p3 + "pushl 36(%esp)", // p2 + "pushl 36(%esp)", // p1 + "pushl %edx", // EDX + "pushl %ecx", // ECX + "calll {logger}", + "addl $32, %esp", // clean 8 cdecl params + // Restore regs + "popl %edx", + "popl %ecx", + "popl %eax", + // Jump to original (fastcall, same params on stack) + "jmpl *({tramp})", + logger = sym log_sprite_region, + tramp = sym SPRITE_REGION_TRAMPOLINE, + options(att_syntax), + ); +} + pub fn install() -> Result<(), String> { unsafe { INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY); DDGAME_CTOR_ADDR = rb(va::CONSTRUCT_DD_GAME); - // Fully converted — only called from impl_init_hardware (Rust). - // Trap panics if WA.exe unexpectedly calls the original. + // Initialize runtime addresses for create_ddgame bridges (future use). + init_constructor_addrs(); + // DDGameWrapper__Constructor is fully converted — trap the original. hook::install_trap!("DDGameWrapper__Constructor", va::CONSTRUCT_DD_GAME_WRAPPER); + + // PCLandscape hook disabled — params verified, may interfere with Rust ctor + + // Param hooks disabled — they served their purpose and may interfere + // with the Rust constructor's own calls to these functions. } Ok(()) } diff --git a/crates/openwa-wormkit/src/replacements/hardware_init.rs b/crates/openwa-wormkit/src/replacements/hardware_init.rs index 15bd552c282e9ab2044d3a67ea5bfb6c9eb82164..871766122b82163242d5a108edf0d99bf3e8968d 100644 --- a/crates/openwa-wormkit/src/replacements/hardware_init.rs +++ b/crates/openwa-wormkit/src/replacements/hardware_init.rs @@ -41,16 +41,16 @@ //! DDNetGameWrapper (0x2C, stdcall ctor) → session+0xC0 //! ``` +use super::game_session; +use crate::hook; +use crate::log_line; use openwa_core::address::va; -use openwa_core::rebase::rb; -use openwa_core::engine::{GameInfo, GameSession, DDGameWrapper, GameTimer, DDNetGameWrapper}; -use openwa_core::input::{DDKeyboard, InputCtrl, InputCtrlVtable}; -use openwa_core::display::{DDDisplay, DisplayBase, DisplayGfx, Palette}; use openwa_core::audio::{DSSound, StreamingAudio}; +use openwa_core::display::{DDDisplay, DisplayBase, DisplayGfx, Palette}; +use openwa_core::engine::{DDGameWrapper, DDNetGameWrapper, GameInfo, GameSession, GameTimer}; +use openwa_core::input::{DDKeyboard, InputCtrl, InputCtrlVtable}; +use openwa_core::rebase::rb; use openwa_core::wa_alloc::WABox; -use crate::hook; -use crate::log_line; -use super::game_session; // ─── Entry trampoline state ─────────────────────────────────────────────────── @@ -73,7 +73,6 @@ static mut INPUT_CTRL_ESI: u32 = 0; /// Saved ESI across the `call_input_ctrl_init` call. static mut INPUT_CTRL_SAVED_ESI: u32 = 0; - // ─── Bridges ───────────────────────────────────────────────────────────────── // // All bridges use the "pop ECX (save bridge_ret) / call callee / push ECX" idiom @@ -83,7 +82,10 @@ static mut INPUT_CTRL_SAVED_ESI: u32 = 0; /// Timer constructor: `usercall(ESI=timer_ptr, EAX=crosshair_threshold)`, plain RET. /// Returns whatever EAX holds after the call. #[unsafe(naked)] -unsafe extern "cdecl" fn call_timer_ctor(_timer_ptr: *mut GameTimer, _crosshair_threshold: u32) -> u32 { +unsafe extern "cdecl" fn call_timer_ctor( + _timer_ptr: *mut GameTimer, + _crosshair_threshold: u32, +) -> u32 { core::arch::naked_asm!( // [esp+0]=bridge_ret, [esp+4]=timer_ptr, [esp+8]=crosshair_threshold "pushl %esi", @@ -191,11 +193,11 @@ unsafe extern "stdcall" fn call_ddisplay_init( /// The returned pointer is always valid (sound may be partially initialized /// if COM steps fail, matching original WA behavior). unsafe fn create_dssound(hwnd: u32) -> *mut DSSound { + use windows::Win32::Foundation::HWND; use windows::Win32::Media::Audio::DirectSound::{ - DirectSoundCreate, IDirectSound, IDirectSoundBuffer, - DSBUFFERDESC, DSBCAPS_PRIMARYBUFFER, DSBPLAY_LOOPING, DSSCL_PRIORITY, + DirectSoundCreate, IDirectSound, IDirectSoundBuffer, DSBCAPS_PRIMARYBUFFER, + DSBPLAY_LOOPING, DSBUFFERDESC, DSSCL_PRIORITY, }; - use windows::Win32::Foundation::HWND; // Pure Rust construction — replaces call_dssound_ctor bridge. let snd = WABox::::from_value(DSSound::new(hwnd)).leak(); @@ -219,7 +221,8 @@ unsafe fn create_dssound(hwnd: u32) -> *mut DSSound { let primary = primary.unwrap(); // GetCaps to populate primary_buffer_caps - let mut caps = core::mem::zeroed::(); + let mut caps = + core::mem::zeroed::(); caps.dwSize = core::mem::size_of_val(&caps) as u32; let _ = primary.GetCaps(&mut caps); (*snd).primary_buffer_caps = caps.dwBufferBytes; @@ -252,7 +255,7 @@ unsafe extern "cdecl" fn impl_init_hardware( let _ = log_line("[hardware_init] GameEngine::InitHardware"); let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession); let gi = &mut *game_info; - let crosshair_threshold = gi.crosshair_overflow_threshold as u32; + let crosshair_threshold = gi.game_version as u32; // ── Input controller (if param4 != 0) ──────────────────────────────────── if param4 == 0 { @@ -338,10 +341,15 @@ unsafe extern "cdecl" fn impl_init_hardware( if suppress == 0 { SetCursorPos(cx, cy); if fullscreen { - use windows_sys::Win32::UI::WindowsAndMessaging::{ClipCursor, GetClientRect}; use windows_sys::Win32::Foundation::{HWND, RECT}; + use windows_sys::Win32::UI::WindowsAndMessaging::{ClipCursor, GetClientRect}; let hwnd_val: HWND = *(rb(va::G_FRONTEND_HWND) as *const HWND); - let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 }; + let mut rect = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; GetClientRect(hwnd_val, &mut rect); let map_fn_ptr = *(rb(va::IAT_MAP_WINDOW_POINTS) as *const usize); let map_fn: unsafe extern "stdcall" fn(HWND, HWND, *mut RECT, u32) -> i32 = @@ -355,7 +363,8 @@ unsafe extern "cdecl" fn impl_init_hardware( let kb = WABox::from_value(DDKeyboard::new( rb(va::DDKEYBOARD_VTABLE), &raw mut gi.input_state_f918 as u32, - )).leak(); + )) + .leak(); (*session).keyboard = kb; // ── Palette (inline construction) ───────────────────────────────────── @@ -379,11 +388,11 @@ unsafe extern "cdecl" fn impl_init_hardware( } } else { // ── Headless / stats mode ───────────────────────────────────────────── - (*session).display = DisplayBase::new_headless() as *mut u8; - (*session).keyboard = core::ptr::null_mut(); - (*session).sound = core::ptr::null_mut(); - (*session).palette = core::ptr::null_mut(); - (*session).streaming_audio = core::ptr::null_mut(); + (*session).display = DisplayBase::new_headless() as *mut u8; + (*session).keyboard = core::ptr::null_mut(); + (*session).sound = core::ptr::null_mut(); + (*session).palette = core::ptr::null_mut(); + (*session).streaming_audio = core::ptr::null_mut(); } // ── Session flags ───────────────────────────────────────────────────────── @@ -459,10 +468,10 @@ unsafe extern "C" fn hook_init_hardware() { pub fn install() -> Result<(), String> { unsafe { - TIMER_CTOR_ADDR = rb(va::GAME_ENGINE_TIMER_CTOR); - INPUT_CTRL_INIT_ADDR = rb(va::INPUT_CTRL_INIT); - STREAM_CTOR_ADDR = rb(va::STREAMING_AUDIO_CTOR); - DDISPLAY_INIT_ADDR = rb(va::DDISPLAY_INIT); + TIMER_CTOR_ADDR = rb(va::GAME_ENGINE_TIMER_CTOR); + INPUT_CTRL_INIT_ADDR = rb(va::INPUT_CTRL_INIT); + STREAM_CTOR_ADDR = rb(va::STREAMING_AUDIO_CTOR); + DDISPLAY_INIT_ADDR = rb(va::DDISPLAY_INIT); // Full replacement — trampoline not needed. let _ = hook::install( diff --git a/crates/openwa-wormkit/src/replacements/headless.rs b/crates/openwa-wormkit/src/replacements/headless.rs index c9519501a9b79554f592d693ff21e9c3e049f1d9..4b5797808cb6863c20360bdc860c1c96f3192cea 100644 --- a/crates/openwa-wormkit/src/replacements/headless.rs +++ b/crates/openwa-wormkit/src/replacements/headless.rs @@ -11,41 +11,63 @@ use crate::log_line; unsafe extern "system" fn hook_messagebox_a( - _hwnd: u32, text: *const u8, caption: *const u8, flags: u32, + _hwnd: u32, + text: *const u8, + caption: *const u8, + flags: u32, ) -> i32 { - let t = if text.is_null() { "".to_string() } - else { + let t = if text.is_null() { + "".to_string() + } else { std::ffi::CStr::from_ptr(text as *const i8) - .to_str().unwrap_or("").to_string() + .to_str() + .unwrap_or("") + .to_string() }; - let c = if caption.is_null() { "".to_string() } - else { + let c = if caption.is_null() { + "".to_string() + } else { std::ffi::CStr::from_ptr(caption as *const i8) - .to_str().unwrap_or("").to_string() + .to_str() + .unwrap_or("") + .to_string() }; let _ = log_line(&format!( "[Headless] Suppressed MessageBoxA: caption={c:?} text={t:?}" )); - if flags & 0xF == 0x4 { 6 } else { 1 } // IDYES for MB_YESNO, IDOK otherwise + if flags & 0xF == 0x4 { + 6 + } else { + 1 + } // IDYES for MB_YESNO, IDOK otherwise } unsafe extern "system" fn hook_messagebox_w( - _hwnd: u32, text: *const u16, caption: *const u16, flags: u32, + _hwnd: u32, + text: *const u16, + caption: *const u16, + flags: u32, ) -> i32 { - let t = if text.is_null() { "".to_string() } - else { + let t = if text.is_null() { + "".to_string() + } else { let len = (0..).take_while(|&i| *text.add(i) != 0).count(); String::from_utf16_lossy(core::slice::from_raw_parts(text, len)) }; - let c = if caption.is_null() { "".to_string() } - else { + let c = if caption.is_null() { + "".to_string() + } else { let len = (0..).take_while(|&i| *caption.add(i) != 0).count(); String::from_utf16_lossy(core::slice::from_raw_parts(caption, len)) }; let _ = log_line(&format!( "[Headless] Suppressed MessageBoxW: caption={c:?} text={t:?}" )); - if flags & 0xF == 0x4 { 6 } else { 1 } + if flags & 0xF == 0x4 { + 6 + } else { + 1 + } } pub fn install() -> Result<(), String> { @@ -56,20 +78,24 @@ pub fn install() -> Result<(), String> { let _ = log_line("[Headless] Suppressing all message boxes"); unsafe { - let module = windows_sys::Win32::System::LibraryLoader::GetModuleHandleA( - b"user32.dll\0".as_ptr(), - ); + let module = + windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(b"user32.dll\0".as_ptr()); if module.is_null() { return Err("user32.dll not loaded".to_string()); } for (name, hook_fn) in [ - (&b"MessageBoxA\0"[..], hook_messagebox_a as *mut core::ffi::c_void), - (&b"MessageBoxW\0"[..], hook_messagebox_w as *mut core::ffi::c_void), + ( + &b"MessageBoxA\0"[..], + hook_messagebox_a as *mut core::ffi::c_void, + ), + ( + &b"MessageBoxW\0"[..], + hook_messagebox_w as *mut core::ffi::c_void, + ), ] { - let proc = windows_sys::Win32::System::LibraryLoader::GetProcAddress( - module, name.as_ptr(), - ); + let proc = + windows_sys::Win32::System::LibraryLoader::GetProcAddress(module, name.as_ptr()); if let Some(addr) = proc { let target = addr as *mut core::ffi::c_void; if let Ok(trampoline) = minhook::MinHook::create_hook(target, hook_fn) { diff --git a/crates/openwa-wormkit/src/replacements/input.rs b/crates/openwa-wormkit/src/replacements/input.rs index 32e7ee16332cab269570c8f6ad8a48d53250a5b2..32c935f56cccc0293dd05683b5cbd91159d58969 100644 --- a/crates/openwa-wormkit/src/replacements/input.rs +++ b/crates/openwa-wormkit/src/replacements/input.rs @@ -16,9 +16,9 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use crate::hook; use crate::log_line; -use openwa_core::rebase::rb; use openwa_core::address::va; use openwa_core::engine::{DDGame, DDGameWrapper}; +use openwa_core::rebase::rb; extern "system" { fn IsBadReadPtr(lp: *const u8, ucb: u32) -> i32; @@ -71,7 +71,12 @@ pub unsafe fn dump_region(base_ptr: *const u8, offset: usize, size: usize, struc let wa_base = rb(va::IMAGE_BASE); let delta = wa_base.wrapping_sub(va::IMAGE_BASE); - let _ = log_line(&format!("\n=== {}+0x{:04X}..0x{:04X} ===", struct_name, offset, offset + size)); + let _ = log_line(&format!( + "\n=== {}+0x{:04X}..0x{:04X} ===", + struct_name, + offset, + offset + size + )); let dword_count = size / 4; for i in 0..dword_count { @@ -89,7 +94,10 @@ pub unsafe fn dump_region(base_ptr: *const u8, offset: usize, size: usize, struc let vt0 = *(val as *const u32); let _ = log_line(&format!( " +0x{:04X}: 0x{:08X} [VTABLE] g:0x{:08X} vt[0]=g:0x{:08X}", - field_offset, val, ghidra_val, vt0.wrapping_sub(delta) + field_offset, + val, + ghidra_val, + vt0.wrapping_sub(delta) )); } else { let _ = log_line(&format!( @@ -135,10 +143,7 @@ pub unsafe fn dump_region(base_ptr: *const u8, offset: usize, size: usize, struc )); } } else { - let _ = log_line(&format!( - " +0x{:04X}: 0x{:08X} [value]", - field_offset, val - )); + let _ = log_line(&format!(" +0x{:04X}: 0x{:08X} [value]", field_offset, val)); } } } diff --git a/crates/openwa-wormkit/src/replacements/mod.rs b/crates/openwa-wormkit/src/replacements/mod.rs index de6a17ab178836c1d25cfd19007ed01dd1c1bd50..de621ff244aa39ee82c9c32cb0ee9f21ebc35925 100644 --- a/crates/openwa-wormkit/src/replacements/mod.rs +++ b/crates/openwa-wormkit/src/replacements/mod.rs @@ -1,12 +1,13 @@ mod config; +mod ddgame_init; mod display; mod frontend; mod game_session; mod hardware_init; mod headless; pub(crate) mod input; -mod scheme; mod render; +mod scheme; mod sound; mod speech; mod team; @@ -26,5 +27,6 @@ pub fn install_all() -> Result<(), String> { sound::install()?; speech::install()?; input::install()?; + ddgame_init::install()?; Ok(()) } diff --git a/crates/openwa-wormkit/src/replacements/render.rs b/crates/openwa-wormkit/src/replacements/render.rs index a666721ab8315b84862537a3eb626ee20d6d7937..1a6ea2c7f48f1488f630d935df08401bf40aa375 100644 --- a/crates/openwa-wormkit/src/replacements/render.rs +++ b/crates/openwa-wormkit/src/replacements/render.rs @@ -3,8 +3,8 @@ //! All functions enqueue commands to the RenderQueue's downward-growing buffer. //! Calling conventions are __usercall variants with register + stack params. -use openwa_core::rebase::rb; use openwa_core::address::va; +use openwa_core::rebase::rb; use openwa_core::render::queue::*; use openwa_core::task::{BungeeTrailTask, WeaponAimTask}; @@ -17,12 +17,7 @@ use crate::hook::{self, usercall_trampoline}; usercall_trampoline!(fn trampoline_draw_pixel; impl_fn = draw_pixel_impl; reg = eax; stack_params = 3; ret_bytes = "0xC"); -unsafe extern "cdecl" fn draw_pixel_impl( - this: u32, - x_pos: u32, - y_pos: u32, - flags: u32, -) { +unsafe extern "cdecl" fn draw_pixel_impl(this: u32, x_pos: u32, y_pos: u32, flags: u32) { let q = &mut *(this as *mut RenderQueue); if let Some(entry) = q.alloc::() { @@ -368,11 +363,7 @@ unsafe fn trig_lookup(table: *const i32, angle: u32) -> i32 { // Gated by task+0xBC flag set by InitWormTrail (0x5008D0). // --------------------------------------------------------------------------- -unsafe extern "stdcall" fn draw_bungee_trail_impl( - task_ptr: u32, - style: u32, - fill: u32, -) { +unsafe extern "stdcall" fn draw_bungee_trail_impl(task_ptr: u32, style: u32, fill: u32) { let task = &*(task_ptr as *const BungeeTrailTask); if task.trail_visible == 0 { @@ -518,8 +509,7 @@ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) { let cos_interp = trig_lookup(cos_table, angle); // Scale = fixed_mul(DDGame.crosshair_scale, 0x140000) + task.aim_range_offset - let scale = fixed_mul(ddgame.crosshair_scale, 0x14_0000) - + task.aim_range_offset; + let scale = fixed_mul(ddgame.crosshair_scale, 0x14_0000) + task.aim_range_offset; // Endpoint = start + direction * scale let mut endpoint_x = fixed_mul(sin_interp, scale).wrapping_add(start_x); @@ -529,7 +519,7 @@ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) { let mut overflowed = false; let mut clamp_factor = 0i32; - let threshold = (*ddgame.game_info).crosshair_overflow_threshold; + let threshold = (*ddgame.game_info).game_version; if threshold > 0x11E { // Check X overflow: sin > 0 but endpoint wrapped below start @@ -554,10 +544,7 @@ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) { // Enqueue polygon line (2 vertices) let poly_param_1 = ddgame.crosshair_line_style; let poly_param_2 = ddgame.crosshair_line_color; - let verts: [[i32; 3]; 2] = [ - [start_x, start_y, 0], - [endpoint_x, endpoint_y, 0], - ]; + let verts: [[i32; 3]; 2] = [[start_x, start_y, 0], [endpoint_x, endpoint_y, 0]]; let total_size = 2 * 0xC + 0x20; if let Some(ptr) = rq.alloc_raw(total_size) { let header = &mut *(ptr as *mut DrawPolygonHeader); @@ -596,78 +583,77 @@ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) { pub fn install() -> Result<(), String> { unsafe { - let _ = hook::install( - "DrawPixel", - va::RQ_DRAW_PIXEL, - trampoline_draw_pixel as *const (), - )?; - - let _ = hook::install( - "DrawLineStrip", - va::RQ_DRAW_LINE_STRIP, - trampoline_draw_line_strip as *const (), - )?; - - let _ = hook::install( - "DrawPolygon", - va::RQ_DRAW_POLYGON, - trampoline_draw_polygon as *const (), - )?; - - let _ = hook::install( - "DrawScaled", - va::RQ_DRAW_SCALED, - trampoline_draw_scaled as *const (), - )?; - - let _ = hook::install( - "DrawRect", - va::RQ_DRAW_RECT, - trampoline_draw_rect as *const (), - )?; - - let _ = hook::install( - "DrawSpriteGlobal", - va::RQ_DRAW_SPRITE_GLOBAL, - trampoline_draw_sprite_global as *const (), - )?; - - let _ = hook::install( - "DrawSpriteLocal", - va::RQ_DRAW_SPRITE_LOCAL, - trampoline_draw_sprite_local as *const (), - )?; - - let _ = hook::install( - "DrawSpriteOffset", - va::RQ_DRAW_SPRITE_OFFSET, - trampoline_draw_sprite_offset as *const (), - )?; - - let _ = hook::install( - "DrawBitmapGlobal", - va::RQ_DRAW_BITMAP_GLOBAL, - trampoline_draw_bitmap_global as *const (), - )?; - - let _ = hook::install( - "DrawTextboxLocal", - va::RQ_DRAW_TEXTBOX_LOCAL, - trampoline_draw_textbox_local as *const (), - )?; - - let _ = hook::install( - "DrawBungeeTrail", - va::DRAW_BUNGEE_TRAIL, - draw_bungee_trail_impl as *const (), - )?; - - let _ = hook::install( - "DrawCrosshairLine", - va::DRAW_CROSSHAIR_LINE, - trampoline_draw_crosshair_line as *const (), - )?; - + let _ = hook::install( + "DrawPixel", + va::RQ_DRAW_PIXEL, + trampoline_draw_pixel as *const (), + )?; + + let _ = hook::install( + "DrawLineStrip", + va::RQ_DRAW_LINE_STRIP, + trampoline_draw_line_strip as *const (), + )?; + + let _ = hook::install( + "DrawPolygon", + va::RQ_DRAW_POLYGON, + trampoline_draw_polygon as *const (), + )?; + + let _ = hook::install( + "DrawScaled", + va::RQ_DRAW_SCALED, + trampoline_draw_scaled as *const (), + )?; + + let _ = hook::install( + "DrawRect", + va::RQ_DRAW_RECT, + trampoline_draw_rect as *const (), + )?; + + let _ = hook::install( + "DrawSpriteGlobal", + va::RQ_DRAW_SPRITE_GLOBAL, + trampoline_draw_sprite_global as *const (), + )?; + + let _ = hook::install( + "DrawSpriteLocal", + va::RQ_DRAW_SPRITE_LOCAL, + trampoline_draw_sprite_local as *const (), + )?; + + let _ = hook::install( + "DrawSpriteOffset", + va::RQ_DRAW_SPRITE_OFFSET, + trampoline_draw_sprite_offset as *const (), + )?; + + let _ = hook::install( + "DrawBitmapGlobal", + va::RQ_DRAW_BITMAP_GLOBAL, + trampoline_draw_bitmap_global as *const (), + )?; + + let _ = hook::install( + "DrawTextboxLocal", + va::RQ_DRAW_TEXTBOX_LOCAL, + trampoline_draw_textbox_local as *const (), + )?; + + let _ = hook::install( + "DrawBungeeTrail", + va::DRAW_BUNGEE_TRAIL, + draw_bungee_trail_impl as *const (), + )?; + + let _ = hook::install( + "DrawCrosshairLine", + va::DRAW_CROSSHAIR_LINE, + trampoline_draw_crosshair_line as *const (), + )?; } Ok(()) } diff --git a/crates/openwa-wormkit/src/replacements/scheme.rs b/crates/openwa-wormkit/src/replacements/scheme.rs index ce74939be69cdc06665add3b76e4ef9ecea6324d..405770717db2cade0359076b40a76baaa8351637 100644 --- a/crates/openwa-wormkit/src/replacements/scheme.rs +++ b/crates/openwa-wormkit/src/replacements/scheme.rs @@ -15,12 +15,12 @@ use std::path::Path; use std::sync::atomic::{AtomicU32, Ordering}; use crate::log_line; -use openwa_core::rebase::rb; use openwa_core::address::va; use openwa_core::game::scheme::{ ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, EXTENDED_OPTIONS_SIZE, SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, SCHEME_PAYLOAD_V3, }; +use openwa_core::rebase::rb; // ============================================================ // Scheme__ReadFile replacement (0x4D3890) @@ -63,11 +63,7 @@ unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) { match scheme.version { SchemeVersion::V1 => { - core::ptr::copy_nonoverlapping( - scheme.payload.as_ptr(), - payload_ptr, - SCHEME_PAYLOAD_V1, - ); + core::ptr::copy_nonoverlapping(scheme.payload.as_ptr(), payload_ptr, SCHEME_PAYLOAD_V1); core::ptr::write_bytes( payload_ptr.add(PAYLOAD_SUPER_WEAPONS), 0, @@ -80,11 +76,7 @@ unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) { ); } SchemeVersion::V2 => { - core::ptr::copy_nonoverlapping( - scheme.payload.as_ptr(), - payload_ptr, - SCHEME_PAYLOAD_V2, - ); + core::ptr::copy_nonoverlapping(scheme.payload.as_ptr(), payload_ptr, SCHEME_PAYLOAD_V2); core::ptr::copy_nonoverlapping( EXTENDED_OPTIONS_DEFAULTS.as_ptr(), payload_ptr.add(PAYLOAD_EXTENDED), @@ -92,11 +84,7 @@ unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) { ); } SchemeVersion::V3 => { - core::ptr::copy_nonoverlapping( - scheme.payload.as_ptr(), - payload_ptr, - SCHEME_PAYLOAD_V3, - ); + core::ptr::copy_nonoverlapping(scheme.payload.as_ptr(), payload_ptr, SCHEME_PAYLOAD_V3); let ext_bytes = &scheme.payload[PAYLOAD_EXTENDED..]; if !ExtendedOptions::validate_bytes(ext_bytes) { core::ptr::copy_nonoverlapping( @@ -284,12 +272,7 @@ static ORIG_SCHEME_SAVE_FILE: AtomicU32 = AtomicU32::new(0); /// /// Detects scheme version from in-memory data, writes SCHM header + payload. /// For V3, writes variable-length payload (only bytes differing from defaults). -unsafe extern "fastcall" fn hook_save_file( - this: u32, - _edx: u32, - name: u32, - flag: u32, -) -> u32 { +unsafe extern "fastcall" fn hook_save_file(this: u32, _edx: u32, name: u32, flag: u32) -> u32 { let c_name = match CStr::from_ptr(name as *const i8).to_str() { Ok(s) => s, Err(_) => { @@ -366,11 +349,7 @@ unsafe extern "fastcall" fn hook_init_from_data( // Step 3: Copy 110 bytes of V3 defaults from ROM to dest+0x138 let defaults = rb(va::SCHEME_V3_DEFAULTS) as *const u8; - core::ptr::copy_nonoverlapping( - defaults, - (dest + 0x138) as *mut u8, - EXTENDED_OPTIONS_SIZE, - ); + core::ptr::copy_nonoverlapping(defaults, (dest + 0x138) as *mut u8, EXTENDED_OPTIONS_SIZE); // Step 4: Set flag byte and index *((dest + 0x04) as *mut u8) = 0; @@ -501,20 +480,20 @@ unsafe fn scan_directory_recursive(dir: &str) { /// AfxFindStringResourceHandle, which is not accessible via plain LoadStringA. /// The names are fixed across all WA 3.8.1 installations. const BUILTIN_SCHEME_NAMES: [&str; 14] = [ - "", // slot 0 unused - "Beginner", // slot 1, resource 0x3CA - "Intermediate", // slot 2, resource 0x3CB - "Pro", // slot 3, resource 0x3CC - "Tournament", // slot 4, resource 0x3D1 - "Classic", // slot 5, resource 0x3CD - "Retro", // slot 6, resource 0x3D2 - "Artillery", // slot 7, resource 0x3D5 - "Sudden Sinking", // slot 8, resource 0x3D0 - "Strategic", // slot 9, resource 0x3D3 - "The Darkside", // slot 10, resource 0x3D4 - "Armageddon", // slot 11, resource 0x3CE - "Blast Zone", // slot 12, resource 0x3CF - "Full Wormage", // slot 13, resource 0x3D6 + "", // slot 0 unused + "Beginner", // slot 1, resource 0x3CA + "Intermediate", // slot 2, resource 0x3CB + "Pro", // slot 3, resource 0x3CC + "Tournament", // slot 4, resource 0x3D1 + "Classic", // slot 5, resource 0x3CD + "Retro", // slot 6, resource 0x3D2 + "Artillery", // slot 7, resource 0x3D5 + "Sudden Sinking", // slot 8, resource 0x3D0 + "Strategic", // slot 9, resource 0x3D3 + "The Darkside", // slot 10, resource 0x3D4 + "Armageddon", // slot 11, resource 0x3CE + "Blast Zone", // slot 12, resource 0x3CF + "Full Wormage", // slot 13, resource 0x3D6 ]; /// Rust replacement for Scheme__ExtractBuiltins (0x4D5720). @@ -581,9 +560,7 @@ static ORIG_LOAD_NUMBERED: AtomicU32 = AtomicU32::new(0); /// This function has zero xrefs in WA.exe (believed dead code). /// We hook it to detect if it's actually called at runtime. unsafe extern "stdcall" fn hook_load_numbered(name: u32) -> u32 { - let c_name = CStr::from_ptr(name as *const i8) - .to_str() - .unwrap_or("???"); + let c_name = CStr::from_ptr(name as *const i8).to_str().unwrap_or("???"); let _ = log_line(&format!( "[Scheme] WARNING: LoadNumbered called (believed dead code): {c_name}" )); diff --git a/crates/openwa-wormkit/src/replacements/sound.rs b/crates/openwa-wormkit/src/replacements/sound.rs index e2ace922d36f2426a625e24aa18b7858c0668284..c7e871d120e8d2b716b8015881cdae79fa9ddf66 100644 --- a/crates/openwa-wormkit/src/replacements/sound.rs +++ b/crates/openwa-wormkit/src/replacements/sound.rs @@ -10,16 +10,15 @@ use std::sync::atomic::Ordering; use openwa_core::address::va; +use openwa_core::audio::{play_sound, play_sound_pooled, SoundId}; use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry}; -use openwa_core::audio::{SoundId, play_sound, play_sound_pooled}; use openwa_core::task::{CGameTask, CTask}; use crate::hook; use crate::log_line; /// Whether sound logging is enabled (checked once at init). -static SOUND_LOG_ENABLED: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); +static SOUND_LOG_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); // ============================================================ // Core: sound queue insertion @@ -172,7 +171,14 @@ unsafe extern "fastcall" fn hook_dispatch_global_sound( return 0; } - play_sound(dssound, sound_id, flags as i32, volume as i32, pitch as i32, 0) as u32 + play_sound( + dssound, + sound_id, + flags as i32, + volume as i32, + pitch as i32, + 0, + ) as u32 } /// PlaySoundPooled_Direct (0x546B50) — fastcall(ECX=unused, EDX=task) + 3 stack, RET 0xC. @@ -256,16 +262,14 @@ pub fn install() -> Result<(), String> { /// Patch DSSound vtable (0x66AF20) to replace trivial methods with Rust. unsafe fn patch_dssound_vtable() -> Result<(), String> { - use openwa_core::rebase::rb; - use openwa_core::vtable::patch_vtable; use openwa_core::audio::{ - update_channels, release_finished, - is_slot_loaded, is_channel_finished, stop_channel, - dssound_destructor, play_sound, play_sound_pooled, - set_volume_params, set_master_volume, set_channel_volume, set_pan, - dssound_sub_destructor, - load_wav, dssound_noop, dssound_returns_0, dssound_returns_1, + dssound_destructor, dssound_noop, dssound_returns_0, dssound_returns_1, + dssound_sub_destructor, is_channel_finished, is_slot_loaded, load_wav, play_sound, + play_sound_pooled, release_finished, set_channel_volume, set_master_volume, set_pan, + set_volume_params, stop_channel, update_channels, }; + use openwa_core::rebase::rb; + use openwa_core::vtable::patch_vtable; let vtable = rb(va::DS_SOUND_VTABLE) as *mut u32; @@ -325,5 +329,6 @@ unsafe fn patch_dssound_vtable() -> Result<(), String> { *vt.add(23) = dssound_returns_1 as *const () as u32; let _ = log_line("[Sound] DSSound vtable: patched 24/24 slots with Rust"); - }).map_err(|e| e.to_string()) + }) + .map_err(|e| e.to_string()) } diff --git a/crates/openwa-wormkit/src/replacements/speech.rs b/crates/openwa-wormkit/src/replacements/speech.rs index 9fbcf6b70fdda2bc4c93dbe94fe42a743f412b67..fd98b1b1023391749163cbc0ea54831ba48942d1 100644 --- a/crates/openwa-wormkit/src/replacements/speech.rs +++ b/crates/openwa-wormkit/src/replacements/speech.rs @@ -12,10 +12,10 @@ use std::sync::atomic::{AtomicU32, Ordering}; use heapless::CString; -use openwa_core::rebase::rb; -use openwa_core::engine::DDGameWrapper; use openwa_core::address::va::{self, game_info_offsets}; use openwa_core::audio::SpeechLineTableEntry; +use openwa_core::engine::DDGameWrapper; +use openwa_core::rebase::rb; use crate::hook::{self, usercall_trampoline}; use crate::log_line; @@ -69,11 +69,11 @@ unsafe extern "cdecl" fn wav_player_stop_raw(_player: u32, _func: u32) { core::arch::naked_asm!( "push esi", "push edi", - "sub esp, 4", // result on stack - "mov esi, [esp + 16]", // player (arg1, past saved esi+edi+result) - "lea edi, [esp]", // &result - "call [esp + 20]", // func (arg2) - "add esp, 4", // drop result + "sub esp, 4", // result on stack + "mov esi, [esp + 16]", // player (arg1, past saved esi+edi+result) + "lea edi, [esp]", // &result + "call [esp + 20]", // func (arg2) + "add esp, 4", // drop result "pop edi", "pop esi", "ret", @@ -88,8 +88,8 @@ unsafe extern "cdecl" fn wav_player_stop_raw(_player: u32, _func: u32) { unsafe extern "cdecl" fn wav_player_load_raw(_player: u32, _path: *const u8, _func: u32) { core::arch::naked_asm!( "push esi", - "sub esp, 4", // result on stack - "lea esi, [esp]", // ESI = &result + "sub esp, 4", // result on stack + "lea esi, [esp]", // ESI = &result // Push in reverse order: 0, path, player "push 0", // third param=0 (ESP: +16=player, +20=path, +24=func) "push dword ptr [esp+20]", // second param=path (ESP: +20=player, +24=path, +28=func) @@ -109,8 +109,8 @@ unsafe extern "cdecl" fn wav_player_load_raw(_player: u32, _path: *const u8, _fu unsafe extern "cdecl" fn wav_player_play_raw(_player: u32, _volume: u32, _func: u32) { core::arch::naked_asm!( "push edi", - "sub esp, 4", // result on stack - "lea edi, [esp]", // EDI = &result + "sub esp, 4", // result on stack + "lea edi, [esp]", // EDI = &result // Push in reverse order: volume, player "push dword ptr [esp+16]", // second param=volume (ESP: +16=player, +24=func) "push dword ptr [esp+16]", // first param=player (ESP: +28=func) @@ -121,7 +121,6 @@ unsafe extern "cdecl" fn wav_player_play_raw(_player: u32, _volume: u32, _func: ); } - // ============================================================ // WA function wrappers // ============================================================ @@ -257,8 +256,8 @@ unsafe fn call_load_speech_wav( full_path: *const c_char, ) -> u32 { use openwa_core::audio::SpeechSlotTable; - use openwa_core::engine::DDGameWrapper; use openwa_core::engine::ddgame_wrapper::SPEECH_NAME_ENTRY_SIZE; + use openwa_core::engine::DDGameWrapper; let wrapper = &mut *(ddgw as *mut DDGameWrapper); let count = wrapper.speech_name_count as usize; @@ -281,7 +280,11 @@ unsafe fn call_load_speech_wav( if let Some(idx) = found_idx { // Found existing — reuse the buffer slot. - slot_table.set(team_index as usize, line_id, idx as u32 + SpeechSlotTable::BUFFER_OFFSET); + slot_table.set( + team_index as usize, + line_id, + idx as u32 + SpeechSlotTable::BUFFER_OFFSET, + ); return 1; } @@ -388,8 +391,7 @@ unsafe extern "cdecl" fn load_speech_bank_impl( let next_entry = &*table_base.add(i + 1); if next_entry.name_ptr.is_null() || next_entry.id != entry.id { let game_info = (*(*ddgw).ddgame).game_info as *const u8; - let default_dir = - game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize); + let default_dir = game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize); let default_base = game_info.add(game_info_offsets::DEFAULT_SPEECH_BASE_PATH as usize); @@ -455,10 +457,8 @@ unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: *const DDGameWrapper) let gi = ddgame.game_info as *const u8; for i in 0..team_count { let team_offset = (i * game_info_offsets::SPEECH_TEAM_STRIDE) as usize; - let base_path = - gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset); - let dir = - gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset); + let base_path = gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset); + let dir = gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset); // Call our Rust impl directly (bypasses the hook trampoline) load_speech_bank_impl(ddgw, i, base_path, dir); @@ -471,11 +471,7 @@ unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: *const DDGameWrapper) pub fn install() -> Result<(), String> { unsafe { - hook::install( - "PlayFeSfx", - va::PLAY_FE_SFX, - hook_play_fe_sfx as *const (), - )?; + hook::install("PlayFeSfx", va::PLAY_FE_SFX, hook_play_fe_sfx as *const ())?; hook::install( "PlayFanfare_Default", diff --git a/crates/openwa-wormkit/src/replacements/team.rs b/crates/openwa-wormkit/src/replacements/team.rs index 429226b929ebc2b33918d42c8abe0acf454269d9..929279f75c791d6b1143b2799e78a4cb0b8808cf 100644 --- a/crates/openwa-wormkit/src/replacements/team.rs +++ b/crates/openwa-wormkit/src/replacements/team.rs @@ -80,9 +80,19 @@ usercall_trampoline!(fn trampoline_get_team_total_health; impl_fn = get_team_tot // RET 0x4, returns EAX = bool (1 if special state) unsafe extern "cdecl" fn is_worm_in_special_state_impl( - team_index: u32, worm_index: u32, arena: TeamArenaRef, + team_index: u32, + worm_index: u32, + arena: TeamArenaRef, ) -> u32 { - if ddgame::worm::is_special_state(arena.team_worm(team_index as usize, worm_index as usize).state) { 1 } else { 0 } + if ddgame::worm::is_special_state( + arena + .team_worm(team_index as usize, worm_index as usize) + .state, + ) { + 1 + } else { + 0 + } } usercall_trampoline!(fn trampoline_is_worm_in_special_state; impl_fn = is_worm_in_special_state_impl; @@ -99,7 +109,11 @@ usercall_trampoline!(fn trampoline_is_worm_in_special_state; impl_fn = is_worm_i // These values appear transient — actual worm positions live in CGameTask objects. unsafe extern "cdecl" fn get_worm_position_impl( - team_index: u32, worm_index: u32, arena: TeamArenaRef, out_x: *mut i32, out_y: *mut i32, + team_index: u32, + worm_index: u32, + arena: TeamArenaRef, + out_x: *mut i32, + out_y: *mut i32, ) { let worm = arena.team_worm(team_index as usize, worm_index as usize); *out_x = *(worm._unknown_90.as_ptr() as *const i32); @@ -150,7 +164,10 @@ usercall_trampoline!(fn trampoline_check_worm_state_0x64; impl_fn = check_worm_s // // Per-team version of CheckWormState0x64. 1 xref (FUN_00556ad0). -unsafe extern "cdecl" fn check_team_worm_state_0x64_impl(arena: TeamArenaRef, team_idx: u32) -> u32 { +unsafe extern "cdecl" fn check_team_worm_state_0x64_impl( + arena: TeamArenaRef, + team_idx: u32, +) -> u32 { let header = arena.team_header(team_idx as usize); if header.eliminated != 0 { diff --git a/crates/openwa-wormkit/src/replacements/weapon.rs b/crates/openwa-wormkit/src/replacements/weapon.rs index 627296420f254820d21d2adaa8dddf7798c7705a..8c752206a7ba1f4d14c39f30894c97d25fd69563 100644 --- a/crates/openwa-wormkit/src/replacements/weapon.rs +++ b/crates/openwa-wormkit/src/replacements/weapon.rs @@ -18,7 +18,12 @@ use crate::hook::{self, usercall_trampoline}; // __usercall: EAX = team_index, EDX = amount, [ESP+4] = team_info_base, [ESP+8] = weapon_id // RET 0x8 -unsafe extern "cdecl" fn add_ammo_impl(team_index: u32, amount: i32, arena: TeamArenaRef, weapon_id: u32) { +unsafe extern "cdecl" fn add_ammo_impl( + team_index: u32, + amount: i32, + arena: TeamArenaRef, + weapon_id: u32, +) { let idx = arena.ammo_index(team_index as usize, weapon_id); let state = arena.state_mut(); let ammo = state.get_ammo(idx); @@ -58,7 +63,11 @@ usercall_trampoline!(fn trampoline_subtract_ammo; impl_fn = subtract_ammo_impl; // __usercall: EAX = team_index, ESI = team_info_base, EDX = weapon_id // plain RET, returns EAX = ammo count -unsafe extern "cdecl" fn get_ammo_impl(team_index: u32, arena: TeamArenaRef, weapon_id: u32) -> u32 { +unsafe extern "cdecl" fn get_ammo_impl( + team_index: u32, + arena: TeamArenaRef, + weapon_id: u32, +) -> u32 { let idx = arena.ammo_index(team_index as usize, weapon_id); let state = arena.state(); @@ -69,7 +78,9 @@ unsafe extern "cdecl" fn get_ammo_impl(team_index: u32, arena: TeamArenaRef, wea } // In sudden death (phase >= 484), delayed weapons return 0 // unless it's Teleport (weapon 0x28) - if state.game_phase >= ddgame::GAME_PHASE_SUDDEN_DEATH && weapon_id != Weapon::Teleport as u32 { + if state.game_phase >= ddgame::GAME_PHASE_SUDDEN_DEATH + && weapon_id != Weapon::Teleport as u32 + { return 0; } } @@ -102,7 +113,11 @@ unsafe extern "cdecl" fn count_alive_worms_impl(team_index: u32, arena: TeamAren alive += 1; } } - if alive > 1 { 1 } else { 0 } + if alive > 1 { + 1 + } else { + 0 + } } usercall_trampoline!(fn trampoline_count_alive_worms; impl_fn = count_alive_worms_impl; @@ -114,17 +129,9 @@ usercall_trampoline!(fn trampoline_count_alive_worms; impl_fn = count_alive_worm pub fn install() -> Result<(), String> { unsafe { - let _ = hook::install( - "AddAmmo", - va::ADD_AMMO, - trampoline_add_ammo as *const (), - )?; + let _ = hook::install("AddAmmo", va::ADD_AMMO, trampoline_add_ammo as *const ())?; - let _ = hook::install( - "GetAmmo", - va::GET_AMMO, - trampoline_get_ammo as *const (), - )?; + let _ = hook::install("GetAmmo", va::GET_AMMO, trampoline_get_ammo as *const ())?; let _ = hook::install( "SubtractAmmo", diff --git a/crates/openwa-wormkit/src/validation/hooks/detour.rs b/crates/openwa-wormkit/src/validation/hooks/detour.rs index ab570e531e2f9b0023e43457c9c8b72d8f732a5e..0cf7937e0f3dd7cee527d457a83377ee4549afcb 100644 --- a/crates/openwa-wormkit/src/validation/hooks/detour.rs +++ b/crates/openwa-wormkit/src/validation/hooks/detour.rs @@ -5,8 +5,8 @@ use std::sync::atomic::{AtomicU32, Ordering}; use minhook::MinHook; -use openwa_core::rebase::rb; use openwa_core::address::va; +use openwa_core::rebase::rb; use super::log_validation; @@ -39,7 +39,12 @@ unsafe extern "stdcall" fn hook_ctask_constructor(this: u32, parent: u32, ddgame static ORIG_CGAMETASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0); -unsafe extern "stdcall" fn hook_cgametask_constructor(this: u32, parent: u32, param3: u32, param4: u32) -> u32 { +unsafe extern "stdcall" fn hook_cgametask_constructor( + this: u32, + parent: u32, + param3: u32, + param4: u32, +) -> u32 { let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 = core::mem::transmute(ORIG_CGAMETASK_CONSTRUCTOR.load(Ordering::Relaxed)); orig(this, parent, param3, param4) diff --git a/crates/openwa-wormkit/src/validation/hooks/mod.rs b/crates/openwa-wormkit/src/validation/hooks/mod.rs index e28c1e5888b3b071526b6509fb7667424075f1d8..0b9635770b43c769126942c7696be06c51e5dd27 100644 --- a/crates/openwa-wormkit/src/validation/hooks/mod.rs +++ b/crates/openwa-wormkit/src/validation/hooks/mod.rs @@ -1,5 +1,5 @@ -pub mod vtable; pub mod detour; +pub mod vtable; use super::log_validation; diff --git a/crates/openwa-wormkit/src/validation/hooks/vtable.rs b/crates/openwa-wormkit/src/validation/hooks/vtable.rs index d12d4b38a6692ccffe686047665d60c59cafef4d..5f02e44ea824f0fcfaa939ce7b5cd0265439de24 100644 --- a/crates/openwa-wormkit/src/validation/hooks/vtable.rs +++ b/crates/openwa-wormkit/src/validation/hooks/vtable.rs @@ -2,8 +2,8 @@ use std::sync::atomic::{AtomicU32, Ordering}; -use openwa_core::rebase::rb; use openwa_core::address::va; +use openwa_core::rebase::rb; use super::log_validation; @@ -34,7 +34,10 @@ impl VtableHook { let mut old_protect: u32 = 0; let ok = VirtualProtect(slot_addr as *mut _, 4, PAGE_READWRITE, &mut old_protect); if ok == 0 { - return Err(format!("VirtualProtect failed for {name} at 0x{:08X}", slot_addr as u32)); + return Err(format!( + "VirtualProtect failed for {name} at 0x{:08X}", + slot_addr as u32 + )); } let original_fn = *slot_addr; @@ -47,7 +50,11 @@ impl VtableHook { slot_addr as u32 )); - Ok(VtableHook { slot_addr, original_fn, name }) + Ok(VtableHook { + slot_addr, + original_fn, + name, + }) } pub fn original(&self) -> u32 { @@ -66,7 +73,12 @@ unsafe extern "fastcall" fn hook_ctask_process_frame(this: u32, _edx: u32, flags static ORIG_CTASK_HANDLE_MESSAGE: AtomicU32 = AtomicU32::new(0); unsafe extern "fastcall" fn hook_ctask_handle_message( - this: u32, _edx: u32, sender: u32, msg_type: u32, size: u32, data: u32, + this: u32, + _edx: u32, + sender: u32, + msg_type: u32, + size: u32, + data: u32, ) -> u32 { let orig: unsafe extern "fastcall" fn(u32, u32, u32, u32, u32, u32) -> u32 = core::mem::transmute(ORIG_CTASK_HANDLE_MESSAGE.load(Ordering::Relaxed)); @@ -76,14 +88,16 @@ unsafe extern "fastcall" fn hook_ctask_handle_message( pub fn install() -> Result<(), String> { unsafe { let hook = VtableHook::install( - va::CTASK_VTABLE, 7, + va::CTASK_VTABLE, + 7, hook_ctask_process_frame as *const () as u32, "CTask::ProcessFrame", )?; ORIG_CTASK_PROCESS_FRAME.store(hook.original(), Ordering::Relaxed); let hook = VtableHook::install( - va::CTASK_VTABLE, 2, + va::CTASK_VTABLE, + 2, hook_ctask_handle_message as *const () as u32, "CTask::HandleMessage", )?; diff --git a/crates/openwa-wormkit/src/validation/mod.rs b/crates/openwa-wormkit/src/validation/mod.rs index 179910855a30aa7e2573309715caaae7a95cf6a9..b2500bb5ca5f4dc4962c9d90e5364a7053034f2a 100644 --- a/crates/openwa-wormkit/src/validation/mod.rs +++ b/crates/openwa-wormkit/src/validation/mod.rs @@ -9,10 +9,13 @@ mod hooks; use std::sync::atomic::{AtomicU32, Ordering}; -use openwa_core::rebase::rb; use openwa_core::address::va; -use openwa_core::task::{CTask, CTaskBfsIter, CGameTask, CTaskMissile, CTaskTeam, CTaskTurnGame, TurnGameCtx, SharedDataTable}; use openwa_core::engine::{DDGame, DDGameWrapper}; +use openwa_core::rebase::rb; +use openwa_core::task::{ + CGameTask, CTask, CTaskBfsIter, CTaskMissile, CTaskTeam, CTaskTurnGame, SharedDataTable, + TurnGameCtx, +}; static DUMP_COUNTER: AtomicU32 = AtomicU32::new(0); @@ -66,7 +69,9 @@ impl ValidationResult { fn summary_line(&self) -> String { format!( "Results: {}/{} passed, {} failed", - self.pass, self.total(), self.fail + self.pass, + self.total(), + self.fail ) } } @@ -106,7 +111,10 @@ fn validate_addresses(result: &mut ValidationResult) { let vtables: &[(&str, u32)] = &[ ("CTask vtable", va::CTASK_VTABLE), ("CGameTask vtable", va::CGAMETASK_VTABLE), - ("CGameTask SoundEmitter vtable", va::CGAMETASK_SOUND_EMITTER_VT), + ( + "CGameTask SoundEmitter vtable", + va::CGAMETASK_SOUND_EMITTER_VT, + ), ("DDGameWrapper vtable", va::DDGAME_WRAPPER_VTABLE), ("GfxHandler vtable", va::GFX_HANDLER_VTABLE), ("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE), @@ -126,7 +134,16 @@ fn validate_addresses(result: &mut ValidationResult) { result.check( &format!("{} location", name), in_rdata, - &format!("0x{:08X} (ghidra 0x{:08X}) {}", addr, ghidra_addr, if in_rdata { "in .rdata" } else { "NOT in .rdata" }), + &format!( + "0x{:08X} (ghidra 0x{:08X}) {}", + addr, + ghidra_addr, + if in_rdata { + "in .rdata" + } else { + "NOT in .rdata" + } + ), ); } @@ -142,7 +159,8 @@ fn validate_addresses(result: &mut ValidationResult) { in_text, &format!( "[0x{:08X}] = 0x{:08X} {}", - addr, first_entry, + addr, + first_entry, if in_text { "in .text" } else { "NOT in .text" } ), ); @@ -180,8 +198,7 @@ fn validate_addresses(result: &mut ValidationResult) { let _ = log_validation(""); let _ = log_validation(" Function prologue checks:"); let valid_prologues: &[u8] = &[ - 0x55, 0x53, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, - 0xB8, 0x51, 0x52, 0x64, 0x85, 0x8D, + 0x55, 0x53, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, 0xB8, 0x51, 0x52, 0x64, 0x85, 0x8D, 0xE9, // JMP — MinHook trampoline (function has been hooked) ]; @@ -208,7 +225,10 @@ fn validate_addresses(result: &mut ValidationResult) { ("CTask::vt2_HandleMessage", va::CTASK_VT2_HANDLE_MESSAGE), ("CGameTask::vt0", va::CGAMETASK_VT0), ("CGameTask::vt1_Free", va::CGAMETASK_VT1_FREE), - ("CGameTask::vt2_HandleMessage", va::CGAMETASK_VT2_HANDLE_MESSAGE), + ( + "CGameTask::vt2_HandleMessage", + va::CGAMETASK_VT2_HANDLE_MESSAGE, + ), ]; for (name, ghidra_addr) in functions { @@ -218,7 +238,10 @@ fn validate_addresses(result: &mut ValidationResult) { result.check( &format!("{} prologue", name), false, - &format!("0x{:08X} (ghidra 0x{:08X}) not in .text range", addr, ghidra_addr), + &format!( + "0x{:08X} (ghidra 0x{:08X}) not in .text range", + addr, ghidra_addr + ), ); continue; } @@ -230,7 +253,8 @@ fn validate_addresses(result: &mut ValidationResult) { ok, &format!( "0x{:08X}: first byte 0x{:02X} {}", - addr, first_byte, + addr, + first_byte, if ok { "valid" } else { "UNEXPECTED" } ), ); @@ -298,53 +322,53 @@ fn validate_struct_offsets(result: &mut ValidationResult) { let _ = log_validation(""); let _ = log_validation(" CTaskTeam:"); - check_offset!(result, CTaskTeam, team_index, 0x38); - check_offset!(result, CTaskTeam, alive_worm_count, 0x48); - check_offset!(result, CTaskTeam, last_launched_weapon, 0x60); - check_offset!(result, CTaskTeam, worm_count, 0x218); - check_offset!(result, CTaskTeam, pos_x, 0x404); - check_offset!(result, CTaskTeam, pos_y, 0x408); + check_offset!(result, CTaskTeam, team_index, 0x38); + check_offset!(result, CTaskTeam, alive_worm_count, 0x48); + check_offset!(result, CTaskTeam, last_launched_weapon, 0x60); + check_offset!(result, CTaskTeam, worm_count, 0x218); + check_offset!(result, CTaskTeam, pos_x, 0x404); + check_offset!(result, CTaskTeam, pos_y, 0x408); let _ = log_validation(""); let _ = log_validation(" TurnGameCtx (embedded at CTaskTurnGame+0x30):"); - check_offset!(result, TurnGameCtx, land_height, 0x10); - check_offset!(result, TurnGameCtx, land_height_2, 0x14); - check_offset!(result, TurnGameCtx, _sentinel_18, 0x18); - check_offset!(result, TurnGameCtx, _sentinel_28, 0x28); - check_offset!(result, TurnGameCtx, _sentinel_38, 0x38); - check_offset!(result, TurnGameCtx, team_count, 0x4C); - check_offset!(result, TurnGameCtx, _slot_d0, 0xA0); + check_offset!(result, TurnGameCtx, land_height, 0x10); + check_offset!(result, TurnGameCtx, land_height_2, 0x14); + check_offset!(result, TurnGameCtx, _sentinel_18, 0x18); + check_offset!(result, TurnGameCtx, _sentinel_28, 0x28); + check_offset!(result, TurnGameCtx, _sentinel_38, 0x38); + check_offset!(result, TurnGameCtx, team_count, 0x4C); + check_offset!(result, TurnGameCtx, _slot_d0, 0xA0); check_offset!(result, TurnGameCtx, _hud_textbox_a, 0xA4); check_offset!(result, TurnGameCtx, _hud_textbox_b, 0xA8); let _ = log_validation(""); let _ = log_validation(" CTaskMissile:"); - check_offset!(result, CTaskMissile, base, 0x00); - check_offset!(result, CTaskMissile, slot_id, 0x12C); - check_offset!(result, CTaskMissile, spawn_params, 0x130); - check_offset!(result, CTaskMissile, weapon_data, 0x15C); - check_offset!(result, CTaskMissile, render_data, 0x2D4); + check_offset!(result, CTaskMissile, base, 0x00); + check_offset!(result, CTaskMissile, slot_id, 0x12C); + check_offset!(result, CTaskMissile, spawn_params, 0x130); + check_offset!(result, CTaskMissile, weapon_data, 0x15C); + check_offset!(result, CTaskMissile, render_data, 0x2D4); check_offset!(result, CTaskMissile, launch_speed_raw, 0x3A0); - check_offset!(result, CTaskMissile, homing_enabled, 0x3A8); - check_offset!(result, CTaskMissile, direction, 0x3C8); + check_offset!(result, CTaskMissile, homing_enabled, 0x3A8); + check_offset!(result, CTaskMissile, direction, 0x3C8); let _ = log_validation(" CTaskTurnGame:"); - check_offset!(result, CTaskTurnGame, game_ctx, 0x30); - check_offset!(result, CTaskTurnGame, worm_active, 0x108); - check_offset!(result, CTaskTurnGame, current_team, 0x12C); - check_offset!(result, CTaskTurnGame, current_worm, 0x130); - check_offset!(result, CTaskTurnGame, arena_team, 0x134); - check_offset!(result, CTaskTurnGame, arena_worm, 0x138); - check_offset!(result, CTaskTurnGame, turn_ended, 0x150); - check_offset!(result, CTaskTurnGame, no_time_limit, 0x154); - check_offset!(result, CTaskTurnGame, retreat_timer, 0x178); - check_offset!(result, CTaskTurnGame, retreat_time_max, 0x17C); - check_offset!(result, CTaskTurnGame, idle_timer, 0x184); + check_offset!(result, CTaskTurnGame, game_ctx, 0x30); + check_offset!(result, CTaskTurnGame, worm_active, 0x108); + check_offset!(result, CTaskTurnGame, current_team, 0x12C); + check_offset!(result, CTaskTurnGame, current_worm, 0x130); + check_offset!(result, CTaskTurnGame, arena_team, 0x134); + check_offset!(result, CTaskTurnGame, arena_worm, 0x138); + check_offset!(result, CTaskTurnGame, turn_ended, 0x150); + check_offset!(result, CTaskTurnGame, no_time_limit, 0x154); + check_offset!(result, CTaskTurnGame, retreat_timer, 0x178); + check_offset!(result, CTaskTurnGame, retreat_time_max, 0x17C); + check_offset!(result, CTaskTurnGame, idle_timer, 0x184); check_offset!(result, CTaskTurnGame, turn_timer_display, 0x188); - check_offset!(result, CTaskTurnGame, turn_timer, 0x18C); + check_offset!(result, CTaskTurnGame, turn_timer, 0x18C); check_offset!(result, CTaskTurnGame, active_worm_frames, 0x2D4); - check_offset!(result, CTaskTurnGame, retreat_frames, 0x2D8); - check_offset!(result, CTaskTurnGame, _timer_scale, 0x2DC); + check_offset!(result, CTaskTurnGame, retreat_frames, 0x2D8); + check_offset!(result, CTaskTurnGame, _timer_scale, 0x2DC); } // --------------------------------------------------------------------------- @@ -459,13 +483,22 @@ fn dump_team_blocks() { unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } let _ = log_validation(&format!(" DDGame = 0x{:08X}", ddgame_ptr)); @@ -473,25 +506,39 @@ fn dump_team_blocks() { let arena = TeamArenaRef::from_raw(arena_base); let tws = arena.state(); let tws_base = arena_base as *const u8; - let _ = log_validation(&format!(" team_count = {} (TeamArenaState.team_count)", tws.team_count)); + let _ = log_validation(&format!( + " team_count = {} (TeamArenaState.team_count)", + tws.team_count + )); let blocks = arena.blocks(); let blocks_addr = blocks as u32; - let _ = log_validation(&format!(" blocks_base = 0x{:08X} (DDGame+0x{:X})", - blocks_addr, blocks_addr - ddgame_ptr)); + let _ = log_validation(&format!( + " blocks_base = 0x{:08X} (DDGame+0x{:X})", + blocks_addr, + blocks_addr - ddgame_ptr + )); let mut result = ValidationResult::new(); let expected_blocks = ddgame_ptr + offsets::TEAM_BLOCKS as u32; - result.check("ARENA_TO_BLOCKS derivation", + result.check( + "ARENA_TO_BLOCKS derivation", blocks_addr == expected_blocks, - &format!("got 0x{:08X}, expected 0x{:08X}", blocks_addr, expected_blocks)); + &format!( + "got 0x{:08X}, expected 0x{:08X}", + blocks_addr, expected_blocks + ), + ); let num_blocks = (tws.team_count as u32 + 1).max(3).min(7); for b in 0..num_blocks { let block = &*blocks.add(b as usize); - let _ = log_validation(&format!("\n === Block {} (0x{:08X}) ===", - b, blocks_addr + b * 0x51C)); + let _ = log_validation(&format!( + "\n === Block {} (0x{:08X}) ===", + b, + blocks_addr + b * 0x51C + )); if (b + 1) < 7 { let header = &(*blocks.add(b as usize + 1)).header.team; @@ -508,8 +555,10 @@ fn dump_team_blocks() { &format!("struct={}, raw={}", worm_count, raw_worm_count), ); - let _ = log_validation(&format!(" header: worm_count={}, eliminated={}, alliance(entry_ptr+4)={}", - worm_count, eliminated, raw_alliance)); + let _ = log_validation(&format!( + " header: worm_count={}, eliminated={}, alliance(entry_ptr+4)={}", + worm_count, eliminated, raw_alliance + )); for w in 0..8usize { let worm = if w == 0 { @@ -519,7 +568,10 @@ fn dump_team_blocks() { }; let active = worm.active_flag; let name_bytes = &worm.name; - let name_len = name_bytes.iter().position(|&c| c == 0).unwrap_or(name_bytes.len()); + let name_len = name_bytes + .iter() + .position(|&c| c == 0) + .unwrap_or(name_bytes.len()); let name_str = core::str::from_utf8(&name_bytes[..name_len]).unwrap_or("?"); if worm.state != 0 || worm.health != 0 || active != 0 || w == 0 { @@ -577,8 +629,11 @@ fn dump_team_blocks() { // slot (including slot 6 which goes "out of bounds"), plus the area // around weapon_slots start (0x1EB4) and team_count (0x1EB0). let _ = log_validation("\n === TeamArenaState Layout Dump ==="); - let _ = log_validation(&format!(" arena_base = 0x{:08X} (DDGame + 0x{:X})", - arena_base, offsets::TEAM_ARENA_STATE)); + let _ = log_validation(&format!( + " arena_base = 0x{:08X} (DDGame + 0x{:X})", + arena_base, + offsets::TEAM_ARENA_STATE + )); // Dump first 16 bytes at each team_index * 0x51C stride (slots -2 to 7) for slot in -2i32..8 { @@ -586,7 +641,9 @@ fn dump_team_blocks() { let ptr = tws_base.offset(offset); let mut hex = String::new(); for i in 0..16usize { - if i > 0 && i % 4 == 0 { hex.push(' '); } + if i > 0 && i % 4 == 0 { + hex.push(' '); + } hex.push_str(&format!("{:02X}", *ptr.add(i))); } let abs_addr = (arena_base as isize + offset) as u32; @@ -599,7 +656,10 @@ fn dump_team_blocks() { }; let _ = log_validation(&format!( " {:+06X} (0x{:08X}) {}: {} val_at+4={}", - offset, abs_addr, label, hex, + offset, + abs_addr, + label, + hex, *(ptr.add(4) as *const i32) )); } @@ -610,7 +670,9 @@ fn dump_team_blocks() { let ptr = tws_base.add(row); let mut hex = String::new(); for i in 0..16usize { - if i > 0 && i % 4 == 0 { hex.push(' '); } + if i > 0 && i % 4 == 0 { + hex.push(' '); + } hex.push_str(&format!("{:02X}", *ptr.add(i))); } // Label known offsets @@ -619,25 +681,31 @@ fn dump_team_blocks() { 0x1EB0 => " (team_count + weapon_slots[0..3])", _ => "", }; - let _ = log_validation(&format!( - " +0x{:04X}: {}{}", row, hex, label - )); + let _ = log_validation(&format!(" +0x{:04X}: {}{}", row, hex, label)); } // Dump team_count and first few weapon_slots - let _ = log_validation(&format!("\n team_count (+0x1EB0) = {}", - *(tws_base.add(0x1EB0) as *const i32))); - let _ = log_validation(&format!(" weapon_slots[0..4] (+0x1EB4) = [{}, {}, {}, {}]", + let _ = log_validation(&format!( + "\n team_count (+0x1EB0) = {}", + *(tws_base.add(0x1EB0) as *const i32) + )); + let _ = log_validation(&format!( + " weapon_slots[0..4] (+0x1EB4) = [{}, {}, {}, {}]", *(tws_base.add(0x1EB4) as *const i32), *(tws_base.add(0x1EB8) as *const i32), *(tws_base.add(0x1EBC) as *const i32), - *(tws_base.add(0x1EC0) as *const i32))); + *(tws_base.add(0x1EC0) as *const i32) + )); // Dump game_mode_flag and game_phase for context - let _ = log_validation(&format!(" game_mode_flag (+0x2C0C) = {}", - *(tws_base.add(0x2C0C) as *const i32))); - let _ = log_validation(&format!(" game_phase (+0x2C28) = {}", - *(tws_base.add(0x2C28) as *const i32))); + let _ = log_validation(&format!( + " game_mode_flag (+0x2C0C) = {}", + *(tws_base.add(0x2C0C) as *const i32) + )); + let _ = log_validation(&format!( + " game_phase (+0x2C28) = {}", + *(tws_base.add(0x2C28) as *const i32) + )); } } @@ -654,37 +722,72 @@ fn dump_landscape() { unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let landscape_ptr = read_u32(wrapper_addr + 0x4CC); - if landscape_ptr == 0 { let _ = log_validation(" PCLandscape is NULL."); return; } + if landscape_ptr == 0 { + let _ = log_validation(" PCLandscape is NULL."); + return; + } let land = &*(landscape_ptr as *const PCLandscape); let _ = log_validation(&format!(" PCLandscape @ 0x{:08X}", landscape_ptr)); let expected_vt = rb(va::PC_LANDSCAPE_VTABLE); let vt_ok = land.vtable as u32 == expected_vt; - let _ = log_validation(&format!(" vtable: 0x{:08X} (expected 0x{:08X}) {}", - land.vtable as u32, expected_vt, if vt_ok { "OK" } else { "MISMATCH" })); + let _ = log_validation(&format!( + " vtable: 0x{:08X} (expected 0x{:08X}) {}", + land.vtable as u32, + expected_vt, + if vt_ok { "OK" } else { "MISMATCH" } + )); let _ = log_validation(&format!(" ddgame: 0x{:08X}", land.ddgame as usize)); - let _ = log_validation(&format!(" _unknown_900: 0x{:08X}", land._unknown_900 as u32)); - let _ = log_validation(&format!(" collision_bitmap: 0x{:08X}", land.collision_bitmap as u32)); + let _ = log_validation(&format!( + " _unknown_900: 0x{:08X}", + land._unknown_900 as u32 + )); + let _ = log_validation(&format!( + " collision_bitmap: 0x{:08X}", + land.collision_bitmap as u32 + )); let _ = log_validation(&format!(" initialized: {}", land.initialized)); - let primary_count = land.crater_sprites.iter().filter(|&&p| !p.is_null()).count(); - let secondary_count = land.crater_sprites_secondary.iter().filter(|&&p| !p.is_null()).count(); - let _ = log_validation(&format!(" crater_sprites: {}/16 non-null, secondary: {}/16 non-null", - primary_count, secondary_count)); + let primary_count = land + .crater_sprites + .iter() + .filter(|&&p| !p.is_null()) + .count(); + let secondary_count = land + .crater_sprites_secondary + .iter() + .filter(|&&p| !p.is_null()) + .count(); + let _ = log_validation(&format!( + " crater_sprites: {}/16 non-null, secondary: {}/16 non-null", + primary_count, secondary_count + )); let _ = log_validation(&format!(" layer_0: 0x{:08X}", land.layer_0 as u32)); let _ = log_validation(&format!(" layer_1: 0x{:08X}", land.layer_1 as u32)); - let _ = log_validation(&format!(" layer_terrain: 0x{:08X}", land.layer_terrain as u32)); + let _ = log_validation(&format!( + " layer_terrain: 0x{:08X}", + land.layer_terrain as u32 + )); let _ = log_validation(&format!(" layer_edges: 0x{:08X}", land.layer_edges as u32)); - let _ = log_validation(&format!(" layer_shadow: 0x{:08X}", land.layer_shadow as u32)); + let _ = log_validation(&format!( + " layer_shadow: 0x{:08X}", + land.layer_shadow as u32 + )); let _ = log_validation(&format!(" layer_5: 0x{:08X}", land.layer_5 as u32)); if !land.layer_terrain.is_null() { @@ -693,8 +796,10 @@ fn dump_landscape() { let stride = read_u32(dgfx + 0x10); let width = read_u32(dgfx + 0x14); let height = read_u32(dgfx + 0x18); - let _ = log_validation(&format!(" layer_terrain DisplayGfx: pixels=0x{:08X} stride={} width={} height={}", - pixel_data, stride, width, height)); + let _ = log_validation(&format!( + " layer_terrain DisplayGfx: pixels=0x{:08X} stride={} width={} height={}", + pixel_data, stride, width, height + )); } let _ = log_validation(&format!(" dirty_rect_count: {}", land.dirty_rect_count)); @@ -702,8 +807,10 @@ fn dump_landscape() { if land.dirty_rect_count > 0 && land.dirty_rect_count <= 256 { for i in 0..land.dirty_rect_count.min(5) as usize { let r = &land.dirty_rects[i]; - let _ = log_validation(&format!(" rect[{}]: ({},{})..({},{})", - i, r.x1, r.y1, r.x2, r.y2)); + let _ = log_validation(&format!( + " rect[{}]: ({},{})..({},{})", + i, r.x1, r.y1, r.x2, r.y2 + )); } if land.dirty_rect_count > 5 { let _ = log_validation(&format!(" ... and {} more", land.dirty_rect_count - 5)); @@ -713,11 +820,22 @@ fn dump_landscape() { let _ = log_validation(&format!(" _unknown_8ec: 0x{:08X}", land._unknown_8ec)); let _ = log_validation(&format!(" _unknown_8f0: 0x{:08X}", land._unknown_8f0)); let _ = log_validation(&format!(" _unknown_8f4: 0x{:08X}", land._unknown_8f4)); - let _ = log_validation(&format!(" resource_handle: 0x{:08X}", land.resource_handle as u32)); - let _ = log_validation(&format!(" level_gfx_handler: 0x{:08X}", land.level_gfx_handler as u32)); - let _ = log_validation(&format!(" water_gfx_handler: 0x{:08X}", land.water_gfx_handler as u32)); - let _ = log_validation(&format!(" visible_bounds: left={} top={} right={} bottom={}", - land.visible_left, land.visible_top, land.visible_right, land.visible_bottom)); + let _ = log_validation(&format!( + " resource_handle: 0x{:08X}", + land.resource_handle as u32 + )); + let _ = log_validation(&format!( + " level_gfx_handler: 0x{:08X}", + land.level_gfx_handler as u32 + )); + let _ = log_validation(&format!( + " water_gfx_handler: 0x{:08X}", + land.water_gfx_handler as u32 + )); + let _ = log_validation(&format!( + " visible_bounds: left={} top={} right={} bottom={}", + land.visible_left, land.visible_top, land.visible_right, land.visible_bottom + )); let level_path = std::ffi::CStr::from_ptr(land.level_dir_path.as_ptr() as *const i8); let theme_path = std::ffi::CStr::from_ptr(land.theme_dir_path.as_ptr() as *const i8); @@ -726,8 +844,10 @@ fn dump_landscape() { if !land.ddgame.is_null() { let dg = &*land.ddgame; - let _ = log_validation(&format!(" DDGame level dims: {}x{} (total={})", - dg.level_width, dg.level_height, dg.level_total_pixels)); + let _ = log_validation(&format!( + " DDGame level dims: {}x{} (total={})", + dg.level_width, dg.level_height, dg.level_total_pixels + )); } let _ = log_validation(" Vtable slots:"); @@ -735,8 +855,12 @@ fn dump_landscape() { for slot in 0..10u32 { let entry = read_u32(vt_addr + slot * 4); let in_text = is_in_text(entry); - let _ = log_validation(&format!(" [{}]: 0x{:08X} {}", - slot, entry, if in_text { "" } else { "(NOT .text)" })); + let _ = log_validation(&format!( + " [{}]: 0x{:08X} {}", + slot, + entry, + if in_text { "" } else { "(NOT .text)" } + )); } let _ = log_validation(&format!(" _unknown_b3c: 0x{:08X}", land._unknown_b3c)); @@ -754,8 +878,21 @@ fn dump_landscape() { fn log_hex_dump(data: &[u8]) { for chunk in data.chunks(16) { - let hex: String = chunk.iter().map(|b| format!("{:02X}", b)).collect::>().join(" "); - let ascii: String = chunk.iter().map(|&b| if (0x20..0x7F).contains(&b) { b as char } else { '.' }).collect(); + let hex: String = chunk + .iter() + .map(|b| format!("{:02X}", b)) + .collect::>() + .join(" "); + let ascii: String = chunk + .iter() + .map(|&b| { + if (0x20..0x7F).contains(&b) { + b as char + } else { + '.' + } + }) + .collect(); let _ = log_validation(&format!(" {} | {}", hex, ascii)); } } @@ -770,11 +907,20 @@ fn dump_entity_census() { unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } let task_land_ptr = read_u32(ddgame_ptr + 0x54C); if task_land_ptr == 0 { @@ -793,17 +939,17 @@ fn dump_entity_census() { // Name table: Ghidra VA → display name let known: &[(u32, &str)] = &[ - (va::CTASK_WORM_VTABLE, "CTaskWorm"), - (va::CTASK_LAND_VTABLE, "CTaskLand"), - (va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"), - (va::CTASK_TEAM_VTABLE, "CTaskTeam"), - (va::CTASK_FILTER_VTABLE, "CTaskFilter"), - (va::CTASK_DIRT_VTABLE, "CTaskDirt"), + (va::CTASK_WORM_VTABLE, "CTaskWorm"), + (va::CTASK_LAND_VTABLE, "CTaskLand"), + (va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"), + (va::CTASK_TEAM_VTABLE, "CTaskTeam"), + (va::CTASK_FILTER_VTABLE, "CTaskFilter"), + (va::CTASK_DIRT_VTABLE, "CTaskDirt"), (va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"), - (va::CTASK_CPU_VTABLE, "CTaskCPU"), - (va::CTASK_CRATE_VTABLE, "CTaskCrate"), - (va::CTASK_VTABLE, "CTask"), - (va::CGAMETASK_VTABLE, "CGameTask"), + (va::CTASK_CPU_VTABLE, "CTaskCPU"), + (va::CTASK_CRATE_VTABLE, "CTaskCrate"), + (va::CTASK_VTABLE, "CTask"), + (va::CGAMETASK_VTABLE, "CGameTask"), ]; // Collect (ghidra_va, entity_ptr) for every node. @@ -813,9 +959,13 @@ fn dump_entity_census() { for node in table.iter() { let entity = (*node).entity; - if entity.is_null() { continue; } + if entity.is_null() { + continue; + } let vtable_runtime = read_u32(entity as u32); - if !is_in_rdata(vtable_runtime) { continue; } + if !is_in_rdata(vtable_runtime) { + continue; + } let vtable_ghidra = vtable_runtime.wrapping_sub(delta); entries.push((vtable_ghidra, entity as u32)); total += 1; @@ -835,22 +985,34 @@ fn dump_entity_census() { groups.sort_by(|a, b| b.1.len().cmp(&a.1.len())); for (vt_ghidra, ptrs) in &groups { - let name = known.iter().find(|(v, _)| *v == *vt_ghidra) + let name = known + .iter() + .find(|(v, _)| *v == *vt_ghidra) .map(|(_, n)| *n) .unwrap_or("UNKNOWN"); let _ = log_validation(&format!( " {:>3}x {:<20} (vtable 0x{:08X})", - ptrs.len(), name, vt_ghidra + ptrs.len(), + name, + vt_ghidra )); for &ptr in ptrs.iter().take(8) { use openwa_core::task::CTaskWorm; if *vt_ghidra == va::CTASK_WORM_VTABLE { let w = &*(ptr as *const CTaskWorm); - let nlen = w.worm_name.iter().position(|&c| c == 0).unwrap_or(w.worm_name.len()); + let nlen = w + .worm_name + .iter() + .position(|&c| c == 0) + .unwrap_or(w.worm_name.len()); let wname = core::str::from_utf8(&w.worm_name[..nlen]).unwrap_or("?"); let _ = log_validation(&format!( " @ 0x{:08X} team={} idx={} state=0x{:02X} name=\"{}\"", - ptr, w.team_index, w.worm_index, w.state(), wname + ptr, + w.team_index, + w.worm_index, + w.state(), + wname )); } else { let a = read_u32(ptr + 4); @@ -879,13 +1041,22 @@ fn dump_worm_tasks() { unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } // DDGame+0x54C = CTaskLand* — always present once map is loaded. let task_land_ptr = read_u32(ddgame_ptr + 0x54C); @@ -910,16 +1081,23 @@ fn dump_worm_tasks() { let table = SharedDataTable::from_ptr(shared_data_ptr); let expected_vtable = rb(va::CTASK_WORM_VTABLE) as *const u8; - let _ = log_validation(&format!(" Expected CTaskWorm vtable: 0x{:08X}", expected_vtable as u32)); + let _ = log_validation(&format!( + " Expected CTaskWorm vtable: 0x{:08X}", + expected_vtable as u32 + )); let mut worm_count = 0u32; for node in table.iter() { let candidate = (*node).entity; - if candidate.is_null() { continue; } + if candidate.is_null() { + continue; + } // Vtable is the first pointer in the object; must be in .rdata. let vtable = read_u32(candidate as u32) as *const u8; - if !is_in_rdata(vtable as u32) || vtable != expected_vtable { continue; } + if !is_in_rdata(vtable as u32) || vtable != expected_vtable { + continue; + } worm_count += 1; let worm = &*(candidate as *const CTaskWorm); @@ -931,7 +1109,11 @@ fn dump_worm_tasks() { let worm_idx = worm.worm_index; let slot_id = worm.slot_id; - let name_len = worm.worm_name.iter().position(|&c| c == 0).unwrap_or(worm.worm_name.len()); + let name_len = worm + .worm_name + .iter() + .position(|&c| c == 0) + .unwrap_or(worm.worm_name.len()); let name = core::str::from_utf8(&worm.worm_name[..name_len]).unwrap_or("?"); let _ = log_validation(&format!( @@ -950,15 +1132,24 @@ fn dump_worm_tasks() { let mut entry_name = [0u8; 17]; for i in 0..17usize { entry_name[i] = read_u8(entry_addr + 0x78 + i as u32); - if entry_name[i] == 0 { break; } + if entry_name[i] == 0 { + break; + } } let entry_name_len = entry_name.iter().position(|&c| c == 0).unwrap_or(17); - let entry_name_str = core::str::from_utf8(&entry_name[..entry_name_len]).unwrap_or("?"); + let entry_name_str = + core::str::from_utf8(&entry_name[..entry_name_len]).unwrap_or("?"); - let state_msg = if state == entry_state { "state:OK".to_string() } - else { format!("state:MISMATCH(entry=0x{:04X})", entry_state) }; - let name_msg = if name == entry_name_str { "name:OK".to_string() } - else { format!("name:MISMATCH(entry=\"{}\")", entry_name_str) }; + let state_msg = if state == entry_state { + "state:OK".to_string() + } else { + format!("state:MISMATCH(entry=0x{:04X})", entry_state) + }; + let name_msg = if name == entry_name_str { + "name:OK".to_string() + } else { + format!("name:MISMATCH(entry=\"{}\")", entry_name_str) + }; let _ = log_validation(&format!( " xcheck WormEntry[{},{}] @ 0x{:08X}: {} {}", team_idx, worm_idx, entry_addr, state_msg, name_msg @@ -975,34 +1166,53 @@ fn dump_worm_tasks() { // --------------------------------------------------------------------------- fn dump_turngame() { - use openwa_core::task::CTaskTurnGame; use crate::replacements::input::dump_region; + use openwa_core::task::CTaskTurnGame; let _ = log_validation(""); let _ = log_validation("--- CTaskTurnGame Dump ---"); unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } // Find CTaskTurnGame via the shared_data entity table. let task_land_ptr = read_u32(ddgame_ptr + 0x54C); - if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL — skipping."); return; } + if task_land_ptr == 0 { + let _ = log_validation(" CTaskLand NULL — skipping."); + return; + } let task_land = task_land_ptr as *const openwa_core::task::CTask; let shared_data_ptr = (*task_land).shared_data; - if shared_data_ptr.is_null() { let _ = log_validation(" shared_data NULL."); return; } + if shared_data_ptr.is_null() { + let _ = log_validation(" shared_data NULL."); + return; + } let table = SharedDataTable::from_ptr(shared_data_ptr); let expected_vt = rb(va::CTASK_TURN_GAME_VTABLE); let mut tg_ptr: u32 = 0; for node in table.iter() { let entity = (*node).entity; - if entity.is_null() { continue; } - if !is_in_rdata(read_u32(entity as u32)) { continue; } + if entity.is_null() { + continue; + } + if !is_in_rdata(read_u32(entity as u32)) { + continue; + } if read_u32(entity as u32) == expected_vt { tg_ptr = entity as u32; break; @@ -1020,38 +1230,97 @@ fn dump_turngame() { let tg = &*(tg_ptr as *const CTaskTurnGame); let ctx = &tg.game_ctx; let _ = log_validation(" TurnGameCtx fields:"); - let _ = log_validation(&format!(" +0x40 land_height = {} ({:.4})", ctx.land_height.0, ctx.land_height.to_f32())); - let _ = log_validation(&format!(" +0x44 land_height_2 = {} ({:.4})", ctx.land_height_2.0, ctx.land_height_2.to_f32())); + let _ = log_validation(&format!( + " +0x40 land_height = {} ({:.4})", + ctx.land_height.0, + ctx.land_height.to_f32() + )); + let _ = log_validation(&format!( + " +0x44 land_height_2 = {} ({:.4})", + ctx.land_height_2.0, + ctx.land_height_2.to_f32() + )); let _ = log_validation(&format!(" +0x48 _sentinel_18 = {}", ctx._sentinel_18)); let _ = log_validation(&format!(" +0x58 _sentinel_28 = {}", ctx._sentinel_28)); let _ = log_validation(&format!(" +0x68 _sentinel_38 = {}", ctx._sentinel_38)); let _ = log_validation(&format!(" +0x7C team_count = {}", ctx.team_count)); let _ = log_validation(&format!(" +0xD0 _slot_d0 = {}", ctx._slot_d0)); - let _ = log_validation(&format!(" +0xD4 _hud_textbox_a = 0x{:08X}", ctx._hud_textbox_a)); - let _ = log_validation(&format!(" +0xD8 _hud_textbox_b = 0x{:08X}", ctx._hud_textbox_b)); + let _ = log_validation(&format!( + " +0xD4 _hud_textbox_a = 0x{:08X}", + ctx._hud_textbox_a + )); + let _ = log_validation(&format!( + " +0xD8 _hud_textbox_b = 0x{:08X}", + ctx._hud_textbox_b + )); let _ = log_validation(" Known fields:"); - let _ = log_validation(&format!(" +0x108 worm_active = {}", tg.worm_active)); - let _ = log_validation(&format!(" +0x12C current_team = {} (1-based, 0=none)", tg.current_team)); - let _ = log_validation(&format!(" +0x130 current_worm = {} (0-based)", tg.current_worm)); - let _ = log_validation(&format!(" +0x134 arena_team = {}", tg.arena_team)); - let _ = log_validation(&format!(" +0x138 arena_worm = {}", tg.arena_worm)); - let _ = log_validation(&format!(" +0x150 turn_ended = {}", tg.turn_ended)); - let _ = log_validation(&format!(" +0x154 no_time_limit = {}", tg.no_time_limit)); - let _ = log_validation(&format!(" +0x178 retreat_timer = {} ms", tg.retreat_timer)); - let _ = log_validation(&format!(" +0x17C retreat_time_max = {} ms", tg.retreat_time_max)); - let _ = log_validation(&format!(" +0x184 idle_timer = {} ms", tg.idle_timer)); - let _ = log_validation(&format!(" +0x188 turn_timer_display = {} ms", tg.turn_timer_display)); - let _ = log_validation(&format!(" +0x18C turn_timer = {} ms", tg.turn_timer)); - let _ = log_validation(&format!(" +0x2D4 active_worm_frames = {}", tg.active_worm_frames)); - let _ = log_validation(&format!(" +0x2D8 retreat_frames = {}", tg.retreat_frames)); - let _ = log_validation(&format!(" +0x2DC _timer_scale = {}", tg._timer_scale)); + let _ = log_validation(&format!( + " +0x108 worm_active = {}", + tg.worm_active + )); + let _ = log_validation(&format!( + " +0x12C current_team = {} (1-based, 0=none)", + tg.current_team + )); + let _ = log_validation(&format!( + " +0x130 current_worm = {} (0-based)", + tg.current_worm + )); + let _ = log_validation(&format!( + " +0x134 arena_team = {}", + tg.arena_team + )); + let _ = log_validation(&format!( + " +0x138 arena_worm = {}", + tg.arena_worm + )); + let _ = log_validation(&format!( + " +0x150 turn_ended = {}", + tg.turn_ended + )); + let _ = log_validation(&format!( + " +0x154 no_time_limit = {}", + tg.no_time_limit + )); + let _ = log_validation(&format!( + " +0x178 retreat_timer = {} ms", + tg.retreat_timer + )); + let _ = log_validation(&format!( + " +0x17C retreat_time_max = {} ms", + tg.retreat_time_max + )); + let _ = log_validation(&format!( + " +0x184 idle_timer = {} ms", + tg.idle_timer + )); + let _ = log_validation(&format!( + " +0x188 turn_timer_display = {} ms", + tg.turn_timer_display + )); + let _ = log_validation(&format!( + " +0x18C turn_timer = {} ms", + tg.turn_timer + )); + let _ = log_validation(&format!( + " +0x2D4 active_worm_frames = {}", + tg.active_worm_frames + )); + let _ = log_validation(&format!( + " +0x2D8 retreat_frames = {}", + tg.retreat_frames + )); + let _ = log_validation(&format!( + " +0x2DC _timer_scale = {}", + tg._timer_scale + )); // Full memory dump — classify every DWORD to discover unknown fields. // Dump in chunks to keep log lines manageable. let base = tg_ptr as *const u8; dump_region(base, 0x00, 0x30, "CTaskTurnGame"); // CTask base - dump_region(base, 0x30, 0x38, "TurnGameCtx"); // 0x30..0x67 (vtable + sentinels) - dump_region(base, 0x68, 0x74, "TurnGameCtx"); // 0x68..0xDB (team_count + unknowns) + dump_region(base, 0x30, 0x38, "TurnGameCtx"); // 0x30..0x67 (vtable + sentinels) + dump_region(base, 0x68, 0x74, "TurnGameCtx"); // 0x68..0xDB (team_count + unknowns) dump_region(base, 0xDC, 0x80, "CTaskTurnGame"); // 0xDC..0x15B dump_region(base, 0x15C, 0x80, "CTaskTurnGame"); // 0x15C..0x1DB dump_region(base, 0x1DC, 0x80, "CTaskTurnGame"); // 0x1DC..0x25B @@ -1080,20 +1349,34 @@ fn dump_ctask_children() { unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } let task_land_ptr = read_u32(ddgame_ptr + 0x54C); - if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL."); return; } + if task_land_ptr == 0 { + let _ = log_validation(" CTaskLand NULL."); + return; + } // Walk up to root (CTaskTurnGame) let mut cursor = task_land_ptr; for _ in 0..10 { let parent = read_u32(cursor + 0x04); - if parent == 0 { break; } + if parent == 0 { + break; + } cursor = parent; } let root = cursor; @@ -1101,11 +1384,13 @@ fn dump_ctask_children() { // Print the children sub-struct fields for root, then walk its first // child (CTaskTeam level) and each of its children (CTaskFilter level). let print_task_children = |label: &str, addr: u32| { - if addr == 0 { return; } - let cap = read_u32(addr + 0x08); + if addr == 0 { + return; + } + let cap = read_u32(addr + 0x08); let dirty = read_u32(addr + 0x0C); let wmark = read_u32(addr + 0x10); - let data = read_u32(addr + 0x14); + let data = read_u32(addr + 0x14); let _ = log_validation(&format!( " {} @ 0x{:08X} cap={} dirty={} watermark={} data=0x{:08X}", label, addr, cap, dirty, wmark, data @@ -1118,22 +1403,28 @@ fn dump_ctask_children() { // First two children of root (CTaskTeam instances) let root_watermark = read_u32(root + 0x10) as usize; - let root_data = read_u32(root + 0x14); + let root_data = read_u32(root + 0x14); let mut teams_found = 0; for i in 0..root_watermark.min(64) { let child = read_u32(root_data + i as u32 * 4); - if child == 0 { continue; } + if child == 0 { + continue; + } print_task_children(&format!(" child[{}] (CTaskTeam?)", i), child); // And print the first few children of this child (CTaskFilter level) let child_wmark = read_u32(child + 0x10) as usize; - let child_data = read_u32(child + 0x14); + let child_data = read_u32(child + 0x14); for j in 0..child_wmark.min(16) { let gc = read_u32(child_data + j as u32 * 4); - if gc == 0 { continue; } + if gc == 0 { + continue; + } print_task_children(&format!(" grandchild[{}] (CTaskFilter?)", j), gc); } teams_found += 1; - if teams_found >= 2 { break; } + if teams_found >= 2 { + break; + } } } } @@ -1143,25 +1434,40 @@ fn dump_ctask_children() { // --------------------------------------------------------------------------- fn dump_ctaskteam_entities() { - use openwa_core::task::CTaskTeam; use crate::replacements::input::dump_region; + use openwa_core::task::CTaskTeam; let _ = log_validation(""); let _ = log_validation("--- CTaskTeam Entity Dump ---"); unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } let task_land_ptr = read_u32(ddgame_ptr + 0x54C); - if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL — skipping."); return; } + if task_land_ptr == 0 { + let _ = log_validation(" CTaskLand NULL — skipping."); + return; + } let task_land = task_land_ptr as *const openwa_core::task::CTask; let shared_data_ptr = (*task_land).shared_data; - if shared_data_ptr.is_null() { let _ = log_validation(" shared_data NULL."); return; } + if shared_data_ptr.is_null() { + let _ = log_validation(" shared_data NULL."); + return; + } let table = SharedDataTable::from_ptr(shared_data_ptr); let expected_vt = rb(va::CTASK_TEAM_VTABLE); @@ -1169,9 +1475,15 @@ fn dump_ctaskteam_entities() { for node in table.iter() { let entity = (*node).entity; - if entity.is_null() { continue; } - if !is_in_rdata(read_u32(entity as u32)) { continue; } - if read_u32(entity as u32) != expected_vt { continue; } + if entity.is_null() { + continue; + } + if !is_in_rdata(read_u32(entity as u32)) { + continue; + } + if read_u32(entity as u32) != expected_vt { + continue; + } let addr = entity as u32; let team = &*(addr as *const CTaskTeam); @@ -1184,15 +1496,15 @@ fn dump_ctaskteam_entities() { // Full memory dump in chunks — classify every DWORD. let base = addr as *const u8; - dump_region(base, 0x000, 0x30, "CTaskTeam"); // CTask base - dump_region(base, 0x030, 0x58, "CTaskTeam"); // 0x30..0x87 (secondary vtable, unknowns) - dump_region(base, 0x088, 0x90, "CTaskTeam"); // 0x88..0x117 (item_slots start, worm_count region) + dump_region(base, 0x000, 0x30, "CTaskTeam"); // CTask base + dump_region(base, 0x030, 0x58, "CTaskTeam"); // 0x30..0x87 (secondary vtable, unknowns) + dump_region(base, 0x088, 0x90, "CTaskTeam"); // 0x88..0x117 (item_slots start, worm_count region) dump_region(base, 0x118, 0x100, "CTaskTeam"); // 0x118..0x217 (item_slots end) - dump_region(base, 0x218, 0x80, "CTaskTeam"); // 0x218..0x297 (worm_count + unknowns) - dump_region(base, 0x298, 0x80, "CTaskTeam"); // 0x298..0x317 - dump_region(base, 0x318, 0x80, "CTaskTeam"); // 0x318..0x397 - dump_region(base, 0x398, 0x68, "CTaskTeam"); // 0x398..0x3FF - dump_region(base, 0x400, 0x60, "CTaskTeam"); // 0x400..0x45F + dump_region(base, 0x218, 0x80, "CTaskTeam"); // 0x218..0x297 (worm_count + unknowns) + dump_region(base, 0x298, 0x80, "CTaskTeam"); // 0x298..0x317 + dump_region(base, 0x318, 0x80, "CTaskTeam"); // 0x318..0x397 + dump_region(base, 0x398, 0x68, "CTaskTeam"); // 0x398..0x3FF + dump_region(base, 0x400, 0x60, "CTaskTeam"); // 0x400..0x45F found += 1; } @@ -1224,10 +1536,17 @@ unsafe fn dump_missile_raw(ptr: u32) { (0x130, 0x15C, "spawn_params"), (0x15C, 0x2D4, "weapon_data"), (0x2D4, 0x37C, "render_data"), - (0x37C, 0x41C, "_unknown_37c / launch_speed / homing / direction"), + ( + 0x37C, + 0x41C, + "_unknown_37c / launch_speed / homing / direction", + ), ]; for &(start, end, label) in sections { - let _ = log_validation(&format!(" -- {} (0x{:03X}..0x{:03X}) --", label, start, end)); + let _ = log_validation(&format!( + " -- {} (0x{:03X}..0x{:03X}) --", + label, start, end + )); let dwords = (end - start) / 4; for i in 0..dwords { let off = start + i * 4; @@ -1245,25 +1564,42 @@ fn dump_missile_tasks() { unsafe { let session_ptr = read_u32(rb(va::G_GAME_SESSION)); - if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; } + if session_ptr == 0 { + let _ = log_validation(" No game session — skipping."); + return; + } let wrapper_addr = read_u32(session_ptr + 0xA0); - if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; } + if wrapper_addr == 0 { + let _ = log_validation(" No DDGameWrapper."); + return; + } let ddgame_ptr = read_u32(wrapper_addr + 0x488); - if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; } + if ddgame_ptr == 0 { + let _ = log_validation(" No DDGame."); + return; + } // Walk up from CTaskLand to root (CTaskTurnGame) via parent links. let task_land_ptr = read_u32(ddgame_ptr + 0x54C); - if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL — game not loaded."); return; } + if task_land_ptr == 0 { + let _ = log_validation(" CTaskLand NULL — game not loaded."); + return; + } let mut root = task_land_ptr; for _ in 0..10 { let parent = read_u32(root + 0x04); - if parent == 0 { break; } + if parent == 0 { + break; + } root = parent; } let expected_vt = rb(va::CTASK_MISSILE_VTABLE); let _ = log_validation(&format!(" Root (CTaskTurnGame) @ 0x{:08X}", root)); - let _ = log_validation(&format!(" Expected CTaskMissile vtable: 0x{:08X}", expected_vt)); + let _ = log_validation(&format!( + " Expected CTaskMissile vtable: 0x{:08X}", + expected_vt + )); let mut found = 0u32; let mut first_ptr: u32 = 0; @@ -1276,7 +1612,9 @@ fn dump_missile_tasks() { let vt = read_u32(node); if vt == expected_vt { found += 1; - if first_ptr == 0 { first_ptr = node; } + if first_ptr == 0 { + first_ptr = node; + } let m = &*(node as *const CTaskMissile); let pos_x = m.base.pos_x.to_f32(); @@ -1285,17 +1623,28 @@ fn dump_missile_tasks() { let spd_y = m.base.speed_y.to_f32(); let _ = log_validation(&format!( " missile#{} @ 0x{:08X} slot={} type={:?} homing={} dir={}", - found, node, m.slot_id, m.missile_type(), m.homing_enabled, m.direction + found, + node, + m.slot_id, + m.missile_type(), + m.homing_enabled, + m.direction )); let _ = log_validation(&format!( " pos=({:.2},{:.2}) speed=({:.3},{:.3}) cursor=({:.1},{:.1})", - pos_x, pos_y, spd_x, spd_y, - m.cursor_x().to_f32(), m.cursor_y().to_f32() + pos_x, + pos_y, + spd_x, + spd_y, + m.cursor_x().to_f32(), + m.cursor_y().to_f32() )); let _ = log_validation(&format!( " spawn_params: owner={} pellet={} spawn=({:.1},{:.1})", - m.spawn_params[0], m.spawn_params[8], - m.spawn_x().to_f32(), m.spawn_y().to_f32() + m.spawn_params[0], + m.spawn_params[8], + m.spawn_x().to_f32(), + m.spawn_y().to_f32() )); let _ = log_validation(&format!( " weapon_data[0..10]: {:?}", @@ -1309,7 +1658,10 @@ fn dump_missile_tasks() { } } - let _ = log_validation(&format!(" Total CTaskMissile found: {} (scanned {} nodes)", found, scanned)); + let _ = log_validation(&format!( + " Total CTaskMissile found: {} (scanned {} nodes)", + found, scanned + )); if found == 0 { let _ = log_validation(" No missiles in flight — fire a weapon first, then press F8."); @@ -1317,7 +1669,10 @@ fn dump_missile_tasks() { } // Full raw dump of the first missile to cross-validate struct layout. - let _ = log_validation(&format!("\n === Raw dump: CTaskMissile @ 0x{:08X} ===", first_ptr)); + let _ = log_validation(&format!( + "\n === Raw dump: CTaskMissile @ 0x{:08X} ===", + first_ptr + )); dump_missile_raw(first_ptr); } } @@ -1339,7 +1694,9 @@ pub fn run() -> Result<(), String> { let delta = base.wrapping_sub(va::IMAGE_BASE); let _ = log_validation(&format!( " Module base: 0x{:08X} (Ghidra base: 0x{:08X}, delta: 0x{:08X})", - base, va::IMAGE_BASE, delta + base, + va::IMAGE_BASE, + delta )); let mut result = ValidationResult::new(); @@ -1353,7 +1710,9 @@ pub fn run() -> Result<(), String> { match hooks::install_all() { Ok(()) => {} - Err(e) => { let _ = log_validation(&format!("[ERROR] Hook installation failed: {}", e)); } + Err(e) => { + let _ = log_validation(&format!("[ERROR] Hook installation failed: {}", e)); + } } let auto_mode = std::env::var("OPENWA_REPLAY_TEST").is_ok(); @@ -1361,7 +1720,8 @@ pub fn run() -> Result<(), String> { if auto_mode { let _ = log_validation(""); let _ = log_validation("--- Auto-Capture Mode (OPENWA_REPLAY_TEST) ---"); - let _ = log_validation(" Replay fast-forward active — game will exit when replay finishes."); + let _ = + log_validation(" Replay fast-forward active — game will exit when replay finishes."); let _ = log_validation(" Safety timeout: 120s (will force exit if replay hangs)."); std::thread::spawn(move || { // Restore minimized window so the replay can start. @@ -1442,8 +1802,8 @@ pub fn run() -> Result<(), String> { /// Dump DSSound channel descriptor raw bytes for field layout verification. unsafe fn dump_dssound_channels() { use openwa_core::address::va; - use openwa_core::rebase::rb; use openwa_core::engine::game_session::GameSession; + use openwa_core::rebase::rb; let session_ptr = rb(va::G_GAME_SESSION) as *const *const GameSession; let session = *session_ptr; @@ -1470,14 +1830,24 @@ unsafe fn dump_dssound_channels() { let desc_offset = 0x14 + i * 0x18; let desc_ptr = base.add(desc_offset) as *const u32; let words: [u32; 6] = [ - *desc_ptr, *desc_ptr.add(1), *desc_ptr.add(2), - *desc_ptr.add(3), *desc_ptr.add(4), *desc_ptr.add(5), + *desc_ptr, + *desc_ptr.add(1), + *desc_ptr.add(2), + *desc_ptr.add(3), + *desc_ptr.add(4), + *desc_ptr.add(5), ]; let has_buffer = words[5] != 0; // ds_buffer at +0x14 = word[5] let _ = log_validation(&format!( " desc[{}] @+0x{:03X}: [{:08X} {:08X} {:08X} {:08X} {:08X} {:08X}]{}", - i, desc_offset, - words[0], words[1], words[2], words[3], words[4], words[5], + i, + desc_offset, + words[0], + words[1], + words[2], + words[3], + words[4], + words[5], if has_buffer { " <-- has buffer" } else { "" } )); } @@ -1508,8 +1878,12 @@ pub fn start_hotkeys() { loop { std::thread::sleep(std::time::Duration::from_millis(100)); unsafe { - if GetAsyncKeyState(VK_F7) & 1 != 0 { dump_dssound_channels(); } - if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_missile_tasks(); } + if GetAsyncKeyState(VK_F7) & 1 != 0 { + dump_dssound_channels(); + } + if GetAsyncKeyState(VK_F8) & 1 != 0 { + dump_missile_tasks(); + } } } }); diff --git a/replay-test.ps1 b/replay-test.ps1 index d1e86f44aec955cd10ce40dc690bc59d073a4816..f4005dfddda129de8df88cef2b9942a54e702c31 100644 --- a/replay-test.ps1 +++ b/replay-test.ps1 @@ -179,5 +179,15 @@ if ($Headless) { Write-Host " Validation: NOT found in log (may not have reached gameplay)" -ForegroundColor Yellow } } + # Check for Rust panics in OpenWA log + if (Test-Path "$logDir\openwa_latest.log") { + $panics = Select-String -Path "$logDir\openwa_latest.log" -Pattern "\[PANIC\]" + if ($panics) { + Write-Host "" + Write-Host "=== RUST PANIC DETECTED ===" -ForegroundColor Red + $panics | ForEach-Object { Write-Host " $($_.Line)" -ForegroundColor Red } + } + } + Write-Host " Logs: $logDir\" -ForegroundColor Gray }