refactor: split ddgame.rs (2240 lines) into 3 modules
Browse filesddgame.rs was monolithic at 2240 lines. Split into focused modules:
- ddgame.rs (618 lines): DDGame struct definition, impl block
(advance_rng, show_pool_overflow_warning), offsets module, Snapshot impl
- ddgame_constructor.rs (1111 lines): create_ddgame(), constructor helpers,
WA function wrappers, naked asm bridges, init_graphics_and_resources()
- team_arena.rs (545 lines): TeamArenaState, WormEntry, TeamHeader,
TeamBlock, WeaponSlots, TeamArenaRef, worm/phase constants, helper structs
(CoordEntry, TeamIndexMap, RenderEntry, SoundQueueEntry, etc.)
All public API paths preserved via pub use re-exports β no consumer
changes needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/engine/ddgame.rs
CHANGED
|
@@ -1,28 +1,32 @@
|
|
| 1 |
-
use crate::address::va;
|
| 2 |
-
use crate::audio::active_sound::ActiveSoundTable;
|
| 3 |
use crate::FieldRegistry;
|
|
|
|
| 4 |
use crate::audio::dssound::DSSound;
|
| 5 |
use crate::audio::music::Music;
|
| 6 |
use crate::audio::speech::SpeechSlotTable;
|
| 7 |
use crate::display::dd_display::DDDisplay;
|
| 8 |
-
use crate::display::gradient::compute_complex_gradient;
|
| 9 |
use crate::display::palette::Palette;
|
| 10 |
-
use crate::engine::ddgame_wrapper::DDGameWrapper;
|
| 11 |
use crate::engine::game_info::GameInfo;
|
| 12 |
-
use crate::engine::net_bridge::NetBridge;
|
| 13 |
use crate::game::weapon::WeaponTable;
|
| 14 |
use crate::input::keyboard::DDKeyboard;
|
| 15 |
-
use crate::rebase::rb;
|
| 16 |
-
// Re-export public GfxHandler functions so existing `engine::ddgame::*` imports keep working.
|
| 17 |
-
use crate::render::gfx_dir::{
|
| 18 |
-
call_gfx_find_and_load, call_gfx_load_and_wrap, call_gfx_load_dir, GfxDir,
|
| 19 |
-
};
|
| 20 |
-
pub use crate::render::gfx_dir::{gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create};
|
| 21 |
use crate::render::landscape::PCLandscape;
|
| 22 |
use crate::render::queue::RenderQueue;
|
| 23 |
use crate::render::turn_order::TurnOrderWidget;
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
/// DDGame β the main game engine object.
|
| 28 |
///
|
|
@@ -391,17 +395,6 @@ pub struct DDGame {
|
|
| 391 |
|
| 392 |
const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98D8);
|
| 393 |
|
| 394 |
-
// ============================================================
|
| 395 |
-
// DDGame constructor β replaces DDGame__Constructor (0x56E220)
|
| 396 |
-
// ============================================================
|
| 397 |
-
//
|
| 398 |
-
// Despite being named DDGame__Constructor, the original function
|
| 399 |
-
// receives DDGameWrapper* as `this` and creates DDGame internally.
|
| 400 |
-
// It populates fields on BOTH the wrapper and the inner DDGame.
|
| 401 |
-
// The Rust entry point is DDGameWrapper::create_game() in
|
| 402 |
-
// ddgame_wrapper.rs; the bulk of the logic lives here because
|
| 403 |
-
// it primarily initializes DDGame fields.
|
| 404 |
-
|
| 405 |
// ============================================================
|
| 406 |
// DDGame methods
|
| 407 |
// ============================================================
|
|
@@ -444,1087 +437,11 @@ impl DDGame {
|
|
| 444 |
}
|
| 445 |
}
|
| 446 |
|
| 447 |
-
//
|
| 448 |
-
//
|
| 449 |
-
// ============================================================
|
| 450 |
-
// These are called both by create_ddgame() and by MinHook
|
| 451 |
-
// trampolines in openwa-wormkit/replacements/ddgame_init.rs.
|
| 452 |
-
|
| 453 |
-
/// Pure Rust implementation of DDGame__InitFields (0x526120).
|
| 454 |
-
///
|
| 455 |
-
/// Zeroes stride-0x194 table entries, calls init_render_indices,
|
| 456 |
-
/// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
|
| 457 |
-
///
|
| 458 |
-
/// # Safety
|
| 459 |
-
/// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98D8 bytes).
|
| 460 |
-
pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
|
| 461 |
-
let base = ddgame as *mut u8;
|
| 462 |
-
|
| 463 |
-
// Zero the stride-0x194 table (10 entries starting at 0x379C).
|
| 464 |
-
// These offsets are deep in the unknown 0x2E00-0x45EB region and don't
|
| 465 |
-
// have named fields yet β keep as raw offsets for now.
|
| 466 |
-
for &off in &[
|
| 467 |
-
0x379Cusize, 0x3930, 0x3AC4, 0x3C58, 0x3DEC,
|
| 468 |
-
0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0,
|
| 469 |
-
] {
|
| 470 |
-
*(base.add(off) as *mut u32) = 0;
|
| 471 |
-
}
|
| 472 |
-
|
| 473 |
-
// init_field_64d8 = TeamArenaState.team_count (arena+0x1EB0)
|
| 474 |
-
(*ddgame).team_arena.team_count = 0;
|
| 475 |
-
// init_field_72a4 = weapon_slots flat[754] = alliance 5, ammo[44]
|
| 476 |
-
(*ddgame).team_arena.weapon_slots.teams[5].ammo[44] = 0;
|
| 477 |
-
|
| 478 |
-
// InitRenderIndices β original sets ESI = ddgame + 0x72D8, now uses typed DDGame ptr
|
| 479 |
-
ddgame_init_render_indices(ddgame);
|
| 480 |
-
|
| 481 |
-
// Zero x and y of each screen coordinate entry (4 entries each)
|
| 482 |
-
for entry in &mut (*ddgame).screen_coords {
|
| 483 |
-
entry.x = 0;
|
| 484 |
-
entry.y = 0;
|
| 485 |
-
}
|
| 486 |
-
for entry in &mut (*ddgame).screen_coords_2 {
|
| 487 |
-
entry.x = 0;
|
| 488 |
-
entry.y = 0;
|
| 489 |
-
}
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
/// Pure Rust implementation of DDGame__InitRenderIndices (0x526080).
|
| 493 |
-
///
|
| 494 |
-
/// Convention: usercall(ESI=base_ptr), plain RET.
|
| 495 |
-
/// Initialize render state flag, render entry table, and team index maps.
|
| 496 |
-
///
|
| 497 |
-
/// Original: called with ESI = ddgame + 0x72D8, but now uses typed DDGame fields.
|
| 498 |
-
///
|
| 499 |
-
/// # Safety
|
| 500 |
-
/// `ddgame` must point to a valid DDGame allocation.
|
| 501 |
-
pub unsafe fn ddgame_init_render_indices(ddgame: *mut DDGame) {
|
| 502 |
-
(*ddgame).render_state_flag = 0;
|
| 503 |
-
|
| 504 |
-
// Zero the active flag of each render entry (eh_vector_constructor_iterator).
|
| 505 |
-
for entry in &mut (*ddgame).render_entries {
|
| 506 |
-
entry.active = 0;
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
// Initialize all three team index maps as identity permutations.
|
| 510 |
-
for map in &mut (*ddgame).team_index_maps {
|
| 511 |
-
for i in 0..16i16 {
|
| 512 |
-
map.entries[i as usize] = i;
|
| 513 |
-
}
|
| 514 |
-
map.count = 0x10;
|
| 515 |
-
map.terminator = 0;
|
| 516 |
-
}
|
| 517 |
-
}
|
| 518 |
|
| 519 |
// BitGrid__Init moved to crate::task::bit_grid
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
/// Pure Rust implementation of FUN_570E20 (display layer color init).
|
| 523 |
-
///
|
| 524 |
-
/// Convention: usercall(ESI=wrapper), plain RET.
|
| 525 |
-
///
|
| 526 |
-
/// Sets display layer color parameters via display->vtable[4](layer, color).
|
| 527 |
-
/// Layer 1 color depends on gfx_mode and game_version.
|
| 528 |
-
/// Layer 2 = 0x20, Layer 3 = 0x70.
|
| 529 |
-
///
|
| 530 |
-
/// # Safety
|
| 531 |
-
/// `wrapper` must be a valid DDGameWrapper with initialized display and ddgame.
|
| 532 |
-
#[cfg(target_arch = "x86")]
|
| 533 |
-
pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) {
|
| 534 |
-
let ddgame = (*wrapper).ddgame;
|
| 535 |
-
let game_info = (*ddgame).game_info;
|
| 536 |
-
let game_version = (*game_info).game_version;
|
| 537 |
-
|
| 538 |
-
// wrapper+0x4C8 is gfx_mode (at DDGameWrapper.gfx_mode)
|
| 539 |
-
let layer1_color = if (*wrapper).gfx_mode == 0 {
|
| 540 |
-
// (game_version > -2) - 1: yields 0 if true, -1 if false
|
| 541 |
-
// Then + 0x69: yields 0x69 or 0x68
|
| 542 |
-
if game_version > -2 {
|
| 543 |
-
0x69i32
|
| 544 |
-
} else {
|
| 545 |
-
0x68i32
|
| 546 |
-
}
|
| 547 |
-
} else {
|
| 548 |
-
5 + 0x69 // = 0x6E
|
| 549 |
-
};
|
| 550 |
-
|
| 551 |
-
let display = (*wrapper).display;
|
| 552 |
-
(*display).set_layer_color(1, layer1_color);
|
| 553 |
-
(*display).set_layer_color(2, 0x20);
|
| 554 |
-
(*display).set_layer_color(3, 0x70);
|
| 555 |
-
}
|
| 556 |
-
|
| 557 |
-
/// Initialize runtime addresses for the constructor bridges.
|
| 558 |
-
/// Must be called once at DLL startup (from lib.rs or similar).
|
| 559 |
-
pub fn init_constructor_addrs() {
|
| 560 |
-
unsafe {
|
| 561 |
-
SPRITE_REGION_CTOR_ADDR = rb(va::SPRITE_REGION_CONSTRUCTOR);
|
| 562 |
-
FUN_570A90_ADDR = rb(va::FUN_570A90);
|
| 563 |
-
FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
|
| 564 |
-
LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
|
| 565 |
-
LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
|
| 566 |
-
GFX_LOAD_SPRITES_ADDR = rb(va::GFX_DIR_LOAD_SPRITES);
|
| 567 |
-
}
|
| 568 |
-
crate::render::gfx_dir::init_addrs();
|
| 569 |
-
}
|
| 570 |
-
|
| 571 |
-
// ββ Typed wrappers for WA stdcall functions ββββββββββββββββββββββββββββββββ
|
| 572 |
-
// Each wraps a single WA function with a typed Rust signature.
|
| 573 |
-
// Replace with pure Rust implementations as functions are ported.
|
| 574 |
-
|
| 575 |
-
/// DDGame__InitVersionFlags (0x525BE0): sets DDGame+0x7E2E/0x7E2F/0x7E3F.
|
| 576 |
-
#[cfg(target_arch = "x86")]
|
| 577 |
-
unsafe fn wa_init_version_flags(wrapper: *mut DDGameWrapper) {
|
| 578 |
-
let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 579 |
-
core::mem::transmute(rb(va::DDGAME_INIT_VERSION_FLAGS) as usize);
|
| 580 |
-
f(wrapper);
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
/// GfxHandler__LoadSprites (0x570B50): usercall(ESI=layer_ctx) + stdcall(4 params).
|
| 584 |
-
///
|
| 585 |
-
/// ESI must hold the display layer context (from DDDisplay::set_active_layer).
|
| 586 |
-
/// The function uses ESI for LoadSpriteFromVfs and GfxResource__Create_Maybe
|
| 587 |
-
/// when param4 (gfx_dir) is non-null.
|
| 588 |
-
#[cfg(target_arch = "x86")]
|
| 589 |
-
#[unsafe(naked)]
|
| 590 |
-
unsafe extern "C" fn wa_load_sprites(
|
| 591 |
-
_wrapper: *mut DDGameWrapper,
|
| 592 |
-
_sprite_data: *mut u8,
|
| 593 |
-
_display_flags: u32,
|
| 594 |
-
_param4: u32,
|
| 595 |
-
_layer_ctx: *mut u8, // β ESI
|
| 596 |
-
) {
|
| 597 |
-
core::arch::naked_asm!(
|
| 598 |
-
"push esi",
|
| 599 |
-
"mov esi, [esp+24]", // layer_ctx (5th param: 4 saved + 4 ret + 4*4 params + 4 = 24)
|
| 600 |
-
"push [esp+20]", // param4
|
| 601 |
-
"push [esp+20]", // display_flags
|
| 602 |
-
"push [esp+20]", // sprite_data
|
| 603 |
-
"push [esp+20]", // wrapper
|
| 604 |
-
"call [{addr}]",
|
| 605 |
-
"pop esi",
|
| 606 |
-
"ret",
|
| 607 |
-
addr = sym GFX_LOAD_SPRITES_ADDR,
|
| 608 |
-
);
|
| 609 |
-
}
|
| 610 |
-
|
| 611 |
-
static mut GFX_LOAD_SPRITES_ADDR: u32 = 0;
|
| 612 |
-
|
| 613 |
-
/// DSSound_LoadEffectWAVs (0x571660): load sound effect WAVs.
|
| 614 |
-
#[cfg(target_arch = "x86")]
|
| 615 |
-
unsafe fn wa_load_effect_wavs(wrapper: *mut DDGameWrapper) {
|
| 616 |
-
let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 617 |
-
core::mem::transmute(rb(va::DSSOUND_LOAD_EFFECT_WAVS) as usize);
|
| 618 |
-
f(wrapper);
|
| 619 |
-
}
|
| 620 |
-
|
| 621 |
-
/// PCLandscape__Constructor (0x57ACB0): construct landscape object (0xB44 bytes, 11 params).
|
| 622 |
-
///
|
| 623 |
-
/// param_5 is `game_info + 0xDAAC` (landscape data path region within GameInfo).
|
| 624 |
-
/// The SoundEmitter sub-constructor reads paths, water settings, etc. from this
|
| 625 |
-
/// pointer with negative offsets back into GameInfo.
|
| 626 |
-
#[cfg(target_arch = "x86")]
|
| 627 |
-
unsafe fn wa_pc_landscape_ctor(
|
| 628 |
-
this: *mut u8,
|
| 629 |
-
ddgame: *mut DDGame,
|
| 630 |
-
gfx_resource: *mut u8,
|
| 631 |
-
display: *mut DDDisplay,
|
| 632 |
-
landscape_data: *const u8,
|
| 633 |
-
byte_output: *mut u8,
|
| 634 |
-
gfx_mode: u32,
|
| 635 |
-
temp_buf: *mut u32,
|
| 636 |
-
coord_output: *mut u32,
|
| 637 |
-
width_output: *mut u32,
|
| 638 |
-
height_output: *mut u32,
|
| 639 |
-
) -> *mut u8 {
|
| 640 |
-
let f: unsafe extern "stdcall" fn(
|
| 641 |
-
*mut u8,
|
| 642 |
-
*mut DDGame,
|
| 643 |
-
*mut u8,
|
| 644 |
-
*mut DDDisplay,
|
| 645 |
-
*const u8,
|
| 646 |
-
*mut u8,
|
| 647 |
-
u32,
|
| 648 |
-
*mut u32,
|
| 649 |
-
*mut u32,
|
| 650 |
-
*mut u32,
|
| 651 |
-
*mut u32,
|
| 652 |
-
) -> *mut u8 = core::mem::transmute(rb(va::PC_LANDSCAPE_CONSTRUCTOR) as usize);
|
| 653 |
-
f(
|
| 654 |
-
this,
|
| 655 |
-
ddgame,
|
| 656 |
-
gfx_resource,
|
| 657 |
-
display,
|
| 658 |
-
landscape_data,
|
| 659 |
-
byte_output,
|
| 660 |
-
gfx_mode,
|
| 661 |
-
temp_buf,
|
| 662 |
-
coord_output,
|
| 663 |
-
width_output,
|
| 664 |
-
height_output,
|
| 665 |
-
)
|
| 666 |
-
}
|
| 667 |
-
|
| 668 |
-
/// DisplayGfx__Constructor (0x4F5E80): wrap raw image data in DisplayGfx object.
|
| 669 |
-
#[cfg(target_arch = "x86")]
|
| 670 |
-
unsafe fn wa_displaygfx_ctor(raw_image: *mut u8) -> *mut u8 {
|
| 671 |
-
let f: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
|
| 672 |
-
core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
|
| 673 |
-
f(raw_image)
|
| 674 |
-
}
|
| 675 |
-
|
| 676 |
-
/// DDGame__InitDisplayFinal (0x56A830): finalize display for non-headless mode.
|
| 677 |
-
#[cfg(target_arch = "x86")]
|
| 678 |
-
unsafe fn wa_init_display_final(display: *mut DDDisplay) {
|
| 679 |
-
let f: unsafe extern "stdcall" fn(*mut DDDisplay) =
|
| 680 |
-
core::mem::transmute(rb(va::DDGAME_INIT_DISPLAY_FINAL) as usize);
|
| 681 |
-
f(display);
|
| 682 |
-
}
|
| 683 |
-
|
| 684 |
-
/// DDGame__LoadHudAndWeaponSprites (0x53D0E0): load weapon icons and HUD sprites.
|
| 685 |
-
/// thiscall(ECX=gfx_dir) + 2 stack(ddgame, secondary_gfx_dir), RET 0x8.
|
| 686 |
-
#[cfg(target_arch = "x86")]
|
| 687 |
-
unsafe fn wa_load_hud_sprites(gfx_dir: *mut u8, ddgame: *mut DDGame, secondary: *mut u8) {
|
| 688 |
-
let f: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) =
|
| 689 |
-
core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
|
| 690 |
-
f(gfx_dir, ddgame, secondary);
|
| 691 |
-
}
|
| 692 |
-
|
| 693 |
-
/// DDGame__InitPaletteGradientSprites (0x5706D0): creates DisplayGfx palette
|
| 694 |
-
/// gradient objects for each team. stdcall(wrapper), RET 0x4.
|
| 695 |
-
#[cfg(target_arch = "x86")]
|
| 696 |
-
unsafe fn wa_init_palette_gradient_sprites(wrapper: *mut DDGameWrapper) {
|
| 697 |
-
let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 698 |
-
core::mem::transmute(rb(va::DDGAME_INIT_PALETTE_GRADIENT_SPRITES) as usize);
|
| 699 |
-
f(wrapper);
|
| 700 |
-
}
|
| 701 |
-
|
| 702 |
-
/// Optional callback invoked right after DDGame allocation (before any field
|
| 703 |
-
/// initialization). Used by the hardware watchpoint debugger to arm DR0βDR3.
|
| 704 |
-
pub static mut ON_DDGAME_ALLOC: Option<unsafe fn(*mut u8)> = None;
|
| 705 |
-
|
| 706 |
-
/// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
|
| 707 |
-
///
|
| 708 |
-
/// Allocates 0x98D8 bytes from WA's heap, initializes all fields, and creates
|
| 709 |
-
/// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the
|
| 710 |
-
/// returned DDGame.
|
| 711 |
-
///
|
| 712 |
-
/// # Safety
|
| 713 |
-
/// All pointer params must be valid WA objects. `wrapper` must be a
|
| 714 |
-
/// partially-initialized DDGameWrapper (vtable, display, sound set).
|
| 715 |
-
#[cfg(target_arch = "x86")]
|
| 716 |
-
pub unsafe fn create_ddgame(
|
| 717 |
-
wrapper: *mut DDGameWrapper,
|
| 718 |
-
keyboard: *mut DDKeyboard,
|
| 719 |
-
display: *mut DDDisplay,
|
| 720 |
-
sound: *mut DSSound,
|
| 721 |
-
palette: *mut Palette,
|
| 722 |
-
music: *mut Music,
|
| 723 |
-
param7: *mut u8, // timer object (0x1F4 observed)
|
| 724 |
-
net_game: *mut u8, // from GameSession
|
| 725 |
-
game_info: *mut GameInfo,
|
| 726 |
-
network_ecx: u32, // implicit ECX from caller
|
| 727 |
-
) -> *mut DDGame {
|
| 728 |
-
// ββ 1. Allocate and zero-fill ββ
|
| 729 |
-
// Original: malloc(0x98D8), memset(ptr, 0, 0x98B8) β last 0x20 bytes not zeroed.
|
| 730 |
-
// We zero the full 0x98D8 for safety (strictly more initialization than original).
|
| 731 |
-
let ddgame = wa_malloc_zeroed(0x98D8) as *mut DDGame;
|
| 732 |
-
if ddgame.is_null() {
|
| 733 |
-
return core::ptr::null_mut();
|
| 734 |
-
}
|
| 735 |
-
|
| 736 |
-
// Notify watchpoint debugger (if active) so it can arm DR0βDR3.
|
| 737 |
-
if let Some(cb) = ON_DDGAME_ALLOC {
|
| 738 |
-
cb(ddgame as *mut u8);
|
| 739 |
-
}
|
| 740 |
-
|
| 741 |
-
// ββ 2. InitFields β pure Rust (replaces usercall bridge) ββ
|
| 742 |
-
ddgame_init_fields(ddgame);
|
| 743 |
-
|
| 744 |
-
// ββ 3-4. Store params BEFORE exposing DDGame via wrapper ββ
|
| 745 |
-
// Critical: game_info must be set before wrapper->ddgame, because the
|
| 746 |
-
// message pump (triggered by audio loading) can cause game tasks to
|
| 747 |
-
// read ddgame->game_info. If game_info is null, they crash.
|
| 748 |
-
(*ddgame).display = display;
|
| 749 |
-
(*ddgame).sound = sound;
|
| 750 |
-
(*ddgame).keyboard = keyboard;
|
| 751 |
-
(*ddgame).palette = palette;
|
| 752 |
-
(*ddgame).music = music;
|
| 753 |
-
(*ddgame).timer_obj = param7;
|
| 754 |
-
(*ddgame).network_ecx = network_ecx;
|
| 755 |
-
(*ddgame).game_info = game_info;
|
| 756 |
-
(*ddgame).net_game = net_game;
|
| 757 |
-
|
| 758 |
-
// Now safe to expose β all fields that concurrent readers check are set.
|
| 759 |
-
(*wrapper).ddgame = ddgame;
|
| 760 |
-
|
| 761 |
-
// ββ 5. Set g_GameInfo global ββ
|
| 762 |
-
*(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info;
|
| 763 |
-
|
| 764 |
-
// ββ 6. Sound available + always-1 flags ββ
|
| 765 |
-
let is_headless = (*game_info).headless_mode != 0;
|
| 766 |
-
// sound_available enables loading progress bar, message pump, and sound during construction.
|
| 767 |
-
(*ddgame).sound_available = if is_headless { 0 } else { 1 };
|
| 768 |
-
(*ddgame).field_7efc = 1;
|
| 769 |
-
|
| 770 |
-
// ββ 7. Network bridge (online games only) ββ
|
| 771 |
-
(*wrapper).net_bridge = core::ptr::null_mut();
|
| 772 |
-
|
| 773 |
-
if (*game_info).game_version == -2 {
|
| 774 |
-
let bridge = wa_malloc_zeroed(core::mem::size_of::<NetBridge>() as u32) as *mut NetBridge;
|
| 775 |
-
(*bridge).ddgame = ddgame;
|
| 776 |
-
(*bridge).net_config_1 = (*game_info).net_config_1;
|
| 777 |
-
(*bridge).net_config_2 = (*game_info).net_config_2;
|
| 778 |
-
(*wrapper).net_bridge = bridge;
|
| 779 |
-
if network_ecx != 0 {
|
| 780 |
-
*((network_ecx as *mut u8).add(0x18) as *mut *mut NetBridge) = bridge;
|
| 781 |
-
}
|
| 782 |
-
}
|
| 783 |
-
|
| 784 |
-
// ββ 9. InitVersionFlags β sets DDGame+0x7E2E/0x7E2F/0x7E3F ββ
|
| 785 |
-
wa_init_version_flags(wrapper);
|
| 786 |
-
|
| 787 |
-
// ββ 10. GfxHandler, landscape, sprites, audio, resources ββ
|
| 788 |
-
init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless);
|
| 789 |
-
|
| 790 |
-
let _ = crate::log::log_line("[DDGame] create_ddgame complete");
|
| 791 |
-
ddgame
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
/// Second half of the constructor: GfxHandler, landscape, sprites, audio, resources.
|
| 795 |
-
///
|
| 796 |
-
/// Second half of the constructor β initializes graphics, audio, landscape, and sprites.
|
| 797 |
-
#[cfg(target_arch = "x86")]
|
| 798 |
-
unsafe fn init_graphics_and_resources(
|
| 799 |
-
wrapper: *mut DDGameWrapper,
|
| 800 |
-
game_info: *mut GameInfo,
|
| 801 |
-
_net_game: *mut u8,
|
| 802 |
-
_display: *mut DDDisplay,
|
| 803 |
-
is_headless: bool,
|
| 804 |
-
) {
|
| 805 |
-
let ddgame = (*wrapper).ddgame;
|
| 806 |
-
use core::ffi::c_char;
|
| 807 |
-
let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
|
| 808 |
-
core::mem::transmute(rb(va::WA_FOPEN) as usize);
|
| 809 |
-
let gfx_dir_vtable = rb(va::GFX_DIR_VTABLE) as u32;
|
| 810 |
-
|
| 811 |
-
// ββ GfxDir #1 (primary) ββ
|
| 812 |
-
let gfx1 = GfxDir::alloc(gfx_dir_vtable);
|
| 813 |
-
(*wrapper).primary_gfx_dir = gfx1 as *mut u8;
|
| 814 |
-
(*wrapper).secondary_gfx_dir = core::ptr::null_mut();
|
| 815 |
-
|
| 816 |
-
// Build path list (order depends on headless + display_flags)
|
| 817 |
-
let headless = (*game_info).headless_mode as i32;
|
| 818 |
-
let display_flags = (*game_info).display_flags as i32;
|
| 819 |
-
let paths: [&core::ffi::CStr; 3] = if headless != 0 {
|
| 820 |
-
if display_flags == 0 {
|
| 821 |
-
[
|
| 822 |
-
c"data\\Gfx\\Gfx.dir",
|
| 823 |
-
c"data\\Gfx\\Gfx0.dir",
|
| 824 |
-
c"data\\Gfx\\Gfx1.dir",
|
| 825 |
-
]
|
| 826 |
-
} else {
|
| 827 |
-
[
|
| 828 |
-
c"data\\Gfx\\Gfx.dir",
|
| 829 |
-
c"data\\Gfx\\Gfx1.dir",
|
| 830 |
-
c"data\\Gfx\\Gfx0.dir",
|
| 831 |
-
]
|
| 832 |
-
}
|
| 833 |
-
} else if display_flags == 0 {
|
| 834 |
-
[
|
| 835 |
-
c"data\\Gfx\\Gfx0.dir",
|
| 836 |
-
c"data\\Gfx\\Gfx1.dir",
|
| 837 |
-
c"data\\Gfx\\Gfx.dir",
|
| 838 |
-
]
|
| 839 |
-
} else {
|
| 840 |
-
[
|
| 841 |
-
c"data\\Gfx\\Gfx1.dir",
|
| 842 |
-
c"data\\Gfx\\Gfx0.dir",
|
| 843 |
-
c"data\\Gfx\\Gfx.dir",
|
| 844 |
-
]
|
| 845 |
-
};
|
| 846 |
-
|
| 847 |
-
let mut gfx_loaded_idx = 0u32;
|
| 848 |
-
for (i, path) in paths.iter().enumerate() {
|
| 849 |
-
let fp = fopen(path.as_ptr(), c"rb".as_ptr());
|
| 850 |
-
(*gfx1).file_handle = fp;
|
| 851 |
-
if !fp.is_null()
|
| 852 |
-
&& call_gfx_load_dir(gfx1 as *mut u8, crate::render::gfx_dir::gfx_load_dir_addr()) != 0
|
| 853 |
-
{
|
| 854 |
-
gfx_loaded_idx = i as u32;
|
| 855 |
-
break;
|
| 856 |
-
}
|
| 857 |
-
if i == 2 {
|
| 858 |
-
panic!("DDGame: couldn't open any Gfx.dir");
|
| 859 |
-
}
|
| 860 |
-
}
|
| 861 |
-
|
| 862 |
-
let headless_offset = if headless != 0 { 1u32 } else { 0u32 };
|
| 863 |
-
(*wrapper).gfx_mode = if gfx_loaded_idx.wrapping_sub(headless_offset) < 2 {
|
| 864 |
-
1
|
| 865 |
-
} else {
|
| 866 |
-
0
|
| 867 |
-
};
|
| 868 |
-
|
| 869 |
-
// ββ GfxDir #2 (conditional β supplemental sprites for certain game versions) ββ
|
| 870 |
-
let game_version = (*game_info).game_version;
|
| 871 |
-
let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
|
| 872 |
-
if game_version < threshold {
|
| 873 |
-
let c_digit = if game_version > -3 { b'2' } else { b'1' };
|
| 874 |
-
let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
|
| 875 |
-
gfx_c_path[14] = c_digit;
|
| 876 |
-
|
| 877 |
-
let gfx2 = GfxDir::alloc(gfx_dir_vtable);
|
| 878 |
-
(*wrapper).secondary_gfx_dir = gfx2 as *mut u8;
|
| 879 |
-
|
| 880 |
-
let fp = fopen(gfx_c_path.as_ptr().cast(), c"rb".as_ptr());
|
| 881 |
-
(*gfx2).file_handle = fp;
|
| 882 |
-
let load_addr = crate::render::gfx_dir::gfx_load_dir_addr();
|
| 883 |
-
if fp.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
|
| 884 |
-
let fp2 = fopen(c"data\\Gfx\\Gfx.dir".as_ptr(), c"rb".as_ptr());
|
| 885 |
-
(*gfx2).file_handle = fp2;
|
| 886 |
-
if fp2.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
|
| 887 |
-
panic!("DDGame: couldn't open secondary Gfx.dir");
|
| 888 |
-
}
|
| 889 |
-
}
|
| 890 |
-
}
|
| 891 |
-
|
| 892 |
-
// ββ Display palette setup (non-headless) ββ
|
| 893 |
-
if !is_headless {
|
| 894 |
-
if *(rb(va::G_DISPLAY_MODE_FLAG) as *const c_char) == 0 {
|
| 895 |
-
call_usercall_eax(wrapper, FUN_570A90_ADDR);
|
| 896 |
-
}
|
| 897 |
-
let disp = (*wrapper).display;
|
| 898 |
-
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 899 |
-
(*disp).set_layer_color(1, 0xFE);
|
| 900 |
-
(*disp).load_sprite(
|
| 901 |
-
1,
|
| 902 |
-
1,
|
| 903 |
-
0,
|
| 904 |
-
gfx_dir,
|
| 905 |
-
rb(va::STR_CDROM_SPR) as *const c_char,
|
| 906 |
-
);
|
| 907 |
-
(*disp).set_layer_visibility(1, -100);
|
| 908 |
-
|
| 909 |
-
// Palette slot range init (raw byte offsets into DisplayBase)
|
| 910 |
-
let disp_raw = disp as *mut u8;
|
| 911 |
-
let palette_range_ptr = *(disp_raw.add(0x3120) as *const *mut i16);
|
| 912 |
-
if !palette_range_ptr.is_null() && *(disp_raw.add(0x3534) as *const i32) == 0 {
|
| 913 |
-
let start = *palette_range_ptr as u32;
|
| 914 |
-
let end = (*palette_range_ptr.add(1) as u32) + 1;
|
| 915 |
-
if start < end {
|
| 916 |
-
for i in start..end {
|
| 917 |
-
*(disp_raw.add(0x312C + i as usize * 4) as *mut u32) = 1;
|
| 918 |
-
}
|
| 919 |
-
}
|
| 920 |
-
crate::wa_alloc::wa_free(*(disp_raw.add(0x3120) as *const *mut u8));
|
| 921 |
-
*(disp_raw.add(0x3120) as *mut u32) = 0;
|
| 922 |
-
}
|
| 923 |
-
}
|
| 924 |
-
|
| 925 |
-
// ββ FUN_00570E20: usercall(ESI=wrapper), plain RET ββ
|
| 926 |
-
// Runs for all modes β headless vtable[4] is 0x5231E0 (same as headful).
|
| 927 |
-
display_layer_color_init(wrapper);
|
| 928 |
-
|
| 929 |
-
// ββ Display vtable slot 5 (offset 0x14) ββ
|
| 930 |
-
// Original: CALL EAX (vtable[5]), saves return value in ESI for use as
|
| 931 |
-
// the `output` parameter in the color-entries GfxResource__Create call below.
|
| 932 |
-
let layer_ctx = (*(*ddgame).display).set_active_layer(1);
|
| 933 |
-
|
| 934 |
-
// ββ GfxDir color entries DDGame+0x730C..0x732C ββ
|
| 935 |
-
// Original logic: if gfx_mode!=0, try GfxResource__Create for colours.img.
|
| 936 |
-
// If gfx_mode==0 OR resource creation fails, fall back to LoadSprites.
|
| 937 |
-
// The fallback's 4th param is primary_gfx_dir when gfx_mode==0, or 0 on resource fail.
|
| 938 |
-
if (*wrapper).gfx_mode != 0 {
|
| 939 |
-
let res = gfx_resource_create(
|
| 940 |
-
(*wrapper).primary_gfx_dir,
|
| 941 |
-
rb(va::STR_COLOURS_IMG) as *const c_char,
|
| 942 |
-
layer_ctx,
|
| 943 |
-
);
|
| 944 |
-
if !res.is_null() {
|
| 945 |
-
let rvt = *(res as *const *const u32);
|
| 946 |
-
let get_color: unsafe extern "thiscall" fn(*mut u8, u32, u32) -> u32 =
|
| 947 |
-
core::mem::transmute(*rvt.add(4));
|
| 948 |
-
for i in 0..9u32 {
|
| 949 |
-
(*ddgame).gfx_color_table[i as usize] = get_color(res, i, 0);
|
| 950 |
-
}
|
| 951 |
-
let release: unsafe extern "thiscall" fn(*mut u8, u8) =
|
| 952 |
-
core::mem::transmute(*rvt.add(3));
|
| 953 |
-
release(res, 1);
|
| 954 |
-
} else {
|
| 955 |
-
// Resource creation failed β fallback with param4=0
|
| 956 |
-
wa_load_sprites(
|
| 957 |
-
wrapper,
|
| 958 |
-
(*ddgame).gfx_sprite_data.as_mut_ptr(),
|
| 959 |
-
(*game_info).display_flags,
|
| 960 |
-
0,
|
| 961 |
-
layer_ctx,
|
| 962 |
-
);
|
| 963 |
-
}
|
| 964 |
-
} else {
|
| 965 |
-
// gfx_mode==0 (headless): fallback LoadSprites with param4=primary_gfx_dir
|
| 966 |
-
wa_load_sprites(
|
| 967 |
-
wrapper,
|
| 968 |
-
(*ddgame).gfx_sprite_data.as_mut_ptr(),
|
| 969 |
-
(*game_info).display_flags,
|
| 970 |
-
(*wrapper).primary_gfx_dir as u32,
|
| 971 |
-
layer_ctx,
|
| 972 |
-
);
|
| 973 |
-
}
|
| 974 |
-
|
| 975 |
-
// ββ Secondary GfxDir object (DDGame+0x2C, conditional) ββ
|
| 976 |
-
if !(*wrapper).secondary_gfx_dir.is_null() {
|
| 977 |
-
let gfxdir2 = wa_malloc_zeroed(0x70C);
|
| 978 |
-
*(gfxdir2 as *mut u16) = 1;
|
| 979 |
-
*(gfxdir2.add(2) as *mut u16) = 0x5A;
|
| 980 |
-
// FUN_5411A0: usercall(EAX=gfxdir2), plain RET
|
| 981 |
-
call_usercall_eax(gfxdir2 as *mut DDGameWrapper, rb(va::PALETTE_CONTEXT_INIT));
|
| 982 |
-
*(gfxdir2.add(0x708) as *mut u16) = 0;
|
| 983 |
-
(*ddgame).secondary_gfxdir = gfxdir2;
|
| 984 |
-
// param4=0 so the ESI-dependent block is skipped; layer_ctx doesn't matter
|
| 985 |
-
wa_load_sprites(
|
| 986 |
-
wrapper,
|
| 987 |
-
(*ddgame).gfx_sprite_data.as_mut_ptr(),
|
| 988 |
-
(*game_info).display_flags,
|
| 989 |
-
0,
|
| 990 |
-
core::ptr::null_mut(),
|
| 991 |
-
);
|
| 992 |
-
}
|
| 993 |
-
|
| 994 |
-
// ββ DDGameWrapper field inits ββ
|
| 995 |
-
(*wrapper).loading_progress = 0;
|
| 996 |
-
if is_headless {
|
| 997 |
-
(*wrapper).loading_total = 0x2AD;
|
| 998 |
-
} else {
|
| 999 |
-
let team_count = (*game_info).speech_team_count as u32;
|
| 1000 |
-
(*wrapper).loading_total = team_count * 0x38 + 0x7E + 0x2AD;
|
| 1001 |
-
}
|
| 1002 |
-
(*wrapper).loading_last_pct = 0xFFFFFF9C; // -100: forces first progress bar update
|
| 1003 |
-
(*wrapper).speech_name_count = 0;
|
| 1004 |
-
|
| 1005 |
-
// ββ Audio init (non-headless + sound available) ββ
|
| 1006 |
-
if !is_headless {
|
| 1007 |
-
// FUN_570F30: usercall(ESI=wrapper)
|
| 1008 |
-
call_usercall_esi(wrapper, FUN_570F30_ADDR);
|
| 1009 |
-
if !(*ddgame).sound.is_null() {
|
| 1010 |
-
wa_load_effect_wavs(wrapper);
|
| 1011 |
-
// DSSound_LoadAllSpeechBanks: the original is hooked to our Rust
|
| 1012 |
-
// replacement (speech.rs), so the usercall bridge calls our code.
|
| 1013 |
-
call_usercall_esi(wrapper, LOAD_SPEECH_BANKS_ADDR);
|
| 1014 |
-
// Allocate ActiveSoundTable (0x608 bytes)
|
| 1015 |
-
let ast = wa_malloc(0x608) as *mut ActiveSoundTable;
|
| 1016 |
-
core::ptr::write_bytes(ast as *mut u8, 0, 0x600);
|
| 1017 |
-
(*ast).ddgame = ddgame;
|
| 1018 |
-
(*ast).counter = 0;
|
| 1019 |
-
(*ddgame).active_sounds = ast;
|
| 1020 |
-
}
|
| 1021 |
-
}
|
| 1022 |
-
|
| 1023 |
-
// ββ GfxResource for masks.img ββ
|
| 1024 |
-
// The original constructor uses a stack-local PaletteContext buffer (ESP+0x1A4)
|
| 1025 |
-
// initialized at the start with (word=1, word=0xFE, PaletteContext__Init).
|
| 1026 |
-
// This same buffer is reused as the output param for GfxResource__Create(masks.img).
|
| 1027 |
-
// We replicate this initialization here. Size 0x900 matches the stack allocation.
|
| 1028 |
-
let gfx_resource: *mut u8;
|
| 1029 |
-
{
|
| 1030 |
-
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 1031 |
-
// Create PaletteContext the same way the original DDGame constructor does:
|
| 1032 |
-
// word[0]=1, word[1]=0xFE, then PaletteContext__Init (0x5411A0)
|
| 1033 |
-
let palette_ctx = wa_malloc_zeroed(0x900);
|
| 1034 |
-
*(palette_ctx as *mut u16) = 1;
|
| 1035 |
-
*(palette_ctx.add(2) as *mut u16) = 0xFE;
|
| 1036 |
-
call_usercall_eax(palette_ctx as *mut DDGameWrapper, rb(va::PALETTE_CONTEXT_INIT));
|
| 1037 |
-
gfx_resource =
|
| 1038 |
-
gfx_resource_create(gfx_dir, rb(va::STR_MASKS_IMG) as *const c_char, palette_ctx);
|
| 1039 |
-
}
|
| 1040 |
-
|
| 1041 |
-
// ββ Dump GfxResource for A/B comparison ββ
|
| 1042 |
-
if !gfx_resource.is_null() {
|
| 1043 |
-
let use_orig = std::env::var("OPENWA_USE_ORIG_CTOR").is_ok();
|
| 1044 |
-
let tag = if use_orig { "orig" } else { "rust" };
|
| 1045 |
-
// Dump GfxResource object + first sub-object
|
| 1046 |
-
let gr_data = core::slice::from_raw_parts(gfx_resource, 0x100);
|
| 1047 |
-
let _ = std::fs::write(format!("gfx_resource_{}.bin", tag), gr_data);
|
| 1048 |
-
}
|
| 1049 |
-
|
| 1050 |
-
// ββ PCLandscape (alloc 0xB44, stdcall 11 params) ββ
|
| 1051 |
-
// Temporary output buffers for landscape coordinate data (used later for coord_list).
|
| 1052 |
-
// These were stack locals in the original code (aiStack_978, iStack_11f9).
|
| 1053 |
-
let mut landscape_coords_buf = [0u32; 0x400]; // coord output: pairs of (x, y)
|
| 1054 |
-
let mut landscape_byte_buf = [0u8; 0x100]; // byte output
|
| 1055 |
-
let mut landscape_temp = [0u32; 0x400]; // [0] = coord count, rest = temp data
|
| 1056 |
-
|
| 1057 |
-
let landscape = {
|
| 1058 |
-
let alloc = wa_malloc_zeroed(0xB44);
|
| 1059 |
-
if !alloc.is_null() {
|
| 1060 |
-
let result = wa_pc_landscape_ctor(
|
| 1061 |
-
alloc,
|
| 1062 |
-
ddgame,
|
| 1063 |
-
gfx_resource,
|
| 1064 |
-
(*wrapper).display,
|
| 1065 |
-
(*game_info).landscape_data_path.as_ptr(),
|
| 1066 |
-
landscape_byte_buf.as_mut_ptr(),
|
| 1067 |
-
(*wrapper).gfx_mode,
|
| 1068 |
-
landscape_temp.as_mut_ptr(),
|
| 1069 |
-
landscape_coords_buf.as_mut_ptr(),
|
| 1070 |
-
&raw mut (*ddgame).level_width_raw,
|
| 1071 |
-
&raw mut (*ddgame).level_height_raw,
|
| 1072 |
-
);
|
| 1073 |
-
(*wrapper).landscape = result as *mut PCLandscape;
|
| 1074 |
-
(*ddgame).landscape = result as *mut PCLandscape;
|
| 1075 |
-
result
|
| 1076 |
-
} else {
|
| 1077 |
-
(*wrapper).landscape = core::ptr::null_mut();
|
| 1078 |
-
(*ddgame).landscape = core::ptr::null_mut();
|
| 1079 |
-
core::ptr::null_mut()
|
| 1080 |
-
}
|
| 1081 |
-
};
|
| 1082 |
-
|
| 1083 |
-
// ββ BitGrid at DDGame+0x380 (alloc 0x4C, memset 0x2C) ββ
|
| 1084 |
-
// Pure Rust: allocate object, call bit_grid_init, override vtable.
|
| 1085 |
-
{
|
| 1086 |
-
let bit_grid = wa_malloc(0x4C);
|
| 1087 |
-
core::ptr::write_bytes(bit_grid, 0, 0x2C);
|
| 1088 |
-
if !bit_grid.is_null() {
|
| 1089 |
-
let width = (*ddgame).level_width;
|
| 1090 |
-
let height = (*ddgame).level_height;
|
| 1091 |
-
bit_grid_init(bit_grid, 1, width, height);
|
| 1092 |
-
*(bit_grid as *mut u32) = rb(va::BIT_GRID_VARIANT_VTABLE);
|
| 1093 |
-
}
|
| 1094 |
-
(*ddgame).bit_grid = bit_grid;
|
| 1095 |
-
}
|
| 1096 |
-
|
| 1097 |
-
// ββ 8Γ SpriteRegion at DDGame+0x46C..0x488 ββ
|
| 1098 |
-
// SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18
|
| 1099 |
-
{
|
| 1100 |
-
// (array_index, ECX, EDX, p2, p3, p4, p5, p6=gfx_resource)
|
| 1101 |
-
// p6 is the GfxResource pointer returned by GfxResource__Create_Maybe.
|
| 1102 |
-
let gr = gfx_resource as u32;
|
| 1103 |
-
let regions: [(usize, u32, u32, u32, u32, u32, u32, u32); 8] = [
|
| 1104 |
-
(2, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, gr),
|
| 1105 |
-
(0, 0x30, 0x0C, 0x2D, 0x07, 0x34, 0x09, gr),
|
| 1106 |
-
(1, 0x11, 0x1A, 0x0D, 0x0A, 0x16, 0x13, gr),
|
| 1107 |
-
(3, 0x0C, 0x3D, 0x00, 0x20, 0x18, 0x33, gr),
|
| 1108 |
-
(4, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, gr),
|
| 1109 |
-
(6, 0x1A2, 0x1B, 0x173, 0x09, 0x1D8, 0x03, gr),
|
| 1110 |
-
(7, 0x1EF, 0x26, 0x1E5, 0x07, 0x1F9, 0x16, gr),
|
| 1111 |
-
(5, 0x2D, 0x08, 0x2D, 0x07, 0x2E, 0x07, gr),
|
| 1112 |
-
];
|
| 1113 |
-
|
| 1114 |
-
for &(idx, ecx, edx, p2, p3, p4, p5, p6) in ®ions {
|
| 1115 |
-
let alloc = wa_malloc_zeroed(0x9C);
|
| 1116 |
-
let result = if !alloc.is_null() {
|
| 1117 |
-
call_sprite_region_ctor(alloc, ecx, edx, p2, p3, p4, p5, p6)
|
| 1118 |
-
} else {
|
| 1119 |
-
core::ptr::null_mut()
|
| 1120 |
-
};
|
| 1121 |
-
(*ddgame).sprite_regions[idx] = result;
|
| 1122 |
-
}
|
| 1123 |
-
}
|
| 1124 |
-
|
| 1125 |
-
// ββ Landscape property at DDGame+0x468 (PCLandscape vtable[0xB]) ββ
|
| 1126 |
-
if !landscape.is_null() {
|
| 1127 |
-
let land_vt = *(landscape as *const *const u32);
|
| 1128 |
-
let get_val: unsafe extern "thiscall" fn(*mut u8) -> u32 =
|
| 1129 |
-
core::mem::transmute(*land_vt.add(0xB));
|
| 1130 |
-
(*ddgame).landscape_property = get_val(landscape);
|
| 1131 |
-
}
|
| 1132 |
-
|
| 1133 |
-
// NOTE: gfx_resource is NOT released here β arrow SpriteRegions need it.
|
| 1134 |
-
// It's released after the arrow loop below.
|
| 1135 |
-
|
| 1136 |
-
// ββ Arrow sprites + collision regions (32 iterations) ββ
|
| 1137 |
-
{
|
| 1138 |
-
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 1139 |
-
|
| 1140 |
-
for i in 0u32..32 {
|
| 1141 |
-
// Format "arrow%02u.img\0" into stack buffer
|
| 1142 |
-
let mut name_buf = *b"arrow00.img\0\0\0\0\0";
|
| 1143 |
-
name_buf[5] = b'0' + (i / 10) as u8;
|
| 1144 |
-
name_buf[6] = b'0' + (i % 10) as u8;
|
| 1145 |
-
|
| 1146 |
-
let layer_ctx = (*(*ddgame).display).set_active_layer(1);
|
| 1147 |
-
|
| 1148 |
-
let entry = gfx_dir_find_entry(name_buf.as_ptr().cast(), gfx_dir);
|
| 1149 |
-
|
| 1150 |
-
let sprite: *mut u8;
|
| 1151 |
-
if !entry.is_null() {
|
| 1152 |
-
// Try gfx_dir->vtable[2](entry->field_4)
|
| 1153 |
-
let gfx_vt = *(gfx_dir as *const *const u32);
|
| 1154 |
-
let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
|
| 1155 |
-
core::mem::transmute(*gfx_vt.add(2));
|
| 1156 |
-
let entry_val = *(entry.add(4) as *const u32);
|
| 1157 |
-
let cached = load_cached(gfx_dir, entry_val);
|
| 1158 |
-
if !cached.is_null() {
|
| 1159 |
-
sprite = wa_displaygfx_ctor(cached);
|
| 1160 |
-
} else {
|
| 1161 |
-
// Fallback: load from file via GfxDir__LoadImage + IMG_Decode
|
| 1162 |
-
sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx);
|
| 1163 |
-
}
|
| 1164 |
-
} else {
|
| 1165 |
-
// Entry not found β try direct file load
|
| 1166 |
-
sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx);
|
| 1167 |
-
}
|
| 1168 |
-
|
| 1169 |
-
// Store arrow sprite at DDGame+0x38+i*4
|
| 1170 |
-
(*ddgame).arrow_sprites[i as usize] = sprite;
|
| 1171 |
-
|
| 1172 |
-
// Calculate collision region dimensions from sprite.
|
| 1173 |
-
// The original creates a CENTERED collision box with 10px margin:
|
| 1174 |
-
// left/top margin = max(0, dim/2 - 10)
|
| 1175 |
-
// right/bottom = dim - margin
|
| 1176 |
-
// Result: this[5]=this[6]=10 for sprites >20px (the 10px inset).
|
| 1177 |
-
if !sprite.is_null() {
|
| 1178 |
-
let tsm = &*(sprite as *const BitGrid);
|
| 1179 |
-
let sprite_w = tsm.width as i32;
|
| 1180 |
-
let sprite_h = tsm.height as i32;
|
| 1181 |
-
let margin_w = (sprite_w / 2 - 10).max(0);
|
| 1182 |
-
let margin_h = (sprite_h / 2 - 10).max(0);
|
| 1183 |
-
|
| 1184 |
-
// Create SpriteRegion for collision
|
| 1185 |
-
let alloc = wa_malloc_zeroed(0x9C);
|
| 1186 |
-
let region = if !alloc.is_null() {
|
| 1187 |
-
call_sprite_region_ctor(
|
| 1188 |
-
alloc,
|
| 1189 |
-
(sprite_w / 2) as u32, // ECX β this[5] = ECX - p2
|
| 1190 |
-
(sprite_h - margin_h) as u32, // EDX β this[4] = EDX - p3
|
| 1191 |
-
margin_w as u32, // p2 (left margin)
|
| 1192 |
-
margin_h as u32, // p3 (top margin)
|
| 1193 |
-
(sprite_w - margin_w) as u32, // p4 β this[3] = p4 - p2
|
| 1194 |
-
(sprite_h / 2) as u32, // p5 β this[6] = p5 - p3
|
| 1195 |
-
sprite as u32, // p6 = arrow sprite (NOT landscape gfx_resource)
|
| 1196 |
-
)
|
| 1197 |
-
} else {
|
| 1198 |
-
core::ptr::null_mut()
|
| 1199 |
-
};
|
| 1200 |
-
(*ddgame).arrow_collision_regions[i as usize] = region;
|
| 1201 |
-
}
|
| 1202 |
-
|
| 1203 |
-
// Arrow GfxDir (conditional on secondary gfxdir)
|
| 1204 |
-
if !(*ddgame).secondary_gfxdir.is_null() {
|
| 1205 |
-
let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
|
| 1206 |
-
core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
|
| 1207 |
-
(*ddgame).arrow_gfxdirs[i as usize] =
|
| 1208 |
-
gfx_resource_create(gfx_dir, core::ptr::null_mut());
|
| 1209 |
-
}
|
| 1210 |
-
}
|
| 1211 |
-
}
|
| 1212 |
-
|
| 1213 |
-
// Release gfx_resource AFTER arrow loop (arrows need it for SpriteRegions)
|
| 1214 |
-
// vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
|
| 1215 |
-
if !gfx_resource.is_null() {
|
| 1216 |
-
let rvt = *(gfx_resource as *const *const u32);
|
| 1217 |
-
let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*rvt.add(3));
|
| 1218 |
-
release(gfx_resource, 1);
|
| 1219 |
-
}
|
| 1220 |
-
|
| 1221 |
-
// ββ DisplayGfx at DDGame+0x138 ββ
|
| 1222 |
-
{
|
| 1223 |
-
let tsm = wa_malloc(0x4C);
|
| 1224 |
-
core::ptr::write_bytes(tsm, 0, 0x2C);
|
| 1225 |
-
if !tsm.is_null() {
|
| 1226 |
-
bit_grid_init(tsm, 8, 0x100, 0x1E0);
|
| 1227 |
-
*(tsm as *mut u32) = rb(va::DISPLAY_GFX_VTABLE);
|
| 1228 |
-
}
|
| 1229 |
-
(*ddgame).display_gfx = tsm;
|
| 1230 |
-
}
|
| 1231 |
-
|
| 1232 |
-
// ββ CoordList at DDGame+0x50C (capacity 600, 0x12C0 buffer) ββ
|
| 1233 |
-
{
|
| 1234 |
-
let cl = wa_malloc(core::mem::size_of::<CoordList>() as u32) as *mut CoordList;
|
| 1235 |
-
(*cl).count = 0;
|
| 1236 |
-
(*cl).capacity = 600;
|
| 1237 |
-
let data = wa_malloc_zeroed(600 * core::mem::size_of::<CoordListEntry>() as u32)
|
| 1238 |
-
as *mut CoordListEntry;
|
| 1239 |
-
(*cl).data = data;
|
| 1240 |
-
(*ddgame).coord_list = cl;
|
| 1241 |
-
|
| 1242 |
-
// Populate coord_list from PCLandscape's coordinate output.
|
| 1243 |
-
// landscape_temp[0] = coordinate count, landscape_coords_buf = pairs of (x, y).
|
| 1244 |
-
// Original packs as: coord = x * 0x10000 + y (Fixed-point).
|
| 1245 |
-
// Duplicates are skipped.
|
| 1246 |
-
let coord_count = landscape_temp[0];
|
| 1247 |
-
for j in 0..coord_count as usize {
|
| 1248 |
-
let x = landscape_coords_buf[j * 2];
|
| 1249 |
-
let y = landscape_coords_buf[j * 2 + 1];
|
| 1250 |
-
let coord_val = x.wrapping_mul(0x10000).wrapping_add(y);
|
| 1251 |
-
let cur_count = (*cl).count as usize;
|
| 1252 |
-
if cur_count >= 600 {
|
| 1253 |
-
break;
|
| 1254 |
-
}
|
| 1255 |
-
// Check for duplicates
|
| 1256 |
-
let mut dup = false;
|
| 1257 |
-
for k in 0..cur_count {
|
| 1258 |
-
if (*data.add(k)).coord == coord_val {
|
| 1259 |
-
dup = true;
|
| 1260 |
-
break;
|
| 1261 |
-
}
|
| 1262 |
-
}
|
| 1263 |
-
if !dup {
|
| 1264 |
-
(*data.add(cur_count)).coord = coord_val;
|
| 1265 |
-
(*data.add(cur_count)).flag = 1;
|
| 1266 |
-
(*cl).count = (cur_count + 1) as u32;
|
| 1267 |
-
}
|
| 1268 |
-
}
|
| 1269 |
-
}
|
| 1270 |
-
|
| 1271 |
-
// Temporary landscape buffers (stack arrays) are dropped automatically here.
|
| 1272 |
-
|
| 1273 |
-
// ββ Loading progress ticks (2 of 4 β before load_resource_list) ββ
|
| 1274 |
-
call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
|
| 1275 |
-
call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
|
| 1276 |
-
|
| 1277 |
-
// ββ Sprite resource loading via DDGameWrapper vtable[0] ββ
|
| 1278 |
-
// DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
|
| 1279 |
-
// 5 stack params (layer, gfx_dir, base_path, data_table, table_size)
|
| 1280 |
-
{
|
| 1281 |
-
let landscape_ptr = (*wrapper).landscape;
|
| 1282 |
-
let water_layer = (*landscape_ptr).water_gfx_dir;
|
| 1283 |
-
let land_layer = (*landscape_ptr).level_gfx_dir;
|
| 1284 |
-
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 1285 |
-
|
| 1286 |
-
let wrapper_vt = *(wrapper as *const *const u32);
|
| 1287 |
-
let load_resource_list: unsafe extern "thiscall" fn(
|
| 1288 |
-
*mut DDGameWrapper,
|
| 1289 |
-
u32,
|
| 1290 |
-
*mut u8,
|
| 1291 |
-
*const u8,
|
| 1292 |
-
*const u8,
|
| 1293 |
-
u32,
|
| 1294 |
-
) = core::mem::transmute(*wrapper_vt);
|
| 1295 |
-
// Load resources for layer 1 (main sprites)
|
| 1296 |
-
load_resource_list(
|
| 1297 |
-
wrapper,
|
| 1298 |
-
1,
|
| 1299 |
-
gfx_dir,
|
| 1300 |
-
rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8, // base path
|
| 1301 |
-
rb(va::SPRITE_RESOURCE_TABLE_1) as *const u8,
|
| 1302 |
-
0x1D88, // table size
|
| 1303 |
-
);
|
| 1304 |
-
// Set global flag based on game version
|
| 1305 |
-
let gv = (*(*ddgame).game_info).game_version;
|
| 1306 |
-
*(rb(va::G_SPRITE_VERSION_FLAG) as *mut u32) = if gv < 8 { 0 } else { 0x10 };
|
| 1307 |
-
|
| 1308 |
-
// Load resources for layer 1 with different table
|
| 1309 |
-
load_resource_list(
|
| 1310 |
-
wrapper,
|
| 1311 |
-
1,
|
| 1312 |
-
gfx_dir,
|
| 1313 |
-
rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
|
| 1314 |
-
rb(va::SPRITE_RESOURCE_TABLE_2) as *const u8,
|
| 1315 |
-
0x18,
|
| 1316 |
-
);
|
| 1317 |
-
|
| 1318 |
-
// Load resources for layer 2 (water)
|
| 1319 |
-
load_resource_list(
|
| 1320 |
-
wrapper,
|
| 1321 |
-
2,
|
| 1322 |
-
water_layer,
|
| 1323 |
-
rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
|
| 1324 |
-
rb(va::WATER_RESOURCE_TABLE) as *const u8,
|
| 1325 |
-
0x2F4,
|
| 1326 |
-
);
|
| 1327 |
-
|
| 1328 |
-
let disp = (*wrapper).display;
|
| 1329 |
-
(*disp).set_active_layer(3);
|
| 1330 |
-
|
| 1331 |
-
// back.spr and debris.spr must be loaded unconditionally β they're used by
|
| 1332 |
-
// GenerateDebrisParticles (0x546F70) for particle effects, which affects
|
| 1333 |
-
// the game RNG (DDGame+0x45EC). The original constructor loads them even
|
| 1334 |
-
// in headless mode. Skipping them causes replay desync.
|
| 1335 |
-
(*disp).load_sprite_by_layer(
|
| 1336 |
-
3,
|
| 1337 |
-
0x26D,
|
| 1338 |
-
land_layer,
|
| 1339 |
-
c"back.spr".as_ptr().cast(),
|
| 1340 |
-
);
|
| 1341 |
-
// debris.spr must be loaded unconditionally β it's used by
|
| 1342 |
-
// GenerateDebrisParticles (0x546F70) for particle effects, which
|
| 1343 |
-
// affects the game RNG (DDGame+0x45EC). Skipping it in headless
|
| 1344 |
-
// mode causes desync (longbow replay checksum mismatch at frame 1350).
|
| 1345 |
-
(*disp).load_sprite(3, 0x26E, 0, land_layer, c"debris.spr".as_ptr());
|
| 1346 |
-
|
| 1347 |
-
(*disp).load_sprite_by_layer(
|
| 1348 |
-
2,
|
| 1349 |
-
0x26C,
|
| 1350 |
-
water_layer,
|
| 1351 |
-
c"layer\\layer.spr|layer.spr".as_ptr().cast(),
|
| 1352 |
-
);
|
| 1353 |
-
|
| 1354 |
-
(*ddgame).gradient_image_2 = core::ptr::null_mut();
|
| 1355 |
-
|
| 1356 |
-
// ββ Gradient image (0x030) ββ
|
| 1357 |
-
let level_height = (*ddgame).level_height as i32;
|
| 1358 |
-
let layer3_ctx = (*disp).set_active_layer(3);
|
| 1359 |
-
let s_var1 = *(layer3_ctx.add(0x606) as *const i16);
|
| 1360 |
-
|
| 1361 |
-
if s_var1 < 0x61 && level_height == 0x2B8 {
|
| 1362 |
-
// Simple gradient: load gradient.img directly
|
| 1363 |
-
let gradient = call_gfx_find_and_load(land_layer, c"gradient.img", layer3_ctx);
|
| 1364 |
-
(*ddgame).gradient_image = gradient;
|
| 1365 |
-
} else {
|
| 1366 |
-
compute_complex_gradient(ddgame, land_layer, layer3_ctx, s_var1);
|
| 1367 |
-
}
|
| 1368 |
-
|
| 1369 |
-
// ββ Fill image β fill_pixel (0x7338) ββ
|
| 1370 |
-
{
|
| 1371 |
-
let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
|
| 1372 |
-
// In the original, fill.img uses piStack_126c which the decompiler
|
| 1373 |
-
// shows was set from piVar3 (water_layer from landscape+0xB38).
|
| 1374 |
-
let fill_sprite = call_gfx_find_and_load(water_layer, c"fill.img", layer2_ctx);
|
| 1375 |
-
if !fill_sprite.is_null() {
|
| 1376 |
-
// Get pixel value: fill_sprite->vtable[4](0, 0)
|
| 1377 |
-
let fill_vt = *(fill_sprite as *const *const u32);
|
| 1378 |
-
let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
|
| 1379 |
-
core::mem::transmute(*fill_vt.add(4));
|
| 1380 |
-
(*ddgame).fill_pixel = get_pixel(fill_sprite, 0, 0);
|
| 1381 |
-
// Release fill sprite: vtable[3] = DisplayGfx__vmethod_3(this, param_2=1)
|
| 1382 |
-
let release: unsafe extern "thiscall" fn(*mut u8, u8) =
|
| 1383 |
-
core::mem::transmute(*fill_vt.add(3));
|
| 1384 |
-
release(fill_sprite, 1);
|
| 1385 |
-
}
|
| 1386 |
-
}
|
| 1387 |
-
|
| 1388 |
-
// ββ DDGame__LoadHudAndWeaponSprites (0x53D0E0) ββ
|
| 1389 |
-
wa_load_hud_sprites(gfx_dir, ddgame, (*wrapper).secondary_gfx_dir);
|
| 1390 |
-
|
| 1391 |
-
// ββ DDGame__InitPaletteGradientSprites (0x5706D0) ββ
|
| 1392 |
-
// Creates DisplayGfx objects at DDGame+0x41C.. for each team's palette
|
| 1393 |
-
// gradient data from GameInfo. stdcall(wrapper), RET 0x4.
|
| 1394 |
-
wa_init_palette_gradient_sprites(wrapper);
|
| 1395 |
-
|
| 1396 |
-
// ββ DisplayGfx__InitTeamPaletteDisplayObjects (0x5703E0) ββ
|
| 1397 |
-
// Creates team palette gradient display objects. Reads DDGame+0x7338
|
| 1398 |
-
// (fill_pixel), creates BitGrid+DisplayGfx per team.
|
| 1399 |
-
// stdcall(wrapper), RET 0x4.
|
| 1400 |
-
let init_team_palette_display: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 1401 |
-
core::mem::transmute(rb(va::DISPLAY_GFX_INIT_TEAM_PALETTE_DISPLAY));
|
| 1402 |
-
init_team_palette_display(wrapper);
|
| 1403 |
-
}
|
| 1404 |
-
// ββ Gradient image stub (DDGame+0x30) ββ
|
| 1405 |
-
// Minimal stub: [6]=0 (zero-width) so CTaskLand skips the gradient column loop.
|
| 1406 |
-
if (*ddgame).gradient_image.is_null() {
|
| 1407 |
-
let obj = wa_malloc_zeroed(core::mem::size_of::<BitGrid>() as u32) as *mut BitGrid;
|
| 1408 |
-
if !obj.is_null() {
|
| 1409 |
-
(*obj).vtable = rb(va::BIT_GRID_VTABLE);
|
| 1410 |
-
// height = 0 β CTaskLand skips the gradient column loop
|
| 1411 |
-
(*ddgame).gradient_image = obj as *mut u8;
|
| 1412 |
-
}
|
| 1413 |
-
}
|
| 1414 |
-
|
| 1415 |
-
// ββ Release primary GfxHandler (vtable[3] = release, param 1 = free) ββ
|
| 1416 |
-
let gfx_dir_4c0 = (*wrapper).primary_gfx_dir;
|
| 1417 |
-
if !gfx_dir_4c0.is_null() {
|
| 1418 |
-
let gfx_vt = *(gfx_dir_4c0 as *const *const u32);
|
| 1419 |
-
let release: unsafe extern "thiscall" fn(*mut u8, u32) =
|
| 1420 |
-
core::mem::transmute(*gfx_vt.add(3));
|
| 1421 |
-
release(gfx_dir_4c0, 1);
|
| 1422 |
-
}
|
| 1423 |
-
|
| 1424 |
-
if !is_headless {
|
| 1425 |
-
wa_init_display_final((*wrapper).display);
|
| 1426 |
-
}
|
| 1427 |
-
|
| 1428 |
-
// ββ FUN_00570A90 (second call, conditional) ββ
|
| 1429 |
-
if *(rb(va::G_DISPLAY_MODE_FLAG) as *const c_char) == 0 {
|
| 1430 |
-
call_usercall_eax(wrapper, FUN_570A90_ADDR);
|
| 1431 |
-
}
|
| 1432 |
-
|
| 1433 |
-
// ββ Final display layer visibility ββ
|
| 1434 |
-
{
|
| 1435 |
-
let disp = (*wrapper).display;
|
| 1436 |
-
(*disp).set_layer_visibility(1, 0);
|
| 1437 |
-
(*disp).set_layer_visibility(2, 0);
|
| 1438 |
-
(*disp).set_layer_visibility(3, 1);
|
| 1439 |
-
}
|
| 1440 |
-
|
| 1441 |
-
let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
|
| 1442 |
-
}
|
| 1443 |
-
|
| 1444 |
-
// Statics for usercall bridge addresses
|
| 1445 |
-
static mut FUN_570A90_ADDR: u32 = 0;
|
| 1446 |
-
static mut FUN_570F30_ADDR: u32 = 0;
|
| 1447 |
-
static mut LOAD_SPEECH_BANKS_ADDR: u32 = 0;
|
| 1448 |
-
static mut LOADING_PROGRESS_TICK_ADDR: u32 = 0;
|
| 1449 |
-
|
| 1450 |
-
/// Bridge: usercall(ESI=wrapper), plain RET. Used by FUN_570E20, FUN_570F30, LoadSpeechBanks.
|
| 1451 |
-
#[cfg(target_arch = "x86")]
|
| 1452 |
-
#[unsafe(naked)]
|
| 1453 |
-
unsafe extern "C" fn call_usercall_esi(_wrapper: *mut DDGameWrapper, _addr: u32) {
|
| 1454 |
-
core::arch::naked_asm!(
|
| 1455 |
-
"pushl %esi",
|
| 1456 |
-
"movl 8(%esp), %esi", // ESI = wrapper
|
| 1457 |
-
"movl 12(%esp), %eax", // EAX = target address
|
| 1458 |
-
"calll *%eax",
|
| 1459 |
-
"popl %esi",
|
| 1460 |
-
"retl",
|
| 1461 |
-
options(att_syntax),
|
| 1462 |
-
);
|
| 1463 |
-
}
|
| 1464 |
-
|
| 1465 |
-
/// Bridge: usercall(EAX=wrapper), plain RET. Used by FUN_570A90.
|
| 1466 |
-
#[cfg(target_arch = "x86")]
|
| 1467 |
-
#[unsafe(naked)]
|
| 1468 |
-
unsafe extern "C" fn call_usercall_eax(_wrapper: *mut DDGameWrapper, _addr: u32) {
|
| 1469 |
-
core::arch::naked_asm!(
|
| 1470 |
-
"movl 4(%esp), %eax", // EAX = wrapper
|
| 1471 |
-
"movl 8(%esp), %ecx", // ECX = target address (temp)
|
| 1472 |
-
"calll *%ecx",
|
| 1473 |
-
"retl",
|
| 1474 |
-
options(att_syntax),
|
| 1475 |
-
);
|
| 1476 |
-
}
|
| 1477 |
-
|
| 1478 |
-
/// Bridge: usercall(ECX=wrapper), plain RET. Used by FUN_5717A0.
|
| 1479 |
-
#[cfg(target_arch = "x86")]
|
| 1480 |
-
unsafe fn call_usercall_ecx(wrapper: *mut DDGameWrapper, addr: u32) {
|
| 1481 |
-
let f: unsafe extern "thiscall" fn(*mut DDGameWrapper) = core::mem::transmute(addr as usize);
|
| 1482 |
-
f(wrapper);
|
| 1483 |
-
}
|
| 1484 |
-
|
| 1485 |
-
/// Bridge to SpriteRegion__Constructor (0x57DB20).
|
| 1486 |
-
/// Convention: fastcall(ECX, EDX) + 6 stack params, RET 0x18.
|
| 1487 |
-
#[cfg(target_arch = "x86")]
|
| 1488 |
-
static mut SPRITE_REGION_CTOR_ADDR: u32 = 0;
|
| 1489 |
-
|
| 1490 |
-
#[cfg(target_arch = "x86")]
|
| 1491 |
-
#[unsafe(naked)]
|
| 1492 |
-
unsafe extern "C" fn call_sprite_region_ctor(
|
| 1493 |
-
_this: *mut u8,
|
| 1494 |
-
_ecx: u32,
|
| 1495 |
-
_edx: u32,
|
| 1496 |
-
_p2: u32,
|
| 1497 |
-
_p3: u32,
|
| 1498 |
-
_p4: u32,
|
| 1499 |
-
_p5: u32,
|
| 1500 |
-
_p6: u32,
|
| 1501 |
-
) -> *mut u8 {
|
| 1502 |
-
// Stack on entry: [ret] [this] [ecx] [edx] [p2] [p3] [p4] [p5] [p6]
|
| 1503 |
-
// Need: ECX=ecx, EDX=edx, push p6 p5 p4 p3 p2 this, call
|
| 1504 |
-
// SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack, RET 0x18
|
| 1505 |
-
// Params: (this, ecx, edx, p2, p3, p4, p5, p6)
|
| 1506 |
-
core::arch::naked_asm!(
|
| 1507 |
-
"pushl %ebp",
|
| 1508 |
-
"pushl %ebx",
|
| 1509 |
-
// Load ECX and EDX (shifted by 2 pushes = 8 bytes)
|
| 1510 |
-
"movl 16(%esp), %ecx", // ecx param (offset 4+8=12? No: 0=ebx,4=ebp,8=ret,12=this,16=ecx)
|
| 1511 |
-
"movl 20(%esp), %edx", // edx param
|
| 1512 |
-
// Push 6 stack params in reverse: p6, p5, p4, p3, p2, this
|
| 1513 |
-
"pushl 40(%esp)", // p6 (0=ebx,4=ebp,8=ret,...,40=p6)
|
| 1514 |
-
"pushl 40(%esp)", // p5 (shifted by 4)
|
| 1515 |
-
"pushl 40(%esp)", // p4 (shifted by 8)
|
| 1516 |
-
"pushl 40(%esp)", // p3 (shifted by 12)
|
| 1517 |
-
"pushl 40(%esp)", // p2 (shifted by 16)
|
| 1518 |
-
"pushl 32(%esp)", // this (shifted by 20)
|
| 1519 |
-
"calll *({addr})", // fastcall, callee cleans 6Γ4=24 bytes
|
| 1520 |
-
"popl %ebx",
|
| 1521 |
-
"popl %ebp",
|
| 1522 |
-
"retl",
|
| 1523 |
-
addr = sym SPRITE_REGION_CTOR_ADDR,
|
| 1524 |
-
options(att_syntax),
|
| 1525 |
-
);
|
| 1526 |
-
}
|
| 1527 |
-
|
| 1528 |
/// Well-known byte offsets into DDGame, for use with raw pointer access.
|
| 1529 |
///
|
| 1530 |
/// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
|
|
@@ -1641,533 +558,6 @@ pub mod offsets {
|
|
| 1641 |
pub const SOUND_QUEUE_COUNT: usize = 0x8140;
|
| 1642 |
}
|
| 1643 |
|
| 1644 |
-
/// Coordinate entry used in DDGame screen coordinate tables (stride 0x10).
|
| 1645 |
-
///
|
| 1646 |
-
/// InitFields zeroes x and y; at runtime they contain fixed-point screen
|
| 1647 |
-
/// coordinates used for camera tracking and rendering regions.
|
| 1648 |
-
#[repr(C)]
|
| 1649 |
-
#[derive(Clone, Copy)]
|
| 1650 |
-
pub struct CoordEntry {
|
| 1651 |
-
pub x: i32,
|
| 1652 |
-
pub y: i32,
|
| 1653 |
-
pub _unknown: [u8; 8],
|
| 1654 |
-
}
|
| 1655 |
-
|
| 1656 |
-
const _: () = assert!(core::mem::size_of::<CoordEntry>() == 0x10);
|
| 1657 |
-
|
| 1658 |
-
/// Team index permutation map (0x64 = 100 bytes).
|
| 1659 |
-
///
|
| 1660 |
-
/// Used for mapping team indices to rendering/turn slots. Three instances
|
| 1661 |
-
/// live in DDGame at offsets 0x7650, 0x76B4, 0x7718 (stride 0x64).
|
| 1662 |
-
/// Initialized as identity permutations [0,1,2,...,15] with count=16.
|
| 1663 |
-
#[repr(C)]
|
| 1664 |
-
#[derive(Clone, Copy)]
|
| 1665 |
-
pub struct TeamIndexMap {
|
| 1666 |
-
/// 16 team index entries (identity permutation on init).
|
| 1667 |
-
pub entries: [i16; 16],
|
| 1668 |
-
/// Number of active entries (initialized to 16).
|
| 1669 |
-
pub count: u16,
|
| 1670 |
-
/// Gap β unknown purpose, not initialized by constructor.
|
| 1671 |
-
pub _gap: [u8; 64],
|
| 1672 |
-
/// Terminator (initialized to 0).
|
| 1673 |
-
pub terminator: u16,
|
| 1674 |
-
}
|
| 1675 |
-
const _: () = assert!(core::mem::size_of::<TeamIndexMap>() == 0x64);
|
| 1676 |
-
|
| 1677 |
-
/// Render table entry (0x14 = 20 bytes).
|
| 1678 |
-
///
|
| 1679 |
-
/// 14 entries live at DDGame+0x73B0 (stride 0x14). Only the first u32
|
| 1680 |
-
/// is zeroed during construction; the rest is uninitialized/unknown.
|
| 1681 |
-
#[repr(C)]
|
| 1682 |
-
#[derive(Clone, Copy)]
|
| 1683 |
-
pub struct RenderEntry {
|
| 1684 |
-
/// Active/state flag (zeroed on init).
|
| 1685 |
-
pub active: u32,
|
| 1686 |
-
/// Unknown data.
|
| 1687 |
-
pub _unknown: [u8; 16],
|
| 1688 |
-
}
|
| 1689 |
-
const _: () = assert!(core::mem::size_of::<RenderEntry>() == 0x14);
|
| 1690 |
-
|
| 1691 |
-
// ============================================================
|
| 1692 |
-
// CoordList β dynamic array of packed terrain coordinates
|
| 1693 |
-
// ============================================================
|
| 1694 |
-
|
| 1695 |
-
/// Packed terrain coordinate entry (8 bytes).
|
| 1696 |
-
///
|
| 1697 |
-
/// `coord` packs x and y as `x * 0x10000 + y` (fixed-point).
|
| 1698 |
-
/// `flag` is always 1 for populated entries.
|
| 1699 |
-
#[repr(C)]
|
| 1700 |
-
#[derive(Clone, Copy)]
|
| 1701 |
-
pub struct CoordListEntry {
|
| 1702 |
-
pub coord: u32,
|
| 1703 |
-
pub flag: u32,
|
| 1704 |
-
}
|
| 1705 |
-
const _: () = assert!(core::mem::size_of::<CoordListEntry>() == 8);
|
| 1706 |
-
|
| 1707 |
-
/// Dynamic coordinate array header (12 bytes).
|
| 1708 |
-
///
|
| 1709 |
-
/// Allocated at DDGame+0x50C during `init_graphics_and_resources`.
|
| 1710 |
-
/// Data buffer is a separate allocation of `capacity * 8` bytes.
|
| 1711 |
-
/// Used for terrain coordinate lookups (spawning, aiming, collision).
|
| 1712 |
-
#[repr(C)]
|
| 1713 |
-
pub struct CoordList {
|
| 1714 |
-
/// Number of entries currently stored.
|
| 1715 |
-
pub count: u32,
|
| 1716 |
-
/// Maximum number of entries (600).
|
| 1717 |
-
pub capacity: u32,
|
| 1718 |
-
/// Pointer to the data buffer (`capacity * sizeof(CoordListEntry)` bytes).
|
| 1719 |
-
pub data: *mut CoordListEntry,
|
| 1720 |
-
}
|
| 1721 |
-
const _: () = assert!(core::mem::size_of::<CoordList>() == 12);
|
| 1722 |
-
|
| 1723 |
-
// ============================================================
|
| 1724 |
-
// Sound queue entry β 16 entries at DDGame + 0x7F00
|
| 1725 |
-
// ============================================================
|
| 1726 |
-
|
| 1727 |
-
/// Sound queue entry (0x24 = 36 bytes, stride between consecutive entries).
|
| 1728 |
-
///
|
| 1729 |
-
/// DDGame maintains a 16-slot sound queue at offset 0x7F00. PlaySoundGlobal
|
| 1730 |
-
/// appends entries; PlaySoundLocal additionally marks entries as local and
|
| 1731 |
-
/// stores position via the task's secondary vtable.
|
| 1732 |
-
#[repr(C)]
|
| 1733 |
-
pub struct SoundQueueEntry {
|
| 1734 |
-
/// Sound effect ID (SoundId enum value).
|
| 1735 |
-
pub sound_id: u32,
|
| 1736 |
-
/// Flags / priority (e.g. 3=weapon, 7=explosion).
|
| 1737 |
-
pub flags: u32,
|
| 1738 |
-
/// Volume (Fixed-point, 0x10000 = 1.0).
|
| 1739 |
-
pub volume: u32,
|
| 1740 |
-
/// Pitch (Fixed-point, 0x10000 = 1.0).
|
| 1741 |
-
pub pitch: u32,
|
| 1742 |
-
/// Reserved, always 0.
|
| 1743 |
-
pub reserved: u32,
|
| 1744 |
-
/// 0 = global, 1 = local (has position tracking).
|
| 1745 |
-
pub is_local: u8,
|
| 1746 |
-
pub _pad: [u8; 3],
|
| 1747 |
-
/// Position X (filled by secondary vtable GetPosition for local sounds).
|
| 1748 |
-
pub pos_x: u32,
|
| 1749 |
-
/// Position Y (filled by secondary vtable GetPosition for local sounds).
|
| 1750 |
-
pub pos_y: u32,
|
| 1751 |
-
/// Pointer to task's secondary vtable (at task+0xE8) for position updates.
|
| 1752 |
-
pub secondary_vtable: u32,
|
| 1753 |
-
}
|
| 1754 |
-
|
| 1755 |
-
const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
|
| 1756 |
-
|
| 1757 |
-
// ============================================================
|
| 1758 |
-
// Team arena state β sub-struct at DDGame + 0x4628
|
| 1759 |
-
// ============================================================
|
| 1760 |
-
|
| 1761 |
-
// ============================================================
|
| 1762 |
-
// Per-worm and per-team block structs
|
| 1763 |
-
// ============================================================
|
| 1764 |
-
|
| 1765 |
-
/// Per-worm data entry (0x9C bytes, stride between consecutive worms).
|
| 1766 |
-
///
|
| 1767 |
-
/// WA supports up to 8 playable worms per team. The original code accesses
|
| 1768 |
-
/// worms via raw pointer arithmetic from the team entry pointer, using
|
| 1769 |
-
/// stride 0x9C. This means the 8th worm crosses the TeamBlock boundary
|
| 1770 |
-
/// into the next block's header slot β see `TeamArenaRef::team_worm()`.
|
| 1771 |
-
///
|
| 1772 |
-
/// Field offsets confirmed by runtime memory dump (validator DLL):
|
| 1773 |
-
/// - state at 0x00: 0x67 = active/selected, 0x65 = idle, 0x80+ = special
|
| 1774 |
-
/// - active_flag at 0x0C: 1 for alive worms
|
| 1775 |
-
/// - max_health at 0x58: initial health value (100)
|
| 1776 |
-
/// - health at 0x5C: current health (100 = full)
|
| 1777 |
-
/// - name at 0x78: null-terminated worm name string (~20 bytes)
|
| 1778 |
-
#[repr(C)]
|
| 1779 |
-
#[derive(Clone, Copy)]
|
| 1780 |
-
pub struct WormEntry {
|
| 1781 |
-
/// 0x00: Worm state machine state.
|
| 1782 |
-
/// Values: 0x65=idle, 0x67=active/selected, 0x68=active variant.
|
| 1783 |
-
/// Special states {0x80..0x85, 0x89} = dying/drowning/special animation.
|
| 1784 |
-
pub state: u32,
|
| 1785 |
-
/// 0x04: Unknown counter. Incremented by Freeze (+10).
|
| 1786 |
-
pub effect_counter_04_Maybe: i32,
|
| 1787 |
-
/// 0x08: Turn action counter. Incremented by weapon handlers:
|
| 1788 |
-
/// Surrender (+14), Mail/Mine/Mole (+7), Freeze (+3).
|
| 1789 |
-
pub turn_action_counter_Maybe: i32,
|
| 1790 |
-
/// 0x0C: Active/alive flag (1 for alive worms in game, 0 otherwise).
|
| 1791 |
-
pub active_flag: i32,
|
| 1792 |
-
/// 0x10-0x57: Unknown
|
| 1793 |
-
pub _unknown_10: [u8; 0x48],
|
| 1794 |
-
/// 0x58: Max health (initial health value, typically 100).
|
| 1795 |
-
pub max_health: i32,
|
| 1796 |
-
/// 0x5C: Current health. Used by GetTeamTotalHealth.
|
| 1797 |
-
pub health: i32,
|
| 1798 |
-
/// 0x60-0x77: Unknown.
|
| 1799 |
-
pub _unknown_60: [u8; 0x18],
|
| 1800 |
-
/// 0x78: Worm name, null-terminated ASCII string (~20 bytes).
|
| 1801 |
-
pub name: [u8; 0x18],
|
| 1802 |
-
/// 0x90-0x9B: Unknown (zeroed in runtime dump for playable worms).
|
| 1803 |
-
/// GetWormPosition reads pos_x/pos_y from +0x90/+0x94 via negative entry_ptr
|
| 1804 |
-
/// arithmetic, but values appear transient β not populated at rest.
|
| 1805 |
-
/// Actual worm positions live in CGameTask objects (+0x84/+0x88).
|
| 1806 |
-
pub _unknown_90: [u8; 0x0C],
|
| 1807 |
-
}
|
| 1808 |
-
|
| 1809 |
-
const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
|
| 1810 |
-
|
| 1811 |
-
/// Team-level metadata stored at slot 0 of each TeamBlock (0x9C bytes).
|
| 1812 |
-
///
|
| 1813 |
-
/// This struct overlays the same memory as a WormEntry but interprets the
|
| 1814 |
-
/// high offsets (0x6C+) as team metadata rather than worm data. The low
|
| 1815 |
-
/// offsets (0x00-0x5F) may contain data from the previous team's 8th worm
|
| 1816 |
-
/// when that team has 8 worms β they are treated as opaque padding.
|
| 1817 |
-
///
|
| 1818 |
-
/// Accessed via `TeamArenaRef::team_header()` and `team_header_b()`.
|
| 1819 |
-
#[repr(C)]
|
| 1820 |
-
#[derive(Clone, Copy)]
|
| 1821 |
-
pub struct TeamHeader {
|
| 1822 |
-
/// 0x00-0x5F: Opaque β may hold 8th worm data from previous team.
|
| 1823 |
-
pub worm_overlap: [u8; 0x60],
|
| 1824 |
-
/// 0x60-0x6B: Unknown padding.
|
| 1825 |
-
pub _unknown_60: [u8; 0x0C],
|
| 1826 |
-
/// 0x6C: Team eliminated flag (nonzero = eliminated).
|
| 1827 |
-
pub eliminated: i32,
|
| 1828 |
-
/// 0x70: Alliance ID used by CountTeamsByAlliance and SetActiveWorm_Maybe.
|
| 1829 |
-
pub alliance: i32,
|
| 1830 |
-
/// 0x74: Active worm index (0 = none, N = worm N is active).
|
| 1831 |
-
pub active_worm: i32,
|
| 1832 |
-
/// 0x78: Number of worms on this team.
|
| 1833 |
-
pub worm_count: i32,
|
| 1834 |
-
/// 0x7C: Per-team turn action flags (bitfield).
|
| 1835 |
-
/// Skip Go (weapon 57) toggles a bit here based on the weapon's fire_params.
|
| 1836 |
-
/// Bit is set to mark the team should skip; in game_version > 0x1C, toggling
|
| 1837 |
-
/// again clears it.
|
| 1838 |
-
pub turn_action_flags: u32,
|
| 1839 |
-
/// 0x80: Alliance ID for ammo/delay table indexing (GetAmmo/AddAmmo/SubtractAmmo).
|
| 1840 |
-
/// Teams with the same weapon_alliance share ammo pools. Distinct from
|
| 1841 |
-
/// `alliance` at 0x70 which is used by CountTeamsByAlliance.
|
| 1842 |
-
pub weapon_alliance: i32,
|
| 1843 |
-
/// 0x84: Team name, null-terminated ASCII string.
|
| 1844 |
-
pub team_name: [u8; 0x14],
|
| 1845 |
-
/// 0x98: Unknown trailing bytes.
|
| 1846 |
-
pub _unknown_98: [u8; 4],
|
| 1847 |
-
}
|
| 1848 |
-
|
| 1849 |
-
const _: () = assert!(core::mem::size_of::<TeamHeader>() == 0x9C);
|
| 1850 |
-
|
| 1851 |
-
/// Union for slot 0 of a TeamBlock.
|
| 1852 |
-
///
|
| 1853 |
-
/// This slot is dual-purpose: its high offsets store team metadata
|
| 1854 |
-
/// (`TeamHeader`), while its low offsets may contain the 8th worm
|
| 1855 |
-
/// of the previous team (`WormEntry`). The two uses don't conflict
|
| 1856 |
-
/// because worm data occupies 0x00-0x5F and header data starts at 0x6C.
|
| 1857 |
-
#[repr(C)]
|
| 1858 |
-
#[derive(Clone, Copy)]
|
| 1859 |
-
pub union TeamSlot0 {
|
| 1860 |
-
/// View as worm data (used when the previous team has 8 worms).
|
| 1861 |
-
pub worm: core::mem::ManuallyDrop<WormEntry>,
|
| 1862 |
-
/// View as team metadata (eliminated, alliance, worm_count, etc.).
|
| 1863 |
-
pub team: core::mem::ManuallyDrop<TeamHeader>,
|
| 1864 |
-
}
|
| 1865 |
-
|
| 1866 |
-
/// Full per-team data block (0x51C bytes, 7 blocks in DDGame).
|
| 1867 |
-
///
|
| 1868 |
-
/// Each block starts with a `TeamSlot0` union (0x9C bytes) that serves
|
| 1869 |
-
/// dual purpose: its high offsets hold team metadata (`TeamHeader`),
|
| 1870 |
-
/// while its low offsets may contain the 8th worm of the previous team.
|
| 1871 |
-
/// The remaining 7 worm slots follow, then 0x3C bytes of metadata.
|
| 1872 |
-
///
|
| 1873 |
-
/// **Block indexing**: Block 0 is unused (preamble, all zeros). Actual team
|
| 1874 |
-
/// data starts at block 1. Worms are accessed via `TeamArenaRef::team_worm()`
|
| 1875 |
-
/// which uses raw pointer arithmetic and naturally crosses block boundaries.
|
| 1876 |
-
///
|
| 1877 |
-
/// **Header access**: Use `TeamArenaRef::team_header(idx)` to get the
|
| 1878 |
-
/// `TeamHeader` for a team (reads from `blocks[idx+1].header.team`).
|
| 1879 |
-
#[repr(C)]
|
| 1880 |
-
pub struct TeamBlock {
|
| 1881 |
-
/// 0x000-0x09B: Header slot (union of TeamHeader and WormEntry).
|
| 1882 |
-
/// Team metadata at high offsets; may hold 8th worm data at low offsets.
|
| 1883 |
-
pub header: TeamSlot0,
|
| 1884 |
-
/// 0x09C-0x4DF: 7 worm entries (slots 1-7, stride 0x9C)
|
| 1885 |
-
pub worms: [WormEntry; 7],
|
| 1886 |
-
/// 0x4E0-0x51B: Team metadata (0x3C bytes)
|
| 1887 |
-
pub trailer: [u8; 0x3C],
|
| 1888 |
-
}
|
| 1889 |
-
|
| 1890 |
-
const _: () = assert!(core::mem::size_of::<TeamBlock>() == 0x51C);
|
| 1891 |
-
|
| 1892 |
-
/// Team arena state area within DDGame (at DDGame + 0x4628).
|
| 1893 |
-
///
|
| 1894 |
-
/// Contains per-team entries and an interleaved ammo/delay table.
|
| 1895 |
-
/// Used by GetAmmo (0x5225E0), AddAmmo (0x522640), SubtractAmmo (0x522680).
|
| 1896 |
-
///
|
| 1897 |
-
/// The ammo/delay table uses stride 142 (= 71 weapons * 2) per alliance.
|
| 1898 |
-
/// Within each alliance block of 142 entries:
|
| 1899 |
-
/// - Entries 0..70 are ammo counts (accessed at base + 0x1EB4 + index * 4)
|
| 1900 |
-
/// - Entries 71..141 are delay flags (accessed at base + 0x1FD0 + index * 4)
|
| 1901 |
-
///
|
| 1902 |
-
/// The original code accesses these as two arrays at different base offsets
|
| 1903 |
-
/// (0x1EB4 and 0x1FD0) using the same index `alliance_id * 142 + weapon_id`.
|
| 1904 |
-
/// Since 0x1FD0 - 0x1EB4 = 71 * 4, this is equivalent to accessing
|
| 1905 |
-
/// `weapon_slots.teams[alliance].ammo[weapon]` and
|
| 1906 |
-
/// `weapon_slots.teams[alliance].delay[weapon]`.
|
| 1907 |
-
|
| 1908 |
-
/// Per-alliance weapon ammo and delay data (0x238 = 568 bytes per alliance).
|
| 1909 |
-
#[repr(C)]
|
| 1910 |
-
pub struct TeamWeaponSlots {
|
| 1911 |
-
/// Ammo counts per weapon (71 entries). -1 = unlimited, 0 = none, >0 = count.
|
| 1912 |
-
pub ammo: [i32; 71],
|
| 1913 |
-
/// Delay flags per weapon (71 entries). Nonzero = weapon on cooldown.
|
| 1914 |
-
pub delay: [i32; 71],
|
| 1915 |
-
}
|
| 1916 |
-
const _: () = assert!(core::mem::size_of::<TeamWeaponSlots>() == 0x238);
|
| 1917 |
-
|
| 1918 |
-
/// Weapon slot data for all 6 alliances (0xD50 = 3408 bytes total).
|
| 1919 |
-
#[repr(C)]
|
| 1920 |
-
pub struct WeaponSlots {
|
| 1921 |
-
pub teams: [TeamWeaponSlots; 6],
|
| 1922 |
-
}
|
| 1923 |
-
const _: () = assert!(core::mem::size_of::<WeaponSlots>() == 852 * 4);
|
| 1924 |
-
|
| 1925 |
-
#[repr(C)]
|
| 1926 |
-
pub struct TeamArenaState {
|
| 1927 |
-
/// 0x0000-0x1EAF: Per-team data region (opaque).
|
| 1928 |
-
///
|
| 1929 |
-
/// This region contains 7 team entries at stride 0x51C (1-indexed, index 0
|
| 1930 |
-
/// is preamble). The original WA.exe accesses team data via raw pointer
|
| 1931 |
-
/// arithmetic relative to the arena base. In our Rust code, team data is
|
| 1932 |
-
/// accessed through `TeamArenaRef`, `TeamBlock`, and `TeamHeader`,
|
| 1933 |
-
/// so this region is treated as opaque padding.
|
| 1934 |
-
///
|
| 1935 |
-
/// The 7th entry (team 6, 1-indexed) only has 8 bytes before team_count;
|
| 1936 |
-
/// the rest overlaps with weapon_slots.
|
| 1937 |
-
pub team_blocks_region: [u8; 0x1EB0],
|
| 1938 |
-
/// 0x1EB0: Number of teams in the game (used by team iteration loops)
|
| 1939 |
-
pub team_count: i32,
|
| 1940 |
-
/// 0x1EB4: Per-alliance weapon ammo and delay data.
|
| 1941 |
-
/// 6 alliances, each with 71 ammo + 71 delay i32 entries.
|
| 1942 |
-
/// Ammo: -1 = unlimited, 0 = none, >0 = count.
|
| 1943 |
-
/// Delay: nonzero = weapon on delay.
|
| 1944 |
-
pub weapon_slots: WeaponSlots,
|
| 1945 |
-
/// 0x2C04: Padding between weapon_slots end and game_mode_flag.
|
| 1946 |
-
/// weapon_slots: 852 * 4 = 3408 = 0xD50. 0x1EB4 + 0xD50 = 0x2C04.
|
| 1947 |
-
pub _pad_2c04: [u8; 8],
|
| 1948 |
-
/// 0x2C0C: Game mode flag (nonzero = override weapon delays in certain conditions)
|
| 1949 |
-
pub game_mode_flag: i32,
|
| 1950 |
-
/// 0x2C10: Unknown padding
|
| 1951 |
-
pub _pad_2c10: [u8; 0x18],
|
| 1952 |
-
/// 0x2C28: Game phase counter (>=484 = sudden death, >=-2 = normal game)
|
| 1953 |
-
pub game_phase: i32,
|
| 1954 |
-
|
| 1955 |
-
// === Alliance tracking (set by CountTeamsByAlliance + SetActiveWorm_Maybe) ===
|
| 1956 |
-
/// 0x2C2C: Current alliance being evaluated
|
| 1957 |
-
pub current_alliance: i32,
|
| 1958 |
-
/// 0x2C30: Count of teams with an active worm (set by SetActiveWorm_Maybe)
|
| 1959 |
-
pub active_worm_count: i32,
|
| 1960 |
-
/// 0x2C34: Count of active teams with valid alliance (>=0)
|
| 1961 |
-
pub active_team_count: i32,
|
| 1962 |
-
/// 0x2C38: Count of teams matching current_alliance
|
| 1963 |
-
pub same_alliance_count: i32,
|
| 1964 |
-
/// 0x2C3C: Count of teams not matching current_alliance
|
| 1965 |
-
pub enemy_team_count: i32,
|
| 1966 |
-
/// 0x2C40: Last team index set active (written by SetActiveWorm_Maybe)
|
| 1967 |
-
pub last_active_team: i32,
|
| 1968 |
-
/// 0x2C44: Alliance of last activated team (written by SetActiveWorm_Maybe)
|
| 1969 |
-
pub last_active_alliance: i32,
|
| 1970 |
-
}
|
| 1971 |
-
|
| 1972 |
-
const _: () = assert!(core::mem::size_of::<TeamArenaState>() == 0x2C48);
|
| 1973 |
-
|
| 1974 |
-
/// Worm state constants and helpers.
|
| 1975 |
-
pub mod worm {
|
| 1976 |
-
// --- Known state values (runtime-validated via validator dumps) ---
|
| 1977 |
-
|
| 1978 |
-
/// Transitional state checked by CheckWormState0x64 (0x5228D0). 11 xrefs.
|
| 1979 |
-
/// Appears briefly during turn transitions.
|
| 1980 |
-
pub const STATE_TRANSITIONAL: u32 = 0x64;
|
| 1981 |
-
/// Idle β worm is waiting, not currently active.
|
| 1982 |
-
pub const STATE_IDLE: u32 = 0x65;
|
| 1983 |
-
/// Active β worm is currently selected and taking its turn.
|
| 1984 |
-
pub const STATE_ACTIVE: u32 = 0x67;
|
| 1985 |
-
/// Dead β worm has been killed (hp=0). Persists across turns.
|
| 1986 |
-
pub const STATE_DEAD: u32 = 0x87;
|
| 1987 |
-
|
| 1988 |
-
/// Special worm states β worm is dying/drowning/in special animation.
|
| 1989 |
-
/// Checked by IsWormInSpecialState (0x5226B0).
|
| 1990 |
-
/// All values in the 0x80+ range. 0x87 (dead) is also in this set.
|
| 1991 |
-
pub const SPECIAL_STATES: [u32; 6] = [0x80, 0x81, 0x82, 0x83, 0x85, 0x89];
|
| 1992 |
-
|
| 1993 |
-
/// Check if a worm state value is a "special" state.
|
| 1994 |
-
pub fn is_special_state(state: u32) -> bool {
|
| 1995 |
-
SPECIAL_STATES.contains(&state)
|
| 1996 |
-
}
|
| 1997 |
-
}
|
| 1998 |
-
|
| 1999 |
-
/// Game phase thresholds (stored in TeamArenaState::game_phase).
|
| 2000 |
-
pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
|
| 2001 |
-
pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
|
| 2002 |
-
|
| 2003 |
-
impl TeamArenaState {
|
| 2004 |
-
/// Get ammo count for a weapon by alliance and weapon ID.
|
| 2005 |
-
pub fn get_ammo(&self, alliance: usize, weapon_id: usize) -> i32 {
|
| 2006 |
-
self.weapon_slots.teams[alliance].ammo[weapon_id]
|
| 2007 |
-
}
|
| 2008 |
-
|
| 2009 |
-
/// Get mutable reference to ammo count.
|
| 2010 |
-
pub fn ammo_mut(&mut self, alliance: usize, weapon_id: usize) -> &mut i32 {
|
| 2011 |
-
&mut self.weapon_slots.teams[alliance].ammo[weapon_id]
|
| 2012 |
-
}
|
| 2013 |
-
|
| 2014 |
-
/// Get delay flag for a weapon by alliance and weapon ID.
|
| 2015 |
-
pub fn get_delay(&self, alliance: usize, weapon_id: usize) -> i32 {
|
| 2016 |
-
self.weapon_slots.teams[alliance].delay[weapon_id]
|
| 2017 |
-
}
|
| 2018 |
-
}
|
| 2019 |
-
|
| 2020 |
-
/// Typed handle to a TeamArenaState pointer received from WA.exe.
|
| 2021 |
-
///
|
| 2022 |
-
/// Wraps the raw `base: u32` from trampoline register captures and provides
|
| 2023 |
-
/// accessor methods that encapsulate the backward pointer arithmetic to reach
|
| 2024 |
-
/// TeamBlock worm data. The TeamBlock array lives 0x598 bytes before
|
| 2025 |
-
/// the TeamArenaState in DDGame memory.
|
| 2026 |
-
///
|
| 2027 |
-
/// # Safety
|
| 2028 |
-
/// Must only be constructed from a valid DDGame + TEAM_ARENA_STATE pointer.
|
| 2029 |
-
/// `repr(transparent)` ensures identical ABI to `*const u8` / `u32` on i686,
|
| 2030 |
-
/// so it can be received directly from usercall trampoline register captures.
|
| 2031 |
-
#[repr(transparent)]
|
| 2032 |
-
#[derive(Clone, Copy)]
|
| 2033 |
-
pub struct TeamArenaRef {
|
| 2034 |
-
base: *mut u8,
|
| 2035 |
-
}
|
| 2036 |
-
|
| 2037 |
-
impl TeamArenaRef {
|
| 2038 |
-
/// Wrap a raw integer pointer (for usercall trampoline register captures).
|
| 2039 |
-
///
|
| 2040 |
-
/// # Safety
|
| 2041 |
-
/// `base` must point to a valid TeamArenaState (DDGame + 0x4628).
|
| 2042 |
-
#[inline]
|
| 2043 |
-
pub unsafe fn from_raw(base: u32) -> Self {
|
| 2044 |
-
Self {
|
| 2045 |
-
base: base as *mut u8,
|
| 2046 |
-
}
|
| 2047 |
-
}
|
| 2048 |
-
|
| 2049 |
-
/// Wrap a typed pointer to TeamArenaState.
|
| 2050 |
-
///
|
| 2051 |
-
/// # Safety
|
| 2052 |
-
/// `arena` must point to a valid TeamArenaState within a live DDGame.
|
| 2053 |
-
#[inline]
|
| 2054 |
-
pub unsafe fn from_ptr(arena: *mut TeamArenaState) -> Self {
|
| 2055 |
-
Self {
|
| 2056 |
-
base: arena as *mut u8,
|
| 2057 |
-
}
|
| 2058 |
-
}
|
| 2059 |
-
|
| 2060 |
-
/// Access the TeamArenaState fields (read-only).
|
| 2061 |
-
#[inline]
|
| 2062 |
-
pub unsafe fn state(&self) -> &TeamArenaState {
|
| 2063 |
-
&*(self.base as *const TeamArenaState)
|
| 2064 |
-
}
|
| 2065 |
-
|
| 2066 |
-
/// Access the TeamArenaState fields (mutable).
|
| 2067 |
-
#[inline]
|
| 2068 |
-
pub unsafe fn state_mut(&self) -> &mut TeamArenaState {
|
| 2069 |
-
&mut *(self.base as *mut TeamArenaState)
|
| 2070 |
-
}
|
| 2071 |
-
|
| 2072 |
-
/// Get pointer to the TeamBlock array base.
|
| 2073 |
-
#[inline]
|
| 2074 |
-
pub unsafe fn blocks(&self) -> *mut TeamBlock {
|
| 2075 |
-
self.base.sub(offsets::ARENA_TO_BLOCKS) as *mut TeamBlock
|
| 2076 |
-
}
|
| 2077 |
-
|
| 2078 |
-
/// Get the team header (metadata) for a team.
|
| 2079 |
-
///
|
| 2080 |
-
/// Returns `&block[team_idx+1].header.team`, which holds team metadata:
|
| 2081 |
-
/// worm_count, eliminated flag, weapon_alliance, team_name.
|
| 2082 |
-
#[inline]
|
| 2083 |
-
pub unsafe fn team_header(&self, team_idx: usize) -> &TeamHeader {
|
| 2084 |
-
&(*self.blocks().add(team_idx + 1)).header.team
|
| 2085 |
-
}
|
| 2086 |
-
|
| 2087 |
-
/// Get a mutable team header for a team.
|
| 2088 |
-
#[inline]
|
| 2089 |
-
pub unsafe fn team_header_mut(&self, team_idx: usize) -> &mut TeamHeader {
|
| 2090 |
-
&mut (*self.blocks().add(team_idx + 1)).header.team
|
| 2091 |
-
}
|
| 2092 |
-
|
| 2093 |
-
/// Get a playable worm entry by 1-indexed worm number (1..=8).
|
| 2094 |
-
///
|
| 2095 |
-
/// Uses raw pointer arithmetic matching the original WA code:
|
| 2096 |
-
/// `base + team_idx * 0x51C + worm_num * 0x9C - 0x598`.
|
| 2097 |
-
/// This naturally crosses TeamBlock boundaries when worm_num = 8,
|
| 2098 |
-
/// since the 8th worm's early fields (state, health) spill into the
|
| 2099 |
-
/// next block's header slot. The header metadata lives at high offsets
|
| 2100 |
-
/// (0x6C+) that don't conflict with worm data (0x00-0x5F).
|
| 2101 |
-
#[inline]
|
| 2102 |
-
pub unsafe fn team_worm(&self, team_idx: usize, worm_num: usize) -> &WormEntry {
|
| 2103 |
-
let ptr = self
|
| 2104 |
-
.base
|
| 2105 |
-
.add(team_idx * 0x51C)
|
| 2106 |
-
.add(worm_num * 0x9C)
|
| 2107 |
-
.sub(0x598);
|
| 2108 |
-
&*(ptr as *const WormEntry)
|
| 2109 |
-
}
|
| 2110 |
-
|
| 2111 |
-
/// Get a mutable reference to a specific worm entry.
|
| 2112 |
-
pub unsafe fn team_worm_mut(&self, team_idx: usize, worm_num: usize) -> &mut WormEntry {
|
| 2113 |
-
let ptr = self
|
| 2114 |
-
.base
|
| 2115 |
-
.add(team_idx * 0x51C)
|
| 2116 |
-
.add(worm_num * 0x9C)
|
| 2117 |
-
.sub(0x598);
|
| 2118 |
-
&mut *(ptr as *mut WormEntry)
|
| 2119 |
-
}
|
| 2120 |
-
|
| 2121 |
-
/// Get a team's block and its header in one call.
|
| 2122 |
-
///
|
| 2123 |
-
/// Returns `(block[team_idx], &block[team_idx+1].header.team)`.
|
| 2124 |
-
/// The block contains worm data, and the header (slot 0 of the next block)
|
| 2125 |
-
/// holds team metadata (worm_count, eliminated flag).
|
| 2126 |
-
///
|
| 2127 |
-
/// **Note**: For accessing worms, prefer `team_worm()` which handles
|
| 2128 |
-
/// 8-worm teams correctly via cross-boundary pointer arithmetic.
|
| 2129 |
-
#[inline]
|
| 2130 |
-
pub unsafe fn team_and_header(&self, team_idx: usize) -> (&TeamBlock, &TeamHeader) {
|
| 2131 |
-
let blocks = self.blocks();
|
| 2132 |
-
let block = &*blocks.add(team_idx);
|
| 2133 |
-
let header = &(*blocks.add(team_idx + 1)).header.team;
|
| 2134 |
-
(block, header)
|
| 2135 |
-
}
|
| 2136 |
-
|
| 2137 |
-
/// Get team header for Pattern B access (alliance/active_worm at +0x70/+0x74).
|
| 2138 |
-
///
|
| 2139 |
-
/// Pattern B indexes from block[i+2] for 0-indexed team `i`:
|
| 2140 |
-
/// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
|
| 2141 |
-
#[inline]
|
| 2142 |
-
pub unsafe fn team_header_b(&self, team_idx: usize) -> &TeamHeader {
|
| 2143 |
-
&(*self.blocks().add(team_idx + 2)).header.team
|
| 2144 |
-
}
|
| 2145 |
-
|
| 2146 |
-
/// Get mutable team header for Pattern B access.
|
| 2147 |
-
#[inline]
|
| 2148 |
-
pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamHeader {
|
| 2149 |
-
&mut (*(self.blocks() as *mut TeamBlock).add(team_idx + 2))
|
| 2150 |
-
.header
|
| 2151 |
-
.team
|
| 2152 |
-
}
|
| 2153 |
-
|
| 2154 |
-
/// Compute the flat index for ammo/delay table access.
|
| 2155 |
-
///
|
| 2156 |
-
/// Reads the weapon alliance ID from the team header for the given
|
| 2157 |
-
/// 1-indexed team, then computes `alliance_id * 142 + weapon_id`.
|
| 2158 |
-
///
|
| 2159 |
-
/// The weapon_slots array is interleaved: per alliance, 71 ammo slots
|
| 2160 |
-
/// then 71 delay slots (stride 142 per alliance).
|
| 2161 |
-
/// Ammo: `weapon_slots[alliance_id * 142 + weapon_id]`
|
| 2162 |
-
/// Delay: `weapon_slots[alliance_id * 142 + 71 + weapon_id]`
|
| 2163 |
-
/// Returns (alliance_id, weapon_id) for accessing weapon slots.
|
| 2164 |
-
#[inline]
|
| 2165 |
-
pub unsafe fn weapon_slot_key(&self, team_index: usize, weapon_id: u32) -> (usize, usize) {
|
| 2166 |
-
let alliance_id = self.team_header(team_index).weapon_alliance as usize;
|
| 2167 |
-
(alliance_id, weapon_id as usize)
|
| 2168 |
-
}
|
| 2169 |
-
}
|
| 2170 |
-
|
| 2171 |
// ββ Snapshot impls ββββββββββββββββββββββββββββββββββββββββββ
|
| 2172 |
|
| 2173 |
#[cfg(target_arch = "x86")]
|
|
@@ -2226,15 +616,3 @@ impl crate::snapshot::Snapshot for DDGame {
|
|
| 2226 |
}
|
| 2227 |
}
|
| 2228 |
|
| 2229 |
-
impl crate::snapshot::Snapshot for WormEntry {
|
| 2230 |
-
unsafe fn write_snapshot(&self, w: &mut dyn core::fmt::Write, indent: usize) -> core::fmt::Result {
|
| 2231 |
-
use crate::snapshot::write_indent;
|
| 2232 |
-
let i = indent;
|
| 2233 |
-
let name = core::ffi::CStr::from_ptr(self.name.as_ptr() as *const core::ffi::c_char)
|
| 2234 |
-
.to_string_lossy();
|
| 2235 |
-
write_indent(w, i)?;
|
| 2236 |
-
writeln!(w, "state=0x{:02X} active={} hp={}/{} name=\"{}\"",
|
| 2237 |
-
self.state, self.active_flag, self.health, self.max_health, name)?;
|
| 2238 |
-
Ok(())
|
| 2239 |
-
}
|
| 2240 |
-
}
|
|
|
|
|
|
|
|
|
|
| 1 |
use crate::FieldRegistry;
|
| 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::game_info::GameInfo;
|
|
|
|
| 9 |
use crate::game::weapon::WeaponTable;
|
| 10 |
use crate::input::keyboard::DDKeyboard;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
use crate::render::landscape::PCLandscape;
|
| 12 |
use crate::render::queue::RenderQueue;
|
| 13 |
use crate::render::turn_order::TurnOrderWidget;
|
| 14 |
+
|
| 15 |
+
// Re-export constructor items so existing `engine::ddgame::*` imports keep working.
|
| 16 |
+
pub use super::ddgame_constructor::{
|
| 17 |
+
bit_grid_init, create_ddgame, ddgame_init_fields, ddgame_init_render_indices,
|
| 18 |
+
display_layer_color_init, gfx_dir_find_entry, gfx_resource_create, init_constructor_addrs,
|
| 19 |
+
ON_DDGAME_ALLOC,
|
| 20 |
+
};
|
| 21 |
+
// Re-export public GfxHandler functions so existing `engine::ddgame::*` imports keep working.
|
| 22 |
+
pub use crate::render::gfx_dir::gfx_dir_load_dir;
|
| 23 |
+
|
| 24 |
+
// Re-export all team_arena types so existing `engine::ddgame::*` imports keep working.
|
| 25 |
+
pub use crate::engine::team_arena::{
|
| 26 |
+
CoordEntry, CoordList, CoordListEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
|
| 27 |
+
RenderEntry, SoundQueueEntry, TeamArenaRef, TeamArenaState, TeamBlock, TeamHeader,
|
| 28 |
+
TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots, WormEntry, worm,
|
| 29 |
+
};
|
| 30 |
|
| 31 |
/// DDGame β the main game engine object.
|
| 32 |
///
|
|
|
|
| 395 |
|
| 396 |
const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98D8);
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
// ============================================================
|
| 399 |
// DDGame methods
|
| 400 |
// ============================================================
|
|
|
|
| 437 |
}
|
| 438 |
}
|
| 439 |
|
| 440 |
+
// DDGame constructor code (create_ddgame, init helpers, usercall bridges)
|
| 441 |
+
// has been moved to ddgame_constructor.rs.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
|
| 443 |
// BitGrid__Init moved to crate::task::bit_grid
|
| 444 |
+
// Re-exported via ddgame_constructor.rs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
/// Well-known byte offsets into DDGame, for use with raw pointer access.
|
| 446 |
///
|
| 447 |
/// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
|
|
|
|
| 558 |
pub const SOUND_QUEUE_COUNT: usize = 0x8140;
|
| 559 |
}
|
| 560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
// ββ Snapshot impls ββββββββββββββββββββββββββββββββββββββββββ
|
| 562 |
|
| 563 |
#[cfg(target_arch = "x86")]
|
|
|
|
| 616 |
}
|
| 617 |
}
|
| 618 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
crates/openwa-core/src/engine/ddgame_constructor.rs
ADDED
|
@@ -0,0 +1,1111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! DDGame constructor β replaces DDGame__Constructor (0x56E220)
|
| 2 |
+
//!
|
| 3 |
+
//! Despite being named DDGame__Constructor, the original function
|
| 4 |
+
//! receives DDGameWrapper* as `this` and creates DDGame internally.
|
| 5 |
+
//! It populates fields on BOTH the wrapper and the inner DDGame.
|
| 6 |
+
//! The Rust entry point is DDGameWrapper::create_game() in
|
| 7 |
+
//! ddgame_wrapper.rs; the bulk of the logic lives here because
|
| 8 |
+
//! it primarily initializes DDGame fields.
|
| 9 |
+
|
| 10 |
+
use super::ddgame::DDGame;
|
| 11 |
+
use crate::address::va;
|
| 12 |
+
use crate::audio::active_sound::ActiveSoundTable;
|
| 13 |
+
use crate::audio::dssound::DSSound;
|
| 14 |
+
use crate::audio::music::Music;
|
| 15 |
+
use crate::display::dd_display::DDDisplay;
|
| 16 |
+
use crate::display::gradient::compute_complex_gradient;
|
| 17 |
+
use crate::display::palette::Palette;
|
| 18 |
+
use crate::engine::ddgame_wrapper::DDGameWrapper;
|
| 19 |
+
use crate::engine::game_info::GameInfo;
|
| 20 |
+
use crate::engine::net_bridge::NetBridge;
|
| 21 |
+
use crate::engine::team_arena::{CoordList, CoordListEntry};
|
| 22 |
+
use crate::input::keyboard::DDKeyboard;
|
| 23 |
+
use crate::rebase::rb;
|
| 24 |
+
use crate::render::gfx_dir::{
|
| 25 |
+
call_gfx_find_and_load, call_gfx_load_and_wrap, call_gfx_load_dir, GfxDir,
|
| 26 |
+
};
|
| 27 |
+
pub use crate::render::gfx_dir::{gfx_dir_find_entry, gfx_resource_create};
|
| 28 |
+
use crate::render::landscape::PCLandscape;
|
| 29 |
+
use crate::task::bit_grid::BitGrid;
|
| 30 |
+
use crate::wa_alloc::{wa_malloc, wa_malloc_zeroed};
|
| 31 |
+
|
| 32 |
+
// Re-export bit_grid_init so existing `engine::ddgame::bit_grid_init` paths keep working.
|
| 33 |
+
pub use crate::task::bit_grid::bit_grid_init;
|
| 34 |
+
|
| 35 |
+
// ============================================================
|
| 36 |
+
// Pure Rust implementations of DDGame sub-functions
|
| 37 |
+
// ============================================================
|
| 38 |
+
// These are called both by create_ddgame() and by MinHook
|
| 39 |
+
// trampolines in openwa-wormkit/replacements/ddgame_init.rs.
|
| 40 |
+
|
| 41 |
+
/// Pure Rust implementation of DDGame__InitFields (0x526120).
|
| 42 |
+
///
|
| 43 |
+
/// Zeroes stride-0x194 table entries, calls init_render_indices,
|
| 44 |
+
/// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
|
| 45 |
+
///
|
| 46 |
+
/// # Safety
|
| 47 |
+
/// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98D8 bytes).
|
| 48 |
+
pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
|
| 49 |
+
let base = ddgame as *mut u8;
|
| 50 |
+
|
| 51 |
+
// Zero the stride-0x194 table (10 entries starting at 0x379C).
|
| 52 |
+
// These offsets are deep in the unknown 0x2E00-0x45EB region and don't
|
| 53 |
+
// have named fields yet β keep as raw offsets for now.
|
| 54 |
+
for &off in &[
|
| 55 |
+
0x379Cusize, 0x3930, 0x3AC4, 0x3C58, 0x3DEC,
|
| 56 |
+
0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0,
|
| 57 |
+
] {
|
| 58 |
+
*(base.add(off) as *mut u32) = 0;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// init_field_64d8 = TeamArenaState.team_count (arena+0x1EB0)
|
| 62 |
+
(*ddgame).team_arena.team_count = 0;
|
| 63 |
+
// init_field_72a4 = weapon_slots flat[754] = alliance 5, ammo[44]
|
| 64 |
+
(*ddgame).team_arena.weapon_slots.teams[5].ammo[44] = 0;
|
| 65 |
+
|
| 66 |
+
// InitRenderIndices β original sets ESI = ddgame + 0x72D8, now uses typed DDGame ptr
|
| 67 |
+
ddgame_init_render_indices(ddgame);
|
| 68 |
+
|
| 69 |
+
// Zero x and y of each screen coordinate entry (4 entries each)
|
| 70 |
+
for entry in &mut (*ddgame).screen_coords {
|
| 71 |
+
entry.x = 0;
|
| 72 |
+
entry.y = 0;
|
| 73 |
+
}
|
| 74 |
+
for entry in &mut (*ddgame).screen_coords_2 {
|
| 75 |
+
entry.x = 0;
|
| 76 |
+
entry.y = 0;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/// Pure Rust implementation of DDGame__InitRenderIndices (0x526080).
|
| 81 |
+
///
|
| 82 |
+
/// Convention: usercall(ESI=base_ptr), plain RET.
|
| 83 |
+
/// Initialize render state flag, render entry table, and team index maps.
|
| 84 |
+
///
|
| 85 |
+
/// Original: called with ESI = ddgame + 0x72D8, but now uses typed DDGame fields.
|
| 86 |
+
///
|
| 87 |
+
/// # Safety
|
| 88 |
+
/// `ddgame` must point to a valid DDGame allocation.
|
| 89 |
+
pub unsafe fn ddgame_init_render_indices(ddgame: *mut DDGame) {
|
| 90 |
+
(*ddgame).render_state_flag = 0;
|
| 91 |
+
|
| 92 |
+
// Zero the active flag of each render entry (eh_vector_constructor_iterator).
|
| 93 |
+
for entry in &mut (*ddgame).render_entries {
|
| 94 |
+
entry.active = 0;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Initialize all three team index maps as identity permutations.
|
| 98 |
+
for map in &mut (*ddgame).team_index_maps {
|
| 99 |
+
for i in 0..16i16 {
|
| 100 |
+
map.entries[i as usize] = i;
|
| 101 |
+
}
|
| 102 |
+
map.count = 0x10;
|
| 103 |
+
map.terminator = 0;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/// Pure Rust implementation of FUN_570E20 (display layer color init).
|
| 108 |
+
///
|
| 109 |
+
/// Convention: usercall(ESI=wrapper), plain RET.
|
| 110 |
+
///
|
| 111 |
+
/// Sets display layer color parameters via display->vtable[4](layer, color).
|
| 112 |
+
/// Layer 1 color depends on gfx_mode and game_version.
|
| 113 |
+
/// Layer 2 = 0x20, Layer 3 = 0x70.
|
| 114 |
+
///
|
| 115 |
+
/// # Safety
|
| 116 |
+
/// `wrapper` must be a valid DDGameWrapper with initialized display and ddgame.
|
| 117 |
+
#[cfg(target_arch = "x86")]
|
| 118 |
+
pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) {
|
| 119 |
+
let ddgame = (*wrapper).ddgame;
|
| 120 |
+
let game_info = (*ddgame).game_info;
|
| 121 |
+
let game_version = (*game_info).game_version;
|
| 122 |
+
|
| 123 |
+
// wrapper+0x4C8 is gfx_mode (at DDGameWrapper.gfx_mode)
|
| 124 |
+
let layer1_color = if (*wrapper).gfx_mode == 0 {
|
| 125 |
+
// (game_version > -2) - 1: yields 0 if true, -1 if false
|
| 126 |
+
// Then + 0x69: yields 0x69 or 0x68
|
| 127 |
+
if game_version > -2 {
|
| 128 |
+
0x69i32
|
| 129 |
+
} else {
|
| 130 |
+
0x68i32
|
| 131 |
+
}
|
| 132 |
+
} else {
|
| 133 |
+
5 + 0x69 // = 0x6E
|
| 134 |
+
};
|
| 135 |
+
|
| 136 |
+
let display = (*wrapper).display;
|
| 137 |
+
(*display).set_layer_color(1, layer1_color);
|
| 138 |
+
(*display).set_layer_color(2, 0x20);
|
| 139 |
+
(*display).set_layer_color(3, 0x70);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/// Initialize runtime addresses for the constructor bridges.
|
| 143 |
+
/// Must be called once at DLL startup (from lib.rs or similar).
|
| 144 |
+
pub fn init_constructor_addrs() {
|
| 145 |
+
unsafe {
|
| 146 |
+
SPRITE_REGION_CTOR_ADDR = rb(va::SPRITE_REGION_CONSTRUCTOR);
|
| 147 |
+
FUN_570A90_ADDR = rb(va::FUN_570A90);
|
| 148 |
+
FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
|
| 149 |
+
LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
|
| 150 |
+
LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
|
| 151 |
+
GFX_LOAD_SPRITES_ADDR = rb(va::GFX_DIR_LOAD_SPRITES);
|
| 152 |
+
}
|
| 153 |
+
crate::render::gfx_dir::init_addrs();
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// ββ Typed wrappers for WA stdcall functions ββββββββββββββββββββββββββββββββ
|
| 157 |
+
// Each wraps a single WA function with a typed Rust signature.
|
| 158 |
+
// Replace with pure Rust implementations as functions are ported.
|
| 159 |
+
|
| 160 |
+
/// DDGame__InitVersionFlags (0x525BE0): sets DDGame+0x7E2E/0x7E2F/0x7E3F.
|
| 161 |
+
#[cfg(target_arch = "x86")]
|
| 162 |
+
unsafe fn wa_init_version_flags(wrapper: *mut DDGameWrapper) {
|
| 163 |
+
let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 164 |
+
core::mem::transmute(rb(va::DDGAME_INIT_VERSION_FLAGS) as usize);
|
| 165 |
+
f(wrapper);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/// GfxHandler__LoadSprites (0x570B50): usercall(ESI=layer_ctx) + stdcall(4 params).
|
| 169 |
+
///
|
| 170 |
+
/// ESI must hold the display layer context (from DDDisplay::set_active_layer).
|
| 171 |
+
/// The function uses ESI for LoadSpriteFromVfs and GfxResource__Create_Maybe
|
| 172 |
+
/// when param4 (gfx_dir) is non-null.
|
| 173 |
+
#[cfg(target_arch = "x86")]
|
| 174 |
+
#[unsafe(naked)]
|
| 175 |
+
unsafe extern "C" fn wa_load_sprites(
|
| 176 |
+
_wrapper: *mut DDGameWrapper,
|
| 177 |
+
_sprite_data: *mut u8,
|
| 178 |
+
_display_flags: u32,
|
| 179 |
+
_param4: u32,
|
| 180 |
+
_layer_ctx: *mut u8, // β ESI
|
| 181 |
+
) {
|
| 182 |
+
core::arch::naked_asm!(
|
| 183 |
+
"push esi",
|
| 184 |
+
"mov esi, [esp+24]", // layer_ctx (5th param: 4 saved + 4 ret + 4*4 params + 4 = 24)
|
| 185 |
+
"push [esp+20]", // param4
|
| 186 |
+
"push [esp+20]", // display_flags
|
| 187 |
+
"push [esp+20]", // sprite_data
|
| 188 |
+
"push [esp+20]", // wrapper
|
| 189 |
+
"call [{addr}]",
|
| 190 |
+
"pop esi",
|
| 191 |
+
"ret",
|
| 192 |
+
addr = sym GFX_LOAD_SPRITES_ADDR,
|
| 193 |
+
);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
static mut GFX_LOAD_SPRITES_ADDR: u32 = 0;
|
| 197 |
+
|
| 198 |
+
/// DSSound_LoadEffectWAVs (0x571660): load sound effect WAVs.
|
| 199 |
+
#[cfg(target_arch = "x86")]
|
| 200 |
+
unsafe fn wa_load_effect_wavs(wrapper: *mut DDGameWrapper) {
|
| 201 |
+
let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 202 |
+
core::mem::transmute(rb(va::DSSOUND_LOAD_EFFECT_WAVS) as usize);
|
| 203 |
+
f(wrapper);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/// PCLandscape__Constructor (0x57ACB0): construct landscape object (0xB44 bytes, 11 params).
|
| 207 |
+
///
|
| 208 |
+
/// param_5 is `game_info + 0xDAAC` (landscape data path region within GameInfo).
|
| 209 |
+
/// The SoundEmitter sub-constructor reads paths, water settings, etc. from this
|
| 210 |
+
/// pointer with negative offsets back into GameInfo.
|
| 211 |
+
#[cfg(target_arch = "x86")]
|
| 212 |
+
unsafe fn wa_pc_landscape_ctor(
|
| 213 |
+
this: *mut u8,
|
| 214 |
+
ddgame: *mut DDGame,
|
| 215 |
+
gfx_resource: *mut u8,
|
| 216 |
+
display: *mut DDDisplay,
|
| 217 |
+
landscape_data: *const u8,
|
| 218 |
+
byte_output: *mut u8,
|
| 219 |
+
gfx_mode: u32,
|
| 220 |
+
temp_buf: *mut u32,
|
| 221 |
+
coord_output: *mut u32,
|
| 222 |
+
width_output: *mut u32,
|
| 223 |
+
height_output: *mut u32,
|
| 224 |
+
) -> *mut u8 {
|
| 225 |
+
let f: unsafe extern "stdcall" fn(
|
| 226 |
+
*mut u8,
|
| 227 |
+
*mut DDGame,
|
| 228 |
+
*mut u8,
|
| 229 |
+
*mut DDDisplay,
|
| 230 |
+
*const u8,
|
| 231 |
+
*mut u8,
|
| 232 |
+
u32,
|
| 233 |
+
*mut u32,
|
| 234 |
+
*mut u32,
|
| 235 |
+
*mut u32,
|
| 236 |
+
*mut u32,
|
| 237 |
+
) -> *mut u8 = core::mem::transmute(rb(va::PC_LANDSCAPE_CONSTRUCTOR) as usize);
|
| 238 |
+
f(
|
| 239 |
+
this,
|
| 240 |
+
ddgame,
|
| 241 |
+
gfx_resource,
|
| 242 |
+
display,
|
| 243 |
+
landscape_data,
|
| 244 |
+
byte_output,
|
| 245 |
+
gfx_mode,
|
| 246 |
+
temp_buf,
|
| 247 |
+
coord_output,
|
| 248 |
+
width_output,
|
| 249 |
+
height_output,
|
| 250 |
+
)
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/// DisplayGfx__Constructor (0x4F5E80): wrap raw image data in DisplayGfx object.
|
| 254 |
+
#[cfg(target_arch = "x86")]
|
| 255 |
+
unsafe fn wa_displaygfx_ctor(raw_image: *mut u8) -> *mut u8 {
|
| 256 |
+
let f: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
|
| 257 |
+
core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
|
| 258 |
+
f(raw_image)
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/// DDGame__InitDisplayFinal (0x56A830): finalize display for non-headless mode.
|
| 262 |
+
#[cfg(target_arch = "x86")]
|
| 263 |
+
unsafe fn wa_init_display_final(display: *mut DDDisplay) {
|
| 264 |
+
let f: unsafe extern "stdcall" fn(*mut DDDisplay) =
|
| 265 |
+
core::mem::transmute(rb(va::DDGAME_INIT_DISPLAY_FINAL) as usize);
|
| 266 |
+
f(display);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
/// DDGame__LoadHudAndWeaponSprites (0x53D0E0): load weapon icons and HUD sprites.
|
| 270 |
+
/// thiscall(ECX=gfx_dir) + 2 stack(ddgame, secondary_gfx_dir), RET 0x8.
|
| 271 |
+
#[cfg(target_arch = "x86")]
|
| 272 |
+
unsafe fn wa_load_hud_sprites(gfx_dir: *mut u8, ddgame: *mut DDGame, secondary: *mut u8) {
|
| 273 |
+
let f: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) =
|
| 274 |
+
core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
|
| 275 |
+
f(gfx_dir, ddgame, secondary);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
/// DDGame__InitPaletteGradientSprites (0x5706D0): creates DisplayGfx palette
|
| 279 |
+
/// gradient objects for each team. stdcall(wrapper), RET 0x4.
|
| 280 |
+
#[cfg(target_arch = "x86")]
|
| 281 |
+
unsafe fn wa_init_palette_gradient_sprites(wrapper: *mut DDGameWrapper) {
|
| 282 |
+
let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 283 |
+
core::mem::transmute(rb(va::DDGAME_INIT_PALETTE_GRADIENT_SPRITES) as usize);
|
| 284 |
+
f(wrapper);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/// Optional callback invoked right after DDGame allocation (before any field
|
| 288 |
+
/// initialization). Used by the hardware watchpoint debugger to arm DR0βDR3.
|
| 289 |
+
pub static mut ON_DDGAME_ALLOC: Option<unsafe fn(*mut u8)> = None;
|
| 290 |
+
|
| 291 |
+
/// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
|
| 292 |
+
///
|
| 293 |
+
/// Allocates 0x98D8 bytes from WA's heap, initializes all fields, and creates
|
| 294 |
+
/// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the
|
| 295 |
+
/// returned DDGame.
|
| 296 |
+
///
|
| 297 |
+
/// # Safety
|
| 298 |
+
/// All pointer params must be valid WA objects. `wrapper` must be a
|
| 299 |
+
/// partially-initialized DDGameWrapper (vtable, display, sound set).
|
| 300 |
+
#[cfg(target_arch = "x86")]
|
| 301 |
+
pub unsafe fn create_ddgame(
|
| 302 |
+
wrapper: *mut DDGameWrapper,
|
| 303 |
+
keyboard: *mut DDKeyboard,
|
| 304 |
+
display: *mut DDDisplay,
|
| 305 |
+
sound: *mut DSSound,
|
| 306 |
+
palette: *mut Palette,
|
| 307 |
+
music: *mut Music,
|
| 308 |
+
param7: *mut u8, // timer object (0x1F4 observed)
|
| 309 |
+
net_game: *mut u8, // from GameSession
|
| 310 |
+
game_info: *mut GameInfo,
|
| 311 |
+
network_ecx: u32, // implicit ECX from caller
|
| 312 |
+
) -> *mut DDGame {
|
| 313 |
+
// ββ 1. Allocate and zero-fill ββ
|
| 314 |
+
// Original: malloc(0x98D8), memset(ptr, 0, 0x98B8) β last 0x20 bytes not zeroed.
|
| 315 |
+
// We zero the full 0x98D8 for safety (strictly more initialization than original).
|
| 316 |
+
let ddgame = wa_malloc_zeroed(0x98D8) as *mut DDGame;
|
| 317 |
+
if ddgame.is_null() {
|
| 318 |
+
return core::ptr::null_mut();
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// Notify watchpoint debugger (if active) so it can arm DR0βDR3.
|
| 322 |
+
if let Some(cb) = ON_DDGAME_ALLOC {
|
| 323 |
+
cb(ddgame as *mut u8);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// ββ 2. InitFields β pure Rust (replaces usercall bridge) ββ
|
| 327 |
+
ddgame_init_fields(ddgame);
|
| 328 |
+
|
| 329 |
+
// ββ 3-4. Store params BEFORE exposing DDGame via wrapper ββ
|
| 330 |
+
// Critical: game_info must be set before wrapper->ddgame, because the
|
| 331 |
+
// message pump (triggered by audio loading) can cause game tasks to
|
| 332 |
+
// read ddgame->game_info. If game_info is null, they crash.
|
| 333 |
+
(*ddgame).display = display;
|
| 334 |
+
(*ddgame).sound = sound;
|
| 335 |
+
(*ddgame).keyboard = keyboard;
|
| 336 |
+
(*ddgame).palette = palette;
|
| 337 |
+
(*ddgame).music = music;
|
| 338 |
+
(*ddgame).timer_obj = param7;
|
| 339 |
+
(*ddgame).network_ecx = network_ecx;
|
| 340 |
+
(*ddgame).game_info = game_info;
|
| 341 |
+
(*ddgame).net_game = net_game;
|
| 342 |
+
|
| 343 |
+
// Now safe to expose β all fields that concurrent readers check are set.
|
| 344 |
+
(*wrapper).ddgame = ddgame;
|
| 345 |
+
|
| 346 |
+
// ββ 5. Set g_GameInfo global ββ
|
| 347 |
+
*(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info;
|
| 348 |
+
|
| 349 |
+
// ββ 6. Sound available + always-1 flags ββ
|
| 350 |
+
let is_headless = (*game_info).headless_mode != 0;
|
| 351 |
+
// sound_available enables loading progress bar, message pump, and sound during construction.
|
| 352 |
+
(*ddgame).sound_available = if is_headless { 0 } else { 1 };
|
| 353 |
+
(*ddgame).field_7efc = 1;
|
| 354 |
+
|
| 355 |
+
// ββ 7. Network bridge (online games only) ββ
|
| 356 |
+
(*wrapper).net_bridge = core::ptr::null_mut();
|
| 357 |
+
|
| 358 |
+
if (*game_info).game_version == -2 {
|
| 359 |
+
let bridge = wa_malloc_zeroed(core::mem::size_of::<NetBridge>() as u32) as *mut NetBridge;
|
| 360 |
+
(*bridge).ddgame = ddgame;
|
| 361 |
+
(*bridge).net_config_1 = (*game_info).net_config_1;
|
| 362 |
+
(*bridge).net_config_2 = (*game_info).net_config_2;
|
| 363 |
+
(*wrapper).net_bridge = bridge;
|
| 364 |
+
if network_ecx != 0 {
|
| 365 |
+
*((network_ecx as *mut u8).add(0x18) as *mut *mut NetBridge) = bridge;
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// ββ 9. InitVersionFlags β sets DDGame+0x7E2E/0x7E2F/0x7E3F ββ
|
| 370 |
+
wa_init_version_flags(wrapper);
|
| 371 |
+
|
| 372 |
+
// ββ 10. GfxHandler, landscape, sprites, audio, resources ββ
|
| 373 |
+
init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless);
|
| 374 |
+
|
| 375 |
+
let _ = crate::log::log_line("[DDGame] create_ddgame complete");
|
| 376 |
+
ddgame
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
/// Second half of the constructor: GfxHandler, landscape, sprites, audio, resources.
|
| 380 |
+
///
|
| 381 |
+
/// Second half of the constructor β initializes graphics, audio, landscape, and sprites.
|
| 382 |
+
#[cfg(target_arch = "x86")]
|
| 383 |
+
unsafe fn init_graphics_and_resources(
|
| 384 |
+
wrapper: *mut DDGameWrapper,
|
| 385 |
+
game_info: *mut GameInfo,
|
| 386 |
+
_net_game: *mut u8,
|
| 387 |
+
_display: *mut DDDisplay,
|
| 388 |
+
is_headless: bool,
|
| 389 |
+
) {
|
| 390 |
+
let ddgame = (*wrapper).ddgame;
|
| 391 |
+
use core::ffi::c_char;
|
| 392 |
+
let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
|
| 393 |
+
core::mem::transmute(rb(va::WA_FOPEN) as usize);
|
| 394 |
+
let gfx_dir_vtable = rb(va::GFX_DIR_VTABLE) as u32;
|
| 395 |
+
|
| 396 |
+
// ββ GfxDir #1 (primary) ββ
|
| 397 |
+
let gfx1 = GfxDir::alloc(gfx_dir_vtable);
|
| 398 |
+
(*wrapper).primary_gfx_dir = gfx1 as *mut u8;
|
| 399 |
+
(*wrapper).secondary_gfx_dir = core::ptr::null_mut();
|
| 400 |
+
|
| 401 |
+
// Build path list (order depends on headless + display_flags)
|
| 402 |
+
let headless = (*game_info).headless_mode as i32;
|
| 403 |
+
let display_flags = (*game_info).display_flags as i32;
|
| 404 |
+
let paths: [&core::ffi::CStr; 3] = if headless != 0 {
|
| 405 |
+
if display_flags == 0 {
|
| 406 |
+
[
|
| 407 |
+
c"data\\Gfx\\Gfx.dir",
|
| 408 |
+
c"data\\Gfx\\Gfx0.dir",
|
| 409 |
+
c"data\\Gfx\\Gfx1.dir",
|
| 410 |
+
]
|
| 411 |
+
} else {
|
| 412 |
+
[
|
| 413 |
+
c"data\\Gfx\\Gfx.dir",
|
| 414 |
+
c"data\\Gfx\\Gfx1.dir",
|
| 415 |
+
c"data\\Gfx\\Gfx0.dir",
|
| 416 |
+
]
|
| 417 |
+
}
|
| 418 |
+
} else if display_flags == 0 {
|
| 419 |
+
[
|
| 420 |
+
c"data\\Gfx\\Gfx0.dir",
|
| 421 |
+
c"data\\Gfx\\Gfx1.dir",
|
| 422 |
+
c"data\\Gfx\\Gfx.dir",
|
| 423 |
+
]
|
| 424 |
+
} else {
|
| 425 |
+
[
|
| 426 |
+
c"data\\Gfx\\Gfx1.dir",
|
| 427 |
+
c"data\\Gfx\\Gfx0.dir",
|
| 428 |
+
c"data\\Gfx\\Gfx.dir",
|
| 429 |
+
]
|
| 430 |
+
};
|
| 431 |
+
|
| 432 |
+
let mut gfx_loaded_idx = 0u32;
|
| 433 |
+
for (i, path) in paths.iter().enumerate() {
|
| 434 |
+
let fp = fopen(path.as_ptr(), c"rb".as_ptr());
|
| 435 |
+
(*gfx1).file_handle = fp;
|
| 436 |
+
if !fp.is_null()
|
| 437 |
+
&& call_gfx_load_dir(gfx1 as *mut u8, crate::render::gfx_dir::gfx_load_dir_addr()) != 0
|
| 438 |
+
{
|
| 439 |
+
gfx_loaded_idx = i as u32;
|
| 440 |
+
break;
|
| 441 |
+
}
|
| 442 |
+
if i == 2 {
|
| 443 |
+
panic!("DDGame: couldn't open any Gfx.dir");
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
let headless_offset = if headless != 0 { 1u32 } else { 0u32 };
|
| 448 |
+
(*wrapper).gfx_mode = if gfx_loaded_idx.wrapping_sub(headless_offset) < 2 {
|
| 449 |
+
1
|
| 450 |
+
} else {
|
| 451 |
+
0
|
| 452 |
+
};
|
| 453 |
+
|
| 454 |
+
// ββ GfxDir #2 (conditional β supplemental sprites for certain game versions) ββ
|
| 455 |
+
let game_version = (*game_info).game_version;
|
| 456 |
+
let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
|
| 457 |
+
if game_version < threshold {
|
| 458 |
+
let c_digit = if game_version > -3 { b'2' } else { b'1' };
|
| 459 |
+
let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
|
| 460 |
+
gfx_c_path[14] = c_digit;
|
| 461 |
+
|
| 462 |
+
let gfx2 = GfxDir::alloc(gfx_dir_vtable);
|
| 463 |
+
(*wrapper).secondary_gfx_dir = gfx2 as *mut u8;
|
| 464 |
+
|
| 465 |
+
let fp = fopen(gfx_c_path.as_ptr().cast(), c"rb".as_ptr());
|
| 466 |
+
(*gfx2).file_handle = fp;
|
| 467 |
+
let load_addr = crate::render::gfx_dir::gfx_load_dir_addr();
|
| 468 |
+
if fp.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
|
| 469 |
+
let fp2 = fopen(c"data\\Gfx\\Gfx.dir".as_ptr(), c"rb".as_ptr());
|
| 470 |
+
(*gfx2).file_handle = fp2;
|
| 471 |
+
if fp2.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
|
| 472 |
+
panic!("DDGame: couldn't open secondary Gfx.dir");
|
| 473 |
+
}
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
// ββ Display palette setup (non-headless) ββ
|
| 478 |
+
if !is_headless {
|
| 479 |
+
if *(rb(va::G_DISPLAY_MODE_FLAG) as *const c_char) == 0 {
|
| 480 |
+
call_usercall_eax(wrapper, FUN_570A90_ADDR);
|
| 481 |
+
}
|
| 482 |
+
let disp = (*wrapper).display;
|
| 483 |
+
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 484 |
+
(*disp).set_layer_color(1, 0xFE);
|
| 485 |
+
(*disp).load_sprite(
|
| 486 |
+
1,
|
| 487 |
+
1,
|
| 488 |
+
0,
|
| 489 |
+
gfx_dir,
|
| 490 |
+
rb(va::STR_CDROM_SPR) as *const c_char,
|
| 491 |
+
);
|
| 492 |
+
(*disp).set_layer_visibility(1, -100);
|
| 493 |
+
|
| 494 |
+
// Palette slot range init (raw byte offsets into DisplayBase)
|
| 495 |
+
let disp_raw = disp as *mut u8;
|
| 496 |
+
let palette_range_ptr = *(disp_raw.add(0x3120) as *const *mut i16);
|
| 497 |
+
if !palette_range_ptr.is_null() && *(disp_raw.add(0x3534) as *const i32) == 0 {
|
| 498 |
+
let start = *palette_range_ptr as u32;
|
| 499 |
+
let end = (*palette_range_ptr.add(1) as u32) + 1;
|
| 500 |
+
if start < end {
|
| 501 |
+
for i in start..end {
|
| 502 |
+
*(disp_raw.add(0x312C + i as usize * 4) as *mut u32) = 1;
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
crate::wa_alloc::wa_free(*(disp_raw.add(0x3120) as *const *mut u8));
|
| 506 |
+
*(disp_raw.add(0x3120) as *mut u32) = 0;
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
// ββ FUN_00570E20: usercall(ESI=wrapper), plain RET ββ
|
| 511 |
+
// Runs for all modes β headless vtable[4] is 0x5231E0 (same as headful).
|
| 512 |
+
display_layer_color_init(wrapper);
|
| 513 |
+
|
| 514 |
+
// ββ Display vtable slot 5 (offset 0x14) ββ
|
| 515 |
+
// Original: CALL EAX (vtable[5]), saves return value in ESI for use as
|
| 516 |
+
// the `output` parameter in the color-entries GfxResource__Create call below.
|
| 517 |
+
let layer_ctx = (*(*ddgame).display).set_active_layer(1);
|
| 518 |
+
|
| 519 |
+
// ββ GfxDir color entries DDGame+0x730C..0x732C ββ
|
| 520 |
+
// Original logic: if gfx_mode!=0, try GfxResource__Create for colours.img.
|
| 521 |
+
// If gfx_mode==0 OR resource creation fails, fall back to LoadSprites.
|
| 522 |
+
// The fallback's 4th param is primary_gfx_dir when gfx_mode==0, or 0 on resource fail.
|
| 523 |
+
if (*wrapper).gfx_mode != 0 {
|
| 524 |
+
let res = gfx_resource_create(
|
| 525 |
+
(*wrapper).primary_gfx_dir,
|
| 526 |
+
rb(va::STR_COLOURS_IMG) as *const core::ffi::c_char,
|
| 527 |
+
layer_ctx,
|
| 528 |
+
);
|
| 529 |
+
if !res.is_null() {
|
| 530 |
+
let rvt = *(res as *const *const u32);
|
| 531 |
+
let get_color: unsafe extern "thiscall" fn(*mut u8, u32, u32) -> u32 =
|
| 532 |
+
core::mem::transmute(*rvt.add(4));
|
| 533 |
+
for i in 0..9u32 {
|
| 534 |
+
(*ddgame).gfx_color_table[i as usize] = get_color(res, i, 0);
|
| 535 |
+
}
|
| 536 |
+
let release: unsafe extern "thiscall" fn(*mut u8, u8) =
|
| 537 |
+
core::mem::transmute(*rvt.add(3));
|
| 538 |
+
release(res, 1);
|
| 539 |
+
} else {
|
| 540 |
+
// Resource creation failed β fallback with param4=0
|
| 541 |
+
wa_load_sprites(
|
| 542 |
+
wrapper,
|
| 543 |
+
(*ddgame).gfx_sprite_data.as_mut_ptr(),
|
| 544 |
+
(*game_info).display_flags,
|
| 545 |
+
0,
|
| 546 |
+
layer_ctx,
|
| 547 |
+
);
|
| 548 |
+
}
|
| 549 |
+
} else {
|
| 550 |
+
// gfx_mode==0 (headless): fallback LoadSprites with param4=primary_gfx_dir
|
| 551 |
+
wa_load_sprites(
|
| 552 |
+
wrapper,
|
| 553 |
+
(*ddgame).gfx_sprite_data.as_mut_ptr(),
|
| 554 |
+
(*game_info).display_flags,
|
| 555 |
+
(*wrapper).primary_gfx_dir as u32,
|
| 556 |
+
layer_ctx,
|
| 557 |
+
);
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
// ββ Secondary GfxDir object (DDGame+0x2C, conditional) ββ
|
| 561 |
+
if !(*wrapper).secondary_gfx_dir.is_null() {
|
| 562 |
+
let gfxdir2 = wa_malloc_zeroed(0x70C);
|
| 563 |
+
*(gfxdir2 as *mut u16) = 1;
|
| 564 |
+
*(gfxdir2.add(2) as *mut u16) = 0x5A;
|
| 565 |
+
// FUN_5411A0: usercall(EAX=gfxdir2), plain RET
|
| 566 |
+
call_usercall_eax(gfxdir2 as *mut DDGameWrapper, rb(va::PALETTE_CONTEXT_INIT));
|
| 567 |
+
*(gfxdir2.add(0x708) as *mut u16) = 0;
|
| 568 |
+
(*ddgame).secondary_gfxdir = gfxdir2;
|
| 569 |
+
// param4=0 so the ESI-dependent block is skipped; layer_ctx doesn't matter
|
| 570 |
+
wa_load_sprites(
|
| 571 |
+
wrapper,
|
| 572 |
+
(*ddgame).gfx_sprite_data.as_mut_ptr(),
|
| 573 |
+
(*game_info).display_flags,
|
| 574 |
+
0,
|
| 575 |
+
core::ptr::null_mut(),
|
| 576 |
+
);
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
// ββ DDGameWrapper field inits ββ
|
| 580 |
+
(*wrapper).loading_progress = 0;
|
| 581 |
+
if is_headless {
|
| 582 |
+
(*wrapper).loading_total = 0x2AD;
|
| 583 |
+
} else {
|
| 584 |
+
let team_count = (*game_info).speech_team_count as u32;
|
| 585 |
+
(*wrapper).loading_total = team_count * 0x38 + 0x7E + 0x2AD;
|
| 586 |
+
}
|
| 587 |
+
(*wrapper).loading_last_pct = 0xFFFFFF9C; // -100: forces first progress bar update
|
| 588 |
+
(*wrapper).speech_name_count = 0;
|
| 589 |
+
|
| 590 |
+
// ββ Audio init (non-headless + sound available) ββ
|
| 591 |
+
if !is_headless {
|
| 592 |
+
// FUN_570F30: usercall(ESI=wrapper)
|
| 593 |
+
call_usercall_esi(wrapper, FUN_570F30_ADDR);
|
| 594 |
+
if !(*ddgame).sound.is_null() {
|
| 595 |
+
wa_load_effect_wavs(wrapper);
|
| 596 |
+
// DSSound_LoadAllSpeechBanks: the original is hooked to our Rust
|
| 597 |
+
// replacement (speech.rs), so the usercall bridge calls our code.
|
| 598 |
+
call_usercall_esi(wrapper, LOAD_SPEECH_BANKS_ADDR);
|
| 599 |
+
// Allocate ActiveSoundTable (0x608 bytes)
|
| 600 |
+
let ast = wa_malloc(0x608) as *mut ActiveSoundTable;
|
| 601 |
+
core::ptr::write_bytes(ast as *mut u8, 0, 0x600);
|
| 602 |
+
(*ast).ddgame = ddgame;
|
| 603 |
+
(*ast).counter = 0;
|
| 604 |
+
(*ddgame).active_sounds = ast;
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
// ββ GfxResource for masks.img ββ
|
| 609 |
+
// The original constructor uses a stack-local PaletteContext buffer (ESP+0x1A4)
|
| 610 |
+
// initialized at the start with (word=1, word=0xFE, PaletteContext__Init).
|
| 611 |
+
// This same buffer is reused as the output param for GfxResource__Create(masks.img).
|
| 612 |
+
// We replicate this initialization here. Size 0x900 matches the stack allocation.
|
| 613 |
+
let gfx_resource: *mut u8;
|
| 614 |
+
{
|
| 615 |
+
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 616 |
+
// Create PaletteContext the same way the original DDGame constructor does:
|
| 617 |
+
// word[0]=1, word[1]=0xFE, then PaletteContext__Init (0x5411A0)
|
| 618 |
+
let palette_ctx = wa_malloc_zeroed(0x900);
|
| 619 |
+
*(palette_ctx as *mut u16) = 1;
|
| 620 |
+
*(palette_ctx.add(2) as *mut u16) = 0xFE;
|
| 621 |
+
call_usercall_eax(palette_ctx as *mut DDGameWrapper, rb(va::PALETTE_CONTEXT_INIT));
|
| 622 |
+
gfx_resource =
|
| 623 |
+
gfx_resource_create(gfx_dir, rb(va::STR_MASKS_IMG) as *const core::ffi::c_char, palette_ctx);
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
// ββ Dump GfxResource for A/B comparison ββ
|
| 627 |
+
if !gfx_resource.is_null() {
|
| 628 |
+
let use_orig = std::env::var("OPENWA_USE_ORIG_CTOR").is_ok();
|
| 629 |
+
let tag = if use_orig { "orig" } else { "rust" };
|
| 630 |
+
// Dump GfxResource object + first sub-object
|
| 631 |
+
let gr_data = core::slice::from_raw_parts(gfx_resource, 0x100);
|
| 632 |
+
let _ = std::fs::write(format!("gfx_resource_{}.bin", tag), gr_data);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
// ββ PCLandscape (alloc 0xB44, stdcall 11 params) ββ
|
| 636 |
+
// Temporary output buffers for landscape coordinate data (used later for coord_list).
|
| 637 |
+
// These were stack locals in the original code (aiStack_978, iStack_11f9).
|
| 638 |
+
let mut landscape_coords_buf = [0u32; 0x400]; // coord output: pairs of (x, y)
|
| 639 |
+
let mut landscape_byte_buf = [0u8; 0x100]; // byte output
|
| 640 |
+
let mut landscape_temp = [0u32; 0x400]; // [0] = coord count, rest = temp data
|
| 641 |
+
|
| 642 |
+
let landscape = {
|
| 643 |
+
let alloc = wa_malloc_zeroed(0xB44);
|
| 644 |
+
if !alloc.is_null() {
|
| 645 |
+
let result = wa_pc_landscape_ctor(
|
| 646 |
+
alloc,
|
| 647 |
+
ddgame,
|
| 648 |
+
gfx_resource,
|
| 649 |
+
(*wrapper).display,
|
| 650 |
+
(*game_info).landscape_data_path.as_ptr(),
|
| 651 |
+
landscape_byte_buf.as_mut_ptr(),
|
| 652 |
+
(*wrapper).gfx_mode,
|
| 653 |
+
landscape_temp.as_mut_ptr(),
|
| 654 |
+
landscape_coords_buf.as_mut_ptr(),
|
| 655 |
+
&raw mut (*ddgame).level_width_raw,
|
| 656 |
+
&raw mut (*ddgame).level_height_raw,
|
| 657 |
+
);
|
| 658 |
+
(*wrapper).landscape = result as *mut PCLandscape;
|
| 659 |
+
(*ddgame).landscape = result as *mut PCLandscape;
|
| 660 |
+
result
|
| 661 |
+
} else {
|
| 662 |
+
(*wrapper).landscape = core::ptr::null_mut();
|
| 663 |
+
(*ddgame).landscape = core::ptr::null_mut();
|
| 664 |
+
core::ptr::null_mut()
|
| 665 |
+
}
|
| 666 |
+
};
|
| 667 |
+
|
| 668 |
+
// ββ BitGrid at DDGame+0x380 (alloc 0x4C, memset 0x2C) ββ
|
| 669 |
+
// Pure Rust: allocate object, call bit_grid_init, override vtable.
|
| 670 |
+
{
|
| 671 |
+
let bit_grid = wa_malloc(0x4C);
|
| 672 |
+
core::ptr::write_bytes(bit_grid, 0, 0x2C);
|
| 673 |
+
if !bit_grid.is_null() {
|
| 674 |
+
let width = (*ddgame).level_width;
|
| 675 |
+
let height = (*ddgame).level_height;
|
| 676 |
+
bit_grid_init(bit_grid, 1, width, height);
|
| 677 |
+
*(bit_grid as *mut u32) = rb(va::BIT_GRID_VARIANT_VTABLE);
|
| 678 |
+
}
|
| 679 |
+
(*ddgame).bit_grid = bit_grid;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
// ββ 8Γ SpriteRegion at DDGame+0x46C..0x488 ββ
|
| 683 |
+
// SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18
|
| 684 |
+
{
|
| 685 |
+
// (array_index, ECX, EDX, p2, p3, p4, p5, p6=gfx_resource)
|
| 686 |
+
// p6 is the GfxResource pointer returned by GfxResource__Create_Maybe.
|
| 687 |
+
let gr = gfx_resource as u32;
|
| 688 |
+
let regions: [(usize, u32, u32, u32, u32, u32, u32, u32); 8] = [
|
| 689 |
+
(2, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, gr),
|
| 690 |
+
(0, 0x30, 0x0C, 0x2D, 0x07, 0x34, 0x09, gr),
|
| 691 |
+
(1, 0x11, 0x1A, 0x0D, 0x0A, 0x16, 0x13, gr),
|
| 692 |
+
(3, 0x0C, 0x3D, 0x00, 0x20, 0x18, 0x33, gr),
|
| 693 |
+
(4, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, gr),
|
| 694 |
+
(6, 0x1A2, 0x1B, 0x173, 0x09, 0x1D8, 0x03, gr),
|
| 695 |
+
(7, 0x1EF, 0x26, 0x1E5, 0x07, 0x1F9, 0x16, gr),
|
| 696 |
+
(5, 0x2D, 0x08, 0x2D, 0x07, 0x2E, 0x07, gr),
|
| 697 |
+
];
|
| 698 |
+
|
| 699 |
+
for &(idx, ecx, edx, p2, p3, p4, p5, p6) in ®ions {
|
| 700 |
+
let alloc = wa_malloc_zeroed(0x9C);
|
| 701 |
+
let result = if !alloc.is_null() {
|
| 702 |
+
call_sprite_region_ctor(alloc, ecx, edx, p2, p3, p4, p5, p6)
|
| 703 |
+
} else {
|
| 704 |
+
core::ptr::null_mut()
|
| 705 |
+
};
|
| 706 |
+
(*ddgame).sprite_regions[idx] = result;
|
| 707 |
+
}
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
// ββ Landscape property at DDGame+0x468 (PCLandscape vtable[0xB]) ββ
|
| 711 |
+
if !landscape.is_null() {
|
| 712 |
+
let land_vt = *(landscape as *const *const u32);
|
| 713 |
+
let get_val: unsafe extern "thiscall" fn(*mut u8) -> u32 =
|
| 714 |
+
core::mem::transmute(*land_vt.add(0xB));
|
| 715 |
+
(*ddgame).landscape_property = get_val(landscape);
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
// NOTE: gfx_resource is NOT released here β arrow SpriteRegions need it.
|
| 719 |
+
// It's released after the arrow loop below.
|
| 720 |
+
|
| 721 |
+
// ββ Arrow sprites + collision regions (32 iterations) ββ
|
| 722 |
+
{
|
| 723 |
+
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 724 |
+
|
| 725 |
+
for i in 0u32..32 {
|
| 726 |
+
// Format "arrow%02u.img\0" into stack buffer
|
| 727 |
+
let mut name_buf = *b"arrow00.img\0\0\0\0\0";
|
| 728 |
+
name_buf[5] = b'0' + (i / 10) as u8;
|
| 729 |
+
name_buf[6] = b'0' + (i % 10) as u8;
|
| 730 |
+
|
| 731 |
+
let layer_ctx = (*(*ddgame).display).set_active_layer(1);
|
| 732 |
+
|
| 733 |
+
let entry = gfx_dir_find_entry(name_buf.as_ptr().cast(), gfx_dir);
|
| 734 |
+
|
| 735 |
+
let sprite: *mut u8;
|
| 736 |
+
if !entry.is_null() {
|
| 737 |
+
// Try gfx_dir->vtable[2](entry->field_4)
|
| 738 |
+
let gfx_vt = *(gfx_dir as *const *const u32);
|
| 739 |
+
let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
|
| 740 |
+
core::mem::transmute(*gfx_vt.add(2));
|
| 741 |
+
let entry_val = *(entry.add(4) as *const u32);
|
| 742 |
+
let cached = load_cached(gfx_dir, entry_val);
|
| 743 |
+
if !cached.is_null() {
|
| 744 |
+
sprite = wa_displaygfx_ctor(cached);
|
| 745 |
+
} else {
|
| 746 |
+
// Fallback: load from file via GfxDir__LoadImage + IMG_Decode
|
| 747 |
+
sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx);
|
| 748 |
+
}
|
| 749 |
+
} else {
|
| 750 |
+
// Entry not found β try direct file load
|
| 751 |
+
sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr().cast(), layer_ctx);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
// Store arrow sprite at DDGame+0x38+i*4
|
| 755 |
+
(*ddgame).arrow_sprites[i as usize] = sprite;
|
| 756 |
+
|
| 757 |
+
// Calculate collision region dimensions from sprite.
|
| 758 |
+
// The original creates a CENTERED collision box with 10px margin:
|
| 759 |
+
// left/top margin = max(0, dim/2 - 10)
|
| 760 |
+
// right/bottom = dim - margin
|
| 761 |
+
// Result: this[5]=this[6]=10 for sprites >20px (the 10px inset).
|
| 762 |
+
if !sprite.is_null() {
|
| 763 |
+
let tsm = &*(sprite as *const BitGrid);
|
| 764 |
+
let sprite_w = tsm.width as i32;
|
| 765 |
+
let sprite_h = tsm.height as i32;
|
| 766 |
+
let margin_w = (sprite_w / 2 - 10).max(0);
|
| 767 |
+
let margin_h = (sprite_h / 2 - 10).max(0);
|
| 768 |
+
|
| 769 |
+
// Create SpriteRegion for collision
|
| 770 |
+
let alloc = wa_malloc_zeroed(0x9C);
|
| 771 |
+
let region = if !alloc.is_null() {
|
| 772 |
+
call_sprite_region_ctor(
|
| 773 |
+
alloc,
|
| 774 |
+
(sprite_w / 2) as u32, // ECX β this[5] = ECX - p2
|
| 775 |
+
(sprite_h - margin_h) as u32, // EDX β this[4] = EDX - p3
|
| 776 |
+
margin_w as u32, // p2 (left margin)
|
| 777 |
+
margin_h as u32, // p3 (top margin)
|
| 778 |
+
(sprite_w - margin_w) as u32, // p4 β this[3] = p4 - p2
|
| 779 |
+
(sprite_h / 2) as u32, // p5 β this[6] = p5 - p3
|
| 780 |
+
sprite as u32, // p6 = arrow sprite (NOT landscape gfx_resource)
|
| 781 |
+
)
|
| 782 |
+
} else {
|
| 783 |
+
core::ptr::null_mut()
|
| 784 |
+
};
|
| 785 |
+
(*ddgame).arrow_collision_regions[i as usize] = region;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
// Arrow GfxDir (conditional on secondary gfxdir)
|
| 789 |
+
if !(*ddgame).secondary_gfxdir.is_null() {
|
| 790 |
+
let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
|
| 791 |
+
core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
|
| 792 |
+
(*ddgame).arrow_gfxdirs[i as usize] =
|
| 793 |
+
gfx_resource_create(gfx_dir, core::ptr::null_mut());
|
| 794 |
+
}
|
| 795 |
+
}
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
// Release gfx_resource AFTER arrow loop (arrows need it for SpriteRegions)
|
| 799 |
+
// vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
|
| 800 |
+
if !gfx_resource.is_null() {
|
| 801 |
+
let rvt = *(gfx_resource as *const *const u32);
|
| 802 |
+
let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*rvt.add(3));
|
| 803 |
+
release(gfx_resource, 1);
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
// ββ DisplayGfx at DDGame+0x138 ββ
|
| 807 |
+
{
|
| 808 |
+
let tsm = wa_malloc(0x4C);
|
| 809 |
+
core::ptr::write_bytes(tsm, 0, 0x2C);
|
| 810 |
+
if !tsm.is_null() {
|
| 811 |
+
bit_grid_init(tsm, 8, 0x100, 0x1E0);
|
| 812 |
+
*(tsm as *mut u32) = rb(va::DISPLAY_GFX_VTABLE);
|
| 813 |
+
}
|
| 814 |
+
(*ddgame).display_gfx = tsm;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
// ββ CoordList at DDGame+0x50C (capacity 600, 0x12C0 buffer) ββ
|
| 818 |
+
{
|
| 819 |
+
let cl = wa_malloc(core::mem::size_of::<CoordList>() as u32) as *mut CoordList;
|
| 820 |
+
(*cl).count = 0;
|
| 821 |
+
(*cl).capacity = 600;
|
| 822 |
+
let data = wa_malloc_zeroed(600 * core::mem::size_of::<CoordListEntry>() as u32)
|
| 823 |
+
as *mut CoordListEntry;
|
| 824 |
+
(*cl).data = data;
|
| 825 |
+
(*ddgame).coord_list = cl;
|
| 826 |
+
|
| 827 |
+
// Populate coord_list from PCLandscape's coordinate output.
|
| 828 |
+
// landscape_temp[0] = coordinate count, landscape_coords_buf = pairs of (x, y).
|
| 829 |
+
// Original packs as: coord = x * 0x10000 + y (Fixed-point).
|
| 830 |
+
// Duplicates are skipped.
|
| 831 |
+
let coord_count = landscape_temp[0];
|
| 832 |
+
for j in 0..coord_count as usize {
|
| 833 |
+
let x = landscape_coords_buf[j * 2];
|
| 834 |
+
let y = landscape_coords_buf[j * 2 + 1];
|
| 835 |
+
let coord_val = x.wrapping_mul(0x10000).wrapping_add(y);
|
| 836 |
+
let cur_count = (*cl).count as usize;
|
| 837 |
+
if cur_count >= 600 {
|
| 838 |
+
break;
|
| 839 |
+
}
|
| 840 |
+
// Check for duplicates
|
| 841 |
+
let mut dup = false;
|
| 842 |
+
for k in 0..cur_count {
|
| 843 |
+
if (*data.add(k)).coord == coord_val {
|
| 844 |
+
dup = true;
|
| 845 |
+
break;
|
| 846 |
+
}
|
| 847 |
+
}
|
| 848 |
+
if !dup {
|
| 849 |
+
(*data.add(cur_count)).coord = coord_val;
|
| 850 |
+
(*data.add(cur_count)).flag = 1;
|
| 851 |
+
(*cl).count = (cur_count + 1) as u32;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
// Temporary landscape buffers (stack arrays) are dropped automatically here.
|
| 857 |
+
|
| 858 |
+
// ββ Loading progress ticks (2 of 4 β before load_resource_list) ββ
|
| 859 |
+
call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
|
| 860 |
+
call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
|
| 861 |
+
|
| 862 |
+
// ββ Sprite resource loading via DDGameWrapper vtable[0] ββ
|
| 863 |
+
// DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
|
| 864 |
+
// 5 stack params (layer, gfx_dir, base_path, data_table, table_size)
|
| 865 |
+
{
|
| 866 |
+
let landscape_ptr = (*wrapper).landscape;
|
| 867 |
+
let water_layer = (*landscape_ptr).water_gfx_dir;
|
| 868 |
+
let land_layer = (*landscape_ptr).level_gfx_dir;
|
| 869 |
+
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 870 |
+
|
| 871 |
+
let wrapper_vt = *(wrapper as *const *const u32);
|
| 872 |
+
let load_resource_list: unsafe extern "thiscall" fn(
|
| 873 |
+
*mut DDGameWrapper,
|
| 874 |
+
u32,
|
| 875 |
+
*mut u8,
|
| 876 |
+
*const u8,
|
| 877 |
+
*const u8,
|
| 878 |
+
u32,
|
| 879 |
+
) = core::mem::transmute(*wrapper_vt);
|
| 880 |
+
// Load resources for layer 1 (main sprites)
|
| 881 |
+
load_resource_list(
|
| 882 |
+
wrapper,
|
| 883 |
+
1,
|
| 884 |
+
gfx_dir,
|
| 885 |
+
rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8, // base path
|
| 886 |
+
rb(va::SPRITE_RESOURCE_TABLE_1) as *const u8,
|
| 887 |
+
0x1D88, // table size
|
| 888 |
+
);
|
| 889 |
+
// Set global flag based on game version
|
| 890 |
+
let gv = (*(*ddgame).game_info).game_version;
|
| 891 |
+
*(rb(va::G_SPRITE_VERSION_FLAG) as *mut u32) = if gv < 8 { 0 } else { 0x10 };
|
| 892 |
+
|
| 893 |
+
// Load resources for layer 1 with different table
|
| 894 |
+
load_resource_list(
|
| 895 |
+
wrapper,
|
| 896 |
+
1,
|
| 897 |
+
gfx_dir,
|
| 898 |
+
rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
|
| 899 |
+
rb(va::SPRITE_RESOURCE_TABLE_2) as *const u8,
|
| 900 |
+
0x18,
|
| 901 |
+
);
|
| 902 |
+
|
| 903 |
+
// Load resources for layer 2 (water)
|
| 904 |
+
load_resource_list(
|
| 905 |
+
wrapper,
|
| 906 |
+
2,
|
| 907 |
+
water_layer,
|
| 908 |
+
rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
|
| 909 |
+
rb(va::WATER_RESOURCE_TABLE) as *const u8,
|
| 910 |
+
0x2F4,
|
| 911 |
+
);
|
| 912 |
+
|
| 913 |
+
let disp = (*wrapper).display;
|
| 914 |
+
(*disp).set_active_layer(3);
|
| 915 |
+
|
| 916 |
+
// back.spr and debris.spr must be loaded unconditionally β they're used by
|
| 917 |
+
// GenerateDebrisParticles (0x546F70) for particle effects, which affects
|
| 918 |
+
// the game RNG (DDGame+0x45EC). The original constructor loads them even
|
| 919 |
+
// in headless mode. Skipping them causes replay desync.
|
| 920 |
+
(*disp).load_sprite_by_layer(
|
| 921 |
+
3,
|
| 922 |
+
0x26D,
|
| 923 |
+
land_layer,
|
| 924 |
+
c"back.spr".as_ptr().cast(),
|
| 925 |
+
);
|
| 926 |
+
// debris.spr must be loaded unconditionally β it's used by
|
| 927 |
+
// GenerateDebrisParticles (0x546F70) for particle effects, which
|
| 928 |
+
// affects the game RNG (DDGame+0x45EC). Skipping it in headless
|
| 929 |
+
// mode causes desync (longbow replay checksum mismatch at frame 1350).
|
| 930 |
+
(*disp).load_sprite(3, 0x26E, 0, land_layer, c"debris.spr".as_ptr());
|
| 931 |
+
|
| 932 |
+
(*disp).load_sprite_by_layer(
|
| 933 |
+
2,
|
| 934 |
+
0x26C,
|
| 935 |
+
water_layer,
|
| 936 |
+
c"layer\\layer.spr|layer.spr".as_ptr().cast(),
|
| 937 |
+
);
|
| 938 |
+
|
| 939 |
+
(*ddgame).gradient_image_2 = core::ptr::null_mut();
|
| 940 |
+
|
| 941 |
+
// ββ Gradient image (0x030) ββ
|
| 942 |
+
let level_height = (*ddgame).level_height as i32;
|
| 943 |
+
let layer3_ctx = (*disp).set_active_layer(3);
|
| 944 |
+
let s_var1 = *(layer3_ctx.add(0x606) as *const i16);
|
| 945 |
+
|
| 946 |
+
if s_var1 < 0x61 && level_height == 0x2B8 {
|
| 947 |
+
// Simple gradient: load gradient.img directly
|
| 948 |
+
let gradient = call_gfx_find_and_load(land_layer, c"gradient.img", layer3_ctx);
|
| 949 |
+
(*ddgame).gradient_image = gradient;
|
| 950 |
+
} else {
|
| 951 |
+
compute_complex_gradient(ddgame, land_layer, layer3_ctx, s_var1);
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
// ββ Fill image β fill_pixel (0x7338) ββ
|
| 955 |
+
{
|
| 956 |
+
let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
|
| 957 |
+
// In the original, fill.img uses piStack_126c which the decompiler
|
| 958 |
+
// shows was set from piVar3 (water_layer from landscape+0xB38).
|
| 959 |
+
let fill_sprite = call_gfx_find_and_load(water_layer, c"fill.img", layer2_ctx);
|
| 960 |
+
if !fill_sprite.is_null() {
|
| 961 |
+
// Get pixel value: fill_sprite->vtable[4](0, 0)
|
| 962 |
+
let fill_vt = *(fill_sprite as *const *const u32);
|
| 963 |
+
let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
|
| 964 |
+
core::mem::transmute(*fill_vt.add(4));
|
| 965 |
+
(*ddgame).fill_pixel = get_pixel(fill_sprite, 0, 0);
|
| 966 |
+
// Release fill sprite: vtable[3] = DisplayGfx__vmethod_3(this, param_2=1)
|
| 967 |
+
let release: unsafe extern "thiscall" fn(*mut u8, u8) =
|
| 968 |
+
core::mem::transmute(*fill_vt.add(3));
|
| 969 |
+
release(fill_sprite, 1);
|
| 970 |
+
}
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
// ββ DDGame__LoadHudAndWeaponSprites (0x53D0E0) ββ
|
| 974 |
+
wa_load_hud_sprites(gfx_dir, ddgame, (*wrapper).secondary_gfx_dir);
|
| 975 |
+
|
| 976 |
+
// ββ DDGame__InitPaletteGradientSprites (0x5706D0) ββ
|
| 977 |
+
// Creates DisplayGfx objects at DDGame+0x41C.. for each team's palette
|
| 978 |
+
// gradient data from GameInfo. stdcall(wrapper), RET 0x4.
|
| 979 |
+
wa_init_palette_gradient_sprites(wrapper);
|
| 980 |
+
|
| 981 |
+
// ββ DisplayGfx__InitTeamPaletteDisplayObjects (0x5703E0) ββ
|
| 982 |
+
// Creates team palette gradient display objects. Reads DDGame+0x7338
|
| 983 |
+
// (fill_pixel), creates BitGrid+DisplayGfx per team.
|
| 984 |
+
// stdcall(wrapper), RET 0x4.
|
| 985 |
+
let init_team_palette_display: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
|
| 986 |
+
core::mem::transmute(rb(va::DISPLAY_GFX_INIT_TEAM_PALETTE_DISPLAY));
|
| 987 |
+
init_team_palette_display(wrapper);
|
| 988 |
+
}
|
| 989 |
+
// ββ Gradient image stub (DDGame+0x30) ββ
|
| 990 |
+
// Minimal stub: [6]=0 (zero-width) so CTaskLand skips the gradient column loop.
|
| 991 |
+
if (*ddgame).gradient_image.is_null() {
|
| 992 |
+
let obj = wa_malloc_zeroed(core::mem::size_of::<BitGrid>() as u32) as *mut BitGrid;
|
| 993 |
+
if !obj.is_null() {
|
| 994 |
+
(*obj).vtable = rb(va::BIT_GRID_VTABLE);
|
| 995 |
+
// height = 0 β CTaskLand skips the gradient column loop
|
| 996 |
+
(*ddgame).gradient_image = obj as *mut u8;
|
| 997 |
+
}
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
// ββ Release primary GfxHandler (vtable[3] = release, param 1 = free) ββ
|
| 1001 |
+
let gfx_dir_4c0 = (*wrapper).primary_gfx_dir;
|
| 1002 |
+
if !gfx_dir_4c0.is_null() {
|
| 1003 |
+
let gfx_vt = *(gfx_dir_4c0 as *const *const u32);
|
| 1004 |
+
let release: unsafe extern "thiscall" fn(*mut u8, u32) =
|
| 1005 |
+
core::mem::transmute(*gfx_vt.add(3));
|
| 1006 |
+
release(gfx_dir_4c0, 1);
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
if !is_headless {
|
| 1010 |
+
wa_init_display_final((*wrapper).display);
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
// ββ FUN_00570A90 (second call, conditional) ββ
|
| 1014 |
+
if *(rb(va::G_DISPLAY_MODE_FLAG) as *const core::ffi::c_char) == 0 {
|
| 1015 |
+
call_usercall_eax(wrapper, FUN_570A90_ADDR);
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
// ββ Final display layer visibility ββ
|
| 1019 |
+
{
|
| 1020 |
+
let disp = (*wrapper).display;
|
| 1021 |
+
(*disp).set_layer_visibility(1, 0);
|
| 1022 |
+
(*disp).set_layer_visibility(2, 0);
|
| 1023 |
+
(*disp).set_layer_visibility(3, 1);
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
// Statics for usercall bridge addresses
|
| 1030 |
+
static mut FUN_570A90_ADDR: u32 = 0;
|
| 1031 |
+
static mut FUN_570F30_ADDR: u32 = 0;
|
| 1032 |
+
static mut LOAD_SPEECH_BANKS_ADDR: u32 = 0;
|
| 1033 |
+
static mut LOADING_PROGRESS_TICK_ADDR: u32 = 0;
|
| 1034 |
+
|
| 1035 |
+
/// Bridge: usercall(ESI=wrapper), plain RET. Used by FUN_570E20, FUN_570F30, LoadSpeechBanks.
|
| 1036 |
+
#[cfg(target_arch = "x86")]
|
| 1037 |
+
#[unsafe(naked)]
|
| 1038 |
+
unsafe extern "C" fn call_usercall_esi(_wrapper: *mut DDGameWrapper, _addr: u32) {
|
| 1039 |
+
core::arch::naked_asm!(
|
| 1040 |
+
"pushl %esi",
|
| 1041 |
+
"movl 8(%esp), %esi", // ESI = wrapper
|
| 1042 |
+
"movl 12(%esp), %eax", // EAX = target address
|
| 1043 |
+
"calll *%eax",
|
| 1044 |
+
"popl %esi",
|
| 1045 |
+
"retl",
|
| 1046 |
+
options(att_syntax),
|
| 1047 |
+
);
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
/// Bridge: usercall(EAX=wrapper), plain RET. Used by FUN_570A90.
|
| 1051 |
+
#[cfg(target_arch = "x86")]
|
| 1052 |
+
#[unsafe(naked)]
|
| 1053 |
+
unsafe extern "C" fn call_usercall_eax(_wrapper: *mut DDGameWrapper, _addr: u32) {
|
| 1054 |
+
core::arch::naked_asm!(
|
| 1055 |
+
"movl 4(%esp), %eax", // EAX = wrapper
|
| 1056 |
+
"movl 8(%esp), %ecx", // ECX = target address (temp)
|
| 1057 |
+
"calll *%ecx",
|
| 1058 |
+
"retl",
|
| 1059 |
+
options(att_syntax),
|
| 1060 |
+
);
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
/// Bridge: usercall(ECX=wrapper), plain RET. Used by FUN_5717A0.
|
| 1064 |
+
#[cfg(target_arch = "x86")]
|
| 1065 |
+
unsafe fn call_usercall_ecx(wrapper: *mut DDGameWrapper, addr: u32) {
|
| 1066 |
+
let f: unsafe extern "thiscall" fn(*mut DDGameWrapper) = core::mem::transmute(addr as usize);
|
| 1067 |
+
f(wrapper);
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
/// Bridge to SpriteRegion__Constructor (0x57DB20).
|
| 1071 |
+
/// Convention: fastcall(ECX, EDX) + 6 stack params, RET 0x18.
|
| 1072 |
+
#[cfg(target_arch = "x86")]
|
| 1073 |
+
static mut SPRITE_REGION_CTOR_ADDR: u32 = 0;
|
| 1074 |
+
|
| 1075 |
+
#[cfg(target_arch = "x86")]
|
| 1076 |
+
#[unsafe(naked)]
|
| 1077 |
+
unsafe extern "C" fn call_sprite_region_ctor(
|
| 1078 |
+
_this: *mut u8,
|
| 1079 |
+
_ecx: u32,
|
| 1080 |
+
_edx: u32,
|
| 1081 |
+
_p2: u32,
|
| 1082 |
+
_p3: u32,
|
| 1083 |
+
_p4: u32,
|
| 1084 |
+
_p5: u32,
|
| 1085 |
+
_p6: u32,
|
| 1086 |
+
) -> *mut u8 {
|
| 1087 |
+
// Stack on entry: [ret] [this] [ecx] [edx] [p2] [p3] [p4] [p5] [p6]
|
| 1088 |
+
// Need: ECX=ecx, EDX=edx, push p6 p5 p4 p3 p2 this, call
|
| 1089 |
+
// SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack, RET 0x18
|
| 1090 |
+
// Params: (this, ecx, edx, p2, p3, p4, p5, p6)
|
| 1091 |
+
core::arch::naked_asm!(
|
| 1092 |
+
"pushl %ebp",
|
| 1093 |
+
"pushl %ebx",
|
| 1094 |
+
// Load ECX and EDX (shifted by 2 pushes = 8 bytes)
|
| 1095 |
+
"movl 16(%esp), %ecx", // ecx param (offset 4+8=12? No: 0=ebx,4=ebp,8=ret,12=this,16=ecx)
|
| 1096 |
+
"movl 20(%esp), %edx", // edx param
|
| 1097 |
+
// Push 6 stack params in reverse: p6, p5, p4, p3, p2, this
|
| 1098 |
+
"pushl 40(%esp)", // p6 (0=ebx,4=ebp,8=ret,...,40=p6)
|
| 1099 |
+
"pushl 40(%esp)", // p5 (shifted by 4)
|
| 1100 |
+
"pushl 40(%esp)", // p4 (shifted by 8)
|
| 1101 |
+
"pushl 40(%esp)", // p3 (shifted by 12)
|
| 1102 |
+
"pushl 40(%esp)", // p2 (shifted by 16)
|
| 1103 |
+
"pushl 32(%esp)", // this (shifted by 20)
|
| 1104 |
+
"calll *({addr})", // fastcall, callee cleans 6Γ4=24 bytes
|
| 1105 |
+
"popl %ebx",
|
| 1106 |
+
"popl %ebp",
|
| 1107 |
+
"retl",
|
| 1108 |
+
addr = sym SPRITE_REGION_CTOR_ADDR,
|
| 1109 |
+
options(att_syntax),
|
| 1110 |
+
);
|
| 1111 |
+
}
|
crates/openwa-core/src/engine/mod.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
pub mod ddgame;
|
|
|
|
| 2 |
pub mod replay;
|
| 3 |
pub mod ddgame_wrapper;
|
| 4 |
pub mod game_info;
|
|
@@ -7,11 +8,17 @@ pub mod game_state_init;
|
|
| 7 |
pub mod game_timer;
|
| 8 |
pub mod net_bridge;
|
| 9 |
pub mod net_wrapper;
|
|
|
|
| 10 |
|
| 11 |
-
pub use ddgame::
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
};
|
| 16 |
pub use ddgame_wrapper::DDGameWrapper;
|
| 17 |
pub use game_info::GameInfo;
|
|
|
|
| 1 |
pub mod ddgame;
|
| 2 |
+
pub mod ddgame_constructor;
|
| 3 |
pub mod replay;
|
| 4 |
pub mod ddgame_wrapper;
|
| 5 |
pub mod game_info;
|
|
|
|
| 8 |
pub mod game_timer;
|
| 9 |
pub mod net_bridge;
|
| 10 |
pub mod net_wrapper;
|
| 11 |
+
pub mod team_arena;
|
| 12 |
|
| 13 |
+
pub use ddgame::DDGame;
|
| 14 |
+
pub use ddgame_constructor::{
|
| 15 |
+
create_ddgame, ddgame_init_fields, ddgame_init_render_indices,
|
| 16 |
+
display_layer_color_init, init_constructor_addrs, ON_DDGAME_ALLOC,
|
| 17 |
+
};
|
| 18 |
+
pub use team_arena::{
|
| 19 |
+
CoordEntry, CoordList, CoordListEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
|
| 20 |
+
RenderEntry, SoundQueueEntry, TeamArenaRef, TeamArenaState, TeamBlock, TeamHeader,
|
| 21 |
+
TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots, WormEntry,
|
| 22 |
};
|
| 23 |
pub use ddgame_wrapper::DDGameWrapper;
|
| 24 |
pub use game_info::GameInfo;
|
crates/openwa-core/src/engine/team_arena.rs
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// Team arena state β sub-struct at DDGame + 0x4628
|
| 3 |
+
// ============================================================
|
| 4 |
+
//
|
| 5 |
+
// Extracted from ddgame.rs: team/worm data structures, TeamArenaRef,
|
| 6 |
+
// and related helper structs used as DDGame fields.
|
| 7 |
+
|
| 8 |
+
use super::ddgame::offsets;
|
| 9 |
+
|
| 10 |
+
/// Coordinate entry used in DDGame screen coordinate tables (stride 0x10).
|
| 11 |
+
///
|
| 12 |
+
/// InitFields zeroes x and y; at runtime they contain fixed-point screen
|
| 13 |
+
/// coordinates used for camera tracking and rendering regions.
|
| 14 |
+
#[repr(C)]
|
| 15 |
+
#[derive(Clone, Copy)]
|
| 16 |
+
pub struct CoordEntry {
|
| 17 |
+
pub x: i32,
|
| 18 |
+
pub y: i32,
|
| 19 |
+
pub _unknown: [u8; 8],
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const _: () = assert!(core::mem::size_of::<CoordEntry>() == 0x10);
|
| 23 |
+
|
| 24 |
+
/// Team index permutation map (0x64 = 100 bytes).
|
| 25 |
+
///
|
| 26 |
+
/// Used for mapping team indices to rendering/turn slots. Three instances
|
| 27 |
+
/// live in DDGame at offsets 0x7650, 0x76B4, 0x7718 (stride 0x64).
|
| 28 |
+
/// Initialized as identity permutations [0,1,2,...,15] with count=16.
|
| 29 |
+
#[repr(C)]
|
| 30 |
+
#[derive(Clone, Copy)]
|
| 31 |
+
pub struct TeamIndexMap {
|
| 32 |
+
/// 16 team index entries (identity permutation on init).
|
| 33 |
+
pub entries: [i16; 16],
|
| 34 |
+
/// Number of active entries (initialized to 16).
|
| 35 |
+
pub count: u16,
|
| 36 |
+
/// Gap β unknown purpose, not initialized by constructor.
|
| 37 |
+
pub _gap: [u8; 64],
|
| 38 |
+
/// Terminator (initialized to 0).
|
| 39 |
+
pub terminator: u16,
|
| 40 |
+
}
|
| 41 |
+
const _: () = assert!(core::mem::size_of::<TeamIndexMap>() == 0x64);
|
| 42 |
+
|
| 43 |
+
/// Render table entry (0x14 = 20 bytes).
|
| 44 |
+
///
|
| 45 |
+
/// 14 entries live at DDGame+0x73B0 (stride 0x14). Only the first u32
|
| 46 |
+
/// is zeroed during construction; the rest is uninitialized/unknown.
|
| 47 |
+
#[repr(C)]
|
| 48 |
+
#[derive(Clone, Copy)]
|
| 49 |
+
pub struct RenderEntry {
|
| 50 |
+
/// Active/state flag (zeroed on init).
|
| 51 |
+
pub active: u32,
|
| 52 |
+
/// Unknown data.
|
| 53 |
+
pub _unknown: [u8; 16],
|
| 54 |
+
}
|
| 55 |
+
const _: () = assert!(core::mem::size_of::<RenderEntry>() == 0x14);
|
| 56 |
+
|
| 57 |
+
// ============================================================
|
| 58 |
+
// CoordList β dynamic array of packed terrain coordinates
|
| 59 |
+
// ============================================================
|
| 60 |
+
|
| 61 |
+
/// Packed terrain coordinate entry (8 bytes).
|
| 62 |
+
///
|
| 63 |
+
/// `coord` packs x and y as `x * 0x10000 + y` (fixed-point).
|
| 64 |
+
/// `flag` is always 1 for populated entries.
|
| 65 |
+
#[repr(C)]
|
| 66 |
+
#[derive(Clone, Copy)]
|
| 67 |
+
pub struct CoordListEntry {
|
| 68 |
+
pub coord: u32,
|
| 69 |
+
pub flag: u32,
|
| 70 |
+
}
|
| 71 |
+
const _: () = assert!(core::mem::size_of::<CoordListEntry>() == 8);
|
| 72 |
+
|
| 73 |
+
/// Dynamic coordinate array header (12 bytes).
|
| 74 |
+
///
|
| 75 |
+
/// Allocated at DDGame+0x50C during `init_graphics_and_resources`.
|
| 76 |
+
/// Data buffer is a separate allocation of `capacity * 8` bytes.
|
| 77 |
+
/// Used for terrain coordinate lookups (spawning, aiming, collision).
|
| 78 |
+
#[repr(C)]
|
| 79 |
+
pub struct CoordList {
|
| 80 |
+
/// Number of entries currently stored.
|
| 81 |
+
pub count: u32,
|
| 82 |
+
/// Maximum number of entries (600).
|
| 83 |
+
pub capacity: u32,
|
| 84 |
+
/// Pointer to the data buffer (`capacity * sizeof(CoordListEntry)` bytes).
|
| 85 |
+
pub data: *mut CoordListEntry,
|
| 86 |
+
}
|
| 87 |
+
const _: () = assert!(core::mem::size_of::<CoordList>() == 12);
|
| 88 |
+
|
| 89 |
+
// ============================================================
|
| 90 |
+
// Sound queue entry β 16 entries at DDGame + 0x7F00
|
| 91 |
+
// ============================================================
|
| 92 |
+
|
| 93 |
+
/// Sound queue entry (0x24 = 36 bytes, stride between consecutive entries).
|
| 94 |
+
///
|
| 95 |
+
/// DDGame maintains a 16-slot sound queue at offset 0x7F00. PlaySoundGlobal
|
| 96 |
+
/// appends entries; PlaySoundLocal additionally marks entries as local and
|
| 97 |
+
/// stores position via the task's secondary vtable.
|
| 98 |
+
#[repr(C)]
|
| 99 |
+
pub struct SoundQueueEntry {
|
| 100 |
+
/// Sound effect ID (SoundId enum value).
|
| 101 |
+
pub sound_id: u32,
|
| 102 |
+
/// Flags / priority (e.g. 3=weapon, 7=explosion).
|
| 103 |
+
pub flags: u32,
|
| 104 |
+
/// Volume (Fixed-point, 0x10000 = 1.0).
|
| 105 |
+
pub volume: u32,
|
| 106 |
+
/// Pitch (Fixed-point, 0x10000 = 1.0).
|
| 107 |
+
pub pitch: u32,
|
| 108 |
+
/// Reserved, always 0.
|
| 109 |
+
pub reserved: u32,
|
| 110 |
+
/// 0 = global, 1 = local (has position tracking).
|
| 111 |
+
pub is_local: u8,
|
| 112 |
+
pub _pad: [u8; 3],
|
| 113 |
+
/// Position X (filled by secondary vtable GetPosition for local sounds).
|
| 114 |
+
pub pos_x: u32,
|
| 115 |
+
/// Position Y (filled by secondary vtable GetPosition for local sounds).
|
| 116 |
+
pub pos_y: u32,
|
| 117 |
+
/// Pointer to task's secondary vtable (at task+0xE8) for position updates.
|
| 118 |
+
pub secondary_vtable: u32,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
|
| 122 |
+
|
| 123 |
+
// ============================================================
|
| 124 |
+
// Per-worm and per-team block structs
|
| 125 |
+
// ============================================================
|
| 126 |
+
|
| 127 |
+
/// Per-worm data entry (0x9C bytes, stride between consecutive worms).
|
| 128 |
+
///
|
| 129 |
+
/// WA supports up to 8 playable worms per team. The original code accesses
|
| 130 |
+
/// worms via raw pointer arithmetic from the team entry pointer, using
|
| 131 |
+
/// stride 0x9C. This means the 8th worm crosses the TeamBlock boundary
|
| 132 |
+
/// into the next block's header slot β see `TeamArenaRef::team_worm()`.
|
| 133 |
+
///
|
| 134 |
+
/// Field offsets confirmed by runtime memory dump (validator DLL):
|
| 135 |
+
/// - state at 0x00: 0x67 = active/selected, 0x65 = idle, 0x80+ = special
|
| 136 |
+
/// - active_flag at 0x0C: 1 for alive worms
|
| 137 |
+
/// - max_health at 0x58: initial health value (100)
|
| 138 |
+
/// - health at 0x5C: current health (100 = full)
|
| 139 |
+
/// - name at 0x78: null-terminated worm name string (~20 bytes)
|
| 140 |
+
#[repr(C)]
|
| 141 |
+
#[derive(Clone, Copy)]
|
| 142 |
+
pub struct WormEntry {
|
| 143 |
+
/// 0x00: Worm state machine state.
|
| 144 |
+
/// Values: 0x65=idle, 0x67=active/selected, 0x68=active variant.
|
| 145 |
+
/// Special states {0x80..0x85, 0x89} = dying/drowning/special animation.
|
| 146 |
+
pub state: u32,
|
| 147 |
+
/// 0x04: Unknown counter. Incremented by Freeze (+10).
|
| 148 |
+
pub effect_counter_04_Maybe: i32,
|
| 149 |
+
/// 0x08: Turn action counter. Incremented by weapon handlers:
|
| 150 |
+
/// Surrender (+14), Mail/Mine/Mole (+7), Freeze (+3).
|
| 151 |
+
pub turn_action_counter_Maybe: i32,
|
| 152 |
+
/// 0x0C: Active/alive flag (1 for alive worms in game, 0 otherwise).
|
| 153 |
+
pub active_flag: i32,
|
| 154 |
+
/// 0x10-0x57: Unknown
|
| 155 |
+
pub _unknown_10: [u8; 0x48],
|
| 156 |
+
/// 0x58: Max health (initial health value, typically 100).
|
| 157 |
+
pub max_health: i32,
|
| 158 |
+
/// 0x5C: Current health. Used by GetTeamTotalHealth.
|
| 159 |
+
pub health: i32,
|
| 160 |
+
/// 0x60-0x77: Unknown.
|
| 161 |
+
pub _unknown_60: [u8; 0x18],
|
| 162 |
+
/// 0x78: Worm name, null-terminated ASCII string (~20 bytes).
|
| 163 |
+
pub name: [u8; 0x18],
|
| 164 |
+
/// 0x90-0x9B: Unknown (zeroed in runtime dump for playable worms).
|
| 165 |
+
/// GetWormPosition reads pos_x/pos_y from +0x90/+0x94 via negative entry_ptr
|
| 166 |
+
/// arithmetic, but values appear transient β not populated at rest.
|
| 167 |
+
/// Actual worm positions live in CGameTask objects (+0x84/+0x88).
|
| 168 |
+
pub _unknown_90: [u8; 0x0C],
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
|
| 172 |
+
|
| 173 |
+
/// Team-level metadata stored at slot 0 of each TeamBlock (0x9C bytes).
|
| 174 |
+
///
|
| 175 |
+
/// This struct overlays the same memory as a WormEntry but interprets the
|
| 176 |
+
/// high offsets (0x6C+) as team metadata rather than worm data. The low
|
| 177 |
+
/// offsets (0x00-0x5F) may contain data from the previous team's 8th worm
|
| 178 |
+
/// when that team has 8 worms β they are treated as opaque padding.
|
| 179 |
+
///
|
| 180 |
+
/// Accessed via `TeamArenaRef::team_header()` and `team_header_b()`.
|
| 181 |
+
#[repr(C)]
|
| 182 |
+
#[derive(Clone, Copy)]
|
| 183 |
+
pub struct TeamHeader {
|
| 184 |
+
/// 0x00-0x5F: Opaque β may hold 8th worm data from previous team.
|
| 185 |
+
pub worm_overlap: [u8; 0x60],
|
| 186 |
+
/// 0x60-0x6B: Unknown padding.
|
| 187 |
+
pub _unknown_60: [u8; 0x0C],
|
| 188 |
+
/// 0x6C: Team eliminated flag (nonzero = eliminated).
|
| 189 |
+
pub eliminated: i32,
|
| 190 |
+
/// 0x70: Alliance ID used by CountTeamsByAlliance and SetActiveWorm_Maybe.
|
| 191 |
+
pub alliance: i32,
|
| 192 |
+
/// 0x74: Active worm index (0 = none, N = worm N is active).
|
| 193 |
+
pub active_worm: i32,
|
| 194 |
+
/// 0x78: Number of worms on this team.
|
| 195 |
+
pub worm_count: i32,
|
| 196 |
+
/// 0x7C: Per-team turn action flags (bitfield).
|
| 197 |
+
/// Skip Go (weapon 57) toggles a bit here based on the weapon's fire_params.
|
| 198 |
+
/// Bit is set to mark the team should skip; in game_version > 0x1C, toggling
|
| 199 |
+
/// again clears it.
|
| 200 |
+
pub turn_action_flags: u32,
|
| 201 |
+
/// 0x80: Alliance ID for ammo/delay table indexing (GetAmmo/AddAmmo/SubtractAmmo).
|
| 202 |
+
/// Teams with the same weapon_alliance share ammo pools. Distinct from
|
| 203 |
+
/// `alliance` at 0x70 which is used by CountTeamsByAlliance.
|
| 204 |
+
pub weapon_alliance: i32,
|
| 205 |
+
/// 0x84: Team name, null-terminated ASCII string.
|
| 206 |
+
pub team_name: [u8; 0x14],
|
| 207 |
+
/// 0x98: Unknown trailing bytes.
|
| 208 |
+
pub _unknown_98: [u8; 4],
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
const _: () = assert!(core::mem::size_of::<TeamHeader>() == 0x9C);
|
| 212 |
+
|
| 213 |
+
/// Union for slot 0 of a TeamBlock.
|
| 214 |
+
///
|
| 215 |
+
/// This slot is dual-purpose: its high offsets store team metadata
|
| 216 |
+
/// (`TeamHeader`), while its low offsets may contain the 8th worm
|
| 217 |
+
/// of the previous team (`WormEntry`). The two uses don't conflict
|
| 218 |
+
/// because worm data occupies 0x00-0x5F and header data starts at 0x6C.
|
| 219 |
+
#[repr(C)]
|
| 220 |
+
#[derive(Clone, Copy)]
|
| 221 |
+
pub union TeamSlot0 {
|
| 222 |
+
/// View as worm data (used when the previous team has 8 worms).
|
| 223 |
+
pub worm: core::mem::ManuallyDrop<WormEntry>,
|
| 224 |
+
/// View as team metadata (eliminated, alliance, worm_count, etc.).
|
| 225 |
+
pub team: core::mem::ManuallyDrop<TeamHeader>,
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/// Full per-team data block (0x51C bytes, 7 blocks in DDGame).
|
| 229 |
+
///
|
| 230 |
+
/// Each block starts with a `TeamSlot0` union (0x9C bytes) that serves
|
| 231 |
+
/// dual purpose: its high offsets hold team metadata (`TeamHeader`),
|
| 232 |
+
/// while its low offsets may contain the 8th worm of the previous team.
|
| 233 |
+
/// The remaining 7 worm slots follow, then 0x3C bytes of metadata.
|
| 234 |
+
///
|
| 235 |
+
/// **Block indexing**: Block 0 is unused (preamble, all zeros). Actual team
|
| 236 |
+
/// data starts at block 1. Worms are accessed via `TeamArenaRef::team_worm()`
|
| 237 |
+
/// which uses raw pointer arithmetic and naturally crosses block boundaries.
|
| 238 |
+
///
|
| 239 |
+
/// **Header access**: Use `TeamArenaRef::team_header(idx)` to get the
|
| 240 |
+
/// `TeamHeader` for a team (reads from `blocks[idx+1].header.team`).
|
| 241 |
+
#[repr(C)]
|
| 242 |
+
pub struct TeamBlock {
|
| 243 |
+
/// 0x000-0x09B: Header slot (union of TeamHeader and WormEntry).
|
| 244 |
+
/// Team metadata at high offsets; may hold 8th worm data at low offsets.
|
| 245 |
+
pub header: TeamSlot0,
|
| 246 |
+
/// 0x09C-0x4DF: 7 worm entries (slots 1-7, stride 0x9C)
|
| 247 |
+
pub worms: [WormEntry; 7],
|
| 248 |
+
/// 0x4E0-0x51B: Team metadata (0x3C bytes)
|
| 249 |
+
pub trailer: [u8; 0x3C],
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
const _: () = assert!(core::mem::size_of::<TeamBlock>() == 0x51C);
|
| 253 |
+
|
| 254 |
+
/// Per-alliance weapon ammo and delay data (0x238 = 568 bytes per alliance).
|
| 255 |
+
#[repr(C)]
|
| 256 |
+
pub struct TeamWeaponSlots {
|
| 257 |
+
/// Ammo counts per weapon (71 entries). -1 = unlimited, 0 = none, >0 = count.
|
| 258 |
+
pub ammo: [i32; 71],
|
| 259 |
+
/// Delay flags per weapon (71 entries). Nonzero = weapon on cooldown.
|
| 260 |
+
pub delay: [i32; 71],
|
| 261 |
+
}
|
| 262 |
+
const _: () = assert!(core::mem::size_of::<TeamWeaponSlots>() == 0x238);
|
| 263 |
+
|
| 264 |
+
/// Weapon slot data for all 6 alliances (0xD50 = 3408 bytes total).
|
| 265 |
+
#[repr(C)]
|
| 266 |
+
pub struct WeaponSlots {
|
| 267 |
+
pub teams: [TeamWeaponSlots; 6],
|
| 268 |
+
}
|
| 269 |
+
const _: () = assert!(core::mem::size_of::<WeaponSlots>() == 852 * 4);
|
| 270 |
+
|
| 271 |
+
/// Team arena state area within DDGame (at DDGame + 0x4628).
|
| 272 |
+
///
|
| 273 |
+
/// Contains per-team entries and an interleaved ammo/delay table.
|
| 274 |
+
/// Used by GetAmmo (0x5225E0), AddAmmo (0x522640), SubtractAmmo (0x522680).
|
| 275 |
+
///
|
| 276 |
+
/// The ammo/delay table uses stride 142 (= 71 weapons * 2) per alliance.
|
| 277 |
+
/// Within each alliance block of 142 entries:
|
| 278 |
+
/// - Entries 0..70 are ammo counts (accessed at base + 0x1EB4 + index * 4)
|
| 279 |
+
/// - Entries 71..141 are delay flags (accessed at base + 0x1FD0 + index * 4)
|
| 280 |
+
///
|
| 281 |
+
/// The original code accesses these as two arrays at different base offsets
|
| 282 |
+
/// (0x1EB4 and 0x1FD0) using the same index `alliance_id * 142 + weapon_id`.
|
| 283 |
+
/// Since 0x1FD0 - 0x1EB4 = 71 * 4, this is equivalent to accessing
|
| 284 |
+
/// `weapon_slots.teams[alliance].ammo[weapon]` and
|
| 285 |
+
/// `weapon_slots.teams[alliance].delay[weapon]`.
|
| 286 |
+
#[repr(C)]
|
| 287 |
+
pub struct TeamArenaState {
|
| 288 |
+
/// 0x0000-0x1EAF: Per-team data region (opaque).
|
| 289 |
+
///
|
| 290 |
+
/// This region contains 7 team entries at stride 0x51C (1-indexed, index 0
|
| 291 |
+
/// is preamble). The original WA.exe accesses team data via raw pointer
|
| 292 |
+
/// arithmetic relative to the arena base. In our Rust code, team data is
|
| 293 |
+
/// accessed through `TeamArenaRef`, `TeamBlock`, and `TeamHeader`,
|
| 294 |
+
/// so this region is treated as opaque padding.
|
| 295 |
+
///
|
| 296 |
+
/// The 7th entry (team 6, 1-indexed) only has 8 bytes before team_count;
|
| 297 |
+
/// the rest overlaps with weapon_slots.
|
| 298 |
+
pub team_blocks_region: [u8; 0x1EB0],
|
| 299 |
+
/// 0x1EB0: Number of teams in the game (used by team iteration loops)
|
| 300 |
+
pub team_count: i32,
|
| 301 |
+
/// 0x1EB4: Per-alliance weapon ammo and delay data.
|
| 302 |
+
/// 6 alliances, each with 71 ammo + 71 delay i32 entries.
|
| 303 |
+
/// Ammo: -1 = unlimited, 0 = none, >0 = count.
|
| 304 |
+
/// Delay: nonzero = weapon on delay.
|
| 305 |
+
pub weapon_slots: WeaponSlots,
|
| 306 |
+
/// 0x2C04: Padding between weapon_slots end and game_mode_flag.
|
| 307 |
+
/// weapon_slots: 852 * 4 = 3408 = 0xD50. 0x1EB4 + 0xD50 = 0x2C04.
|
| 308 |
+
pub _pad_2c04: [u8; 8],
|
| 309 |
+
/// 0x2C0C: Game mode flag (nonzero = override weapon delays in certain conditions)
|
| 310 |
+
pub game_mode_flag: i32,
|
| 311 |
+
/// 0x2C10: Unknown padding
|
| 312 |
+
pub _pad_2c10: [u8; 0x18],
|
| 313 |
+
/// 0x2C28: Game phase counter (>=484 = sudden death, >=-2 = normal game)
|
| 314 |
+
pub game_phase: i32,
|
| 315 |
+
|
| 316 |
+
// === Alliance tracking (set by CountTeamsByAlliance + SetActiveWorm_Maybe) ===
|
| 317 |
+
/// 0x2C2C: Current alliance being evaluated
|
| 318 |
+
pub current_alliance: i32,
|
| 319 |
+
/// 0x2C30: Count of teams with an active worm (set by SetActiveWorm_Maybe)
|
| 320 |
+
pub active_worm_count: i32,
|
| 321 |
+
/// 0x2C34: Count of active teams with valid alliance (>=0)
|
| 322 |
+
pub active_team_count: i32,
|
| 323 |
+
/// 0x2C38: Count of teams matching current_alliance
|
| 324 |
+
pub same_alliance_count: i32,
|
| 325 |
+
/// 0x2C3C: Count of teams not matching current_alliance
|
| 326 |
+
pub enemy_team_count: i32,
|
| 327 |
+
/// 0x2C40: Last team index set active (written by SetActiveWorm_Maybe)
|
| 328 |
+
pub last_active_team: i32,
|
| 329 |
+
/// 0x2C44: Alliance of last activated team (written by SetActiveWorm_Maybe)
|
| 330 |
+
pub last_active_alliance: i32,
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
const _: () = assert!(core::mem::size_of::<TeamArenaState>() == 0x2C48);
|
| 334 |
+
|
| 335 |
+
/// Worm state constants and helpers.
|
| 336 |
+
pub mod worm {
|
| 337 |
+
// --- Known state values (runtime-validated via validator dumps) ---
|
| 338 |
+
|
| 339 |
+
/// Transitional state checked by CheckWormState0x64 (0x5228D0). 11 xrefs.
|
| 340 |
+
/// Appears briefly during turn transitions.
|
| 341 |
+
pub const STATE_TRANSITIONAL: u32 = 0x64;
|
| 342 |
+
/// Idle β worm is waiting, not currently active.
|
| 343 |
+
pub const STATE_IDLE: u32 = 0x65;
|
| 344 |
+
/// Active β worm is currently selected and taking its turn.
|
| 345 |
+
pub const STATE_ACTIVE: u32 = 0x67;
|
| 346 |
+
/// Dead β worm has been killed (hp=0). Persists across turns.
|
| 347 |
+
pub const STATE_DEAD: u32 = 0x87;
|
| 348 |
+
|
| 349 |
+
/// Special worm states β worm is dying/drowning/in special animation.
|
| 350 |
+
/// Checked by IsWormInSpecialState (0x5226B0).
|
| 351 |
+
/// All values in the 0x80+ range. 0x87 (dead) is also in this set.
|
| 352 |
+
pub const SPECIAL_STATES: [u32; 6] = [0x80, 0x81, 0x82, 0x83, 0x85, 0x89];
|
| 353 |
+
|
| 354 |
+
/// Check if a worm state value is a "special" state.
|
| 355 |
+
pub fn is_special_state(state: u32) -> bool {
|
| 356 |
+
SPECIAL_STATES.contains(&state)
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/// Game phase thresholds (stored in TeamArenaState::game_phase).
|
| 361 |
+
pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
|
| 362 |
+
pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
|
| 363 |
+
|
| 364 |
+
impl TeamArenaState {
|
| 365 |
+
/// Get ammo count for a weapon by alliance and weapon ID.
|
| 366 |
+
pub fn get_ammo(&self, alliance: usize, weapon_id: usize) -> i32 {
|
| 367 |
+
self.weapon_slots.teams[alliance].ammo[weapon_id]
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
/// Get mutable reference to ammo count.
|
| 371 |
+
pub fn ammo_mut(&mut self, alliance: usize, weapon_id: usize) -> &mut i32 {
|
| 372 |
+
&mut self.weapon_slots.teams[alliance].ammo[weapon_id]
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/// Get delay flag for a weapon by alliance and weapon ID.
|
| 376 |
+
pub fn get_delay(&self, alliance: usize, weapon_id: usize) -> i32 {
|
| 377 |
+
self.weapon_slots.teams[alliance].delay[weapon_id]
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/// Typed handle to a TeamArenaState pointer received from WA.exe.
|
| 382 |
+
///
|
| 383 |
+
/// Wraps the raw `base: u32` from trampoline register captures and provides
|
| 384 |
+
/// accessor methods that encapsulate the backward pointer arithmetic to reach
|
| 385 |
+
/// TeamBlock worm data. The TeamBlock array lives 0x598 bytes before
|
| 386 |
+
/// the TeamArenaState in DDGame memory.
|
| 387 |
+
///
|
| 388 |
+
/// # Safety
|
| 389 |
+
/// Must only be constructed from a valid DDGame + TEAM_ARENA_STATE pointer.
|
| 390 |
+
/// `repr(transparent)` ensures identical ABI to `*const u8` / `u32` on i686,
|
| 391 |
+
/// so it can be received directly from usercall trampoline register captures.
|
| 392 |
+
#[repr(transparent)]
|
| 393 |
+
#[derive(Clone, Copy)]
|
| 394 |
+
pub struct TeamArenaRef {
|
| 395 |
+
base: *mut u8,
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
impl TeamArenaRef {
|
| 399 |
+
/// Wrap a raw integer pointer (for usercall trampoline register captures).
|
| 400 |
+
///
|
| 401 |
+
/// # Safety
|
| 402 |
+
/// `base` must point to a valid TeamArenaState (DDGame + 0x4628).
|
| 403 |
+
#[inline]
|
| 404 |
+
pub unsafe fn from_raw(base: u32) -> Self {
|
| 405 |
+
Self {
|
| 406 |
+
base: base as *mut u8,
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/// Wrap a typed pointer to TeamArenaState.
|
| 411 |
+
///
|
| 412 |
+
/// # Safety
|
| 413 |
+
/// `arena` must point to a valid TeamArenaState within a live DDGame.
|
| 414 |
+
#[inline]
|
| 415 |
+
pub unsafe fn from_ptr(arena: *mut TeamArenaState) -> Self {
|
| 416 |
+
Self {
|
| 417 |
+
base: arena as *mut u8,
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/// Access the TeamArenaState fields (read-only).
|
| 422 |
+
#[inline]
|
| 423 |
+
pub unsafe fn state(&self) -> &TeamArenaState {
|
| 424 |
+
&*(self.base as *const TeamArenaState)
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
/// Access the TeamArenaState fields (mutable).
|
| 428 |
+
#[inline]
|
| 429 |
+
pub unsafe fn state_mut(&self) -> &mut TeamArenaState {
|
| 430 |
+
&mut *(self.base as *mut TeamArenaState)
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
/// Get pointer to the TeamBlock array base.
|
| 434 |
+
#[inline]
|
| 435 |
+
pub unsafe fn blocks(&self) -> *mut TeamBlock {
|
| 436 |
+
self.base.sub(offsets::ARENA_TO_BLOCKS) as *mut TeamBlock
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
/// Get the team header (metadata) for a team.
|
| 440 |
+
///
|
| 441 |
+
/// Returns `&block[team_idx+1].header.team`, which holds team metadata:
|
| 442 |
+
/// worm_count, eliminated flag, weapon_alliance, team_name.
|
| 443 |
+
#[inline]
|
| 444 |
+
pub unsafe fn team_header(&self, team_idx: usize) -> &TeamHeader {
|
| 445 |
+
&(*self.blocks().add(team_idx + 1)).header.team
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/// Get a mutable team header for a team.
|
| 449 |
+
#[inline]
|
| 450 |
+
pub unsafe fn team_header_mut(&self, team_idx: usize) -> &mut TeamHeader {
|
| 451 |
+
&mut (*self.blocks().add(team_idx + 1)).header.team
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
/// Get a playable worm entry by 1-indexed worm number (1..=8).
|
| 455 |
+
///
|
| 456 |
+
/// Uses raw pointer arithmetic matching the original WA code:
|
| 457 |
+
/// `base + team_idx * 0x51C + worm_num * 0x9C - 0x598`.
|
| 458 |
+
/// This naturally crosses TeamBlock boundaries when worm_num = 8,
|
| 459 |
+
/// since the 8th worm's early fields (state, health) spill into the
|
| 460 |
+
/// next block's header slot. The header metadata lives at high offsets
|
| 461 |
+
/// (0x6C+) that don't conflict with worm data (0x00-0x5F).
|
| 462 |
+
#[inline]
|
| 463 |
+
pub unsafe fn team_worm(&self, team_idx: usize, worm_num: usize) -> &WormEntry {
|
| 464 |
+
let ptr = self
|
| 465 |
+
.base
|
| 466 |
+
.add(team_idx * 0x51C)
|
| 467 |
+
.add(worm_num * 0x9C)
|
| 468 |
+
.sub(0x598);
|
| 469 |
+
&*(ptr as *const WormEntry)
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/// Get a mutable reference to a specific worm entry.
|
| 473 |
+
pub unsafe fn team_worm_mut(&self, team_idx: usize, worm_num: usize) -> &mut WormEntry {
|
| 474 |
+
let ptr = self
|
| 475 |
+
.base
|
| 476 |
+
.add(team_idx * 0x51C)
|
| 477 |
+
.add(worm_num * 0x9C)
|
| 478 |
+
.sub(0x598);
|
| 479 |
+
&mut *(ptr as *mut WormEntry)
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
/// Get a team's block and its header in one call.
|
| 483 |
+
///
|
| 484 |
+
/// Returns `(block[team_idx], &block[team_idx+1].header.team)`.
|
| 485 |
+
/// The block contains worm data, and the header (slot 0 of the next block)
|
| 486 |
+
/// holds team metadata (worm_count, eliminated flag).
|
| 487 |
+
///
|
| 488 |
+
/// **Note**: For accessing worms, prefer `team_worm()` which handles
|
| 489 |
+
/// 8-worm teams correctly via cross-boundary pointer arithmetic.
|
| 490 |
+
#[inline]
|
| 491 |
+
pub unsafe fn team_and_header(&self, team_idx: usize) -> (&TeamBlock, &TeamHeader) {
|
| 492 |
+
let blocks = self.blocks();
|
| 493 |
+
let block = &*blocks.add(team_idx);
|
| 494 |
+
let header = &(*blocks.add(team_idx + 1)).header.team;
|
| 495 |
+
(block, header)
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
/// Get team header for Pattern B access (alliance/active_worm at +0x70/+0x74).
|
| 499 |
+
///
|
| 500 |
+
/// Pattern B indexes from block[i+2] for 0-indexed team `i`:
|
| 501 |
+
/// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
|
| 502 |
+
#[inline]
|
| 503 |
+
pub unsafe fn team_header_b(&self, team_idx: usize) -> &TeamHeader {
|
| 504 |
+
&(*self.blocks().add(team_idx + 2)).header.team
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
/// Get mutable team header for Pattern B access.
|
| 508 |
+
#[inline]
|
| 509 |
+
pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamHeader {
|
| 510 |
+
&mut (*(self.blocks() as *mut TeamBlock).add(team_idx + 2))
|
| 511 |
+
.header
|
| 512 |
+
.team
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
/// Compute the flat index for ammo/delay table access.
|
| 516 |
+
///
|
| 517 |
+
/// Reads the weapon alliance ID from the team header for the given
|
| 518 |
+
/// 1-indexed team, then computes `alliance_id * 142 + weapon_id`.
|
| 519 |
+
///
|
| 520 |
+
/// The weapon_slots array is interleaved: per alliance, 71 ammo slots
|
| 521 |
+
/// then 71 delay slots (stride 142 per alliance).
|
| 522 |
+
/// Ammo: `weapon_slots[alliance_id * 142 + weapon_id]`
|
| 523 |
+
/// Delay: `weapon_slots[alliance_id * 142 + 71 + weapon_id]`
|
| 524 |
+
/// Returns (alliance_id, weapon_id) for accessing weapon slots.
|
| 525 |
+
#[inline]
|
| 526 |
+
pub unsafe fn weapon_slot_key(&self, team_index: usize, weapon_id: u32) -> (usize, usize) {
|
| 527 |
+
let alliance_id = self.team_header(team_index).weapon_alliance as usize;
|
| 528 |
+
(alliance_id, weapon_id as usize)
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
// ββ Snapshot impl ββββββββββββββββββββββββββββββββββββββββββ
|
| 533 |
+
|
| 534 |
+
impl crate::snapshot::Snapshot for WormEntry {
|
| 535 |
+
unsafe fn write_snapshot(&self, w: &mut dyn core::fmt::Write, indent: usize) -> core::fmt::Result {
|
| 536 |
+
use crate::snapshot::write_indent;
|
| 537 |
+
let i = indent;
|
| 538 |
+
let name = core::ffi::CStr::from_ptr(self.name.as_ptr() as *const core::ffi::c_char)
|
| 539 |
+
.to_string_lossy();
|
| 540 |
+
write_indent(w, i)?;
|
| 541 |
+
writeln!(w, "state=0x{:02X} active={} hp={}/{} name=\"{}\"",
|
| 542 |
+
self.state, self.active_flag, self.health, self.max_health, name)?;
|
| 543 |
+
Ok(())
|
| 544 |
+
}
|
| 545 |
+
}
|