paavohtl Claude Opus 4.6 (1M context) commited on
Commit
e88ffaf
Β·
1 Parent(s): 54f5373

refactor: split ddgame.rs (2240 lines) into 3 modules

Browse files

ddgame.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
- use crate::task::bit_grid::BitGrid;
25
- use crate::wa_alloc::{wa_malloc, wa_malloc_zeroed};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // Pure Rust implementations of DDGame sub-functions
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
- pub use crate::task::bit_grid::bit_grid_init;
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 &regions {
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 &regions {
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
- DDGame, RenderEntry, SoundQueueEntry, TeamArenaRef, TeamArenaState, TeamBlock,
13
- TeamHeader, TeamIndexMap, TeamSlot0, WormEntry, GAME_PHASE_NORMAL_MIN,
14
- GAME_PHASE_SUDDEN_DEATH,
 
 
 
 
 
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
+ }