paavohtl Claude Opus 4.6 (1M context) commited on
Commit
49ff7ed
Β·
1 Parent(s): 4d0c588

feat: replace DDGame__Constructor with Rust create_ddgame()

Browse files

First half of the 887-line constructor ported:
- Allocation + zero-fill + InitFields bridge
- Parameter storage (keyboard, display, sound, palette, music, etc.)
- g_GameInfo global, sound_available flag
- Conditional network object creation (game_info+0xD778 == -2)
- InitVersionFlags bridge
- GfxHandler #1 and #2 creation with Gfx.dir loading
- Display palette setup (non-headless)
- GfxDir color entries
- Secondary GfxDir object
- Audio init (effect WAVs, speech banks, ActiveSoundTable)
- DDGameWrapper field inits

15 new sub-function addresses added, all labeled in Ghidra.
Original DDGame__Constructor trapped.

NOT YET RUNNABLE: remaining sub-object construction (PCLandscape,
SpriteRegions, arrows, CoordList, weapons, gradients, HUD, display
finalization) still TODO.

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

crates/openwa-core/src/address.rs CHANGED
@@ -333,6 +333,34 @@ pub mod va {
333
  pub const CONSTRUCT_OPENGL_CPU: u32 = 0x005A_0850;
334
  pub const OPENGL_INIT: u32 = 0x0059_F000;
335
  pub const DDGAME_INIT_FIELDS: u32 = 0x0052_6120;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
337
  pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
338
  pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
@@ -668,6 +696,8 @@ pub mod va {
668
  pub const WA_MALLOC: u32 = 0x005C_0AE3;
669
  pub const WA_MALLOC_MEMSET: u32 = 0x0053_E910;
670
  pub const WA_FREE: u32 = 0x005D_0D2B;
 
 
671
 
672
  // === Bitmap font system ===
673
 
 
333
  pub const CONSTRUCT_OPENGL_CPU: u32 = 0x005A_0850;
334
  pub const OPENGL_INIT: u32 = 0x0059_F000;
335
  pub const DDGAME_INIT_FIELDS: u32 = 0x0052_6120;
336
+ /// DDGame__InitRenderIndices (0x526080): usercall(ESI=ddgame), plain RET.
337
+ /// Initializes render queue index arrays at DDGame+0xC4..+0x4A2.
338
+ pub const DDGAME_INIT_RENDER_INDICES: u32 = 0x0052_6080;
339
+ /// DDGame__InitVersionFlags (0x525BE0): stdcall(ddgame_wrapper).
340
+ /// Sets DDGame+0x7E2E/0x7E2F/0x7E3F flags based on game version/mode.
341
+ pub const DDGAME_INIT_VERSION_FLAGS: u32 = 0x0052_5BE0;
342
+ /// GfxResource__Create_Maybe (0x4F6300): returns GfxResource ptr or null.
343
+ pub const GFX_RESOURCE_CREATE: u32 = 0x004F_6300;
344
+ /// TaskStateMachine__Init_Maybe (0x4F6370): initializes TSM-like object.
345
+ pub const TASK_STATE_MACHINE_INIT: u32 = 0x004F_6370;
346
+ /// DDGame__InitSoundPaths_Maybe (0x570F30): called before DSSound_LoadEffectWAVs.
347
+ pub const DDGAME_INIT_SOUND_PATHS: u32 = 0x0057_0F30;
348
+ /// DDGame__LoadWeaponSprites_Maybe (0x5717A0): loads weapon sprite data.
349
+ pub const DDGAME_LOAD_WEAPON_SPRITES: u32 = 0x0057_17A0;
350
+ /// DDGame__InitDisplayFinal_Maybe (0x56A830): display finalization for non-headless.
351
+ pub const DDGAME_INIT_DISPLAY_FINAL: u32 = 0x0056_A830;
352
+ /// PCLandscape__Constructor (0x57ACB0): constructs 0xB44-byte landscape object.
353
+ pub const PC_LANDSCAPE_CONSTRUCTOR: u32 = 0x0057_ACB0;
354
+ /// SpriteRegion__Constructor (0x57DB20): constructs 0x9C-byte sprite region.
355
+ pub const SPRITE_REGION_CONSTRUCTOR: u32 = 0x0057_DB20;
356
+ /// g_GameInfo global β€” set to current GameInfo* during DDGame construction.
357
+ pub const G_GAME_INFO: u32 = 0x0077_49A0;
358
+ /// GfxHandler__LoadSprites_Maybe (0x570B50): loads sprites from GfxHandler.
359
+ pub const GFX_HANDLER_LOAD_SPRITES: u32 = 0x0057_0B50;
360
+ /// FUN_00570A90: called before display palette setup (non-headless only).
361
+ pub const FUN_570A90: u32 = 0x0057_0A90;
362
+ /// FUN_00570E20: called after GfxHandler initialization.
363
+ pub const FUN_570E20: u32 = 0x0057_0E20;
364
  pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
365
  pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
366
  pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
 
696
  pub const WA_MALLOC: u32 = 0x005C_0AE3;
697
  pub const WA_MALLOC_MEMSET: u32 = 0x0053_E910;
698
  pub const WA_FREE: u32 = 0x005D_0D2B;
699
+ /// WA's CRT _fopen β€” cdecl(path, mode) β†’ FILE*.
700
+ pub const WA_FOPEN: u32 = 0x005D_3271;
701
 
702
  // === Bitmap font system ===
703
 
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -1,14 +1,18 @@
 
1
  use crate::audio::active_sound::ActiveSoundTable;
2
  use crate::display::dd_display::DDDisplay;
 
 
3
  use crate::input::keyboard::DDKeyboard;
4
  use crate::audio::dssound::DSSound;
5
- use crate::engine::game_info::GameInfo;
6
  use crate::render::landscape::PCLandscape;
7
  use crate::audio::music::Music;
8
  use crate::display::palette::Palette;
 
9
  use crate::render::queue::RenderQueue;
10
  use crate::audio::speech::SpeechSlotTable;
11
  use crate::render::turn_order::TurnOrderWidget;
 
12
 
13
  /// DDGame β€” the main game engine object.
14
  ///
@@ -346,6 +350,357 @@ pub struct DDGame {
346
 
347
  const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  /// Well-known byte offsets into DDGame, for use with raw pointer access.
350
  ///
351
  /// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
 
1
+ use crate::address::va;
2
  use crate::audio::active_sound::ActiveSoundTable;
3
  use crate::display::dd_display::DDDisplay;
4
+ use crate::engine::ddgame_wrapper::DDGameWrapper;
5
+ use crate::engine::game_info::GameInfo;
6
  use crate::input::keyboard::DDKeyboard;
7
  use crate::audio::dssound::DSSound;
 
8
  use crate::render::landscape::PCLandscape;
9
  use crate::audio::music::Music;
10
  use crate::display::palette::Palette;
11
+ use crate::rebase::rb;
12
  use crate::render::queue::RenderQueue;
13
  use crate::audio::speech::SpeechSlotTable;
14
  use crate::render::turn_order::TurnOrderWidget;
15
+ use crate::wa_alloc::wa_malloc;
16
 
17
  /// DDGame β€” the main game engine object.
18
  ///
 
350
 
351
  const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
352
 
353
+ // ============================================================
354
+ // DDGame constructor β€” replaces DDGame__Constructor (0x56E220)
355
+ // ============================================================
356
+ //
357
+ // Despite being named DDGame__Constructor, the original function
358
+ // receives DDGameWrapper* as `this` and creates DDGame internally.
359
+ // It populates fields on BOTH the wrapper and the inner DDGame.
360
+ // The Rust entry point is DDGameWrapper::create_game() in
361
+ // ddgame_wrapper.rs; the bulk of the logic lives here because
362
+ // it primarily initializes DDGame fields.
363
+
364
+ /// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
365
+ static mut INIT_FIELDS_ADDR: u32 = 0;
366
+
367
+ /// Initialize runtime addresses for the constructor bridges.
368
+ /// Must be called once at DLL startup (from lib.rs or similar).
369
+ pub fn init_constructor_addrs() {
370
+ unsafe {
371
+ INIT_FIELDS_ADDR = rb(va::DDGAME_INIT_FIELDS);
372
+ }
373
+ }
374
+
375
+ /// Bridge to DDGame__InitFields (0x526120).
376
+ /// Convention: usercall(EDI=ddgame), plain RET.
377
+ #[cfg(target_arch = "x86")]
378
+ #[unsafe(naked)]
379
+ unsafe extern "C" fn call_init_fields(_ddgame: *mut DDGame) {
380
+ core::arch::naked_asm!(
381
+ "pushl %edi",
382
+ "movl 8(%esp), %edi",
383
+ "calll *({addr})",
384
+ "popl %edi",
385
+ "retl",
386
+ addr = sym INIT_FIELDS_ADDR,
387
+ options(att_syntax),
388
+ );
389
+ }
390
+
391
+ /// Bridge to DDGame__InitVersionFlags (0x525BE0).
392
+ /// Convention: stdcall(ddgame_wrapper).
393
+ #[cfg(target_arch = "x86")]
394
+ unsafe fn call_init_version_flags(wrapper: *mut DDGameWrapper) {
395
+ let f: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
396
+ core::mem::transmute(rb(va::DDGAME_INIT_VERSION_FLAGS) as usize);
397
+ f(wrapper);
398
+ }
399
+
400
+ /// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
401
+ ///
402
+ /// Allocates 0x98B8 bytes from WA's heap, initializes all fields, and creates
403
+ /// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the
404
+ /// returned DDGame.
405
+ ///
406
+ /// # Safety
407
+ /// All pointer params must be valid WA objects. `wrapper` must be a
408
+ /// partially-initialized DDGameWrapper (vtable, display, sound set).
409
+ #[cfg(target_arch = "x86")]
410
+ pub unsafe fn create_ddgame(
411
+ wrapper: *mut DDGameWrapper,
412
+ keyboard: *mut DDKeyboard,
413
+ display: *mut DDDisplay,
414
+ sound: *mut DSSound,
415
+ palette: *mut Palette,
416
+ music: *mut Music,
417
+ param7: *mut u8, // timer object (0x1F4 observed)
418
+ net_game: *mut u8, // from GameSession
419
+ game_info: *mut GameInfo,
420
+ network_ecx: u32, // implicit ECX from caller
421
+ ) -> *mut DDGame {
422
+ // ── 1. Allocate and zero-fill (matches: memset(piVar3, 0, 0x98B8)) ──
423
+ let ddgame = wa_malloc(0x98B8) as *mut DDGame;
424
+ if ddgame.is_null() {
425
+ return core::ptr::null_mut();
426
+ }
427
+ core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8);
428
+
429
+ // ── 2. InitFields β€” usercall(EDI=ddgame) ──
430
+ // Zeroes stride-0x194 table, coordinate entries, calls InitRenderIndices.
431
+ call_init_fields(ddgame);
432
+
433
+ // ── 3. Store DDGame* at DDGameWrapper+0x488 ──
434
+ (*wrapper).ddgame = ddgame;
435
+
436
+ // ── 4. Store constructor parameters into DDGame ──
437
+ (*ddgame).display = display;
438
+ (*ddgame).sound = sound;
439
+ (*ddgame).keyboard = keyboard;
440
+ (*ddgame).palette = palette;
441
+ (*ddgame).music = music;
442
+ (*ddgame)._param_018 = param7;
443
+ (*ddgame)._caller = network_ecx as *mut u8;
444
+ (*ddgame).game_info = game_info;
445
+ (*ddgame)._param_028 = net_game;
446
+
447
+ // ── 5. Set g_GameInfo global ──
448
+ *(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info;
449
+
450
+ // ── 6. Sound available + always-1 flags ──
451
+ let is_headless = *(game_info as *const u8).add(0xF914).cast::<i32>() != 0;
452
+ (*ddgame).sound_available = if is_headless { 0 } else { 1 };
453
+ (*ddgame)._field_7efc = 1;
454
+
455
+ // ── 7. DDGameWrapper+0x48C init ──
456
+ (*wrapper).ddgame_secondary = core::ptr::null_mut();
457
+
458
+ // ── 8. Conditional network object (game_info+0xD778 == -2) ──
459
+ let d778 = *(game_info as *const u8).add(0xD778).cast::<i32>();
460
+ if d778 == -2 {
461
+ let net_obj = wa_malloc(0x2C);
462
+ core::ptr::write_bytes(net_obj, 0, 0x2C);
463
+ *(net_obj as *mut *mut DDGame) = ddgame;
464
+ let gi = (*ddgame).game_info as *const u8;
465
+ *net_obj.add(0x28) = *gi.add(0xD944);
466
+ *net_obj.add(0x29) = *gi.add(0xD946);
467
+ (*wrapper).ddgame_secondary = net_obj;
468
+ if network_ecx != 0 {
469
+ *((network_ecx as *mut u8).add(0x18) as *mut *mut u8) = net_obj;
470
+ }
471
+ }
472
+
473
+ // ── 9. InitVersionFlags β€” sets DDGame+0x7E2E/0x7E2F/0x7E3F ──
474
+ call_init_version_flags(wrapper);
475
+
476
+ // ── 10. GfxHandler, landscape, sprites, audio, resources ──
477
+ init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless);
478
+
479
+ ddgame
480
+ }
481
+
482
+ /// Second half of the constructor: GfxHandler, landscape, sprites, audio, resources.
483
+ ///
484
+ /// Separated for readability. Will be broken into smaller functions as
485
+ /// individual sections are converted to pure Rust.
486
+ #[cfg(target_arch = "x86")]
487
+ unsafe fn init_graphics_and_resources(
488
+ wrapper: *mut DDGameWrapper,
489
+ game_info: *mut GameInfo,
490
+ net_game: *mut u8,
491
+ display: *mut DDDisplay,
492
+ is_headless: bool,
493
+ ) {
494
+ let ddgame = (*wrapper).ddgame;
495
+ let fopen: unsafe extern "cdecl" fn(*const u8, *const u8) -> *mut u8 =
496
+ core::mem::transmute(rb(va::WA_FOPEN) as usize);
497
+ let gfx_load_dir: unsafe extern "thiscall" fn(*mut u8) -> i32 =
498
+ core::mem::transmute(rb(va::GFX_HANDLER_LOAD_DIR) as usize);
499
+ let gfx_handler_vtable = rb(0x664308) as u32;
500
+
501
+ // ── GfxHandler #1 (primary) ──
502
+ let gfx1 = wa_malloc(0x19C);
503
+ core::ptr::write_bytes(gfx1, 0, 0x19C);
504
+ *(gfx1 as *mut u32) = gfx_handler_vtable;
505
+ (*wrapper)._field_4c0 = gfx1;
506
+ (*wrapper)._field_4c4 = core::ptr::null_mut();
507
+
508
+ // Build path list (order depends on headless + GameInfo+0xF374)
509
+ let f914 = *(game_info as *const u8).add(0xF914).cast::<i32>();
510
+ let f374 = *(game_info as *const u8).add(0xF374).cast::<i32>();
511
+ let paths: [&[u8]; 3] = if f914 != 0 {
512
+ if f374 == 0 {
513
+ [b"data\\Gfx\\Gfx.dir\0", b"data\\Gfx\\Gfx0.dir\0", b"data\\Gfx\\Gfx1.dir\0"]
514
+ } else {
515
+ [b"data\\Gfx\\Gfx.dir\0", b"data\\Gfx\\Gfx1.dir\0", b"data\\Gfx\\Gfx0.dir\0"]
516
+ }
517
+ } else if f374 == 0 {
518
+ [b"data\\Gfx\\Gfx0.dir\0", b"data\\Gfx\\Gfx1.dir\0", b"data\\Gfx\\Gfx.dir\0"]
519
+ } else {
520
+ [b"data\\Gfx\\Gfx1.dir\0", b"data\\Gfx\\Gfx0.dir\0", b"data\\Gfx\\Gfx.dir\0"]
521
+ };
522
+
523
+ let mut gfx_loaded_idx = 0u32;
524
+ for (i, path) in paths.iter().enumerate() {
525
+ let fp = fopen(path.as_ptr(), b"rb\0".as_ptr());
526
+ *(gfx1.add(0x198) as *mut *mut u8) = fp;
527
+ if !fp.is_null() && gfx_load_dir(gfx1) != 0 {
528
+ gfx_loaded_idx = i as u32;
529
+ break;
530
+ }
531
+ if i == 2 {
532
+ panic!("DDGame: couldn't open any Gfx.dir");
533
+ }
534
+ }
535
+
536
+ let headless_offset = if f914 != 0 { 1u32 } else { 0u32 };
537
+ (*wrapper).gfx_mode = if gfx_loaded_idx.wrapping_sub(headless_offset) < 2 { 1 } else { 0 };
538
+
539
+ // ── GfxHandler #2 (conditional) ──
540
+ let d778 = *(game_info as *const u8).add(0xD778).cast::<i32>();
541
+ let threshold = if (*wrapper).gfx_mode != 0 {
542
+ (0x23u32.wrapping_sub(2)) as i32
543
+ } else {
544
+ -2i32
545
+ };
546
+ if d778 < threshold {
547
+ let c_digit = if d778 > -3 { b'2' } else { b'1' };
548
+ let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
549
+ gfx_c_path[14] = c_digit;
550
+
551
+ let gfx2 = wa_malloc(0x19C);
552
+ core::ptr::write_bytes(gfx2, 0, 0x19C);
553
+ *(gfx2 as *mut u32) = gfx_handler_vtable;
554
+ (*wrapper)._field_4c4 = gfx2;
555
+
556
+ let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr());
557
+ *(gfx2.add(0x198) as *mut *mut u8) = fp;
558
+ if fp.is_null() || gfx_load_dir(gfx2) == 0 {
559
+ let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr());
560
+ *(gfx2.add(0x198) as *mut *mut u8) = fp2;
561
+ if fp2.is_null() || gfx_load_dir(gfx2) == 0 {
562
+ panic!("DDGame: couldn't open secondary Gfx.dir");
563
+ }
564
+ }
565
+ }
566
+
567
+ // ── Display palette setup (non-headless) ──
568
+ if !is_headless {
569
+ if *(rb(0x88E485) as *const u8) == 0 {
570
+ let fun_570a90: unsafe extern "C" fn() =
571
+ core::mem::transmute(rb(va::FUN_570A90) as usize);
572
+ fun_570a90();
573
+ }
574
+ // DDGameWrapper+0x4D0 points to the display object used for palette calls
575
+ let disp = *(wrapper as *const u8).add(0x4D0) as *mut u8;
576
+ let vt = *(disp as *const *const u32);
577
+ let vt_10: unsafe extern "thiscall" fn(*mut u8) =
578
+ core::mem::transmute(*vt.add(4));
579
+ vt_10(disp);
580
+ let vt_7c: unsafe extern "thiscall" fn(*mut u8, i32, i32, i32) =
581
+ core::mem::transmute(*vt.add(0x1F));
582
+ vt_7c(disp, 1, 1, 0);
583
+ let vt_5c: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
584
+ core::mem::transmute(*vt.add(0x17));
585
+ vt_5c(disp, -100, 1);
586
+
587
+ // Palette slot range init
588
+ let palette_range_ptr = *(disp.add(0x3120) as *const *mut i16);
589
+ if !palette_range_ptr.is_null() && *(disp.add(0x3534) as *const i32) == 0 {
590
+ let start = *palette_range_ptr as u32;
591
+ let end = (*palette_range_ptr.add(1) as u32) + 1;
592
+ if start < end {
593
+ for i in start..end {
594
+ *(disp.add(0x312C + i as usize * 4) as *mut u32) = 1;
595
+ }
596
+ }
597
+ crate::wa_alloc::wa_free(*(disp.add(0x3120) as *const *mut u8));
598
+ *(disp.add(0x3120) as *mut u32) = 0;
599
+ }
600
+ }
601
+
602
+ // ── FUN_00570E20 ──
603
+ {
604
+ let f: unsafe extern "C" fn() = core::mem::transmute(rb(va::FUN_570E20) as usize);
605
+ f();
606
+ }
607
+
608
+ // ── Display vtable slot 5 (offset 0x14) ──
609
+ {
610
+ let vt = *((*ddgame).display as *const *const u32);
611
+ let f: unsafe extern "thiscall" fn(*mut DDDisplay) =
612
+ core::mem::transmute(*vt.add(5));
613
+ f((*ddgame).display);
614
+ }
615
+
616
+ // ── GfxDir color entries DDGame+0x730C..0x732C ──
617
+ if (*wrapper).gfx_mode != 0 {
618
+ let create: unsafe extern "C" fn() -> *mut u8 =
619
+ core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
620
+ let res = create();
621
+ if !res.is_null() {
622
+ let rvt = *(res as *const *const u32);
623
+ let get_color: unsafe extern "thiscall" fn(*mut u8) -> u32 =
624
+ core::mem::transmute(*rvt.add(4));
625
+ let mut off = 0x730Cu32;
626
+ while off < 0x732Du32 {
627
+ *((ddgame as *mut u8).add(off as usize) as *mut u32) = get_color(res);
628
+ off += 4;
629
+ }
630
+ let release: unsafe extern "thiscall" fn(*mut u8) =
631
+ core::mem::transmute(*rvt.add(3));
632
+ release(res);
633
+ }
634
+ } else {
635
+ let f: unsafe extern "thiscall" fn(*mut u8) =
636
+ core::mem::transmute(rb(va::GFX_HANDLER_LOAD_SPRITES) as usize);
637
+ f(wrapper as *mut u8);
638
+ }
639
+
640
+ // ── Secondary GfxDir object (DDGame+0x2C, conditional) ──
641
+ if !(*wrapper)._field_4c4.is_null() {
642
+ let gfxdir2 = wa_malloc(0x70C);
643
+ core::ptr::write_bytes(gfxdir2, 0, 0x70C);
644
+ *(gfxdir2 as *mut u16) = 1;
645
+ *(gfxdir2.add(2) as *mut u16) = 0x5A;
646
+ {
647
+ let f: unsafe extern "C" fn() = core::mem::transmute(rb(0x5411A0) as usize);
648
+ f();
649
+ }
650
+ *(gfxdir2.add(0x708) as *mut u16) = 0;
651
+ (*ddgame).secondary_gfxdir = gfxdir2;
652
+ let f: unsafe extern "thiscall" fn(*mut u8) =
653
+ core::mem::transmute(rb(va::GFX_HANDLER_LOAD_SPRITES) as usize);
654
+ f(wrapper as *mut u8);
655
+ }
656
+
657
+ // ── DDGameWrapper field inits ──
658
+ (*wrapper)._field_4d8 = 0;
659
+ if display.is_null() {
660
+ (*wrapper)._field_4dc = 0x2AD;
661
+ } else {
662
+ let byte_val = *net_game.add(0x44C) as u32;
663
+ (*wrapper)._field_4dc = byte_val * 0x38 + 0x7E + 0x2AD;
664
+ }
665
+ (*wrapper)._field_4e0 = 0xFFFFFF9C; // -100
666
+ (*wrapper).speech_name_count = 0;
667
+
668
+ // ── Audio init (non-headless + sound available) ──
669
+ if !is_headless {
670
+ {
671
+ let f: unsafe extern "C" fn() =
672
+ core::mem::transmute(rb(va::DDGAME_INIT_SOUND_PATHS) as usize);
673
+ f();
674
+ }
675
+ if !(*ddgame).sound.is_null() {
676
+ {
677
+ let f: unsafe extern "C" fn() =
678
+ core::mem::transmute(rb(va::DSSOUND_LOAD_EFFECT_WAVS) as usize);
679
+ f();
680
+ }
681
+ {
682
+ let f: unsafe extern "C" fn() =
683
+ core::mem::transmute(rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS) as usize);
684
+ f();
685
+ }
686
+ // Allocate ActiveSoundTable (0x608 bytes)
687
+ let ast = wa_malloc(0x608) as *mut ActiveSoundTable;
688
+ (*ast).ddgame = ddgame;
689
+ (*ast).counter = 0;
690
+ core::ptr::write_bytes(ast as *mut u8, 0, 0x600);
691
+ (*ddgame).active_sounds = ast;
692
+ }
693
+ }
694
+
695
+ // ════════════════════════════════════════════════════════════
696
+ // TODO: Remaining sub-object construction (PCLandscape,
697
+ // TaskStateMachine, SpriteRegions, arrow sprites, CoordList,
698
+ // weapon sprites, gradient images, fill image, HUD, display
699
+ // finalization). These are ~400 lines of the original 887-line
700
+ // constructor. Each will be added incrementally.
701
+ // ════════════════════════════════════════════════════════════
702
+ }
703
+
704
  /// Well-known byte offsets into DDGame, for use with raw pointer access.
