paavohtl commited on
Commit
6db1334
·
2 Parent(s): 4d0c5889ddf61a

Merge branch 'ddgame-constructor': Rust DDGame constructor

Browse files

Full Rust replacement for DDGame__Constructor (0x56E220).
53 commits, 7 sub-function hooks + complete constructor flow.
Headless replay determinism verified. Game fully functional.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. crates/openwa-core/src/address.rs +40 -0
  2. crates/openwa-core/src/audio/dssound.rs +82 -52
  3. crates/openwa-core/src/audio/mod.rs +6 -8
  4. crates/openwa-core/src/display/palette.rs +9 -3
  5. crates/openwa-core/src/engine/ddgame.rs +1781 -11
  6. crates/openwa-core/src/engine/ddgame_wrapper.rs +1 -1
  7. crates/openwa-core/src/engine/game_info.rs +71 -24
  8. crates/openwa-core/src/engine/game_session.rs +2 -2
  9. crates/openwa-core/src/fixed.rs +1 -1
  10. crates/openwa-core/src/game/frontend.rs +0 -4
  11. crates/openwa-core/src/game/message.rs +1 -2
  12. crates/openwa-core/src/game/scheme.rs +38 -29
  13. crates/openwa-core/src/input/controller.rs +3 -1
  14. crates/openwa-core/src/lib.rs +4 -4
  15. crates/openwa-core/src/render/queue.rs +8 -8
  16. crates/openwa-core/src/render/sprite.rs +0 -1
  17. crates/openwa-core/src/task/base.rs +4 -2
  18. crates/openwa-core/src/task/cloud.rs +1 -1
  19. crates/openwa-core/src/task/game_task.rs +1 -1
  20. crates/openwa-core/src/task/missile.rs +1 -6
  21. crates/openwa-core/src/task/mod.rs +2 -2
  22. crates/openwa-core/src/task/overlays.rs +1 -1
  23. crates/openwa-core/src/task/team.rs +1 -1
  24. crates/openwa-core/src/task/turn_game.rs +1 -2
  25. crates/openwa-core/src/task/worm.rs +1 -2
  26. crates/openwa-core/src/wa/frontend.rs +1 -1
  27. crates/openwa-core/src/wa/mfc.rs +2 -3
  28. crates/openwa-core/src/wa_alloc.rs +1 -2
  29. crates/openwa-core/src/wa_call.rs +3 -6
  30. crates/openwa-core/tests/scheme_parse.rs +12 -11
  31. crates/openwa-debugui/src/app.rs +579 -365
  32. crates/openwa-debugui/src/log.rs +7 -2
  33. crates/openwa-harness/src/lib.rs +18 -9
  34. crates/openwa-launcher/src/inject.rs +39 -16
  35. crates/openwa-launcher/src/main.rs +57 -32
  36. crates/openwa-wormkit/src/hook.rs +3 -6
  37. crates/openwa-wormkit/src/lib.rs +7 -2
  38. crates/openwa-wormkit/src/replacements/config.rs +29 -30
  39. crates/openwa-wormkit/src/replacements/ddgame_init.rs +205 -0
  40. crates/openwa-wormkit/src/replacements/display.rs +2 -2
  41. crates/openwa-wormkit/src/replacements/frontend.rs +5 -3
  42. crates/openwa-wormkit/src/replacements/game_session.rs +371 -50
  43. crates/openwa-wormkit/src/replacements/hardware_init.rs +35 -26
  44. crates/openwa-wormkit/src/replacements/headless.rs +48 -22
  45. crates/openwa-wormkit/src/replacements/input.rs +12 -7
  46. crates/openwa-wormkit/src/replacements/mod.rs +3 -1
  47. crates/openwa-wormkit/src/replacements/render.rs +77 -91
  48. crates/openwa-wormkit/src/replacements/scheme.rs +21 -44
  49. crates/openwa-wormkit/src/replacements/sound.rs +18 -13
  50. crates/openwa-wormkit/src/replacements/speech.rs +21 -25
crates/openwa-core/src/address.rs CHANGED
@@ -333,9 +333,47 @@ pub mod va {
333
  pub const CONSTRUCT_OPENGL_CPU: u32 = 0x005A_0850;
334
  pub const OPENGL_INIT: u32 = 0x0059_F000;
335
  pub const DDGAME_INIT_FIELDS: u32 = 0x0052_6120;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
337
  pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
338
  pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
 
 
 
 
 
 
339
 
340
  // === Higher-level drawing functions ===
341
 
@@ -668,6 +706,8 @@ pub mod va {
668
  pub const WA_MALLOC: u32 = 0x005C_0AE3;
669
  pub const WA_MALLOC_MEMSET: u32 = 0x0053_E910;
670
  pub const WA_FREE: u32 = 0x005D_0D2B;
 
 
671
 
672
  // === Bitmap font system ===
673
 
 
333
  pub const CONSTRUCT_OPENGL_CPU: u32 = 0x005A_0850;
334
  pub const OPENGL_INIT: u32 = 0x0059_F000;
335
  pub const DDGAME_INIT_FIELDS: u32 = 0x0052_6120;
336
+ /// DDGame__InitRenderIndices (0x526080): usercall(ESI=ddgame), plain RET.
337
+ /// Initializes render queue index arrays at DDGame+0xC4..+0x4A2.
338
+ pub const DDGAME_INIT_RENDER_INDICES: u32 = 0x0052_6080;
339
+ /// DDGame__InitVersionFlags (0x525BE0): stdcall(ddgame_wrapper).
340
+ /// Sets DDGame+0x7E2E/0x7E2F/0x7E3F flags based on game version/mode.
341
+ pub const DDGAME_INIT_VERSION_FLAGS: u32 = 0x0052_5BE0;
342
+ /// GfxResource__Create_Maybe (0x4F6300): returns GfxResource ptr or null.
343
+ pub const GFX_RESOURCE_CREATE: u32 = 0x004F_6300;
344
+ /// TaskStateMachine__Init_Maybe (0x4F6370): initializes TSM-like object.
345
+ pub const TASK_STATE_MACHINE_INIT: u32 = 0x004F_6370;
346
+ /// DDGame__InitSoundPaths_Maybe (0x570F30): called before DSSound_LoadEffectWAVs.
347
+ pub const DDGAME_INIT_SOUND_PATHS: u32 = 0x0057_0F30;
348
+ /// DDGameWrapper__LoadingProgressTick (0x5717A0): loading progress bar + message pump.
349
+ pub const DDGAME_WRAPPER_LOADING_PROGRESS_TICK: u32 = 0x0057_17A0;
350
+ /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): loads weapon icons, wind indicators,
351
+ /// girder sprites, and creates DDGame+0x37C DisplayGfx object.
352
+ /// thiscall(ECX=gfx_handler_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8.
353
+ pub const DDGAME_LOAD_HUD_AND_WEAPON_SPRITES: u32 = 0x0053_D0E0;
354
+ /// DDGame__InitDisplayFinal_Maybe (0x56A830): display finalization for non-headless.
355
+ pub const DDGAME_INIT_DISPLAY_FINAL: u32 = 0x0056_A830;
356
+ /// PCLandscape__Constructor (0x57ACB0): constructs 0xB44-byte landscape object.
357
+ pub const PC_LANDSCAPE_CONSTRUCTOR: u32 = 0x0057_ACB0;
358
+ /// SpriteRegion__Constructor (0x57DB20): constructs 0x9C-byte sprite region.
359
+ pub const SPRITE_REGION_CONSTRUCTOR: u32 = 0x0057_DB20;
360
+ /// g_GameInfo global — set to current GameInfo* during DDGame construction.
361
+ pub const G_GAME_INFO: u32 = 0x0077_49A0;
362
+ /// GfxHandler__LoadSprites_Maybe (0x570B50): loads sprites from GfxHandler.
363
+ pub const GFX_HANDLER_LOAD_SPRITES: u32 = 0x0057_0B50;
364
+ /// FUN_00570A90: called before display palette setup (non-headless only).
365
+ pub const FUN_570A90: u32 = 0x0057_0A90;
366
+ /// FUN_00570E20: called after GfxHandler initialization.
367
+ pub const FUN_570E20: u32 = 0x0057_0E20;
368
  pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
369
  pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
370
  pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
371
+ /// DisplayGfx__Constructor_Maybe (0x4F5E80): stdcall(raw_image) -> DisplayGfx*, RET 0x4.
372
+ pub const DISPLAYGFX_CONSTRUCTOR: u32 = 0x004F_5E80;
373
+ /// IMG_Decode (0x4F5F80): stdcall(output, raw_image, 1) -> DisplayGfx*, RET 0xC.
374
+ /// Reads IMG\x1A header, decodes image data into output buffer.
375
+ /// Ghidra mislabels this as "TaskStateMachine__Destructor_Maybe".
376
+ pub const IMG_DECODE: u32 = 0x004F_5F80;
377
 
378
  // === Higher-level drawing functions ===
379
 
 
706
  pub const WA_MALLOC: u32 = 0x005C_0AE3;
707
  pub const WA_MALLOC_MEMSET: u32 = 0x0053_E910;
708
  pub const WA_FREE: u32 = 0x005D_0D2B;
709
+ /// WA's CRT _fopen — cdecl(path, mode) → FILE*.
710
+ pub const WA_FOPEN: u32 = 0x005D_3271;
711
 
712
  // === Bitmap font system ===
713
 
crates/openwa-core/src/audio/dssound.rs CHANGED
@@ -7,14 +7,11 @@ use windows::Win32::Media::Audio::DirectSound::IDirectSoundBuffer;
7
  /// Index 0 = silence (-10000 dB), index 63 = near-unity (-22 dB).
8
  /// Used by set_master_volume, set_channel_volume, and play_sound.
9
  const VOLUME_DB_TABLE: [i16; 64] = [
10
- -10000, -6000, -5000, -4415, -4000, -3678, -3415, -3000,
11
- -2744, -2522, -2326, -2150, -1991, -1845, -1712, -1589,
12
- -1475, -1368, -1268, -1176, -1088, -1006, -928, -855,
13
- -786, -720, -658, -599, -543, -490, -439, -390,
14
- -344, -299, -256, -216, -177, -139, -104, -70,
15
- -38, -7, 24, 54, 83, 111, 138, 163,
16
- 188, 211, 234, 256, 277, 296, 315, 333,
17
- 350, 367, 383, 398, 413, 427, 441, 454,
18
  ]; // NOT: these might be negative centibels, not actual dB. Exact interpretation TBD.
19
 
20
  /// DSSound — DirectSound audio subsystem.
@@ -91,7 +88,8 @@ pub struct DSSoundVtable {
91
  /// Slot 3 (0x574730): play_sound — wrapper around core play, returns bool. RET 0x14 = 5 stack params.
92
  pub play_sound: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> bool,
93
  /// Slot 4 (0x574770): play_sound_pooled — allocates from buffer pool, plays. RET 0x14 = 5 stack params.
94
- pub play_sound_pooled: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> i32,
 
95
  /// Slot 5 (0x574900): set_pan — sets pan on channel (dB lookup)
96
  pub set_pan: unsafe extern "thiscall" fn(*mut DSSound, u32, u32) -> u32,
97
  /// Slot 6 (0x505430): **stub** — returns 0
@@ -189,17 +187,26 @@ const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
189
  /// Slot 13: is_slot_loaded — returns whether sound at `slot_idx` has a buffer loaded.
190
  /// Slot index is used directly as array index (1-based, slot 0 unused).
191
  pub unsafe extern "thiscall" fn is_slot_loaded(this: *mut DSSound, slot_idx: i32) -> bool {
192
- (*this).channel_slots.get(slot_idx as usize).copied().unwrap_or(0) != 0
 
 
 
 
 
193
  }
194
 
195
  /// Trivial noop — returns void. Used for slots 15, 16.
196
  pub unsafe extern "thiscall" fn noop(_this: *mut DSSound) {}
197
 
198
  /// Trivial stub — returns 0. Used for slots 6, 17, 18, 19, 20, 21, 22.
199
- pub unsafe extern "thiscall" fn returns_0(_this: *mut DSSound) -> u32 { 0 }
 
 
200
 
201
  /// Trivial stub — returns 1. Used for slot 23.
202
- pub unsafe extern "thiscall" fn returns_1(_this: *mut DSSound) -> u32 { 1 }
 
 
203
 
204
  /// Slot 1: update_channels — iterates 8 channel descriptors, releases finished buffers.
205
  /// Called each frame to clean up buffers that have finished playing.
@@ -254,9 +261,7 @@ fn volume_to_db(volume: i32) -> i32 {
254
 
255
  /// Slot 7: set_master_volume — sets the master volume and adjusts all active channels.
256
  /// `new_volume` is Fixed 16.16 (0 = silent, 0x10000 = max).
257
- pub unsafe extern "thiscall" fn set_master_volume(
258
- this: *mut DSSound, new_volume: i32,
259
- ) -> u32 {
260
  let snd = &mut *this;
261
 
262
  // Clamp to [0, 0x10000].
@@ -291,9 +296,7 @@ pub unsafe extern "thiscall" fn set_master_volume(
291
  /// Slot 2: set_volume_params — sets frequency scaling params and adjusts active channels.
292
  /// `param1` is the divisor (status_1), `param2` is the multiplier (status_2).
293
  /// For each playing channel: new_freq = channel_freq * param2 / param1, clamped to 200000.
294
- pub unsafe extern "thiscall" fn set_volume_params(
295
- this: *mut DSSound, param1: u32, param2: i32,
296
- ) {
297
  let snd = &mut *this;
298
 
299
  // No change → early return.
@@ -307,7 +310,9 @@ pub unsafe extern "thiscall" fn set_volume_params(
307
  let Some(buf) = desc.buffer() else { continue };
308
 
309
  // Check if buffer is playing.
310
- let Ok(status) = buf.GetStatus() else { continue };
 
 
311
  if status & 1 == 0 {
312
  continue;
313
  }
@@ -326,9 +331,7 @@ pub unsafe extern "thiscall" fn set_volume_params(
326
  /// Slot 5: set_pan — sets stereo panning on a specific channel.
327
  /// `pool_id` is 1-based (1..64). `pan` is Fixed 16.16 (-0x10000..0x10000).
328
  /// Negative = left, positive = right.
329
- pub unsafe extern "thiscall" fn set_pan(
330
- this: *mut DSSound, pool_id: i32, pan: i32,
331
- ) -> u32 {
332
  let idx = (pool_id - 1) as usize;
333
  if idx >= 64 {
334
  return 0;
@@ -362,7 +365,9 @@ pub unsafe extern "thiscall" fn set_pan(
362
  /// Slot 8: set_channel_volume — sets volume on a specific channel.
363
  /// `pool_id` is 1-based (1..64). `volume` is Fixed 16.16 (0..0x10000).
364
  pub unsafe extern "thiscall" fn set_channel_volume(
365
- this: *mut DSSound, pool_id: i32, volume: i32,
 
 
366
  ) -> u32 {
367
  let idx = (pool_id - 1) as usize;
368
  if idx >= 64 {
@@ -397,9 +402,7 @@ pub unsafe extern "thiscall" fn set_channel_volume(
397
  }
398
 
399
  /// Slot 0: destructor — releases all COM objects and frees memory.
400
- pub unsafe extern "thiscall" fn destructor(
401
- this: *mut DSSound, flags: u8,
402
- ) -> *mut DSSound {
403
  let snd = &mut *this;
404
 
405
  // Reset vtable to primary (destructor chain pattern).
@@ -488,8 +491,7 @@ unsafe fn core_play_sound(
488
  &*(&snd.channel_slots[slot_idx] as *const Ptr32 as *const IDirectSoundBuffer);
489
 
490
  // Duplicate the buffer via IDirectSound::DuplicateSoundBuffer.
491
- let ds: &IDirectSound =
492
- &*(&snd.direct_sound as *const Ptr32 as *const IDirectSound);
493
  let new_buf = match ds.DuplicateSoundBuffer(template_buf) {
494
  Ok(buf) => buf,
495
  Err(_) => {
@@ -543,7 +545,9 @@ unsafe fn core_play_sound(
543
  let pan_clamped = pan.max(-0x10000).min(0x10000);
544
  let pan_idx = ((pan_clamped.unsigned_abs() >> 10) as usize).min(63);
545
  let mut pan_db = VOLUME_DB_TABLE[63 - pan_idx] as i32;
546
- if pan_clamped > 0 { pan_db = -pan_db; }
 
 
547
  let _ = buf.SetPan(pan_db);
548
 
549
  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 {
598
  /// Slot 3: play_sound — pure thiscall with 5 stack params, RET 0x14.
599
  /// Params: flags_and_slot, priority, frequency, volume, pan.
600
  pub unsafe extern "thiscall" fn play_sound(
601
- this: *mut DSSound, flags_and_slot: u32, priority: i32, frequency: i32, volume: i32, pan: i32,
 
 
 
 
 
602
  ) -> bool {
603
  let snd = &mut *this;
604
- let result = core_play_sound(snd, flags_and_slot & 0xFFFEFFFF, priority, frequency, volume, pan);
 
 
 
 
 
 
 
605
  result >= 0
606
  }
607
 
608
  /// Slot 4: play_sound_pooled — pure thiscall with 5 stack params, RET 0x14.
609
  /// Returns pool_id (1-based) on success, 0 on failure.
610
  pub unsafe extern "thiscall" fn play_sound_pooled(
611
- this: *mut DSSound, flags_and_slot: u32, priority: i32, frequency: i32, volume: i32, pan: i32,
 
 
 
 
 
612
  ) -> i32 {
613
  let snd = &mut *this;
614
 
@@ -640,9 +661,7 @@ pub unsafe extern "thiscall" fn play_sound_pooled(
640
 
641
  /// Slot 14: sub_destructor — sets secondary vtable (0x66AF58), optionally frees.
642
  /// This is a base-class destructor called by the primary destructor (slot 0).
643
- pub unsafe extern "thiscall" fn sub_destructor(
644
- this: *mut DSSound, flags: u8,
645
- ) -> *mut DSSound {
646
  use crate::rebase::rb;
647
  // Set secondary vtable (base class vtable for destructor chain).
648
  (*this).vtable = rb(0x0066_AF58) as *const DSSoundVtable;
@@ -661,9 +680,7 @@ pub unsafe extern "thiscall" fn sub_destructor(
661
  /// - **0** if pool_id is out of range
662
  ///
663
  /// The original inverts DSBSTATUS_PLAYING (bit 0): `NOT status; AND 1`.
664
- pub unsafe extern "thiscall" fn is_channel_finished(
665
- this: *mut DSSound, pool_id: i32,
666
- ) -> u8 {
667
  let idx = (pool_id - 1) as usize;
668
  if idx >= 64 {
669
  return 0;
@@ -677,7 +694,11 @@ pub unsafe extern "thiscall" fn is_channel_finished(
677
  let Some(buf) = desc.buffer() else { return 1 };
678
  match buf.GetStatus() {
679
  Ok(status) => {
680
- if status & 1 != 0 { 0 } else { 1 } // playing → 0, stopped → 1
 
 
 
 
681
  }
682
  Err(_) => 1, // error → treat as finished
683
  }
@@ -685,9 +706,7 @@ pub unsafe extern "thiscall" fn is_channel_finished(
685
 
686
  /// Slot 10: stop_channel — stops a buffer, releases it, and returns the pool entry.
687
  /// `pool_id` is 1-based (1..64). Returns 1 on success, 0 on invalid.
688
- pub unsafe extern "thiscall" fn stop_channel(
689
- this: *mut DSSound, pool_id: i32,
690
- ) -> u32 {
691
  let idx = (pool_id - 1) as usize;
692
  if idx >= 64 {
693
  return 0;
@@ -752,8 +771,7 @@ pub unsafe extern "thiscall" fn load_wav(
752
  path: *const u8,
753
  ) -> u32 {
754
  use windows::Win32::Media::Audio::DirectSound::{
755
- IDirectSound, IDirectSoundBuffer,
756
- DSBUFFERDESC, DSBLOCK_ENTIREBUFFER,
757
  };
758
  use windows::Win32::Media::Audio::{WAVEFORMATEX, WAVE_FORMAT_PCM};
759
 
@@ -821,12 +839,18 @@ pub unsafe extern "thiscall" fn load_wav(
821
  // Lock the buffer and fill with PCM data.
822
  let mut audio_ptr1: *mut core::ffi::c_void = core::ptr::null_mut();
823
  let mut audio_len1: u32 = 0;
824
- if buf.Lock(
825
- 0, data_len,
826
- &mut audio_ptr1, &mut audio_len1,
827
- None, None,
828
- DSBLOCK_ENTIREBUFFER,
829
- ).is_err() {
 
 
 
 
 
 
830
  return 0;
831
  }
832
 
@@ -843,7 +867,10 @@ pub unsafe extern "thiscall" fn load_wav(
843
  16 => {
844
  let mut reader = match hound::WavReader::open(c_path) {
845
  Ok(r) => r,
846
- Err(_) => { let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0); return 0; }
 
 
 
847
  };
848
  let mut offset = 0usize;
849
  for sample in reader.samples::<i16>() {
@@ -858,7 +885,10 @@ pub unsafe extern "thiscall" fn load_wav(
858
  8 => {
859
  let mut reader = match hound::WavReader::open(c_path) {
860
  Ok(r) => r,
861
- Err(_) => { let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0); return 0; }
 
 
 
862
  };
863
  let mut offset = 0usize;
864
  for sample in reader.samples::<i16>() {
 
7
  /// Index 0 = silence (-10000 dB), index 63 = near-unity (-22 dB).
8
  /// Used by set_master_volume, set_channel_volume, and play_sound.
9
  const VOLUME_DB_TABLE: [i16; 64] = [
10
+ -10000, -6000, -5000, -4415, -4000, -3678, -3415, -3000, -2744, -2522, -2326, -2150, -1991,
11
+ -1845, -1712, -1589, -1475, -1368, -1268, -1176, -1088, -1006, -928, -855, -786, -720, -658,
12
+ -599, -543, -490, -439, -390, -344, -299, -256, -216, -177, -139, -104, -70, -38, -7, 24, 54,
13
+ 83, 111, 138, 163, 188, 211, 234, 256, 277, 296, 315, 333, 350, 367, 383, 398, 413, 427, 441,
14
+ 454,
 
 
 
15
  ]; // NOT: these might be negative centibels, not actual dB. Exact interpretation TBD.
16
 
17
  /// DSSound — DirectSound audio subsystem.
 
88
  /// Slot 3 (0x574730): play_sound — wrapper around core play, returns bool. RET 0x14 = 5 stack params.
89
  pub play_sound: unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> bool,
90
  /// Slot 4 (0x574770): play_sound_pooled — allocates from buffer pool, plays. RET 0x14 = 5 stack params.
91
+ pub play_sound_pooled:
92
+ unsafe extern "thiscall" fn(*mut DSSound, u32, u32, u32, u32, u32) -> i32,
93
  /// Slot 5 (0x574900): set_pan — sets pan on channel (dB lookup)
94
  pub set_pan: unsafe extern "thiscall" fn(*mut DSSound, u32, u32) -> u32,
95
  /// Slot 6 (0x505430): **stub** — returns 0
 
187
  /// Slot 13: is_slot_loaded — returns whether sound at `slot_idx` has a buffer loaded.
188
  /// Slot index is used directly as array index (1-based, slot 0 unused).
189
  pub unsafe extern "thiscall" fn is_slot_loaded(this: *mut DSSound, slot_idx: i32) -> bool {
190
+ (*this)
191
+ .channel_slots
192
+ .get(slot_idx as usize)
193
+ .copied()
194
+ .unwrap_or(0)
195
+ != 0
196
  }
197
 
198
  /// Trivial noop — returns void. Used for slots 15, 16.
199
  pub unsafe extern "thiscall" fn noop(_this: *mut DSSound) {}
200
 
201
  /// Trivial stub — returns 0. Used for slots 6, 17, 18, 19, 20, 21, 22.
202
+ pub unsafe extern "thiscall" fn returns_0(_this: *mut DSSound) -> u32 {
203
+ 0
204
+ }
205
 
206
  /// Trivial stub — returns 1. Used for slot 23.
207
+ pub unsafe extern "thiscall" fn returns_1(_this: *mut DSSound) -> u32 {
208
+ 1
209
+ }
210
 
211
  /// Slot 1: update_channels — iterates 8 channel descriptors, releases finished buffers.
212
  /// Called each frame to clean up buffers that have finished playing.
 
261
 
262
  /// Slot 7: set_master_volume — sets the master volume and adjusts all active channels.
263
  /// `new_volume` is Fixed 16.16 (0 = silent, 0x10000 = max).
264
+ pub unsafe extern "thiscall" fn set_master_volume(this: *mut DSSound, new_volume: i32) -> u32 {
 
 
265
  let snd = &mut *this;
266
 
267
  // Clamp to [0, 0x10000].
 
296
  /// Slot 2: set_volume_params — sets frequency scaling params and adjusts active channels.
297
  /// `param1` is the divisor (status_1), `param2` is the multiplier (status_2).
298
  /// For each playing channel: new_freq = channel_freq * param2 / param1, clamped to 200000.
299
+ pub unsafe extern "thiscall" fn set_volume_params(this: *mut DSSound, param1: u32, param2: i32) {
 
 
300
  let snd = &mut *this;
301
 
302
  // No change → early return.
 
310
  let Some(buf) = desc.buffer() else { continue };
311
 
312
  // Check if buffer is playing.
313
+ let Ok(status) = buf.GetStatus() else {
314
+ continue;
315
+ };
316
  if status & 1 == 0 {
317
  continue;
318
  }
 
331
  /// Slot 5: set_pan — sets stereo panning on a specific channel.
332
  /// `pool_id` is 1-based (1..64). `pan` is Fixed 16.16 (-0x10000..0x10000).
333
  /// Negative = left, positive = right.
334
+ pub unsafe extern "thiscall" fn set_pan(this: *mut DSSound, pool_id: i32, pan: i32) -> u32 {
 
 
335
  let idx = (pool_id - 1) as usize;
336
  if idx >= 64 {
337
  return 0;
 
365
  /// Slot 8: set_channel_volume — sets volume on a specific channel.
366
  /// `pool_id` is 1-based (1..64). `volume` is Fixed 16.16 (0..0x10000).
367
  pub unsafe extern "thiscall" fn set_channel_volume(
368
+ this: *mut DSSound,
369
+ pool_id: i32,
370
+ volume: i32,
371
  ) -> u32 {
372
  let idx = (pool_id - 1) as usize;
373
  if idx >= 64 {
 
402
  }
403
 
404
  /// Slot 0: destructor — releases all COM objects and frees memory.
405
+ pub unsafe extern "thiscall" fn destructor(this: *mut DSSound, flags: u8) -> *mut DSSound {
 
 
406
  let snd = &mut *this;
407
 
408
  // Reset vtable to primary (destructor chain pattern).
 
491
  &*(&snd.channel_slots[slot_idx] as *const Ptr32 as *const IDirectSoundBuffer);
492
 
493
  // Duplicate the buffer via IDirectSound::DuplicateSoundBuffer.
494
+ let ds: &IDirectSound = &*(&snd.direct_sound as *const Ptr32 as *const IDirectSound);
 
495
  let new_buf = match ds.DuplicateSoundBuffer(template_buf) {
496
  Ok(buf) => buf,
497
  Err(_) => {
 
545
  let pan_clamped = pan.max(-0x10000).min(0x10000);
546
  let pan_idx = ((pan_clamped.unsigned_abs() >> 10) as usize).min(63);
547
  let mut pan_db = VOLUME_DB_TABLE[63 - pan_idx] as i32;
548
+ if pan_clamped > 0 {
549
+ pan_db = -pan_db;
550
+ }
551
  let _ = buf.SetPan(pan_db);
552
 
553
  let _ = buf.Play(0, 0, if loop_flag { 1 } else { 0 });
 
602
  /// Slot 3: play_sound — pure thiscall with 5 stack params, RET 0x14.
603
  /// Params: flags_and_slot, priority, frequency, volume, pan.
604
  pub unsafe extern "thiscall" fn play_sound(
605
+ this: *mut DSSound,
606
+ flags_and_slot: u32,
607
+ priority: i32,
608
+ frequency: i32,
609
+ volume: i32,
610
+ pan: i32,
611
  ) -> bool {
612
  let snd = &mut *this;
613
+ let result = core_play_sound(
614
+ snd,
615
+ flags_and_slot & 0xFFFEFFFF,
616
+ priority,
617
+ frequency,
618
+ volume,
619
+ pan,
620
+ );
621
  result >= 0
622
  }
623
 
624
  /// Slot 4: play_sound_pooled — pure thiscall with 5 stack params, RET 0x14.
625
  /// Returns pool_id (1-based) on success, 0 on failure.
626
  pub unsafe extern "thiscall" fn play_sound_pooled(
627
+ this: *mut DSSound,
628
+ flags_and_slot: u32,
629
+ priority: i32,
630
+ frequency: i32,
631
+ volume: i32,
632
+ pan: i32,
633
  ) -> i32 {
634
  let snd = &mut *this;
635
 
 
661
 
662
  /// Slot 14: sub_destructor — sets secondary vtable (0x66AF58), optionally frees.
663
  /// This is a base-class destructor called by the primary destructor (slot 0).
664
+ pub unsafe extern "thiscall" fn sub_destructor(this: *mut DSSound, flags: u8) -> *mut DSSound {
 
 
665
  use crate::rebase::rb;
666
  // Set secondary vtable (base class vtable for destructor chain).
667
  (*this).vtable = rb(0x0066_AF58) as *const DSSoundVtable;
 
680
  /// - **0** if pool_id is out of range
681
  ///
682
  /// The original inverts DSBSTATUS_PLAYING (bit 0): `NOT status; AND 1`.
683
+ pub unsafe extern "thiscall" fn is_channel_finished(this: *mut DSSound, pool_id: i32) -> u8 {
 
 
684
  let idx = (pool_id - 1) as usize;
685
  if idx >= 64 {
686
  return 0;
 
694
  let Some(buf) = desc.buffer() else { return 1 };
695
  match buf.GetStatus() {
696
  Ok(status) => {
697
+ if status & 1 != 0 {
698
+ 0
699
+ } else {
700
+ 1
701
+ } // playing → 0, stopped → 1
702
  }
703
  Err(_) => 1, // error → treat as finished
704
  }
 
706
 
707
  /// Slot 10: stop_channel — stops a buffer, releases it, and returns the pool entry.
708
  /// `pool_id` is 1-based (1..64). Returns 1 on success, 0 on invalid.
709
+ pub unsafe extern "thiscall" fn stop_channel(this: *mut DSSound, pool_id: i32) -> u32 {
 
 
710
  let idx = (pool_id - 1) as usize;
711
  if idx >= 64 {
712
  return 0;
 
771
  path: *const u8,
772
  ) -> u32 {
773
  use windows::Win32::Media::Audio::DirectSound::{
774
+ IDirectSound, IDirectSoundBuffer, DSBLOCK_ENTIREBUFFER, DSBUFFERDESC,
 
775
  };
776
  use windows::Win32::Media::Audio::{WAVEFORMATEX, WAVE_FORMAT_PCM};
777
 
 
839
  // Lock the buffer and fill with PCM data.
840
  let mut audio_ptr1: *mut core::ffi::c_void = core::ptr::null_mut();
841
  let mut audio_len1: u32 = 0;
842
+ if buf
843
+ .Lock(
844
+ 0,
845
+ data_len,
846
+ &mut audio_ptr1,
847
+ &mut audio_len1,
848
+ None,
849
+ None,
850
+ DSBLOCK_ENTIREBUFFER,
851
+ )
852
+ .is_err()
853
+ {
854
  return 0;
855
  }
856
 
 
867
  16 => {
868
  let mut reader = match hound::WavReader::open(c_path) {
869
  Ok(r) => r,
870
+ Err(_) => {
871
+ let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0);
872
+ return 0;
873
+ }
874
  };
875
  let mut offset = 0usize;
876
  for sample in reader.samples::<i16>() {
 
885
  8 => {
886
  let mut reader = match hound::WavReader::open(c_path) {
887
  Ok(r) => r,
888
+ Err(_) => {
889
+ let _ = buf.Unlock(audio_ptr1, audio_len1, None, 0);
890
+ return 0;
891
+ }
892
  };
893
  let mut offset = 0usize;
894
  for sample in reader.samples::<i16>() {
crates/openwa-core/src/audio/mod.rs CHANGED
@@ -6,16 +6,14 @@ pub mod speech;
6
  pub mod streaming;
7
 
8
  pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
9
- pub use dssound::{DSSound, DSSoundVtable, ChannelDescriptor};
10
  pub use dssound::{
11
- destructor as dssound_destructor,
12
- update_channels, release_finished,
13
- is_slot_loaded, is_channel_finished, stop_channel,
14
- play_sound, play_sound_pooled,
15
- set_volume_params, set_master_volume, set_channel_volume, set_pan,
16
- sub_destructor as dssound_sub_destructor, load_wav,
17
- noop as dssound_noop, returns_0 as dssound_returns_0, returns_1 as dssound_returns_1,
18
  };
 
19
  pub use music::Music;
20
  pub use sound::SoundId;
21
  pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
 
6
  pub mod streaming;
7
 
8
  pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
 
9
  pub use dssound::{
10
+ destructor as dssound_destructor, is_channel_finished, is_slot_loaded, load_wav,
11
+ noop as dssound_noop, play_sound, play_sound_pooled, release_finished,
12
+ returns_0 as dssound_returns_0, returns_1 as dssound_returns_1, set_channel_volume,
13
+ set_master_volume, set_pan, set_volume_params, stop_channel,
14
+ sub_destructor as dssound_sub_destructor, update_channels,
 
 
15
  };
16
+ pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
17
  pub use music::Music;
18
  pub use sound::SoundId;
19
  pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
crates/openwa-core/src/display/palette.rs CHANGED
@@ -49,9 +49,15 @@ impl Palette {
49
  }
50
 
51
  /// Vtable[4]: Reset palette state.
52
- pub unsafe fn reset(&mut self) { vcall!(self, reset) }
 
 
53
  /// Vtable[3]: Initialize palette.
54
- pub unsafe fn init(&mut self) { vcall!(self, init) }
 
 
55
  /// Vtable[2]: Set palette mode.
56
- pub unsafe fn set_mode(&mut self, mode: u32) { vcall!(self, set_mode, mode) }
 
 
57
  }
 
49
  }
50
 
51
  /// Vtable[4]: Reset palette state.
52
+ pub unsafe fn reset(&mut self) {
53
+ vcall!(self, reset)
54
+ }
55
  /// Vtable[3]: Initialize palette.
56
+ pub unsafe fn init(&mut self) {
57
+ vcall!(self, init)
58
+ }
59
  /// Vtable[2]: Set palette mode.
60
+ pub unsafe fn set_mode(&mut self, mode: u32) {
61
+ vcall!(self, set_mode, mode)
62
+ }
63
  }
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -1,14 +1,19 @@
 
1
  use crate::audio::active_sound::ActiveSoundTable;
2
- use crate::display::dd_display::DDDisplay;
3
- use crate::input::keyboard::DDKeyboard;
4
  use crate::audio::dssound::DSSound;
5
- use crate::engine::game_info::GameInfo;
6
- use crate::render::landscape::PCLandscape;
7
  use crate::audio::music::Music;
 
 
8
  use crate::display::palette::Palette;
 
 
 
 
 
 
9
  use crate::render::queue::RenderQueue;
10
- use crate::audio::speech::SpeechSlotTable;
11
  use crate::render::turn_order::TurnOrderWidget;
 
12
 
13
  /// DDGame — the main game engine object.
14
  ///
@@ -346,6 +351,1769 @@ pub struct DDGame {
346
 
347
  const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  /// Well-known byte offsets into DDGame, for use with raw pointer access.
350
  ///
351
  /// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
@@ -500,7 +2268,6 @@ const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
500
  // Team arena state — sub-struct at DDGame + 0x4628
501
  // ============================================================
502
 
503
-
504
  // ============================================================
505
  // Per-worm and per-team block structs
506
  // ============================================================
@@ -673,7 +2440,6 @@ pub struct TeamArenaState {
673
  pub game_phase: i32,
674
 
675
  // === Alliance tracking (set by CountTeamsByAlliance + SetActiveWorm_Maybe) ===
676
-
677
  /// 0x2C2C: Current alliance being evaluated
678
  pub current_alliance: i32,
679
  /// 0x2C30: Count of teams with an active worm (set by SetActiveWorm_Maybe)
@@ -721,7 +2487,6 @@ pub mod worm {
721
  pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
722
  pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
723
 
724
-
725
  impl TeamArenaState {
726
  /// Get ammo count for a weapon slot (by flat index).
727
  pub fn get_ammo(&self, index: usize) -> i32 {
@@ -764,7 +2529,9 @@ impl TeamArenaRef {
764
  /// `base` must point to DDGame + TEAM_ARENA_STATE (0x4628).
765
  #[inline]
766
  pub unsafe fn from_raw(base: u32) -> Self {
767
- Self { base: base as *const u8 }
 
 
768
  }
769
 
770
  /// Access the TeamArenaState fields (read-only).
@@ -804,7 +2571,8 @@ impl TeamArenaRef {
804
  /// (0x6C+) that don't conflict with worm data (0x00-0x5F).
805
  #[inline]
806
  pub unsafe fn team_worm(&self, team_idx: usize, worm_num: usize) -> &WormEntry {
807
- let ptr = self.base
 
808
  .add(team_idx * 0x51C)
809
  .add(worm_num * 0x9C)
810
  .sub(0x598);
@@ -839,7 +2607,9 @@ impl TeamArenaRef {
839
  /// Get mutable team header for Pattern B access.
840
  #[inline]
841
  pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamBlockHeader {
842
- &mut (*(self.blocks() as *mut FullTeamBlock).add(team_idx + 2)).header.team
 
 
843
  }
844
 
845
  /// Compute the flat index for ammo/delay table access.
 
1
+ use crate::address::va;
2
  use crate::audio::active_sound::ActiveSoundTable;
 
 
3
  use crate::audio::dssound::DSSound;
 
 
4
  use crate::audio::music::Music;
5
+ use crate::audio::speech::SpeechSlotTable;
6
+ use crate::display::dd_display::DDDisplay;
7
  use crate::display::palette::Palette;
8
+ use crate::engine::ddgame_wrapper::DDGameWrapper;
9
+ use crate::engine::game_info::GameInfo;
10
+ use crate::input::keyboard::DDKeyboard;
11
+ use crate::log::log_line;
12
+ use crate::rebase::rb;
13
+ use crate::render::landscape::PCLandscape;
14
  use crate::render::queue::RenderQueue;
 
15
  use crate::render::turn_order::TurnOrderWidget;
16
+ use crate::wa_alloc::wa_malloc;
17
 
18
  /// DDGame — the main game engine object.
19
  ///
 
351
 
352
  const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
353
 
354
+ // ============================================================
355
+ // DDGame constructor — replaces DDGame__Constructor (0x56E220)
356
+ // ============================================================
357
+ //
358
+ // Despite being named DDGame__Constructor, the original function
359
+ // receives DDGameWrapper* as `this` and creates DDGame internally.
360
+ // It populates fields on BOTH the wrapper and the inner DDGame.
361
+ // The Rust entry point is DDGameWrapper::create_game() in
362
+ // ddgame_wrapper.rs; the bulk of the logic lives here because
363
+ // it primarily initializes DDGame fields.
364
+
365
+ // ============================================================
366
+ // Pure Rust implementations of DDGame sub-functions
367
+ // ============================================================
368
+ // These are called both by create_ddgame() and by MinHook
369
+ // trampolines in openwa-wormkit/replacements/ddgame_init.rs.
370
+
371
+ /// Pure Rust implementation of DDGame__InitFields (0x526120).
372
+ ///
373
+ /// Zeroes stride-0x194 table entries, calls init_render_indices,
374
+ /// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
375
+ ///
376
+ /// # Safety
377
+ /// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98B8 bytes).
378
+ pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
379
+ let base = ddgame as *mut u8;
380
+
381
+ // Zero the stride-0x194 table (10 entries)
382
+ for &off in &[
383
+ 0x379Cusize,
384
+ 0x3930,
385
+ 0x3AC4,
386
+ 0x3C58,
387
+ 0x3DEC,
388
+ 0x3F80,
389
+ 0x4114,
390
+ 0x42A8,
391
+ 0x443C,
392
+ 0x45D0,
393
+ ] {
394
+ *(base.add(off as usize) as *mut u32) = 0;
395
+ }
396
+
397
+ *(base.add(0x64D8) as *mut u32) = 0;
398
+ *(base.add(0x72A4) as *mut u32) = 0;
399
+
400
+ // InitRenderIndices — original sets ESI = ddgame + 0x72D8 before calling
401
+ ddgame_init_render_indices(base.add(0x72D8));
402
+
403
+ // Zero 8 fields at 0x8Cxx
404
+ for &off in &[
405
+ 0x8CBCusize,
406
+ 0x8CC0,
407
+ 0x8CCC,
408
+ 0x8CD0,
409
+ 0x8CDC,
410
+ 0x8CE0,
411
+ 0x8CEC,
412
+ 0x8CF0,
413
+ ] {
414
+ *(base.add(off as usize) as *mut u32) = 0;
415
+ }
416
+
417
+ // Zero 8 fields at 0x98xx
418
+ for &off in &[
419
+ 0x9850usize,
420
+ 0x9854,
421
+ 0x9860,
422
+ 0x9864,
423
+ 0x9870,
424
+ 0x9874,
425
+ 0x9880,
426
+ 0x9884,
427
+ ] {
428
+ *(base.add(off as usize) as *mut u32) = 0;
429
+ }
430
+ }
431
+
432
+ /// Pure Rust implementation of DDGame__InitRenderIndices (0x526080).
433
+ ///
434
+ /// Convention: usercall(ESI=base_ptr), plain RET.
435
+ ///
436
+ /// **Important:** The base pointer is NOT the DDGame pointer!
437
+ /// InitFields calls this with ESI = ddgame + 0x72D8.
438
+ /// All offsets are relative to whatever ESI points to.
439
+ ///
440
+ /// Absolute DDGame offsets (base = ddgame+0x72D8):
441
+ /// - base+0xC4 = ddgame+0x739C
442
+ /// - base+0xD8 = ddgame+0x73B0 (eh_vector_constructor_iterator region)
443
+ /// - base+0x378 = ddgame+0x7650 (team_index_map_1)
444
+ /// - base+0x3DC = ddgame+0x76B4 (team_index_map_2)
445
+ /// - base+0x440 = ddgame+0x7718 (team_index_map_3)
446
+ ///
447
+ /// # Safety
448
+ /// `base` must point to a valid memory region with at least 0x4A4 bytes.
449
+ pub unsafe fn ddgame_init_render_indices(base: *mut u8) {
450
+ // Zero base+0xC4
451
+ *(base.add(0xC4) as *mut u32) = 0;
452
+
453
+ // eh_vector_constructor_iterator equivalent:
454
+ // FUN_525F40 is fastcall { *param_1 = 0; }
455
+ // 14 entries at stride 0x14 starting from +0xD8
456
+ for i in 0..14usize {
457
+ *(base.add(0xD8 + i * 0x14) as *mut u32) = 0;
458
+ }
459
+
460
+ // Identity permutation 1: base+0x378 (= ddgame+0x7650), 16 entries (i16)
461
+ for i in 0..16i16 {
462
+ *(base.add(0x378 + i as usize * 2) as *mut i16) = i;
463
+ }
464
+ *(base.add(0x398) as *mut u16) = 0x10;
465
+ *(base.add(0x3DA) as *mut u16) = 0;
466
+
467
+ // Identity permutation 2: base+0x3DC (= ddgame+0x76B4), 16 entries (i16)
468
+ for i in 0..16i16 {
469
+ *(base.add(0x3DC + i as usize * 2) as *mut i16) = i;
470
+ }
471
+ *(base.add(0x3FC) as *mut u16) = 0x10;
472
+ *(base.add(0x43E) as *mut u16) = 0;
473
+
474
+ // Identity permutation 3: base+0x440 (= ddgame+0x7718), 16 entries (i16)
475
+ for i in 0..16i16 {
476
+ *(base.add(0x440 + i as usize * 2) as *mut i16) = i;
477
+ }
478
+ *(base.add(0x4A2) as *mut u16) = 0;
479
+ *(base.add(0x460) as *mut u16) = 0x10;
480
+ }
481
+
482
+ /// Pure Rust implementation of TaskStateMachine__Init (0x4F6370).
483
+ ///
484
+ /// Convention: usercall(ESI=object, ECX=param1, EDI=height) + 1 stack(width), RET 0x4.
485
+ ///
486
+ /// Allocates a bit-per-cell grid buffer. `param1` is typically 1 (cells per unit).
487
+ /// `width` and `height` are pixel dimensions. The buffer is a row-major bitfield
488
+ /// with rows aligned to 4 bytes.
489
+ ///
490
+ /// # Safety
491
+ /// `object` must point to a zero-filled allocation of at least 0x2C bytes.
492
+ pub unsafe fn task_state_machine_init(object: *mut u8, param1: u32, width: u32, height: u32) {
493
+ // Row stride: bits-to-bytes rounded up, then aligned to 4
494
+ let bits = param1.wrapping_mul(width).wrapping_add(7) as i32;
495
+ let row_stride = ((bits >> 3) + 3) & !3;
496
+ let total_size = row_stride as u32 * height;
497
+
498
+ // Allocate data buffer with 0x20-byte header
499
+ let alloc_size = ((total_size + 3) & !3) + 0x20;
500
+ let buffer = wa_malloc(alloc_size);
501
+
502
+ let _ = crate::log::log_line(&format!(
503
+ "[TSM] init: obj=0x{:08X} p1={} w={} h={} stride={} total={} alloc={} buf=0x{:08X}",
504
+ object as u32, param1, width, height, row_stride, total_size, alloc_size, buffer as u32,
505
+ ));
506
+
507
+ if buffer.is_null() {
508
+ let _ = crate::log::log_line("[TSM] WARN: wa_malloc returned null, skipping memset");
509
+ return;
510
+ }
511
+ // Guard against integer overflow producing tiny alloc_size with huge total_size
512
+ if total_size as usize > alloc_size as usize {
513
+ let _ = crate::log::log_line(&format!(
514
+ "[TSM] OVERFLOW: total_size {} > alloc_size {}, skipping memset",
515
+ total_size, alloc_size,
516
+ ));
517
+ return;
518
+ }
519
+
520
+ // Memset twice (matches original — likely redundant but exact match)
521
+ core::ptr::write_bytes(buffer, 0, total_size as usize);
522
+ core::ptr::write_bytes(buffer, 0, total_size as usize);
523
+
524
+ // Set object fields (DWORD array layout)
525
+ let obj = object as *mut u32;
526
+ *obj.add(0) = rb(0x6640EC); // vtable
527
+ *obj.add(1) = 0; // unused
528
+ *obj.add(2) = buffer as u32; // data pointer
529
+ *obj.add(3) = param1; // param1
530
+ *obj.add(4) = row_stride as u32; // row stride
531
+ *obj.add(5) = width; // width
532
+ *obj.add(6) = height; // height
533
+ *obj.add(7) = 0; // unused
534
+ *obj.add(8) = 0; // unused
535
+ *obj.add(9) = width; // width (duplicate)
536
+ *obj.add(10) = height; // height (duplicate)
537
+ }
538
+
539
+ /// Pure Rust implementation of FUN_570E20 (display layer color init).
540
+ ///
541
+ /// Convention: usercall(ESI=wrapper), plain RET.
542
+ ///
543
+ /// Sets display layer color parameters via display->vtable[4](layer, color).
544
+ /// Layer 1 color depends on gfx_mode and game_version.
545
+ /// Layer 2 = 0x20, Layer 3 = 0x70.
546
+ ///
547
+ /// # Safety
548
+ /// `wrapper` must be a valid DDGameWrapper with initialized display and ddgame.
549
+ #[cfg(target_arch = "x86")]
550
+ pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) {
551
+ let ddgame = (*wrapper).ddgame;
552
+ let game_info = (*ddgame).game_info;
553
+ let game_version = (*game_info).game_version;
554
+
555
+ // wrapper+0x4C8 is gfx_mode (at DDGameWrapper.gfx_mode)
556
+ let layer1_color = if (*wrapper).gfx_mode == 0 {
557
+ // (game_version > -2) - 1: yields 0 if true, -1 if false
558
+ // Then + 0x69: yields 0x69 or 0x68
559
+ if game_version > -2 {
560
+ 0x69i32
561
+ } else {
562
+ 0x68i32
563
+ }
564
+ } else {
565
+ 5 + 0x69 // = 0x6E
566
+ };
567
+
568
+ let display = (*wrapper).display as *mut u8;
569
+ let vt = *(display as *const *const u32);
570
+ // vtable[4] (offset 0x10): thiscall(display, layer, color), RET 0x8
571
+ let set_color: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
572
+ core::mem::transmute(*vt.add(4));
573
+
574
+ set_color(display, 1, layer1_color);
575
+ set_color(display, 2, 0x20);
576
+ set_color(display, 3, 0x70);
577
+ }
578
+
579
+ /// Hash function for GfxDir entry lookup (FUN_566390).
580
+ ///
581
+ /// 10-bit CRC-like hash over the global name buffer at 0x8ACF58.
582
+ /// Operates on the already-lowercased name.
583
+ ///
584
+ /// # Safety
585
+ /// `name` must be a valid null-terminated C string pointer.
586
+ unsafe fn gfx_dir_hash(name: *const u8) -> u32 {
587
+ let mut hash: u32 = 0;
588
+ let mut p = name;
589
+ while *p != 0 {
590
+ let bit9 = (hash >> 9) & 1;
591
+ hash = ((hash << 1) | bit9) & 0x3FF;
592
+ hash = hash.wrapping_add(*p as u32) & 0x3FF;
593
+ p = p.add(1);
594
+ }
595
+ hash
596
+ }
597
+
598
+ /// Pure Rust implementation of GfxDir__FindEntry (0x566520).
599
+ ///
600
+ /// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4.
601
+ ///
602
+ /// Looks up a name in the GfxHandler's hash table. Names are case-insensitive.
603
+ /// Supports `|`-separated fallback names (e.g. "path\\file.img|fallback.img").
604
+ ///
605
+ /// Returns entry pointer or null. Entry layout:
606
+ /// - entry+0x00: next pointer (linked list)
607
+ /// - entry+0x04: value (passed to vtable[2] for cached load)
608
+ /// - entry+0x08: unknown
609
+ /// - entry+0x0C: name string (null-terminated, lowercase)
610
+ ///
611
+ /// # Safety
612
+ /// `gfx_handler` must be a valid GfxHandler with initialized hash table at +0x04.
613
+ /// `name` must be a valid null-terminated C string.
614
+ #[cfg(target_arch = "x86")]
615
+ pub unsafe fn gfx_dir_find_entry(name: *const u8, gfx_handler: *mut u8) -> *mut u8 {
616
+ let mut current_name = name;
617
+
618
+ loop {
619
+ // Copy name to stack buffer (max 0xFF chars + null)
620
+ let mut buf = [0u8; 0x100];
621
+ let mut i = 0usize;
622
+ let mut src = current_name;
623
+ while *src != 0 && i < 0xFF {
624
+ buf[i] = *src;
625
+ src = src.add(1);
626
+ i += 1;
627
+ }
628
+ buf[i] = 0;
629
+
630
+ // Find '|' separator in buffer
631
+ let mut pipe_pos: Option<usize> = None;
632
+ for j in 0..i {
633
+ if buf[j] == b'|' {
634
+ buf[j] = 0; // truncate at pipe
635
+ pipe_pos = Some(j);
636
+ break;
637
+ }
638
+ }
639
+
640
+ // Compute next_name: pointer into original string after '|'
641
+ let next_name: *const u8 = if let Some(pos) = pipe_pos {
642
+ // Offset of '|' in buffer = pos
643
+ // Same offset in original string → current_name + pos + 1
644
+ current_name.add(pos + 1)
645
+ } else {
646
+ core::ptr::null()
647
+ };
648
+
649
+ // Lowercase A-Z in buffer
650
+ for j in 0..buf.len() {
651
+ if buf[j] == 0 {
652
+ break;
653
+ }
654
+ if buf[j] >= b'A' && buf[j] <= b'Z' {
655
+ buf[j] += 0x20;
656
+ }
657
+ }
658
+
659
+ // Hash the lowercased name
660
+ let bucket = gfx_dir_hash(buf.as_ptr());
661
+
662
+ // Walk linked list in hash bucket
663
+ // gfx_handler+4 = pointer to bucket array (1024 entries)
664
+ let bucket_array = *(gfx_handler.add(4) as *const *const u32);
665
+ let mut entry = *bucket_array.add(bucket as usize) as *mut u8;
666
+
667
+ while !entry.is_null() {
668
+ // Compare entry name at entry+0x0C with our buffer
669
+ let entry_name = entry.add(0x0C);
670
+ let mut match_found = true;
671
+ let mut k = 0usize;
672
+ loop {
673
+ let a = *entry_name.add(k);
674
+ let b = buf[k];
675
+ if a != b {
676
+ match_found = false;
677
+ break;
678
+ }
679
+ if a == 0 {
680
+ break;
681
+ }
682
+ k += 1;
683
+ }
684
+
685
+ if match_found {
686
+ return entry;
687
+ }
688
+
689
+ // Next in linked list: entry+0x00
690
+ entry = *(entry as *const *mut u8);
691
+ }
692
+
693
+ // Not found — try fallback name after '|'
694
+ if next_name.is_null() {
695
+ return core::ptr::null_mut();
696
+ }
697
+ current_name = next_name;
698
+ }
699
+ }
700
+
701
+ /// Pure Rust implementation of FUN_5665F0 (GfxHandler reset).
702
+ ///
703
+ /// Convention: usercall(ESI=handler), plain RET. Called at start of LoadDir.
704
+ unsafe fn gfx_handler_reset(handler: *mut u8) {
705
+ *(handler.add(0x04) as *mut u32) = 0;
706
+ *(handler.add(0x08) as *mut u32) = 0;
707
+ *(handler.add(0x194) as *mut u32) = 0;
708
+
709
+ for i in 0..16u32 {
710
+ // handler+0x0C + i*0x10 = 0 (16 entries at stride 0x10)
711
+ *(handler.add(0x0C + i as usize * 0x10) as *mut u32) = 0;
712
+ // handler+0x10C + i*4 = i (index array)
713
+ *(handler.add(0x10C + i as usize * 4) as *mut u32) = i;
714
+ }
715
+
716
+ *(handler.add(400) as *mut u32) = 0; // 0x190
717
+ *(handler.add(0x18C) as *mut u32) = 0x10;
718
+ }
719
+
720
+ /// Pure Rust implementation of GfxHandler__LoadDir (0x5663E0).
721
+ ///
722
+ /// Convention: usercall(EAX=handler), plain RET. Returns 1 on success, 0 on failure.
723
+ ///
724
+ /// Reads a .dir file through the handler's vtable I/O methods:
725
+ /// - vtable[0]: read(buf, size) → thiscall, returns bytes read
726
+ /// - vtable[1]: seek/reposition(size) → thiscall
727
+ /// - vtable[2]: allocate(size) → thiscall, returns buffer or null
728
+ ///
729
+ /// .dir file format:
730
+ /// - 4 bytes: magic "DIR\x1A" (0x1A524944)
731
+ /// - 4 bytes: total_file_size
732
+ /// - 4 bytes: data_size (hash table + entries)
733
+ /// - data: 1024-bucket hash table followed by linked list nodes
734
+ /// All pointers are relative offsets from (data_start + 4)
735
+ ///
736
+ /// # Safety
737
+ /// `handler` must be a valid GfxHandler with file handle at +0x198.
738
+ #[cfg(target_arch = "x86")]
739
+ pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
740
+ gfx_handler_reset(handler);
741
+
742
+ let vt = *(handler as *const *const u32);
743
+ // vtable[0] = read: thiscall(handler, buf, size) -> bytes_read
744
+ let vt_read: unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) -> u32 =
745
+ core::mem::transmute(*vt);
746
+ // vtable[1] = seek: thiscall(handler, size)
747
+ let vt_seek: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*vt.add(1));
748
+ // vtable[2] = allocate: thiscall(handler, size) -> *mut u8
749
+ let vt_alloc: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
750
+ core::mem::transmute(*vt.add(2));
751
+
752
+ // Read and validate magic
753
+ let mut magic: u32 = 0;
754
+ if vt_read(handler, &mut magic as *mut u32 as *mut u8, 4) != 4 {
755
+ return 0;
756
+ }
757
+ if magic != 0x1A524944 {
758
+ // "DIR\x1A"
759
+ return 0;
760
+ }
761
+
762
+ // Read total_file_size and data_size
763
+ let mut total_file_size: u32 = 0;
764
+ if vt_read(handler, &mut total_file_size as *mut u32 as *mut u8, 4) != 4 {
765
+ return 0;
766
+ }
767
+ let mut data_size: u32 = 0;
768
+ if vt_read(handler, &mut data_size as *mut u32 as *mut u8, 4) != 4 {
769
+ return 0;
770
+ }
771
+
772
+ let alloc_size = data_size + 4;
773
+
774
+ // Try fast path: vtable[2] allocate (memory-maps the data)
775
+ let data = vt_alloc(handler, alloc_size);
776
+ *(handler.add(4) as *mut *mut u8) = data;
777
+
778
+ if data.is_null() {
779
+ // Fallback: seek past header, then malloc + read entire data block.
780
+ // Original calls vt_seek(alloc_size) then WA_MallocMemset(EDI=read_size).
781
+ // WA_MallocMemset allocates (size+3)&~3 + 0x20 and memsets size bytes.
782
+ vt_seek(handler, alloc_size);
783
+
784
+ let read_size = total_file_size - data_size - 4;
785
+ let malloc_size = ((read_size + 3) & !3) + 0x20;
786
+ let buf = wa_malloc(malloc_size);
787
+ if buf.is_null() {
788
+ return 0;
789
+ }
790
+ core::ptr::write_bytes(buf, 0, read_size as usize);
791
+ *(handler.add(4) as *mut *mut u8) = buf;
792
+
793
+ let bytes_read = vt_read(handler, buf, read_size);
794
+ if bytes_read != read_size {
795
+ crate::wa_alloc::wa_free(buf);
796
+ return 0;
797
+ }
798
+
799
+ // Mark as "loaded from fallback" at handler+8
800
+ *(handler.add(8) as *mut u32) = 1;
801
+ }
802
+
803
+ // Fix up relative pointers in the hash table
804
+ // 1024 buckets at data[0..0x1000], each is a pointer to a linked list node
805
+ let data = *(handler.add(4) as *const *mut u8);
806
+ let base = data as u32;
807
+
808
+ for bucket in 0..1024u32 {
809
+ let bucket_ptr = data.add(bucket as usize * 4) as *mut u32;
810
+ let entry_offset = *bucket_ptr;
811
+ if entry_offset == 0 {
812
+ continue;
813
+ }
814
+
815
+ // Convert relative offset to absolute: offset + base - 4
816
+ let entry_addr = entry_offset.wrapping_add(base).wrapping_sub(4);
817
+ *bucket_ptr = entry_addr;
818
+
819
+ // Walk linked list, fix up each next pointer
820
+ let mut node = entry_addr as *mut u32;
821
+ loop {
822
+ if node.is_null() {
823
+ break;
824
+ }
825
+ let next_offset = *node;
826
+ if next_offset == 0 {
827
+ break;
828
+ }
829
+ let next_addr = next_offset.wrapping_add(base).wrapping_sub(4);
830
+ *node = next_addr;
831
+ node = next_addr as *mut u32;
832
+ }
833
+ }
834
+
835
+ // Mark as loaded
836
+ *(handler.add(0x194) as *mut u32) = 1;
837
+
838
+ 1 // success
839
+ }
840
+
841
+ /// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
842
+ ///
843
+ /// Convention: usercall(ECX=gfx_handler, EAX=name) + 1 stack(output), RET 0x4.
844
+ ///
845
+ /// Looks up `name` in the GfxHandler's directory, tries cached load, wraps
846
+ /// as DisplayGfx. Falls back to loading the raw image and decoding it.
847
+ ///
848
+ /// # Safety
849
+ /// All pointers must be valid WA objects.
850
+ #[cfg(target_arch = "x86")]
851
+ pub unsafe fn gfx_resource_create(
852
+ gfx_handler: *mut u8,
853
+ name: *const u8,
854
+ output: *mut u8,
855
+ ) -> *mut u8 {
856
+ let _ = crate::log::log_line(&format!(
857
+ "[GfxRes] create: handler=0x{:08X} name=0x{:08X} out=0x{:08X}",
858
+ gfx_handler as u32, name as u32, output as u32,
859
+ ));
860
+ // 1. Try FindEntry → cached load → DisplayGfx wrap
861
+ let entry = gfx_dir_find_entry(name, gfx_handler);
862
+ let _ = crate::log::log_line(&format!("[GfxRes] entry=0x{:08X}", entry as u32));
863
+ if !entry.is_null() {
864
+ // gfx_handler->vtable[2](entry->field_4) — cached load
865
+ let vt = *(gfx_handler as *const *const u32);
866
+ let _ = crate::log::log_line(&format!("[GfxRes] vt=0x{:08X}", vt as u32));
867
+ let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
868
+ core::mem::transmute(*vt.add(2));
869
+ let entry_val = *(entry.add(4) as *const u32);
870
+ let cached = load_cached(gfx_handler, entry_val);
871
+ let _ = crate::log::log_line(&format!("[GfxRes] cached=0x{:08X}", cached as u32));
872
+ if !cached.is_null() {
873
+ // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
874
+ let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
875
+ core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
876
+ return ctor(cached);
877
+ }
878
+ }
879
+
880
+ // 2. Fallback: LoadImage → IMG_Decode
881
+ let _ = crate::log::log_line("[GfxRes] fallback: LoadImage");
882
+ let raw_image = call_gfx_load_image(gfx_handler, name);
883
+ let _ = crate::log::log_line(&format!("[GfxRes] raw_image=0x{:08X}", raw_image as u32));
884
+ if raw_image.is_null() {
885
+ return core::ptr::null_mut();
886
+ }
887
+
888
+ // IMG_Decode: stdcall(output, raw_image, 1), RET 0xC
889
+ let _ = crate::log::log_line("[GfxRes] calling IMG_Decode");
890
+ let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 =
891
+ core::mem::transmute(rb(va::IMG_DECODE) as usize);
892
+ let result = decode(output, raw_image, 1);
893
+ let _ = crate::log::log_line(&format!(
894
+ "[GfxRes] IMG_Decode result=0x{:08X}",
895
+ result as u32
896
+ ));
897
+
898
+ // Release raw image: raw_image->vtable[0](1)
899
+ let img_vt = *(raw_image as *const *const u32);
900
+ let fn_ptr = *img_vt;
901
+ let slot_idx = *(raw_image.add(4) as *const u32);
902
+ let stored_handler = *(raw_image.add(8) as *const u32);
903
+ let _ = crate::log::log_line(&format!(
904
+ "[GfxRes] release: raw=0x{:08X} vt=0x{:08X} fn=0x{:08X} slot={} handler=0x{:08X}",
905
+ raw_image as u32, img_vt as u32, fn_ptr as u32, slot_idx, stored_handler,
906
+ ));
907
+ let _ = crate::log::log_line(&format!(
908
+ "[GfxRes] handler_usedcnt={} handler_freecnt={}",
909
+ *(gfx_handler.add(0x190) as *const u32),
910
+ *(gfx_handler.add(0x18C) as *const u32),
911
+ ));
912
+ let release: unsafe extern "thiscall" fn(*mut u8, i32) = core::mem::transmute(fn_ptr);
913
+ let _ = crate::log::log_line("[GfxRes] calling release...");
914
+ release(raw_image, 1);
915
+ let _ = crate::log::log_line("[GfxRes] release done");
916
+ let _ = crate::log::log_line(&format!(
917
+ "[GfxRes] heap-ok raw=0x{:08X} result=0x{:08X}",
918
+ raw_image as u32, result as u32
919
+ ));
920
+ let _ = crate::log::log_line("[GfxRes] before return");
921
+
922
+ result
923
+ }
924
+
925
+ /// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
926
+ static mut INIT_FIELDS_ADDR: u32 = 0;
927
+ static mut GFX_LOAD_DIR_ADDR: u32 = 0;
928
+
929
+ /// Initialize runtime addresses for the constructor bridges.
930
+ /// Must be called once at DLL startup (from lib.rs or similar).
931
+ pub fn init_constructor_addrs() {
932
+ unsafe {
933
+ INIT_FIELDS_ADDR = rb(va::DDGAME_INIT_FIELDS);
934
+ SPRITE_REGION_CTOR_ADDR = rb(va::SPRITE_REGION_CONSTRUCTOR);
935
+ FUN_570E20_ADDR = rb(va::FUN_570E20);
936
+ FUN_570A90_ADDR = rb(va::FUN_570A90);
937
+ GFX_RESOURCE_CREATE_ADDR = rb(va::GFX_RESOURCE_CREATE);
938
+ TSM_INIT_ADDR = rb(va::TASK_STATE_MACHINE_INIT);
939
+ GFX_FIND_ENTRY_ADDR = rb(va::GFX_DIR_FIND_ENTRY);
940
+ GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
941
+ GFX_LOAD_DIR_ADDR = rb(va::GFX_HANDLER_LOAD_DIR);
942
+ FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
943
+ LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
944
+ LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
945
+ }
946
+ }
947
+
948
+ /// Bridge to DDGame__InitFields (0x526120).
949
+ /// Convention: usercall(EDI=ddgame), plain RET.
950
+ #[cfg(target_arch = "x86")]
951
+ #[unsafe(naked)]
952
+ unsafe extern "C" fn call_init_fields(_ddgame: *mut DDGame) {
953
+ core::arch::naked_asm!(
954
+ "pushl %edi",
955
+ "movl 8(%esp), %edi",
956
+ "calll *({addr})",
957
+ "popl %edi",
958
+ "retl",
959
+ addr = sym INIT_FIELDS_ADDR,
960
+ options(att_syntax),
961
+ );
962
+ }
963
+
964
+ /// Bridge to DDGame__InitVersionFlags (0x525BE0).
965
+ /// Convention: stdcall(ddgame_wrapper).
966
+ #[cfg(target_arch = "x86")]
967
+ unsafe fn call_init_version_flags(wrapper: *mut DDGameWrapper) {
968
+ let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
969
+ core::mem::transmute(rb(va::DDGAME_INIT_VERSION_FLAGS) as usize);
970
+ f(wrapper);
971
+ }
972
+
973
+ /// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
974
+ ///
975
+ /// Allocates 0x98B8 bytes from WA's heap, initializes all fields, and creates
976
+ /// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the
977
+ /// returned DDGame.
978
+ ///
979
+ /// # Safety
980
+ /// All pointer params must be valid WA objects. `wrapper` must be a
981
+ /// partially-initialized DDGameWrapper (vtable, display, sound set).
982
+ #[cfg(target_arch = "x86")]
983
+ pub unsafe fn create_ddgame(
984
+ wrapper: *mut DDGameWrapper,
985
+ keyboard: *mut DDKeyboard,
986
+ display: *mut DDDisplay,
987
+ sound: *mut DSSound,
988
+ palette: *mut Palette,
989
+ music: *mut Music,
990
+ param7: *mut u8, // timer object (0x1F4 observed)
991
+ net_game: *mut u8, // from GameSession
992
+ game_info: *mut GameInfo,
993
+ network_ecx: u32, // implicit ECX from caller
994
+ ) -> *mut DDGame {
995
+ // ── 1. Allocate and zero-fill (matches: memset(piVar3, 0, 0x98B8)) ──
996
+ let ddgame = wa_malloc(0x98B8) as *mut DDGame;
997
+ if ddgame.is_null() {
998
+ return core::ptr::null_mut();
999
+ }
1000
+ core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8);
1001
+
1002
+ // ── 2. InitFields — pure Rust (replaces usercall bridge) ──
1003
+ ddgame_init_fields(ddgame);
1004
+
1005
+ // ── 3-4. Store params BEFORE exposing DDGame via wrapper ──
1006
+ // Critical: game_info must be set before wrapper->ddgame, because the
1007
+ // message pump (triggered by audio loading) can cause game tasks to
1008
+ // read ddgame->game_info. If game_info is null, they crash.
1009
+ (*ddgame).display = display;
1010
+ (*ddgame).sound = sound;
1011
+ (*ddgame).keyboard = keyboard;
1012
+ (*ddgame).palette = palette;
1013
+ (*ddgame).music = music;
1014
+ (*ddgame)._param_018 = param7;
1015
+ (*ddgame)._caller = network_ecx as *mut u8;
1016
+ (*ddgame).game_info = game_info;
1017
+ (*ddgame)._param_028 = net_game;
1018
+
1019
+ // Now safe to expose — all fields that concurrent readers check are set.
1020
+ (*wrapper).ddgame = ddgame;
1021
+
1022
+ // Verify game_info is set
1023
+ let _ = crate::log::log_line(&format!(
1024
+ "[DDGame] exposed: ddgame=0x{:08X}, game_info=0x{:08X}, net_game=0x{:08X}",
1025
+ ddgame as u32, game_info as u32, net_game as u32,
1026
+ ));
1027
+
1028
+ // ── 5. Set g_GameInfo global ──
1029
+ *(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info;
1030
+
1031
+ // ── 6. Sound available + always-1 flags ──
1032
+ let is_headless = (*game_info).headless_mode != 0;
1033
+ // Original: sound_available = (headless_mode == 0) — set EARLY.
1034
+ // This enables the loading progress bar, message pump, and sound during
1035
+ // construction. Previously deferred to avoid message pump crashes, but all
1036
+ // critical DDGame fields (game_info, display, sound, etc.) are now set.
1037
+ (*ddgame).sound_available = if is_headless { 0 } else { 1 };
1038
+ (*ddgame)._field_7efc = 1;
1039
+
1040
+ // ── 7. DDGameWrapper+0x48C init ──
1041
+ (*wrapper).ddgame_secondary = core::ptr::null_mut();
1042
+
1043
+ // ── 8. Conditional network object (game_version == -2) ──
1044
+ if (*game_info).game_version == -2 {
1045
+ let net_obj = wa_malloc(0x2C);
1046
+ core::ptr::write_bytes(net_obj, 0, 0x2C);
1047
+ *(net_obj as *mut *mut DDGame) = ddgame;
1048
+ *net_obj.add(0x28) = (*game_info).net_config_1;
1049
+ *net_obj.add(0x29) = (*game_info).net_config_2;
1050
+ (*wrapper).ddgame_secondary = net_obj;
1051
+ if network_ecx != 0 {
1052
+ *((network_ecx as *mut u8).add(0x18) as *mut *mut u8) = net_obj;
1053
+ }
1054
+ }
1055
+
1056
+ // ── 9. InitVersionFlags — sets DDGame+0x7E2E/0x7E2F/0x7E3F ──
1057
+ let _ = crate::log::log_line("[DDGame] calling InitVersionFlags");
1058
+ call_init_version_flags(wrapper);
1059
+ let _ = crate::log::log_line("[DDGame] calling init_graphics_and_resources");
1060
+
1061
+ // ── 10. GfxHandler, landscape, sprites, audio, resources ──
1062
+ init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless);
1063
+
1064
+ let _ = crate::log::log_line("[DDGame] create_ddgame complete");
1065
+ ddgame
1066
+ }
1067
+
1068
+ /// Second half of the constructor: GfxHandler, landscape, sprites, audio, resources.
1069
+ ///
1070
+ /// Separated for readability. Will be broken into smaller functions as
1071
+ /// individual sections are converted to pure Rust.
1072
+ #[cfg(target_arch = "x86")]
1073
+ unsafe fn init_graphics_and_resources(
1074
+ wrapper: *mut DDGameWrapper,
1075
+ game_info: *mut GameInfo,
1076
+ net_game: *mut u8,
1077
+ display: *mut DDDisplay,
1078
+ is_headless: bool,
1079
+ ) {
1080
+ let ddgame = (*wrapper).ddgame;
1081
+ let fopen: unsafe extern "cdecl" fn(*const u8, *const u8) -> *mut u8 =
1082
+ core::mem::transmute(rb(va::WA_FOPEN) as usize);
1083
+ // gfx_load_dir_addr removed — using direct Rust gfx_handler_load_dir() now
1084
+ let gfx_handler_vtable = rb(0x66B280) as u32; // GfxHandler__vtable (from asm: MOV [ESI], 0x66B280)
1085
+
1086
+ // ── GfxHandler #1 (primary) ──
1087
+ let gfx1 = wa_malloc(0x19C);
1088
+ core::ptr::write_bytes(gfx1, 0, 0x19C);
1089
+ *(gfx1 as *mut u32) = gfx_handler_vtable;
1090
+ (*wrapper)._field_4c0 = gfx1;
1091
+ (*wrapper)._field_4c4 = core::ptr::null_mut();
1092
+
1093
+ // Build path list (order depends on headless + GameInfo+0xF374)
1094
+ let f914 = (*game_info).headless_mode as i32;
1095
+ let f374 = (*game_info).display_flags as i32;
1096
+ let paths: [&[u8]; 3] = if f914 != 0 {
1097
+ if f374 == 0 {
1098
+ [
1099
+ b"data\\Gfx\\Gfx.dir\0",
1100
+ b"data\\Gfx\\Gfx0.dir\0",
1101
+ b"data\\Gfx\\Gfx1.dir\0",
1102
+ ]
1103
+ } else {
1104
+ [
1105
+ b"data\\Gfx\\Gfx.dir\0",
1106
+ b"data\\Gfx\\Gfx1.dir\0",
1107
+ b"data\\Gfx\\Gfx0.dir\0",
1108
+ ]
1109
+ }
1110
+ } else if f374 == 0 {
1111
+ [
1112
+ b"data\\Gfx\\Gfx0.dir\0",
1113
+ b"data\\Gfx\\Gfx1.dir\0",
1114
+ b"data\\Gfx\\Gfx.dir\0",
1115
+ ]
1116
+ } else {
1117
+ [
1118
+ b"data\\Gfx\\Gfx1.dir\0",
1119
+ b"data\\Gfx\\Gfx0.dir\0",
1120
+ b"data\\Gfx\\Gfx.dir\0",
1121
+ ]
1122
+ };
1123
+
1124
+ let mut gfx_loaded_idx = 0u32;
1125
+ for (i, path) in paths.iter().enumerate() {
1126
+ let fp = fopen(path.as_ptr(), b"rb\0".as_ptr());
1127
+ *(gfx1.add(0x198) as *mut *mut u8) = fp;
1128
+ if !fp.is_null() && call_gfx_load_dir(gfx1, GFX_LOAD_DIR_ADDR) != 0 {
1129
+ gfx_loaded_idx = i as u32;
1130
+ break;
1131
+ }
1132
+ if i == 2 {
1133
+ panic!("DDGame: couldn't open any Gfx.dir");
1134
+ }
1135
+ }
1136
+
1137
+ let headless_offset = if f914 != 0 { 1u32 } else { 0u32 };
1138
+ (*wrapper).gfx_mode = if gfx_loaded_idx.wrapping_sub(headless_offset) < 2 {
1139
+ 1
1140
+ } else {
1141
+ 0
1142
+ };
1143
+
1144
+ // ── GfxHandler #2 (conditional) ──
1145
+ let d778 = (*game_info).game_version;
1146
+ // Original: (-(gfx_mode != 0) & 0x23) - 2 → gfx_mode ? 33 : -2
1147
+ let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
1148
+ if d778 < threshold {
1149
+ let c_digit = if d778 > -3 { b'2' } else { b'1' };
1150
+ let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
1151
+ gfx_c_path[14] = c_digit;
1152
+
1153
+ let gfx2 = wa_malloc(0x19C);
1154
+ core::ptr::write_bytes(gfx2, 0, 0x19C);
1155
+ *(gfx2 as *mut u32) = gfx_handler_vtable;
1156
+ (*wrapper)._field_4c4 = gfx2;
1157
+
1158
+ let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr());
1159
+ *(gfx2.add(0x198) as *mut *mut u8) = fp;
1160
+ if fp.is_null() || call_gfx_load_dir(gfx2, GFX_LOAD_DIR_ADDR) == 0 {
1161
+ let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr());
1162
+ *(gfx2.add(0x198) as *mut *mut u8) = fp2;
1163
+ if fp2.is_null() || call_gfx_load_dir(gfx2, GFX_LOAD_DIR_ADDR) == 0 {
1164
+ panic!("DDGame: couldn't open secondary Gfx.dir");
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ // ── Display palette setup (non-headless) ──
1170
+ if !is_headless {
1171
+ if *(rb(0x88E485) as *const u8) == 0 {
1172
+ call_usercall_eax(wrapper, FUN_570A90_ADDR);
1173
+ }
1174
+ // DDGameWrapper+0x4D0 points to the display object used for palette calls.
1175
+ // BUT: FUN_00570A90 or something else may write to wrapper in ways we
1176
+ // don't expect. Read via struct field instead of raw offset.
1177
+ let disp = (*wrapper).display as *mut u8;
1178
+ let gfx_handler = (*wrapper)._field_4c0;
1179
+ let vt = *(disp as *const *const u32);
1180
+ // vtable[4] (offset 0x10): thiscall + 2 stack params (1, 0xFE), RET 0x8
1181
+ let vt_10: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
1182
+ core::mem::transmute(*vt.add(4));
1183
+ vt_10(disp, 1, 0xFE);
1184
+ // vtable[0x1F] (offset 0x7C): thiscall + 5 stack params, RET 0x14
1185
+ let vt_7c: unsafe extern "thiscall" fn(*mut u8, i32, i32, i32, *mut u8, u32) =
1186
+ core::mem::transmute(*vt.add(0x1F));
1187
+ vt_7c(disp, 1, 1, 0, gfx_handler, rb(0x66A3A8));
1188
+ // vtable[0x17] (offset 0x5C): thiscall + 2 stack params, RET 0x8
1189
+ let vt_5c: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
1190
+ core::mem::transmute(*vt.add(0x17));
1191
+ vt_5c(disp, 1, -100);
1192
+
1193
+ // Palette slot range init
1194
+ let palette_range_ptr = *(disp.add(0x3120) as *const *mut i16);
1195
+ if !palette_range_ptr.is_null() && *(disp.add(0x3534) as *const i32) == 0 {
1196
+ let start = *palette_range_ptr as u32;
1197
+ let end = (*palette_range_ptr.add(1) as u32) + 1;
1198
+ if start < end {
1199
+ for i in start..end {
1200
+ *(disp.add(0x312C + i as usize * 4) as *mut u32) = 1;
1201
+ }
1202
+ }
1203
+ crate::wa_alloc::wa_free(*(disp.add(0x3120) as *const *mut u8));
1204
+ *(disp.add(0x3120) as *mut u32) = 0;
1205
+ }
1206
+ }
1207
+
1208
+ // ── FUN_00570E20: usercall(ESI=wrapper), plain RET ──
1209
+ // Runs for all modes — headless vtable[4] is 0x5231E0 (same as headful).
1210
+ display_layer_color_init(wrapper);
1211
+
1212
+ // ── Display vtable slot 5 (offset 0x14) ──
1213
+ // Original: CALL EAX (vtable[5]), saves return value in ESI for use as
1214
+ // the `output` parameter in the color-entries GfxResource__Create call below.
1215
+ let layer_ctx = {
1216
+ let vt = *((*ddgame).display as *const *const u32);
1217
+ let f: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 =
1218
+ core::mem::transmute(*vt.add(5));
1219
+ f((*ddgame).display, 1)
1220
+ };
1221
+
1222
+ let _ = crate::log::log_line(&format!("[DDGame] gfx_mode={}", (*wrapper).gfx_mode));
1223
+ // ── GfxDir color entries DDGame+0x730C..0x732C ──
1224
+ if (*wrapper).gfx_mode != 0 {
1225
+ // Original: PUSH ESI (=layer_ctx from vtable[5] call), MOV EAX=0x66A3B4, CALL 0x4F6300
1226
+ // The layer_ctx is used as the output buffer, not a plain stack alloc.
1227
+ let res = gfx_resource_create((*wrapper)._field_4c0, rb(0x66A3B4) as *const u8, layer_ctx);
1228
+ if !res.is_null() {
1229
+ let rvt = *(res as *const *const u32);
1230
+ let _ = crate::log::log_line(&format!(
1231
+ "[DDGame] color entries: res=0x{:08X} vt=0x{:08X} vt[4]=0x{:08X}",
1232
+ res as u32,
1233
+ rvt as u32,
1234
+ *rvt.add(4),
1235
+ ));
1236
+ // vtable[4] = get_pixel: thiscall(this, x, y) -> color, RET 0x8.
1237
+ let get_color: unsafe extern "thiscall" fn(*mut u8, u32, u32) -> u32 =
1238
+ core::mem::transmute(*rvt.add(4));
1239
+ let mut off = 0x730Cu32;
1240
+ let mut idx = 0u32;
1241
+ while off < 0x732Du32 {
1242
+ let c = get_color(res, idx, 0);
1243
+ *((ddgame as *mut u8).add(off as usize) as *mut u32) = c;
1244
+ if idx == 0 {
1245
+ let _ = crate::log::log_line(&format!("[DDGame] color[0] = 0x{:08X}", c,));
1246
+ }
1247
+ off += 4;
1248
+ idx += 1;
1249
+ }
1250
+ // DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
1251
+ // param_2 & 1 = free the object itself.
1252
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) =
1253
+ core::mem::transmute(*rvt.add(3));
1254
+ release(res, 1);
1255
+ }
1256
+ } else {
1257
+ // GFX_HANDLER_LOAD_SPRITES: SKIP for testing (gfx_mode==0 path)
1258
+ }
1259
+
1260
+ let _ = crate::log::log_line("[DDGame] past color entries");
1261
+ let _ = crate::log::log_line(&format!(
1262
+ "[DDGame] 4c0=0x{:08X} 4c4=0x{:08X}",
1263
+ (*wrapper)._field_4c0 as u32,
1264
+ (*wrapper)._field_4c4 as u32
1265
+ ));
1266
+ // ── Secondary GfxDir object (DDGame+0x2C, conditional) ──
1267
+ if !(*wrapper)._field_4c4.is_null() {
1268
+ let gfxdir2 = wa_malloc(0x70C);
1269
+ core::ptr::write_bytes(gfxdir2, 0, 0x70C);
1270
+ *(gfxdir2 as *mut u16) = 1;
1271
+ *(gfxdir2.add(2) as *mut u16) = 0x5A;
1272
+ // FUN_5411A0: usercall(EAX=gfxdir2), plain RET
1273
+ call_usercall_eax(gfxdir2 as *mut DDGameWrapper, rb(0x5411A0));
1274
+ *(gfxdir2.add(0x708) as *mut u16) = 0;
1275
+ (*ddgame).secondary_gfxdir = gfxdir2;
1276
+ // GFX_HANDLER_LOAD_SPRITES: stdcall(wrapper, ddgame+0x7308, game_info+0xF374, 0), RET 0x10
1277
+
1278
+ let f: unsafe extern "stdcall" fn(*mut DDGameWrapper, *mut u8, u32, u32) =
1279
+ core::mem::transmute(rb(va::GFX_HANDLER_LOAD_SPRITES) as usize);
1280
+ f(
1281
+ wrapper,
1282
+ (ddgame as *mut u8).add(0x7308),
1283
+ (*game_info).display_flags,
1284
+ 0,
1285
+ );
1286
+ }
1287
+
1288
+ // ── DDGameWrapper field inits ──
1289
+ (*wrapper)._field_4d8 = 0;
1290
+ // Original: if (display == NULL) iVar4=0 else iVar4=*(byte*)(game_info+0x44C)*0x38+0x7E
1291
+ // Ghidra mislabeled game_info as param8 — the original reads game_info, not net_game.
1292
+ // Our headless display is non-null, so use is_headless as the condition.
1293
+ if is_headless {
1294
+ (*wrapper)._field_4dc = 0x2AD;
1295
+ } else {
1296
+ let byte_val = *(game_info as *const u8).add(0x44C) as u32;
1297
+ (*wrapper)._field_4dc = byte_val * 0x38 + 0x7E + 0x2AD;
1298
+ }
1299
+ (*wrapper)._field_4e0 = 0xFFFFFF9C; // -100
1300
+ (*wrapper).speech_name_count = 0;
1301
+
1302
+ // ── Audio init (non-headless + sound available) ──
1303
+ let _ = crate::log::log_line("[DDGame] audio: start");
1304
+ if !is_headless {
1305
+ // FUN_570F30: usercall(ESI=wrapper)
1306
+ call_usercall_esi(wrapper, FUN_570F30_ADDR);
1307
+ let _ = crate::log::log_line("[DDGame] audio: past 570F30");
1308
+
1309
+ if !(*ddgame).sound.is_null() {
1310
+ // DSSound_LoadEffectWAVs: stdcall(wrapper)
1311
+ let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
1312
+ core::mem::transmute(rb(va::DSSOUND_LOAD_EFFECT_WAVS) as usize);
1313
+ f(wrapper);
1314
+ // DSSound_LoadAllSpeechBanks: the original is hooked to our Rust
1315
+ // replacement (speech.rs), so the usercall bridge calls our code.
1316
+ call_usercall_esi(wrapper, LOAD_SPEECH_BANKS_ADDR);
1317
+ let _ = crate::log::log_line("[DDGame] audio: done");
1318
+ // Allocate ActiveSoundTable (0x608 bytes)
1319
+ let ast = wa_malloc(0x608) as *mut ActiveSoundTable;
1320
+ (*ast).ddgame = ddgame;
1321
+ (*ast).counter = 0;
1322
+ core::ptr::write_bytes(ast as *mut u8, 0, 0x600);
1323
+ (*ddgame).active_sounds = ast;
1324
+ }
1325
+ }
1326
+
1327
+ let _ = crate::log::log_line("[DDGame] past ActiveSoundTable");
1328
+
1329
+ // ── GfxResource: thiscall(ECX=gfx_handler) + EAX=name + 1 stack(output), RET 0x4 ──
1330
+ // Test: call GfxResource with real params
1331
+ let gfx_resource: *mut u8;
1332
+ {
1333
+ {
1334
+ let gfx_handler = (*wrapper)._field_4c0;
1335
+ let out_buf = wa_malloc(0x900);
1336
+ core::ptr::write_bytes(out_buf, 0, 0x900);
1337
+ // return moved to before color entries
1338
+ let _ = crate::log::log_line("[DDGame] calling GfxResource_Create");
1339
+ gfx_resource = gfx_resource_create(gfx_handler, rb(0x66A3C0) as *const u8, out_buf);
1340
+ let _ = crate::log::log_line("[DDGame] after gfx_resource_create");
1341
+ let _ = crate::log::log_line("[DDGame] GfxResource created (value elided)");
1342
+ }
1343
+ }
1344
+
1345
+ // ── PCLandscape (alloc 0xB44, stdcall 11 params) ──
1346
+ // Allocate output buffers for landscape coordinate data (used later for coord_list)
1347
+ let landscape_coords_buf = wa_malloc(0x1000); // 4KB for coord output (aiStack_978)
1348
+ core::ptr::write_bytes(landscape_coords_buf, 0, 0x1000);
1349
+ let landscape_byte_buf = wa_malloc(0x100); // generous for byte output (iStack_11f9)
1350
+ core::ptr::write_bytes(landscape_byte_buf, 0, 0x100);
1351
+ let stack_local_8 = wa_malloc(0x1000); // stack local for coord count + temp data
1352
+ core::ptr::write_bytes(stack_local_8, 0, 0x1000);
1353
+
1354
+ let landscape = {
1355
+ let alloc = wa_malloc(0xB44);
1356
+ core::ptr::write_bytes(alloc, 0, 0xB44);
1357
+ if !alloc.is_null() {
1358
+ let pc_ctor: unsafe extern "stdcall" fn(
1359
+ *mut u8,
1360
+ *mut DDGame,
1361
+ *mut u8, // 1=this, 2=ddgame, 3=gfx_resource
1362
+ *mut DDDisplay,
1363
+ *const u8, // 4=display, 5=game_info+0xDAAC
1364
+ *mut u8,
1365
+ u32, // 6=&landscape_byte, 7=gfx_mode
1366
+ *mut u8,
1367
+ *mut u8, // 8=stack local, 9=coord output
1368
+ *mut u8,
1369
+ *mut u8, // 10=&ddgame+0x777C, 11=&ddgame+0x7780
1370
+ ) -> *mut u8 = core::mem::transmute(rb(va::PC_LANDSCAPE_CONSTRUCTOR) as usize);
1371
+
1372
+ let result = pc_ctor(
1373
+ alloc,
1374
+ ddgame,
1375
+ gfx_resource,
1376
+ (*wrapper).display, // param 4: display (NOT wrapper!)
1377
+ (*game_info).landscape_data_path.as_ptr(), // param 5
1378
+ landscape_byte_buf, // param 6
1379
+ (*wrapper).gfx_mode, // param 7
1380
+ stack_local_8, // param 8
1381
+ landscape_coords_buf, // param 9
1382
+ (ddgame as *mut u8).add(0x777C), // param 10
1383
+ (ddgame as *mut u8).add(0x7780), // param 11
1384
+ );
1385
+ let _ = crate::log::log_line(&format!(
1386
+ "[DDGame] PCLandscape returned: 0x{:08X}, 777C=0x{:08X} 7780=0x{:08X}",
1387
+ result as u32,
1388
+ *((ddgame as *mut u8).add(0x777C) as *const u32),
1389
+ *((ddgame as *mut u8).add(0x7780) as *const u32),
1390
+ ));
1391
+ // Dump landscape_coords_buf to find coordinate count and data format
1392
+ let coords = landscape_coords_buf as *const u32;
1393
+ let mut coord_count = 0u32;
1394
+ // Count non-zero pairs (each coord is 2 u32s)
1395
+ for j in 0..285 {
1396
+ if *coords.add(j * 2) != 0 || *coords.add(j * 2 + 1) != 0 {
1397
+ coord_count = j as u32 + 1;
1398
+ }
1399
+ }
1400
+ let _ = crate::log::log_line(&format!(
1401
+ "[DDGame] coords: first=[0x{:08X},0x{:08X}] count_est={} byte_buf[0]=0x{:02X}",
1402
+ *coords,
1403
+ *coords.add(1),
1404
+ coord_count,
1405
+ *landscape_byte_buf,
1406
+ ));
1407
+ // Also check stack_local_8 for count
1408
+ let sl8 = stack_local_8 as *const u32;
1409
+ let _ = crate::log::log_line(&format!(
1410
+ "[DDGame] stack8: [0]=0x{:08X} [1]=0x{:08X} [2]=0x{:08X} [3]=0x{:08X}",
1411
+ *sl8,
1412
+ *sl8.add(1),
1413
+ *sl8.add(2),
1414
+ *sl8.add(3),
1415
+ ));
1416
+ // Store landscape pointer
1417
+ (*wrapper).landscape = result as *mut PCLandscape;
1418
+ (*ddgame).landscape = result as *mut PCLandscape;
1419
+ result
1420
+ } else {
1421
+ (*wrapper).landscape = core::ptr::null_mut();
1422
+ (*ddgame).landscape = core::ptr::null_mut();
1423
+ core::ptr::null_mut()
1424
+ }
1425
+ };
1426
+
1427
+ // ── TaskStateMachine at DDGame+0x380 (alloc 0x4C, memset 0x2C) ──
1428
+ // Pure Rust: allocate object, call task_state_machine_init, override vtable.
1429
+ {
1430
+ let tsm = wa_malloc(0x4C);
1431
+ core::ptr::write_bytes(tsm, 0, 0x2C);
1432
+ if !tsm.is_null() {
1433
+ let width = (*ddgame).level_width;
1434
+ let height = (*ddgame).level_height;
1435
+ task_state_machine_init(tsm, 1, width, height);
1436
+ *(tsm as *mut u32) = rb(0x664118); // Override vtable to TaskStateMachine
1437
+ }
1438
+ (*ddgame).task_state_machine = tsm;
1439
+ }
1440
+
1441
+ // ── 8× SpriteRegion at DDGame+0x46C..0x488 ──
1442
+ // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18
1443
+ {
1444
+ // Params verified by hooking original (see SpriteRegion param logger).
1445
+ // (ddgame_offset, ECX, EDX, p2, p3, p4, p5, p6=gfx_resource)
1446
+ // p6 is the GfxResource pointer returned by GfxResource__Create_Maybe.
1447
+ let gr = gfx_resource as u32;
1448
+ let regions: [(u32, u32, u32, u32, u32, u32, u32, u32); 8] = [
1449
+ (0x474, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, gr),
1450
+ (0x46C, 0x30, 0x0C, 0x2D, 0x07, 0x34, 0x09, gr),
1451
+ (0x470, 0x11, 0x1A, 0x0D, 0x0A, 0x16, 0x13, gr),
1452
+ (0x478, 0x0C, 0x3D, 0x00, 0x20, 0x18, 0x33, gr),
1453
+ (0x47C, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, gr),
1454
+ (0x484, 0x1A2, 0x1B, 0x173, 0x09, 0x1D8, 0x03, gr),
1455
+ (0x488, 0x1EF, 0x26, 0x1E5, 0x07, 0x1F9, 0x16, gr),
1456
+ (0x480, 0x2D, 0x08, 0x2D, 0x07, 0x2E, 0x07, gr),
1457
+ ];
1458
+
1459
+ for &(offset, ecx, edx, p2, p3, p4, p5, p6) in &regions {
1460
+ let alloc = wa_malloc(0x9C);
1461
+ core::ptr::write_bytes(alloc, 0, 0x9C);
1462
+ let result = if !alloc.is_null() {
1463
+ call_sprite_region_ctor(alloc, ecx, edx, p2, p3, p4, p5, p6)
1464
+ } else {
1465
+ core::ptr::null_mut()
1466
+ };
1467
+ *((ddgame as *mut u8).add(offset as usize) as *mut *mut u8) = result;
1468
+ }
1469
+ }
1470
+
1471
+ // ── Landscape-derived value at DDGame+0x468 ──
1472
+ if !landscape.is_null() {
1473
+ let land_vt = *(landscape as *const *const u32);
1474
+ let get_val: unsafe extern "thiscall" fn(*mut u8) -> u32 =
1475
+ core::mem::transmute(*land_vt.add(0xB));
1476
+ *((ddgame as *mut u8).add(0x468) as *mut u32) = get_val(landscape);
1477
+ }
1478
+
1479
+ // NOTE: gfx_resource is NOT released here — arrow SpriteRegions need it.
1480
+ // It's released after the arrow loop below.
1481
+
1482
+ // ── Arrow sprites + collision regions (32 iterations) ──
1483
+ {
1484
+ let gfx_handler = (*wrapper)._field_4c0;
1485
+
1486
+ for i in 0u32..32 {
1487
+ // Format "arrow%02u.img\0" into stack buffer
1488
+ let mut name_buf = *b"arrow00.img\0\0\0\0\0";
1489
+ name_buf[5] = b'0' + (i / 10) as u8;
1490
+ name_buf[6] = b'0' + (i % 10) as u8;
1491
+
1492
+ // Display vtable[5](1) — set active layer, returns context ptr
1493
+ let disp_vt = *((*ddgame).display as *const *const u32);
1494
+ let set_layer: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 =
1495
+ core::mem::transmute(*disp_vt.add(5));
1496
+ let layer_ctx = set_layer((*ddgame).display, 1);
1497
+ if i == 0 {
1498
+ let _ = crate::log::log_line(&format!(
1499
+ "[DDGame] arrow[0]: layer_ctx=0x{:08X}, gfx=0x{:08X}",
1500
+ layer_ctx as u32, gfx_handler as u32
1501
+ ));
1502
+ }
1503
+
1504
+ let entry = gfx_dir_find_entry(name_buf.as_ptr(), gfx_handler);
1505
+ if i == 0 {
1506
+ let _ = crate::log::log_line(&format!(
1507
+ "[DDGame] arrow[0]: entry=0x{:08X}",
1508
+ entry as u32
1509
+ ));
1510
+ }
1511
+
1512
+ let sprite: *mut u8;
1513
+ if !entry.is_null() {
1514
+ // Try gfx_handler->vtable[2](entry->field_4)
1515
+ let gfx_vt = *(gfx_handler as *const *const u32);
1516
+ let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
1517
+ core::mem::transmute(*gfx_vt.add(2));
1518
+ let entry_val = *(entry.add(4) as *const u32);
1519
+ let cached = load_cached(gfx_handler, entry_val);
1520
+ if i == 0 {
1521
+ let _ = crate::log::log_line(&format!(
1522
+ "[DDGame] arrow[0]: cached=0x{:08X}",
1523
+ cached as u32
1524
+ ));
1525
+ }
1526
+ if !cached.is_null() {
1527
+ // Wrap with DisplayGfx constructor
1528
+ let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
1529
+ core::mem::transmute(rb(0x4F5E80) as usize);
1530
+ sprite = ctor(cached);
1531
+ } else {
1532
+ // Fallback: load from file via GfxDir__LoadImage + IMG_Decode
1533
+ sprite = call_gfx_load_and_wrap(gfx_handler, name_buf.as_ptr(), layer_ctx);
1534
+ }
1535
+ } else {
1536
+ // Entry not found — try direct file load
1537
+ sprite = call_gfx_load_and_wrap(gfx_handler, name_buf.as_ptr(), layer_ctx);
1538
+ }
1539
+ if i == 0 {
1540
+ let _ = crate::log::log_line(&format!(
1541
+ "[DDGame] arrow[0]: sprite=0x{:08X}",
1542
+ sprite as u32
1543
+ ));
1544
+ }
1545
+
1546
+ // Store arrow sprite at DDGame+0x38+i*4
1547
+ (*ddgame).arrow_sprites[i as usize] = sprite;
1548
+
1549
+ // Calculate collision region dimensions from sprite
1550
+ if !sprite.is_null() {
1551
+ let sprite_w = *(sprite.add(0x14) as *const i32);
1552
+ let sprite_h = *(sprite.add(0x18) as *const i32);
1553
+ let half_w = (sprite_w / 2 - 10).max(0);
1554
+ let half_h = (sprite_h / 2 - 10).max(0);
1555
+
1556
+ // Create SpriteRegion for collision
1557
+ let alloc = wa_malloc(0x9C);
1558
+ core::ptr::write_bytes(alloc, 0, 0x9C);
1559
+ let region = if !alloc.is_null() {
1560
+ // SpriteRegion params: ECX, EDX, this, p2, p3, p4, p5, p6
1561
+ // this[3] = p4 - p2 (width), this[4] = EDX - p3 (height)
1562
+ // For arrows: region from (0,0) to (half_w, half_h)
1563
+ call_sprite_region_ctor(
1564
+ alloc,
1565
+ 0, // ECX (x_max for this[5])
1566
+ half_h as u32, // EDX (y_max → this[4] = EDX - p3)
1567
+ 0, // p2 (x_offset)
1568
+ 0, // p3 (y_offset)
1569
+ half_w as u32, // p4 (x_limit → this[3] = p4 - p2)
1570
+ half_h as u32, // p5 (y_limit for this[6])
1571
+ gfx_resource as u32, // p6 (gfx_resource)
1572
+ )
1573
+ } else {
1574
+ core::ptr::null_mut()
1575
+ };
1576
+ (*ddgame).arrow_collision_regions[i as usize] = region;
1577
+ }
1578
+
1579
+ // Arrow GfxDir (conditional on secondary gfxdir)
1580
+ if !(*ddgame).secondary_gfxdir.is_null() {
1581
+ let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
1582
+ core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
1583
+ (*ddgame).arrow_gfxdirs[i as usize] =
1584
+ gfx_resource_create(gfx_handler, core::ptr::null_mut());
1585
+ }
1586
+ }
1587
+ }
1588
+
1589
+ let _ = crate::log::log_line("[DDGame] arrow loop done");
1590
+
1591
+ // Release gfx_resource AFTER arrow loop (arrows need it for SpriteRegions)
1592
+ // vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
1593
+ if !gfx_resource.is_null() {
1594
+ let rvt = *(gfx_resource as *const *const u32);
1595
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*rvt.add(3));
1596
+ release(gfx_resource, 1);
1597
+ }
1598
+
1599
+ // ── DisplayGfx at DDGame+0x138 ──
1600
+ // Original: alloc 0x4C, memset 0x2C, PUSH 0x100, MOV ECX=8, MOV EDI=0x1E0,
1601
+ // CALL 0x4F6370, MOV [alloc]=0x664144
1602
+ {
1603
+ let tsm = wa_malloc(0x4C);
1604
+ core::ptr::write_bytes(tsm, 0, 0x2C);
1605
+ if !tsm.is_null() {
1606
+ // Original: TaskStateMachine__Init(ECX=8, EDI=0x1E0, stack=0x100)
1607
+ // → task_state_machine_init(object, param1=8, width=0x100, height=0x1E0)
1608
+ task_state_machine_init(tsm, 8, 0x100, 0x1E0);
1609
+ *(tsm as *mut u32) = rb(0x664144); // Override vtable to DisplayGfx
1610
+ }
1611
+ (*ddgame).display_gfx = tsm;
1612
+ }
1613
+
1614
+ // ── CoordList at DDGame+0x50C (capacity 600, 0x12C0 buffer) ──
1615
+ {
1616
+ let cl = wa_malloc(12) as *mut u32;
1617
+ *cl = 0; // count
1618
+ *cl.add(1) = 600; // capacity
1619
+ let data = wa_malloc(0x12C0);
1620
+ core::ptr::write_bytes(data, 0, 0x12C0);
1621
+ *cl.add(2) = data as u32;
1622
+ (*ddgame).coord_list = cl as *mut u8;
1623
+
1624
+ // Populate coord_list from PCLandscape's coordinate output.
1625
+ // stack_local_8[0] = coordinate count, landscape_coords_buf = pairs of (x, y).
1626
+ // Original packs as: coord = x * 0x10000 + y (Fixed-point).
1627
+ // Each coord_list entry is 8 bytes: [coord_value, 1]. Duplicates are skipped.
1628
+ let coord_count = *(stack_local_8 as *const u32);
1629
+ let coords_src = landscape_coords_buf as *const u32;
1630
+ let data_ptr = data as *mut u32;
1631
+ for j in 0..coord_count as usize {
1632
+ let x = *coords_src.add(j * 2);
1633
+ let y = *coords_src.add(j * 2 + 1);
1634
+ let coord_val = x.wrapping_mul(0x10000).wrapping_add(y);
1635
+ let cur_count = *cl as usize;
1636
+ if cur_count >= 600 {
1637
+ break;
1638
+ }
1639
+ // Check for duplicates
1640
+ let mut dup = false;
1641
+ for k in 0..cur_count {
1642
+ if *data_ptr.add(k * 2) == coord_val {
1643
+ dup = true;
1644
+ break;
1645
+ }
1646
+ }
1647
+ if !dup {
1648
+ *data_ptr.add(cur_count * 2) = coord_val;
1649
+ *data_ptr.add(cur_count * 2 + 1) = 1;
1650
+ *cl = (cur_count + 1) as u32;
1651
+ }
1652
+ }
1653
+ let _ = crate::log::log_line(&format!(
1654
+ "[DDGame] coord_list populated: {} entries from {} landscape coords",
1655
+ *cl, coord_count,
1656
+ ));
1657
+ }
1658
+
1659
+ // ── Loading progress ticks (2 of 4 — before load_resource_list) ──
1660
+ call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
1661
+ call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
1662
+
1663
+ // ── Sprite resource loading via DDGameWrapper vtable[0] ──
1664
+ // DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
1665
+ // 5 stack params (layer, gfx_handler, base_path, data_table, table_size)
1666
+ {
1667
+ let landscape_ptr = (*wrapper).landscape as *const u8;
1668
+ let water_layer = *(landscape_ptr.add(0xB38) as *const *mut u8);
1669
+ let land_layer = *(landscape_ptr.add(0xB34) as *const *mut u8);
1670
+ let gfx_handler = (*wrapper)._field_4c0;
1671
+
1672
+ let wrapper_vt = *(wrapper as *const *const u32);
1673
+ let _ = crate::log::log_line(&format!(
1674
+ "[DDGame] sprite resource loading: wrapper_vt=0x{:08X}, vt[0]=0x{:08X}, landscape=0x{:08X}, water=0x{:08X}, land=0x{:08X}",
1675
+ wrapper_vt as u32, *wrapper_vt, landscape_ptr as u32, water_layer as u32, land_layer as u32,
1676
+ ));
1677
+ let load_resource_list: unsafe extern "thiscall" fn(
1678
+ *mut DDGameWrapper,
1679
+ u32,
1680
+ *mut u8,
1681
+ *const u8,
1682
+ *const u8,
1683
+ u32,
1684
+ ) = core::mem::transmute(*wrapper_vt);
1685
+ // Load resources for layer 1 (main sprites)
1686
+ let _ = crate::log::log_line("[DDGame] load_resource_list #1 (main sprites)");
1687
+ load_resource_list(
1688
+ wrapper,
1689
+ 1,
1690
+ gfx_handler,
1691
+ rb(0x643F2B) as *const u8, // base path
1692
+ rb(0x6AD2C0) as *const u8, // resource table
1693
+ 0x1D88, // table size
1694
+ );
1695
+ let _ = crate::log::log_line(&format!(
1696
+ "[DDGame] after #1: sprite_cache[144]=0x{:08X}",
1697
+ (*ddgame).sprite_cache[144] as u32,
1698
+ ));
1699
+
1700
+ // Set global flag based on game version
1701
+ let gv = (*(*ddgame).game_info).game_version;
1702
+ *(rb(0x6AF050) as *mut u32) = if gv < 8 { 0 } else { 0x10 };
1703
+
1704
+ // Load resources for layer 1 with different table
1705
+ let _ = crate::log::log_line("[DDGame] load_resource_list #2");
1706
+ load_resource_list(
1707
+ wrapper,
1708
+ 1,
1709
+ gfx_handler,
1710
+ rb(0x643F2B) as *const u8,
1711
+ rb(0x6AF048) as *const u8,
1712
+ 0x18,
1713
+ );
1714
+
1715
+ // Load resources for layer 2 (water)
1716
+ let _ = crate::log::log_line("[DDGame] load_resource_list #3 (water)");
1717
+ load_resource_list(
1718
+ wrapper,
1719
+ 2,
1720
+ water_layer,
1721
+ rb(0x643F2B) as *const u8,
1722
+ rb(0x6AF060) as *const u8,
1723
+ 0x2F4,
1724
+ );
1725
+
1726
+ // Display vtable[5](3) — set active layer 3, returns context ptr
1727
+ let disp_vt = *((*ddgame).display as *const *const u32);
1728
+ let set_layer: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 =
1729
+ core::mem::transmute(*disp_vt.add(5));
1730
+ let _layer3_ctx = set_layer((*ddgame).display, 3);
1731
+
1732
+ // Load back.spr and debris.spr (conditional)
1733
+ let disp_obj = (*wrapper).display as *mut u8;
1734
+ let disp_obj_vt = *(disp_obj as *const *const u32);
1735
+ // Check if gfx_mode high byte is set (original: uStack_123c._3_1_ != '\0')
1736
+ if (*wrapper).gfx_mode != 0 {
1737
+ let load_spr_94: unsafe extern "thiscall" fn(*mut u8, u32, u32, *mut u8, *const u8) =
1738
+ core::mem::transmute(*disp_obj_vt.add(0x94 / 4));
1739
+ load_spr_94(disp_obj, 3, 0x26D, land_layer, b"back.spr\0".as_ptr());
1740
+
1741
+ let load_spr_7c: unsafe extern "thiscall" fn(
1742
+ *mut u8,
1743
+ u32,
1744
+ u32,
1745
+ u32,
1746
+ *mut u8,
1747
+ *const u8,
1748
+ ) = core::mem::transmute(*disp_obj_vt.add(0x7C / 4));
1749
+ load_spr_7c(disp_obj, 3, 0x26E, 0, land_layer, b"debris.spr\0".as_ptr());
1750
+ }
1751
+
1752
+ // Load layer.spr into layer 2
1753
+ let load_spr_94: unsafe extern "thiscall" fn(*mut u8, u32, u32, *mut u8, *const u8) =
1754
+ core::mem::transmute(*disp_obj_vt.add(0x94 / 4));
1755
+ load_spr_94(
1756
+ disp_obj,
1757
+ 2,
1758
+ 0x26C,
1759
+ water_layer,
1760
+ b"layer\\layer.spr|layer.spr\0".as_ptr(),
1761
+ );
1762
+
1763
+ // gradient_image_2 = 0
1764
+ (*ddgame).gradient_image_2 = core::ptr::null_mut();
1765
+
1766
+ // ── Gradient image (0x030) ──
1767
+ // The gradient is loaded from "gradient.img" via GfxDir.
1768
+ // Simple path: height <= 0x60 AND level_height == 0x2B8
1769
+ let level_height = (*ddgame).level_height as i32;
1770
+ // Read sVar1 from display layer 3 context
1771
+ let layer3_ctx = set_layer((*ddgame).display, 3);
1772
+ let s_var1 = *(layer3_ctx.add(0x606) as *const i16);
1773
+
1774
+ if s_var1 < 0x61 && level_height == 0x2B8 {
1775
+ // Simple gradient: load gradient.img directly
1776
+ let gradient =
1777
+ call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1778
+ (*ddgame).gradient_image = gradient;
1779
+ } else {
1780
+ // Complex gradient computation — ~200 lines of color interpolation.
1781
+ // For now, skip this path (gradient_image remains null/0).
1782
+ // This affects the sky background appearance but not gameplay.
1783
+ // Set gradient_image to null (already zero from memset)
1784
+ }
1785
+
1786
+ // ── Fill image → fill_pixel (0x7338) ──
1787
+ {
1788
+ let layer2_ctx = set_layer((*ddgame).display, 2);
1789
+ // In the original, fill.img uses piStack_126c which the decompiler
1790
+ // shows was set from piVar3 (water_layer from landscape+0xB38).
1791
+ let fill_sprite =
1792
+ call_gfx_find_and_load(water_layer, b"fill.img\0".as_ptr(), layer2_ctx);
1793
+ let _ = crate::log::log_line(&format!(
1794
+ "[DDGame] fill.img: water=0x{:08X} sprite=0x{:08X}",
1795
+ water_layer as u32, fill_sprite as u32,
1796
+ ));
1797
+ if !fill_sprite.is_null() {
1798
+ // Get pixel value: fill_sprite->vtable[4](0, 0)
1799
+ let fill_vt = *(fill_sprite as *const *const u32);
1800
+ let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
1801
+ core::mem::transmute(*fill_vt.add(4));
1802
+ (*ddgame).fill_pixel = get_pixel(fill_sprite, 0, 0);
1803
+ // Release fill sprite: vtable[3] = DisplayGfx__vmethod_3(this, param_2=1)
1804
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) =
1805
+ core::mem::transmute(*fill_vt.add(3));
1806
+ release(fill_sprite, 1);
1807
+ }
1808
+ }
1809
+
1810
+ // ── DDGame__LoadHudAndWeaponSprites (0x53D0E0) ──
1811
+ // Loads weapon icons (cow.img, pigeon.img, etc.), wind indicators, stop sign,
1812
+ // girder sprites, and creates the DDGame+0x37C DisplayGfx object needed by
1813
+ // CTaskLand__InitLandscape.
1814
+ // Convention: thiscall(ECX=gfx_handler_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8.
1815
+ {
1816
+ let hud_load: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) =
1817
+ core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
1818
+ hud_load(gfx_handler, ddgame, (*wrapper)._field_4c4);
1819
+ }
1820
+
1821
+ // ── Loading progress ticks (2 of 4 — after LoadHudAndWeaponSprites) ──
1822
+ call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
1823
+ call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
1824
+ }
1825
+ // ── Gradient image stub (DDGame+0x30) ──
1826
+ // The original constructor loads "gradient.img" here and stores the result at DDGame+0x30.
1827
+ // CTaskLand__InitLandscape reads DDGame+0x30/+0x34 and calls vtable[4](0, i) for each
1828
+ // pixel column. vtable[4] = CTaskOilDrum__ProcessFrame_stub (XOR EAX,EAX; RET 0x8) returns 0.
1829
+ // A minimal stub with [6]=0 (zero-width) causes the loop to not execute at all.
1830
+ if (*ddgame).gradient_image.is_null() {
1831
+ let obj = wa_malloc(0x2C);
1832
+ core::ptr::write_bytes(obj, 0, 0x2C);
1833
+ if !obj.is_null() {
1834
+ *(obj as *mut u32) = rb(0x6640EC); // vtable (DisplayGfx vtable, vtable[4]=ProcessFrame_stub)
1835
+ // [6] = height/width = 0 → CTaskLand loop: `if (0 < 0)` → skip
1836
+ (*ddgame).gradient_image = obj;
1837
+ }
1838
+ }
1839
+
1840
+ // ── Release primary GfxHandler (vtable[3] = release, param 1 = free) ──
1841
+ let gfx_handler_4c0 = (*wrapper)._field_4c0;
1842
+ if !gfx_handler_4c0.is_null() {
1843
+ let gfx_vt = *(gfx_handler_4c0 as *const *const u32);
1844
+ let release: unsafe extern "thiscall" fn(*mut u8, u32) =
1845
+ core::mem::transmute(*gfx_vt.add(3));
1846
+ release(gfx_handler_4c0, 1);
1847
+ }
1848
+
1849
+ // ── DDGame__InitDisplayFinal_Maybe (0x56A830): non-headless display finalization ──
1850
+ if !is_headless {
1851
+ let f: unsafe extern "stdcall" fn(*mut DDDisplay) =
1852
+ core::mem::transmute(rb(va::DDGAME_INIT_DISPLAY_FINAL) as usize);
1853
+ f((*wrapper).display);
1854
+ }
1855
+
1856
+ // ── FUN_00570A90 (second call, conditional) ──
1857
+ if *(rb(0x88E485) as *const u8) == 0 {
1858
+ call_usercall_eax(wrapper, FUN_570A90_ADDR);
1859
+ }
1860
+
1861
+ // ── Final display layer visibility (vtable[0x17], offset 0x5C) ──
1862
+ {
1863
+ let disp = (*wrapper).display as *mut u8;
1864
+ let vt = *(disp as *const *const u32);
1865
+ let set_vis: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
1866
+ core::mem::transmute(*vt.add(0x17));
1867
+ set_vis(disp, 1, 0);
1868
+ set_vis(disp, 2, 0);
1869
+ set_vis(disp, 3, 1);
1870
+ }
1871
+
1872
+ let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
1873
+ }
1874
+
1875
+ /// Helper: find entry in GfxDir and load image, or load directly.
1876
+ /// Returns a DisplayGfx/sprite pointer or null.
1877
+ #[cfg(target_arch = "x86")]
1878
+ unsafe fn call_gfx_find_and_load(
1879
+ gfx_dir: *mut u8,
1880
+ name: *const u8,
1881
+ display_ctx: *mut u8,
1882
+ ) -> *mut u8 {
1883
+ let entry = gfx_dir_find_entry(name, gfx_dir);
1884
+
1885
+ if !entry.is_null() {
1886
+ // Try cached load: gfx_dir->vtable[2](entry->field_4)
1887
+ let dir_vt = *(gfx_dir as *const *const u32);
1888
+ let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
1889
+ core::mem::transmute(*dir_vt.add(2));
1890
+ let cached = load_cached(gfx_dir, *(entry.add(4) as *const u32));
1891
+ if !cached.is_null() {
1892
+ // Wrap with DisplayGfx__Constructor_Maybe (0x4F5E80)
1893
+ // This is stdcall(1 param), RET 0x4
1894
+ let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
1895
+ core::mem::transmute(rb(0x4F5E80) as usize);
1896
+ return ctor(cached);
1897
+ }
1898
+ }
1899
+
1900
+ // Fallback: load image directly
1901
+ call_gfx_load_and_wrap(gfx_dir, name, display_ctx)
1902
+ }
1903
+
1904
+ // Statics for usercall bridge addresses
1905
+ static mut FUN_570E20_ADDR: u32 = 0;
1906
+ static mut FUN_570A90_ADDR: u32 = 0;
1907
+ static mut FUN_570F30_ADDR: u32 = 0;
1908
+ static mut LOAD_SPEECH_BANKS_ADDR: u32 = 0;
1909
+ static mut LOADING_PROGRESS_TICK_ADDR: u32 = 0;
1910
+
1911
+ /// Bridge: usercall(ESI=wrapper), plain RET. Used by FUN_570E20, FUN_570F30, LoadSpeechBanks.
1912
+ #[cfg(target_arch = "x86")]
1913
+ #[unsafe(naked)]
1914
+ unsafe extern "C" fn call_usercall_esi(_wrapper: *mut DDGameWrapper, _addr: u32) {
1915
+ core::arch::naked_asm!(
1916
+ "pushl %esi",
1917
+ "movl 8(%esp), %esi", // ESI = wrapper
1918
+ "movl 12(%esp), %eax", // EAX = target address
1919
+ "calll *%eax",
1920
+ "popl %esi",
1921
+ "retl",
1922
+ options(att_syntax),
1923
+ );
1924
+ }
1925
+
1926
+ /// Bridge: usercall(EAX=wrapper), plain RET. Used by FUN_570A90.
1927
+ #[cfg(target_arch = "x86")]
1928
+ #[unsafe(naked)]
1929
+ unsafe extern "C" fn call_usercall_eax(_wrapper: *mut DDGameWrapper, _addr: u32) {
1930
+ core::arch::naked_asm!(
1931
+ "movl 4(%esp), %eax", // EAX = wrapper
1932
+ "movl 8(%esp), %ecx", // ECX = target address (temp)
1933
+ "calll *%ecx",
1934
+ "retl",
1935
+ options(att_syntax),
1936
+ );
1937
+ }
1938
+
1939
+ /// Bridge: usercall(ECX=wrapper), plain RET. Used by FUN_5717A0.
1940
+ #[cfg(target_arch = "x86")]
1941
+ unsafe fn call_usercall_ecx(wrapper: *mut DDGameWrapper, addr: u32) {
1942
+ let f: unsafe extern "thiscall" fn(*mut DDGameWrapper) = core::mem::transmute(addr as usize);
1943
+ f(wrapper);
1944
+ }
1945
+
1946
+ /// Runtime address of TaskStateMachine__Init_Maybe.
1947
+ static mut TSM_INIT_ADDR: u32 = 0;
1948
+
1949
+ /// Bridge to TaskStateMachine__Init_Maybe (0x4F6370).
1950
+ /// Convention: usercall(ESI=object, ECX=1, EDI=height) + 1 stack(width), RET 0x4.
1951
+ #[cfg(target_arch = "x86")]
1952
+ #[unsafe(naked)]
1953
+ unsafe extern "C" fn call_tsm_init(_object: *mut u8, _width: u32, _height: u32) {
1954
+ core::arch::naked_asm!(
1955
+ "pushl %esi",
1956
+ "pushl %edi",
1957
+ "movl 12(%esp), %esi", // ESI = object (1st param, shifted by 2 pushes)
1958
+ "movl 20(%esp), %edi", // EDI = height (3rd param, shifted by 2 pushes)
1959
+ "pushl 16(%esp)", // push width (2nd param, shifted by 2 pushes + 1 push)
1960
+ "movl $1, %ecx", // ECX = 1
1961
+ "calll *({addr})", // RET 0x4 cleans the pushed width
1962
+ "popl %edi",
1963
+ "popl %esi",
1964
+ "retl",
1965
+ addr = sym TSM_INIT_ADDR,
1966
+ options(att_syntax),
1967
+ );
1968
+ }
1969
+
1970
+ /// Runtime address of GfxResource__Create_Maybe (set by init_constructor_addrs()).
1971
+ static mut GFX_RESOURCE_CREATE_ADDR: u32 = 0;
1972
+
1973
+ /// Bridge to GfxResource__Create_Maybe (0x4F6300).
1974
+ /// Convention: usercall(ECX=gfx_handler, EAX=data_ptr) + 1 stack(output), RET 0x4.
1975
+ #[cfg(target_arch = "x86")]
1976
+ #[unsafe(naked)]
1977
+ unsafe extern "C" fn call_gfx_resource_create(
1978
+ _gfx_handler: *mut u8,
1979
+ _data_ptr: *const u8,
1980
+ _output: *mut u8,
1981
+ ) -> *mut u8 {
1982
+ core::arch::naked_asm!(
1983
+ "pushl %ebp",
1984
+ "pushl %ebx",
1985
+ "movl 12(%esp), %ecx", // ECX = gfx_handler (0=ebx,4=ebp,8=ret,12=handler)
1986
+ "movl 16(%esp), %eax", // EAX = data_ptr (16=data)
1987
+ "pushl 20(%esp)", // push output (20=output, but shifts after push!)
1988
+ "calll *({addr})", // RET 0x4 cleans output
1989
+ "popl %ebx",
1990
+ "popl %ebp",
1991
+ "retl",
1992
+ addr = sym GFX_RESOURCE_CREATE_ADDR,
1993
+ options(att_syntax),
1994
+ );
1995
+ }
1996
+
1997
+ /// Runtime address of GfxDir__FindEntry (set by init_constructor_addrs()).
1998
+ static mut GFX_FIND_ENTRY_ADDR: u32 = 0;
1999
+
2000
+ /// Bridge to GfxDir__FindEntry (0x566520).
2001
+ /// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4.
2002
+ /// Returns entry pointer or null.
2003
+ #[cfg(target_arch = "x86")]
2004
+ #[unsafe(naked)]
2005
+ unsafe extern "C" fn call_gfx_find_entry(_name: *const u8, _gfx_handler: *mut u8) -> *mut u8 {
2006
+ core::arch::naked_asm!(
2007
+ "movl 4(%esp), %eax", // EAX = name
2008
+ "pushl 8(%esp)", // push gfx_handler (callee cleans via RET 0x4)
2009
+ "calll *({addr})",
2010
+ "retl", // caller cleans our 2 cdecl params
2011
+ addr = sym GFX_FIND_ENTRY_ADDR,
2012
+ options(att_syntax),
2013
+ );
2014
+ }
2015
+
2016
+ /// Bridge to GfxHandler__LoadDir (0x5663E0).
2017
+ /// Convention: usercall(EAX=handler), plain RET. Returns nonzero on success.
2018
+ #[cfg(target_arch = "x86")]
2019
+ unsafe fn call_gfx_load_dir(handler: *mut u8, addr: u32) -> i32 {
2020
+ let result: i32;
2021
+ core::arch::asm!(
2022
+ "call {addr}",
2023
+ addr = in(reg) addr,
2024
+ inlateout("eax") handler => result,
2025
+ out("ecx") _,
2026
+ out("edx") _,
2027
+ clobber_abi("C"),
2028
+ );
2029
+ result
2030
+ }
2031
+
2032
+ // GfxDir__LoadImage is usercall(ESI=gfx_handler) + 1 stack(name), RET 0x4.
2033
+ // Returns raw image pointer or null.
2034
+ static mut GFX_LOAD_IMAGE_ADDR: u32 = 0;
2035
+
2036
+ #[cfg(target_arch = "x86")]
2037
+ #[unsafe(naked)]
2038
+ unsafe extern "C" fn call_gfx_load_image(_gfx_handler: *mut u8, _name: *const u8) -> *mut u8 {
2039
+ core::arch::naked_asm!(
2040
+ "pushl %esi",
2041
+ "movl 8(%esp), %esi", // ESI = gfx_handler
2042
+ "pushl 12(%esp)", // push name (callee cleans via RET 0x4)
2043
+ "calll *({addr})",
2044
+ "popl %esi",
2045
+ "retl",
2046
+ addr = sym GFX_LOAD_IMAGE_ADDR,
2047
+ options(att_syntax),
2048
+ );
2049
+ }
2050
+
2051
+ /// Helper: load image via GfxDir__LoadImage + wrap as DisplayGfx.
2052
+ /// Used by arrow sprite loop when GfxDir__FindEntry returns null.
2053
+ #[cfg(target_arch = "x86")]
2054
+ unsafe fn call_gfx_load_and_wrap(
2055
+ gfx_handler: *mut u8,
2056
+ name: *const u8,
2057
+ display_ctx: *mut u8,
2058
+ ) -> *mut u8 {
2059
+ let image = call_gfx_load_image(gfx_handler, name);
2060
+ if image.is_null() {
2061
+ return core::ptr::null_mut();
2062
+ }
2063
+ // FUN_004F5F80(display_ctx, image, 1) — stdcall, RET 0xC (3 params)
2064
+ let f: unsafe extern "stdcall" fn(*mut u8, *mut u8, u32) -> *mut u8 =
2065
+ core::mem::transmute(rb(0x4F5F80) as usize);
2066
+ let result = f(display_ctx, image, 1);
2067
+ // Release the raw image
2068
+ let image_vt = *(image as *const *const u32);
2069
+ let destroy: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*image_vt);
2070
+ destroy(image, 1);
2071
+ result
2072
+ }
2073
+
2074
+ /// Bridge to SpriteRegion__Constructor (0x57DB20).
2075
+ /// Convention: fastcall(ECX, EDX) + 6 stack params, RET 0x18.
2076
+ #[cfg(target_arch = "x86")]
2077
+ static mut SPRITE_REGION_CTOR_ADDR: u32 = 0;
2078
+
2079
+ #[cfg(target_arch = "x86")]
2080
+ #[unsafe(naked)]
2081
+ unsafe extern "C" fn call_sprite_region_ctor(
2082
+ _this: *mut u8,
2083
+ _ecx: u32,
2084
+ _edx: u32,
2085
+ _p2: u32,
2086
+ _p3: u32,
2087
+ _p4: u32,
2088
+ _p5: u32,
2089
+ _p6: u32,
2090
+ ) -> *mut u8 {
2091
+ // Stack on entry: [ret] [this] [ecx] [edx] [p2] [p3] [p4] [p5] [p6]
2092
+ // Need: ECX=ecx, EDX=edx, push p6 p5 p4 p3 p2 this, call
2093
+ // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack, RET 0x18
2094
+ // Params: (this, ecx, edx, p2, p3, p4, p5, p6)
2095
+ core::arch::naked_asm!(
2096
+ "pushl %ebp",
2097
+ "pushl %ebx",
2098
+ // Load ECX and EDX (shifted by 2 pushes = 8 bytes)
2099
+ "movl 16(%esp), %ecx", // ecx param (offset 4+8=12? No: 0=ebx,4=ebp,8=ret,12=this,16=ecx)
2100
+ "movl 20(%esp), %edx", // edx param
2101
+ // Push 6 stack params in reverse: p6, p5, p4, p3, p2, this
2102
+ "pushl 40(%esp)", // p6 (0=ebx,4=ebp,8=ret,...,40=p6)
2103
+ "pushl 40(%esp)", // p5 (shifted by 4)
2104
+ "pushl 40(%esp)", // p4 (shifted by 8)
2105
+ "pushl 40(%esp)", // p3 (shifted by 12)
2106
+ "pushl 40(%esp)", // p2 (shifted by 16)
2107
+ "pushl 32(%esp)", // this (shifted by 20)
2108
+ "calll *({addr})", // fastcall, callee cleans 6×4=24 bytes
2109
+ "popl %ebx",
2110
+ "popl %ebp",
2111
+ "retl",
2112
+ addr = sym SPRITE_REGION_CTOR_ADDR,
2113
+ options(att_syntax),
2114
+ );
2115
+ }
2116
+
2117
  /// Well-known byte offsets into DDGame, for use with raw pointer access.
2118
  ///
2119
  /// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
 
2268
  // Team arena state — sub-struct at DDGame + 0x4628
2269
  // ============================================================
2270
 
 
2271
  // ============================================================
2272
  // Per-worm and per-team block structs
2273
  // ============================================================
 
2440
  pub game_phase: i32,
2441
 
2442
  // === Alliance tracking (set by CountTeamsByAlliance + SetActiveWorm_Maybe) ===
 
2443
  /// 0x2C2C: Current alliance being evaluated
2444
  pub current_alliance: i32,
2445
  /// 0x2C30: Count of teams with an active worm (set by SetActiveWorm_Maybe)
 
2487
  pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
2488
  pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
2489
 
 
2490
  impl TeamArenaState {
2491
  /// Get ammo count for a weapon slot (by flat index).
2492
  pub fn get_ammo(&self, index: usize) -> i32 {
 
2529
  /// `base` must point to DDGame + TEAM_ARENA_STATE (0x4628).
2530
  #[inline]
2531
  pub unsafe fn from_raw(base: u32) -> Self {
2532
+ Self {
2533
+ base: base as *const u8,
2534
+ }
2535
  }
2536
 
2537
  /// Access the TeamArenaState fields (read-only).
 
2571
  /// (0x6C+) that don't conflict with worm data (0x00-0x5F).
2572
  #[inline]
2573
  pub unsafe fn team_worm(&self, team_idx: usize, worm_num: usize) -> &WormEntry {
2574
+ let ptr = self
2575
+ .base
2576
  .add(team_idx * 0x51C)
2577
  .add(worm_num * 0x9C)
2578
  .sub(0x598);
 
2607
  /// Get mutable team header for Pattern B access.
2608
  #[inline]
2609
  pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamBlockHeader {
2610
+ &mut (*(self.blocks() as *mut FullTeamBlock).add(team_idx + 2))
2611
+ .header
2612
+ .team
2613
  }
2614
 
2615
  /// Compute the flat index for ammo/delay table access.
crates/openwa-core/src/engine/ddgame_wrapper.rs CHANGED
@@ -1,6 +1,6 @@
 
1
  use crate::display::dd_display::DDDisplay;
2
  use crate::engine::ddgame::DDGame;
3
- use crate::audio::dssound::DSSound;
4
  use crate::render::landscape::PCLandscape;
5
 
6
  /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
 
1
+ use crate::audio::dssound::DSSound;
2
  use crate::display::dd_display::DDDisplay;
3
  use crate::engine::ddgame::DDGame;
 
4
  use crate::render::landscape::PCLandscape;
5
 
6
  /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
crates/openwa-core/src/engine/game_info.rs CHANGED
@@ -13,7 +13,6 @@ pub struct GameInfo {
13
  pub _unknown_0000: [u8; 0x44C],
14
 
15
  // --- Speech configuration ---
16
-
17
  /// 0x044C: Number of teams with speech banks loaded (byte).
18
  /// Used by DSSound_LoadAllSpeechBanks to iterate teams.
19
  pub speech_team_count: u8,
@@ -21,12 +20,23 @@ pub struct GameInfo {
21
  /// 0x044D-0xD777: Unknown
22
  pub _unknown_044d: [u8; 0xD778 - 0x44D],
23
 
24
- /// 0xD778: Crosshair overflow clamp threshold (compared to 0x11E in render).
25
- /// Also passed to timer constructor and input controller init.
26
- pub crosshair_overflow_threshold: i32,
 
 
 
 
27
 
28
- /// 0xD77C-0xD9DF: Unknown
29
- pub _unknown_d77c: [u8; 0xD9E0 - 0xD77C],
 
 
 
 
 
 
 
30
 
31
  /// 0xD9E0: Streaming audio config data (path config passed to streaming audio ctor).
32
  /// Address of this field is passed as a pointer parameter.
@@ -36,11 +46,14 @@ pub struct GameInfo {
36
  /// If nonzero, streaming audio subsystem is created in InitHardware.
37
  pub speech_enabled: u8,
38
 
39
- /// 0xDAA5-0xDAE7: Unknown
40
- pub _unknown_daa5: [u8; 0xDAE8 - 0xDAA5],
41
 
42
- // --- Cluster 1: data paths ---
 
 
43
 
 
44
  /// 0xDAE8: Config DWORD (copied from global 0x88E390)
45
  pub _config_dword_dae8: u32,
46
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
@@ -68,7 +81,6 @@ pub struct GameInfo {
68
  pub _unknown_f378: [u8; 0xF3A0 - 0xF378],
69
 
70
  // --- Cluster 2: game options (populated by LoadOptions) ---
71
-
72
  /// 0xF3A0: Unknown config byte (from global 0x7C0D38)
73
  pub _config_byte_f3a0: u8,
74
  /// 0xF3A1: Detail level (registry: DetailLevel, default 5)
@@ -132,7 +144,6 @@ pub struct GameInfo {
132
  pub _unknown_f4c5: [u8; 0xF500 - 0xF4C5],
133
 
134
  // --- Extended region (beyond original 0xF500 conservative estimate) ---
135
-
136
  /// 0xF500-0xF913: Unknown
137
  pub _unknown_f500: [u8; 0xF914 - 0xF500],
138
 
@@ -153,7 +164,9 @@ impl core::fmt::Debug for HexU32s<'_> {
153
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
154
  write!(f, "[")?;
155
  for (i, v) in self.0.iter().enumerate() {
156
- if i > 0 { write!(f, ", ")?; }
 
 
157
  write!(f, "0x{v:08X}")?;
158
  }
159
  write!(f, "]")
@@ -163,20 +176,27 @@ impl core::fmt::Debug for HexU32s<'_> {
163
  impl core::fmt::Debug for GameInfo {
164
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
165
  // Extract land_dat_path as a string (null-terminated)
166
- let land_str = self.land_dat_path.iter()
 
 
167
  .position(|&b| b == 0)
168
  .map(|end| core::str::from_utf8(&self.land_dat_path[..end]).unwrap_or("<invalid utf8>"))
169
  .unwrap_or(core::str::from_utf8(&self.land_dat_path).unwrap_or("<invalid utf8>"));
170
 
171
  // Extract speech_path as a string (null-terminated)
172
- let speech_str = self.speech_path.iter()
 
 
173
  .position(|&b| b == 0)
174
  .map(|end| core::str::from_utf8(&self.speech_path[..end]).unwrap_or("<invalid utf8>"))
175
  .unwrap_or(core::str::from_utf8(&self.speech_path).unwrap_or("<invalid utf8>"));
176
 
177
  f.debug_struct("GameInfo")
178
  // Cluster 1: data paths
179
- .field("_config_dword_dae8", &format_args!("0x{:08X}", self._config_dword_dae8))
 
 
 
180
  .field("land_dat_path", &land_str)
181
  // Cluster 2: game options
182
  .field("_config_byte_f3a0", &self._config_byte_f3a0)
@@ -186,25 +206,52 @@ impl core::fmt::Debug for GameInfo {
186
  .field("info_spy", &self.info_spy)
187
  .field("chat_pinned", &self.chat_pinned)
188
  .field("chat_lines", &self.chat_lines)
189
- .field("pinned_chat_lines", &format_args!("0x{:08X}", self.pinned_chat_lines))
 
 
 
190
  .field("home_lock", &self.home_lock)
191
- .field("display_flags", &format_args!("0x{:08X}", self.display_flags))
 
 
 
192
  .field("display_width", &self.display_width)
193
  .field("display_height", &self.display_height)
194
  .field("_config_dwords_f3bc", &HexU32s(&self._config_dwords_f3bc))
195
- .field("_config_dword_f3d4", &format_args!("0x{:08X}", self._config_dword_f3d4))
196
- .field("_config_dword_f3d8", &format_args!("0x{:08X}", self._config_dword_f3d8))
 
 
 
 
 
 
197
  .field("capture_transparent_pngs", &self.capture_transparent_pngs)
198
  .field("camera_unlock_mouse_speed", &self.camera_unlock_mouse_speed)
199
- .field("_config_dword_f3e4", &format_args!("0x{:08X}", self._config_dword_f3e4))
200
- .field("background_debris_parallax", &format_args!("0x{:08X}", self.background_debris_parallax))
201
- .field("topmost_explosion_onomatopoeia", &self.topmost_explosion_onomatopoeia)
 
 
 
 
 
 
 
 
 
202
  .field("_zeroed_f3f0", &self._zeroed_f3f0)
203
- .field("_conditional_config_f3f4", &HexU32s(&self._conditional_config_f3f4))
 
 
 
204
  .field("speech_path", &speech_str)
205
  .field("speech_enabled", &self.speech_enabled)
206
  .field("headless_mode", &self.headless_mode)
207
- .field("input_state_f918", &format_args!("0x{:08X}", self.input_state_f918))
 
 
 
208
  .finish()
209
  }
210
  }
 
13
  pub _unknown_0000: [u8; 0x44C],
14
 
15
  // --- Speech configuration ---
 
16
  /// 0x044C: Number of teams with speech banks loaded (byte).
17
  /// Used by DSSound_LoadAllSpeechBanks to iterate teams.
18
  pub speech_team_count: u8,
 
20
  /// 0x044D-0xD777: Unknown
21
  pub _unknown_044d: [u8; 0xD778 - 0x44D],
22
 
23
+ /// 0xD778: Game version/mode. Compared against various thresholds:
24
+ /// -2 = network game, -3..0 = different game modes, 8+ = new versions.
25
+ /// Used by DDGame constructor for conditional initialization.
26
+ pub game_version: i32,
27
+
28
+ /// 0xD77C-0xD943: Unknown
29
+ pub _unknown_d77c: [u8; 0xD944 - 0xD77C],
30
 
31
+ /// 0xD944: Network config byte 1 (copied to network object+0x28).
32
+ pub net_config_1: u8,
33
+ /// 0xD945: Unknown
34
+ pub _unknown_d945: u8,
35
+ /// 0xD946: Network config byte 2 (copied to network object+0x29).
36
+ pub net_config_2: u8,
37
+
38
+ /// 0xD947-0xD9DF: Unknown
39
+ pub _unknown_d947: [u8; 0xD9E0 - 0xD947],
40
 
41
  /// 0xD9E0: Streaming audio config data (path config passed to streaming audio ctor).
42
  /// Address of this field is passed as a pointer parameter.
 
46
  /// If nonzero, streaming audio subsystem is created in InitHardware.
47
  pub speech_enabled: u8,
48
 
49
+ /// 0xDAA5-0xDAAB: Unknown
50
+ pub _unknown_daa5: [u8; 0xDAAC - 0xDAA5],
51
 
52
+ /// 0xDAAC: Landscape data path (passed to PCLandscape constructor).
53
+ /// Points to a path string used for loading level terrain data.
54
+ pub landscape_data_path: [u8; 0xDAE8 - 0xDAAC],
55
 
56
+ // --- Cluster 1: data paths ---
57
  /// 0xDAE8: Config DWORD (copied from global 0x88E390)
58
  pub _config_dword_dae8: u32,
59
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
 
81
  pub _unknown_f378: [u8; 0xF3A0 - 0xF378],
82
 
83
  // --- Cluster 2: game options (populated by LoadOptions) ---
 
84
  /// 0xF3A0: Unknown config byte (from global 0x7C0D38)
85
  pub _config_byte_f3a0: u8,
86
  /// 0xF3A1: Detail level (registry: DetailLevel, default 5)
 
144
  pub _unknown_f4c5: [u8; 0xF500 - 0xF4C5],
145
 
146
  // --- Extended region (beyond original 0xF500 conservative estimate) ---
 
147
  /// 0xF500-0xF913: Unknown
148
  pub _unknown_f500: [u8; 0xF914 - 0xF500],
149
 
 
164
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
165
  write!(f, "[")?;
166
  for (i, v) in self.0.iter().enumerate() {
167
+ if i > 0 {
168
+ write!(f, ", ")?;
169
+ }
170
  write!(f, "0x{v:08X}")?;
171
  }
172
  write!(f, "]")
 
176
  impl core::fmt::Debug for GameInfo {
177
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
178
  // Extract land_dat_path as a string (null-terminated)
179
+ let land_str = self
180
+ .land_dat_path
181
+ .iter()
182
  .position(|&b| b == 0)
183
  .map(|end| core::str::from_utf8(&self.land_dat_path[..end]).unwrap_or("<invalid utf8>"))
184
  .unwrap_or(core::str::from_utf8(&self.land_dat_path).unwrap_or("<invalid utf8>"));
185
 
186
  // Extract speech_path as a string (null-terminated)
187
+ let speech_str = self
188
+ .speech_path
189
+ .iter()
190
  .position(|&b| b == 0)
191
  .map(|end| core::str::from_utf8(&self.speech_path[..end]).unwrap_or("<invalid utf8>"))
192
  .unwrap_or(core::str::from_utf8(&self.speech_path).unwrap_or("<invalid utf8>"));
193
 
194
  f.debug_struct("GameInfo")
195
  // Cluster 1: data paths
196
+ .field(
197
+ "_config_dword_dae8",
198
+ &format_args!("0x{:08X}", self._config_dword_dae8),
199
+ )
200
  .field("land_dat_path", &land_str)
201
  // Cluster 2: game options
202
  .field("_config_byte_f3a0", &self._config_byte_f3a0)
 
206
  .field("info_spy", &self.info_spy)
207
  .field("chat_pinned", &self.chat_pinned)
208
  .field("chat_lines", &self.chat_lines)
209
+ .field(
210
+ "pinned_chat_lines",
211
+ &format_args!("0x{:08X}", self.pinned_chat_lines),
212
+ )
213
  .field("home_lock", &self.home_lock)
214
+ .field(
215
+ "display_flags",
216
+ &format_args!("0x{:08X}", self.display_flags),
217
+ )
218
  .field("display_width", &self.display_width)
219
  .field("display_height", &self.display_height)
220
  .field("_config_dwords_f3bc", &HexU32s(&self._config_dwords_f3bc))
221
+ .field(
222
+ "_config_dword_f3d4",
223
+ &format_args!("0x{:08X}", self._config_dword_f3d4),
224
+ )
225
+ .field(
226
+ "_config_dword_f3d8",
227
+ &format_args!("0x{:08X}", self._config_dword_f3d8),
228
+ )
229
  .field("capture_transparent_pngs", &self.capture_transparent_pngs)
230
  .field("camera_unlock_mouse_speed", &self.camera_unlock_mouse_speed)
231
+ .field(
232
+ "_config_dword_f3e4",
233
+ &format_args!("0x{:08X}", self._config_dword_f3e4),
234
+ )
235
+ .field(
236
+ "background_debris_parallax",
237
+ &format_args!("0x{:08X}", self.background_debris_parallax),
238
+ )
239
+ .field(
240
+ "topmost_explosion_onomatopoeia",
241
+ &self.topmost_explosion_onomatopoeia,
242
+ )
243
  .field("_zeroed_f3f0", &self._zeroed_f3f0)
244
+ .field(
245
+ "_conditional_config_f3f4",
246
+ &HexU32s(&self._conditional_config_f3f4),
247
+ )
248
  .field("speech_path", &speech_str)
249
  .field("speech_enabled", &self.speech_enabled)
250
  .field("headless_mode", &self.headless_mode)
251
+ .field(
252
+ "input_state_f918",
253
+ &format_args!("0x{:08X}", self.input_state_f918),
254
+ )
255
  .finish()
256
  }
257
  }
crates/openwa-core/src/engine/game_session.rs CHANGED
@@ -1,7 +1,7 @@
1
- use crate::engine::ddgame_wrapper::DDGameWrapper;
2
- use crate::input::keyboard::DDKeyboard;
3
  use crate::audio::dssound::DSSound;
4
  use crate::display::palette::Palette;
 
 
5
 
6
  /// Top-level game session context, allocated once per game run.
7
  ///
 
 
 
1
  use crate::audio::dssound::DSSound;
2
  use crate::display::palette::Palette;
3
+ use crate::engine::ddgame_wrapper::DDGameWrapper;
4
+ use crate::input::keyboard::DDKeyboard;
5
 
6
  /// Top-level game session context, allocated once per game run.
7
  ///
crates/openwa-core/src/fixed.rs CHANGED
@@ -1,4 +1,4 @@
1
- use core::ops::{Add, Sub, Mul, Neg};
2
 
3
  /// 16.16 fixed-point number used throughout WA for coordinates and velocities.
4
  ///
 
1
+ use core::ops::{Add, Mul, Neg, Sub};
2
 
3
  /// 16.16 fixed-point number used throughout WA for coordinates and velocities.
4
  ///
crates/openwa-core/src/game/frontend.rs CHANGED
@@ -21,7 +21,6 @@
21
  #[repr(i32)]
22
  pub enum ScreenId {
23
  // --- Dialog result codes (not main-loop dispatch targets) ---
24
-
25
  /// Cancel / return to previous screen (EndDialog result only)
26
  Cancel = 0,
27
  /// Splash / intro trigger (result code)
@@ -34,7 +33,6 @@ pub enum ScreenId {
34
  SinglePlayerConfirm = 5,
35
 
36
  // --- Valid main-loop screen IDs ---
37
-
38
  /// Deathmatch setup dialog (constructor 0x440F40)
39
  Deathmatch = 10,
40
  /// Local multiplayer setup dialog (constructor 0x49C420)
@@ -80,7 +78,6 @@ pub enum ScreenId {
80
  // 35/36 and 37/38 form offline/online pairs:
81
  // Start Game path: this+0x244 flag → 35 (no network) / 36 (network)
82
  // Multiplayer path: this+0x140 flag → 37 (no network) / 38 (network)
83
-
84
  /// Scheme/weapon editor done — return from editor
85
  SchemeEditorDone = 32,
86
  /// Start game, network unavailable (offline/local path)
@@ -101,7 +98,6 @@ pub enum ScreenId {
101
  InGameC = 58,
102
 
103
  // --- Network/lobby screen IDs (high range, 0x6A4-0x6AB) ---
104
-
105
  /// Network provider selection dialog (constructor 0x4A7990)
106
  NetworkProviderSelect = 1700,
107
  /// Network settings dialog (constructor 0x4C23C0)
 
21
  #[repr(i32)]
22
  pub enum ScreenId {
23
  // --- Dialog result codes (not main-loop dispatch targets) ---
 
24
  /// Cancel / return to previous screen (EndDialog result only)
25
  Cancel = 0,
26
  /// Splash / intro trigger (result code)
 
33
  SinglePlayerConfirm = 5,
34
 
35
  // --- Valid main-loop screen IDs ---
 
36
  /// Deathmatch setup dialog (constructor 0x440F40)
37
  Deathmatch = 10,
38
  /// Local multiplayer setup dialog (constructor 0x49C420)
 
78
  // 35/36 and 37/38 form offline/online pairs:
79
  // Start Game path: this+0x244 flag → 35 (no network) / 36 (network)
80
  // Multiplayer path: this+0x140 flag → 37 (no network) / 38 (network)
 
81
  /// Scheme/weapon editor done — return from editor
82
  SchemeEditorDone = 32,
83
  /// Start game, network unavailable (offline/local path)
 
98
  InGameC = 58,
99
 
100
  // --- Network/lobby screen IDs (high range, 0x6A4-0x6AB) ---
 
101
  /// Network provider selection dialog (constructor 0x4A7990)
102
  NetworkProviderSelect = 1700,
103
  /// Network settings dialog (constructor 0x4C23C0)
crates/openwa-core/src/game/message.rs CHANGED
@@ -126,8 +126,7 @@ impl TryFrom<u32> for TaskMessage {
126
 
127
  fn try_from(value: u32) -> Result<Self, Self::Error> {
128
  match value {
129
- 0..=9 | 11..=23 | 26..=41 | 43..=64 | 67..=81 | 84..=86
130
- | 88..=94 | 96..=114 => {
131
  // SAFETY: all matched values correspond to valid variants
132
  Ok(unsafe { core::mem::transmute(value) })
133
  }
 
126
 
127
  fn try_from(value: u32) -> Result<Self, Self::Error> {
128
  match value {
129
+ 0..=9 | 11..=23 | 26..=41 | 43..=64 | 67..=81 | 84..=86 | 88..=94 | 96..=114 => {
 
130
  // SAFETY: all matched values correspond to valid variants
131
  Ok(unsafe { core::mem::transmute(value) })
132
  }
crates/openwa-core/src/game/scheme.rs CHANGED
@@ -90,10 +90,17 @@ impl core::fmt::Display for SchemeError {
90
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91
  match self {
92
  SchemeError::TooShort { len } => {
93
- write!(f, "file too short ({len} bytes, need at least {SCHEME_HEADER_SIZE})")
 
 
 
94
  }
95
  SchemeError::BadMagic(m) => {
96
- write!(f, "bad magic: {:02X} {:02X} {:02X} {:02X} (expected SCHM)", m[0], m[1], m[2], m[3])
 
 
 
 
97
  }
98
  SchemeError::UnknownVersion(v) => {
99
  write!(f, "unknown version byte: 0x{v:02X}")
@@ -129,8 +136,8 @@ impl SchemeFile {
129
  return Err(SchemeError::BadMagic(magic));
130
  }
131
 
132
- let version = SchemeVersion::from_byte(data[4])
133
- .ok_or(SchemeError::UnknownVersion(data[4]))?;
134
  let expected_payload = version.payload_size();
135
  let actual_payload = data.len() - SCHEME_HEADER_SIZE;
136
 
@@ -153,9 +160,8 @@ impl SchemeFile {
153
  // Fill remaining extended options with defaults
154
  if actual_payload < SCHEME_PAYLOAD_V3 {
155
  let defaults_start = actual_payload.saturating_sub(SCHEME_PAYLOAD_V2);
156
- payload[actual_payload..SCHEME_PAYLOAD_V3].copy_from_slice(
157
- &EXTENDED_OPTIONS_DEFAULTS[defaults_start..],
158
- );
159
  }
160
  return Ok(SchemeFile { version, payload });
161
  }
@@ -477,25 +483,25 @@ pub const SCHEME_WEAPON_ORDER: [&str; 64] = [
477
  "Invisibility", // 43
478
  "Damage x2", // 44
479
  // V2 super weapons (45-63)
480
- "Freeze", // 45
481
- "Super Banana Bomb", // 46
482
- "Mine Strike", // 47
483
- "Girder Starter Pack", // 48
484
- "Earthquake", // 49
485
- "Scales of Justice", // 50
486
- "Ming Vase", // 51
487
- "Mike's Carpet Bomb", // 52
488
  "Patsy's Magic Bullet", // 53
489
- "Indian Nuclear Test", // 54
490
- "Select Worm", // 55
491
- "Salvation Army", // 56
492
- "Mole Squadron", // 57
493
- "MB Bomb", // 58
494
- "Concrete Donkey", // 59
495
- "Suicide Bomber", // 60
496
- "Sheep Strike", // 61
497
- "Mail Strike", // 62
498
- "Armageddon", // 63
499
  ];
500
 
501
  /// V3 extended options (110 bytes, payload offset 0x124).
@@ -907,7 +913,7 @@ impl ExtendedOptions {
907
  && b[0x69] < 6 // rw_kaos_mod
908
  && b[0x6A].wrapping_sub(1) < 7 // sheep_heavens_gate [1,7]
909
  && is_bool(b[0x6B]) // conserve_instant_utilities
910
- && is_bool(b[0x6C]) // expedite_instant_utilities
911
  }
912
  }
913
 
@@ -931,7 +937,7 @@ pub const EXTENDED_OPTIONS_DEFAULTS: [u8; EXTENDED_OPTIONS_SIZE] = [
931
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +0x50
932
  0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // +0x58
933
  0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // +0x60
934
- 0x01, 0x00, 0x07, 0x00, 0x00, 0x01, // +0x68
935
  ];
936
 
937
  // === Typed accessors on SchemeFile ===
@@ -1087,7 +1093,10 @@ mod tests {
1087
 
1088
  assert!(matches!(
1089
  SchemeFile::from_bytes(&data),
1090
- Err(SchemeError::PayloadMismatch { expected: 0xD8, got: 10 })
 
 
 
1091
  ));
1092
  }
1093
 
@@ -1098,7 +1107,7 @@ mod tests {
1098
  data.extend_from_slice(b"SCHM");
1099
  data.push(0x03);
1100
  data.extend_from_slice(&[0xAA; SCHEME_PAYLOAD_V2]); // V2 portion filled with 0xAA
1101
- // No extended options bytes — should be padded with defaults
1102
 
1103
  let scheme = SchemeFile::from_bytes(&data).expect("should accept short V3");
1104
  assert_eq!(scheme.version, SchemeVersion::V3);
 
90
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91
  match self {
92
  SchemeError::TooShort { len } => {
93
+ write!(
94
+ f,
95
+ "file too short ({len} bytes, need at least {SCHEME_HEADER_SIZE})"
96
+ )
97
  }
98
  SchemeError::BadMagic(m) => {
99
+ write!(
100
+ f,
101
+ "bad magic: {:02X} {:02X} {:02X} {:02X} (expected SCHM)",
102
+ m[0], m[1], m[2], m[3]
103
+ )
104
  }
105
  SchemeError::UnknownVersion(v) => {
106
  write!(f, "unknown version byte: 0x{v:02X}")
 
136
  return Err(SchemeError::BadMagic(magic));
137
  }
138
 
139
+ let version =
140
+ SchemeVersion::from_byte(data[4]).ok_or(SchemeError::UnknownVersion(data[4]))?;
141
  let expected_payload = version.payload_size();
142
  let actual_payload = data.len() - SCHEME_HEADER_SIZE;
143
 
 
160
  // Fill remaining extended options with defaults
161
  if actual_payload < SCHEME_PAYLOAD_V3 {
162
  let defaults_start = actual_payload.saturating_sub(SCHEME_PAYLOAD_V2);
163
+ payload[actual_payload..SCHEME_PAYLOAD_V3]
164
+ .copy_from_slice(&EXTENDED_OPTIONS_DEFAULTS[defaults_start..]);
 
165
  }
166
  return Ok(SchemeFile { version, payload });
167
  }
 
483
  "Invisibility", // 43
484
  "Damage x2", // 44
485
  // V2 super weapons (45-63)
486
+ "Freeze", // 45
487
+ "Super Banana Bomb", // 46
488
+ "Mine Strike", // 47
489
+ "Girder Starter Pack", // 48
490
+ "Earthquake", // 49
491
+ "Scales of Justice", // 50
492
+ "Ming Vase", // 51
493
+ "Mike's Carpet Bomb", // 52
494
  "Patsy's Magic Bullet", // 53
495
+ "Indian Nuclear Test", // 54
496
+ "Select Worm", // 55
497
+ "Salvation Army", // 56
498
+ "Mole Squadron", // 57
499
+ "MB Bomb", // 58
500
+ "Concrete Donkey", // 59
501
+ "Suicide Bomber", // 60
502
+ "Sheep Strike", // 61
503
+ "Mail Strike", // 62
504
+ "Armageddon", // 63
505
  ];
506
 
507
  /// V3 extended options (110 bytes, payload offset 0x124).
 
913
  && b[0x69] < 6 // rw_kaos_mod
914
  && b[0x6A].wrapping_sub(1) < 7 // sheep_heavens_gate [1,7]
915
  && is_bool(b[0x6B]) // conserve_instant_utilities
916
+ && is_bool(b[0x6C]) // expedite_instant_utilities
917
  }
918
  }
919
 
 
937
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +0x50
938
  0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // +0x58
939
  0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // +0x60
940
+ 0x01, 0x00, 0x07, 0x00, 0x00, 0x01, // +0x68
941
  ];
942
 
943
  // === Typed accessors on SchemeFile ===
 
1093
 
1094
  assert!(matches!(
1095
  SchemeFile::from_bytes(&data),
1096
+ Err(SchemeError::PayloadMismatch {
1097
+ expected: 0xD8,
1098
+ got: 10
1099
+ })
1100
  ));
1101
  }
1102
 
 
1107
  data.extend_from_slice(b"SCHM");
1108
  data.push(0x03);
1109
  data.extend_from_slice(&[0xAA; SCHEME_PAYLOAD_V2]); // V2 portion filled with 0xAA
1110
+ // No extended options bytes — should be padded with defaults
1111
 
1112
  let scheme = SchemeFile::from_bytes(&data).expect("should accept short V3");
1113
  assert_eq!(scheme.version, SchemeVersion::V3);
crates/openwa-core/src/input/controller.rs CHANGED
@@ -31,5 +31,7 @@ pub struct InputCtrlVtable {
31
 
32
  impl InputCtrl {
33
  /// Vtable[0]: Destroy and optionally free (flags & 1 = free).
34
- pub unsafe fn destroy(&mut self, flags: u32) { vcall!(self, destructor, flags) }
 
 
35
  }
 
31
 
32
  impl InputCtrl {
33
  /// Vtable[0]: Destroy and optionally free (flags & 1 = free).
34
+ pub unsafe fn destroy(&mut self, flags: u32) {
35
+ vcall!(self, destructor, flags)
36
+ }
37
  }
crates/openwa-core/src/lib.rs CHANGED
@@ -10,12 +10,12 @@ pub mod game;
10
  pub mod input;
11
  pub mod render;
12
 
 
13
  pub mod fixed;
14
  pub mod log;
15
- pub mod task;
16
- pub mod address;
17
  pub mod rebase;
 
 
18
  pub mod wa;
19
- pub mod wa_call;
20
  pub mod wa_alloc;
21
- pub mod vtable;
 
10
  pub mod input;
11
  pub mod render;
12
 
13
+ pub mod address;
14
  pub mod fixed;
15
  pub mod log;
 
 
16
  pub mod rebase;
17
+ pub mod task;
18
+ pub mod vtable;
19
  pub mod wa;
 
20
  pub mod wa_alloc;
21
+ pub mod wa_call;
crates/openwa-core/src/render/queue.rs CHANGED
@@ -112,10 +112,10 @@ pub struct DrawRectCmd {
112
  pub command_type: u32, // 0
113
  pub layer: u32,
114
  pub color: u32,
115
- pub x1: u32, // & 0xFFFF0000
116
- pub y1: u32, // & 0xFFFF0000
117
- pub x2: u32, // & 0xFFFF0000
118
- pub y2: u32, // & 0xFFFF0000
119
  pub y_clip: u32, // EDX & 0xFFFF0000
120
  }
121
 
@@ -127,8 +127,8 @@ pub struct DrawSpriteOffsetCmd {
127
  pub command_type: u32, // 6
128
  pub layer: u32,
129
  pub sprite_id: u32,
130
- pub x_pos: u32, // & 0xFFFF0000
131
- pub y_pos: u32, // & 0xFFFF0000
132
  pub _reserved: u32, // hardcoded 0
133
  pub y_clip: u32, // EDX & 0xFFFF0000
134
  pub param_7: u32,
@@ -142,8 +142,8 @@ const _: () = assert!(core::mem::size_of::<DrawSpriteOffsetCmd>() == 0x24);
142
  pub struct DrawBitmapGlobalCmd {
143
  pub command_type: u32, // 1
144
  pub layer: u32,
145
- pub x_pos: u32, // & 0xFFFF0000
146
- pub y_pos: u32, // EDX & 0xFFFF0000
147
  pub bitmap_ptr: u32,
148
  pub _reserved: u32, // hardcoded 0
149
  pub param_6: u32,
 
112
  pub command_type: u32, // 0
113
  pub layer: u32,
114
  pub color: u32,
115
+ pub x1: u32, // & 0xFFFF0000
116
+ pub y1: u32, // & 0xFFFF0000
117
+ pub x2: u32, // & 0xFFFF0000
118
+ pub y2: u32, // & 0xFFFF0000
119
  pub y_clip: u32, // EDX & 0xFFFF0000
120
  }
121
 
 
127
  pub command_type: u32, // 6
128
  pub layer: u32,
129
  pub sprite_id: u32,
130
+ pub x_pos: u32, // & 0xFFFF0000
131
+ pub y_pos: u32, // & 0xFFFF0000
132
  pub _reserved: u32, // hardcoded 0
133
  pub y_clip: u32, // EDX & 0xFFFF0000
134
  pub param_7: u32,
 
142
  pub struct DrawBitmapGlobalCmd {
143
  pub command_type: u32, // 1
144
  pub layer: u32,
145
+ pub x_pos: u32, // & 0xFFFF0000
146
+ pub y_pos: u32, // EDX & 0xFFFF0000
147
  pub bitmap_ptr: u32,
148
  pub _reserved: u32, // hardcoded 0
149
  pub param_6: u32,
crates/openwa-core/src/render/sprite.rs CHANGED
@@ -723,7 +723,6 @@ impl TryFrom<u32> for SpriteId {
723
  // Sprite struct layout (from Ghidra + wkJellyWorm Sprites.h)
724
  // ---------------------------------------------------------------------------
725
 
726
-
727
  /// Per-frame metadata within a Sprite (0x0C bytes).
728
  ///
729
  /// Describes the bounding box and bitmap data offset for one animation frame.
 
723
  // Sprite struct layout (from Ghidra + wkJellyWorm Sprites.h)
724
  // ---------------------------------------------------------------------------
725
 
 
726
  /// Per-frame metadata within a Sprite (0x0C bytes).
727
  ///
728
  /// Describes the bounding box and bitmap data offset for one animation frame.
crates/openwa-core/src/task/base.rs CHANGED
@@ -1,5 +1,5 @@
1
- use crate::game::class_type::ClassType;
2
  use crate::engine::ddgame::DDGame;
 
3
 
4
  /// Base task class in WA's entity hierarchy.
5
  ///
@@ -114,7 +114,9 @@ impl SharedDataTable {
114
  /// # Safety
115
  /// `ptr` must point to a valid shared-data region of at least 256 × 4 = 1024 bytes.
116
  pub unsafe fn from_ptr(ptr: *mut u8) -> Self {
117
- Self { buckets: ptr as *const *mut SharedDataNode }
 
 
118
  }
119
 
120
  /// Construct from a `CTask` pointer (reads `task.shared_data`).
 
 
1
  use crate::engine::ddgame::DDGame;
2
+ use crate::game::class_type::ClassType;
3
 
4
  /// Base task class in WA's entity hierarchy.
5
  ///
 
114
  /// # Safety
115
  /// `ptr` must point to a valid shared-data region of at least 256 × 4 = 1024 bytes.
116
  pub unsafe fn from_ptr(ptr: *mut u8) -> Self {
117
+ Self {
118
+ buckets: ptr as *const *mut SharedDataNode,
119
+ }
120
  }
121
 
122
  /// Construct from a `CTask` pointer (reads `task.shared_data`).
crates/openwa-core/src/task/cloud.rs CHANGED
@@ -1,5 +1,5 @@
1
- use crate::fixed::Fixed;
2
  use super::base::CTask;
 
3
 
4
  /// Airstrike / weather cloud task.
5
  ///
 
 
1
  use super::base::CTask;
2
+ use crate::fixed::Fixed;
3
 
4
  /// Airstrike / weather cloud task.
5
  ///
crates/openwa-core/src/task/game_task.rs CHANGED
@@ -1,5 +1,5 @@
1
- use crate::fixed::Fixed;
2
  use super::base::CTask;
 
3
 
4
  /// Game task - extends CTask with physics and gameplay data.
5
  ///
 
 
1
  use super::base::CTask;
2
+ use crate::fixed::Fixed;
3
 
4
  /// Game task - extends CTask with physics and gameplay data.
5
  ///
crates/openwa-core/src/task/missile.rs CHANGED
@@ -1,5 +1,5 @@
1
- use crate::fixed::Fixed;
2
  use super::game_task::CGameTask;
 
3
 
4
  /// Projectile / missile entity task.
5
  ///
@@ -24,7 +24,6 @@ pub struct CTaskMissile {
24
  pub base: CGameTask,
25
 
26
  // ---- 0xFC–0x12F: missile init fields ----
27
-
28
  /// 0xFC–0x10F: Unknown missile flags and state
29
  pub _unknown_fc: [u8; 0x14],
30
  /// 0x110: Unknown
@@ -47,7 +46,6 @@ pub struct CTaskMissile {
47
  pub slot_id: u32,
48
 
49
  // ---- 0x130–0x15B: spawn data (11 DWORDs, from param_4) ----
50
-
51
  /// 0x130–0x15B: Spawn parameters (11 DWORDs copied from param_4).
52
  ///
53
  /// Runtime-confirmed indices:
@@ -65,7 +63,6 @@ pub struct CTaskMissile {
65
  pub spawn_params: [u32; 0x0B],
66
 
67
  // ---- 0x15C–0x2D3: weapon/scheme data (94 DWORDs, from param_3) ----
68
-
69
  /// 0x15C–0x2D3: Weapon/scheme properties (94 DWORDs copied verbatim from param_3).
70
  ///
71
  /// The WGT scheme blob is split into two logical halves:
@@ -85,7 +82,6 @@ pub struct CTaskMissile {
85
  pub weapon_data: [u32; 0x5E],
86
 
87
  // ---- 0x2D4–0x37B: render/physics parameters (42 DWORDs) ----
88
-
89
  /// 0x2D4–0x37B: Per-projectile render and physics parameters (42 DWORDs).
90
  ///
91
  /// This is NOT a separate data block — it is a shifted copy of weapon_data:
@@ -107,7 +103,6 @@ pub struct CTaskMissile {
107
  pub render_data: [u32; 0x2A],
108
 
109
  // ---- 0x37C–0x41B: post-render physics and state ----
110
-
111
  /// 0x37C–0x39F: Post-render dynamic state. render_data[0x11] is copied to
112
  /// [0x37C] by the constructor; physics updates the values each frame.
113
  pub _unknown_37c: [u8; 0x24],
 
 
1
  use super::game_task::CGameTask;
2
+ use crate::fixed::Fixed;
3
 
4
  /// Projectile / missile entity task.
5
  ///
 
24
  pub base: CGameTask,
25
 
26
  // ---- 0xFC–0x12F: missile init fields ----
 
27
  /// 0xFC–0x10F: Unknown missile flags and state
28
  pub _unknown_fc: [u8; 0x14],
29
  /// 0x110: Unknown
 
46
  pub slot_id: u32,
47
 
48
  // ---- 0x130–0x15B: spawn data (11 DWORDs, from param_4) ----
 
49
  /// 0x130–0x15B: Spawn parameters (11 DWORDs copied from param_4).
50
  ///
51
  /// Runtime-confirmed indices:
 
63
  pub spawn_params: [u32; 0x0B],
64
 
65
  // ---- 0x15C–0x2D3: weapon/scheme data (94 DWORDs, from param_3) ----
 
66
  /// 0x15C–0x2D3: Weapon/scheme properties (94 DWORDs copied verbatim from param_3).
67
  ///
68
  /// The WGT scheme blob is split into two logical halves:
 
82
  pub weapon_data: [u32; 0x5E],
83
 
84
  // ---- 0x2D4–0x37B: render/physics parameters (42 DWORDs) ----
 
85
  /// 0x2D4–0x37B: Per-projectile render and physics parameters (42 DWORDs).
86
  ///
87
  /// This is NOT a separate data block — it is a shifted copy of weapon_data:
 
103
  pub render_data: [u32; 0x2A],
104
 
105
  // ---- 0x37C–0x41B: post-render physics and state ----
 
106
  /// 0x37C–0x39F: Post-render dynamic state. render_data[0x11] is copied to
107
  /// [0x37C] by the constructor; physics updates the values each frame.
108
  pub _unknown_37c: [u8; 0x24],
crates/openwa-core/src/task/mod.rs CHANGED
@@ -1,25 +1,25 @@
1
  mod base;
2
  mod cloud;
3
- mod supply_crate;
4
  mod filter;
5
  mod fire;
6
  mod game_task;
7
  mod mine_oil_drum;
8
  mod missile;
9
  mod overlays;
 
10
  mod team;
11
  mod turn_game;
12
  mod worm;
13
 
14
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
15
  pub use cloud::CTaskCloud;
16
- pub use supply_crate::CTaskCrate;
17
  pub use filter::CTaskFilter;
18
  pub use fire::CTaskFire;
19
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
20
  pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
21
  pub use missile::{CTaskMissile, MissileType};
22
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
 
23
  pub use team::CTaskTeam;
24
  pub use turn_game::{CTaskTurnGame, TurnGameCtx};
25
  pub use worm::{CTaskWorm, CTaskWormVTable};
 
1
  mod base;
2
  mod cloud;
 
3
  mod filter;
4
  mod fire;
5
  mod game_task;
6
  mod mine_oil_drum;
7
  mod missile;
8
  mod overlays;
9
+ mod supply_crate;
10
  mod team;
11
  mod turn_game;
12
  mod worm;
13
 
14
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
15
  pub use cloud::CTaskCloud;
 
16
  pub use filter::CTaskFilter;
17
  pub use fire::CTaskFire;
18
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
19
  pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
20
  pub use missile::{CTaskMissile, MissileType};
21
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
22
+ pub use supply_crate::CTaskCrate;
23
  pub use team::CTaskTeam;
24
  pub use turn_game::{CTaskTurnGame, TurnGameCtx};
25
  pub use worm::{CTaskWorm, CTaskWormVTable};
crates/openwa-core/src/task/overlays.rs CHANGED
@@ -1,6 +1,6 @@
1
- use crate::fixed::Fixed;
2
  use super::base::CTask;
3
  use super::game_task::{CGameTask, SoundEmitter};
 
4
 
5
  /// Bungee trail rendering task fields.
6
  ///
 
 
1
  use super::base::CTask;
2
  use super::game_task::{CGameTask, SoundEmitter};
3
+ use crate::fixed::Fixed;
4
 
5
  /// Bungee trail rendering task fields.
6
  ///
crates/openwa-core/src/task/team.rs CHANGED
@@ -1,5 +1,5 @@
1
- use crate::fixed::Fixed;
2
  use super::base::CTask;
 
3
 
4
  /// Per-team state-tracker task — one instance per team, child of CTaskTurnGame.
5
  ///
 
 
1
  use super::base::CTask;
2
+ use crate::fixed::Fixed;
3
 
4
  /// Per-team state-tracker task — one instance per team, child of CTaskTurnGame.
5
  ///
crates/openwa-core/src/task/turn_game.rs CHANGED
@@ -1,5 +1,5 @@
1
- use crate::fixed::Fixed;
2
  use super::base::CTask;
 
3
 
4
  /// Embedded intermediate game-context sub-object within `CTaskTurnGame`.
5
  ///
@@ -100,7 +100,6 @@ pub struct CTaskTurnGame {
100
  pub game_ctx: TurnGameCtx,
101
 
102
  // ---- CTaskTurnGame-specific fields (0xDC onwards) ----
103
-
104
  pub _unknown_dc: u32,
105
  pub _unknown_e0: u32,
106
  /// 0xE4: turn-seed: ~(DDGame+0x45EC % 9000), used for random initialisation.
 
 
1
  use super::base::CTask;
2
+ use crate::fixed::Fixed;
3
 
4
  /// Embedded intermediate game-context sub-object within `CTaskTurnGame`.
5
  ///
 
100
  pub game_ctx: TurnGameCtx,
101
 
102
  // ---- CTaskTurnGame-specific fields (0xDC onwards) ----
 
103
  pub _unknown_dc: u32,
104
  pub _unknown_e0: u32,
105
  /// 0xE4: turn-seed: ~(DDGame+0x45EC % 9000), used for random initialisation.
crates/openwa-core/src/task/worm.rs CHANGED
@@ -31,8 +31,7 @@ pub struct CTaskWormVTable {
31
  pub _inherited_4_to_6: [*const (); 3],
32
  /// [7] 0x0050D5D0: OnContactEntity — handles physical contact with another entity;
33
  /// worm-on-dying-worm → apply crush damage; projectile close-pass → SetState(0x80)
34
- pub on_contact_entity:
35
- unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
36
  /// [8] 0x0050D9A0: OnWormPush — post-contact worm-worm push impulse;
37
  /// deduplicates via a recent-collision table, then adjusts pos_x or pos_y
38
  pub on_worm_push: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
 
31
  pub _inherited_4_to_6: [*const (); 3],
32
  /// [7] 0x0050D5D0: OnContactEntity — handles physical contact with another entity;
33
  /// worm-on-dying-worm → apply crush damage; projectile close-pass → SetState(0x80)
34
+ pub on_contact_entity: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
 
35
  /// [8] 0x0050D9A0: OnWormPush — post-contact worm-worm push impulse;
36
  /// deduplicates via a recent-collision table, then adjusts pos_x or pos_y
37
  pub on_worm_push: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
crates/openwa-core/src/wa/frontend.rs CHANGED
@@ -1,7 +1,7 @@
1
  //! WA-internal frontend function wrappers.
2
 
3
- use crate::rebase::rb;
4
  use crate::address::va;
 
5
 
6
  /// Frontend__PaletteAnimation (0x422180)
7
  ///
 
1
  //! WA-internal frontend function wrappers.
2
 
 
3
  use crate::address::va;
4
+ use crate::rebase::rb;
5
 
6
  /// Frontend__PaletteAnimation (0x422180)
7
  ///
crates/openwa-core/src/wa/mfc.rs CHANGED
@@ -1,8 +1,8 @@
1
  //! MFC library function wrappers.
2
 
 
3
  use crate::rebase::rb;
4
  use crate::wa_call;
5
- use crate::address::va;
6
 
7
  /// Zero-cost handle to a CWnd-derived MFC window (raw pointer as u32).
8
  #[derive(Clone, Copy)]
@@ -84,8 +84,7 @@ impl CStringRef {
84
  /// FUN_004A39F0: EDX=resource_id, stack param=dest CString object pointer.
85
  pub unsafe fn assign_resource(&mut self, resource_id: u32) {
86
  let addr = rb(va::CSTRING_ASSIGN_RESOURCE);
87
- let f: unsafe extern "fastcall" fn(u32, u32, u32) =
88
- core::mem::transmute(addr);
89
  // ECX is overwritten by the function (loads from global), so any value works.
90
  // EDX = resource_id, stack = self.ptr
91
  f(0, resource_id, self.ptr);
 
1
  //! MFC library function wrappers.
2
 
3
+ use crate::address::va;
4
  use crate::rebase::rb;
5
  use crate::wa_call;
 
6
 
7
  /// Zero-cost handle to a CWnd-derived MFC window (raw pointer as u32).
8
  #[derive(Clone, Copy)]
 
84
  /// FUN_004A39F0: EDX=resource_id, stack param=dest CString object pointer.
85
  pub unsafe fn assign_resource(&mut self, resource_id: u32) {
86
  let addr = rb(va::CSTRING_ASSIGN_RESOURCE);
87
+ let f: unsafe extern "fastcall" fn(u32, u32, u32) = core::mem::transmute(addr);
 
88
  // ECX is overwritten by the function (loads from global), so any value works.
89
  // EDX = resource_id, stack = self.ptr
90
  f(0, resource_id, self.ptr);
crates/openwa-core/src/wa_alloc.rs CHANGED
@@ -18,8 +18,7 @@ pub unsafe fn wa_malloc(size: u32) -> *mut u8 {
18
  /// # Safety
19
  /// `ptr` must have been returned by `wa_malloc` (or null, which is a no-op).
20
  pub unsafe fn wa_free(ptr: *mut u8) {
21
- let f: unsafe extern "cdecl" fn(*mut u8) =
22
- core::mem::transmute(rb(va::WA_FREE) as usize);
23
  f(ptr);
24
  }
25
 
 
18
  /// # Safety
19
  /// `ptr` must have been returned by `wa_malloc` (or null, which is a no-op).
20
  pub unsafe fn wa_free(ptr: *mut u8) {
21
+ let f: unsafe extern "cdecl" fn(*mut u8) = core::mem::transmute(rb(va::WA_FREE) as usize);
 
22
  f(ptr);
23
  }
24
 
crates/openwa-core/src/wa_call.rs CHANGED
@@ -14,8 +14,7 @@ pub unsafe fn read_global(ghidra_addr: u32) -> u32 {
14
  /// Uses the fastcall trick: ECX = this, EDX = dummy.
15
  #[inline]
16
  pub unsafe fn thiscall_1(ghidra_addr: u32, this: u32, arg1: u32) {
17
- let f: unsafe extern "fastcall" fn(u32, u32, u32) =
18
- core::mem::transmute(rb(ghidra_addr));
19
  f(this, 0, arg1);
20
  }
21
 
@@ -32,15 +31,13 @@ pub unsafe fn thiscall_1_ret(ghidra_addr: u32, this: u32, arg1: u32) -> u32 {
32
  #[inline]
33
  pub unsafe fn thiscall_indirect_1(vtable_slot_addr: u32, this: u32, arg1: u32) {
34
  let fn_ptr = *(vtable_slot_addr as *const u32);
35
- let f: unsafe extern "fastcall" fn(u32, u32, u32) =
36
- core::mem::transmute(fn_ptr);
37
  f(this, 0, arg1);
38
  }
39
 
40
  /// Call a stdcall function with 2 arguments.
41
  #[inline]
42
  pub unsafe fn stdcall_2(ghidra_addr: u32, arg1: u32, arg2: u32) {
43
- let f: unsafe extern "stdcall" fn(u32, u32) =
44
- core::mem::transmute(rb(ghidra_addr));
45
  f(arg1, arg2);
46
  }
 
14
  /// Uses the fastcall trick: ECX = this, EDX = dummy.
15
  #[inline]
16
  pub unsafe fn thiscall_1(ghidra_addr: u32, this: u32, arg1: u32) {
17
+ let f: unsafe extern "fastcall" fn(u32, u32, u32) = core::mem::transmute(rb(ghidra_addr));
 
18
  f(this, 0, arg1);
19
  }
20
 
 
31
  #[inline]
32
  pub unsafe fn thiscall_indirect_1(vtable_slot_addr: u32, this: u32, arg1: u32) {
33
  let fn_ptr = *(vtable_slot_addr as *const u32);
34
+ let f: unsafe extern "fastcall" fn(u32, u32, u32) = core::mem::transmute(fn_ptr);
 
35
  f(this, 0, arg1);
36
  }
37
 
38
  /// Call a stdcall function with 2 arguments.
39
  #[inline]
40
  pub unsafe fn stdcall_2(ghidra_addr: u32, arg1: u32, arg2: u32) {
41
+ let f: unsafe extern "stdcall" fn(u32, u32) = core::mem::transmute(rb(ghidra_addr));
 
42
  f(arg1, arg2);
43
  }
crates/openwa-core/tests/scheme_parse.rs CHANGED
@@ -1,8 +1,8 @@
1
  use std::path::Path;
2
 
3
  use openwa_core::game::scheme::{
4
- ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS,
5
- EXTENDED_OPTIONS_SIZE, SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, WEAPONS_V1_COUNT,
6
  };
7
 
8
  const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
@@ -66,7 +66,7 @@ fn beginner_options() {
66
  let opts = scheme.options();
67
 
68
  // Verified against hex dump of beginner.wsc payload bytes
69
- assert_eq!(opts.hot_seat_delay, 10); // 0x0A
70
  assert_eq!(opts.retreat_time, 5);
71
  assert_eq!(opts.rope_retreat_time, 5);
72
  assert_eq!(opts.display_total_round_time, true);
@@ -74,10 +74,10 @@ fn beginner_options() {
74
  assert_eq!(opts.fall_damage, 0);
75
  assert_eq!(opts.artillery_mode, false);
76
  assert_eq!(opts.bounty_mode, 0);
77
- assert_eq!(opts.stockpiling, 1); // On
78
- assert_eq!(opts.worm_select, 1); // Manual
79
  assert_eq!(opts.sudden_death_event, 3); // Nothing
80
- assert_eq!(opts.worm_energy, 150); // 0x96
81
  }
82
 
83
  #[test]
@@ -94,9 +94,9 @@ fn classic_options() {
94
  assert_eq!(opts.automatic_replays, true);
95
  assert_eq!(opts.fall_damage, 1);
96
  assert_eq!(opts.artillery_mode, false);
97
- assert_eq!(opts.stockpiling, 0); // Off
98
- assert_eq!(opts.worm_select, 1); // Manual
99
- assert_eq!(opts.worm_energy, 100); // hex: 0x64
100
  }
101
 
102
  #[test]
@@ -181,7 +181,8 @@ fn v2_super_weapons_zeroed_region() {
181
  /// Parse all .wsc files from the game directory if available.
182
  #[test]
183
  fn parse_all_game_schemes() {
184
- let schemes_dir = Path::new("I:/games/SteamLibrary/steamapps/common/Worms Armageddon/User/Schemes");
 
185
  if !schemes_dir.exists() {
186
  eprintln!("Skipping: game schemes directory not found");
187
  return;
@@ -228,7 +229,7 @@ fn make_valid_extended_options() -> [u8; EXTENDED_OPTIONS_SIZE] {
228
  b[0x34..0x38].copy_from_slice(&1i32.to_le_bytes()); // max_projectile_speed
229
  b[0x38..0x3C].copy_from_slice(&1i32.to_le_bytes()); // max_rope_speed
230
  b[0x3C..0x40].copy_from_slice(&1i32.to_le_bytes()); // max_jet_pack_speed
231
- // game_engine_speed in [0x1000, 0x800000]
232
  b[0x40..0x44].copy_from_slice(&0x10000i32.to_le_bytes());
233
  // sheep_heavens_gate in [1, 7]
234
  b[0x6A] = 1;
 
1
  use std::path::Path;
2
 
3
  use openwa_core::game::scheme::{
4
+ ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, EXTENDED_OPTIONS_SIZE,
5
+ SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, WEAPONS_V1_COUNT,
6
  };
7
 
8
  const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
 
66
  let opts = scheme.options();
67
 
68
  // Verified against hex dump of beginner.wsc payload bytes
69
+ assert_eq!(opts.hot_seat_delay, 10); // 0x0A
70
  assert_eq!(opts.retreat_time, 5);
71
  assert_eq!(opts.rope_retreat_time, 5);
72
  assert_eq!(opts.display_total_round_time, true);
 
74
  assert_eq!(opts.fall_damage, 0);
75
  assert_eq!(opts.artillery_mode, false);
76
  assert_eq!(opts.bounty_mode, 0);
77
+ assert_eq!(opts.stockpiling, 1); // On
78
+ assert_eq!(opts.worm_select, 1); // Manual
79
  assert_eq!(opts.sudden_death_event, 3); // Nothing
80
+ assert_eq!(opts.worm_energy, 150); // 0x96
81
  }
82
 
83
  #[test]
 
94
  assert_eq!(opts.automatic_replays, true);
95
  assert_eq!(opts.fall_damage, 1);
96
  assert_eq!(opts.artillery_mode, false);
97
+ assert_eq!(opts.stockpiling, 0); // Off
98
+ assert_eq!(opts.worm_select, 1); // Manual
99
+ assert_eq!(opts.worm_energy, 100); // hex: 0x64
100
  }
101
 
102
  #[test]
 
181
  /// Parse all .wsc files from the game directory if available.
182
  #[test]
183
  fn parse_all_game_schemes() {
184
+ let schemes_dir =
185
+ Path::new("I:/games/SteamLibrary/steamapps/common/Worms Armageddon/User/Schemes");
186
  if !schemes_dir.exists() {
187
  eprintln!("Skipping: game schemes directory not found");
188
  return;
 
229
  b[0x34..0x38].copy_from_slice(&1i32.to_le_bytes()); // max_projectile_speed
230
  b[0x38..0x3C].copy_from_slice(&1i32.to_le_bytes()); // max_rope_speed
231
  b[0x3C..0x40].copy_from_slice(&1i32.to_le_bytes()); // max_jet_pack_speed
232
+ // game_engine_speed in [0x1000, 0x800000]
233
  b[0x40..0x44].copy_from_slice(&0x10000i32.to_le_bytes());
234
  // sheep_heavens_gate in [1, 7]
235
  b[0x6A] = 1;
crates/openwa-debugui/src/app.rs CHANGED
@@ -2,7 +2,9 @@ use eframe::egui;
2
  use openwa_core::address::va;
3
  use openwa_core::engine::{DDGame, DDGameWrapper};
4
  use openwa_core::rebase::rb;
5
- use openwa_core::task::{CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx};
 
 
6
 
7
  use crate::log;
8
 
@@ -11,31 +13,30 @@ use crate::log;
11
  // ---------------------------------------------------------------------------
12
 
13
  const KNOWN_VTABLES: &[(u32, &str)] = &[
14
- (va::CTASK_WORM_VTABLE, "CTaskWorm"),
15
- (va::CTASK_LAND_VTABLE, "CTaskLand"),
16
- (va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"),
17
- (va::CTASK_TEAM_VTABLE, "CTaskTeam"),
18
- (va::CTASK_FILTER_VTABLE, "CTaskFilter"),
19
- (va::CTASK_DIRT_VTABLE, "CTaskDirt"),
20
  (va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"),
21
- (va::CTASK_CPU_VTABLE, "CTaskCPU"),
22
- (va::CTASK_MISSILE_VTABLE, "CTaskMissile"),
23
- (va::CTASK_MINE_VTABLE, "CTaskMine"),
24
- (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"),
25
- (va::CTASK_CRATE_VTABLE, "CTaskCrate"),
26
- (va::CTASK_CLOUD_VTABLE, "CTaskCloud"),
27
- (va::CTASK_SEA_BUBBLE_VTABLE, "CTaskSeaBubble"),
28
- (va::CTASK_FIRE_VTABLE, "CTaskFire"),
29
  ];
30
 
31
  /// Vtables of entities that are created/destroyed every frame (particles,
32
  /// bubbles, etc.). Filtered from the census by default to reduce noise.
33
- const TRANSIENT_VTABLES: &[u32] = &[
34
- va::CTASK_SEA_BUBBLE_VTABLE,
35
- ];
36
 
37
  fn vtable_name(runtime_vtable: u32) -> Option<&'static str> {
38
- KNOWN_VTABLES.iter()
 
39
  .find(|&&(ghidra_va, _)| rb(ghidra_va) == runtime_vtable)
40
  .map(|&(_, name)| name)
41
  }
@@ -43,7 +44,9 @@ fn vtable_name(runtime_vtable: u32) -> Option<&'static str> {
43
  /// Returns a display name for the entity at `addr`.
44
  /// Tries the known-vtable map first; falls back to CTask.class_type.
45
  unsafe fn entity_type_name(addr: u32) -> String {
46
- if addr == 0 { return "(null)".to_owned(); }
 
 
47
  let vtable = *(addr as *const u32);
48
  if let Some(name) = vtable_name(vtable) {
49
  return name.to_owned();
@@ -54,7 +57,9 @@ unsafe fn entity_type_name(addr: u32) -> String {
54
 
55
  /// One-line label for a task: "TypeName @ 0xADDR"
56
  unsafe fn entity_label(addr: u32) -> String {
57
- if addr == 0 { return "(null)".to_owned(); }
 
 
58
  format!("{} @ {:#010X}", entity_type_name(addr), addr)
59
  }
60
 
@@ -65,11 +70,17 @@ unsafe fn entity_label(addr: u32) -> String {
65
  /// Returns a pointer to DDGame, or None if not in-game.
66
  unsafe fn get_ddgame() -> Option<*const DDGame> {
67
  let session_ptr = *(rb(va::G_GAME_SESSION) as *const u32);
68
- if session_ptr == 0 { return None; }
 
 
69
  let wrapper_addr = *((session_ptr + 0xA0) as *const u32);
70
- if wrapper_addr == 0 { return None; }
 
 
71
  let ddgame_ptr = (*(wrapper_addr as *const DDGameWrapper)).ddgame;
72
- if ddgame_ptr.is_null() { return None; }
 
 
73
  Some(ddgame_ptr)
74
  }
75
 
@@ -81,8 +92,10 @@ unsafe fn get_ddgame() -> Option<*const DDGame> {
81
  /// that bound so the caller can filter nulls and display the live set.
82
  unsafe fn read_children(task: *const CTask) -> Vec<u32> {
83
  let slots = (*task).children_watermark as usize;
84
- let data = (*task).children_data as *const u32;
85
- if data.is_null() || slots == 0 { return Vec::new(); }
 
 
86
  // Hard safety cap: 4096 slots × 4 bytes = 16 KB max read
87
  let slots = slots.min(4096);
88
  (0..slots).map(|i| *data.add(i)).collect()
@@ -97,11 +110,15 @@ unsafe fn read_children(task: *const CTask) -> Vec<u32> {
97
  /// MAX_DEPTH steps (guard against corrupt/circular pointers).
98
  unsafe fn find_root_task(ddgame: *const DDGame) -> Option<u32> {
99
  let task_land = (*ddgame).task_land as u32;
100
- if task_land == 0 { return None; }
 
 
101
  let mut current = task_land;
102
  for _ in 0..64 {
103
  let parent = (*(current as *const CTask)).parent as u32;
104
- if parent == 0 { return Some(current); }
 
 
105
  current = parent;
106
  }
107
  None // chain didn't terminate — corrupt data
@@ -110,23 +127,31 @@ unsafe fn find_root_task(ddgame: *const DDGame) -> Option<u32> {
110
  /// DFS the task tree from `root`, returning (vtable, addr) for every node.
111
  /// A visited set prevents infinite loops from corrupt/circular pointers.
112
  unsafe fn collect_task_tree(root: u32) -> Vec<(u32, u32)> {
113
- let mut out = Vec::new();
114
- let mut stack = vec![root];
115
  let mut visited = std::collections::HashSet::new();
116
  while let Some(addr) = stack.pop() {
117
- if addr == 0 || !visited.insert(addr) { continue; }
 
 
118
  let vtable = *(addr as *const u32);
119
  out.push((vtable, addr));
120
  for child in read_children(addr as *const CTask) {
121
- if child != 0 { stack.push(child); }
 
 
122
  }
123
  }
124
  out
125
  }
126
 
127
  unsafe fn collect_live_entities() -> Vec<(u32, u32)> {
128
- let Some(ddgame) = get_ddgame() else { return Vec::new(); };
129
- let Some(root) = find_root_task(ddgame) else { return Vec::new(); };
 
 
 
 
130
  collect_task_tree(root)
131
  }
132
 
@@ -147,7 +172,12 @@ pub struct DebugApp {
147
 
148
  impl Default for DebugApp {
149
  fn default() -> Self {
150
- Self { selected_entity: None, nav_history: Vec::new(), log_auto_scroll: true, show_transient: false }
 
 
 
 
 
151
  }
152
  }
153
 
@@ -203,8 +233,12 @@ impl eframe::App for DebugApp {
203
  let mut navigate_to: Option<u32> = None;
204
  let mut go_back = false;
205
  self.show_inspector(ui, &mut navigate_to, &mut go_back);
206
- if go_back { self.navigate_back(); }
207
- if let Some(addr) = navigate_to { self.navigate_to(addr); }
 
 
 
 
208
  });
209
 
210
  egui::TopBottomPanel::bottom("log_panel")
@@ -217,7 +251,9 @@ impl eframe::App for DebugApp {
217
  egui::CentralPanel::default().show(ctx, |ui| {
218
  let mut navigate_to: Option<u32> = None;
219
  self.show_census(ui, &live_entities, &mut navigate_to);
220
- if let Some(addr) = navigate_to { self.navigate_to(addr); }
 
 
221
  });
222
  }
223
  }
@@ -227,7 +263,12 @@ impl eframe::App for DebugApp {
227
  // ---------------------------------------------------------------------------
228
 
229
  impl DebugApp {
230
- fn show_census(&mut self, ui: &mut egui::Ui, rows: &[(u32, u32)], navigate_to: &mut Option<u32>) {
 
 
 
 
 
231
  ui.heading("Entity Census");
232
 
233
  if rows.is_empty() {
@@ -239,44 +280,46 @@ impl DebugApp {
239
  rows.iter().collect()
240
  } else {
241
  rows.iter()
242
- .filter(|&&(vtable, _)| {
243
- !TRANSIENT_VTABLES.iter().any(|&t| rb(t) == vtable)
244
- })
245
  .collect()
246
  };
247
 
248
  if visible.len() == rows.len() {
249
  ui.label(format!("{} entities", rows.len()));
250
  } else {
251
- ui.label(format!("{} entities ({} hidden transient)", visible.len(), rows.len() - visible.len()));
 
 
 
 
252
  }
253
  ui.separator();
254
 
255
  egui::ScrollArea::vertical()
256
  .auto_shrink([false, false])
257
  .show(ui, |ui| {
258
- egui::Grid::new("census_grid")
259
- .striped(true)
260
- .num_columns(3)
261
- .min_col_width(60.0)
262
- .show(ui, |ui| {
263
- ui.strong("Type");
264
- ui.strong("Address");
265
- ui.strong("Vtable");
266
- ui.end_row();
267
-
268
- for &&(vtable, entity) in &visible {
269
- let name = unsafe { entity_type_name(entity) };
270
- let is_selected = self.selected_entity == Some(entity);
271
- if ui.selectable_label(is_selected, name).clicked() {
272
- *navigate_to = Some(entity);
273
- }
274
- ui.label(format!("{:#010X}", entity));
275
- ui.label(format!("{:#010X}", vtable));
276
  ui.end_row();
277
- }
278
- });
279
- });
 
 
 
 
 
 
 
 
 
 
280
  }
281
  }
282
 
@@ -325,12 +368,16 @@ unsafe fn show_game_task_raw_fields(
325
  let base = addr as *const u32;
326
 
327
  let label_for = |off: usize| -> Option<&str> {
328
- CGAMETASK_FIELD_LABELS.iter()
 
329
  .find(|&&(o, _)| o == off)
330
  .map(|&(_, n)| n)
331
- .or_else(|| extra_labels.iter()
332
- .find(|&&(o, _)| o == off)
333
- .map(|&(_, n)| n))
 
 
 
334
  };
335
 
336
  // Sections: CTask base, CGameTask unknowns, pos/speed, more unknowns, emitter,
@@ -346,14 +393,19 @@ unsafe fn show_game_task_raw_fields(
346
  let mut chunk_start = 0x0FC;
347
  while chunk_start < total_size {
348
  let chunk_end = (chunk_start + 0x80).min(total_size);
349
- sections.push((chunk_start, chunk_end,
350
- format!("{} +0x{:03X}..0x{:03X}", type_name, chunk_start, chunk_end)));
 
 
 
351
  chunk_start = chunk_end;
352
  }
353
 
354
  for (start, end, section_name) in &sections {
355
  let (start, end) = (*start, *end);
356
- if start >= total_size { break; }
 
 
357
  let end = end.min(total_size);
358
  let header = format!("{} (0x{:03X}..0x{:03X})", section_name, start, end);
359
  let default_open = false;
@@ -402,7 +454,10 @@ impl DebugApp {
402
  ui.horizontal(|ui| {
403
  ui.heading("Inspector");
404
  ui.add_space(8.0);
405
- if ui.add_enabled(!self.nav_history.is_empty(), egui::Button::new("← Back")).clicked() {
 
 
 
406
  *go_back = true;
407
  }
408
  if !self.nav_history.is_empty() {
@@ -422,312 +477,471 @@ impl DebugApp {
422
 
423
  egui::ScrollArea::vertical()
424
  .auto_shrink([false, false])
425
- .show(ui, |ui| { unsafe {
426
- let vtable = *(addr as *const u32);
427
- let name = entity_type_name(addr);
428
-
429
- ui.label(format!("Entity: {:#010X}", addr));
430
- ui.label(format!("Type: {}", name));
431
- ui.label(format!("Vtable: {:#010X}", vtable));
432
- ui.separator();
433
-
434
- let task = addr as *const CTask;
435
-
436
- // --- CTask base ---
437
- egui::CollapsingHeader::new("CTask base")
438
- .default_open(true)
439
- .show(ui, |ui| {
440
- egui::Grid::new("ctask_grid").striped(true).show(ui, |ui| {
441
- // Parent — clickable link
442
- let parent = (*task).parent as u32;
443
- ui.label("parent");
444
- if parent != 0 {
445
- if ui.link(entity_label(parent)).clicked() {
446
- *navigate_to = Some(parent);
447
- }
448
- } else {
449
- ui.label("(none)");
450
- }
451
- ui.end_row();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
- // children_watermark = total insertions (sparse array); live = non-null slots
454
- ui.label("child slots"); ui.label(format!("{} watermark / {} cap", (*task).children_watermark, (*task).children_capacity)); ui.end_row();
455
- ui.label("class_type"); ui.label(format!("{:?}", (*task).class_type)); ui.end_row();
456
- });
457
- });
458
-
459
- // --- Children tree ---
460
- // read_children returns all slots (sparse); filter nulls for live count.
461
- let children = read_children(task);
462
- let live_count = children.iter().filter(|&&a| a != 0).count();
463
- let slot_count = (*task).children_watermark as usize;
464
- if live_count > 0 || slot_count > 0 {
465
- egui::CollapsingHeader::new(format!("Children ({} live / {} slots)", live_count, slot_count))
466
- .default_open(true)
467
- .show(ui, |ui| {
468
- for child_addr in &children {
469
- let child_addr = *child_addr;
470
- if child_addr == 0 { continue; }
471
- let child_name = entity_type_name(child_addr);
472
-
473
- // Expand inline if the child itself has children
474
- let child_task = child_addr as *const CTask;
475
- let grandchildren = read_children(child_task);
476
- let grandchild_count = grandchildren.iter().filter(|&&a| a != 0).count();
477
-
478
- if grandchild_count > 0 {
479
- // Show as a sub-collapsing header with its own children
480
- let header_label = format!(
481
- "{} @ {:#010X} ({} children)",
482
- child_name, child_addr, grandchild_count
483
- );
484
- egui::CollapsingHeader::new(&header_label)
485
- .id_source(child_addr)
486
- .default_open(false)
487
- .show(ui, |ui| {
488
- // Link to inspect this child in detail
489
- if ui.link("→ Inspect").clicked() {
490
- *navigate_to = Some(child_addr);
491
- }
492
- // Show grandchildren
493
- for gc_addr in &grandchildren {
494
- let gc_addr = *gc_addr;
495
- if gc_addr == 0 { continue; }
496
- if ui.link(entity_label(gc_addr)).clicked() {
497
- *navigate_to = Some(gc_addr);
498
  }
499
- }
500
- });
501
- } else {
502
- // Leaf child — single clickable link
503
- let label = format!(" {} @ {:#010X}", child_name, child_addr);
504
- if ui.link(label).clicked() {
505
- *navigate_to = Some(child_addr);
 
 
 
 
 
 
 
 
 
 
506
  }
507
  }
508
- }
509
- });
510
- }
511
-
512
- // --- CTaskMine-specific fields ---
513
- if name == "CTaskMine" {
514
- show_game_task_raw_fields(ui, addr, "CTaskMine", 0x128, &[
515
- (0x0FC, "unknown_fc[0]"),
516
- (0x100, "unknown_fc[4]"),
517
- (0x104, "unknown_fc[8]"),
518
- (0x108, "unknown_fc[C]"),
519
- (0x10C, "unknown_fc[10]"),
520
- (0x110, "slot_id"),
521
- (0x114, "unknown_114"),
522
- (0x118, "fuse_timer"),
523
- (0x11C, "unknown_11c"),
524
- (0x120, "unknown_120"),
525
- (0x124, "owner_team"),
526
- ]);
527
- }
528
-
529
- // --- CTaskOilDrum-specific fields ---
530
- if name == "CTaskOilDrum" {
531
- show_game_task_raw_fields(ui, addr, "CTaskOilDrum", 0x110, &[
532
- (0x0FC, "triggered"),
533
- (0x100, "slot_id"),
534
- (0x104, "unknown_104"),
535
- (0x108, "health"),
536
- (0x10C, "roll_counter"),
537
- ]);
538
- }
539
 
540
- // --- CTaskCrate-specific fields ---
541
- if name == "CTaskCrate" {
542
- show_game_task_raw_fields(ui, addr, "CTaskCrate", 0x4B0, &[
543
- (0x0FC, "unknown_fc"),
544
- (0x100, "slot_id"),
545
- (0x104, "unknown_104"),
546
- (0x108, "unknown_108"),
547
- (0x10C, "timer"),
548
- // scheme_data starts at 0x110 (0xE5 DWORDs)
549
- (0x124, "crate_type"),
550
- (0x24C, "heal_amount"),
551
- (0x264, "extra_init_flag"),
552
- (0x4A4, "unknown_4a4"),
553
- (0x4A8, "sequence_ref"),
554
- (0x4AC, "unknown_4ac"),
555
- ]);
556
- }
 
 
 
 
 
557
 
558
- // --- CTaskCloud-specific fields ---
559
- if name == "CTaskCloud" {
560
- let cloud = addr as *const CTaskCloud;
561
- egui::CollapsingHeader::new("CTaskCloud")
562
- .default_open(true)
563
- .show(ui, |ui| {
564
- egui::Grid::new("cloud_grid").striped(true).show(ui, |ui| {
565
- ui.label("pos_x"); ui.label(format!("{:.1}", (*cloud).pos_x.to_f32())); ui.end_row();
566
- ui.label("pos_y"); ui.label(format!("{:.1}", (*cloud).pos_y.to_f32())); ui.end_row();
567
- ui.label("vel_x"); ui.label(format!("{:.4}", (*cloud).vel_x.to_f32())); ui.end_row();
568
- ui.label("vel_y"); ui.label(format!("{:.4}", (*cloud).vel_y.to_f32())); ui.end_row();
569
- ui.label("wind_accel"); ui.label(format!("{:.4}", (*cloud).wind_accel.to_f32())); ui.end_row();
570
- ui.label("wind_target"); ui.label(format!("{:.4}", (*cloud).wind_target.to_f32())); ui.end_row();
571
- ui.label("layer_depth"); ui.label(format!("{:.1}", (*cloud).layer_depth.to_f32())); ui.end_row();
572
- ui.label("sprite_id"); ui.label(format!("{:#06X}", (*cloud).sprite_id)); ui.end_row();
573
- });
574
- });
575
- }
576
 
577
- // --- CTaskTurnGame-specific fields ---
578
- if name == "CTaskTurnGame" {
579
- let tg = addr as *const CTaskTurnGame;
580
- egui::CollapsingHeader::new("CTaskTurnGame")
581
- .default_open(true)
582
- .show(ui, |ui| {
583
- egui::Grid::new("tg_grid").striped(true).show(ui, |ui| {
584
- ui.label("current_team"); ui.label(format!("{}", (*tg).current_team)); ui.end_row();
585
- ui.label("current_worm"); ui.label(format!("{}", (*tg).current_worm)); ui.end_row();
586
- ui.label("arena_team"); ui.label(format!("{}", (*tg).arena_team)); ui.end_row();
587
- ui.label("arena_worm"); ui.label(format!("{}", (*tg).arena_worm)); ui.end_row();
588
- ui.label("worm_active"); ui.label(format!("{}", (*tg).worm_active != 0)); ui.end_row();
589
- ui.label("turn_ended"); ui.label(format!("{}", (*tg).turn_ended != 0)); ui.end_row();
590
- ui.label("no_time_lim"); ui.label(format!("{}", (*tg).no_time_limit != 0)); ui.end_row();
591
- ui.label("turn_timer"); ui.label(format!("{:.1}s", (*tg).turn_timer as f32 / 1000.0)); ui.end_row();
592
- ui.label("retreat"); ui.label(format!("{:.1}s", (*tg).retreat_timer as f32 / 1000.0)); ui.end_row();
593
- ui.label("idle_timer"); ui.label(format!("{:.1}s", (*tg).idle_timer as f32 / 1000.0)); ui.end_row();
594
- ui.label("num_teams"); ui.label(format!("{}", (*tg).num_teams)); ui.end_row();
595
- ui.label("active_frm"); ui.label(format!("{}", (*tg).active_worm_frames)); ui.end_row();
596
- ui.label("retreat_frm"); ui.label(format!("{}", (*tg).retreat_frames)); ui.end_row();
597
- });
598
- });
 
599
 
600
- let ctx = &(*tg).game_ctx as *const TurnGameCtx;
601
- egui::CollapsingHeader::new("TurnGameCtx (+0x30)")
602
- .default_open(false)
603
- .show(ui, |ui| {
604
- egui::Grid::new("ctx_grid").striped(true).show(ui, |ui| {
605
- ui.label("land_height"); ui.label(format!("{:.1}", (*ctx).land_height.to_f32())); ui.end_row();
606
- ui.label("land_height_2"); ui.label(format!("{:.1}", (*ctx).land_height_2.to_f32())); ui.end_row();
607
- ui.label("sentinel_18"); ui.label(format!("{}", (*ctx)._sentinel_18)); ui.end_row();
608
- ui.label("sentinel_28"); ui.label(format!("{}", (*ctx)._sentinel_28)); ui.end_row();
609
- ui.label("sentinel_38"); ui.label(format!("{}", (*ctx)._sentinel_38)); ui.end_row();
610
- ui.label("team_count"); ui.label(format!("{}", (*ctx).team_count)); ui.end_row();
611
- ui.label("_slot_d0"); ui.label(format!("{}", (*ctx)._slot_d0)); ui.end_row();
612
- let ta = (*ctx)._hud_textbox_a;
613
- let tb = (*ctx)._hud_textbox_b;
614
- ui.label("hud_tb_a"); ui.label(if ta == 0 { "(null)".into() } else { format!("{:#010X}", ta) }); ui.end_row();
615
- ui.label("hud_tb_b"); ui.label(if tb == 0 { "(null)".into() } else { format!("{:#010X}", tb) }); ui.end_row();
616
- });
617
- });
618
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
- // --- CTaskTeam-specific fields ---
621
- if name == "CTaskTeam" {
622
- let team = addr as *const CTaskTeam;
623
- egui::CollapsingHeader::new("CTaskTeam")
624
- .default_open(true)
625
- .show(ui, |ui| {
626
- egui::Grid::new("team_grid").striped(true).show(ui, |ui| {
627
- ui.label("team_index"); ui.label(format!("{}", (*team).team_index)); ui.end_row();
628
- ui.label("alive_worms"); ui.label(format!("{}", (*team).alive_worm_count)); ui.end_row();
629
- ui.label("worm_count"); ui.label(format!("{}", (*team).worm_count)); ui.end_row();
630
- ui.label("last_weapon"); ui.label(format!("{}", (*team).last_launched_weapon)); ui.end_row();
631
- ui.label("pos_x"); ui.label(format!("{:.2}", (*team).pos_x.to_f32())); ui.end_row();
632
- ui.label("pos_y"); ui.label(format!("{:.2}", (*team).pos_y.to_f32())); ui.end_row();
633
- });
634
- });
635
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
 
637
- // --- CTaskFire-specific fields ---
638
- if name == "CTaskFire" {
639
- let fire = addr as *const CTaskFire;
640
- egui::CollapsingHeader::new("CTaskFire")
641
- .default_open(true)
642
- .show(ui, |ui| {
643
- egui::Grid::new("fire_grid").striped(true).show(ui, |ui| {
644
- ui.label("spawn_x"); ui.label(format!("{:.1}", (*fire).spawn_x.to_f32())); ui.end_row();
645
- ui.label("spawn_y"); ui.label(format!("{:.1}", (*fire).spawn_y.to_f32())); ui.end_row();
646
- ui.label("timer"); ui.label(format!("{}", (*fire).timer)); ui.end_row();
647
- ui.label("burn_rate"); ui.label(format!("{}", (*fire).burn_rate)); ui.end_row();
648
- ui.label("spread_ctr"); ui.label(format!("{}", (*fire).spread_counter)); ui.end_row();
649
- ui.label("lifetime"); ui.label(format!("{}", (*fire).lifetime)); ui.end_row();
650
- ui.label("slot_index"); ui.label(format!("{}", (*fire).slot_index)); ui.end_row();
651
- });
652
- });
653
- }
 
 
 
 
 
 
 
 
 
 
 
654
 
655
- // --- CTaskWorm-specific fields ---
656
- if name == "CTaskWorm" || (*task).class_type == openwa_core::game::ClassType::Worm {
657
- // Summary header with key info
658
- let worm = addr as *const CTaskWorm;
659
- let name_bytes = &(*worm).worm_name;
660
- let nul = name_bytes.iter().position(|&b| b == 0).unwrap_or(name_bytes.len());
661
- let worm_name = std::str::from_utf8(&name_bytes[..nul]).unwrap_or("?");
662
- ui.label(format!("Worm: \"{}\" state={:#04X} team={} idx={}",
663
- worm_name, (*worm).state(), (*worm).team_index, (*worm).worm_index));
664
- ui.separator();
665
-
666
- show_game_task_raw_fields(ui, addr, "CTaskWorm", 0x3FC, &[
667
- (0x0FC, "team_index"),
668
- (0x100, "worm_index"),
669
- (0x104, "unknown_104"),
670
- (0x110, "spawn_params[0]"),
671
- (0x114, "spawn_params[1]"),
672
- (0x118, "spawn_params[2]"),
673
- (0x11C, "spawn_params[3]"),
674
- (0x120, "spawn_params[4]"),
675
- (0x124, "spawn_params[5]"),
676
- (0x128, "spawn_params[6]"),
677
- (0x12C, "spawn_params[7]"),
678
- (0x130, "spawn_params[8]"),
679
- (0x134, "spawn_params[9]"),
680
- (0x158, "slot_id"),
681
- (0x164, "stationary_frames"),
682
- (0x170, "selected_weapon"),
683
- (0x1A8, "facing_dir_2"),
684
- (0x1AC, "facing_dir_inv"),
685
- (0x1EC, "movement_streak"),
686
- (0x24C, "aim_angle"),
687
- (0x268, "show_cursor"),
688
- (0x2F0, "worm_name[0..4]"),
689
- (0x2F4, "worm_name[4..8]"),
690
- (0x2F8, "worm_name[8..12]"),
691
- (0x2FC, "worm_name[12..16]"),
692
- (0x334, "facing_dir_3"),
693
- (0x368, "animator"),
694
- (0x3DC, "facing_direction"),
695
- (0x3E4, "input_aim_up"),
696
- (0x3E8, "input_aim_down"),
697
- (0x3EC, "input_move_left"),
698
- (0x3F0, "input_move_right"),
699
- ]);
700
- }
701
 
702
- // --- CTaskMissile-specific fields ---
703
- if name == "CTaskMissile" {
704
- use openwa_core::task::CTaskMissile;
705
- let m = &*(addr as *const CTaskMissile);
706
- ui.label(format!("Missile: type={:?} slot={} homing={} dir={}",
707
- m.missile_type(), m.slot_id, m.homing_enabled, m.direction));
708
- ui.separator();
709
-
710
- show_game_task_raw_fields(ui, addr, "CTaskMissile", 0x41C, &[
711
- (0x128, "launch_seed"),
712
- (0x12C, "slot_id"),
713
- (0x130, "spawn: owner_id"),
714
- (0x134, "spawn: [1]"),
715
- (0x138, "spawn: spawn_x"),
716
- (0x13C, "spawn: spawn_y"),
717
- (0x140, "spawn: speed_x"),
718
- (0x144, "spawn: speed_y"),
719
- (0x148, "spawn: cursor_x"),
720
- (0x14C, "spawn: cursor_y"),
721
- (0x150, "spawn: pellet_idx"),
722
- (0x154, "spawn: fb_timer"),
723
- (0x158, "spawn: fb_param"),
724
- (0x3A0, "launch_speed"),
725
- (0x3A8, "homing_enabled"),
726
- (0x3C8, "direction"),
727
- ]);
728
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
- }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  }
732
  }
733
 
 
2
  use openwa_core::address::va;
3
  use openwa_core::engine::{DDGame, DDGameWrapper};
4
  use openwa_core::rebase::rb;
5
+ use openwa_core::task::{
6
+ CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx,
7
+ };
8
 
9
  use crate::log;
10
 
 
13
  // ---------------------------------------------------------------------------
14
 
15
  const KNOWN_VTABLES: &[(u32, &str)] = &[
16
+ (va::CTASK_WORM_VTABLE, "CTaskWorm"),
17
+ (va::CTASK_LAND_VTABLE, "CTaskLand"),
18
+ (va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"),
19
+ (va::CTASK_TEAM_VTABLE, "CTaskTeam"),
20
+ (va::CTASK_FILTER_VTABLE, "CTaskFilter"),
21
+ (va::CTASK_DIRT_VTABLE, "CTaskDirt"),
22
  (va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"),
23
+ (va::CTASK_CPU_VTABLE, "CTaskCPU"),
24
+ (va::CTASK_MISSILE_VTABLE, "CTaskMissile"),
25
+ (va::CTASK_MINE_VTABLE, "CTaskMine"),
26
+ (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"),
27
+ (va::CTASK_CRATE_VTABLE, "CTaskCrate"),
28
+ (va::CTASK_CLOUD_VTABLE, "CTaskCloud"),
29
+ (va::CTASK_SEA_BUBBLE_VTABLE, "CTaskSeaBubble"),
30
+ (va::CTASK_FIRE_VTABLE, "CTaskFire"),
31
  ];
32
 
33
  /// Vtables of entities that are created/destroyed every frame (particles,
34
  /// bubbles, etc.). Filtered from the census by default to reduce noise.
35
+ const TRANSIENT_VTABLES: &[u32] = &[va::CTASK_SEA_BUBBLE_VTABLE];
 
 
36
 
37
  fn vtable_name(runtime_vtable: u32) -> Option<&'static str> {
38
+ KNOWN_VTABLES
39
+ .iter()
40
  .find(|&&(ghidra_va, _)| rb(ghidra_va) == runtime_vtable)
41
  .map(|&(_, name)| name)
42
  }
 
44
  /// Returns a display name for the entity at `addr`.
45
  /// Tries the known-vtable map first; falls back to CTask.class_type.
46
  unsafe fn entity_type_name(addr: u32) -> String {
47
+ if addr == 0 {
48
+ return "(null)".to_owned();
49
+ }
50
  let vtable = *(addr as *const u32);
51
  if let Some(name) = vtable_name(vtable) {
52
  return name.to_owned();
 
57
 
58
  /// One-line label for a task: "TypeName @ 0xADDR"
59
  unsafe fn entity_label(addr: u32) -> String {
60
+ if addr == 0 {
61
+ return "(null)".to_owned();
62
+ }
63
  format!("{} @ {:#010X}", entity_type_name(addr), addr)
64
  }
65
 
 
70
  /// Returns a pointer to DDGame, or None if not in-game.
71
  unsafe fn get_ddgame() -> Option<*const DDGame> {
72
  let session_ptr = *(rb(va::G_GAME_SESSION) as *const u32);
73
+ if session_ptr == 0 {
74
+ return None;
75
+ }
76
  let wrapper_addr = *((session_ptr + 0xA0) as *const u32);
77
+ if wrapper_addr == 0 {
78
+ return None;
79
+ }
80
  let ddgame_ptr = (*(wrapper_addr as *const DDGameWrapper)).ddgame;
81
+ if ddgame_ptr.is_null() {
82
+ return None;
83
+ }
84
  Some(ddgame_ptr)
85
  }
86
 
 
92
  /// that bound so the caller can filter nulls and display the live set.
93
  unsafe fn read_children(task: *const CTask) -> Vec<u32> {
94
  let slots = (*task).children_watermark as usize;
95
+ let data = (*task).children_data as *const u32;
96
+ if data.is_null() || slots == 0 {
97
+ return Vec::new();
98
+ }
99
  // Hard safety cap: 4096 slots × 4 bytes = 16 KB max read
100
  let slots = slots.min(4096);
101
  (0..slots).map(|i| *data.add(i)).collect()
 
110
  /// MAX_DEPTH steps (guard against corrupt/circular pointers).
111
  unsafe fn find_root_task(ddgame: *const DDGame) -> Option<u32> {
112
  let task_land = (*ddgame).task_land as u32;
113
+ if task_land == 0 {
114
+ return None;
115
+ }
116
  let mut current = task_land;
117
  for _ in 0..64 {
118
  let parent = (*(current as *const CTask)).parent as u32;
119
+ if parent == 0 {
120
+ return Some(current);
121
+ }
122
  current = parent;
123
  }
124
  None // chain didn't terminate — corrupt data
 
127
  /// DFS the task tree from `root`, returning (vtable, addr) for every node.
128
  /// A visited set prevents infinite loops from corrupt/circular pointers.
129
  unsafe fn collect_task_tree(root: u32) -> Vec<(u32, u32)> {
130
+ let mut out = Vec::new();
131
+ let mut stack = vec![root];
132
  let mut visited = std::collections::HashSet::new();
133
  while let Some(addr) = stack.pop() {
134
+ if addr == 0 || !visited.insert(addr) {
135
+ continue;
136
+ }
137
  let vtable = *(addr as *const u32);
138
  out.push((vtable, addr));
139
  for child in read_children(addr as *const CTask) {
140
+ if child != 0 {
141
+ stack.push(child);
142
+ }
143
  }
144
  }
145
  out
146
  }
147
 
148
  unsafe fn collect_live_entities() -> Vec<(u32, u32)> {
149
+ let Some(ddgame) = get_ddgame() else {
150
+ return Vec::new();
151
+ };
152
+ let Some(root) = find_root_task(ddgame) else {
153
+ return Vec::new();
154
+ };
155
  collect_task_tree(root)
156
  }
157
 
 
172
 
173
  impl Default for DebugApp {
174
  fn default() -> Self {
175
+ Self {
176
+ selected_entity: None,
177
+ nav_history: Vec::new(),
178
+ log_auto_scroll: true,
179
+ show_transient: false,
180
+ }
181
  }
182
  }
183
 
 
233
  let mut navigate_to: Option<u32> = None;
234
  let mut go_back = false;
235
  self.show_inspector(ui, &mut navigate_to, &mut go_back);
236
+ if go_back {
237
+ self.navigate_back();
238
+ }
239
+ if let Some(addr) = navigate_to {
240
+ self.navigate_to(addr);
241
+ }
242
  });
243
 
244
  egui::TopBottomPanel::bottom("log_panel")
 
251
  egui::CentralPanel::default().show(ctx, |ui| {
252
  let mut navigate_to: Option<u32> = None;
253
  self.show_census(ui, &live_entities, &mut navigate_to);
254
+ if let Some(addr) = navigate_to {
255
+ self.navigate_to(addr);
256
+ }
257
  });
258
  }
259
  }
 
263
  // ---------------------------------------------------------------------------
264
 
265
  impl DebugApp {
266
+ fn show_census(
267
+ &mut self,
268
+ ui: &mut egui::Ui,
269
+ rows: &[(u32, u32)],
270
+ navigate_to: &mut Option<u32>,
271
+ ) {
272
  ui.heading("Entity Census");
273
 
274
  if rows.is_empty() {
 
280
  rows.iter().collect()
281
  } else {
282
  rows.iter()
283
+ .filter(|&&(vtable, _)| !TRANSIENT_VTABLES.iter().any(|&t| rb(t) == vtable))
 
 
284
  .collect()
285
  };
286
 
287
  if visible.len() == rows.len() {
288
  ui.label(format!("{} entities", rows.len()));
289
  } else {
290
+ ui.label(format!(
291
+ "{} entities ({} hidden transient)",
292
+ visible.len(),
293
+ rows.len() - visible.len()
294
+ ));
295
  }
296
  ui.separator();
297
 
298
  egui::ScrollArea::vertical()
299
  .auto_shrink([false, false])
300
  .show(ui, |ui| {
301
+ egui::Grid::new("census_grid")
302
+ .striped(true)
303
+ .num_columns(3)
304
+ .min_col_width(60.0)
305
+ .show(ui, |ui| {
306
+ ui.strong("Type");
307
+ ui.strong("Address");
308
+ ui.strong("Vtable");
 
 
 
 
 
 
 
 
 
 
309
  ui.end_row();
310
+
311
+ for &&(vtable, entity) in &visible {
312
+ let name = unsafe { entity_type_name(entity) };
313
+ let is_selected = self.selected_entity == Some(entity);
314
+ if ui.selectable_label(is_selected, name).clicked() {
315
+ *navigate_to = Some(entity);
316
+ }
317
+ ui.label(format!("{:#010X}", entity));
318
+ ui.label(format!("{:#010X}", vtable));
319
+ ui.end_row();
320
+ }
321
+ });
322
+ });
323
  }
324
  }
325
 
 
368
  let base = addr as *const u32;
369
 
370
  let label_for = |off: usize| -> Option<&str> {
371
+ CGAMETASK_FIELD_LABELS
372
+ .iter()
373
  .find(|&&(o, _)| o == off)
374
  .map(|&(_, n)| n)
375
+ .or_else(|| {
376
+ extra_labels
377
+ .iter()
378
+ .find(|&&(o, _)| o == off)
379
+ .map(|&(_, n)| n)
380
+ })
381
  };
382
 
383
  // Sections: CTask base, CGameTask unknowns, pos/speed, more unknowns, emitter,
 
393
  let mut chunk_start = 0x0FC;
394
  while chunk_start < total_size {
395
  let chunk_end = (chunk_start + 0x80).min(total_size);
396
+ sections.push((
397
+ chunk_start,
398
+ chunk_end,
399
+ format!("{} +0x{:03X}..0x{:03X}", type_name, chunk_start, chunk_end),
400
+ ));
401
  chunk_start = chunk_end;
402
  }
403
 
404
  for (start, end, section_name) in &sections {
405
  let (start, end) = (*start, *end);
406
+ if start >= total_size {
407
+ break;
408
+ }
409
  let end = end.min(total_size);
410
  let header = format!("{} (0x{:03X}..0x{:03X})", section_name, start, end);
411
  let default_open = false;
 
454
  ui.horizontal(|ui| {
455
  ui.heading("Inspector");
456
  ui.add_space(8.0);
457
+ if ui
458
+ .add_enabled(!self.nav_history.is_empty(), egui::Button::new("← Back"))
459
+ .clicked()
460
+ {
461
  *go_back = true;
462
  }
463
  if !self.nav_history.is_empty() {
 
477
 
478
  egui::ScrollArea::vertical()
479
  .auto_shrink([false, false])
480
+ .show(ui, |ui| {
481
+ unsafe {
482
+ let vtable = *(addr as *const u32);
483
+ let name = entity_type_name(addr);
484
+
485
+ ui.label(format!("Entity: {:#010X}", addr));
486
+ ui.label(format!("Type: {}", name));
487
+ ui.label(format!("Vtable: {:#010X}", vtable));
488
+ ui.separator();
489
+
490
+ let task = addr as *const CTask;
491
+
492
+ // --- CTask base ---
493
+ egui::CollapsingHeader::new("CTask base")
494
+ .default_open(true)
495
+ .show(ui, |ui| {
496
+ egui::Grid::new("ctask_grid").striped(true).show(ui, |ui| {
497
+ // Parent — clickable link
498
+ let parent = (*task).parent as u32;
499
+ ui.label("parent");
500
+ if parent != 0 {
501
+ if ui.link(entity_label(parent)).clicked() {
502
+ *navigate_to = Some(parent);
503
+ }
504
+ } else {
505
+ ui.label("(none)");
506
+ }
507
+ ui.end_row();
508
+
509
+ // children_watermark = total insertions (sparse array); live = non-null slots
510
+ ui.label("child slots");
511
+ ui.label(format!(
512
+ "{} watermark / {} cap",
513
+ (*task).children_watermark,
514
+ (*task).children_capacity
515
+ ));
516
+ ui.end_row();
517
+ ui.label("class_type");
518
+ ui.label(format!("{:?}", (*task).class_type));
519
+ ui.end_row();
520
+ });
521
+ });
522
 
523
+ // --- Children tree ---
524
+ // read_children returns all slots (sparse); filter nulls for live count.
525
+ let children = read_children(task);
526
+ let live_count = children.iter().filter(|&&a| a != 0).count();
527
+ let slot_count = (*task).children_watermark as usize;
528
+ if live_count > 0 || slot_count > 0 {
529
+ egui::CollapsingHeader::new(format!(
530
+ "Children ({} live / {} slots)",
531
+ live_count, slot_count
532
+ ))
533
+ .default_open(true)
534
+ .show(ui, |ui| {
535
+ for child_addr in &children {
536
+ let child_addr = *child_addr;
537
+ if child_addr == 0 {
538
+ continue;
539
+ }
540
+ let child_name = entity_type_name(child_addr);
541
+
542
+ // Expand inline if the child itself has children
543
+ let child_task = child_addr as *const CTask;
544
+ let grandchildren = read_children(child_task);
545
+ let grandchild_count =
546
+ grandchildren.iter().filter(|&&a| a != 0).count();
547
+
548
+ if grandchild_count > 0 {
549
+ // Show as a sub-collapsing header with its own children
550
+ let header_label = format!(
551
+ "{} @ {:#010X} ({} children)",
552
+ child_name, child_addr, grandchild_count
553
+ );
554
+ egui::CollapsingHeader::new(&header_label)
555
+ .id_source(child_addr)
556
+ .default_open(false)
557
+ .show(ui, |ui| {
558
+ // Link to inspect this child in detail
559
+ if ui.link("→ Inspect").clicked() {
560
+ *navigate_to = Some(child_addr);
 
 
 
 
 
 
 
561
  }
562
+ // Show grandchildren
563
+ for gc_addr in &grandchildren {
564
+ let gc_addr = *gc_addr;
565
+ if gc_addr == 0 {
566
+ continue;
567
+ }
568
+ if ui.link(entity_label(gc_addr)).clicked() {
569
+ *navigate_to = Some(gc_addr);
570
+ }
571
+ }
572
+ });
573
+ } else {
574
+ // Leaf child — single clickable link
575
+ let label = format!(" {} @ {:#010X}", child_name, child_addr);
576
+ if ui.link(label).clicked() {
577
+ *navigate_to = Some(child_addr);
578
+ }
579
  }
580
  }
581
+ });
582
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
 
584
+ // --- CTaskMine-specific fields ---
585
+ if name == "CTaskMine" {
586
+ show_game_task_raw_fields(
587
+ ui,
588
+ addr,
589
+ "CTaskMine",
590
+ 0x128,
591
+ &[
592
+ (0x0FC, "unknown_fc[0]"),
593
+ (0x100, "unknown_fc[4]"),
594
+ (0x104, "unknown_fc[8]"),
595
+ (0x108, "unknown_fc[C]"),
596
+ (0x10C, "unknown_fc[10]"),
597
+ (0x110, "slot_id"),
598
+ (0x114, "unknown_114"),
599
+ (0x118, "fuse_timer"),
600
+ (0x11C, "unknown_11c"),
601
+ (0x120, "unknown_120"),
602
+ (0x124, "owner_team"),
603
+ ],
604
+ );
605
+ }
606
 
607
+ // --- CTaskOilDrum-specific fields ---
608
+ if name == "CTaskOilDrum" {
609
+ show_game_task_raw_fields(
610
+ ui,
611
+ addr,
612
+ "CTaskOilDrum",
613
+ 0x110,
614
+ &[
615
+ (0x0FC, "triggered"),
616
+ (0x100, "slot_id"),
617
+ (0x104, "unknown_104"),
618
+ (0x108, "health"),
619
+ (0x10C, "roll_counter"),
620
+ ],
621
+ );
622
+ }
 
 
623
 
624
+ // --- CTaskCrate-specific fields ---
625
+ if name == "CTaskCrate" {
626
+ show_game_task_raw_fields(
627
+ ui,
628
+ addr,
629
+ "CTaskCrate",
630
+ 0x4B0,
631
+ &[
632
+ (0x0FC, "unknown_fc"),
633
+ (0x100, "slot_id"),
634
+ (0x104, "unknown_104"),
635
+ (0x108, "unknown_108"),
636
+ (0x10C, "timer"),
637
+ // scheme_data starts at 0x110 (0xE5 DWORDs)
638
+ (0x124, "crate_type"),
639
+ (0x24C, "heal_amount"),
640
+ (0x264, "extra_init_flag"),
641
+ (0x4A4, "unknown_4a4"),
642
+ (0x4A8, "sequence_ref"),
643
+ (0x4AC, "unknown_4ac"),
644
+ ],
645
+ );
646
+ }
647
 
648
+ // --- CTaskCloud-specific fields ---
649
+ if name == "CTaskCloud" {
650
+ let cloud = addr as *const CTaskCloud;
651
+ egui::CollapsingHeader::new("CTaskCloud")
652
+ .default_open(true)
653
+ .show(ui, |ui| {
654
+ egui::Grid::new("cloud_grid").striped(true).show(ui, |ui| {
655
+ ui.label("pos_x");
656
+ ui.label(format!("{:.1}", (*cloud).pos_x.to_f32()));
657
+ ui.end_row();
658
+ ui.label("pos_y");
659
+ ui.label(format!("{:.1}", (*cloud).pos_y.to_f32()));
660
+ ui.end_row();
661
+ ui.label("vel_x");
662
+ ui.label(format!("{:.4}", (*cloud).vel_x.to_f32()));
663
+ ui.end_row();
664
+ ui.label("vel_y");
665
+ ui.label(format!("{:.4}", (*cloud).vel_y.to_f32()));
666
+ ui.end_row();
667
+ ui.label("wind_accel");
668
+ ui.label(format!("{:.4}", (*cloud).wind_accel.to_f32()));
669
+ ui.end_row();
670
+ ui.label("wind_target");
671
+ ui.label(format!("{:.4}", (*cloud).wind_target.to_f32()));
672
+ ui.end_row();
673
+ ui.label("layer_depth");
674
+ ui.label(format!("{:.1}", (*cloud).layer_depth.to_f32()));
675
+ ui.end_row();
676
+ ui.label("sprite_id");
677
+ ui.label(format!("{:#06X}", (*cloud).sprite_id));
678
+ ui.end_row();
679
+ });
680
+ });
681
+ }
682
 
683
+ // --- CTaskTurnGame-specific fields ---
684
+ if name == "CTaskTurnGame" {
685
+ let tg = addr as *const CTaskTurnGame;
686
+ egui::CollapsingHeader::new("CTaskTurnGame")
687
+ .default_open(true)
688
+ .show(ui, |ui| {
689
+ egui::Grid::new("tg_grid").striped(true).show(ui, |ui| {
690
+ ui.label("current_team");
691
+ ui.label(format!("{}", (*tg).current_team));
692
+ ui.end_row();
693
+ ui.label("current_worm");
694
+ ui.label(format!("{}", (*tg).current_worm));
695
+ ui.end_row();
696
+ ui.label("arena_team");
697
+ ui.label(format!("{}", (*tg).arena_team));
698
+ ui.end_row();
699
+ ui.label("arena_worm");
700
+ ui.label(format!("{}", (*tg).arena_worm));
701
+ ui.end_row();
702
+ ui.label("worm_active");
703
+ ui.label(format!("{}", (*tg).worm_active != 0));
704
+ ui.end_row();
705
+ ui.label("turn_ended");
706
+ ui.label(format!("{}", (*tg).turn_ended != 0));
707
+ ui.end_row();
708
+ ui.label("no_time_lim");
709
+ ui.label(format!("{}", (*tg).no_time_limit != 0));
710
+ ui.end_row();
711
+ ui.label("turn_timer");
712
+ ui.label(format!("{:.1}s", (*tg).turn_timer as f32 / 1000.0));
713
+ ui.end_row();
714
+ ui.label("retreat");
715
+ ui.label(format!(
716
+ "{:.1}s",
717
+ (*tg).retreat_timer as f32 / 1000.0
718
+ ));
719
+ ui.end_row();
720
+ ui.label("idle_timer");
721
+ ui.label(format!("{:.1}s", (*tg).idle_timer as f32 / 1000.0));
722
+ ui.end_row();
723
+ ui.label("num_teams");
724
+ ui.label(format!("{}", (*tg).num_teams));
725
+ ui.end_row();
726
+ ui.label("active_frm");
727
+ ui.label(format!("{}", (*tg).active_worm_frames));
728
+ ui.end_row();
729
+ ui.label("retreat_frm");
730
+ ui.label(format!("{}", (*tg).retreat_frames));
731
+ ui.end_row();
732
+ });
733
+ });
734
+
735
+ let ctx = &(*tg).game_ctx as *const TurnGameCtx;
736
+ egui::CollapsingHeader::new("TurnGameCtx (+0x30)")
737
+ .default_open(false)
738
+ .show(ui, |ui| {
739
+ egui::Grid::new("ctx_grid").striped(true).show(ui, |ui| {
740
+ ui.label("land_height");
741
+ ui.label(format!("{:.1}", (*ctx).land_height.to_f32()));
742
+ ui.end_row();
743
+ ui.label("land_height_2");
744
+ ui.label(format!("{:.1}", (*ctx).land_height_2.to_f32()));
745
+ ui.end_row();
746
+ ui.label("sentinel_18");
747
+ ui.label(format!("{}", (*ctx)._sentinel_18));
748
+ ui.end_row();
749
+ ui.label("sentinel_28");
750
+ ui.label(format!("{}", (*ctx)._sentinel_28));
751
+ ui.end_row();
752
+ ui.label("sentinel_38");
753
+ ui.label(format!("{}", (*ctx)._sentinel_38));
754
+ ui.end_row();
755
+ ui.label("team_count");
756
+ ui.label(format!("{}", (*ctx).team_count));
757
+ ui.end_row();
758
+ ui.label("_slot_d0");
759
+ ui.label(format!("{}", (*ctx)._slot_d0));
760
+ ui.end_row();
761
+ let ta = (*ctx)._hud_textbox_a;
762
+ let tb = (*ctx)._hud_textbox_b;
763
+ ui.label("hud_tb_a");
764
+ ui.label(if ta == 0 {
765
+ "(null)".into()
766
+ } else {
767
+ format!("{:#010X}", ta)
768
+ });
769
+ ui.end_row();
770
+ ui.label("hud_tb_b");
771
+ ui.label(if tb == 0 {
772
+ "(null)".into()
773
+ } else {
774
+ format!("{:#010X}", tb)
775
+ });
776
+ ui.end_row();
777
+ });
778
+ });
779
+ }
780
 
781
+ // --- CTaskTeam-specific fields ---
782
+ if name == "CTaskTeam" {
783
+ let team = addr as *const CTaskTeam;
784
+ egui::CollapsingHeader::new("CTaskTeam")
785
+ .default_open(true)
786
+ .show(ui, |ui| {
787
+ egui::Grid::new("team_grid").striped(true).show(ui, |ui| {
788
+ ui.label("team_index");
789
+ ui.label(format!("{}", (*team).team_index));
790
+ ui.end_row();
791
+ ui.label("alive_worms");
792
+ ui.label(format!("{}", (*team).alive_worm_count));
793
+ ui.end_row();
794
+ ui.label("worm_count");
795
+ ui.label(format!("{}", (*team).worm_count));
796
+ ui.end_row();
797
+ ui.label("last_weapon");
798
+ ui.label(format!("{}", (*team).last_launched_weapon));
799
+ ui.end_row();
800
+ ui.label("pos_x");
801
+ ui.label(format!("{:.2}", (*team).pos_x.to_f32()));
802
+ ui.end_row();
803
+ ui.label("pos_y");
804
+ ui.label(format!("{:.2}", (*team).pos_y.to_f32()));
805
+ ui.end_row();
806
+ });
807
+ });
808
+ }
809
 
810
+ // --- CTaskFire-specific fields ---
811
+ if name == "CTaskFire" {
812
+ let fire = addr as *const CTaskFire;
813
+ egui::CollapsingHeader::new("CTaskFire")
814
+ .default_open(true)
815
+ .show(ui, |ui| {
816
+ egui::Grid::new("fire_grid").striped(true).show(ui, |ui| {
817
+ ui.label("spawn_x");
818
+ ui.label(format!("{:.1}", (*fire).spawn_x.to_f32()));
819
+ ui.end_row();
820
+ ui.label("spawn_y");
821
+ ui.label(format!("{:.1}", (*fire).spawn_y.to_f32()));
822
+ ui.end_row();
823
+ ui.label("timer");
824
+ ui.label(format!("{}", (*fire).timer));
825
+ ui.end_row();
826
+ ui.label("burn_rate");
827
+ ui.label(format!("{}", (*fire).burn_rate));
828
+ ui.end_row();
829
+ ui.label("spread_ctr");
830
+ ui.label(format!("{}", (*fire).spread_counter));
831
+ ui.end_row();
832
+ ui.label("lifetime");
833
+ ui.label(format!("{}", (*fire).lifetime));
834
+ ui.end_row();
835
+ ui.label("slot_index");
836
+ ui.label(format!("{}", (*fire).slot_index));
837
+ ui.end_row();
838
+ });
839
+ });
840
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
841
 
842
+ // --- CTaskWorm-specific fields ---
843
+ if name == "CTaskWorm"
844
+ || (*task).class_type == openwa_core::game::ClassType::Worm
845
+ {
846
+ // Summary header with key info
847
+ let worm = addr as *const CTaskWorm;
848
+ let name_bytes = &(*worm).worm_name;
849
+ let nul = name_bytes
850
+ .iter()
851
+ .position(|&b| b == 0)
852
+ .unwrap_or(name_bytes.len());
853
+ let worm_name = std::str::from_utf8(&name_bytes[..nul]).unwrap_or("?");
854
+ ui.label(format!(
855
+ "Worm: \"{}\" state={:#04X} team={} idx={}",
856
+ worm_name,
857
+ (*worm).state(),
858
+ (*worm).team_index,
859
+ (*worm).worm_index
860
+ ));
861
+ ui.separator();
862
+
863
+ show_game_task_raw_fields(
864
+ ui,
865
+ addr,
866
+ "CTaskWorm",
867
+ 0x3FC,
868
+ &[
869
+ (0x0FC, "team_index"),
870
+ (0x100, "worm_index"),
871
+ (0x104, "unknown_104"),
872
+ (0x110, "spawn_params[0]"),
873
+ (0x114, "spawn_params[1]"),
874
+ (0x118, "spawn_params[2]"),
875
+ (0x11C, "spawn_params[3]"),
876
+ (0x120, "spawn_params[4]"),
877
+ (0x124, "spawn_params[5]"),
878
+ (0x128, "spawn_params[6]"),
879
+ (0x12C, "spawn_params[7]"),
880
+ (0x130, "spawn_params[8]"),
881
+ (0x134, "spawn_params[9]"),
882
+ (0x158, "slot_id"),
883
+ (0x164, "stationary_frames"),
884
+ (0x170, "selected_weapon"),
885
+ (0x1A8, "facing_dir_2"),
886
+ (0x1AC, "facing_dir_inv"),
887
+ (0x1EC, "movement_streak"),
888
+ (0x24C, "aim_angle"),
889
+ (0x268, "show_cursor"),
890
+ (0x2F0, "worm_name[0..4]"),
891
+ (0x2F4, "worm_name[4..8]"),
892
+ (0x2F8, "worm_name[8..12]"),
893
+ (0x2FC, "worm_name[12..16]"),
894
+ (0x334, "facing_dir_3"),
895
+ (0x368, "animator"),
896
+ (0x3DC, "facing_direction"),
897
+ (0x3E4, "input_aim_up"),
898
+ (0x3E8, "input_aim_down"),
899
+ (0x3EC, "input_move_left"),
900
+ (0x3F0, "input_move_right"),
901
+ ],
902
+ );
903
+ }
904
 
905
+ // --- CTaskMissile-specific fields ---
906
+ if name == "CTaskMissile" {
907
+ use openwa_core::task::CTaskMissile;
908
+ let m = &*(addr as *const CTaskMissile);
909
+ ui.label(format!(
910
+ "Missile: type={:?} slot={} homing={} dir={}",
911
+ m.missile_type(),
912
+ m.slot_id,
913
+ m.homing_enabled,
914
+ m.direction
915
+ ));
916
+ ui.separator();
917
+
918
+ show_game_task_raw_fields(
919
+ ui,
920
+ addr,
921
+ "CTaskMissile",
922
+ 0x41C,
923
+ &[
924
+ (0x128, "launch_seed"),
925
+ (0x12C, "slot_id"),
926
+ (0x130, "spawn: owner_id"),
927
+ (0x134, "spawn: [1]"),
928
+ (0x138, "spawn: spawn_x"),
929
+ (0x13C, "spawn: spawn_y"),
930
+ (0x140, "spawn: speed_x"),
931
+ (0x144, "spawn: speed_y"),
932
+ (0x148, "spawn: cursor_x"),
933
+ (0x14C, "spawn: cursor_y"),
934
+ (0x150, "spawn: pellet_idx"),
935
+ (0x154, "spawn: fb_timer"),
936
+ (0x158, "spawn: fb_param"),
937
+ (0x3A0, "launch_speed"),
938
+ (0x3A8, "homing_enabled"),
939
+ (0x3C8, "direction"),
940
+ ],
941
+ );
942
+ }
943
+ }
944
+ });
945
  }
946
  }
947
 
crates/openwa-debugui/src/log.rs CHANGED
@@ -19,13 +19,18 @@ pub fn push(msg: impl Into<String>) {
19
  if b.len() >= 500 {
20
  b.pop_front();
21
  }
22
- b.push_back(LogEntry { ts: Instant::now(), text: msg.into() });
 
 
 
23
  }
24
  }
25
 
26
  /// Snapshot up to `max` of the most recent entries (cloned).
27
  pub fn snapshot(max: usize) -> Vec<(Instant, String)> {
28
- let Ok(b) = buf().lock() else { return Vec::new() };
 
 
29
  let start = b.len().saturating_sub(max);
30
  b.range(start..).map(|e| (e.ts, e.text.clone())).collect()
31
  }
 
19
  if b.len() >= 500 {
20
  b.pop_front();
21
  }
22
+ b.push_back(LogEntry {
23
+ ts: Instant::now(),
24
+ text: msg.into(),
25
+ });
26
  }
27
  }
28
 
29
  /// Snapshot up to `max` of the most recent entries (cloned).
30
  pub fn snapshot(max: usize) -> Vec<(Instant, String)> {
31
+ let Ok(b) = buf().lock() else {
32
+ return Vec::new();
33
+ };
34
  let start = b.len().saturating_sub(max);
35
  b.range(start..).map(|e| (e.ts, e.text.clone())).collect()
36
  }
crates/openwa-harness/src/lib.rs CHANGED
@@ -149,8 +149,8 @@ mod iat {
149
  original_first_thunk: u32, // RVA to INT (Import Name Table)
150
  time_date_stamp: u32,
151
  forwarder_chain: u32,
152
- name: u32, // RVA to DLL name string
153
- first_thunk: u32, // RVA to IAT (Import Address Table) — this is what we patch
154
  }
155
 
156
  /// PE import-by-name entry
@@ -434,8 +434,8 @@ mod tests {
434
  let img = wa();
435
 
436
  let valid_prologues: &[u8] = &[
437
- 0x55, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, 0xB8,
438
- 0x51, 0x52, 0x64, 0x85, 0x8D, 0x53, 0xC2, 0xC3, 0x33,
439
  ];
440
 
441
  // Read all CTask vtable entries and check their prologues
@@ -501,7 +501,10 @@ mod tests {
501
 
502
  *heap_handle_ptr = heap as u32;
503
  *heap_type_ptr = 1; // Simple HeapAlloc mode — no SBH
504
- eprintln!(" CRT heap initialized: handle=0x{:08X}, type=1", heap as u32);
 
 
 
505
  }
506
 
507
  #[test]
@@ -509,7 +512,9 @@ mod tests {
509
  let _lock = mutation_lock().lock().unwrap();
510
  let img = wa();
511
  img.patch_imports().expect("IAT patching failed");
512
- unsafe { init_wa_crt_heap(img); }
 
 
513
 
514
  // thunk_FUN_005c0ab8 is __cdecl: (size) -> ptr
515
  // This is WA's malloc wrapper — calls _malloc, checks for null
@@ -540,7 +545,9 @@ mod tests {
540
  let _lock = mutation_lock().lock().unwrap();
541
  let img = wa();
542
  img.patch_imports().expect("IAT patching failed");
543
- unsafe { init_wa_crt_heap(img); }
 
 
544
 
545
  // Initialize security cookie (needed for stack protection in SEH functions)
546
  const SECURITY_INIT_COOKIE: u32 = 0x005E_7EAB;
@@ -552,7 +559,8 @@ mod tests {
552
  }
553
 
554
  // CTask::Constructor is __stdcall: (this, parent, ddgame) -> this
555
- type CTaskCtorFn = unsafe extern "stdcall" fn(this: *mut u8, parent: u32, ddgame: u32) -> *mut u8;
 
556
 
557
  unsafe {
558
  let ctor: CTaskCtorFn = img.func_ptr(va::CTASK_CONSTRUCTOR);
@@ -571,7 +579,8 @@ mod tests {
571
  let vtable = *(task_ptr as *const u32);
572
  let vtable_ghidra = img.to_ghidra(vtable);
573
  assert_eq!(
574
- vtable_ghidra, va::CTASK_VTABLE,
 
575
  "CTask vtable: expected 0x{:08X}, got 0x{vtable_ghidra:08X}",
576
  va::CTASK_VTABLE
577
  );
 
149
  original_first_thunk: u32, // RVA to INT (Import Name Table)
150
  time_date_stamp: u32,
151
  forwarder_chain: u32,
152
+ name: u32, // RVA to DLL name string
153
+ first_thunk: u32, // RVA to IAT (Import Address Table) — this is what we patch
154
  }
155
 
156
  /// PE import-by-name entry
 
434
  let img = wa();
435
 
436
  let valid_prologues: &[u8] = &[
437
+ 0x55, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, 0xB8, 0x51, 0x52, 0x64, 0x85, 0x8D, 0x53,
438
+ 0xC2, 0xC3, 0x33,
439
  ];
440
 
441
  // Read all CTask vtable entries and check their prologues
 
501
 
502
  *heap_handle_ptr = heap as u32;
503
  *heap_type_ptr = 1; // Simple HeapAlloc mode — no SBH
504
+ eprintln!(
505
+ " CRT heap initialized: handle=0x{:08X}, type=1",
506
+ heap as u32
507
+ );
508
  }
509
 
510
  #[test]
 
512
  let _lock = mutation_lock().lock().unwrap();
513
  let img = wa();
514
  img.patch_imports().expect("IAT patching failed");
515
+ unsafe {
516
+ init_wa_crt_heap(img);
517
+ }
518
 
519
  // thunk_FUN_005c0ab8 is __cdecl: (size) -> ptr
520
  // This is WA's malloc wrapper — calls _malloc, checks for null
 
545
  let _lock = mutation_lock().lock().unwrap();
546
  let img = wa();
547
  img.patch_imports().expect("IAT patching failed");
548
+ unsafe {
549
+ init_wa_crt_heap(img);
550
+ }
551
 
552
  // Initialize security cookie (needed for stack protection in SEH functions)
553
  const SECURITY_INIT_COOKIE: u32 = 0x005E_7EAB;
 
559
  }
560
 
561
  // CTask::Constructor is __stdcall: (this, parent, ddgame) -> this
562
+ type CTaskCtorFn =
563
+ unsafe extern "stdcall" fn(this: *mut u8, parent: u32, ddgame: u32) -> *mut u8;
564
 
565
  unsafe {
566
  let ctor: CTaskCtorFn = img.func_ptr(va::CTASK_CONSTRUCTOR);
 
579
  let vtable = *(task_ptr as *const u32);
580
  let vtable_ghidra = img.to_ghidra(vtable);
581
  assert_eq!(
582
+ vtable_ghidra,
583
+ va::CTASK_VTABLE,
584
  "CTask vtable: expected 0x{:08X}, got 0x{vtable_ghidra:08X}",
585
  va::CTASK_VTABLE
586
  );
crates/openwa-launcher/src/inject.rs CHANGED
@@ -19,26 +19,41 @@ type FARPROC = unsafe extern "system" fn() -> isize;
19
 
20
  extern "system" {
21
  fn VirtualAllocEx(
22
- hProcess: HANDLE, lpAddress: LPVOID, dwSize: SIZE_T,
23
- flAllocationType: DWORD, flProtect: DWORD,
 
 
 
24
  ) -> LPVOID;
25
- fn VirtualFreeEx(hProcess: HANDLE, lpAddress: LPVOID, dwSize: SIZE_T, dwFreeType: DWORD) -> BOOL;
 
 
 
 
 
26
  fn WriteProcessMemory(
27
- hProcess: HANDLE, lpBaseAddress: LPVOID, lpBuffer: LPCVOID,
28
- nSize: SIZE_T, lpNumberOfBytesWritten: *mut SIZE_T,
 
 
 
29
  ) -> BOOL;
30
  fn GetModuleHandleA(lpModuleName: *const u8) -> HANDLE;
31
  fn GetProcAddress(hModule: HANDLE, lpProcName: *const u8) -> Option<FARPROC>;
32
  fn CreateRemoteThread(
33
- hProcess: HANDLE, lpThreadAttributes: LPVOID, dwStackSize: SIZE_T,
 
 
34
  lpStartAddress: unsafe extern "system" fn(LPVOID) -> DWORD,
35
- lpParameter: LPVOID, dwCreationFlags: DWORD, lpThreadId: *mut DWORD,
 
 
36
  ) -> HANDLE;
37
  fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD;
38
  fn CloseHandle(hObject: HANDLE) -> BOOL;
39
  }
40
 
41
- const MEM_COMMIT: DWORD = 0x1000;
42
  const MEM_RESERVE: DWORD = 0x2000;
43
  const MEM_RELEASE: DWORD = 0x8000;
44
  const PAGE_READWRITE: DWORD = 0x04;
@@ -49,14 +64,16 @@ const INFINITE: DWORD = 0xFFFF_FFFF;
49
  /// Blocks until `DllMain` returns (i.e. all hooks are installed) before returning,
50
  /// so the caller can safely resume the main thread.
51
  pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String> {
52
- let path_cstr = CString::new(dll_path)
53
- .map_err(|e| format!("bad DLL path: {e}"))?;
54
  let path_bytes = path_cstr.as_bytes_with_nul();
55
 
56
  // Allocate memory in the target process for the DLL path string.
57
  let remote_buf = VirtualAllocEx(
58
- process, ptr::null_mut(), path_bytes.len(),
59
- MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE,
 
 
 
60
  );
61
  if remote_buf.is_null() {
62
  return Err("VirtualAllocEx failed".to_string());
@@ -64,8 +81,10 @@ pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String>
64
 
65
  // Write the path into the target process.
66
  let ok = WriteProcessMemory(
67
- process, remote_buf,
68
- path_bytes.as_ptr().cast(), path_bytes.len(),
 
 
69
  ptr::null_mut(),
70
  );
71
  if ok == 0 {
@@ -89,9 +108,13 @@ pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String>
89
  // Spin up a remote thread that calls LoadLibraryA(dll_path).
90
  // This triggers DllMain(DLL_PROCESS_ATTACH) which installs all our hooks.
91
  let thread = CreateRemoteThread(
92
- process, ptr::null_mut(), 0,
 
 
93
  mem::transmute(load_library),
94
- remote_buf, 0, ptr::null_mut(),
 
 
95
  );
96
  if thread.is_null() {
97
  VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
 
19
 
20
  extern "system" {
21
  fn VirtualAllocEx(
22
+ hProcess: HANDLE,
23
+ lpAddress: LPVOID,
24
+ dwSize: SIZE_T,
25
+ flAllocationType: DWORD,
26
+ flProtect: DWORD,
27
  ) -> LPVOID;
28
+ fn VirtualFreeEx(
29
+ hProcess: HANDLE,
30
+ lpAddress: LPVOID,
31
+ dwSize: SIZE_T,
32
+ dwFreeType: DWORD,
33
+ ) -> BOOL;
34
  fn WriteProcessMemory(
35
+ hProcess: HANDLE,
36
+ lpBaseAddress: LPVOID,
37
+ lpBuffer: LPCVOID,
38
+ nSize: SIZE_T,
39
+ lpNumberOfBytesWritten: *mut SIZE_T,
40
  ) -> BOOL;
41
  fn GetModuleHandleA(lpModuleName: *const u8) -> HANDLE;
42
  fn GetProcAddress(hModule: HANDLE, lpProcName: *const u8) -> Option<FARPROC>;
43
  fn CreateRemoteThread(
44
+ hProcess: HANDLE,
45
+ lpThreadAttributes: LPVOID,
46
+ dwStackSize: SIZE_T,
47
  lpStartAddress: unsafe extern "system" fn(LPVOID) -> DWORD,
48
+ lpParameter: LPVOID,
49
+ dwCreationFlags: DWORD,
50
+ lpThreadId: *mut DWORD,
51
  ) -> HANDLE;
52
  fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD;
53
  fn CloseHandle(hObject: HANDLE) -> BOOL;
54
  }
55
 
56
+ const MEM_COMMIT: DWORD = 0x1000;
57
  const MEM_RESERVE: DWORD = 0x2000;
58
  const MEM_RELEASE: DWORD = 0x8000;
59
  const PAGE_READWRITE: DWORD = 0x04;
 
64
  /// Blocks until `DllMain` returns (i.e. all hooks are installed) before returning,
65
  /// so the caller can safely resume the main thread.
66
  pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String> {
67
+ let path_cstr = CString::new(dll_path).map_err(|e| format!("bad DLL path: {e}"))?;
 
68
  let path_bytes = path_cstr.as_bytes_with_nul();
69
 
70
  // Allocate memory in the target process for the DLL path string.
71
  let remote_buf = VirtualAllocEx(
72
+ process,
73
+ ptr::null_mut(),
74
+ path_bytes.len(),
75
+ MEM_COMMIT | MEM_RESERVE,
76
+ PAGE_READWRITE,
77
  );
78
  if remote_buf.is_null() {
79
  return Err("VirtualAllocEx failed".to_string());
 
81
 
82
  // Write the path into the target process.
83
  let ok = WriteProcessMemory(
84
+ process,
85
+ remote_buf,
86
+ path_bytes.as_ptr().cast(),
87
+ path_bytes.len(),
88
  ptr::null_mut(),
89
  );
90
  if ok == 0 {
 
108
  // Spin up a remote thread that calls LoadLibraryA(dll_path).
109
  // This triggers DllMain(DLL_PROCESS_ATTACH) which installs all our hooks.
110
  let thread = CreateRemoteThread(
111
+ process,
112
+ ptr::null_mut(),
113
+ 0,
114
  mem::transmute(load_library),
115
+ remote_buf,
116
+ 0,
117
+ ptr::null_mut(),
118
  );
119
  if thread.is_null() {
120
  VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
crates/openwa-launcher/src/main.rs CHANGED
@@ -6,41 +6,67 @@ use std::path::{Path, PathBuf};
6
  use std::ptr;
7
 
8
  // Declare Win32 types and functions directly to avoid windows-sys feature-flag issues.
9
- #[allow(non_camel_case_types)] type HANDLE = *mut core::ffi::c_void;
10
- #[allow(non_camel_case_types)] type HMODULE = *mut core::ffi::c_void;
11
- #[allow(non_camel_case_types)] type DWORD = u32;
12
- #[allow(non_camel_case_types)] type BOOL = i32;
13
- #[allow(non_camel_case_types)] type LPVOID = *mut core::ffi::c_void;
14
- #[allow(non_camel_case_types)] type LPDWORD = *mut u32;
 
 
 
 
 
 
15
 
16
  #[repr(C)]
17
  #[allow(non_snake_case)]
18
  struct STARTUPINFOA {
19
- cb: DWORD, lpReserved: *mut u8, lpDesktop: *mut u8, lpTitle: *mut u8,
20
- dwX: DWORD, dwY: DWORD, dwXSize: DWORD, dwYSize: DWORD,
21
- dwXCountChars: DWORD, dwYCountChars: DWORD, dwFillAttribute: DWORD,
22
- dwFlags: DWORD, wShowWindow: u16, cbReserved2: u16,
23
- lpReserved2: *mut u8, hStdInput: HANDLE, hStdOutput: HANDLE, hStdError: HANDLE,
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
  #[repr(C)]
27
  #[allow(non_snake_case)]
28
  struct PROCESS_INFORMATION {
29
- hProcess: HANDLE, hThread: HANDLE, dwProcessId: DWORD, dwThreadId: DWORD,
 
 
 
30
  }
31
 
32
- const CREATE_SUSPENDED: DWORD = 0x0000_0004;
33
- const STARTF_USESHOWWINDOW: DWORD = 0x0000_0001;
34
- const SW_SHOWMINIMIZED: u16 = 2;
35
- const INFINITE: DWORD = 0xFFFF_FFFF;
36
 
37
  #[link(name = "kernel32")]
38
  extern "system" {
39
  fn CreateProcessA(
40
- lpApplicationName: *const u8, lpCommandLine: *mut u8,
41
- lpProcessAttributes: LPVOID, lpThreadAttributes: LPVOID,
42
- bInheritHandles: BOOL, dwCreationFlags: DWORD,
43
- lpEnvironment: LPVOID, lpCurrentDirectory: *const u8,
 
 
 
 
44
  lpStartupInfo: *const STARTUPINFOA,
45
  lpProcessInformation: *mut PROCESS_INFORMATION,
46
  ) -> BOOL;
@@ -51,7 +77,9 @@ extern "system" {
51
  fn CloseHandle(hObject: HANDLE) -> BOOL;
52
  fn GetModuleFileNameA(hModule: HMODULE, lpFilename: *mut u8, nSize: DWORD) -> DWORD;
53
  fn CreateEventA(
54
- lpEventAttributes: LPVOID, bManualReset: BOOL, bInitialState: BOOL,
 
 
55
  lpName: *const u8,
56
  ) -> HANDLE;
57
  }
@@ -143,7 +171,8 @@ unsafe fn launch(
143
  let ok = CreateProcessA(
144
  exe_cstr.as_ptr().cast(),
145
  cmdline_buf.as_mut_ptr(),
146
- ptr::null_mut(), ptr::null_mut(),
 
147
  0, // bInheritHandles = FALSE
148
  CREATE_SUSPENDED,
149
  ptr::null_mut(), // inherit environment
@@ -156,9 +185,7 @@ unsafe fn launch(
156
  return Err("CreateProcessA failed — is WA.exe path correct?".to_string());
157
  }
158
 
159
- let dll_str = dll_path
160
- .to_str()
161
- .ok_or("DLL path is not valid UTF-8")?;
162
 
163
  // Create a named event that the DLL will signal after all hooks are
164
  // installed. This ensures the main thread doesn't run any WA code
@@ -168,7 +195,9 @@ unsafe fn launch(
168
  // event may be null if CreateEventA fails — we'll fall through gracefully.
169
 
170
  if let Err(e) = inject::inject_dll(pi.hProcess, dll_str) {
171
- if !event.is_null() { CloseHandle(event); }
 
 
172
  TerminateProcess(pi.hProcess, 1);
173
  CloseHandle(pi.hProcess);
174
  CloseHandle(pi.hThread);
@@ -204,9 +233,7 @@ unsafe fn launch(
204
 
205
  fn launcher_dir() -> Result<PathBuf, String> {
206
  let mut buf = vec![0u8; 32768];
207
- let len = unsafe {
208
- GetModuleFileNameA(ptr::null_mut(), buf.as_mut_ptr(), buf.len() as DWORD)
209
- };
210
  if len == 0 {
211
  return Err("GetModuleFileNameA failed".to_string());
212
  }
@@ -251,7 +278,5 @@ fn find_wa_hardcoded() -> Option<PathBuf> {
251
  r"C:\Program Files\Steam\steamapps\common\Worms Armageddon\WA.exe",
252
  ];
253
 
254
- CANDIDATES.iter()
255
- .map(PathBuf::from)
256
- .find(|p| p.exists())
257
  }
 
6
  use std::ptr;
7
 
8
  // Declare Win32 types and functions directly to avoid windows-sys feature-flag issues.
9
+ #[allow(non_camel_case_types)]
10
+ type HANDLE = *mut core::ffi::c_void;
11
+ #[allow(non_camel_case_types)]
12
+ type HMODULE = *mut core::ffi::c_void;
13
+ #[allow(non_camel_case_types)]
14
+ type DWORD = u32;
15
+ #[allow(non_camel_case_types)]
16
+ type BOOL = i32;
17
+ #[allow(non_camel_case_types)]
18
+ type LPVOID = *mut core::ffi::c_void;
19
+ #[allow(non_camel_case_types)]
20
+ type LPDWORD = *mut u32;
21
 
22
  #[repr(C)]
23
  #[allow(non_snake_case)]
24
  struct STARTUPINFOA {
25
+ cb: DWORD,
26
+ lpReserved: *mut u8,
27
+ lpDesktop: *mut u8,
28
+ lpTitle: *mut u8,
29
+ dwX: DWORD,
30
+ dwY: DWORD,
31
+ dwXSize: DWORD,
32
+ dwYSize: DWORD,
33
+ dwXCountChars: DWORD,
34
+ dwYCountChars: DWORD,
35
+ dwFillAttribute: DWORD,
36
+ dwFlags: DWORD,
37
+ wShowWindow: u16,
38
+ cbReserved2: u16,
39
+ lpReserved2: *mut u8,
40
+ hStdInput: HANDLE,
41
+ hStdOutput: HANDLE,
42
+ hStdError: HANDLE,
43
  }
44
 
45
  #[repr(C)]
46
  #[allow(non_snake_case)]
47
  struct PROCESS_INFORMATION {
48
+ hProcess: HANDLE,
49
+ hThread: HANDLE,
50
+ dwProcessId: DWORD,
51
+ dwThreadId: DWORD,
52
  }
53
 
54
+ const CREATE_SUSPENDED: DWORD = 0x0000_0004;
55
+ const STARTF_USESHOWWINDOW: DWORD = 0x0000_0001;
56
+ const SW_SHOWMINIMIZED: u16 = 2;
57
+ const INFINITE: DWORD = 0xFFFF_FFFF;
58
 
59
  #[link(name = "kernel32")]
60
  extern "system" {
61
  fn CreateProcessA(
62
+ lpApplicationName: *const u8,
63
+ lpCommandLine: *mut u8,
64
+ lpProcessAttributes: LPVOID,
65
+ lpThreadAttributes: LPVOID,
66
+ bInheritHandles: BOOL,
67
+ dwCreationFlags: DWORD,
68
+ lpEnvironment: LPVOID,
69
+ lpCurrentDirectory: *const u8,
70
  lpStartupInfo: *const STARTUPINFOA,
71
  lpProcessInformation: *mut PROCESS_INFORMATION,
72
  ) -> BOOL;
 
77
  fn CloseHandle(hObject: HANDLE) -> BOOL;
78
  fn GetModuleFileNameA(hModule: HMODULE, lpFilename: *mut u8, nSize: DWORD) -> DWORD;
79
  fn CreateEventA(
80
+ lpEventAttributes: LPVOID,
81
+ bManualReset: BOOL,
82
+ bInitialState: BOOL,
83
  lpName: *const u8,
84
  ) -> HANDLE;
85
  }
 
171
  let ok = CreateProcessA(
172
  exe_cstr.as_ptr().cast(),
173
  cmdline_buf.as_mut_ptr(),
174
+ ptr::null_mut(),
175
+ ptr::null_mut(),
176
  0, // bInheritHandles = FALSE
177
  CREATE_SUSPENDED,
178
  ptr::null_mut(), // inherit environment
 
185
  return Err("CreateProcessA failed — is WA.exe path correct?".to_string());
186
  }
187
 
188
+ let dll_str = dll_path.to_str().ok_or("DLL path is not valid UTF-8")?;
 
 
189
 
190
  // Create a named event that the DLL will signal after all hooks are
191
  // installed. This ensures the main thread doesn't run any WA code
 
195
  // event may be null if CreateEventA fails — we'll fall through gracefully.
196
 
197
  if let Err(e) = inject::inject_dll(pi.hProcess, dll_str) {
198
+ if !event.is_null() {
199
+ CloseHandle(event);
200
+ }
201
  TerminateProcess(pi.hProcess, 1);
202
  CloseHandle(pi.hProcess);
203
  CloseHandle(pi.hThread);
 
233
 
234
  fn launcher_dir() -> Result<PathBuf, String> {
235
  let mut buf = vec![0u8; 32768];
236
+ let len = unsafe { GetModuleFileNameA(ptr::null_mut(), buf.as_mut_ptr(), buf.len() as DWORD) };
 
 
237
  if len == 0 {
238
  return Err("GetModuleFileNameA failed".to_string());
239
  }
 
278
  r"C:\Program Files\Steam\steamapps\common\Worms Armageddon\WA.exe",
279
  ];
280
 
281
+ CANDIDATES.iter().map(PathBuf::from).find(|p| p.exists())
 
 
282
  }
crates/openwa-wormkit/src/hook.rs CHANGED
@@ -326,15 +326,12 @@ macro_rules! install_trap {
326
  ($name:literal, $addr:expr) => {{
327
  unsafe extern "C" fn trap() {
328
  panic!(concat!(
329
- "TRAP: ", $name,
 
330
  " called by WA.exe — all callers should be Rust"
331
  ));
332
  }
333
- let _ = hook::install(
334
- concat!($name, " [TRAP]"),
335
- $addr,
336
- trap as *const (),
337
- )?;
338
  }};
339
  }
340
 
 
326
  ($name:literal, $addr:expr) => {{
327
  unsafe extern "C" fn trap() {
328
  panic!(concat!(
329
+ "TRAP: ",
330
+ $name,
331
  " called by WA.exe — all callers should be Rust"
332
  ));
333
  }
334
+ let _ = hook::install(concat!($name, " [TRAP]"), $addr, trap as *const ())?;
 
 
 
 
335
  }};
336
  }
337
 
crates/openwa-wormkit/src/lib.rs CHANGED
@@ -2,8 +2,8 @@
2
 
3
  use std::ffi::c_void;
4
 
5
- pub mod hook;
6
  mod debug_ui;
 
7
  mod replacements;
8
  mod validation;
9
 
@@ -53,6 +53,11 @@ fn run() -> Result<(), String> {
53
  let _ = clear_log();
54
  let _ = std::fs::write("OpenWA_validation.log", "");
55
 
 
 
 
 
 
56
  let delta = openwa_core::rebase::init();
57
  let _ = log_line(&format!(
58
  "=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
@@ -88,8 +93,8 @@ fn run() -> Result<(), String> {
88
  /// Signal the `OpenWA_HooksReady` named event so the launcher knows it's
89
  /// safe to resume WA.exe's main thread.
90
  fn signal_hooks_ready() {
91
- use windows_sys::Win32::System::Threading::{OpenEventA, SetEvent};
92
  use windows_sys::Win32::Foundation::CloseHandle;
 
93
 
94
  const EVENT_MODIFY_STATE: u32 = 0x0002;
95
 
 
2
 
3
  use std::ffi::c_void;
4
 
 
5
  mod debug_ui;
6
+ pub mod hook;
7
  mod replacements;
8
  mod validation;
9
 
 
53
  let _ = clear_log();
54
  let _ = std::fs::write("OpenWA_validation.log", "");
55
 
56
+ // Install panic hook that writes to our log file
57
+ std::panic::set_hook(Box::new(|info| {
58
+ let _ = log_line(&format!("[PANIC] {info}"));
59
+ }));
60
+
61
  let delta = openwa_core::rebase::init();
62
  let _ = log_line(&format!(
63
  "=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
 
93
  /// Signal the `OpenWA_HooksReady` named event so the launcher knows it's
94
  /// safe to resume WA.exe's main thread.
95
  fn signal_hooks_ready() {
 
96
  use windows_sys::Win32::Foundation::CloseHandle;
97
+ use windows_sys::Win32::System::Threading::{OpenEventA, SetEvent};
98
 
99
  const EVENT_MODIFY_STATE: u32 = 0x0002;
100
 
crates/openwa-wormkit/src/replacements/config.rs CHANGED
@@ -9,9 +9,9 @@
9
  //! - GameInfo__LoadOptions (0x460AC0): game options from registry
10
  //! - Options__GetCrashReportURL (0x5A63F0): crash report URL from registry
11
 
12
- use openwa_core::{address::va, engine::GameInfo};
13
  use crate::log_line;
14
  use openwa_core::rebase::rb;
 
15
 
16
  const THEME_PATH: &str = "data\\current.thm";
17
  const CLEAN_ALL_FLAG_OFFSET: usize = 0xE0;
@@ -68,7 +68,12 @@ fn show_error_message(msg: &str) {
68
  let mut msg_buf: Vec<u8> = msg.bytes().collect();
69
  msg_buf.push(0);
70
  unsafe {
71
- MessageBoxA(core::ptr::null_mut(), msg_buf.as_ptr(), core::ptr::null(), MB_OK);
 
 
 
 
 
72
  }
73
  }
74
 
@@ -86,10 +91,8 @@ unsafe extern "stdcall" fn hook_delete_key_recursive(hkey: u32, subkey: u32) ->
86
 
87
  let _ = log_line(&format!("[Config] DeleteKeyRecursive: {subkey_str}"));
88
 
89
- let result = openwa_core::wa::registry::delete_key_recursive(
90
- hkey as usize as HKEY,
91
- &subkey_str,
92
- );
93
 
94
  let _ = log_line(&format!("[Config] DeleteKeyRecursive result: {result}"));
95
  result as i32
@@ -159,25 +162,27 @@ unsafe extern "stdcall" fn hook_load_options(game_info: *mut GameInfo) {
159
  // Format streams directory and randomize stream indices (global, not GameInfo)
160
  let streams_dest = rb(va::G_STREAMS_DIR) as *mut u8;
161
  let streams_path = format!("{}\\streams\0", base_str.to_string_lossy());
162
- core::ptr::copy_nonoverlapping(
163
- streams_path.as_ptr(),
164
- streams_dest,
165
- streams_path.len(),
166
- );
167
 
168
  // Randomize stream indices (16 entries, each rand() % 11 + 1)
169
  let indices = rb(va::G_STREAM_INDICES) as *mut u32;
170
  let indices_end = rb(va::G_STREAM_INDICES_END) as usize;
171
  let mut ptr = indices;
172
  while (ptr as usize) < indices_end {
173
- extern "cdecl" { fn rand() -> i32; }
 
 
174
  *ptr = (rand() % 11 + 1) as u32;
175
  ptr = ptr.add(1);
176
  }
177
 
178
  // Stream volume: 0x10 if flag set, else 0
179
  let stream_vol_addr = rb(va::G_STREAM_INDICES_END) as *mut u8;
180
- *stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 { 0x10 } else { 0 };
 
 
 
 
181
  *(rb(va::G_STREAM_VOLUME) as *mut u8) = 0x4B;
182
 
183
  // Copy "data\land.dat" string (14 bytes)
@@ -226,7 +231,11 @@ unsafe extern "stdcall" fn hook_load_options(game_info: *mut GameInfo) {
226
  }
227
 
228
  gi.info_transparency = read_profile_int("Options", "InfoTransparency", 0) as u8;
229
- gi.info_spy = if read_profile_int("Options", "InfoSpy", 1) != 0 { 1 } else { 0 };
 
 
 
 
230
  gi.chat_pinned = read_profile_int("Options", "ChatPinned", 0) as u8;
231
  gi.chat_lines = read_profile_int("Options", "ChatLines", 0);
232
  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) {
244
  }
245
  gi.background_debris_parallax = parallax << 16;
246
 
247
- gi.topmost_explosion_onomatopoeia = read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0);
 
248
  gi.capture_transparent_pngs = read_profile_int("Options", "CaptureTransparentPNGs", 0);
249
 
250
  // CameraUnlockMouseSpeed: clamp to max 0xB504, then square
@@ -273,11 +283,8 @@ unsafe extern "cdecl" fn hook_get_crash_report_url() -> u32 {
273
  let buf = rb(va::G_CRASH_REPORT_URL) as *mut u8;
274
  let buf_slice = core::slice::from_raw_parts_mut(buf, CRASH_REPORT_URL_BUF_SIZE);
275
 
276
- let len = openwa_core::wa::registry::read_profile_string(
277
- "Options",
278
- "CrashReportURL",
279
- buf_slice,
280
- );
281
 
282
  if len > 0 {
283
  // Null-terminate
@@ -300,17 +307,9 @@ pub fn install() -> Result<(), String> {
300
  hook_theme_get_file_size as *const (),
301
  )?;
302
 
303
- let _ = crate::hook::install(
304
- "Theme__Load",
305
- va::THEME_LOAD,
306
- hook_theme_load as *const (),
307
- )?;
308
 
309
- let _ = crate::hook::install(
310
- "Theme__Save",
311
- va::THEME_SAVE,
312
- hook_theme_save as *const (),
313
- )?;
314
 
315
  let _ = crate::hook::install(
316
  "Registry__DeleteKeyRecursive",
 
9
  //! - GameInfo__LoadOptions (0x460AC0): game options from registry
10
  //! - Options__GetCrashReportURL (0x5A63F0): crash report URL from registry
11
 
 
12
  use crate::log_line;
13
  use openwa_core::rebase::rb;
14
+ use openwa_core::{address::va, engine::GameInfo};
15
 
16
  const THEME_PATH: &str = "data\\current.thm";
17
  const CLEAN_ALL_FLAG_OFFSET: usize = 0xE0;
 
68
  let mut msg_buf: Vec<u8> = msg.bytes().collect();
69
  msg_buf.push(0);
70
  unsafe {
71
+ MessageBoxA(
72
+ core::ptr::null_mut(),
73
+ msg_buf.as_ptr(),
74
+ core::ptr::null(),
75
+ MB_OK,
76
+ );
77
  }
78
  }
79
 
 
91
 
92
  let _ = log_line(&format!("[Config] DeleteKeyRecursive: {subkey_str}"));
93
 
94
+ let result =
95
+ openwa_core::wa::registry::delete_key_recursive(hkey as usize as HKEY, &subkey_str);
 
 
96
 
97
  let _ = log_line(&format!("[Config] DeleteKeyRecursive result: {result}"));
98
  result as i32
 
162
  // Format streams directory and randomize stream indices (global, not GameInfo)
163
  let streams_dest = rb(va::G_STREAMS_DIR) as *mut u8;
164
  let streams_path = format!("{}\\streams\0", base_str.to_string_lossy());
165
+ core::ptr::copy_nonoverlapping(streams_path.as_ptr(), streams_dest, streams_path.len());
 
 
 
 
166
 
167
  // Randomize stream indices (16 entries, each rand() % 11 + 1)
168
  let indices = rb(va::G_STREAM_INDICES) as *mut u32;
169
  let indices_end = rb(va::G_STREAM_INDICES_END) as usize;
170
  let mut ptr = indices;
171
  while (ptr as usize) < indices_end {
172
+ extern "cdecl" {
173
+ fn rand() -> i32;
174
+ }
175
  *ptr = (rand() % 11 + 1) as u32;
176
  ptr = ptr.add(1);
177
  }
178
 
179
  // Stream volume: 0x10 if flag set, else 0
180
  let stream_vol_addr = rb(va::G_STREAM_INDICES_END) as *mut u8;
181
+ *stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 {
182
+ 0x10
183
+ } else {
184
+ 0
185
+ };
186
  *(rb(va::G_STREAM_VOLUME) as *mut u8) = 0x4B;
187
 
188
  // Copy "data\land.dat" string (14 bytes)
 
231
  }
232
 
233
  gi.info_transparency = read_profile_int("Options", "InfoTransparency", 0) as u8;
234
+ gi.info_spy = if read_profile_int("Options", "InfoSpy", 1) != 0 {
235
+ 1
236
+ } else {
237
+ 0
238
+ };
239
  gi.chat_pinned = read_profile_int("Options", "ChatPinned", 0) as u8;
240
  gi.chat_lines = read_profile_int("Options", "ChatLines", 0);
241
  gi.pinned_chat_lines = read_profile_int("Options", "PinnedChatLines", 0xFFFFFFFF);
 
253
  }
254
  gi.background_debris_parallax = parallax << 16;
255
 
256
+ gi.topmost_explosion_onomatopoeia =
257
+ read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0);
258
  gi.capture_transparent_pngs = read_profile_int("Options", "CaptureTransparentPNGs", 0);
259
 
260
  // CameraUnlockMouseSpeed: clamp to max 0xB504, then square
 
283
  let buf = rb(va::G_CRASH_REPORT_URL) as *mut u8;
284
  let buf_slice = core::slice::from_raw_parts_mut(buf, CRASH_REPORT_URL_BUF_SIZE);
285
 
286
+ let len =
287
+ openwa_core::wa::registry::read_profile_string("Options", "CrashReportURL", buf_slice);
 
 
 
288
 
289
  if len > 0 {
290
  // Null-terminate
 
307
  hook_theme_get_file_size as *const (),
308
  )?;
309
 
310
+ let _ = crate::hook::install("Theme__Load", va::THEME_LOAD, hook_theme_load as *const ())?;
 
 
 
 
311
 
312
+ let _ = crate::hook::install("Theme__Save", va::THEME_SAVE, hook_theme_save as *const ())?;
 
 
 
 
313
 
314
  let _ = crate::hook::install(
315
  "Registry__DeleteKeyRecursive",
crates/openwa-wormkit/src/replacements/ddgame_init.rs ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Rust replacements for DDGame constructor sub-functions.
2
+ //!
3
+ //! Each function is hooked individually so it works regardless of whether the
4
+ //! DDGame constructor itself is Rust or the original WA code.
5
+ //!
6
+ //! ## Ported functions
7
+ //!
8
+ //! - `DDGame__InitFields` (0x526120): usercall(EDI=ddgame), plain RET → EAX=ddgame
9
+ //! - `DDGame__InitRenderIndices` (0x526080): usercall(ESI=base), plain RET → EAX=base
10
+ //! - `TaskStateMachine__Init` (0x4F6370): usercall(ESI,ECX,EDI) + 1 stack, RET 0x4
11
+ //! - `GfxResource__Create_Maybe` (0x4F6300): usercall(ECX,EAX) + 1 stack, RET 0x4
12
+ //! - `FUN_570E20` (display layer init): usercall(ESI=wrapper), plain RET
13
+
14
+ use crate::hook;
15
+ use openwa_core::address::va;
16
+ use openwa_core::engine::ddgame::{
17
+ ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init, gfx_dir_find_entry,
18
+ gfx_handler_load_dir, gfx_resource_create, task_state_machine_init, DDGame,
19
+ };
20
+ use openwa_core::engine::DDGameWrapper;
21
+
22
+ // ─── DDGame__InitFields (0x526120) ──────────────────────────────────────────
23
+
24
+ hook::usercall_trampoline!(
25
+ fn init_fields_trampoline;
26
+ impl_fn = impl_init_fields;
27
+ reg = edi
28
+ );
29
+
30
+ extern "cdecl" fn impl_init_fields(ddgame: u32) -> u32 {
31
+ unsafe { ddgame_init_fields(ddgame as *mut DDGame) }
32
+ ddgame // Original: MOV EAX, EDI; RET
33
+ }
34
+
35
+ // ─── DDGame__InitRenderIndices (0x526080) ───────────────────────────────────
36
+
37
+ hook::usercall_trampoline!(
38
+ fn init_render_indices_trampoline;
39
+ impl_fn = impl_init_render_indices;
40
+ reg = esi
41
+ );
42
+
43
+ extern "cdecl" fn impl_init_render_indices(base: u32) -> u32 {
44
+ unsafe { ddgame_init_render_indices(base as *mut u8) }
45
+ base // Original: MOV EAX, ESI; RET
46
+ }
47
+
48
+ // ─── TaskStateMachine__Init (0x4F6370) ──────────────────────────────────────
49
+
50
+ extern "cdecl" fn impl_tsm_init(object: u32, param1: u32, height: u32, width: u32) -> u32 {
51
+ unsafe { task_state_machine_init(object as *mut u8, param1, width, height) }
52
+ object // Original: MOV EAX, ESI; RET 0x4
53
+ }
54
+
55
+ #[unsafe(naked)]
56
+ unsafe extern "C" fn tsm_init_trampoline() {
57
+ core::arch::naked_asm!(
58
+ "push edx",
59
+ "push [esp+8]", // width (stack param)
60
+ "push edi", // height
61
+ "push ecx", // param1
62
+ "push esi", // object
63
+ "call {impl_fn}",
64
+ "add esp, 16",
65
+ "pop edx",
66
+ "ret 0x4",
67
+ impl_fn = sym impl_tsm_init,
68
+ );
69
+ }
70
+
71
+ // ─── FUN_570E20 (display layer color init) ──────────────────────────────────
72
+
73
+ hook::usercall_trampoline!(
74
+ fn display_layer_init_trampoline;
75
+ impl_fn = impl_display_layer_init;
76
+ reg = esi
77
+ );
78
+
79
+ extern "cdecl" fn impl_display_layer_init(wrapper: u32) -> u32 {
80
+ unsafe { display_layer_color_init(wrapper as *mut DDGameWrapper) }
81
+ wrapper
82
+ }
83
+
84
+ // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
85
+
86
+ extern "cdecl" fn impl_gfx_resource_create(gfx_handler: u32, name: u32, output: u32) -> u32 {
87
+ let result = unsafe {
88
+ gfx_resource_create(gfx_handler as *mut u8, name as *const u8, output as *mut u8)
89
+ };
90
+ result as u32
91
+ }
92
+
93
+ #[unsafe(naked)]
94
+ unsafe extern "C" fn gfx_resource_create_trampoline() {
95
+ core::arch::naked_asm!(
96
+ "push edx",
97
+ "push [esp+8]", // output (stack param)
98
+ "push eax", // name
99
+ "push ecx", // gfx_handler
100
+ "call {impl_fn}",
101
+ "add esp, 12",
102
+ "pop edx",
103
+ "ret 0x4",
104
+ impl_fn = sym impl_gfx_resource_create,
105
+ );
106
+ }
107
+
108
+ // ─── GfxDir__FindEntry (0x566520) ────────────────────────────────────────────
109
+ //
110
+ // Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4.
111
+
112
+ hook::usercall_trampoline!(
113
+ fn find_entry_trampoline;
114
+ impl_fn = impl_find_entry;
115
+ reg = eax;
116
+ stack_params = 1; ret_bytes = "0x4"
117
+ );
118
+
119
+ static FIND_ENTRY_LOG_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
120
+
121
+ extern "cdecl" fn impl_find_entry(name: u32, gfx_handler: u32) -> u32 {
122
+ let result = unsafe { gfx_dir_find_entry(name as *const u8, gfx_handler as *mut u8) };
123
+
124
+ // Log first 20 lookups for debugging
125
+ let count = FIND_ENTRY_LOG_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
126
+ if count < 20 {
127
+ let name_str = unsafe {
128
+ let p = name as *const u8;
129
+ let mut len = 0;
130
+ while *p.add(len) != 0 && len < 64 {
131
+ len += 1;
132
+ }
133
+ core::str::from_utf8_unchecked(core::slice::from_raw_parts(p, len))
134
+ };
135
+ let _ = crate::log_line(&format!(
136
+ "[FindEntry] #{count}: \"{name_str}\" -> 0x{:08X}",
137
+ result as u32
138
+ ));
139
+ }
140
+ result as u32
141
+ }
142
+
143
+ // ─── GfxHandler__LoadDir (0x5663E0) ──────────────────────────────────────────
144
+ //
145
+ // Convention: usercall(EAX=handler), plain RET. Returns 1/0.
146
+
147
+ hook::usercall_trampoline!(
148
+ fn load_dir_trampoline;
149
+ impl_fn = impl_load_dir;
150
+ reg = eax
151
+ );
152
+
153
+ extern "cdecl" fn impl_load_dir(handler: u32) -> u32 {
154
+ unsafe { gfx_handler_load_dir(handler as *mut u8) as u32 }
155
+ }
156
+
157
+ // ─── Hook installation ──────────────────────────────────────────────────────
158
+
159
+ pub fn install() -> Result<(), String> {
160
+ unsafe {
161
+ hook::install(
162
+ "DDGame__InitFields",
163
+ va::DDGAME_INIT_FIELDS,
164
+ init_fields_trampoline as *const (),
165
+ )?;
166
+
167
+ hook::install(
168
+ "DDGame__InitRenderIndices",
169
+ va::DDGAME_INIT_RENDER_INDICES,
170
+ init_render_indices_trampoline as *const (),
171
+ )?;
172
+
173
+ hook::install(
174
+ "TaskStateMachine__Init",
175
+ va::TASK_STATE_MACHINE_INIT,
176
+ tsm_init_trampoline as *const (),
177
+ )?;
178
+
179
+ hook::install(
180
+ "GfxResource__Create",
181
+ va::GFX_RESOURCE_CREATE,
182
+ gfx_resource_create_trampoline as *const (),
183
+ )?;
184
+
185
+ hook::install(
186
+ "FUN_570E20_DisplayLayerInit",
187
+ va::FUN_570E20,
188
+ display_layer_init_trampoline as *const (),
189
+ )?;
190
+
191
+ hook::install(
192
+ "GfxDir__FindEntry",
193
+ va::GFX_DIR_FIND_ENTRY,
194
+ find_entry_trampoline as *const (),
195
+ )?;
196
+
197
+ hook::install(
198
+ "GfxHandler__LoadDir",
199
+ va::GFX_HANDLER_LOAD_DIR,
200
+ load_dir_trampoline as *const (),
201
+ )?;
202
+ }
203
+
204
+ Ok(())
205
+ }
crates/openwa-wormkit/src/replacements/display.rs CHANGED
@@ -5,12 +5,12 @@
5
  //! - Headless vtable (0x66A0F8): replaces destructor with Rust version that
6
  //! correctly frees our Rust-allocated sprite cache sub-objects
7
 
 
8
  use openwa_core::address::va;
9
- use openwa_core::display::{DisplayBase, SpriteCacheWrapper, SpriteBufferCtrl};
10
  use openwa_core::rebase::rb;
11
  use openwa_core::vtable::patch_vtable;
12
  use openwa_core::wa_alloc::wa_free;
13
- use crate::log_line;
14
 
15
  /// The _purecall function address (calls abort).
16
  const PURECALL: u32 = 0x005D_4E16;
 
5
  //! - Headless vtable (0x66A0F8): replaces destructor with Rust version that
6
  //! correctly frees our Rust-allocated sprite cache sub-objects
7
 
8
+ use crate::log_line;
9
  use openwa_core::address::va;
10
+ use openwa_core::display::{DisplayBase, SpriteBufferCtrl, SpriteCacheWrapper};
11
  use openwa_core::rebase::rb;
12
  use openwa_core::vtable::patch_vtable;
13
  use openwa_core::wa_alloc::wa_free;
 
14
 
15
  /// The _purecall function address (calls abort).
16
  const PURECALL: u32 = 0x005D_4E16;
crates/openwa-wormkit/src/replacements/frontend.rs CHANGED
@@ -6,10 +6,10 @@
6
  use std::sync::atomic::{AtomicU32, Ordering};
7
 
8
  use crate::log_line;
9
- use openwa_core::wa::mfc::{CDialogHandle, CWndHandle};
10
- use openwa_core::wa_call;
11
  use openwa_core::address::va;
12
  use openwa_core::game::ScreenId;
 
 
13
 
14
  // Frontend dialog struct offsets (MFC CDialog-derived)
15
  const DIALOG_FLAGS: usize = 0x3C;
@@ -67,7 +67,9 @@ unsafe extern "cdecl" fn frontend_change_screen_impl(dialog: u32, screen_id: u32
67
  let name = ScreenId::try_from(screen_id as i32)
68
  .map(|s| format!("{s:?}"))
69
  .unwrap_or_else(|v| format!("Unknown({v})"));
70
- let _ = log_line(&format!("[FrontendChangeScreen] screen_id={screen_id} ({name})"));
 
 
71
  }
72
 
73
  pub fn install() -> Result<(), String> {
 
6
  use std::sync::atomic::{AtomicU32, Ordering};
7
 
8
  use crate::log_line;
 
 
9
  use openwa_core::address::va;
10
  use openwa_core::game::ScreenId;
11
+ use openwa_core::wa::mfc::{CDialogHandle, CWndHandle};
12
+ use openwa_core::wa_call;
13
 
14
  // Frontend dialog struct offsets (MFC CDialog-derived)
15
  const DIALOG_FLAGS: usize = 0x3C;
 
67
  let name = ScreenId::try_from(screen_id as i32)
68
  .map(|s| format!("{s:?}"))
69
  .unwrap_or_else(|v| format!("Unknown({v})"));
70
+ let _ = log_line(&format!(
71
+ "[FrontendChangeScreen] screen_id={screen_id} ({name})"
72
+ ));
73
  }
74
 
75
  pub fn install() -> Result<(), String> {
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -10,20 +10,19 @@
10
  //! - `DDGameWrapper__InitReplay` (0x56F860): usercall(EAX=game_info, ESI=this),
11
  //! plain RET (no stack args). Bridged via `call_init_replay`.
12
  //! - `DDGame__Constructor` (0x56E220): stdcall 9 params + implicit ECX=network.
13
- //! Bridged via `ddgame_constructor_call` which sets ECX then **tail-jumps**,
14
- //! preserving the exact stack so DDGame::ctor's `RET 0x24` returns to ctor_impl.
15
  //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute.
16
 
17
- use openwa_core::address::va;
18
- use openwa_core::engine::GameInfo;
19
- use openwa_core::rebase::rb;
20
- use openwa_core::engine::DDGameWrapper;
21
- use openwa_core::display::DDDisplay;
22
- use openwa_core::audio::DSSound;
23
- use openwa_core::engine::GameSession;
24
- use openwa_core::display::Palette;
25
  use crate::hook;
26
  use crate::log_line;
 
 
 
 
 
 
 
27
 
28
  /// Implicit EDI = game_info pointer, captured from EDI on entry.
29
  static mut GAME_INFO: *mut GameInfo = core::ptr::null_mut();
@@ -41,25 +40,15 @@ static mut DDGAME_CTOR_ADDR: u32 = 0;
41
  // ─── Bridge: DDGameWrapper__InitReplay ───────────────────────────────────────
42
  //
43
  // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params).
44
- // Declared as stdcall(game_info, this) so the Rust caller pushes both; the
45
- // naked body loads EAX/ESI from the stack and calls via INIT_REPLAY_ADDR.
46
- //
47
- // Stack on entry (after stdcall push):
48
- // [esp+0] = ret addr
49
- // [esp+4] = game_info (arg 1, pushed last by stdcall)
50
- // [esp+8] = this (arg 2, pushed first by stdcall)
51
- //
52
- // After `pushl %esi`:
53
- // [esp+0] = old_esi, [esp+4] = ret addr, [esp+8] = game_info, [esp+0xC] = this
54
  #[unsafe(naked)]
55
  unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *mut DDGameWrapper) {
56
  core::arch::naked_asm!(
57
  "pushl %esi",
58
  "movl 8(%esp), %eax", // EAX = game_info
59
  "movl 0xC(%esp), %esi", // ESI = this
60
- "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET from callee
61
- "popl %esi", // restore ESI
62
- "retl $8", // stdcall callee-cleanup: 2 × u32 = 8 bytes
63
  fn = sym INIT_REPLAY_ADDR,
64
  options(att_syntax),
65
  );
@@ -68,16 +57,7 @@ unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *m
68
  // ─── Bridge: DDGame__Constructor ─────────────────────────────────────────────
69
  //
70
  // Convention: stdcall 9 params + implicit ECX=network.
71
- //
72
- // Rust calls this as a normal stdcall(9 params). The naked body loads ECX from
73
- // DDGAME_CTOR_ECX and then **tail-jumps** (jmp, not call) to DDGame::ctor.
74
- //
75
- // Tail-jump rationale: the call instruction in ctor_impl already pushed the
76
- // return address at [esp+0]. After the tail-jump, DDGame::ctor sees exactly:
77
- // [esp+0] = return address back into ctor_impl
78
- // [esp+4] = this … [esp+24] = game_info (9 stdcall args)
79
- // ECX = network (implicit)
80
- // DDGame::ctor's `RET 0x24` pops 0x24 bytes and returns to ctor_impl. ✓
81
  #[unsafe(naked)]
82
  unsafe extern "stdcall" fn ddgame_constructor_call(
83
  _this: *mut DDGameWrapper,
@@ -91,8 +71,8 @@ unsafe extern "stdcall" fn ddgame_constructor_call(
91
  _game_info: *mut GameInfo,
92
  ) -> *mut u8 {
93
  core::arch::naked_asm!(
94
- "movl {ecx_val}, %ecx", // ECX = input_ctrl (implicit param for DDGame::ctor)
95
- "jmpl *({fn})", // tail-jump; DDGame::ctor's RET 0x24 returns to caller
96
  ecx_val = sym DDGAME_CTOR_ECX,
97
  fn = sym DDGAME_CTOR_ADDR,
98
  options(att_syntax),
@@ -100,10 +80,6 @@ unsafe extern "stdcall" fn ddgame_constructor_call(
100
  }
101
 
102
  /// Called by `impl_init_hardware` to construct the DDGameWrapper in-place.
103
- ///
104
- /// Sets `GAME_INFO` (read by `ctor_impl`) and delegates directly to `ctor_impl`,
105
- /// bypassing the naked entry trampoline which is designed as a hook target, not a
106
- /// callable subroutine.
107
  pub(crate) unsafe fn construct_ddgame_wrapper(
108
  game_info: *mut GameInfo,
109
  this: *mut DDGameWrapper,
@@ -116,8 +92,6 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
116
  ) -> *mut DDGameWrapper {
117
  GAME_INFO = game_info;
118
 
119
- let this = this;
120
-
121
  // Initialize DDGameWrapper fields (order matches original decompile).
122
  (*this).ddgame = core::ptr::null_mut();
123
  (*this).landscape = core::ptr::null_mut();
@@ -131,19 +105,112 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
131
  // Read timer_obj and net_game from the live game session struct.
132
  let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
133
  let timer_obj = (*session).timer_obj;
134
- let net_game = (*session).net_game;
135
 
136
- // Store input_ctrl as the implicit ECX for DDGame::ctor, then tail-jump-call.
137
- DDGAME_CTOR_ECX = input_ctrl as u32;
138
- ddgame_constructor_call(
139
- this, display, sound, keyboard, palette, streaming_audio,
140
- timer_obj, net_game, game_info,
141
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  // Initialize DDGame's game-state fields.
 
144
  let init_state: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
145
  core::mem::transmute(rb(va::DDGAME_INIT_GAME_STATE) as usize);
146
  init_state(this);
 
147
 
148
  let ddgame = (*this).ddgame;
149
  let _ = log_line(&format!(
@@ -151,16 +218,270 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
151
  this as u32, ddgame as u32,
152
  ));
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  this
155
  }
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  pub fn install() -> Result<(), String> {
158
  unsafe {
159
  INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY);
160
  DDGAME_CTOR_ADDR = rb(va::CONSTRUCT_DD_GAME);
161
- // Fully converted — only called from impl_init_hardware (Rust).
162
- // Trap panics if WA.exe unexpectedly calls the original.
 
163
  hook::install_trap!("DDGameWrapper__Constructor", va::CONSTRUCT_DD_GAME_WRAPPER);
 
 
 
 
 
164
  }
165
  Ok(())
166
  }
 
10
  //! - `DDGameWrapper__InitReplay` (0x56F860): usercall(EAX=game_info, ESI=this),
11
  //! plain RET (no stack args). Bridged via `call_init_replay`.
12
  //! - `DDGame__Constructor` (0x56E220): stdcall 9 params + implicit ECX=network.
13
+ //! Bridged via `ddgame_constructor_call`. Being incrementally replaced by
14
+ //! `create_ddgame()` in openwa-core (not yet complete).
15
  //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute.
16
 
 
 
 
 
 
 
 
 
17
  use crate::hook;
18
  use crate::log_line;
19
+ use openwa_core::address::va;
20
+ use openwa_core::audio::DSSound;
21
+ use openwa_core::display::{DDDisplay, Palette};
22
+ use openwa_core::engine::ddgame::{create_ddgame, init_constructor_addrs, DDGame};
23
+ use openwa_core::engine::{DDGameWrapper, GameInfo, GameSession};
24
+ use openwa_core::rebase::rb;
25
+ use openwa_core::wa_alloc::{wa_free, wa_malloc};
26
 
27
  /// Implicit EDI = game_info pointer, captured from EDI on entry.
28
  static mut GAME_INFO: *mut GameInfo = core::ptr::null_mut();
 
40
  // ─── Bridge: DDGameWrapper__InitReplay ───────────────────────────────────────
41
  //
42
  // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params).
 
 
 
 
 
 
 
 
 
 
43
  #[unsafe(naked)]
44
  unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *mut DDGameWrapper) {
45
  core::arch::naked_asm!(
46
  "pushl %esi",
47
  "movl 8(%esp), %eax", // EAX = game_info
48
  "movl 0xC(%esp), %esi", // ESI = this
49
+ "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET
50
+ "popl %esi",
51
+ "retl $8", // stdcall cleanup: 2 × u32 = 8
52
  fn = sym INIT_REPLAY_ADDR,
53
  options(att_syntax),
54
  );
 
57
  // ─── Bridge: DDGame__Constructor ─────────────────────────────────────────────
58
  //
59
  // Convention: stdcall 9 params + implicit ECX=network.
60
+ // Tail-jumps to original; DDGame::ctor's RET 0x24 returns to caller.
 
 
 
 
 
 
 
 
 
61
  #[unsafe(naked)]
62
  unsafe extern "stdcall" fn ddgame_constructor_call(
63
  _this: *mut DDGameWrapper,
 
71
  _game_info: *mut GameInfo,
72
  ) -> *mut u8 {
73
  core::arch::naked_asm!(
74
+ "movl {ecx_val}, %ecx", // ECX = network (implicit param)
75
+ "jmpl *({fn})", // tail-jump; RET 0x24 returns to caller
76
  ecx_val = sym DDGAME_CTOR_ECX,
77
  fn = sym DDGAME_CTOR_ADDR,
78
  options(att_syntax),
 
80
  }
81
 
82
  /// Called by `impl_init_hardware` to construct the DDGameWrapper in-place.
 
 
 
 
83
  pub(crate) unsafe fn construct_ddgame_wrapper(
84
  game_info: *mut GameInfo,
85
  this: *mut DDGameWrapper,
 
92
  ) -> *mut DDGameWrapper {
93
  GAME_INFO = game_info;
94
 
 
 
95
  // Initialize DDGameWrapper fields (order matches original decompile).
96
  (*this).ddgame = core::ptr::null_mut();
97
  (*this).landscape = core::ptr::null_mut();
 
105
  // Read timer_obj and net_game from the live game session struct.
106
  let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
107
  let timer_obj = (*session).timer_obj;
108
+ let net_game = (*session).net_game;
109
 
110
+ // Debug: dump session fields around net_game (0xBC-0xC8)
111
+ let session_base = session as *const u8;
112
+ let _ = log_line(&format!(
113
+ "[GameSession] session=0x{:08X} +0xBC=0x{:08X} +0xC0=0x{:08X} +0xC4=0x{:08X}",
114
+ session as u32,
115
+ *(session_base.add(0xBC) as *const u32),
116
+ *(session_base.add(0xC0) as *const u32),
117
+ *(session_base.add(0xC4) as *const u32),
118
+ ));
119
+ let _ = log_line(&format!(
120
+ "[GameSession] display=0x{:08X}, net_game=0x{:08X}, timer=0x{:08X}, game_info(EDI)=0x{:08X}",
121
+ display as u32, net_game as u32, timer_obj as u32, game_info as u32,
122
+ ));
123
+
124
+ // Toggle: use Rust constructor (true) or original (false)
125
+ // TODO: set back to true once sprite/gradient loading is ported.
126
+ const USE_RUST_CTOR: bool = true;
127
+ if USE_RUST_CTOR {
128
+ create_ddgame(
129
+ this,
130
+ keyboard as *mut openwa_core::input::DDKeyboard,
131
+ display,
132
+ sound,
133
+ palette,
134
+ streaming_audio as *mut openwa_core::audio::Music,
135
+ timer_obj,
136
+ net_game,
137
+ game_info,
138
+ input_ctrl as u32,
139
+ );
140
+ } else {
141
+ DDGAME_CTOR_ECX = input_ctrl as u32;
142
+ ddgame_constructor_call(
143
+ this,
144
+ display,
145
+ sound,
146
+ keyboard,
147
+ palette,
148
+ streaming_audio,
149
+ timer_obj,
150
+ net_game,
151
+ game_info,
152
+ );
153
+ }
154
+
155
+ let _ = log_line(&format!(
156
+ "[GameSession] create_ddgame returned, ddgame=0x{:08X}",
157
+ (*this).ddgame as u32
158
+ ));
159
+
160
+ // Dump DDGame state BEFORE InitGameState (constructor output only)
161
+ if std::env::var("OPENWA_VALIDATE").is_ok() {
162
+ let ddgame_pre = (*this).ddgame;
163
+ let real_dwords = ddgame_pre as *const u32;
164
+ let dword_count = 0x98B8 / 4;
165
+ let mut nonzero = 0u32;
166
+ let _ = log_line("[Shadow] === DDGame after constructor (before InitGameState) ===");
167
+ for i in 0..dword_count {
168
+ let val = *real_dwords.add(i);
169
+ if val != 0 {
170
+ nonzero += 1;
171
+ if nonzero <= 300 {
172
+ let _ = log_line(&format!("[Shadow:Pre] +0x{:04X} = 0x{:08X}", i * 4, val,));
173
+ }
174
+ }
175
+ }
176
+ let _ = log_line(&format!(
177
+ "[Shadow:Pre] Total non-zero: {} / {}",
178
+ nonzero, dword_count,
179
+ ));
180
+ }
181
+
182
+ // Dump DDGameWrapper state
183
+ if std::env::var("OPENWA_VALIDATE").is_ok() {
184
+ let wrapper_dwords = this as *const u32;
185
+ // DDGameWrapper is ~0x6EF0 bytes (from DDGameWrapper__Constructor at 0x56DEF0)
186
+ let w_count = 0x6EF0 / 4;
187
+ let mut w_nonzero = 0u32;
188
+ let _ = log_line("[Shadow:Wrapper] === DDGameWrapper ===");
189
+ for i in 0..w_count {
190
+ let val = *wrapper_dwords.add(i);
191
+ if val != 0 {
192
+ w_nonzero += 1;
193
+ if w_nonzero <= 200 {
194
+ let _ = log_line(&format!(
195
+ "[Shadow:Wrapper] +0x{:04X} = 0x{:08X}",
196
+ i * 4,
197
+ val,
198
+ ));
199
+ }
200
+ }
201
+ }
202
+ let _ = log_line(&format!(
203
+ "[Shadow:Wrapper] Total non-zero: {} / {}",
204
+ w_nonzero, w_count,
205
+ ));
206
+ }
207
 
208
  // Initialize DDGame's game-state fields.
209
+ let _ = log_line("[GameSession] calling InitGameState");
210
  let init_state: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
211
  core::mem::transmute(rb(va::DDGAME_INIT_GAME_STATE) as usize);
212
  init_state(this);
213
+ let _ = log_line("[GameSession] InitGameState done");
214
 
215
  let ddgame = (*this).ddgame;
216
  let _ = log_line(&format!(
 
218
  this as u32, ddgame as u32,
219
  ));
220
 
221
+ // ── Shadow comparison: run create_ddgame() into a temp allocation ──
222
+ // Compare byte-by-byte with the original to find porting bugs.
223
+ if std::env::var("OPENWA_VALIDATE").is_ok() {
224
+ shadow_compare_ddgame(
225
+ this,
226
+ keyboard,
227
+ display,
228
+ sound,
229
+ palette,
230
+ streaming_audio,
231
+ input_ctrl,
232
+ timer_obj,
233
+ net_game,
234
+ game_info,
235
+ ddgame,
236
+ );
237
+ }
238
+
239
  this
240
  }
241
 
242
+ /// Shadow-construct a DDGame via create_ddgame() and compare byte-by-byte
243
+ /// with the real DDGame produced by the original constructor.
244
+ ///
245
+ /// Logs the first N differing DWORD offsets. This lets us verify each section
246
+ /// of create_ddgame() against the original without breaking the game.
247
+ #[allow(clippy::too_many_arguments)]
248
+ unsafe fn shadow_compare_ddgame(
249
+ wrapper: *mut DDGameWrapper,
250
+ keyboard: *mut u8,
251
+ display: *mut DDDisplay,
252
+ sound: *mut DSSound,
253
+ palette: *mut Palette,
254
+ streaming_audio: *mut u8,
255
+ input_ctrl: *mut u8,
256
+ timer_obj: *mut u8,
257
+ net_game: *mut u8,
258
+ game_info: *mut GameInfo,
259
+ real_ddgame: *mut DDGame,
260
+ ) {
261
+ let _ = log_line("[Shadow] Running create_ddgame() for comparison...");
262
+
263
+ // Create a temporary wrapper clone so create_ddgame can write to it
264
+ // without corrupting the real wrapper.
265
+ let shadow_wrapper = wa_malloc(core::mem::size_of::<DDGameWrapper>() as u32);
266
+ core::ptr::copy_nonoverlapping(
267
+ wrapper as *const u8,
268
+ shadow_wrapper,
269
+ core::mem::size_of::<DDGameWrapper>(),
270
+ );
271
+ let _sw = shadow_wrapper as *mut DDGameWrapper;
272
+
273
+ // Instead of running create_ddgame (which has side effects from resource
274
+ // loading), compare the fields that we KNOW create_ddgame sets correctly:
275
+ // the parameter storage and simple flag fields in the first 0x30 bytes
276
+ // and scattered known offsets.
277
+ //
278
+ // We compare the real DDGame against what create_ddgame WOULD write,
279
+ // computed without side effects.
280
+ let r = real_ddgame as *const u8;
281
+
282
+ // Check param storage (offsets 0x00-0x28)
283
+ let checks: &[(&str, usize, u32)] = &[
284
+ ("keyboard", 0x00, keyboard as u32),
285
+ ("display", 0x04, display as u32),
286
+ ("sound", 0x08, sound as u32),
287
+ ("palette", 0x10, palette as u32),
288
+ ("music", 0x14, streaming_audio as u32),
289
+ ("param7", 0x18, timer_obj as u32),
290
+ ("caller/ECX", 0x1C, input_ctrl as u32),
291
+ ("game_info", 0x24, game_info as u32),
292
+ ("net_game", 0x28, net_game as u32),
293
+ (
294
+ "sound_available",
295
+ 0x7EF8,
296
+ if *(game_info as *const u8).add(0xF914).cast::<i32>() == 0 {
297
+ 1
298
+ } else {
299
+ 0
300
+ },
301
+ ),
302
+ ("field_7EFC", 0x7EFC, 1),
303
+ ];
304
+
305
+ let mut ok = 0u32;
306
+ let mut fail = 0u32;
307
+ for &(name, offset, expected) in checks {
308
+ let actual = *(r.add(offset) as *const u32);
309
+ if actual == expected {
310
+ ok += 1;
311
+ } else {
312
+ let _ = log_line(&format!(
313
+ "[Shadow] FAIL +0x{:04X} ({}): expected=0x{:08X} actual=0x{:08X}",
314
+ offset, name, expected, actual,
315
+ ));
316
+ fail += 1;
317
+ }
318
+ }
319
+
320
+ // Dump a summary of selected DDGame regions for future comparison
321
+ // (pointer fields that would differ between original and shadow)
322
+ let _ = log_line(&format!("[Shadow] Param check: {} OK, {} FAIL", ok, fail,));
323
+
324
+ // Dump ALL non-zero DWORDs from the real DDGame.
325
+ // This gives us the exact map of what the constructor initializes.
326
+ let real_dwords = real_ddgame as *const u32;
327
+ let dword_count = 0x98B8 / 4;
328
+ let mut nonzero = 0u32;
329
+ for i in 0..dword_count {
330
+ let val = *real_dwords.add(i);
331
+ if val != 0 {
332
+ nonzero += 1;
333
+ // Log up to 200 non-zero fields
334
+ if nonzero <= 200 {
335
+ let _ = log_line(&format!("[Shadow] DDGame+0x{:04X} = 0x{:08X}", i * 4, val,));
336
+ }
337
+ }
338
+ }
339
+ let _ = log_line(&format!(
340
+ "[Shadow] Total non-zero DWORDs: {} / {}",
341
+ nonzero, dword_count,
342
+ ));
343
+
344
+ // Also dump non-zero wrapper fields
345
+ let wrapper_dwords = core::mem::size_of::<DDGameWrapper>() / 4;
346
+ let real_w = wrapper as *const u32;
347
+ let mut w_nonzero = 0u32;
348
+ for i in 0..wrapper_dwords {
349
+ let val = *real_w.add(i);
350
+ if val != 0 {
351
+ w_nonzero += 1;
352
+ if w_nonzero <= 50 {
353
+ let _ = log_line(&format!("[Shadow] Wrapper+0x{:04X} = 0x{:08X}", i * 4, val,));
354
+ }
355
+ }
356
+ }
357
+ let _ = log_line(&format!(
358
+ "[Shadow] Wrapper non-zero DWORDs: {} / {}",
359
+ w_nonzero, wrapper_dwords,
360
+ ));
361
+
362
+ wa_free(shadow_wrapper);
363
+ }
364
+
365
+ // ── PCLandscape__Constructor param logger ──────────────────────────
366
+ static mut PC_LANDSCAPE_TRAMPOLINE: *const () = core::ptr::null();
367
+
368
+ /// Passthrough hook: logs all 11 params, then calls original.
369
+ unsafe extern "stdcall" fn hook_pc_landscape_ctor(
370
+ p1: u32,
371
+ p2: u32,
372
+ p3: u32,
373
+ p4: u32,
374
+ p5: u32,
375
+ p6: u32,
376
+ p7: u32,
377
+ p8: u32,
378
+ p9: u32,
379
+ p10: u32,
380
+ p11: u32,
381
+ ) -> u32 {
382
+ let _ = log_line(&format!(
383
+ "[PCLandscape] p1=0x{:08X} p2=0x{:08X} p3=0x{:08X} p4=0x{:08X} p5=0x{:08X} p6=0x{:08X}",
384
+ p1, p2, p3, p4, p5, p6
385
+ ));
386
+ let _ = log_line(&format!(
387
+ "[PCLandscape] p7=0x{:08X} p8=0x{:08X} p9=0x{:08X} p10=0x{:08X} p11=0x{:08X}",
388
+ p7, p8, p9, p10, p11
389
+ ));
390
+ // Call original
391
+ let orig: unsafe extern "stdcall" fn(
392
+ u32,
393
+ u32,
394
+ u32,
395
+ u32,
396
+ u32,
397
+ u32,
398
+ u32,
399
+ u32,
400
+ u32,
401
+ u32,
402
+ u32,
403
+ ) -> u32 = core::mem::transmute(PC_LANDSCAPE_TRAMPOLINE);
404
+ orig(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
405
+ }
406
+
407
+ // ── HUD_LoadWeaponSprites param logger ──────────────────────────────
408
+ static mut HUD_LOAD_TRAMPOLINE: *const () = core::ptr::null();
409
+
410
+ unsafe extern "thiscall" fn hook_hud_load(this_ecx: u32, p1: u32, p2: u32) -> u32 {
411
+ let _ = log_line(&format!(
412
+ "[HUD] ECX=0x{:08X} p1=0x{:08X} p2=0x{:08X}",
413
+ this_ecx, p1, p2
414
+ ));
415
+ let orig: unsafe extern "thiscall" fn(u32, u32, u32) -> u32 =
416
+ core::mem::transmute(HUD_LOAD_TRAMPOLINE);
417
+ orig(this_ecx, p1, p2)
418
+ }
419
+
420
+ // ── SpriteRegion__Constructor param logger ──────────────────────────
421
+ static mut SPRITE_REGION_TRAMPOLINE: *const () = core::ptr::null();
422
+
423
+ /// Log fastcall params. Called from naked trampoline with all params on stack.
424
+ unsafe extern "cdecl" fn log_sprite_region(
425
+ ecx: u32,
426
+ edx: u32,
427
+ p1: u32,
428
+ p2: u32,
429
+ p3: u32,
430
+ p4: u32,
431
+ p5: u32,
432
+ p6: u32,
433
+ ) {
434
+ let _ = log_line(&format!(
435
+ "[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}",
436
+ ecx, edx, p1, p2, p3, p4, p5, p6));
437
+ }
438
+
439
+ /// Naked trampoline: captures ECX/EDX, logs, then calls original fastcall.
440
+ #[unsafe(naked)]
441
+ unsafe extern "C" fn hook_sprite_region_ctor() {
442
+ core::arch::naked_asm!(
443
+ // Save all regs
444
+ "pushl %eax",
445
+ "pushl %ecx",
446
+ "pushl %edx",
447
+ // Push all 8 params for logger: ECX, EDX, then 6 stack params
448
+ // Stack after 3 pushes: [edx][ecx][eax][ret][p1][p2][p3][p4][p5][p6]
449
+ // p1 at ESP+16, p2 at ESP+20, ...
450
+ "pushl 36(%esp)", // p6
451
+ "pushl 36(%esp)", // p5
452
+ "pushl 36(%esp)", // p4
453
+ "pushl 36(%esp)", // p3
454
+ "pushl 36(%esp)", // p2
455
+ "pushl 36(%esp)", // p1
456
+ "pushl %edx", // EDX
457
+ "pushl %ecx", // ECX
458
+ "calll {logger}",
459
+ "addl $32, %esp", // clean 8 cdecl params
460
+ // Restore regs
461
+ "popl %edx",
462
+ "popl %ecx",
463
+ "popl %eax",
464
+ // Jump to original (fastcall, same params on stack)
465
+ "jmpl *({tramp})",
466
+ logger = sym log_sprite_region,
467
+ tramp = sym SPRITE_REGION_TRAMPOLINE,
468
+ options(att_syntax),
469
+ );
470
+ }
471
+
472
  pub fn install() -> Result<(), String> {
473
  unsafe {
474
  INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY);
475
  DDGAME_CTOR_ADDR = rb(va::CONSTRUCT_DD_GAME);
476
+ // Initialize runtime addresses for create_ddgame bridges (future use).
477
+ init_constructor_addrs();
478
+ // DDGameWrapper__Constructor is fully converted — trap the original.
479
  hook::install_trap!("DDGameWrapper__Constructor", va::CONSTRUCT_DD_GAME_WRAPPER);
480
+
481
+ // PCLandscape hook disabled — params verified, may interfere with Rust ctor
482
+
483
+ // Param hooks disabled — they served their purpose and may interfere
484
+ // with the Rust constructor's own calls to these functions.
485
  }
486
  Ok(())
487
  }
crates/openwa-wormkit/src/replacements/hardware_init.rs CHANGED
@@ -41,16 +41,16 @@
41
  //! DDNetGameWrapper (0x2C, stdcall ctor) → session+0xC0
42
  //! ```
43
 
 
 
 
44
  use openwa_core::address::va;
45
- use openwa_core::rebase::rb;
46
- use openwa_core::engine::{GameInfo, GameSession, DDGameWrapper, GameTimer, DDNetGameWrapper};
47
- use openwa_core::input::{DDKeyboard, InputCtrl, InputCtrlVtable};
48
- use openwa_core::display::{DDDisplay, DisplayBase, DisplayGfx, Palette};
49
  use openwa_core::audio::{DSSound, StreamingAudio};
 
 
 
 
50
  use openwa_core::wa_alloc::WABox;
51
- use crate::hook;
52
- use crate::log_line;
53
- use super::game_session;
54
 
55
  // ─── Entry trampoline state ───────────────────────────────────────────────────
56
 
@@ -73,7 +73,6 @@ static mut INPUT_CTRL_ESI: u32 = 0;
73
  /// Saved ESI across the `call_input_ctrl_init` call.
74
  static mut INPUT_CTRL_SAVED_ESI: u32 = 0;
75
 
76
-
77
  // ─── Bridges ─────────────────────────────────────────────────────────────────
78
  //
79
  // 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;
83
  /// Timer constructor: `usercall(ESI=timer_ptr, EAX=crosshair_threshold)`, plain RET.
84
  /// Returns whatever EAX holds after the call.
85
  #[unsafe(naked)]
86
- unsafe extern "cdecl" fn call_timer_ctor(_timer_ptr: *mut GameTimer, _crosshair_threshold: u32) -> u32 {
 
 
 
87
  core::arch::naked_asm!(
88
  // [esp+0]=bridge_ret, [esp+4]=timer_ptr, [esp+8]=crosshair_threshold
89
  "pushl %esi",
@@ -191,11 +193,11 @@ unsafe extern "stdcall" fn call_ddisplay_init(
191
  /// The returned pointer is always valid (sound may be partially initialized
192
  /// if COM steps fail, matching original WA behavior).
193
  unsafe fn create_dssound(hwnd: u32) -> *mut DSSound {
 
194
  use windows::Win32::Media::Audio::DirectSound::{
195
- DirectSoundCreate, IDirectSound, IDirectSoundBuffer,
196
- DSBUFFERDESC, DSBCAPS_PRIMARYBUFFER, DSBPLAY_LOOPING, DSSCL_PRIORITY,
197
  };
198
- use windows::Win32::Foundation::HWND;
199
 
200
  // Pure Rust construction — replaces call_dssound_ctor bridge.
201
  let snd = WABox::<DSSound>::from_value(DSSound::new(hwnd)).leak();
@@ -219,7 +221,8 @@ unsafe fn create_dssound(hwnd: u32) -> *mut DSSound {
219
  let primary = primary.unwrap();
220
 
221
  // GetCaps to populate primary_buffer_caps
222
- let mut caps = core::mem::zeroed::<windows::Win32::Media::Audio::DirectSound::DSBCAPS>();
 
223
  caps.dwSize = core::mem::size_of_val(&caps) as u32;
224
  let _ = primary.GetCaps(&mut caps);
225
  (*snd).primary_buffer_caps = caps.dwBufferBytes;
@@ -252,7 +255,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
252
  let _ = log_line("[hardware_init] GameEngine::InitHardware");
253
  let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
254
  let gi = &mut *game_info;
255
- let crosshair_threshold = gi.crosshair_overflow_threshold as u32;
256
 
257
  // ── Input controller (if param4 != 0) ────────────────────────────────────
258
  if param4 == 0 {
@@ -338,10 +341,15 @@ unsafe extern "cdecl" fn impl_init_hardware(
338
  if suppress == 0 {
339
  SetCursorPos(cx, cy);
340
  if fullscreen {
341
- use windows_sys::Win32::UI::WindowsAndMessaging::{ClipCursor, GetClientRect};
342
  use windows_sys::Win32::Foundation::{HWND, RECT};
 
343
  let hwnd_val: HWND = *(rb(va::G_FRONTEND_HWND) as *const HWND);
344
- let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 };
 
 
 
 
 
345
  GetClientRect(hwnd_val, &mut rect);
346
  let map_fn_ptr = *(rb(va::IAT_MAP_WINDOW_POINTS) as *const usize);
347
  let map_fn: unsafe extern "stdcall" fn(HWND, HWND, *mut RECT, u32) -> i32 =
@@ -355,7 +363,8 @@ unsafe extern "cdecl" fn impl_init_hardware(
355
  let kb = WABox::from_value(DDKeyboard::new(
356
  rb(va::DDKEYBOARD_VTABLE),
357
  &raw mut gi.input_state_f918 as u32,
358
- )).leak();
 
359
  (*session).keyboard = kb;
360
 
361
  // ── Palette (inline construction) ─────────────────────────────────────
@@ -379,11 +388,11 @@ unsafe extern "cdecl" fn impl_init_hardware(
379
  }
380
  } else {
381
  // ── Headless / stats mode ─────────────────────────────────────────────
382
- (*session).display = DisplayBase::new_headless() as *mut u8;
383
- (*session).keyboard = core::ptr::null_mut();
384
- (*session).sound = core::ptr::null_mut();
385
- (*session).palette = core::ptr::null_mut();
386
- (*session).streaming_audio = core::ptr::null_mut();
387
  }
388
 
389
  // ── Session flags ─────────────────────────────────────────────────────────
@@ -459,10 +468,10 @@ unsafe extern "C" fn hook_init_hardware() {
459
 
460
  pub fn install() -> Result<(), String> {
461
  unsafe {
462
- TIMER_CTOR_ADDR = rb(va::GAME_ENGINE_TIMER_CTOR);
463
- INPUT_CTRL_INIT_ADDR = rb(va::INPUT_CTRL_INIT);
464
- STREAM_CTOR_ADDR = rb(va::STREAMING_AUDIO_CTOR);
465
- DDISPLAY_INIT_ADDR = rb(va::DDISPLAY_INIT);
466
 
467
  // Full replacement — trampoline not needed.
468
  let _ = hook::install(
 
41
  //! DDNetGameWrapper (0x2C, stdcall ctor) → session+0xC0
42
  //! ```
43
 
44
+ use super::game_session;
45
+ use crate::hook;
46
+ use crate::log_line;
47
  use openwa_core::address::va;
 
 
 
 
48
  use openwa_core::audio::{DSSound, StreamingAudio};
49
+ use openwa_core::display::{DDDisplay, DisplayBase, DisplayGfx, Palette};
50
+ use openwa_core::engine::{DDGameWrapper, DDNetGameWrapper, GameInfo, GameSession, GameTimer};
51
+ use openwa_core::input::{DDKeyboard, InputCtrl, InputCtrlVtable};
52
+ use openwa_core::rebase::rb;
53
  use openwa_core::wa_alloc::WABox;
 
 
 
54
 
55
  // ─── Entry trampoline state ───────────────────────────────────────────────────
56
 
 
73
  /// Saved ESI across the `call_input_ctrl_init` call.
74
  static mut INPUT_CTRL_SAVED_ESI: u32 = 0;
75
 
 
76
  // ─── Bridges ─────────────────────────────────────────────────────────────────
77
  //
78
  // All bridges use the "pop ECX (save bridge_ret) / call callee / push ECX" idiom
 
82
  /// Timer constructor: `usercall(ESI=timer_ptr, EAX=crosshair_threshold)`, plain RET.
83
  /// Returns whatever EAX holds after the call.
84
  #[unsafe(naked)]
85
+ unsafe extern "cdecl" fn call_timer_ctor(
86
+ _timer_ptr: *mut GameTimer,
87
+ _crosshair_threshold: u32,
88
+ ) -> u32 {
89
  core::arch::naked_asm!(
90
  // [esp+0]=bridge_ret, [esp+4]=timer_ptr, [esp+8]=crosshair_threshold
91
  "pushl %esi",
 
193
  /// The returned pointer is always valid (sound may be partially initialized
194
  /// if COM steps fail, matching original WA behavior).
195
  unsafe fn create_dssound(hwnd: u32) -> *mut DSSound {
196
+ use windows::Win32::Foundation::HWND;
197
  use windows::Win32::Media::Audio::DirectSound::{
198
+ DirectSoundCreate, IDirectSound, IDirectSoundBuffer, DSBCAPS_PRIMARYBUFFER,
199
+ DSBPLAY_LOOPING, DSBUFFERDESC, DSSCL_PRIORITY,
200
  };
 
201
 
202
  // Pure Rust construction — replaces call_dssound_ctor bridge.
203
  let snd = WABox::<DSSound>::from_value(DSSound::new(hwnd)).leak();
 
221
  let primary = primary.unwrap();
222
 
223
  // GetCaps to populate primary_buffer_caps
224
+ let mut caps =
225
+ core::mem::zeroed::<windows::Win32::Media::Audio::DirectSound::DSBCAPS>();
226
  caps.dwSize = core::mem::size_of_val(&caps) as u32;
227
  let _ = primary.GetCaps(&mut caps);
228
  (*snd).primary_buffer_caps = caps.dwBufferBytes;
 
255
  let _ = log_line("[hardware_init] GameEngine::InitHardware");
256
  let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
257
  let gi = &mut *game_info;
258
+ let crosshair_threshold = gi.game_version as u32;
259
 
260
  // ── Input controller (if param4 != 0) ────────────────────────────────────
261
  if param4 == 0 {
 
341
  if suppress == 0 {
342
  SetCursorPos(cx, cy);
343
  if fullscreen {
 
344
  use windows_sys::Win32::Foundation::{HWND, RECT};
345
+ use windows_sys::Win32::UI::WindowsAndMessaging::{ClipCursor, GetClientRect};
346
  let hwnd_val: HWND = *(rb(va::G_FRONTEND_HWND) as *const HWND);
347
+ let mut rect = RECT {
348
+ left: 0,
349
+ top: 0,
350
+ right: 0,
351
+ bottom: 0,
352
+ };
353
  GetClientRect(hwnd_val, &mut rect);
354
  let map_fn_ptr = *(rb(va::IAT_MAP_WINDOW_POINTS) as *const usize);
355
  let map_fn: unsafe extern "stdcall" fn(HWND, HWND, *mut RECT, u32) -> i32 =
 
363
  let kb = WABox::from_value(DDKeyboard::new(
364
  rb(va::DDKEYBOARD_VTABLE),
365
  &raw mut gi.input_state_f918 as u32,
366
+ ))
367
+ .leak();
368
  (*session).keyboard = kb;
369
 
370
  // ── Palette (inline construction) ─────────────────────────────────────
 
388
  }
389
  } else {
390
  // ── Headless / stats mode ─────────────────────────────────────────────
391
+ (*session).display = DisplayBase::new_headless() as *mut u8;
392
+ (*session).keyboard = core::ptr::null_mut();
393
+ (*session).sound = core::ptr::null_mut();
394
+ (*session).palette = core::ptr::null_mut();
395
+ (*session).streaming_audio = core::ptr::null_mut();
396
  }
397
 
398
  // ── Session flags ─────────────────────────────────────────────────────────
 
468
 
469
  pub fn install() -> Result<(), String> {
470
  unsafe {
471
+ TIMER_CTOR_ADDR = rb(va::GAME_ENGINE_TIMER_CTOR);
472
+ INPUT_CTRL_INIT_ADDR = rb(va::INPUT_CTRL_INIT);
473
+ STREAM_CTOR_ADDR = rb(va::STREAMING_AUDIO_CTOR);
474
+ DDISPLAY_INIT_ADDR = rb(va::DDISPLAY_INIT);
475
 
476
  // Full replacement — trampoline not needed.
477
  let _ = hook::install(
crates/openwa-wormkit/src/replacements/headless.rs CHANGED
@@ -11,41 +11,63 @@
11
  use crate::log_line;
12
 
13
  unsafe extern "system" fn hook_messagebox_a(
14
- _hwnd: u32, text: *const u8, caption: *const u8, flags: u32,
 
 
 
15
  ) -> i32 {
16
- let t = if text.is_null() { "<null>".to_string() }
17
- else {
 
18
  std::ffi::CStr::from_ptr(text as *const i8)
19
- .to_str().unwrap_or("<invalid utf8>").to_string()
 
 
20
  };
21
- let c = if caption.is_null() { "<null>".to_string() }
22
- else {
 
23
  std::ffi::CStr::from_ptr(caption as *const i8)
24
- .to_str().unwrap_or("<invalid utf8>").to_string()
 
 
25
  };
26
  let _ = log_line(&format!(
27
  "[Headless] Suppressed MessageBoxA: caption={c:?} text={t:?}"
28
  ));
29
- if flags & 0xF == 0x4 { 6 } else { 1 } // IDYES for MB_YESNO, IDOK otherwise
 
 
 
 
30
  }
31
 
32
  unsafe extern "system" fn hook_messagebox_w(
33
- _hwnd: u32, text: *const u16, caption: *const u16, flags: u32,
 
 
 
34
  ) -> i32 {
35
- let t = if text.is_null() { "<null>".to_string() }
36
- else {
 
37
  let len = (0..).take_while(|&i| *text.add(i) != 0).count();
38
  String::from_utf16_lossy(core::slice::from_raw_parts(text, len))
39
  };
40
- let c = if caption.is_null() { "<null>".to_string() }
41
- else {
 
42
  let len = (0..).take_while(|&i| *caption.add(i) != 0).count();
43
  String::from_utf16_lossy(core::slice::from_raw_parts(caption, len))
44
  };
45
  let _ = log_line(&format!(
46
  "[Headless] Suppressed MessageBoxW: caption={c:?} text={t:?}"
47
  ));
48
- if flags & 0xF == 0x4 { 6 } else { 1 }
 
 
 
 
49
  }
50
 
51
  pub fn install() -> Result<(), String> {
@@ -56,20 +78,24 @@ pub fn install() -> Result<(), String> {
56
  let _ = log_line("[Headless] Suppressing all message boxes");
57
 
58
  unsafe {
59
- let module = windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(
60
- b"user32.dll\0".as_ptr(),
61
- );
62
  if module.is_null() {
63
  return Err("user32.dll not loaded".to_string());
64
  }
65
 
66
  for (name, hook_fn) in [
67
- (&b"MessageBoxA\0"[..], hook_messagebox_a as *mut core::ffi::c_void),
68
- (&b"MessageBoxW\0"[..], hook_messagebox_w as *mut core::ffi::c_void),
 
 
 
 
 
 
69
  ] {
70
- let proc = windows_sys::Win32::System::LibraryLoader::GetProcAddress(
71
- module, name.as_ptr(),
72
- );
73
  if let Some(addr) = proc {
74
  let target = addr as *mut core::ffi::c_void;
75
  if let Ok(trampoline) = minhook::MinHook::create_hook(target, hook_fn) {
 
11
  use crate::log_line;
12
 
13
  unsafe extern "system" fn hook_messagebox_a(
14
+ _hwnd: u32,
15
+ text: *const u8,
16
+ caption: *const u8,
17
+ flags: u32,
18
  ) -> i32 {
19
+ let t = if text.is_null() {
20
+ "<null>".to_string()
21
+ } else {
22
  std::ffi::CStr::from_ptr(text as *const i8)
23
+ .to_str()
24
+ .unwrap_or("<invalid utf8>")
25
+ .to_string()
26
  };
27
+ let c = if caption.is_null() {
28
+ "<null>".to_string()
29
+ } else {
30
  std::ffi::CStr::from_ptr(caption as *const i8)
31
+ .to_str()
32
+ .unwrap_or("<invalid utf8>")
33
+ .to_string()
34
  };
35
  let _ = log_line(&format!(
36
  "[Headless] Suppressed MessageBoxA: caption={c:?} text={t:?}"
37
  ));
38
+ if flags & 0xF == 0x4 {
39
+ 6
40
+ } else {
41
+ 1
42
+ } // IDYES for MB_YESNO, IDOK otherwise
43
  }
44
 
45
  unsafe extern "system" fn hook_messagebox_w(
46
+ _hwnd: u32,
47
+ text: *const u16,
48
+ caption: *const u16,
49
+ flags: u32,
50
  ) -> i32 {
51
+ let t = if text.is_null() {
52
+ "<null>".to_string()
53
+ } else {
54
  let len = (0..).take_while(|&i| *text.add(i) != 0).count();
55
  String::from_utf16_lossy(core::slice::from_raw_parts(text, len))
56
  };
57
+ let c = if caption.is_null() {
58
+ "<null>".to_string()
59
+ } else {
60
  let len = (0..).take_while(|&i| *caption.add(i) != 0).count();
61
  String::from_utf16_lossy(core::slice::from_raw_parts(caption, len))
62
  };
63
  let _ = log_line(&format!(
64
  "[Headless] Suppressed MessageBoxW: caption={c:?} text={t:?}"
65
  ));
66
+ if flags & 0xF == 0x4 {
67
+ 6
68
+ } else {
69
+ 1
70
+ }
71
  }
72
 
73
  pub fn install() -> Result<(), String> {
 
78
  let _ = log_line("[Headless] Suppressing all message boxes");
79
 
80
  unsafe {
81
+ let module =
82
+ windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(b"user32.dll\0".as_ptr());
 
83
  if module.is_null() {
84
  return Err("user32.dll not loaded".to_string());
85
  }
86
 
87
  for (name, hook_fn) in [
88
+ (
89
+ &b"MessageBoxA\0"[..],
90
+ hook_messagebox_a as *mut core::ffi::c_void,
91
+ ),
92
+ (
93
+ &b"MessageBoxW\0"[..],
94
+ hook_messagebox_w as *mut core::ffi::c_void,
95
+ ),
96
  ] {
97
+ let proc =
98
+ windows_sys::Win32::System::LibraryLoader::GetProcAddress(module, name.as_ptr());
 
99
  if let Some(addr) = proc {
100
  let target = addr as *mut core::ffi::c_void;
101
  if let Ok(trampoline) = minhook::MinHook::create_hook(target, hook_fn) {
crates/openwa-wormkit/src/replacements/input.rs CHANGED
@@ -16,9 +16,9 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
16
 
17
  use crate::hook;
18
  use crate::log_line;
19
- use openwa_core::rebase::rb;
20
  use openwa_core::address::va;
21
  use openwa_core::engine::{DDGame, DDGameWrapper};
 
22
 
23
  extern "system" {
24
  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
71
  let wa_base = rb(va::IMAGE_BASE);
72
  let delta = wa_base.wrapping_sub(va::IMAGE_BASE);
73
 
74
- let _ = log_line(&format!("\n=== {}+0x{:04X}..0x{:04X} ===", struct_name, offset, offset + size));
 
 
 
 
 
75
 
76
  let dword_count = size / 4;
77
  for i in 0..dword_count {
@@ -89,7 +94,10 @@ pub unsafe fn dump_region(base_ptr: *const u8, offset: usize, size: usize, struc
89
  let vt0 = *(val as *const u32);
90
  let _ = log_line(&format!(
91
  " +0x{:04X}: 0x{:08X} [VTABLE] g:0x{:08X} vt[0]=g:0x{:08X}",
92
- field_offset, val, ghidra_val, vt0.wrapping_sub(delta)
 
 
 
93
  ));
94
  } else {
95
  let _ = log_line(&format!(
@@ -135,10 +143,7 @@ pub unsafe fn dump_region(base_ptr: *const u8, offset: usize, size: usize, struc
135
  ));
136
  }
137
  } else {
138
- let _ = log_line(&format!(
139
- " +0x{:04X}: 0x{:08X} [value]",
140
- field_offset, val
141
- ));
142
  }
143
  }
144
  }
 
16
 
17
  use crate::hook;
18
  use crate::log_line;
 
19
  use openwa_core::address::va;
20
  use openwa_core::engine::{DDGame, DDGameWrapper};
21
+ use openwa_core::rebase::rb;
22
 
23
  extern "system" {
24
  fn IsBadReadPtr(lp: *const u8, ucb: u32) -> i32;
 
71
  let wa_base = rb(va::IMAGE_BASE);
72
  let delta = wa_base.wrapping_sub(va::IMAGE_BASE);
73
 
74
+ let _ = log_line(&format!(
75
+ "\n=== {}+0x{:04X}..0x{:04X} ===",
76
+ struct_name,
77
+ offset,
78
+ offset + size
79
+ ));
80
 
81
  let dword_count = size / 4;
82
  for i in 0..dword_count {
 
94
  let vt0 = *(val as *const u32);
95
  let _ = log_line(&format!(
96
  " +0x{:04X}: 0x{:08X} [VTABLE] g:0x{:08X} vt[0]=g:0x{:08X}",
97
+ field_offset,
98
+ val,
99
+ ghidra_val,
100
+ vt0.wrapping_sub(delta)
101
  ));
102
  } else {
103
  let _ = log_line(&format!(
 
143
  ));
144
  }
145
  } else {
146
+ let _ = log_line(&format!(" +0x{:04X}: 0x{:08X} [value]", field_offset, val));
 
 
 
147
  }
148
  }
149
  }
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -1,12 +1,13 @@
1
  mod config;
 
2
  mod display;
3
  mod frontend;
4
  mod game_session;
5
  mod hardware_init;
6
  mod headless;
7
  pub(crate) mod input;
8
- mod scheme;
9
  mod render;
 
10
  mod sound;
11
  mod speech;
12
  mod team;
@@ -26,5 +27,6 @@ pub fn install_all() -> Result<(), String> {
26
  sound::install()?;
27
  speech::install()?;
28
  input::install()?;
 
29
  Ok(())
30
  }
 
1
  mod config;
2
+ mod ddgame_init;
3
  mod display;
4
  mod frontend;
5
  mod game_session;
6
  mod hardware_init;
7
  mod headless;
8
  pub(crate) mod input;
 
9
  mod render;
10
+ mod scheme;
11
  mod sound;
12
  mod speech;
13
  mod team;
 
27
  sound::install()?;
28
  speech::install()?;
29
  input::install()?;
30
+ ddgame_init::install()?;
31
  Ok(())
32
  }
crates/openwa-wormkit/src/replacements/render.rs CHANGED
@@ -3,8 +3,8 @@
3
  //! All functions enqueue commands to the RenderQueue's downward-growing buffer.
4
  //! Calling conventions are __usercall variants with register + stack params.
5
 
6
- use openwa_core::rebase::rb;
7
  use openwa_core::address::va;
 
8
  use openwa_core::render::queue::*;
9
  use openwa_core::task::{BungeeTrailTask, WeaponAimTask};
10
 
@@ -17,12 +17,7 @@ use crate::hook::{self, usercall_trampoline};
17
  usercall_trampoline!(fn trampoline_draw_pixel; impl_fn = draw_pixel_impl;
18
  reg = eax; stack_params = 3; ret_bytes = "0xC");
19
 
20
- unsafe extern "cdecl" fn draw_pixel_impl(
21
- this: u32,
22
- x_pos: u32,
23
- y_pos: u32,
24
- flags: u32,
25
- ) {
26
  let q = &mut *(this as *mut RenderQueue);
27
 
28
  if let Some(entry) = q.alloc::<DrawPixelCmd>() {
@@ -368,11 +363,7 @@ unsafe fn trig_lookup(table: *const i32, angle: u32) -> i32 {
368
  // Gated by task+0xBC flag set by InitWormTrail (0x5008D0).
369
  // ---------------------------------------------------------------------------
370
 
371
- unsafe extern "stdcall" fn draw_bungee_trail_impl(
372
- task_ptr: u32,
373
- style: u32,
374
- fill: u32,
375
- ) {
376
  let task = &*(task_ptr as *const BungeeTrailTask);
377
 
378
  if task.trail_visible == 0 {
@@ -518,8 +509,7 @@ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) {
518
  let cos_interp = trig_lookup(cos_table, angle);
519
 
520
  // Scale = fixed_mul(DDGame.crosshair_scale, 0x140000) + task.aim_range_offset
521
- let scale = fixed_mul(ddgame.crosshair_scale, 0x14_0000)
522
- + task.aim_range_offset;
523
 
524
  // Endpoint = start + direction * scale
525
  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) {
529
  let mut overflowed = false;
530
  let mut clamp_factor = 0i32;
531
 
532
- let threshold = (*ddgame.game_info).crosshair_overflow_threshold;
533
 
534
  if threshold > 0x11E {
535
  // 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) {
554
  // Enqueue polygon line (2 vertices)
555
  let poly_param_1 = ddgame.crosshair_line_style;
556
  let poly_param_2 = ddgame.crosshair_line_color;
557
- let verts: [[i32; 3]; 2] = [
558
- [start_x, start_y, 0],
559
- [endpoint_x, endpoint_y, 0],
560
- ];
561
  let total_size = 2 * 0xC + 0x20;
562
  if let Some(ptr) = rq.alloc_raw(total_size) {
563
  let header = &mut *(ptr as *mut DrawPolygonHeader);
@@ -596,78 +583,77 @@ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) {
596
 
597
  pub fn install() -> Result<(), String> {
598
  unsafe {
599
- let _ = hook::install(
600
- "DrawPixel",
601
- va::RQ_DRAW_PIXEL,
602
- trampoline_draw_pixel as *const (),
603
- )?;
604
-
605
- let _ = hook::install(
606
- "DrawLineStrip",
607
- va::RQ_DRAW_LINE_STRIP,
608
- trampoline_draw_line_strip as *const (),
609
- )?;
610
-
611
- let _ = hook::install(
612
- "DrawPolygon",
613
- va::RQ_DRAW_POLYGON,
614
- trampoline_draw_polygon as *const (),
615
- )?;
616
-
617
- let _ = hook::install(
618
- "DrawScaled",
619
- va::RQ_DRAW_SCALED,
620
- trampoline_draw_scaled as *const (),
621
- )?;
622
-
623
- let _ = hook::install(
624
- "DrawRect",
625
- va::RQ_DRAW_RECT,
626
- trampoline_draw_rect as *const (),
627
- )?;
628
-
629
- let _ = hook::install(
630
- "DrawSpriteGlobal",
631
- va::RQ_DRAW_SPRITE_GLOBAL,
632
- trampoline_draw_sprite_global as *const (),
633
- )?;
634
-
635
- let _ = hook::install(
636
- "DrawSpriteLocal",
637
- va::RQ_DRAW_SPRITE_LOCAL,
638
- trampoline_draw_sprite_local as *const (),
639
- )?;
640
-
641
- let _ = hook::install(
642
- "DrawSpriteOffset",
643
- va::RQ_DRAW_SPRITE_OFFSET,
644
- trampoline_draw_sprite_offset as *const (),
645
- )?;
646
-
647
- let _ = hook::install(
648
- "DrawBitmapGlobal",
649
- va::RQ_DRAW_BITMAP_GLOBAL,
650
- trampoline_draw_bitmap_global as *const (),
651
- )?;
652
-
653
- let _ = hook::install(
654
- "DrawTextboxLocal",
655
- va::RQ_DRAW_TEXTBOX_LOCAL,
656
- trampoline_draw_textbox_local as *const (),
657
- )?;
658
-
659
- let _ = hook::install(
660
- "DrawBungeeTrail",
661
- va::DRAW_BUNGEE_TRAIL,
662
- draw_bungee_trail_impl as *const (),
663
- )?;
664
-
665
- let _ = hook::install(
666
- "DrawCrosshairLine",
667
- va::DRAW_CROSSHAIR_LINE,
668
- trampoline_draw_crosshair_line as *const (),
669
- )?;
670
-
671
  }
672
  Ok(())
673
  }
 
3
  //! All functions enqueue commands to the RenderQueue's downward-growing buffer.
4
  //! Calling conventions are __usercall variants with register + stack params.
5
 
 
6
  use openwa_core::address::va;
7
+ use openwa_core::rebase::rb;
8
  use openwa_core::render::queue::*;
9
  use openwa_core::task::{BungeeTrailTask, WeaponAimTask};
10
 
 
17
  usercall_trampoline!(fn trampoline_draw_pixel; impl_fn = draw_pixel_impl;
18
  reg = eax; stack_params = 3; ret_bytes = "0xC");
19
 
20
+ unsafe extern "cdecl" fn draw_pixel_impl(this: u32, x_pos: u32, y_pos: u32, flags: u32) {
 
 
 
 
 
21
  let q = &mut *(this as *mut RenderQueue);
22
 
23
  if let Some(entry) = q.alloc::<DrawPixelCmd>() {
 
363
  // Gated by task+0xBC flag set by InitWormTrail (0x5008D0).
364
  // ---------------------------------------------------------------------------
365
 
366
+ unsafe extern "stdcall" fn draw_bungee_trail_impl(task_ptr: u32, style: u32, fill: u32) {
 
 
 
 
367
  let task = &*(task_ptr as *const BungeeTrailTask);
368
 
369
  if task.trail_visible == 0 {
 
509
  let cos_interp = trig_lookup(cos_table, angle);
510
 
511
  // Scale = fixed_mul(DDGame.crosshair_scale, 0x140000) + task.aim_range_offset
512
+ let scale = fixed_mul(ddgame.crosshair_scale, 0x14_0000) + task.aim_range_offset;
 
513
 
514
  // Endpoint = start + direction * scale
515
  let mut endpoint_x = fixed_mul(sin_interp, scale).wrapping_add(start_x);
 
519
  let mut overflowed = false;
520
  let mut clamp_factor = 0i32;
521
 
522
+ let threshold = (*ddgame.game_info).game_version;
523
 
524
  if threshold > 0x11E {
525
  // Check X overflow: sin > 0 but endpoint wrapped below start
 
544
  // Enqueue polygon line (2 vertices)
545
  let poly_param_1 = ddgame.crosshair_line_style;
546
  let poly_param_2 = ddgame.crosshair_line_color;
547
+ let verts: [[i32; 3]; 2] = [[start_x, start_y, 0], [endpoint_x, endpoint_y, 0]];
 
 
 
548
  let total_size = 2 * 0xC + 0x20;
549
  if let Some(ptr) = rq.alloc_raw(total_size) {
550
  let header = &mut *(ptr as *mut DrawPolygonHeader);
 
583
 
584
  pub fn install() -> Result<(), String> {
585
  unsafe {
586
+ let _ = hook::install(
587
+ "DrawPixel",
588
+ va::RQ_DRAW_PIXEL,
589
+ trampoline_draw_pixel as *const (),
590
+ )?;
591
+
592
+ let _ = hook::install(
593
+ "DrawLineStrip",
594
+ va::RQ_DRAW_LINE_STRIP,
595
+ trampoline_draw_line_strip as *const (),
596
+ )?;
597
+
598
+ let _ = hook::install(
599
+ "DrawPolygon",
600
+ va::RQ_DRAW_POLYGON,
601
+ trampoline_draw_polygon as *const (),
602
+ )?;
603
+
604
+ let _ = hook::install(
605
+ "DrawScaled",
606
+ va::RQ_DRAW_SCALED,
607
+ trampoline_draw_scaled as *const (),
608
+ )?;
609
+
610
+ let _ = hook::install(
611
+ "DrawRect",
612
+ va::RQ_DRAW_RECT,
613
+ trampoline_draw_rect as *const (),
614
+ )?;
615
+
616
+ let _ = hook::install(
617
+ "DrawSpriteGlobal",
618
+ va::RQ_DRAW_SPRITE_GLOBAL,
619
+ trampoline_draw_sprite_global as *const (),
620
+ )?;
621
+
622
+ let _ = hook::install(
623
+ "DrawSpriteLocal",
624
+ va::RQ_DRAW_SPRITE_LOCAL,
625
+ trampoline_draw_sprite_local as *const (),
626
+ )?;
627
+
628
+ let _ = hook::install(
629
+ "DrawSpriteOffset",
630
+ va::RQ_DRAW_SPRITE_OFFSET,
631
+ trampoline_draw_sprite_offset as *const (),
632
+ )?;
633
+
634
+ let _ = hook::install(
635
+ "DrawBitmapGlobal",
636
+ va::RQ_DRAW_BITMAP_GLOBAL,
637
+ trampoline_draw_bitmap_global as *const (),
638
+ )?;
639
+
640
+ let _ = hook::install(
641
+ "DrawTextboxLocal",
642
+ va::RQ_DRAW_TEXTBOX_LOCAL,
643
+ trampoline_draw_textbox_local as *const (),
644
+ )?;
645
+
646
+ let _ = hook::install(
647
+ "DrawBungeeTrail",
648
+ va::DRAW_BUNGEE_TRAIL,
649
+ draw_bungee_trail_impl as *const (),
650
+ )?;
651
+
652
+ let _ = hook::install(
653
+ "DrawCrosshairLine",
654
+ va::DRAW_CROSSHAIR_LINE,
655
+ trampoline_draw_crosshair_line as *const (),
656
+ )?;
 
657
  }
658
  Ok(())
659
  }
crates/openwa-wormkit/src/replacements/scheme.rs CHANGED
@@ -15,12 +15,12 @@ use std::path::Path;
15
  use std::sync::atomic::{AtomicU32, Ordering};
16
 
17
  use crate::log_line;
18
- use openwa_core::rebase::rb;
19
  use openwa_core::address::va;
20
  use openwa_core::game::scheme::{
21
  ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, EXTENDED_OPTIONS_SIZE,
22
  SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, SCHEME_PAYLOAD_V3,
23
  };
 
24
 
25
  // ============================================================
26
  // Scheme__ReadFile replacement (0x4D3890)
@@ -63,11 +63,7 @@ unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) {
63
 
64
  match scheme.version {
65
  SchemeVersion::V1 => {
66
- core::ptr::copy_nonoverlapping(
67
- scheme.payload.as_ptr(),
68
- payload_ptr,
69
- SCHEME_PAYLOAD_V1,
70
- );
71
  core::ptr::write_bytes(
72
  payload_ptr.add(PAYLOAD_SUPER_WEAPONS),
73
  0,
@@ -80,11 +76,7 @@ unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) {
80
  );
81
  }
82
  SchemeVersion::V2 => {
83
- core::ptr::copy_nonoverlapping(
84
- scheme.payload.as_ptr(),
85
- payload_ptr,
86
- SCHEME_PAYLOAD_V2,
87
- );
88
  core::ptr::copy_nonoverlapping(
89
  EXTENDED_OPTIONS_DEFAULTS.as_ptr(),
90
  payload_ptr.add(PAYLOAD_EXTENDED),
@@ -92,11 +84,7 @@ unsafe fn write_scheme_to_dest(scheme: &SchemeFile, dest: u32) {
92
  );
93
  }
94
  SchemeVersion::V3 => {
95
- core::ptr::copy_nonoverlapping(
96
- scheme.payload.as_ptr(),
97
- payload_ptr,
98
- SCHEME_PAYLOAD_V3,
99
- );
100
  let ext_bytes = &scheme.payload[PAYLOAD_EXTENDED..];
101
  if !ExtendedOptions::validate_bytes(ext_bytes) {
102
  core::ptr::copy_nonoverlapping(
@@ -284,12 +272,7 @@ static ORIG_SCHEME_SAVE_FILE: AtomicU32 = AtomicU32::new(0);
284
  ///
285
  /// Detects scheme version from in-memory data, writes SCHM header + payload.
286
  /// For V3, writes variable-length payload (only bytes differing from defaults).
287
- unsafe extern "fastcall" fn hook_save_file(
288
- this: u32,
289
- _edx: u32,
290
- name: u32,
291
- flag: u32,
292
- ) -> u32 {
293
  let c_name = match CStr::from_ptr(name as *const i8).to_str() {
294
  Ok(s) => s,
295
  Err(_) => {
@@ -366,11 +349,7 @@ unsafe extern "fastcall" fn hook_init_from_data(
366
 
367
  // Step 3: Copy 110 bytes of V3 defaults from ROM to dest+0x138
368
  let defaults = rb(va::SCHEME_V3_DEFAULTS) as *const u8;
369
- core::ptr::copy_nonoverlapping(
370
- defaults,
371
- (dest + 0x138) as *mut u8,
372
- EXTENDED_OPTIONS_SIZE,
373
- );
374
 
375
  // Step 4: Set flag byte and index
376
  *((dest + 0x04) as *mut u8) = 0;
@@ -501,20 +480,20 @@ unsafe fn scan_directory_recursive(dir: &str) {
501
  /// AfxFindStringResourceHandle, which is not accessible via plain LoadStringA.
502
  /// The names are fixed across all WA 3.8.1 installations.
503
  const BUILTIN_SCHEME_NAMES: [&str; 14] = [
504
- "", // slot 0 unused
505
- "Beginner", // slot 1, resource 0x3CA
506
- "Intermediate", // slot 2, resource 0x3CB
507
- "Pro", // slot 3, resource 0x3CC
508
- "Tournament", // slot 4, resource 0x3D1
509
- "Classic", // slot 5, resource 0x3CD
510
- "Retro", // slot 6, resource 0x3D2
511
- "Artillery", // slot 7, resource 0x3D5
512
- "Sudden Sinking", // slot 8, resource 0x3D0
513
- "Strategic", // slot 9, resource 0x3D3
514
- "The Darkside", // slot 10, resource 0x3D4
515
- "Armageddon", // slot 11, resource 0x3CE
516
- "Blast Zone", // slot 12, resource 0x3CF
517
- "Full Wormage", // slot 13, resource 0x3D6
518
  ];
519
 
520
  /// Rust replacement for Scheme__ExtractBuiltins (0x4D5720).
@@ -581,9 +560,7 @@ static ORIG_LOAD_NUMBERED: AtomicU32 = AtomicU32::new(0);
581
  /// This function has zero xrefs in WA.exe (believed dead code).
582
  /// We hook it to detect if it's actually called at runtime.
583
  unsafe extern "stdcall" fn hook_load_numbered(name: u32) -> u32 {
584
- let c_name = CStr::from_ptr(name as *const i8)
585
- .to_str()
586
- .unwrap_or("???");
587
  let _ = log_line(&format!(
588
  "[Scheme] WARNING: LoadNumbered called (believed dead code): {c_name}"
589
  ));
 
15
  use std::sync::atomic::{AtomicU32, Ordering};
16
 
17
  use crate::log_line;
 
18
  use openwa_core::address::va;
19
  use openwa_core::game::scheme::{
20
  ExtendedOptions, SchemeFile, SchemeVersion, EXTENDED_OPTIONS_DEFAULTS, EXTENDED_OPTIONS_SIZE,
21
  SCHEME_PAYLOAD_V1, SCHEME_PAYLOAD_V2, SCHEME_PAYLOAD_V3,
22
  };
23
+ use openwa_core::rebase::rb;
24
 
25
  // ============================================================
26
  // Scheme__ReadFile replacement (0x4D3890)
 
63
 
64
  match scheme.version {
65
  SchemeVersion::V1 => {
66
+ core::ptr::copy_nonoverlapping(scheme.payload.as_ptr(), payload_ptr, SCHEME_PAYLOAD_V1);
 
 
 
 
67
  core::ptr::write_bytes(
68
  payload_ptr.add(PAYLOAD_SUPER_WEAPONS),
69
  0,
 
76
  );
77
  }
78
  SchemeVersion::V2 => {
79
+ core::ptr::copy_nonoverlapping(scheme.payload.as_ptr(), payload_ptr, SCHEME_PAYLOAD_V2);
 
 
 
 
80
  core::ptr::copy_nonoverlapping(
81
  EXTENDED_OPTIONS_DEFAULTS.as_ptr(),
82
  payload_ptr.add(PAYLOAD_EXTENDED),
 
84
  );
85
  }
86
  SchemeVersion::V3 => {
87
+ core::ptr::copy_nonoverlapping(scheme.payload.as_ptr(), payload_ptr, SCHEME_PAYLOAD_V3);
 
 
 
 
88
  let ext_bytes = &scheme.payload[PAYLOAD_EXTENDED..];
89
  if !ExtendedOptions::validate_bytes(ext_bytes) {
90
  core::ptr::copy_nonoverlapping(
 
272
  ///
273
  /// Detects scheme version from in-memory data, writes SCHM header + payload.
274
  /// For V3, writes variable-length payload (only bytes differing from defaults).
275
+ unsafe extern "fastcall" fn hook_save_file(this: u32, _edx: u32, name: u32, flag: u32) -> u32 {
 
 
 
 
 
276
  let c_name = match CStr::from_ptr(name as *const i8).to_str() {
277
  Ok(s) => s,
278
  Err(_) => {
 
349
 
350
  // Step 3: Copy 110 bytes of V3 defaults from ROM to dest+0x138
351
  let defaults = rb(va::SCHEME_V3_DEFAULTS) as *const u8;
352
+ core::ptr::copy_nonoverlapping(defaults, (dest + 0x138) as *mut u8, EXTENDED_OPTIONS_SIZE);
 
 
 
 
353
 
354
  // Step 4: Set flag byte and index
355
  *((dest + 0x04) as *mut u8) = 0;
 
480
  /// AfxFindStringResourceHandle, which is not accessible via plain LoadStringA.
481
  /// The names are fixed across all WA 3.8.1 installations.
482
  const BUILTIN_SCHEME_NAMES: [&str; 14] = [
483
+ "", // slot 0 unused
484
+ "Beginner", // slot 1, resource 0x3CA
485
+ "Intermediate", // slot 2, resource 0x3CB
486
+ "Pro", // slot 3, resource 0x3CC
487
+ "Tournament", // slot 4, resource 0x3D1
488
+ "Classic", // slot 5, resource 0x3CD
489
+ "Retro", // slot 6, resource 0x3D2
490
+ "Artillery", // slot 7, resource 0x3D5
491
+ "Sudden Sinking", // slot 8, resource 0x3D0
492
+ "Strategic", // slot 9, resource 0x3D3
493
+ "The Darkside", // slot 10, resource 0x3D4
494
+ "Armageddon", // slot 11, resource 0x3CE
495
+ "Blast Zone", // slot 12, resource 0x3CF
496
+ "Full Wormage", // slot 13, resource 0x3D6
497
  ];
498
 
499
  /// Rust replacement for Scheme__ExtractBuiltins (0x4D5720).
 
560
  /// This function has zero xrefs in WA.exe (believed dead code).
561
  /// We hook it to detect if it's actually called at runtime.
562
  unsafe extern "stdcall" fn hook_load_numbered(name: u32) -> u32 {
563
+ let c_name = CStr::from_ptr(name as *const i8).to_str().unwrap_or("???");
 
 
564
  let _ = log_line(&format!(
565
  "[Scheme] WARNING: LoadNumbered called (believed dead code): {c_name}"
566
  ));
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -10,16 +10,15 @@
10
  use std::sync::atomic::Ordering;
11
 
12
  use openwa_core::address::va;
 
13
  use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
14
- use openwa_core::audio::{SoundId, play_sound, play_sound_pooled};
15
  use openwa_core::task::{CGameTask, CTask};
16
 
17
  use crate::hook;
18
  use crate::log_line;
19
 
20
  /// Whether sound logging is enabled (checked once at init).
21
- static SOUND_LOG_ENABLED: std::sync::atomic::AtomicBool =
22
- std::sync::atomic::AtomicBool::new(false);
23
 
24
  // ============================================================
25
  // Core: sound queue insertion
@@ -172,7 +171,14 @@ unsafe extern "fastcall" fn hook_dispatch_global_sound(
172
  return 0;
173
  }
174
 
175
- play_sound(dssound, sound_id, flags as i32, volume as i32, pitch as i32, 0) as u32
 
 
 
 
 
 
 
176
  }
177
 
178
  /// PlaySoundPooled_Direct (0x546B50) — fastcall(ECX=unused, EDX=task) + 3 stack, RET 0xC.
@@ -256,16 +262,14 @@ pub fn install() -> Result<(), String> {
256
 
257
  /// Patch DSSound vtable (0x66AF20) to replace trivial methods with Rust.
258
  unsafe fn patch_dssound_vtable() -> Result<(), String> {
259
- use openwa_core::rebase::rb;
260
- use openwa_core::vtable::patch_vtable;
261
  use openwa_core::audio::{
262
- update_channels, release_finished,
263
- is_slot_loaded, is_channel_finished, stop_channel,
264
- dssound_destructor, play_sound, play_sound_pooled,
265
- set_volume_params, set_master_volume, set_channel_volume, set_pan,
266
- dssound_sub_destructor,
267
- load_wav, dssound_noop, dssound_returns_0, dssound_returns_1,
268
  };
 
 
269
 
270
  let vtable = rb(va::DS_SOUND_VTABLE) as *mut u32;
271
 
@@ -325,5 +329,6 @@ unsafe fn patch_dssound_vtable() -> Result<(), String> {
325
  *vt.add(23) = dssound_returns_1 as *const () as u32;
326
 
327
  let _ = log_line("[Sound] DSSound vtable: patched 24/24 slots with Rust");
328
- }).map_err(|e| e.to_string())
 
329
  }
 
10
  use std::sync::atomic::Ordering;
11
 
12
  use openwa_core::address::va;
13
+ use openwa_core::audio::{play_sound, play_sound_pooled, SoundId};
14
  use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
 
15
  use openwa_core::task::{CGameTask, CTask};
16
 
17
  use crate::hook;
18
  use crate::log_line;
19
 
20
  /// Whether sound logging is enabled (checked once at init).
21
+ static SOUND_LOG_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
 
22
 
23
  // ============================================================
24
  // Core: sound queue insertion
 
171
  return 0;
172
  }
173
 
174
+ play_sound(
175
+ dssound,
176
+ sound_id,
177
+ flags as i32,
178
+ volume as i32,
179
+ pitch as i32,
180
+ 0,
181
+ ) as u32
182
  }
183
 
184
  /// PlaySoundPooled_Direct (0x546B50) — fastcall(ECX=unused, EDX=task) + 3 stack, RET 0xC.
 
262
 
263
  /// Patch DSSound vtable (0x66AF20) to replace trivial methods with Rust.
264
  unsafe fn patch_dssound_vtable() -> Result<(), String> {
 
 
265
  use openwa_core::audio::{
266
+ dssound_destructor, dssound_noop, dssound_returns_0, dssound_returns_1,
267
+ dssound_sub_destructor, is_channel_finished, is_slot_loaded, load_wav, play_sound,
268
+ play_sound_pooled, release_finished, set_channel_volume, set_master_volume, set_pan,
269
+ set_volume_params, stop_channel, update_channels,
 
 
270
  };
271
+ use openwa_core::rebase::rb;
272
+ use openwa_core::vtable::patch_vtable;
273
 
274
  let vtable = rb(va::DS_SOUND_VTABLE) as *mut u32;
275
 
 
329
  *vt.add(23) = dssound_returns_1 as *const () as u32;
330
 
331
  let _ = log_line("[Sound] DSSound vtable: patched 24/24 slots with Rust");
332
+ })
333
+ .map_err(|e| e.to_string())
334
  }
crates/openwa-wormkit/src/replacements/speech.rs CHANGED
@@ -12,10 +12,10 @@ use std::sync::atomic::{AtomicU32, Ordering};
12
 
13
  use heapless::CString;
14
 
15
- use openwa_core::rebase::rb;
16
- use openwa_core::engine::DDGameWrapper;
17
  use openwa_core::address::va::{self, game_info_offsets};
18
  use openwa_core::audio::SpeechLineTableEntry;
 
 
19
 
20
  use crate::hook::{self, usercall_trampoline};
21
  use crate::log_line;
@@ -69,11 +69,11 @@ unsafe extern "cdecl" fn wav_player_stop_raw(_player: u32, _func: u32) {
69
  core::arch::naked_asm!(
70
  "push esi",
71
  "push edi",
72
- "sub esp, 4", // result on stack
73
- "mov esi, [esp + 16]", // player (arg1, past saved esi+edi+result)
74
- "lea edi, [esp]", // &result
75
- "call [esp + 20]", // func (arg2)
76
- "add esp, 4", // drop result
77
  "pop edi",
78
  "pop esi",
79
  "ret",
@@ -88,8 +88,8 @@ unsafe extern "cdecl" fn wav_player_stop_raw(_player: u32, _func: u32) {
88
  unsafe extern "cdecl" fn wav_player_load_raw(_player: u32, _path: *const u8, _func: u32) {
89
  core::arch::naked_asm!(
90
  "push esi",
91
- "sub esp, 4", // result on stack
92
- "lea esi, [esp]", // ESI = &result
93
  // Push in reverse order: 0, path, player
94
  "push 0", // third param=0 (ESP: +16=player, +20=path, +24=func)
95
  "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
109
  unsafe extern "cdecl" fn wav_player_play_raw(_player: u32, _volume: u32, _func: u32) {
110
  core::arch::naked_asm!(
111
  "push edi",
112
- "sub esp, 4", // result on stack
113
- "lea edi, [esp]", // EDI = &result
114
  // Push in reverse order: volume, player
115
  "push dword ptr [esp+16]", // second param=volume (ESP: +16=player, +24=func)
116
  "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:
121
  );
122
  }
123
 
124
-
125
  // ============================================================
126
  // WA function wrappers
127
  // ============================================================
@@ -257,8 +256,8 @@ unsafe fn call_load_speech_wav(
257
  full_path: *const c_char,
258
  ) -> u32 {
259
  use openwa_core::audio::SpeechSlotTable;
260
- use openwa_core::engine::DDGameWrapper;
261
  use openwa_core::engine::ddgame_wrapper::SPEECH_NAME_ENTRY_SIZE;
 
262
 
263
  let wrapper = &mut *(ddgw as *mut DDGameWrapper);
264
  let count = wrapper.speech_name_count as usize;
@@ -281,7 +280,11 @@ unsafe fn call_load_speech_wav(
281
 
282
  if let Some(idx) = found_idx {
283
  // Found existing — reuse the buffer slot.
284
- slot_table.set(team_index as usize, line_id, idx as u32 + SpeechSlotTable::BUFFER_OFFSET);
 
 
 
 
285
  return 1;
286
  }
287
 
@@ -388,8 +391,7 @@ unsafe extern "cdecl" fn load_speech_bank_impl(
388
  let next_entry = &*table_base.add(i + 1);
389
  if next_entry.name_ptr.is_null() || next_entry.id != entry.id {
390
  let game_info = (*(*ddgw).ddgame).game_info as *const u8;
391
- let default_dir =
392
- game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize);
393
  let default_base =
394
  game_info.add(game_info_offsets::DEFAULT_SPEECH_BASE_PATH as usize);
395
 
@@ -455,10 +457,8 @@ unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: *const DDGameWrapper)
455
  let gi = ddgame.game_info as *const u8;
456
  for i in 0..team_count {
457
  let team_offset = (i * game_info_offsets::SPEECH_TEAM_STRIDE) as usize;
458
- let base_path =
459
- gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset);
460
- let dir =
461
- gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset);
462
 
463
  // Call our Rust impl directly (bypasses the hook trampoline)
464
  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)
471
 
472
  pub fn install() -> Result<(), String> {
473
  unsafe {
474
- hook::install(
475
- "PlayFeSfx",
476
- va::PLAY_FE_SFX,
477
- hook_play_fe_sfx as *const (),
478
- )?;
479
 
480
  hook::install(
481
  "PlayFanfare_Default",
 
12
 
13
  use heapless::CString;
14
 
 
 
15
  use openwa_core::address::va::{self, game_info_offsets};
16
  use openwa_core::audio::SpeechLineTableEntry;
17
+ use openwa_core::engine::DDGameWrapper;
18
+ use openwa_core::rebase::rb;
19
 
20
  use crate::hook::{self, usercall_trampoline};
21
  use crate::log_line;
 
69
  core::arch::naked_asm!(
70
  "push esi",
71
  "push edi",
72
+ "sub esp, 4", // result on stack
73
+ "mov esi, [esp + 16]", // player (arg1, past saved esi+edi+result)
74
+ "lea edi, [esp]", // &result
75
+ "call [esp + 20]", // func (arg2)
76
+ "add esp, 4", // drop result
77
  "pop edi",
78
  "pop esi",
79
  "ret",
 
88
  unsafe extern "cdecl" fn wav_player_load_raw(_player: u32, _path: *const u8, _func: u32) {
89
  core::arch::naked_asm!(
90
  "push esi",
91
+ "sub esp, 4", // result on stack
92
+ "lea esi, [esp]", // ESI = &result
93
  // Push in reverse order: 0, path, player
94
  "push 0", // third param=0 (ESP: +16=player, +20=path, +24=func)
95
  "push dword ptr [esp+20]", // second param=path (ESP: +20=player, +24=path, +28=func)
 
109
  unsafe extern "cdecl" fn wav_player_play_raw(_player: u32, _volume: u32, _func: u32) {
110
  core::arch::naked_asm!(
111
  "push edi",
112
+ "sub esp, 4", // result on stack
113
+ "lea edi, [esp]", // EDI = &result
114
  // Push in reverse order: volume, player
115
  "push dword ptr [esp+16]", // second param=volume (ESP: +16=player, +24=func)
116
  "push dword ptr [esp+16]", // first param=player (ESP: +28=func)
 
121
  );
122
  }
123
 
 
124
  // ============================================================
125
  // WA function wrappers
126
  // ============================================================
 
256
  full_path: *const c_char,
257
  ) -> u32 {
258
  use openwa_core::audio::SpeechSlotTable;
 
259
  use openwa_core::engine::ddgame_wrapper::SPEECH_NAME_ENTRY_SIZE;
260
+ use openwa_core::engine::DDGameWrapper;
261
 
262
  let wrapper = &mut *(ddgw as *mut DDGameWrapper);
263
  let count = wrapper.speech_name_count as usize;
 
280
 
281
  if let Some(idx) = found_idx {
282
  // Found existing — reuse the buffer slot.
283
+ slot_table.set(
284
+ team_index as usize,
285
+ line_id,
286
+ idx as u32 + SpeechSlotTable::BUFFER_OFFSET,
287
+ );
288
  return 1;
289
  }
290
 
 
391
  let next_entry = &*table_base.add(i + 1);
392
  if next_entry.name_ptr.is_null() || next_entry.id != entry.id {
393
  let game_info = (*(*ddgw).ddgame).game_info as *const u8;
394
+ let default_dir = game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize);
 
395
  let default_base =
396
  game_info.add(game_info_offsets::DEFAULT_SPEECH_BASE_PATH as usize);
397
 
 
457
  let gi = ddgame.game_info as *const u8;
458
  for i in 0..team_count {
459
  let team_offset = (i * game_info_offsets::SPEECH_TEAM_STRIDE) as usize;
460
+ let base_path = gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset);
461
+ let dir = gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset);
 
 
462
 
463
  // Call our Rust impl directly (bypasses the hook trampoline)
464
  load_speech_bank_impl(ddgw, i, base_path, dir);
 
471
 
472
  pub fn install() -> Result<(), String> {
473
  unsafe {
474
+ hook::install("PlayFeSfx", va::PLAY_FE_SFX, hook_play_fe_sfx as *const ())?;
 
 
 
 
475
 
476
  hook::install(
477
  "PlayFanfare_Default",