paavohtl Claude Opus 4.6 (1M context) commited on
Commit
d621fac
Β·
1 Parent(s): 0844196

feat: port 5 DDGame constructor sub-functions to Rust hooks

Browse files

Port DDGame constructor sub-functions as individual MinHook replacements,
so each works in isolation regardless of whether the DDGame constructor
itself is Rust or original WA code. All 5 hooks verified with replay
test (133 PASS, 0 FAIL).

Ported functions:
- DDGame__InitFields (0x526120): zeroes scattered offsets + render indices
- DDGame__InitRenderIndices (0x526080): identity permutation tables at
ddgame+0x72D8 (team index maps, NOT base of DDGame)
- TaskStateMachine__Init (0x4F6370): bit-per-cell grid allocator
- GfxResource__Create_Maybe (0x4F6300): FindEntry β†’ cached load β†’ DisplayGfx
wrap, with LoadImage β†’ IMG_Decode fallback
- FUN_570E20 / DDGame__InitDisplayLayerColors: 3 display vtable[4] calls

Key bugs found and fixed:
- InitRenderIndices base pointer: ESI = ddgame+0x72D8, not ddgame itself.
Original was writing render indices to team_index_map region.
- InitFields return value: caller stores EAX as DDGame pointer at
wrapper+0x488. Missing return caused null DDGame β†’ crash on loading.

Also adds:
- Panic hook (writes [PANIC] to OpenWA.log for crash diagnosis)
- PANIC detection in replay-test.ps1
- IMG_Decode label in Ghidra (was mislabeled TaskStateMachine__Destructor)
- DISPLAYGFX_CONSTRUCTOR and IMG_DECODE address constants

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

crates/openwa-core/src/address.rs CHANGED
@@ -364,6 +364,12 @@ pub mod va {
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;
 
 
 
 
 
 
367
 
368
  // === Higher-level drawing functions ===
369
 
 
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;
367
+ /// DisplayGfx__Constructor_Maybe (0x4F5E80): stdcall(raw_image) -> DisplayGfx*, RET 0x4.
368
+ pub const DISPLAYGFX_CONSTRUCTOR: u32 = 0x004F_5E80;
369
+ /// IMG_Decode (0x4F5F80): stdcall(output, raw_image, 1) -> DisplayGfx*, RET 0xC.
370
+ /// Reads IMG\x1A header, decodes image data into output buffer.
371
+ /// Ghidra mislabels this as "TaskStateMachine__Destructor_Maybe".
372
+ pub const IMG_DECODE: u32 = 0x004F_5F80;
373
 
374
  // === Higher-level drawing functions ===
375
 
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -362,6 +362,235 @@ const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
362
  // ddgame_wrapper.rs; the bulk of the logic lives here because
363
  // it primarily initializes DDGame fields.
364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  /// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
366
  static mut INIT_FIELDS_ADDR: u32 = 0;
367
 
@@ -437,9 +666,8 @@ pub unsafe fn create_ddgame(
437
  }
438
  core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8);
439
 
440
- // ── 2. InitFields β€” usercall(EDI=ddgame) ──
441
- // Zeroes stride-0x194 table, coordinate entries, calls InitRenderIndices.
442
- call_init_fields(ddgame);
443
 
444
  // ── 3-4. Store params BEFORE exposing DDGame via wrapper ──
445
  // Critical: game_info must be set before wrapper->ddgame, because the
@@ -796,16 +1024,15 @@ unsafe fn init_graphics_and_resources(
796
  };
797
 
798
  // ── TaskStateMachine at DDGame+0x380 (alloc 0x4C, memset 0x2C) ──
799
- // Original: alloc 0x4C, memset 0x2C, PUSH width, MOV ECX=1, CALL 0x4F6370 (RET 0x4)
800
- // Also EDI=height before the call (implicit param).
801
  {
802
  let tsm = wa_malloc(0x4C);
803
  core::ptr::write_bytes(tsm, 0, 0x2C);
804
  if !tsm.is_null() {
805
- let width = *((ddgame as *const u8).add(0x77C0) as *const u32);
806
- let height = *((ddgame as *const u8).add(0x77C4) as *const u32);
807
- call_tsm_init(tsm, width, height);
808
- *(tsm as *mut u32) = rb(0x664118); // TaskStateMachine__vtable
809
  }
810
  (*ddgame).task_state_machine = tsm;
811
  }
 