705
  ///
706
  /// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -9,101 +9,44 @@
9
  //!
10
  //! - `DDGameWrapper__InitReplay` (0x56F860): usercall(EAX=game_info, ESI=this),
11
  //! plain RET (no stack args). Bridged via `call_init_replay`.
12
- //! - `DDGame__Constructor` (0x56E220): stdcall 9 params + implicit ECX=network.
13
- //! Bridged via `ddgame_constructor_call` which sets ECX then **tail-jumps**,
14
- //! preserving the exact stack so DDGame::ctor's `RET 0x24` returns to ctor_impl.
15
  //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute.
16
 
17
  use openwa_core::address::va;
18
- use openwa_core::engine::GameInfo;
19
- use openwa_core::rebase::rb;
20
- use openwa_core::engine::DDGameWrapper;
21
- use openwa_core::display::DDDisplay;
22
  use openwa_core::audio::DSSound;
23
- use openwa_core::engine::GameSession;
24
- use openwa_core::display::Palette;
 
 
 
25
  use crate::hook;
26
  use crate::log_line;
27
 
28
  /// Implicit EDI = game_info pointer, captured from EDI on entry.
29
  static mut GAME_INFO: *mut GameInfo = core::ptr::null_mut();
30
 
31
- /// Implicit ECX = network pointer for `DDGame__Constructor`. Set in `ctor_impl`
32
- /// just before calling `ddgame_constructor_call`.
33
- static mut DDGAME_CTOR_ECX: u32 = 0;
34
-
35
  /// Runtime address of `DDGameWrapper__InitReplay` (set at install time).
