refactor: typed vtables, port PaletteContext__Init and GameStateStream__Init
Browse filesReplace 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 |
-
///
|
| 93 |
-
pub
|
| 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:
|
| 188 |
-
pub
|
| 189 |
-
/// 0xBB8:
|
| 190 |
-
pub
|
| 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:
|
| 314 |
-
/// `
|
| 315 |
-
/// For each playing channel: new_freq = channel_freq *
|
| 316 |
-
pub unsafe extern "thiscall" fn
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
let snd = &mut *this;
|
| 318 |
|
| 319 |
// No change → early return.
|
| 320 |
-
if snd.
|
| 321 |
return;
|
| 322 |
}
|
| 323 |
-
snd.
|
| 324 |
-
snd.
|
| 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 *
|
| 338 |
-
if
|
| 339 |
continue;
|
| 340 |
}
|
| 341 |
-
let freq = (desc.channel_freq.0 as i64 *
|
| 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 *
|
| 546 |
-
let freq = if snd.
|
| 547 |
-
let f = (channel_freq as i64 * snd.
|
|
|
|
| 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 |
-
//
|
| 969 |
-
snd.
|
| 970 |
-
snd.
|
| 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,
|
| 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 |
-
//
|
| 496 |
-
// param = game_info.donkey_disabled (byte at +0xD94C)
|
| 497 |
let ddgame4 = ddgame_from_wrapper(wrapper);
|
| 498 |
-
let landscape = (*ddgame4).landscape
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
//
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 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 |
-
|
| 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
|
| 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::
|
| 1115 |
-
use crate::rebase::rb;
|
| 1116 |
-
use crate::wa_alloc::wa_malloc_zeroed;
|
| 1117 |
|
| 1118 |
-
let
|
| 1119 |
-
if
|
| 1120 |
return core::ptr::null_mut();
|
| 1121 |
}
|
| 1122 |
-
*
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 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 |
-
|
| 1784 |
-
|
| 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
|
| 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: *
|
| 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:
|
| 93 |
-
pub
|
| 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
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
pub
|
| 117 |
-
///
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 283 |
-
|
| 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 |
-
|
| 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,
|