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

feat: create_ddgame() with bridges for first 60% of constructor

Browse files

Implement create_ddgame() covering: allocation, zero-fill, InitFields,
param storage, version flags, GfxHandler creation, display palette,
color entries, secondary GfxDir, audio init, PCLandscape bridge,
TaskStateMachine, 8x SpriteRegion bridges, landscape value.

Not yet wired in β€” game_session.rs still calls the original DDGame
constructor via bridge. create_ddgame() will replace it once the
remaining sections (arrow sprites, coord list, gradients, weapons,
HUD, display finalization) are ported.

15 new sub-function addresses, SpriteRegion fastcall bridge.

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

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -369,6 +369,7 @@ static mut INIT_FIELDS_ADDR: u32 = 0;
369
  pub fn init_constructor_addrs() {
370
  unsafe {
371
  INIT_FIELDS_ADDR = rb(va::DDGAME_INIT_FIELDS);
 
372
  }
373
  }
374
 
@@ -692,13 +693,166 @@ unsafe fn init_graphics_and_resources(
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.
 
369
  pub fn init_constructor_addrs() {
370
  unsafe {
371
  INIT_FIELDS_ADDR = rb(va::DDGAME_INIT_FIELDS);
372
+ SPRITE_REGION_CTOR_ADDR = rb(va::SPRITE_REGION_CONSTRUCTOR);
373
  }
374
  }
375
 
 
693
  }
694
  }
695
 