36
  static mut INIT_REPLAY_ADDR: u32 = 0;
37
 
38
- /// Runtime address of `DDGame__Constructor` (set at install time).
39
- static mut DDGAME_CTOR_ADDR: u32 = 0;
40
-
41
  // ─── Bridge: DDGameWrapper__InitReplay ───────────────────────────────────────
42
  //
43
  // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params).
44
- // Declared as stdcall(game_info, this) so the Rust caller pushes both; the
45
- // naked body loads EAX/ESI from the stack and calls via INIT_REPLAY_ADDR.
46
- //
47
- // Stack on entry (after stdcall push):
48
- // [esp+0] = ret addr
49
- // [esp+4] = game_info (arg 1, pushed last by stdcall)
50
- // [esp+8] = this (arg 2, pushed first by stdcall)
51
- //
52
- // After `pushl %esi`:
53
- // [esp+0] = old_esi, [esp+4] = ret addr, [esp+8] = game_info, [esp+0xC] = this
54
  #[unsafe(naked)]
55
  unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *mut DDGameWrapper) {
56
  core::arch::naked_asm!(
57
  "pushl %esi",
58
  "movl 8(%esp), %eax", // EAX = game_info
59
  "movl 0xC(%esp), %esi", // ESI = this
60
- "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET from callee
61
- "popl %esi", // restore ESI
62
- "retl $8", // stdcall callee-cleanup: 2 Γ— u32 = 8 bytes
63
  fn = sym INIT_REPLAY_ADDR,
64
  options(att_syntax),
65
  );
66
  }
