Merge branch 'ddgame-constructor': Rust DDGame constructor
Browse filesFull 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
- crates/openwa-core/src/address.rs +40 -0
- crates/openwa-core/src/audio/dssound.rs +82 -52
- crates/openwa-core/src/audio/mod.rs +6 -8
- crates/openwa-core/src/display/palette.rs +9 -3
- crates/openwa-core/src/engine/ddgame.rs +1781 -11
- crates/openwa-core/src/engine/ddgame_wrapper.rs +1 -1
- crates/openwa-core/src/engine/game_info.rs +71 -24
- crates/openwa-core/src/engine/game_session.rs +2 -2
- crates/openwa-core/src/fixed.rs +1 -1
- crates/openwa-core/src/game/frontend.rs +0 -4
- crates/openwa-core/src/game/message.rs +1 -2
- crates/openwa-core/src/game/scheme.rs +38 -29
- crates/openwa-core/src/input/controller.rs +3 -1
- crates/openwa-core/src/lib.rs +4 -4
- crates/openwa-core/src/render/queue.rs +8 -8
- crates/openwa-core/src/render/sprite.rs +0 -1
- crates/openwa-core/src/task/base.rs +4 -2
- crates/openwa-core/src/task/cloud.rs +1 -1
- crates/openwa-core/src/task/game_task.rs +1 -1
- crates/openwa-core/src/task/missile.rs +1 -6
- crates/openwa-core/src/task/mod.rs +2 -2
- crates/openwa-core/src/task/overlays.rs +1 -1
- crates/openwa-core/src/task/team.rs +1 -1
- crates/openwa-core/src/task/turn_game.rs +1 -2
- crates/openwa-core/src/task/worm.rs +1 -2
- crates/openwa-core/src/wa/frontend.rs +1 -1
- crates/openwa-core/src/wa/mfc.rs +2 -3
- crates/openwa-core/src/wa_alloc.rs +1 -2
- crates/openwa-core/src/wa_call.rs +3 -6
- crates/openwa-core/tests/scheme_parse.rs +12 -11
- crates/openwa-debugui/src/app.rs +579 -365
- crates/openwa-debugui/src/log.rs +7 -2
- crates/openwa-harness/src/lib.rs +18 -9
- crates/openwa-launcher/src/inject.rs +39 -16
- crates/openwa-launcher/src/main.rs +57 -32
- crates/openwa-wormkit/src/hook.rs +3 -6
- crates/openwa-wormkit/src/lib.rs +7 -2
- crates/openwa-wormkit/src/replacements/config.rs +29 -30
- crates/openwa-wormkit/src/replacements/ddgame_init.rs +205 -0
- crates/openwa-wormkit/src/replacements/display.rs +2 -2
- crates/openwa-wormkit/src/replacements/frontend.rs +5 -3
- crates/openwa-wormkit/src/replacements/game_session.rs +371 -50
- crates/openwa-wormkit/src/replacements/hardware_init.rs +35 -26
- crates/openwa-wormkit/src/replacements/headless.rs +48 -22
- crates/openwa-wormkit/src/replacements/input.rs +12 -7
- crates/openwa-wormkit/src/replacements/mod.rs +3 -1
- crates/openwa-wormkit/src/replacements/render.rs +77 -91
- crates/openwa-wormkit/src/replacements/scheme.rs +21 -44
- crates/openwa-wormkit/src/replacements/sound.rs +18 -13
- 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 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 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:
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 200 |
|
| 201 |
/// Trivial stub — returns 1. Used for slot 23.
|
| 202 |
-
pub unsafe extern "thiscall" fn returns_1(_this: *mut DSSound) -> u32 {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
) -> bool {
|
| 603 |
let snd = &mut *this;
|
| 604 |
-
let result = core_play_sound(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(_) => {
|
|
|
|
|
|
|
|
|
|
| 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(_) => {
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 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) {
|
|
|
|
|
|
|
| 53 |
/// Vtable[3]: Initialize palette.
|
| 54 |
-
pub unsafe fn init(&mut self) {
|
|
|
|
|
|
|
| 55 |
/// Vtable[2]: Set palette mode.
|
| 56 |
-
pub unsafe fn set_mode(&mut self, mode: u32) {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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))
|
|
|
|
|
|
|
| 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 ®ions {
|
| 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:
|
| 25 |
-
///
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
///
|
| 29 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 40 |
-
pub _unknown_daa5: [u8;
|
| 41 |
|
| 42 |
-
//
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 190 |
.field("home_lock", &self.home_lock)
|
| 191 |
-
.field(
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
.field("capture_transparent_pngs", &self.capture_transparent_pngs)
|
| 198 |
.field("camera_unlock_mouse_speed", &self.camera_unlock_mouse_speed)
|
| 199 |
-
.field(
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
.field("_zeroed_f3f0", &self._zeroed_f3f0)
|
| 203 |
-
.field(
|
|
|
|
|
|
|
|
|
|
| 204 |
.field("speech_path", &speech_str)
|
| 205 |
.field("speech_enabled", &self.speech_enabled)
|
| 206 |
.field("headless_mode", &self.headless_mode)
|
| 207 |
-
.field(
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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!(
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
SchemeError::BadMagic(m) => {
|
| 96 |
-
write!(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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]
|
| 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",
|
| 481 |
-
"Super Banana Bomb",
|
| 482 |
-
"Mine Strike",
|
| 483 |
-
"Girder Starter Pack",
|
| 484 |
-
"Earthquake",
|
| 485 |
-
"Scales of Justice",
|
| 486 |
-
"Ming Vase",
|
| 487 |
-
"Mike's Carpet Bomb",
|
| 488 |
"Patsy's Magic Bullet", // 53
|
| 489 |
-
"Indian Nuclear Test",
|
| 490 |
-
"Select Worm",
|
| 491 |
-
"Salvation Army",
|
| 492 |
-
"Mole Squadron",
|
| 493 |
-
"MB Bomb",
|
| 494 |
-
"Concrete Donkey",
|
| 495 |
-
"Suicide Bomber",
|
| 496 |
-
"Sheep Strike",
|
| 497 |
-
"Mail Strike",
|
| 498 |
-
"Armageddon",
|
| 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])
|
| 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,
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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) {
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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,
|
| 116 |
-
pub y1: u32,
|
| 117 |
-
pub x2: u32,
|
| 118 |
-
pub y2: u32,
|
| 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,
|
| 131 |
-
pub y_pos: u32,
|
| 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,
|
| 146 |
-
pub y_pos: u32,
|
| 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 {
|
|
|
|
|
|
|
| 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 |
-
|
| 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);
|
| 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);
|
| 78 |
-
assert_eq!(opts.worm_select, 1);
|
| 79 |
assert_eq!(opts.sudden_death_event, 3); // Nothing
|
| 80 |
-
assert_eq!(opts.worm_energy, 150);
|
| 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);
|
| 98 |
-
assert_eq!(opts.worm_select, 1);
|
| 99 |
-
assert_eq!(opts.worm_energy, 100);
|
| 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 =
|
|
|
|
| 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 |
-
|
| 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::{
|
|
|
|
|
|
|
| 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,
|
| 15 |
-
(va::CTASK_LAND_VTABLE,
|
| 16 |
-
(va::CTASK_TURN_GAME_VTABLE,
|
| 17 |
-
(va::CTASK_TEAM_VTABLE,
|
| 18 |
-
(va::CTASK_FILTER_VTABLE,
|
| 19 |
-
(va::CTASK_DIRT_VTABLE,
|
| 20 |
(va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"),
|
| 21 |
-
(va::CTASK_CPU_VTABLE,
|
| 22 |
-
(va::CTASK_MISSILE_VTABLE,
|
| 23 |
-
(va::CTASK_MINE_VTABLE,
|
| 24 |
-
(va::CTASK_OILDRUM_VTABLE,
|
| 25 |
-
(va::CTASK_CRATE_VTABLE,
|
| 26 |
-
(va::CTASK_CLOUD_VTABLE,
|
| 27 |
-
(va::CTASK_SEA_BUBBLE_VTABLE,
|
| 28 |
-
(va::CTASK_FIRE_VTABLE,
|
| 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
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 69 |
let wrapper_addr = *((session_ptr + 0xA0) as *const u32);
|
| 70 |
-
if wrapper_addr == 0 {
|
|
|
|
|
|
|
| 71 |
let ddgame_ptr = (*(wrapper_addr as *const DDGameWrapper)).ddgame;
|
| 72 |
-
if ddgame_ptr.is_null() {
|
|
|
|
|
|
|
| 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
|
| 85 |
-
if data.is_null() || slots == 0 {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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
|
| 114 |
-
let mut stack
|
| 115 |
let mut visited = std::collections::HashSet::new();
|
| 116 |
while let Some(addr) = stack.pop() {
|
| 117 |
-
if addr == 0 || !visited.insert(addr) {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
}
|
| 124 |
out
|
| 125 |
}
|
| 126 |
|
| 127 |
unsafe fn collect_live_entities() -> Vec<(u32, u32)> {
|
| 128 |
-
let Some(ddgame) = get_ddgame() else {
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 221 |
});
|
| 222 |
}
|
| 223 |
}
|
|
@@ -227,7 +263,12 @@ impl eframe::App for DebugApp {
|
|
| 227 |
// ---------------------------------------------------------------------------
|
| 228 |
|
| 229 |
impl DebugApp {
|
| 230 |
-
fn show_census(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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!(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
ui.separator();
|
| 254 |
|
| 255 |
egui::ScrollArea::vertical()
|
| 256 |
.auto_shrink([false, false])
|
| 257 |
.show(ui, |ui| {
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 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
|
|
|
|
| 329 |
.find(|&&(o, _)| o == off)
|
| 330 |
.map(|&(_, n)| n)
|
| 331 |
-
.or_else(||
|
| 332 |
-
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
| 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((
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
| 351 |
chunk_start = chunk_end;
|
| 352 |
}
|
| 353 |
|
| 354 |
for (start, end, section_name) in §ions {
|
| 355 |
let (start, end) = (*start, *end);
|
| 356 |
-
if start >= total_size {
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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| {
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 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 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
});
|
| 575 |
-
}
|
| 576 |
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
|
|
|
| 599 |
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 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 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 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 §ions {
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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,
|
| 153 |
-
first_thunk: u32,
|
| 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 |
-
|
| 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!(
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| 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 =
|
|
|
|
| 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,
|
|
|
|
| 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,
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
) -> LPVOID;
|
| 25 |
-
fn VirtualFreeEx(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
fn WriteProcessMemory(
|
| 27 |
-
hProcess: HANDLE,
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 34 |
lpStartAddress: unsafe extern "system" fn(LPVOID) -> DWORD,
|
| 35 |
-
lpParameter: LPVOID,
|
|
|
|
|
|
|
| 36 |
) -> HANDLE;
|
| 37 |
fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD;
|
| 38 |
fn CloseHandle(hObject: HANDLE) -> BOOL;
|
| 39 |
}
|
| 40 |
|
| 41 |
-
const MEM_COMMIT: DWORD
|
| 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,
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 68 |
-
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 93 |
mem::transmute(load_library),
|
| 94 |
-
remote_buf,
|
|
|
|
|
|
|
| 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)]
|
| 10 |
-
|
| 11 |
-
#[allow(non_camel_case_types)]
|
| 12 |
-
|
| 13 |
-
#[allow(non_camel_case_types)]
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
#[repr(C)]
|
| 17 |
#[allow(non_snake_case)]
|
| 18 |
struct STARTUPINFOA {
|
| 19 |
-
cb: DWORD,
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
#[repr(C)]
|
| 27 |
#[allow(non_snake_case)]
|
| 28 |
struct PROCESS_INFORMATION {
|
| 29 |
-
hProcess: HANDLE,
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
-
const CREATE_SUSPENDED: DWORD
|
| 33 |
-
const STARTF_USESHOWWINDOW: DWORD
|
| 34 |
-
const SW_SHOWMINIMIZED: u16
|
| 35 |
-
const INFINITE: DWORD
|
| 36 |
|
| 37 |
#[link(name = "kernel32")]
|
| 38 |
extern "system" {
|
| 39 |
fn CreateProcessA(
|
| 40 |
-
lpApplicationName: *const u8,
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 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(),
|
|
|
|
| 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() {
|
|
|
|
|
|
|
| 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: ",
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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" {
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
|
|
|
| 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 =
|
| 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,
|
| 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!(
|
|
|
|
|
|
|
| 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`
|
| 14 |
-
//!
|
| 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
|
| 61 |
-
"popl %esi",
|
| 62 |
-
"retl $8", // stdcall
|
| 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 =
|
| 95 |
-
"jmpl *({fn})", // tail-jump;
|
| 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
|
| 135 |
|
| 136 |
-
//
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 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 |
-
//
|
| 162 |
-
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 =
|
|
|
|
| 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.
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
))
|
|
|
|
| 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
|
| 383 |
-
(*session).keyboard
|
| 384 |
-
(*session).sound
|
| 385 |
-
(*session).palette
|
| 386 |
-
(*session).streaming_audio
|
| 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
|
| 463 |
-
INPUT_CTRL_INIT_ADDR
|
| 464 |
-
STREAM_CTOR_ADDR
|
| 465 |
-
DDISPLAY_INIT_ADDR
|
| 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,
|
|
|
|
|
|
|
|
|
|
| 15 |
) -> i32 {
|
| 16 |
-
let t = if text.is_null() {
|
| 17 |
-
|
|
|
|
| 18 |
std::ffi::CStr::from_ptr(text as *const i8)
|
| 19 |
-
.to_str()
|
|
|
|
|
|
|
| 20 |
};
|
| 21 |
-
let c = if caption.is_null() {
|
| 22 |
-
|
|
|
|
| 23 |
std::ffi::CStr::from_ptr(caption as *const i8)
|
| 24 |
-
.to_str()
|
|
|
|
|
|
|
| 25 |
};
|
| 26 |
let _ = log_line(&format!(
|
| 27 |
"[Headless] Suppressed MessageBoxA: caption={c:?} text={t:?}"
|
| 28 |
));
|
| 29 |
-
if flags & 0xF == 0x4 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
unsafe extern "system" fn hook_messagebox_w(
|
| 33 |
-
_hwnd: u32,
|
|
|
|
|
|
|
|
|
|
| 34 |
) -> i32 {
|
| 35 |
-
let t = if text.is_null() {
|
| 36 |
-
|
|
|
|
| 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() {
|
| 41 |
-
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
(
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
] {
|
| 70 |
-
let proc =
|
| 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!(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
| 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).
|
| 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 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 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 |
-
"",
|
| 505 |
-
"Beginner",
|
| 506 |
-
"Intermediate",
|
| 507 |
-
"Pro",
|
| 508 |
-
"Tournament",
|
| 509 |
-
"Classic",
|
| 510 |
-
"Retro",
|
| 511 |
-
"Artillery",
|
| 512 |
-
"Sudden Sinking",
|
| 513 |
-
"Strategic",
|
| 514 |
-
"The Darkside",
|
| 515 |
-
"Armageddon",
|
| 516 |
-
"Blast Zone",
|
| 517 |
-
"Full Wormage",
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
set_volume_params,
|
| 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 |
-
})
|
|
|
|
| 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",
|
| 73 |
-
"mov esi, [esp + 16]",
|
| 74 |
-
"lea edi, [esp]",
|
| 75 |
-
"call [esp + 20]",
|
| 76 |
-
"add esp, 4",
|
| 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",
|
| 92 |
-
"lea esi, [esp]",
|
| 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",
|
| 113 |
-
"lea edi, [esp]",
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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",
|