paavohtl Claude Opus 4.6 (1M context) commited on
Commit
4902606
·
1 Parent(s): 314b694

refactor: typed vtables, port PaletteContext__Init and GameStateStream__Init

Browse files

Replace raw vtable dispatch with typed vtable calls in game_state_init:
- Add bind_DSSoundVtable! macro, enabling _raw method wrappers
- Create PCLandscapeVtable (slots 0,1,2,6,8,18) with bind macro
- Replace 7 manual vtable dispatches (DisplayGfx, DSSound, PCLandscape)

Port two WA functions to Rust:
- PaletteContext__Init (0x5411A0): free stack initializer
- GameStateStream__Init (0x4FB490): sub-buffer array constructor
- Remove call_usercall_eax naked asm bridge (no longer needed)

Rename DSSound::set_volume_params → set_frequency_scale (it adjusts
pitch, not volume). Rename status_1/status_2 → freq_scale_divisor/
freq_scale_multiplier. Update Ghidra accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/audio/dssound.rs CHANGED
@@ -89,8 +89,8 @@ pub struct DSSoundVtable {
89
  pub destructor: fn(this: *mut DSSound, flags: u8) -> *mut DSSound,
90
  /// update_channels — iterates 8 descs, releases finished buffers
91
  pub update_channels: fn(this: *mut DSSound),
92
- /// set_volume_params — sets status_1/2, adjusts channel volumes
93
- pub set_volume_params: fn(this: *mut DSSound, status: u32, value: i32),
94
  /// play_sound — wrapper around core play. RET 0x14 = 5 stack params.
95
  pub play_sound: fn(
96
  this: *mut DSSound,
@@ -184,10 +184,10 @@ pub struct DSSound {
184
  pub buffer_pool_used_count: u32,
185
  /// 0xBB0: Total bytes loaded (incremented by load_wav)
186
  pub total_bytes_loaded: u32,
187
- /// 0xBB4: Status flag 1 (init 1, used by set_volume_params)
188
- pub status_1: u32,
189
- /// 0xBB8: Status flag 2 (init 1, used by set_volume_params)
190
- pub status_2: u32,
191
  /// 0xBBC: Init success flag — set to 1 when DirectSoundCreate +
192
  /// init_buffers + IDirectSoundBuffer::Play all succeed.
193
  pub init_success: u32,
@@ -197,6 +197,8 @@ pub struct DSSound {
197
 
198
  const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
199
 
 
 
200
  // ── Trivial vtable method implementations ─────────────────────────────────
201
 
202
  // ── Rust vtable method implementations ────────────────────────────────────
@@ -310,18 +312,22 @@ pub unsafe extern "thiscall" fn set_master_volume(this: *mut DSSound, new_volume
310
  1
311
  }
312
 
313
- /// Slot 2: set_volume_params — sets frequency scaling params and adjusts active channels.
314
- /// `param1` is the divisor (status_1), `param2` is the multiplier (status_2).
315
- /// For each playing channel: new_freq = channel_freq * param2 / param1, clamped to 200000.
316
- pub unsafe extern "thiscall" fn set_volume_params(this: *mut DSSound, param1: u32, param2: i32) {
 
 
 
 
317
  let snd = &mut *this;
318
 
319
  // No change → early return.
320
- if snd.status_1 == param1 && snd.status_2 == param2 as u32 {
321
  return;
322
  }
323
- snd.status_1 = param1;
324
- snd.status_2 = param2 as u32;
325
 
326
  for desc in &snd.channel_descs {
327
  let Some(buf) = desc.buffer() else { continue };
@@ -334,11 +340,11 @@ pub unsafe extern "thiscall" fn set_volume_params(this: *mut DSSound, param1: u3
334
  continue;
335
  }
336
 
337
- // Compute adjusted frequency: channel_freq * param2 / param1.
338
- if param1 == 0 {
339
  continue;
340
  }
341
- let freq = (desc.channel_freq.0 as i64 * param2 as i64 / param1 as i64) as i32;
342
  let freq = freq.min(200_000);
343
 
344
  let _ = buf.SetFrequency(freq as u32);
@@ -542,9 +548,10 @@ unsafe fn core_play_sound(
542
  desc.ds_buffer = core::mem::transmute_copy(&new_buf);
543
  core::mem::forget(new_buf);
544
 
545
- // Compute actual playback frequency: channel_freq * status_2 / status_1.
546
- let freq = if snd.status_1 != 0 {
547
- let f = (channel_freq as i64 * snd.status_2 as i64 / snd.status_1 as i64) as i32;
 
548
  f.min(200_000)
549
  } else {
550
  channel_freq
@@ -965,9 +972,9 @@ impl DSSound {
965
  }
966
  snd.buffer_pool_free_count = 64;
967
 
968
- // Status flags
969
- snd.status_1 = 1;
970
- snd.status_2 = 1;
971
  // init_success stays 0 until COM init succeeds
972
 
973
  snd
 
89
  pub destructor: fn(this: *mut DSSound, flags: u8) -> *mut DSSound,
90
  /// update_channels — iterates 8 descs, releases finished buffers
91
  pub update_channels: fn(this: *mut DSSound),
92
+ /// set_frequency_scale — sets pitch scaling factors, adjusts active channel frequencies
93
+ pub set_frequency_scale: fn(this: *mut DSSound, divisor: u32, multiplier: i32),
94
  /// play_sound — wrapper around core play. RET 0x14 = 5 stack params.
95
  pub play_sound: fn(
96
  this: *mut DSSound,
 
184
  pub buffer_pool_used_count: u32,
185
  /// 0xBB0: Total bytes loaded (incremented by load_wav)
186
  pub total_bytes_loaded: u32,
187
+ /// 0xBB4: Frequency scale divisor (init 1, used by set_frequency_scale)
188
+ pub freq_scale_divisor: u32,
189
+ /// 0xBB8: Frequency scale multiplier (init 1, used by set_frequency_scale)
190
+ pub freq_scale_multiplier: u32,
191
  /// 0xBBC: Init success flag — set to 1 when DirectSoundCreate +
192
  /// init_buffers + IDirectSoundBuffer::Play all succeed.
193
  pub init_success: u32,
 
197
 
198
  const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
199
 
200
+ bind_DSSoundVtable!(DSSound, vtable);
201
+
202
  // ── Trivial vtable method implementations ─────────────────────────────────
203
 
204
  // ── Rust vtable method implementations ────────────────────────────────────
 
312
  1
313
  }
314
 
315
+ /// Slot 2: set_frequency_scale — sets pitch scaling factors and adjusts active channel frequencies.
316
+ /// `divisor` is the frequency divisor, `multiplier` is the frequency multiplier.
317
+ /// For each playing channel: new_freq = channel_freq * multiplier / divisor, clamped to 200000.
318
+ pub unsafe extern "thiscall" fn set_frequency_scale(
319
+ this: *mut DSSound,
320
+ divisor: u32,
321
+ multiplier: i32,
322
+ ) {
323
  let snd = &mut *this;
324
 
325
  // No change → early return.
326
+ if snd.freq_scale_divisor == divisor && snd.freq_scale_multiplier == multiplier as u32 {
327
  return;
328
  }
329
+ snd.freq_scale_divisor = divisor;
330
+ snd.freq_scale_multiplier = multiplier as u32;
331
 
332
  for desc in &snd.channel_descs {
333
  let Some(buf) = desc.buffer() else { continue };
 
340
  continue;
341
  }
342
 
343
+ // Compute adjusted frequency: channel_freq * multiplier / divisor.
344
+ if divisor == 0 {
345
  continue;
346
  }
347
+ let freq = (desc.channel_freq.0 as i64 * multiplier as i64 / divisor as i64) as i32;
348
  let freq = freq.min(200_000);
349
 
350
  let _ = buf.SetFrequency(freq as u32);
 
548
  desc.ds_buffer = core::mem::transmute_copy(&new_buf);
549
  core::mem::forget(new_buf);
550
 
551
+ // Compute actual playback frequency: channel_freq * multiplier / divisor.
552
+ let freq = if snd.freq_scale_divisor != 0 {
553
+ let f = (channel_freq as i64 * snd.freq_scale_multiplier as i64
554
+ / snd.freq_scale_divisor as i64) as i32;
555
  f.min(200_000)
556
  } else {
557
  channel_freq
 
972
  }
973
  snd.buffer_pool_free_count = 64;
974
 
975
+ // Frequency scaling (1:1 = no scaling)
976
+ snd.freq_scale_divisor = 1;
977
+ snd.freq_scale_multiplier = 1;
978
  // init_success stays 0 until COM init succeeds
979
 
980
  snd
crates/openwa-core/src/audio/mod.rs CHANGED
@@ -14,7 +14,7 @@ pub use dssound::{
14
  destructor as dssound_destructor, is_channel_finished, is_slot_loaded, load_wav,
15
  noop as dssound_noop, play_sound, play_sound_pooled, release_finished,
16
  returns_0 as dssound_returns_0, returns_1 as dssound_returns_1, set_channel_volume,
17
- set_master_volume, set_pan, set_volume_params, stop_channel,
18
  sub_destructor as dssound_sub_destructor, update_channels,
19
  };
20
  pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
 
14
  destructor as dssound_destructor, is_channel_finished, is_slot_loaded, load_wav,
15
  noop as dssound_noop, play_sound, play_sound_pooled, release_finished,
16
  returns_0 as dssound_returns_0, returns_1 as dssound_returns_1, set_channel_volume,
17
+ set_frequency_scale, set_master_volume, set_pan, stop_channel,
18
  sub_destructor as dssound_sub_destructor, update_channels,
19
  };
20
  pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
crates/openwa-core/src/engine/game_state_init.rs CHANGED
@@ -11,6 +11,7 @@ use crate::engine::game_info::GameInfo;
11
  use crate::fixed::Fixed;
12
  use crate::render::display::gfx::DisplayGfx;
13
  use crate::render::landscape::PCLandscape;
 
14
 
15
  /// Pure Rust implementation of SpriteGfxTable__Init (0x541620).
16
  ///
@@ -492,15 +493,12 @@ pub unsafe fn init_turn_state(wrapper: *mut DDGameWrapper) {
492
  (*ddgame3)._field_8160 = 0;
493
  (*ddgame3)._field_8164 = 0;
494
 
495
- // Vtable dispatch: DDGame.landscape → vtable slot 1,
496
- // param = game_info.donkey_disabled (byte at +0xD94C)
497
  let ddgame4 = ddgame_from_wrapper(wrapper);
498
- let landscape = (*ddgame4).landscape as *mut u8;
499
  if !landscape.is_null() {
500
- let vtable = *(landscape as *const *const usize);
501
- let slot1: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*vtable.add(1));
502
  let param = (*(*ddgame4).game_info).donkey_disabled as u32;
503
- slot1(landscape, param);
504
  }
505
  }
506
 
@@ -522,22 +520,15 @@ pub unsafe fn init_landscape_flags(wrapper: *mut DDGameWrapper) {
522
  let field_734c = (*ddgame)._field_734c;
523
  let field_7340 = (*ddgame)._field_7340;
524
 
525
- // DDGame.landscape object
526
- let landscape = (*ddgame).landscape as *mut u8;
527
- let vtable = *(landscape as *const *const usize);
528
- // Vtable slot 6 = offset +0x18
529
- let slot6: unsafe extern "thiscall" fn(*mut u8, u32, u32, u32, u32, u32, u32, u32, u32) =
530
- core::mem::transmute(*vtable.add(6));
531
 
532
  if scheme_flag != 0 {
533
- // Call with (1, 1, 1, 1, field_7318, field_730c, field_734c, field_7340)
534
- slot6(
535
  landscape, 1, 1, 1, 1, field_7318, field_730c, field_734c, field_7340,
536
  );
537
  (*ddgame).level_width_raw = 1;
538
  } else if (*ddgame).level_width_raw != 0 {
539
- // Call with (0, 0, 1, 0, field_7318, field_730c, field_734c, field_7340)
540
- slot6(
541
  landscape, 0, 0, 1, 0, field_7318, field_730c, field_734c, field_7340,
542
  );
543
  }
@@ -622,9 +613,7 @@ pub unsafe fn init_game_state(wrapper: *mut DDGameWrapper) {
622
  let mem = wa_malloc_zeroed(0x264) as *mut u32;
623
  if !mem.is_null() {
624
  *mem = rb(0x664194); // GameStateStream vtable
625
- let init: unsafe extern "stdcall" fn(*mut u32) =
626
- core::mem::transmute(rb(va::GAME_STATE_STREAM_INIT) as usize);
627
- init(mem.add(1)); // Init sub-object at offset +4
628
  *mem.add(1) = 0;
629
  *mem.add(0x90) = ddgame as u32; // DDGame ptr backref
630
  *mem.add(0x8B) = 0;
@@ -642,18 +631,12 @@ pub unsafe fn init_game_state(wrapper: *mut DDGameWrapper) {
642
  (*ddgame)._unknown_52c = mem;
643
  }
644
 
645
- // ===== Virtual call: display->vtable[1](&level_width_sound, &screen_height_pixels) =====
646
- {
647
- let display = (*ddgame).display;
648
- let vtable = *(display as *const *const u32);
649
- let vmethod: unsafe extern "thiscall" fn(*mut DisplayGfx, *mut i32, *mut i32) =
650
- core::mem::transmute(*vtable.add(1));
651
- vmethod(
652
- display,
653
- core::ptr::addr_of_mut!((*ddgame).level_width_sound),
654
- core::ptr::addr_of_mut!((*ddgame).screen_height_pixels),
655
- );
656
- }
657
  (*ddgame)._field_77b0 = 0;
658
 
659
  // ===== Allocate BufferObject (main_buffer at wrapper+0x0C) =====
@@ -936,10 +919,7 @@ pub unsafe fn init_game_state(wrapper: *mut DDGameWrapper) {
936
  // ===== Network sync callback =====
937
  let sound = (*ddgame).sound;
938
  if !sound.is_null() {
939
- let vtable = *(sound as *const *const u32);
940
- let vmethod: unsafe extern "thiscall" fn(*mut DSSound) =
941
- core::mem::transmute(*vtable.add(1));
942
- vmethod(sound);
943
  }
944
 
945
  // ===== CTaskTurnGame constructor =====
@@ -1024,10 +1004,7 @@ pub unsafe fn init_game_state(wrapper: *mut DDGameWrapper) {
1024
 
1025
  // Get frame counter via landscape vtable
1026
  let landscape = (*ddgame).landscape;
1027
- let landscape_vtable = *(landscape as *const *const u32);
1028
- let get_frame: unsafe extern "thiscall" fn(*mut PCLandscape) -> u32 =
1029
- core::mem::transmute(*landscape_vtable.add(0x12));
1030
- let frame = get_frame(landscape);
1031
 
1032
  (*wrapper).initial_checksum = frame.wrapping_add(hash);
1033
  }
@@ -1111,33 +1088,17 @@ unsafe fn allocate_ring_buffer_init() -> *mut u8 {
1111
  /// Allocate and initialize a PaletteContext (0x72C bytes).
1112
  /// Sets dirty_range_min=1, dirty_range_max=0xFF, then calls PaletteContext__Init.
1113
  unsafe fn allocate_palette_context() -> *mut crate::render::palette::PaletteContext {
1114
- use crate::address::va;
1115
- use crate::rebase::rb;
1116
- use crate::wa_alloc::wa_malloc_zeroed;
1117
 
1118
- let mem = wa_malloc_zeroed(0x72C) as *mut u16;
1119
- if mem.is_null() {
1120
  return core::ptr::null_mut();
1121
  }
1122
- *mem = 1; // dirty_range_min
1123
- *mem.add(1) = 0xFF; // dirty_range_max
1124
- // Call PaletteContext__Init — usercall(EAX=ctx)
1125
- call_usercall_eax(mem as *mut u8, rb(va::PALETTE_CONTEXT_INIT));
1126
- *mem.add(900) = 0; // offset 0x708 (word)
1127
- mem as *mut crate::render::palette::PaletteContext
1128
- }
1129
-
1130
- /// Bridge: usercall(EAX=ptr), plain RET.
1131
- #[cfg(target_arch = "x86")]
1132
- #[unsafe(naked)]
1133
- unsafe extern "C" fn call_usercall_eax(_ptr: *mut u8, _addr: u32) {
1134
- core::arch::naked_asm!(
1135
- "movl 4(%esp), %eax", // EAX = ptr
1136
- "movl 8(%esp), %ecx", // ECX = target address (temp)
1137
- "calll *%ecx",
1138
- "retl",
1139
- options(att_syntax),
1140
- );
1141
  }
1142
 
1143
  /// Bridge: stdcall constructor with ECX = implicit register param.
@@ -1200,16 +1161,9 @@ unsafe fn init_game_state_display(
1200
 
1201
  let max_team_render = (*wrapper).max_team_render_index;
1202
 
1203
- // Display virtual call: display->vtable[1](&display_width, &display_height)
1204
  let mut display_width: u32 = 0;
1205
  let mut display_height: u32 = 0;
1206
- {
1207
- let display = (*ddgame).display;
1208
- let vtable = *(display as *const *const u32);
1209
- let vmethod: unsafe extern "thiscall" fn(*mut DisplayGfx, *mut u32, *mut u32) =
1210
- core::mem::transmute(*vtable.add(1));
1211
- vmethod(display, &mut display_width, &mut display_height);
1212
- }
1213
 
1214
  // Screen height for HUD: 0x12C (300) if wide layout, 0x8C (140) if narrow
1215
  let screen_height: i32 = if max_team_render != 0xC {
@@ -1780,14 +1734,10 @@ unsafe fn init_game_state_tracking_arrays(ddgame: *mut DDGame, game_info: *const
1780
  // Network speed callback
1781
  let sound = (*ddgame).sound;
1782
  if !sound.is_null() {
1783
- let snd_ptr = sound as *const u8;
1784
- let vtable = *(snd_ptr as *const *const u32);
1785
- let vmethod: unsafe extern "thiscall" fn(*const u8, u32, u32) =
1786
- core::mem::transmute(*vtable.add(2));
1787
- vmethod(
1788
- snd_ptr,
1789
  (*ddgame).game_speed.0 as u32,
1790
- (*ddgame).game_speed_target.0 as u32,
1791
  );
1792
  }
1793
 
@@ -1848,3 +1798,37 @@ unsafe fn init_game_state_tracking_arrays(ddgame: *mut DDGame, game_info: *const
1848
  }
1849
  }
1850
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  use crate::fixed::Fixed;
12
  use crate::render::display::gfx::DisplayGfx;
13
  use crate::render::landscape::PCLandscape;
14
+ use crate::wa_alloc::{wa_malloc_struct_zeroed, wa_malloc_zeroed};
15
 
16
  /// Pure Rust implementation of SpriteGfxTable__Init (0x541620).
17
  ///
 
493
  (*ddgame3)._field_8160 = 0;
494
  (*ddgame3)._field_8164 = 0;
495
 
496
+ // Landscape vtable slot 1: set control flag (donkey_disabled)
 
497
  let ddgame4 = ddgame_from_wrapper(wrapper);
498
+ let landscape = (*ddgame4).landscape;
499
  if !landscape.is_null() {
 
 
500
  let param = (*(*ddgame4).game_info).donkey_disabled as u32;
501
+ PCLandscape::set_control_flag_raw(landscape, param);
502
  }
503
  }
504
 
 
520
  let field_734c = (*ddgame)._field_734c;
521
  let field_7340 = (*ddgame)._field_7340;
522
 
523
+ let landscape = (*ddgame).landscape;
 
 
 
 
 
524
 
525
  if scheme_flag != 0 {
526
+ PCLandscape::init_borders_raw(
 
527
  landscape, 1, 1, 1, 1, field_7318, field_730c, field_734c, field_7340,
528
  );
529
  (*ddgame).level_width_raw = 1;
530
  } else if (*ddgame).level_width_raw != 0 {
531
+ PCLandscape::init_borders_raw(
 
532
  landscape, 0, 0, 1, 0, field_7318, field_730c, field_734c, field_7340,
533
  );
534
  }
 
613
  let mem = wa_malloc_zeroed(0x264) as *mut u32;
614
  if !mem.is_null() {
615
  *mem = rb(0x664194); // GameStateStream vtable
616
+ game_state_stream_init(mem.add(1));
 
 
617
  *mem.add(1) = 0;
618
  *mem.add(0x90) = ddgame as u32; // DDGame ptr backref
619
  *mem.add(0x8B) = 0;
 
631
  (*ddgame)._unknown_52c = mem;
632
  }
633
 
634
+ // Get display dimensions
635
+ DisplayGfx::get_dimensions_raw(
636
+ (*ddgame).display,
637
+ core::ptr::addr_of_mut!((*ddgame).level_width_sound) as *mut u32,
638
+ core::ptr::addr_of_mut!((*ddgame).screen_height_pixels) as *mut u32,
639
+ );
 
 
 
 
 
 
640
  (*ddgame)._field_77b0 = 0;
641
 
642
  // ===== Allocate BufferObject (main_buffer at wrapper+0x0C) =====
 
919
  // ===== Network sync callback =====
920
  let sound = (*ddgame).sound;
921
  if !sound.is_null() {
922
+ DSSound::update_channels_raw(sound);
 
 
 
923
  }
924
 
925
  // ===== CTaskTurnGame constructor =====
 
1004
 
1005
  // Get frame counter via landscape vtable
1006
  let landscape = (*ddgame).landscape;
1007
+ let frame = PCLandscape::get_frame_checksum_raw(landscape);
 
 
 
1008
 
1009
  (*wrapper).initial_checksum = frame.wrapping_add(hash);
1010
  }
 
1088
  /// Allocate and initialize a PaletteContext (0x72C bytes).
1089
  /// Sets dirty_range_min=1, dirty_range_max=0xFF, then calls PaletteContext__Init.
1090
  unsafe fn allocate_palette_context() -> *mut crate::render::palette::PaletteContext {
1091
+ use crate::render::palette::{palette_context_init, PaletteContext};
 
 
1092
 
1093
+ let ctx = wa_malloc_struct_zeroed::<PaletteContext>();
1094
+ if ctx.is_null() {
1095
  return core::ptr::null_mut();
1096
  }
1097
+ (*ctx).dirty_range_min = 1;
1098
+ (*ctx).dirty_range_max = 0xFF;
1099
+ palette_context_init(ctx);
1100
+ (*ctx).dirty = 0;
1101
+ ctx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1102
  }
1103
 
1104
  /// Bridge: stdcall constructor with ECX = implicit register param.
 
1161
 
1162
  let max_team_render = (*wrapper).max_team_render_index;
1163
 
 
1164
  let mut display_width: u32 = 0;
1165
  let mut display_height: u32 = 0;
1166
+ DisplayGfx::get_dimensions_raw((*ddgame).display, &mut display_width, &mut display_height);
 
 
 
 
 
 
1167
 
1168
  // Screen height for HUD: 0x12C (300) if wide layout, 0x8C (140) if narrow
1169
  let screen_height: i32 = if max_team_render != 0xC {
 
1734
  // Network speed callback
1735
  let sound = (*ddgame).sound;
1736
  if !sound.is_null() {
1737
+ DSSound::set_frequency_scale_raw(
1738
+ sound,
 
 
 
 
1739
  (*ddgame).game_speed.0 as u32,
1740
+ (*ddgame).game_speed_target.0,
1741
  );
1742
  }
1743
 
 
1798
  }
1799
  }
1800
  }
1801
+
1802
+ /// Pure Rust port of GameStateStream__Init (0x4FB490).
1803
+ ///
1804
+ /// Convention: stdcall(sub_object_ptr), plain RET.
1805
+ ///
1806
+ /// Initializes the sub-object within GameStateStream:
1807
+ /// - +0x14: capacity (0x100)
1808
+ /// - +0x18/+0x1C: zeroed
1809
+ /// - +0x20: main buffer (0x420 bytes, first 0x400 zeroed)
1810
+ /// - +0x24..+0x224: 32 sub-buffer elements (each 0x10 bytes)
1811
+ ///
1812
+ /// Each sub-buffer element (FUN_004fdc20):
1813
+ /// - [0]: capacity (0x100)
1814
+ /// - [1]/[2]: zeroed
1815
+ /// - [3]: buffer (0x420 bytes, first 0x400 zeroed)
1816
+ unsafe fn game_state_stream_init(sub_obj: *mut u32) {
1817
+ *sub_obj.add(5) = 0x100; // +0x14: capacity
1818
+ *sub_obj.add(6) = 0; // +0x18
1819
+ *sub_obj.add(7) = 0; // +0x1C
1820
+
1821
+ // +0x20: main buffer
1822
+ let buf = wa_malloc_zeroed(0x420);
1823
+ *sub_obj.add(8) = buf as u32;
1824
+
1825
+ // +0x24: 32 sub-buffer elements, each 0x10 bytes (4 u32s)
1826
+ for i in 0..32usize {
1827
+ let elem = sub_obj.add(9 + i * 4); // +0x24 + i*0x10
1828
+ *elem = 0x100; // capacity
1829
+ *elem.add(1) = 0;
1830
+ *elem.add(2) = 0;
1831
+ let elem_buf = wa_malloc_zeroed(0x420);
1832
+ *elem.add(3) = elem_buf as u32;
1833
+ }
1834
+ }
crates/openwa-core/src/render/landscape.rs CHANGED
@@ -24,7 +24,7 @@ use crate::{engine::ddgame::DDGame, render::sprite::gfx_dir::GfxDir};
24
  #[repr(C)]
25
  pub struct PCLandscape {
26
  /// 0x000: Vtable pointer (0x66B208)
27
- pub vtable: *mut u8,
28
  /// 0x004: Parent DDGame pointer (param_1[1])
29
  pub ddgame: *mut DDGame,
30
  /// 0x008-0x0CB: Pre-rendered crater sprites for 15 explosion sizes.
@@ -89,8 +89,8 @@ pub struct PCLandscape {
89
  pub level_gfx_dir: *mut GfxDir,
90
  /// 0xB38: GfxHandler for Water.dir (param_1[0x2CE])
91
  pub water_gfx_dir: *mut GfxDir,
92
- /// 0xB3C: Remaining control flag
93
- pub _unknown_b3c: u32,
94
  }
95
 
96
  const _: () = assert!(core::mem::size_of::<PCLandscape>() == 0xB40);
@@ -108,14 +108,37 @@ pub struct DirtyRect {
108
 
109
  const _: () = assert!(core::mem::size_of::<DirtyRect>() == 8);
110
 
111
- /// PCLandscape vtable slot indices (32 entries at 0x66B208).
112
- pub mod vtable_slots {
113
- /// Slot 0: Destructor (0x57B540)
114
- pub const DESTRUCTOR: usize = 0;
115
- /// Slot 2: Apply explosion crater (0x57C820) — terrain destruction
116
- pub const APPLY_EXPLOSION: usize = 2;
117
- /// Slot 6: Draw borders — 8px checkered borders at landscape edges (0x57D7F0)
118
- pub const DRAW_BORDERS: usize = 6;
119
- /// Slot 8: Redraw single row (0x57CF60)
120
- pub const REDRAW_ROW: usize = 8;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
 
 
 
24
  #[repr(C)]
25
  pub struct PCLandscape {
26
  /// 0x000: Vtable pointer (0x66B208)
27
+ pub vtable: *const PCLandscapeVtable,
28
  /// 0x004: Parent DDGame pointer (param_1[1])
29
  pub ddgame: *mut DDGame,
30
  /// 0x008-0x0CB: Pre-rendered crater sprites for 15 explosion sizes.
 
89
  pub level_gfx_dir: *mut GfxDir,
90
  /// 0xB38: GfxHandler for Water.dir (param_1[0x2CE])
91
  pub water_gfx_dir: *mut GfxDir,
92
+ /// 0xB3C: Control flag set by vtable slot 1 (donkey mode / landscape control)
93
+ pub control_flag: u32,
94
  }
95
 
96
  const _: () = assert!(core::mem::size_of::<PCLandscape>() == 0xB40);
 
108
 
109
  const _: () = assert!(core::mem::size_of::<DirtyRect>() == 8);
110
 
111
+ /// PCLandscape vtable (32 slots at 0x66B208).
112
+ #[openwa_core::vtable(size = 32, va = 0x0066_B208, class = "PCLandscape")]
113
+ pub struct PCLandscapeVtable {
114
+ /// destructor (0x57B540, RET 0x4)
115
+ #[slot(0)]
116
+ pub destructor: fn(this: *mut PCLandscape, flags: u32) -> *mut PCLandscape,
117
+ /// set control flag at +0xB3C (0x57BD10, RET 0x4)
118
+ #[slot(1)]
119
+ pub set_control_flag: fn(this: *mut PCLandscape, flag: u32),
120
+ /// apply explosion crater (0x57C820, RET 0xC) — terrain destruction
121
+ #[slot(2)]
122
+ pub apply_explosion: fn(this: *mut PCLandscape, p1: u32, p2: u32, p3: u32),
123
+ /// init landscape borders and layers (0x57D7F0, RET 0x20)
124
+ #[slot(6)]
125
+ pub init_borders: fn(
126
+ this: *mut PCLandscape,
127
+ p1: u32,
128
+ p2: u32,
129
+ p3: u32,
130
+ p4: u32,
131
+ p5: u32,
132
+ p6: u32,
133
+ p7: u32,
134
+ p8: u32,
135
+ ),
136
+ /// redraw single row (0x57CF60, RET 0x4)
137
+ #[slot(8)]
138
+ pub redraw_row: fn(this: *mut PCLandscape, row: u32),
139
+ /// get frame checksum component (0x57D540, plain RET)
140
+ #[slot(18)]
141
+ pub get_frame_checksum: fn(this: *mut PCLandscape) -> u32,
142
  }
143
+
144
+ bind_PCLandscapeVtable!(PCLandscape, vtable);
crates/openwa-core/src/render/palette.rs CHANGED
@@ -44,6 +44,29 @@ const _: () = assert!(core::mem::offset_of!(PaletteContext, cache) == 0x608);
44
  const _: () = assert!(core::mem::offset_of!(PaletteContext, dirty) == 0x708);
45
  const _: () = assert!(core::mem::offset_of!(PaletteContext, cache_iter) == 0x70A);
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  /// Map an RGB color to the nearest display palette index.
48
  ///
49
  /// Rust port of `PaletteContext__MapColor` (0x5412B0). Operates on a raw
 
44
  const _: () = assert!(core::mem::offset_of!(PaletteContext, dirty) == 0x708);
45
  const _: () = assert!(core::mem::offset_of!(PaletteContext, cache_iter) == 0x70A);
46
 
47
+ /// Pure Rust port of PaletteContext__Init (0x5411A0).
48
+ ///
49
+ /// Usercall: EAX = ctx pointer, plain RET.
50
+ ///
51
+ /// Initializes the free slot stack and clears the in-use table.
52
+ /// Caller must set `dirty_range_min` and `dirty_range_max` before calling.
53
+ pub unsafe fn palette_context_init(ctx: *mut PaletteContext) {
54
+ let min = (*ctx).dirty_range_min;
55
+ let max = (*ctx).dirty_range_max;
56
+ let count = (max - min) + 1;
57
+
58
+ (*ctx).cache_count = 0;
59
+ (*ctx).free_count = count;
60
+
61
+ // Fill free stack with palette indices from max down to min
62
+ for i in 0..count as usize {
63
+ (*ctx).free_stack[i] = (max as u8).wrapping_sub(i as u8);
64
+ }
65
+
66
+ (*ctx).cache_iter = 0;
67
+ (*ctx).in_use = [0u8; 256];
68
+ }
69
+
70
  /// Map an RGB color to the nearest display palette index.
71
  ///
72
  /// Rust port of `PaletteContext__MapColor` (0x5412B0). Operates on a raw
crates/openwa-dll/src/replacements/sound.rs CHANGED
@@ -279,15 +279,15 @@ unsafe fn patch_dssound_vtable() -> Result<(), &'static str> {
279
  use openwa_core::audio::{
280
  dssound_destructor, dssound_noop, dssound_returns_0, dssound_returns_1,
281
  dssound_sub_destructor, is_channel_finished, is_slot_loaded, load_wav, play_sound,
282
- play_sound_pooled, release_finished, set_channel_volume, set_master_volume, set_pan,
283
- set_volume_params, stop_channel, update_channels, DSSoundVtable,
284
  };
285
  use openwa_core::vtable_replace;
286
 
287
  vtable_replace!(DSSoundVtable, va::DS_SOUND_VTABLE, {
288
  destructor => dssound_destructor,
289
  update_channels => update_channels,
290
- set_volume_params => set_volume_params,
291
  play_sound => play_sound,
292
  play_sound_pooled => play_sound_pooled,
293
  set_pan => set_pan,
 
279
  use openwa_core::audio::{
280
  dssound_destructor, dssound_noop, dssound_returns_0, dssound_returns_1,
281
  dssound_sub_destructor, is_channel_finished, is_slot_loaded, load_wav, play_sound,
282
+ play_sound_pooled, release_finished, set_channel_volume, set_frequency_scale,
283
+ set_master_volume, set_pan, stop_channel, update_channels, DSSoundVtable,
284
  };
285
  use openwa_core::vtable_replace;
286
 
287
  vtable_replace!(DSSoundVtable, va::DS_SOUND_VTABLE, {
288
  destructor => dssound_destructor,
289
  update_channels => update_channels,
290
+ set_frequency_scale => set_frequency_scale,
291
  play_sound => play_sound,
292
  play_sound_pooled => play_sound_pooled,
293
  set_pan => set_pan,