67
 
68
- // ─── Bridge: DDGame__Constructor ─────────────────────────────────────────────
69
- //
70
- // Convention: stdcall 9 params + implicit ECX=network.
71
- //
72
- // Rust calls this as a normal stdcall(9 params). The naked body loads ECX from
73
- // DDGAME_CTOR_ECX and then **tail-jumps** (jmp, not call) to DDGame::ctor.
74
- //
75
- // Tail-jump rationale: the call instruction in ctor_impl already pushed the
76
- // return address at [esp+0]. After the tail-jump, DDGame::ctor sees exactly:
77
- // [esp+0] = return address back into ctor_impl
78
- // [esp+4] = this … [esp+24] = game_info (9 stdcall args)
79
- // ECX = network (implicit)
80
- // DDGame::ctor's `RET 0x24` pops 0x24 bytes and returns to ctor_impl. βœ“
81
- #[unsafe(naked)]
82
- unsafe extern "stdcall" fn ddgame_constructor_call(
83
- _this: *mut DDGameWrapper,
84
- _display: *mut DDDisplay,
85
- _sound: *mut DSSound,
86
- _keyboard: *mut u8,
87
- _palette: *mut Palette,
88
- _streaming_audio: *mut u8,
89
- _timer_obj: *mut u8,
90
- _net_game: *mut u8,
91
- _game_info: *mut GameInfo,
92
- ) -> *mut u8 {
93
- core::arch::naked_asm!(
94
- "movl {ecx_val}, %ecx", // ECX = input_ctrl (implicit param for DDGame::ctor)
95
- "jmpl *({fn})", // tail-jump; DDGame::ctor's RET 0x24 returns to caller
96
- ecx_val = sym DDGAME_CTOR_ECX,
97
- fn = sym DDGAME_CTOR_ADDR,
98
- options(att_syntax),
99
- );
100
- }
101
-
102
  /// Called by `impl_init_hardware` to construct the DDGameWrapper in-place.