362
  // ddgame_wrapper.rs; the bulk of the logic lives here because
363
  // it primarily initializes DDGame fields.
364
 
365
+ // ============================================================
366
+ // Pure Rust implementations of DDGame sub-functions
367
+ // ============================================================
368
+ // These are called both by create_ddgame() and by MinHook
369
+ // trampolines in openwa-wormkit/replacements/ddgame_init.rs.
370
+
371
+ /// Pure Rust implementation of DDGame__InitFields (0x526120).
372
+ ///
373
+ /// Zeroes stride-0x194 table entries, calls init_render_indices,
374
+ /// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
375
+ ///
376
+ /// # Safety
377
+ /// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98B8 bytes).
378
+ pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
379
+ let base = ddgame as *mut u8;
380
+
381
+ // Zero the stride-0x194 table (10 entries)
382
+ for &off in &[
383
+ 0x379Cusize, 0x3930, 0x3AC4, 0x3C58, 0x3DEC,
384
+ 0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0,
385
+ ] {
386
+ *(base.add(off as usize) as *mut u32) = 0;
387
+ }
388
+
389
+ *(base.add(0x64D8) as *mut u32) = 0;
390
+ *(base.add(0x72A4) as *mut u32) = 0;
391
+
392
+ // InitRenderIndices β€” original sets ESI = ddgame + 0x72D8 before calling
393
+ ddgame_init_render_indices(base.add(0x72D8));
394
+
395
+ // Zero 8 fields at 0x8Cxx
396
+ for &off in &[
397
+ 0x8CBCusize, 0x8CC0, 0x8CCC, 0x8CD0,
398
+ 0x8CDC, 0x8CE0, 0x8CEC, 0x8CF0,
399
+ ] {
400
+ *(base.add(off as usize) as *mut u32) = 0;
401
+ }
402
+
403
+ // Zero 8 fields at 0x98xx
404
+ for &off in &[
405
+ 0x9850usize, 0x9854, 0x9860, 0x9864,
406
+ 0x9870, 0x9874, 0x9880, 0x9884,
407
+ ] {
408
+ *(base.add(off as usize) as *mut u32) = 0;
409
+ }
410
+ }
411
+
412
+ /// Pure Rust implementation of DDGame__InitRenderIndices (0x526080).
413
+ ///
414
+ /// Convention: usercall(ESI=base_ptr), plain RET.
415
+ ///
416
+ /// **Important:** The base pointer is NOT the DDGame pointer!
417
+ /// InitFields calls this with ESI = ddgame + 0x72D8.
418
+ /// All offsets are relative to whatever ESI points to.
419
+ ///
420
+ /// Absolute DDGame offsets (base = ddgame+0x72D8):
421
+ /// - base+0xC4 = ddgame+0x739C
422
+ /// - base+0xD8 = ddgame+0x73B0 (eh_vector_constructor_iterator region)
423
+ /// - base+0x378 = ddgame+0x7650 (team_index_map_1)
424
+ /// - base+0x3DC = ddgame+0x76B4 (team_index_map_2)
425
+ /// - base+0x440 = ddgame+0x7718 (team_index_map_3)
426
+ ///
427
+ /// # Safety
428
+ /// `base` must point to a valid memory region with at least 0x4A4 bytes.
429
+ pub unsafe fn ddgame_init_render_indices(base: *mut u8) {
430
+ // Zero base+0xC4
431
+ *(base.add(0xC4) as *mut u32) = 0;
432
+
433
+ // eh_vector_constructor_iterator equivalent:
434
+ // FUN_525F40 is fastcall { *param_1 = 0; }
435
+ // 14 entries at stride 0x14 starting from +0xD8
436
+ for i in 0..14usize {
437
+ *(base.add(0xD8 + i * 0x14) as *mut u32) = 0;
438
+ }
439
+
440
+ // Identity permutation 1: base+0x378 (= ddgame+0x7650), 16 entries (i16)
441
+ for i in 0..16i16 {
442
+ *(base.add(0x378 + i as usize * 2) as *mut i16) = i;
443
+ }
444
+ *(base.add(0x398) as *mut u16) = 0x10;
445
+ *(base.add(0x3DA) as *mut u16) = 0;
446
+
447
+ // Identity permutation 2: base+0x3DC (= ddgame+0x76B4), 16 entries (i16)
448
+ for i in 0..16i16 {
449
+ *(base.add(0x3DC + i as usize * 2) as *mut i16) = i;
450
+ }
451
+ *(base.add(0x3FC) as *mut u16) = 0x10;
452
+ *(base.add(0x43E) as *mut u16) = 0;
453
+
454
+ // Identity permutation 3: base+0x440 (= ddgame+0x7718), 16 entries (i16)
455
+ for i in 0..16i16 {
456
+ *(base.add(0x440 + i as usize * 2) as *mut i16) = i;
457
+ }
458
+ *(base.add(0x4A2) as *mut u16) = 0;
459
+ *(base.add(0x460) as *mut u16) = 0x10;
460
+ }
461
+
462
+ /// Pure Rust implementation of TaskStateMachine__Init (0x4F6370).
463
+ ///
464
+ /// Convention: usercall(ESI=object, ECX=param1, EDI=height) + 1 stack(width), RET 0x4.
465
+ ///
466
+ /// Allocates a bit-per-cell grid buffer. `param1` is typically 1 (cells per unit).
467
+ /// `width` and `height` are pixel dimensions. The buffer is a row-major bitfield
468
+ /// with rows aligned to 4 bytes.
469
+ ///
470
+ /// # Safety
471
+ /// `object` must point to a zero-filled allocation of at least 0x2C bytes.
472
+ pub unsafe fn task_state_machine_init(
473
+ object: *mut u8,
474
+ param1: u32,
475
+ width: u32,
476
+ height: u32,
477
+ ) {
478
+ // Row stride: bits-to-bytes rounded up, then aligned to 4
479
+ let bits = param1.wrapping_mul(width).wrapping_add(7) as i32;
480
+ let row_stride = ((bits >> 3) + 3) & !3;
481
+ let total_size = row_stride as u32 * height;
482
+
483
+ // Allocate data buffer with 0x20-byte header
484
+ let alloc_size = ((total_size + 3) & !3) + 0x20;
485
+ let buffer = wa_malloc(alloc_size);
486
+
487
+ // Memset twice (matches original β€” likely redundant but exact match)
488
+ core::ptr::write_bytes(buffer, 0, total_size as usize);
489
+ core::ptr::write_bytes(buffer, 0, total_size as usize);
490
+
491
+ // Set object fields (DWORD array layout)
492
+ let obj = object as *mut u32;
493
+ *obj.add(0) = rb(0x6640EC); // vtable
494
+ *obj.add(1) = 0; // unused
495
+ *obj.add(2) = buffer as u32; // data pointer
496
+ *obj.add(3) = param1; // param1
497
+ *obj.add(4) = row_stride as u32; // row stride
498
+ *obj.add(5) = width; // width
499
+ *obj.add(6) = height; // height
500
+ *obj.add(7) = 0; // unused
501
+ *obj.add(8) = 0; // unused
502
+ *obj.add(9) = width; // width (duplicate)
503
+ *obj.add(10) = height; // height (duplicate)
504
+ }
505
+
506
+ /// Pure Rust implementation of FUN_570E20 (display layer color init).
507
+ ///
508
+ /// Convention: usercall(ESI=wrapper), plain RET.
509
+ ///
510
+ /// Sets display layer color parameters via display->vtable[4](layer, color).
511
+ /// Layer 1 color depends on gfx_mode and game_version.
512
+ /// Layer 2 = 0x20, Layer 3 = 0x70.
513
+ ///
514
+ /// # Safety
515
+ /// `wrapper` must be a valid DDGameWrapper with initialized display and ddgame.
516
+ #[cfg(target_arch = "x86")]
517
+ pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) {
518
+ let ddgame = (*wrapper).ddgame;
519
+ let game_info = (*ddgame).game_info;
520
+ let game_version = (*game_info).game_version;
521
+
522
+ // wrapper+0x4C8 is gfx_mode (at DDGameWrapper.gfx_mode)
523
+ let layer1_color = if (*wrapper).gfx_mode == 0 {
524
+ // (game_version > -2) - 1: yields 0 if true, -1 if false
525
+ // Then + 0x69: yields 0x69 or 0x68
526
+ if game_version > -2 { 0x69i32 } else { 0x68i32 }
527
+ } else {
528
+ 5 + 0x69 // = 0x6E
529
+ };
530
+
531
+ let display = (*wrapper).display as *mut u8;
532
+ let vt = *(display as *const *const u32);
533
+ // vtable[4] (offset 0x10): thiscall(display, layer, color), RET 0x8
534
+ let set_color: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
535
+ core::mem::transmute(*vt.add(4));
536
+
537
+ set_color(display, 1, layer1_color);
538
+ set_color(display, 2, 0x20);
539
+ set_color(display, 3, 0x70);
540
+ }
541
+
542
+ /// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
543
+ ///
544
+ /// Convention: usercall(ECX=gfx_handler, EAX=name) + 1 stack(output), RET 0x4.
545
+ ///
546
+ /// Looks up `name` in the GfxHandler's directory, tries cached load, wraps
547
+ /// as DisplayGfx. Falls back to loading the raw image and decoding it.
548
+ ///
549
+ /// # Safety
550
+ /// All pointers must be valid WA objects.
551
+ #[cfg(target_arch = "x86")]
552
+ pub unsafe fn gfx_resource_create(
553
+ gfx_handler: *mut u8,
554
+ name: *const u8,
555
+ output: *mut u8,
556
+ ) -> *mut u8 {
557
+ // 1. Try FindEntry β†’ cached load β†’ DisplayGfx wrap
558
+ let entry = call_gfx_find_entry(name, gfx_handler);
559
+ if !entry.is_null() {
560
+ // gfx_handler->vtable[2](entry->field_4) β€” cached load
561
+ let vt = *(gfx_handler as *const *const u32);
562
+ let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
563
+ core::mem::transmute(*vt.add(2));
564
+ let entry_val = *(entry.add(4) as *const u32);
565
+ let cached = load_cached(gfx_handler, entry_val);
566
+ if !cached.is_null() {
567
+ // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
568
+ let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
569
+ core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
570
+ return ctor(cached);
571
+ }
572
+ }
573
+
574
+ // 2. Fallback: LoadImage β†’ IMG_Decode
575
+ let raw_image = call_gfx_load_image(gfx_handler, name);
576
+ if raw_image.is_null() {
577
+ return core::ptr::null_mut();
578
+ }
579
+
580
+ // IMG_Decode: stdcall(output, raw_image, 1), RET 0xC
581
+ let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 =
582
+ core::mem::transmute(rb(va::IMG_DECODE) as usize);
583
+ let result = decode(output, raw_image, 1);
584
+
585
+ // Release raw image: raw_image->vtable[0](1)
586
+ let img_vt = *(raw_image as *const *const u32);
587
+ let release: unsafe extern "thiscall" fn(*mut u8, i32) =
588
+ core::mem::transmute(*img_vt);
589
+ release(raw_image, 1);
590
+
591
+ result
592
+ }
593
+
594
  /// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