696
+ // ── GfxResource (used by PCLandscape and later by gradients) ──
697
+ let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
698
+ core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
699
+ let gfx_resource = gfx_resource_create(
700
+ (*wrapper)._field_4c0,
701
+ (ddgame as *mut u8).add(0x77A4), // output: pass ddgame+0x77A4 area
702
+ );
703
+
704
+ // ── PCLandscape (alloc 0xB44, stdcall 11 params) ──
705
+ let landscape = {
706
+ let alloc = wa_malloc(0xB44);
707
+ core::ptr::write_bytes(alloc, 0, 0xB44);
708
+ if !alloc.is_null() {
709
+ // Output locals for landscape data (used later for coord_list)
710
+ let mut landscape_coords: [i32; 2] = [0; 2]; // aiStack_11d4
711
+ let mut landscape_byte: u8 = 0; // iStack_11f9
712
+
713
+ let pc_ctor: unsafe extern "stdcall" fn(
714
+ *mut u8, *mut DDGame, *mut u8, // this, ddgame, gfx_resource
715
+ *mut DDGameWrapper, *const u8, // wrapper, game_info+0xDAAC
716
+ *mut u8, u32, // &landscape_byte, gfx_mode
717
+ *mut [i32; 2], *mut i32, // outputs: coords, more coords
718
+ *mut u8, *mut u8, // &ddgame+0x777C, &ddgame+0x7780
719
+ ) -> *mut u8 = core::mem::transmute(rb(va::PC_LANDSCAPE_CONSTRUCTOR) as usize);
720
+
721
+ let gi = (*ddgame).game_info as *const u8;
722
+ let result = pc_ctor(
723
+ alloc,
724
+ ddgame,
725
+ gfx_resource,
726
+ wrapper,
727
+ gi.add(0xDAAC),
728
+ &mut landscape_byte as *mut u8,
729
+ (*wrapper).gfx_mode,
730
+ &mut landscape_coords as *mut [i32; 2],
731
+ landscape_coords.as_mut_ptr(), // placeholder for extra output
732
+ (ddgame as *mut u8).add(0x777C),
733
+ (ddgame as *mut u8).add(0x7780),
734
+ );
735
+ // Store landscape pointer
736
+ (*wrapper).landscape = result as *mut PCLandscape;
737
+ (*ddgame).landscape = result as *mut PCLandscape;
738
+ result
739
+ } else {
740
+ (*wrapper).landscape = core::ptr::null_mut();
741
+ (*ddgame).landscape = core::ptr::null_mut();
742
+ core::ptr::null_mut()
743
+ }
744
+ };
745
+
746
+ // ── TaskStateMachine at DDGame+0x380 (alloc 0x2C) ──
747
+ {
748
+ let tsm = wa_malloc(0x2C);
749
+ core::ptr::write_bytes(tsm, 0, 0x2C);
750
+ if !tsm.is_null() {
751
+ // TaskStateMachine__Init_Maybe is fastcall(ECX=width, EDX=?)
752
+ // but called here with ECX=1, then set vtable
753
+ let width = *((ddgame as *const u8).add(0x77C0) as *const u32);
754
+ let _height = *((ddgame as *const u8).add(0x77C4) as *const u32);
755
+ let tsm_init: unsafe extern "fastcall" fn(u32, u32) =
756
+ core::mem::transmute(rb(va::TASK_STATE_MACHINE_INIT) as usize);
757
+ tsm_init(1, width); // ECX=1, EDX=width
758
+ *(tsm as *mut u32) = rb(0x664118); // TaskStateMachine__vtable
759
+ }
760
+ let ddgame_ptr = ddgame as *mut u8;
761
+ *(ddgame_ptr.add(0x380) as *mut *mut u8) = tsm;
762
+ }
763
+
764
+ // ── 8Γ— SpriteRegion at DDGame+0x46C..0x488 ──
765
+ // SpriteRegion__Constructor: fastcall(ECX, EDX) + 6 stack(this, p2, p3, p4, p5, p6), RET 0x18
766
+ {
767
+ // (ddgame_offset, ECX, EDX, stack2, stack3, stack4, stack5, stack6)
768
+ let regions: [(u32, u32, u32, u32, u32, u32, u32, u32); 8] = [
769
+ (0x474, 0x37, 0x36, 0x2E, 0x24, 0x41, 0x2D, 0),
770
+ (0x46C, 0x30, 0x0C, 0x2D, 0x07, 0x34, 0x09, 0),
771
+ (0x470, 0x11, 0x1A, 0x0D, 0x0A, 0x16, 0x13, 0),
772
+ (0x478, 0x21, 0x01, 0x00, 0x20, 0x22, 0x01, 0),
773
+ (0x47C, 0x01, 0x01, 0x00, 0x00, 0x02, 0x01, 0),
774
+ (0x484, 0x0A, 0x01, 0x173, 0x09, 0x17C, 0x02, 0),
775
+ (0x488, 0x08, 0x01, 0x1E5, 0x07, 0x1EC, 0x02, 0),
776
+ (0x480, 0x08, 0x01, 0x2D, 0x07, 0x34, 0x02, 0),
777
+ ];
778
+
779
+ for &(offset, ecx, edx, p2, p3, p4, p5, p6) in &regions {
780
+ let alloc = wa_malloc(0x9C);
781
+ core::ptr::write_bytes(alloc, 0, 0x9C);
782
+ let result = if !alloc.is_null() {
783
+ call_sprite_region_ctor(alloc, ecx, edx, p2, p3, p4, p5, p6)
784
+ } else {
785
+ core::ptr::null_mut()
786
+ };
787
+ *((ddgame as *mut u8).add(offset as usize) as *mut *mut u8) = result;
788
+ }
789
+ }
790
+
791
+ // ── Landscape-derived value at DDGame+0x468 ──
792
+ if !landscape.is_null() {
793
+ let land_vt = *(landscape as *const *const u32);
794
+ let get_val: unsafe extern "thiscall" fn(*mut u8) -> u32 =
795
+ core::mem::transmute(*land_vt.add(0xB));
796
+ *((ddgame as *mut u8).add(0x468) as *mut u32) = get_val(landscape);
797
+ }
798
+
799
+ // Release gfx_resource if non-null
800
+ if !gfx_resource.is_null() {
801
+ let rvt = *(gfx_resource as *const *const u32);
802
+ let release: unsafe extern "thiscall" fn(*mut u8) =
803
+ core::mem::transmute(*rvt.add(3));
804
+ release(gfx_resource);
805
+ }
806
+
807
+ // ── Arrow sprites + collision regions (32 iterations) ──
808
+ // ── DisplayGfx at +0x138 ──
809
+ // ── CoordList at +0x50C ──
810
+ // ── Weapon sprites, gradient images, fill image, HUD ──
811
+ // ── Display finalization ──
812
+ //
813
+ // These remaining sections (~300 lines of decompile) involve complex
814
+ // GfxDir lookups, sprite loading, gradient computation, and display
815
+ // layer management. They will be added in subsequent commits.
816
+ // For now, the constructor returns after audio + landscape + sprite regions.
817
+ }
818
+
819
+ /// Bridge to SpriteRegion__Constructor (0x57DB20).
820
+ /// Convention: fastcall(ECX, EDX) + 6 stack params, RET 0x18.
821
+ #[cfg(target_arch = "x86")]
822
+ static mut SPRITE_REGION_CTOR_ADDR: u32 = 0;
823
+
824
+ #[cfg(target_arch = "x86")]
825
+ #[unsafe(naked)]
826
+ unsafe extern "C" fn call_sprite_region_ctor(
827
+ _this: *mut u8, _ecx: u32, _edx: u32,
828
+ _p2: u32, _p3: u32, _p4: u32, _p5: u32, _p6: u32,
829
+ ) -> *mut u8 {
830
+ // Stack on entry: [ret] [this] [ecx] [edx] [p2] [p3] [p4] [p5] [p6]
831
+ // Need: ECX=ecx, EDX=edx, push p6 p5 p4 p3 p2 this, call
832
+ core::arch::naked_asm!(
833
+ "pushl %ebx",
834
+ // Load ECX and EDX from our params
835
+ "movl 12(%esp), %ecx", // ecx param
836
+ "movl 16(%esp), %edx", // edx param
837
+ // Push stack params in reverse: p6, p5, p4, p3, p2, this
838
+ "movl 36(%esp), %eax", // p6
839
+ "pushl %eax",
840
+ "movl 36(%esp), %eax", // p5 (shifted by 4)
841
+ "pushl %eax",
842
+ "movl 36(%esp), %eax", // p4 (shifted by 8)
843
+ "pushl %eax",
844
+ "movl 36(%esp), %eax", // p3 (shifted by 12)
845
+ "pushl %eax",
846
+ "movl 36(%esp), %eax", // p2 (shifted by 16)
847
+ "pushl %eax",
848
+ "movl 28(%esp), %eax", // this (shifted by 20)
849
+ "pushl %eax",
850
+ "calll *({addr})", // fastcall, callee cleans 6Γ—4=24 bytes
851
+ "popl %ebx",
852
+ "retl",
853
+ addr = sym SPRITE_REGION_CTOR_ADDR,
854
+ options(att_syntax),
855
+ );
856
  }