103
- ///
104
- /// Sets `GAME_INFO` (read by `ctor_impl`) and delegates directly to `ctor_impl`,
105
- /// bypassing the naked entry trampoline which is designed as a hook target, not a
106
- /// callable subroutine.
107
  pub(crate) unsafe fn construct_ddgame_wrapper(
108
  game_info: *mut GameInfo,
109
  this: *mut DDGameWrapper,
@@ -116,8 +59,6 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
116
  ) -> *mut DDGameWrapper {
117
  GAME_INFO = game_info;
118
 
119
- let this = this;
120
-
121
  // Initialize DDGameWrapper fields (order matches original decompile).
122
  (*this).ddgame = core::ptr::null_mut();
123
  (*this).landscape = core::ptr::null_mut();
@@ -133,11 +74,18 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
133
  let timer_obj = (*session).timer_obj;
134
  let net_game = (*session).net_game;
135
 
136
- // Store input_ctrl as the implicit ECX for DDGame::ctor, then tail-jump-call.
137
- DDGAME_CTOR_ECX = input_ctrl as u32;
138
- ddgame_constructor_call(
139
- this, display, sound, keyboard, palette, streaming_audio,
140
- timer_obj, net_game, game_info,
 
 
 
 
 
 
 
141
  );
142
 
143
  // Initialize DDGame's game-state fields.
@@ -157,10 +105,11 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
157
  pub fn install() -> Result<(), String> {
158
  unsafe {
159
  INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY);
160
- DDGAME_CTOR_ADDR = rb(va::CONSTRUCT_DD_GAME);
161
- // Fully converted β€” only called from impl_init_hardware (Rust).
162
- // Trap panics if WA.exe unexpectedly calls the original.
163
  hook::install_trap!("DDGameWrapper__Constructor", va::CONSTRUCT_DD_GAME_WRAPPER);
 
164
  }
165
  Ok(())
166
  }
 