595
  static mut INIT_FIELDS_ADDR: u32 = 0;
596
 
 
666
  }
667
  core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8);
668
 
669
+ // ── 2. InitFields β€” pure Rust (replaces usercall bridge) ──
670
+ ddgame_init_fields(ddgame);
 
671
 
672
  // ── 3-4. Store params BEFORE exposing DDGame via wrapper ──
673
  // Critical: game_info must be set before wrapper->ddgame, because the
 
1024
  };
1025
 
1026
  // ── TaskStateMachine at DDGame+0x380 (alloc 0x4C, memset 0x2C) ──
1027
+ // Pure Rust: allocate object, call task_state_machine_init, override vtable.
 
1028
  {
1029
  let tsm = wa_malloc(0x4C);
1030
  core::ptr::write_bytes(tsm, 0, 0x2C);
1031
  if !tsm.is_null() {
1032
+ let width = (*ddgame).level_width;
1033
+ let height = (*ddgame).level_height;
1034
+ task_state_machine_init(tsm, 1, width, height);
1035
+ *(tsm as *mut u32) = rb(0x664118); // Override vtable to TaskStateMachine
1036
  }
1037
  (*ddgame).task_state_machine = tsm;
1038
  }
crates/openwa-wormkit/src/lib.rs CHANGED
@@ -53,6 +53,11 @@ fn run() -> Result<(), String> {
53
  let _ = clear_log();
54
  let _ = std::fs::write("OpenWA_validation.log", "");
55
 
 
 
 
 
 
56
  let delta = openwa_core::rebase::init();
57
  let _ = log_line(&format!(
58
  "=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
 
53
  let _ = clear_log();
54
  let _ = std::fs::write("OpenWA_validation.log", "");
55
 
56
+ // Install panic hook that writes to our log file
57
+ std::panic::set_hook(Box::new(|info| {
58
+ let _ = log_line(&format!("[PANIC] {info}"));
59
+ }));
60
+
61
  let delta = openwa_core::rebase::init();
62
  let _ = log_line(&format!(
63
  "=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
crates/openwa-wormkit/src/replacements/ddgame_init.rs ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Rust replacements for DDGame constructor sub-functions.
2
+ //!
3
+ //! Each function is hooked individually so it works regardless of whether the
4
+ //! DDGame constructor itself is Rust or the original WA code.
5
+ //!
6
+ //! ## Ported functions
7
+ //!
8
+ //! - `DDGame__InitFields` (0x526120): usercall(EDI=ddgame), plain RET β†’ EAX=ddgame
9
+ //! - `DDGame__InitRenderIndices` (0x526080): usercall(ESI=base), plain RET β†’ EAX=base
10
+ //! - `TaskStateMachine__Init` (0x4F6370): usercall(ESI,ECX,EDI) + 1 stack, RET 0x4
11
+ //! - `GfxResource__Create_Maybe` (0x4F6300): usercall(ECX,EAX) + 1 stack, RET 0x4
12
+ //! - `FUN_570E20` (display layer init): usercall(ESI=wrapper), plain RET
13
+
14
+ use openwa_core::address::va;
15
+ use openwa_core::engine::ddgame::{
16
+ DDGame, ddgame_init_fields, ddgame_init_render_indices,
17
+ task_state_machine_init, gfx_resource_create, display_layer_color_init,
18
+ };
19
+ use openwa_core::engine::DDGameWrapper;
20
+ use crate::hook;
21
+
22
+ // ─── DDGame__InitFields (0x526120) ──────────────────────────────────────────
23
+
24
+ hook::usercall_trampoline!(
25
+ fn init_fields_trampoline;
26
+ impl_fn = impl_init_fields;
27
+ reg = edi
28
+ );
29
+
30
+ extern "cdecl" fn impl_init_fields(ddgame: u32) -> u32 {
31
+ unsafe { ddgame_init_fields(ddgame as *mut DDGame) }
32
+ ddgame // Original: MOV EAX, EDI; RET
33
+ }
34
+
35
+ // ─── DDGame__InitRenderIndices (0x526080) ───────────────────────────────────
36
+
37
+ hook::usercall_trampoline!(
38
+ fn init_render_indices_trampoline;
39
+ impl_fn = impl_init_render_indices;
40
+ reg = esi
41
+ );
42
+
43
+ extern "cdecl" fn impl_init_render_indices(base: u32) -> u32 {
44
+ unsafe { ddgame_init_render_indices(base as *mut u8) }
45
+ base // Original: MOV EAX, ESI; RET
46
+ }
47
+
48
+ // ─── TaskStateMachine__Init (0x4F6370) ──────────────────────────────────────
49
+
50
+ extern "cdecl" fn impl_tsm_init(object: u32, param1: u32, height: u32, width: u32) -> u32 {
51
+ unsafe { task_state_machine_init(object as *mut u8, param1, width, height) }
52
+ object // Original: MOV EAX, ESI; RET 0x4
53
+ }
54
+
55
+ #[unsafe(naked)]
56
+ unsafe extern "C" fn tsm_init_trampoline() {
57
+ core::arch::naked_asm!(
58
+ "push edx",
59
+ "push [esp+8]", // width (stack param)
60
+ "push edi", // height
61
+ "push ecx", // param1
62
+ "push esi", // object
63
+ "call {impl_fn}",
64
+ "add esp, 16",
65
+ "pop edx",
66
+ "ret 0x4",
67
+ impl_fn = sym impl_tsm_init,
68
+ );
69
+ }
70
+
71
+ // ─── FUN_570E20 (display layer color init) ──────────────────────────────────
72
+
73
+ hook::usercall_trampoline!(
74
+ fn display_layer_init_trampoline;
75
+ impl_fn = impl_display_layer_init;
76
+ reg = esi
77
+ );
78
+
79
+ extern "cdecl" fn impl_display_layer_init(wrapper: u32) -> u32 {
80
+ unsafe { display_layer_color_init(wrapper as *mut DDGameWrapper) }
81
+ wrapper
82
+ }
83
+
84
+ // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
85
+
86
+ extern "cdecl" fn impl_gfx_resource_create(
87
+ gfx_handler: u32, name: u32, output: u32,
88
+ ) -> u32 {
89
+ let result = unsafe {
90
+ gfx_resource_create(gfx_handler as *mut u8, name as *const u8, output as *mut u8)
91
+ };
92
+ result as u32
93
+ }
94
+
95
+ #[unsafe(naked)]
96
+ unsafe extern "C" fn gfx_resource_create_trampoline() {
97
+ core::arch::naked_asm!(
98
+ "push edx",
99
+ "push [esp+8]", // output (stack param)
100
+ "push eax", // name
101
+ "push ecx", // gfx_handler
102
+ "call {impl_fn}",
103
+ "add esp, 12",
104
+ "pop edx",
105
+ "ret 0x4",
106
+ impl_fn = sym impl_gfx_resource_create,
107
+ );
108
+ }
109
+
110
+ // ─── Hook installation ──────────────────────────────────────────────────────
111
+
112
+ pub fn install() -> Result<(), String> {
113
+ unsafe {
114
+ hook::install(
115
+ "DDGame__InitFields",
116
+ va::DDGAME_INIT_FIELDS,
117
+ init_fields_trampoline as *const (),
118
+ )?;
119
+
120
+ hook::install(
121
+ "DDGame__InitRenderIndices",
122
+ va::DDGAME_INIT_RENDER_INDICES,
123
+ init_render_indices_trampoline as *const (),
124
+ )?;
125
+
126
+ hook::install(
127
+ "TaskStateMachine__Init",
128
+ va::TASK_STATE_MACHINE_INIT,
129
+ tsm_init_trampoline as *const (),
130
+ )?;
131
+
132
+ hook::install(
133
+ "GfxResource__Create",
134
+ va::GFX_RESOURCE_CREATE,
135
+ gfx_resource_create_trampoline as *const (),
136
+ )?;
137
+
138
+ hook::install(
139
+ "FUN_570E20_DisplayLayerInit",
140
+ va::FUN_570E20,
141
+ display_layer_init_trampoline as *const (),
142
+ )?;
143
+ }
144
+
145
+ Ok(())
146
+ }
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -113,7 +113,7 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
113
  ));
114
 
115
  // Toggle: use Rust constructor (true) or original (false)
116
- const USE_RUST_CTOR: bool = true;
117
  if USE_RUST_CTOR {
118
  create_ddgame(
119
  this,
 
113
  ));
114
 
115
  // Toggle: use Rust constructor (true) or original (false)
116
+ const USE_RUST_CTOR: bool = false;
117
  if USE_RUST_CTOR {
118
  create_ddgame(
119
  this,
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -1,4 +1,5 @@
1
  mod config;
 
2
  mod display;
3
  mod frontend;
4
  mod game_session;
@@ -26,5 +27,6 @@ pub fn install_all() -> Result<(), String> {
26
  sound::install()?;
27
  speech::install()?;
28
  input::install()?;
 
29
  Ok(())
30
  }
 
1
  mod config;
2
+ mod ddgame_init;
3
  mod display;
4
  mod frontend;
5
  mod game_session;
 
27
  sound::install()?;
28
  speech::install()?;
29
  input::install()?;
30
+ ddgame_init::install()?;
31
  Ok(())
32
  }
replay-test.ps1 CHANGED
@@ -179,5 +179,15 @@ if ($Headless) {
179
  Write-Host " Validation: NOT found in log (may not have reached gameplay)" -ForegroundColor Yellow
180
  }
181
  }
 
 
 
 
 
 
 
 
 
 
182
  Write-Host " Logs: $logDir\" -ForegroundColor Gray
183
  }
 
179
  Write-Host " Validation: NOT found in log (may not have reached gameplay)" -ForegroundColor Yellow
180
  }
181
  }
182
+ # Check for Rust panics in OpenWA log
183
+ if (Test-Path "$logDir\openwa_latest.log") {
184
+ $panics = Select-String -Path "$logDir\openwa_latest.log" -Pattern "\[PANIC\]"
185
+ if ($panics) {
186
+ Write-Host ""
187
+ Write-Host "=== RUST PANIC DETECTED ===" -ForegroundColor Red
188
+ $panics | ForEach-Object { Write-Host " $($_.Line)" -ForegroundColor Red }
189
+ }
190
+ }
191
+
192
  Write-Host " Logs: $logDir\" -ForegroundColor Gray
193
  }