857
 
858
  /// Well-known byte offsets into DDGame, for use with raw pointer access.
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -9,16 +9,16 @@
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;
@@ -26,9 +26,16 @@ use crate::log_line;
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).
@@ -46,6 +53,31 @@ unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *m
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,
@@ -74,18 +106,12 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
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,11 +131,13 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
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
  }
 
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`. Being incrementally replaced by
14
+ //! `create_ddgame()` in openwa-core (not yet complete).
15
  //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute.
16
 
17
  use openwa_core::address::va;
18
  use openwa_core::audio::DSSound;
19
  use openwa_core::display::{DDDisplay, Palette};
20
+ use openwa_core::engine::ddgame::init_constructor_addrs;
21
  use openwa_core::engine::{DDGameWrapper, GameInfo, GameSession};
 
22
  use openwa_core::rebase::rb;
23
  use crate::hook;
24
  use crate::log_line;
 
26
  /// Implicit EDI = game_info pointer, captured from EDI on entry.
27
  static mut GAME_INFO: *mut GameInfo = core::ptr::null_mut();
28
 
29
+ /// Implicit ECX = network pointer for `DDGame__Constructor`. Set in `ctor_impl`
30
+ /// just before calling `ddgame_constructor_call`.
31
+ static mut DDGAME_CTOR_ECX: u32 = 0;
32
+
33
  /// Runtime address of `DDGameWrapper__InitReplay` (set at install time).
34
  static mut INIT_REPLAY_ADDR: u32 = 0;
35
 
36
+ /// Runtime address of `DDGame__Constructor` (set at install time).
37
+ static mut DDGAME_CTOR_ADDR: u32 = 0;
38
+
39
  // ─── Bridge: DDGameWrapper__InitReplay ───────────────────────────────────────
40
  //
41
  // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params).
 
53
  );
54
  }
55
 
56
+ // ─── Bridge: DDGame__Constructor ─────────────────────────────────────────────
57
+ //
58
+ // Convention: stdcall 9 params + implicit ECX=network.
59
+ // Tail-jumps to original; DDGame::ctor's RET 0x24 returns to caller.
60
+ #[unsafe(naked)]
61
+ unsafe extern "stdcall" fn ddgame_constructor_call(
62
+ _this: *mut DDGameWrapper,
63
+ _display: *mut DDDisplay,
64
+ _sound: *mut DSSound,
65
+ _keyboard: *mut u8,
66
+ _palette: *mut Palette,
67
+ _streaming_audio: *mut u8,
68
+ _timer_obj: *mut u8,
69
+ _net_game: *mut u8,
70
+ _game_info: *mut GameInfo,
71
+ ) -> *mut u8 {
72
+ core::arch::naked_asm!(
73
+ "movl {ecx_val}, %ecx", // ECX = network (implicit param)
74
+ "jmpl *({fn})", // tail-jump; RET 0x24 returns to caller
75
+ ecx_val = sym DDGAME_CTOR_ECX,
76
+ fn = sym DDGAME_CTOR_ADDR,
77
+ options(att_syntax),
78
+ );
79
+ }
80
+
81
  /// Called by `impl_init_hardware` to construct the DDGameWrapper in-place.
82
  pub(crate) unsafe fn construct_ddgame_wrapper(
83
  game_info: *mut GameInfo,
 
106
  let timer_obj = (*session).timer_obj;
107
  let net_game = (*session).net_game;
108
 
109
+ // Call original DDGame__Constructor via bridge.
110
+ // TODO: Replace with create_ddgame() once fully ported.
111
+ DDGAME_CTOR_ECX = input_ctrl as u32;
112
+ ddgame_constructor_call(
113
+ this, display, sound, keyboard, palette, streaming_audio,
114
+ timer_obj, net_game, game_info,
 
 
 
 
 
 
115
  );
116
 
117
  // Initialize DDGame's game-state fields.
 
131
  pub fn install() -> Result<(), String> {
132
  unsafe {
133
  INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY);
134
+ DDGAME_CTOR_ADDR = rb(va::CONSTRUCT_DD_GAME);
135
+ // Initialize runtime addresses for create_ddgame bridges (future use).
136
  init_constructor_addrs();
137
+ // DDGameWrapper__Constructor is fully converted β€” trap the original.
138
  hook::install_trap!("DDGameWrapper__Constructor", va::CONSTRUCT_DD_GAME_WRAPPER);
139
+ // DDGame__Constructor still uses the original via bridge.
140
+ // Will be trapped once create_ddgame() is complete.
141
  }
142
  Ok(())
143
  }