9
  //!
10
  //! - `DDGameWrapper__InitReplay` (0x56F860): usercall(EAX=game_info, ESI=this),
11
  //! plain RET (no stack args). Bridged via `call_init_replay`.
12
+ //! - `DDGame__Constructor` (0x56E220): **REPLACED** by `create_ddgame()` in
13
+ //! openwa-core. Original function is trapped.
 
14
  //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute.
15
 
16
  use openwa_core::address::va;
 
 
 
 
17
  use openwa_core::audio::DSSound;
18
+ use openwa_core::display::{DDDisplay, Palette};
19
+ use openwa_core::engine::ddgame::{create_ddgame, init_constructor_addrs};
20
+ use openwa_core::engine::{DDGameWrapper, GameInfo, GameSession};
21
+ use openwa_core::input::DDKeyboard;
22
+ use openwa_core::rebase::rb;
23
  use crate::hook;
24
  use crate::log_line;
25
 
26
  /// Implicit EDI = game_info pointer, captured from EDI on entry.
27
  static mut GAME_INFO: *mut GameInfo = core::ptr::null_mut();
28
 
 
 
 
 
29
  /// Runtime address of `DDGameWrapper__InitReplay` (set at install time).
30
  static mut INIT_REPLAY_ADDR: u32 = 0;
31
 
 
 
 
32
  // ─── Bridge: DDGameWrapper__InitReplay ───────────────────────────────────────
33
  //
34
  // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params).
 
 
 
 
 
 
 
 
 
 
35
  #[unsafe(naked)]
36
  unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *mut DDGameWrapper) {
37
  core::arch::naked_asm!(
38
  "pushl %esi",
39
  "movl 8(%esp), %eax", // EAX = game_info
40
  "movl 0xC(%esp), %esi", // ESI = this
41
+ "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET
42
+ "popl %esi",
43
+ "retl $8", // stdcall cleanup: 2 Γ— u32 = 8
44
  fn = sym INIT_REPLAY_ADDR,
45
  options(att_syntax),
46
  );
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  /// Called by `impl_init_hardware` to construct the DDGameWrapper in-place.
 
 
 
 
50
  pub(crate) unsafe fn construct_ddgame_wrapper(
51
  game_info: *mut GameInfo,
52
  this: *mut DDGameWrapper,
 
59
  ) -> *mut DDGameWrapper {
60
  GAME_INFO = game_info;
61
 
 
 
62
  // Initialize DDGameWrapper fields (order matches original decompile).
63
  (*this).ddgame = core::ptr::null_mut();
64
  (*this).landscape = core::ptr::null_mut();
 
74
  let timer_obj = (*session).timer_obj;
75
  let net_game = (*session).net_game;
76
 
77
+ // Create DDGame β€” replaces the original DDGame__Constructor (0x56E220).
78
+ create_ddgame(
79
+ this,
80
+ keyboard as *mut DDKeyboard,
81
+ display,
82
+ sound,
83
+ palette,
84
+ streaming_audio as *mut openwa_core::audio::Music,
85
+ timer_obj,
86
+ net_game,
87
+ game_info,
88
+ input_ctrl as u32, // network ECX
89
  );
90
 
91
  // Initialize DDGame's game-state fields.
 
105
  pub fn install() -> Result<(), String> {
106
  unsafe {
107
  INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY);
108
+ // Initialize runtime addresses for create_ddgame bridges.
109
+ init_constructor_addrs();
110
+ // Both constructors are fully converted β€” trap the originals.
111
  hook::install_trap!("DDGameWrapper__Constructor", va::CONSTRUCT_DD_GAME_WRAPPER);
112
+ hook::install_trap!("DDGame__Constructor", va::CONSTRUCT_DD_GAME);
113
  }
114
  Ok(())
115
  }