paavohtl Claude Opus 4.6 (1M context) commited on
Commit
f2d43fe
Β·
1 Parent(s): 7dd5442

refactor: add typed structs and named fields to hardware_init

Browse files

- Extend GameInfo to 0xF91C with named fields (display_width/height,
display_flags, headless_mode, speech_enabled, etc.)
- Add 6 placeholder structs: DisplayGfx, InputCtrl, GameTimer,
StreamingAudio, DDNetGameWrapper, GameStats
- Expand DDKeyboard with construction fields and 0x33C size assert
- Fix DSSound size to 0xBE0 and rename COM fields (direct_sound,
primary_buffer, init_success)
- Type GameSession pointers: DDGameWrapper, DDKeyboard, DSSound, Palette
- Fix DDKeyboard+0x4: must store ADDRESS of GameInfo+0xF918, not value
- Fix DSSound init: check Play() result, not outer DirectSoundCreate hr

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

crates/openwa-core/src/address.rs CHANGED
@@ -664,6 +664,8 @@ pub mod va {
664
  pub const G_FULLSCREEN_FLAG: u32 = 0x007A_084C;
665
  /// Suppress-cursor flag β€” if non-zero, skip SetCursorPos/ClipCursor in hardware init.
666
  pub const G_SUPPRESS_CURSOR: u32 = 0x0088_E485;
 
 
667
  /// Total sprite data bytes loaded (accumulated by ProcessSprite)
668
  pub const G_SPRITE_DATA_BYTES: u32 = 0x007A_0864;
669
  /// Total sprite frame count loaded
 
664
  pub const G_FULLSCREEN_FLAG: u32 = 0x007A_084C;
665
  /// Suppress-cursor flag β€” if non-zero, skip SetCursorPos/ClipCursor in hardware init.
666
  pub const G_SUPPRESS_CURSOR: u32 = 0x0088_E485;
667
+ /// IAT thunk for MapWindowPoints (USER32.dll import).
668
+ pub const IAT_MAP_WINDOW_POINTS: u32 = 0x0061_A588;
669
  /// Total sprite data bytes loaded (accumulated by ProcessSprite)
670
  pub const G_SPRITE_DATA_BYTES: u32 = 0x007A_0864;
671
  /// Total sprite frame count loaded
crates/openwa-core/src/ddkeyboard.rs CHANGED
@@ -5,17 +5,39 @@
5
  /// PollKeyboardState (0x572290): drains WM_KEY messages, calls GetKeyboardState,
6
  /// normalizes to both `key_state` (+0x11C) and `prev_state` (+0x21C) buffers.
7
  ///
8
- /// PARTIAL: Only confirmed fields. Minimum size estimated from known offsets.
 
 
 
 
 
 
 
9
  #[repr(C)]
10
  pub struct DDKeyboard {
11
  /// 0x000: Vtable pointer (0x66AEC8)
12
  pub vtable: *mut u8,
13
- /// 0x004-0x11B: Unknown
14
- pub _unknown_004: [u8; 0x118],
 
 
 
 
 
 
 
 
 
 
 
15
  /// 0x11C: Current key state buffer (256 bytes).
16
  /// Populated by PollKeyboardState via GetKeyboardState.
17
  pub key_state: [u8; 256],
18
  /// 0x21C: Previous key state buffer (256 bytes).
19
  /// Copied from key_state at start of each poll cycle.
20
  pub prev_state: [u8; 256],
 
 
21
  }
 
 
 
5
  /// PollKeyboardState (0x572290): drains WM_KEY messages, calls GetKeyboardState,
6
  /// normalizes to both `key_state` (+0x11C) and `prev_state` (+0x21C) buffers.
7
  ///
8
+ /// Size: 0x33C bytes.
9
+ /// Inline construction in GameEngine__InitHardware:
10
+ /// - vtable = 0x66AEC8
11
+ /// - dinput_device = GameInfo+0xF918
12
+ /// - _field_008 = 1
13
+ /// - key_state zeroed (0x100 bytes)
14
+ /// - prev_state zeroed (0x100 bytes)
15
+ /// - _field_014 = 0, _field_018 = 0
16
  #[repr(C)]
17
  pub struct DDKeyboard {
18
  /// 0x000: Vtable pointer (0x66AEC8)
19
  pub vtable: *mut u8,
20
+ /// 0x004: Pointer into GameInfo+0xF918 (shared input state location).
21
+ /// Stores the ADDRESS of GameInfo+0xF918, not its value.
22
+ pub game_info_input_ptr: u32,
23
+ /// 0x008: Init flag (set to 1)
24
+ pub _field_008: u32,
25
+ /// 0x00C-0x013: Unknown
26
+ pub _unknown_00c: [u8; 8],
27
+ /// 0x014: Cleared to 0 during construction
28
+ pub _field_014: u32,
29
+ /// 0x018: Cleared to 0 during construction
30
+ pub _field_018: u32,
31
+ /// 0x01C-0x11B: Unknown
32
+ pub _unknown_01c: [u8; 0x100],
33
  /// 0x11C: Current key state buffer (256 bytes).
34
  /// Populated by PollKeyboardState via GetKeyboardState.
35
  pub key_state: [u8; 256],
36
  /// 0x21C: Previous key state buffer (256 bytes).
37
  /// Copied from key_state at start of each poll cycle.
38
  pub prev_state: [u8; 256],
39
+ /// 0x31C: Unknown trailing fields
40
+ pub _unknown_31c: [u8; 0x20],
41
  }
42
+
43
+ const _: () = assert!(core::mem::size_of::<DDKeyboard>() == 0x33C);
crates/openwa-core/src/ddnetgame_wrapper.rs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// DDNetGameWrapper β€” network game wrapper.
2
+ ///
3
+ /// Constructor: DDNetGameWrapper__Constructor (0x56D1F0), stdcall(this) β†’ DDNetGameWrapper*.
4
+ /// Size: 0x2C bytes.
5
+ ///
6
+ /// Created by GameEngine__InitHardware (always, after DDGameWrapper).
7
+ /// Stored at GameSession+0xC0.
8
+ ///
9
+ /// OPAQUE: Internal layout not yet mapped.
10
+ #[repr(C)]
11
+ pub struct DDNetGameWrapper {
12
+ pub _data: [u8; 0x2C],
13
+ }
14
+
15
+ const _: () = assert!(core::mem::size_of::<DDNetGameWrapper>() == 0x2C);
crates/openwa-core/src/display_gfx.rs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// DisplayGfx β€” main display/graphics subsystem container.
2
+ ///
3
+ /// Constructor: DisplayGfx__Constructor (0x569C10), stdcall(this) β†’ DisplayGfx*.
4
+ /// Size: 0x24E28 bytes.
5
+ ///
6
+ /// Created by GameEngine__InitHardware in normal (non-headless) mode.
7
+ /// Stored at GameSession+0xAC.
8
+ ///
9
+ /// OPAQUE: Internal layout not yet mapped.
10
+ #[repr(C)]
11
+ pub struct DisplayGfx {
12
+ pub vtable: *mut u8,
13
+ pub _unknown_004: [u8; 0x24E28 - 4],
14
+ }
15
+
16
+ const _: () = assert!(core::mem::size_of::<DisplayGfx>() == 0x24E28);
crates/openwa-core/src/dssound.rs CHANGED
@@ -3,40 +3,39 @@ use crate::fixed::Fixed;
3
  ///
4
  /// Created by DSSound__Constructor (0x573D50).
5
  /// Vtable: 0x66AF20
 
6
  ///
7
  /// Manages sound playback through DirectSound.
8
  /// Has 500 sound channel slots and a master volume control.
9
  ///
10
- /// PARTIAL: Only constructor-confirmed fields.
11
- /// Ghidra uses DWORD-indexed offsets.
12
  #[repr(C)]
13
  pub struct DSSound {
14
  /// 0x000: Vtable pointer (0x66AF20)
15
  pub vtable: *mut u8,
16
- /// 0x004: Unknown
17
- pub _unknown_004: [u8; 4],
18
- /// 0x008: Init field (set to 0)
19
- pub _field_008: u32,
20
- /// 0x00C: Init field (set to 0)
21
- pub _field_00c: u32,
22
- /// 0x010: Init field (set to 0)
23
- pub _field_010: u32,
24
- /// 0x014-0x8A3: Unknown fields
25
  pub _unknown_014: [u8; 0x890],
26
  /// 0x8A4: Master volume (Fixed, init 0x10000 = 1.0)
27
  pub volume: Fixed,
28
- /// 0x8A8-0xBB3: Unknown (includes 500-entry channel slot table)
29
  pub _unknown_8a8: [u8; 0x30C],
30
  /// 0xBB4: Status flag 1 (init 1)
31
  pub status_1: u32,
32
  /// 0xBB8: Status flag 2 (init 1)
33
  pub status_2: u32,
34
- /// 0xBBC: Status flag 3 (init 0)
35
- pub status_3: u32,
36
- /// 0xBC0-end: Unknown trailing fields
37
- pub _unknown_bc0: [u8; 0x10],
 
38
  }
39
 
40
- // Total size uncertain β€” this is an estimate based on constructor analysis.
41
- // The 500 dword channel table starts around offset 0xD4+ but exact layout unclear.
42
- const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBD0);
 
3
  ///
4
  /// Created by DSSound__Constructor (0x573D50).
5
  /// Vtable: 0x66AF20
6
+ /// Size: 0xBE0 bytes (allocated by GameEngine__InitHardware).
7
  ///
8
  /// Manages sound playback through DirectSound.
9
  /// Has 500 sound channel slots and a master volume control.
10
  ///
11
+ /// PARTIAL: Only constructor-confirmed and InitHardware-confirmed fields.
 
12
  #[repr(C)]
13
  pub struct DSSound {
14
  /// 0x000: Vtable pointer (0x66AF20)
15
  pub vtable: *mut u8,
16
+ /// 0x004: HWND (set to hwnd after construction)
17
+ pub hwnd: u32,
18
+ /// 0x008: IDirectSound* (from DirectSoundCreate)
19
+ pub direct_sound: *mut u8,
20
+ /// 0x00C: Output param from DSSOUND_INIT_BUFFERS (primary buffer caps/format)
21
+ pub primary_buffer_caps: u32,
22
+ /// 0x010: IDirectSoundBuffer* (primary buffer, from DSSOUND_INIT_BUFFERS)
23
+ pub primary_buffer: *mut u8,
24
+ /// 0x014-0x8A3: Unknown fields (includes 500-entry channel slot table)
25
  pub _unknown_014: [u8; 0x890],
26
  /// 0x8A4: Master volume (Fixed, init 0x10000 = 1.0)
27
  pub volume: Fixed,
28
+ /// 0x8A8-0xBB3: Unknown
29
  pub _unknown_8a8: [u8; 0x30C],
30
  /// 0xBB4: Status flag 1 (init 1)
31
  pub status_1: u32,
32
  /// 0xBB8: Status flag 2 (init 1)
33
  pub status_2: u32,
34
+ /// 0xBBC: Init success flag β€” set to 1 when DirectSoundCreate +
35
+ /// DSSOUND_INIT_BUFFERS + IDirectSoundBuffer::Play all succeed.
36
+ pub init_success: u32,
37
+ /// 0xBC0-0xBDF: Unknown trailing fields
38
+ pub _unknown_bc0: [u8; 0x20],
39
  }
40
 
41
+ const _: () = assert!(core::mem::size_of::<DSSound>() == 0xBE0);
 
 
crates/openwa-core/src/game_info.rs CHANGED
@@ -4,8 +4,9 @@
4
  /// `GameInfo__LoadOptions` (0x460AC0) which reads registry values and
5
  /// copies global data into known offsets.
6
  ///
7
- /// PARTIAL: Only fields discovered through GameInfo__LoadOptions are mapped.
8
- /// The actual struct is likely larger. Conservative size 0xF500.
 
9
  #[repr(C)]
10
  pub struct GameInfo {
11
  /// 0x0000-0x044B: Unknown
@@ -21,10 +22,22 @@ pub struct GameInfo {
21
  pub _unknown_044d: [u8; 0xD778 - 0x44D],
22
 
23
  /// 0xD778: Crosshair overflow clamp threshold (compared to 0x11E in render).
 
24
  pub crosshair_overflow_threshold: i32,
25
 
26
- /// 0xD77C-0xDAE7: Unknown
27
- pub _unknown_d77c: [u8; 0xDAE8 - 0xD77C],
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  // --- Cluster 1: data paths ---
30
 
@@ -33,8 +46,14 @@ pub struct GameInfo {
33
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
34
  pub land_dat_path: [u8; 14],
35
 
36
- /// 0xDAFA-0xF39F: Unknown
37
- pub _unknown_dafa: [u8; 0x18A6],
 
 
 
 
 
 
38
 
39
  // --- Cluster 2: game options (populated by LoadOptions) ---
40
 
@@ -60,10 +79,16 @@ pub struct GameInfo {
60
  pub home_lock: u8,
61
  /// 0xF3B1: Unknown
62
  pub _unknown_f3b1: [u8; 3],
63
- /// 0xF3B4: Config DWORDs from globals (7 consecutive u32s).
64
- /// LoadOptions writes 5 DWORDs from G_CONFIG_DWORDS_F3B4 at indices 0..5,
65
- /// then 3 DWORDs from G_CONFIG_DWORDS_F3C4 at indices 4..7 (overlapping).
66
- pub _config_block_f3b4: [u32; 7],
 
 
 
 
 
 
67
  /// 0xF3D0: Unknown (not written by LoadOptions)
68
  pub _unknown_f3d0: [u8; 4],
69
  /// 0xF3D4: Config DWORD (from global 0x88E3B0[0])
@@ -91,11 +116,24 @@ pub struct GameInfo {
91
  /// 0xF485: Config data block (64 bytes copied from global 0x88DFF3)
92
  pub _config_block_f485: [u8; 64],
93
 
94
- /// 0xF4C5-0xF4FF: Unknown remainder
95
- pub _unknown_f4c5: [u8; 0x3B],
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
- const _: () = assert!(core::mem::size_of::<GameInfo>() == 0xF500);
99
 
100
  struct HexU32s<'a>(&'a [u32]);
101
 
@@ -138,7 +176,10 @@ impl core::fmt::Debug for GameInfo {
138
  .field("chat_lines", &self.chat_lines)
139
  .field("pinned_chat_lines", &format_args!("0x{:08X}", self.pinned_chat_lines))
140
  .field("home_lock", &self.home_lock)
141
- .field("_config_block_f3b4", &HexU32s(&self._config_block_f3b4))
 
 
 
142
  .field("_config_dword_f3d4", &format_args!("0x{:08X}", self._config_dword_f3d4))
143
  .field("_config_dword_f3d8", &format_args!("0x{:08X}", self._config_dword_f3d8))
144
  .field("capture_transparent_pngs", &self.capture_transparent_pngs)
@@ -149,6 +190,9 @@ impl core::fmt::Debug for GameInfo {
149
  .field("_zeroed_f3f0", &self._zeroed_f3f0)
150
  .field("_conditional_config_f3f4", &HexU32s(&self._conditional_config_f3f4))
151
  .field("speech_path", &speech_str)
 
 
 
152
  .finish()
153
  }
154
  }
 
4
  /// `GameInfo__LoadOptions` (0x460AC0) which reads registry values and
5
  /// copies global data into known offsets.
6
  ///
7
+ /// PARTIAL: Only fields discovered through GameInfo__LoadOptions and
8
+ /// GameEngine__InitHardware are mapped. Size extended to 0xF91C to
9
+ /// cover all known accesses.
10
  #[repr(C)]
11
  pub struct GameInfo {
12
  /// 0x0000-0x044B: Unknown
 
22
  pub _unknown_044d: [u8; 0xD778 - 0x44D],
23
 
24
  /// 0xD778: Crosshair overflow clamp threshold (compared to 0x11E in render).
25
+ /// Also passed to timer constructor and input controller init.
26
  pub crosshair_overflow_threshold: i32,
27
 
28
+ /// 0xD77C-0xD9DF: Unknown
29
+ pub _unknown_d77c: [u8; 0xD9E0 - 0xD77C],
30
+
31
+ /// 0xD9E0: Streaming audio config data (path config passed to streaming audio ctor).
32
+ /// Address of this field is passed as a pointer parameter.
33
+ pub streaming_audio_config: [u8; 0xDAA4 - 0xD9E0],
34
+
35
+ /// 0xDAA4: Speech/streaming audio enabled flag.
36
+ /// If nonzero, streaming audio subsystem is created in InitHardware.
37
+ pub speech_enabled: u8,
38
+
39
+ /// 0xDAA5-0xDAE7: Unknown
40
+ pub _unknown_daa5: [u8; 0xDAE8 - 0xDAA5],
41
 
42
  // --- Cluster 1: data paths ---
43
 
 
46
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
47
  pub land_dat_path: [u8; 14],
48
 
49
+ /// 0xDAFA-0xF373: Unknown
50
+ pub _unknown_dafa: [u8; 0xF374 - 0xDAFA],
51
+
52
+ /// 0xF374: Display flags passed to DDDisplay::Init.
53
+ pub display_flags: u32,
54
+
55
+ /// 0xF378-0xF39F: Unknown
56
+ pub _unknown_f378: [u8; 0xF3A0 - 0xF378],
57
 
58
  // --- Cluster 2: game options (populated by LoadOptions) ---
59
 
 
79
  pub home_lock: u8,
80
  /// 0xF3B1: Unknown
81
  pub _unknown_f3b1: [u8; 3],
82
+ /// 0xF3B4: Display width β€” first DWORD of the config block.
83
+ /// Written from G_CONFIG_DWORDS_F3B4, updated by DDDisplay::Init retry loop.
84
+ pub display_width: u32,
85
+ /// 0xF3B8: Display height β€” second DWORD of the config block.
86
+ /// Written from G_CONFIG_DWORDS_F3B4, updated by DDDisplay::Init retry loop.
87
+ pub display_height: u32,
88
+ /// 0xF3BC: Remaining config DWORDs (indices 2..7 of the original block).
89
+ /// LoadOptions writes indices 0..5 from G_CONFIG_DWORDS_F3B4,
90
+ /// then indices 4..7 from G_CONFIG_DWORDS_F3C4 (overlapping at [2]).
91
+ pub _config_dwords_f3bc: [u32; 5],
92
  /// 0xF3D0: Unknown (not written by LoadOptions)
93
  pub _unknown_f3d0: [u8; 4],
94
  /// 0xF3D4: Config DWORD (from global 0x88E3B0[0])
 
116
  /// 0xF485: Config data block (64 bytes copied from global 0x88DFF3)
117
  pub _config_block_f485: [u8; 64],
118
 
119
+ /// 0xF4C5-0xF4FF: Unknown
120
+ pub _unknown_f4c5: [u8; 0xF500 - 0xF4C5],
121
+
122
+ // --- Extended region (beyond original 0xF500 conservative estimate) ---
123
+
124
+ /// 0xF500-0xF913: Unknown
125
+ pub _unknown_f500: [u8; 0xF914 - 0xF500],
126
+
127
+ /// 0xF914: Headless/stats mode flag.
128
+ /// If nonzero, InitHardware creates a GameStats stub instead of display hardware.
129
+ pub headless_mode: u32,
130
+
131
+ /// 0xF918: Input state field β€” DDKeyboard+0x4 stores a pointer TO this address.
132
+ /// The game reads/writes through DDKeyboard's pointer, so this must stay in place.
133
+ pub input_state_f918: u32,
134
  }
135
 
136
+ const _: () = assert!(core::mem::size_of::<GameInfo>() == 0xF91C);
137
 
138
  struct HexU32s<'a>(&'a [u32]);
139
 
 
176
  .field("chat_lines", &self.chat_lines)
177
  .field("pinned_chat_lines", &format_args!("0x{:08X}", self.pinned_chat_lines))
178
  .field("home_lock", &self.home_lock)
179
+ .field("display_flags", &format_args!("0x{:08X}", self.display_flags))
180
+ .field("display_width", &self.display_width)
181
+ .field("display_height", &self.display_height)
182
+ .field("_config_dwords_f3bc", &HexU32s(&self._config_dwords_f3bc))
183
  .field("_config_dword_f3d4", &format_args!("0x{:08X}", self._config_dword_f3d4))
184
  .field("_config_dword_f3d8", &format_args!("0x{:08X}", self._config_dword_f3d8))
185
  .field("capture_transparent_pngs", &self.capture_transparent_pngs)
 
190
  .field("_zeroed_f3f0", &self._zeroed_f3f0)
191
  .field("_conditional_config_f3f4", &HexU32s(&self._conditional_config_f3f4))
192
  .field("speech_path", &speech_str)
193
+ .field("speech_enabled", &self.speech_enabled)
194
+ .field("headless_mode", &self.headless_mode)
195
+ .field("input_state_f918", &format_args!("0x{:08X}", self.input_state_f918))
196
  .finish()
197
  }
198
  }
crates/openwa-core/src/game_session.rs CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  /// Top-level game session context, allocated once per game run.
2
  ///
3
  /// `G_GAME_SESSION` (0x7A0884) stores a pointer to this struct. Created by
@@ -60,15 +65,16 @@ pub struct GameSession {
60
  /// 0x09C: high DWORD
61
  pub timer_counter_hi: u32,
62
  /// 0x0A0: `DDGameWrapper*` β€” the main game object wrapper (β†’ `DDGame` at `+0x488`)
63
- pub ddgame_wrapper: *mut u8,
64
  /// 0x0A4: `DDKeyboard*` β€” 0x33C bytes, vtable `DDKeyboard_vtable` (0x66AEC8)
65
- pub keyboard: *mut u8,
66
  /// 0x0A8: `DSSound*` β€” 0xBE0 bytes, vtable `DSSound_vtable` (0x66AF20)
67
- pub sound: *mut u8,
68
- /// 0x0AC: `DisplayGfx*` β€” 0x24E28 bytes (normal), or `GameStats*` in headless mode
 
69
  pub display_gfx: *mut u8,
70
  /// 0x0B0: `Palette*` β€” 0x28 bytes, vtable `Palette_vtable_Maybe`
71
- pub palette: *mut u8,
72
  /// 0x0B4: streaming audio object β€” 0x354 bytes (`FUN_0058bc10`)
73
  pub streaming_audio: *mut u8,
74
  /// 0x0B8: input controller β€” 0x1800 bytes; null if `param_4 == 0` at init
 
1
+ use crate::ddgame_wrapper::DDGameWrapper;
2
+ use crate::ddkeyboard::DDKeyboard;
3
+ use crate::dssound::DSSound;
4
+ use crate::palette::Palette;
5
+
6
  /// Top-level game session context, allocated once per game run.
7
  ///
8
  /// `G_GAME_SESSION` (0x7A0884) stores a pointer to this struct. Created by
 
65
  /// 0x09C: high DWORD
66
  pub timer_counter_hi: u32,
67
  /// 0x0A0: `DDGameWrapper*` β€” the main game object wrapper (β†’ `DDGame` at `+0x488`)
68
+ pub ddgame_wrapper: *mut DDGameWrapper,
69
  /// 0x0A4: `DDKeyboard*` β€” 0x33C bytes, vtable `DDKeyboard_vtable` (0x66AEC8)
70
+ pub keyboard: *mut DDKeyboard,
71
  /// 0x0A8: `DSSound*` β€” 0xBE0 bytes, vtable `DSSound_vtable` (0x66AF20)
72
+ pub sound: *mut DSSound,
73
+ /// 0x0AC: `DisplayGfx*` β€” 0x24E28 bytes (normal), or `GameStats*` in headless mode.
74
+ /// Stays `*mut u8` because it's dual-purpose (DisplayGfx vs GameStats).
75
  pub display_gfx: *mut u8,
76
  /// 0x0B0: `Palette*` β€” 0x28 bytes, vtable `Palette_vtable_Maybe`
77
+ pub palette: *mut Palette,
78
  /// 0x0B4: streaming audio object β€” 0x354 bytes (`FUN_0058bc10`)
79
  pub streaming_audio: *mut u8,
80
  /// 0x0B8: input controller β€” 0x1800 bytes; null if `param_4 == 0` at init
crates/openwa-core/src/game_stats.rs ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// GameStats β€” headless mode display stub.
2
+ ///
3
+ /// Constructor: GameStats__Constructor (0x522DB0), stdcall(this) β†’ GameStats*.
4
+ /// Vtable overlay: 0x66A0F8 (applied after constructor returns).
5
+ /// Size: 0x3560 bytes.
6
+ ///
7
+ /// Created by GameEngine__InitHardware in headless mode (GameInfo+0xF914 != 0)
8
+ /// instead of DisplayGfx. Stored in the same GameSession+0xAC slot.
9
+ ///
10
+ /// OPAQUE: Internal layout not yet mapped.
11
+ #[repr(C)]
12
+ pub struct GameStats {
13
+ pub vtable: *mut u8,
14
+ pub _unknown_004: [u8; 0x3560 - 4],
15
+ }
16
+
17
+ const _: () = assert!(core::mem::size_of::<GameStats>() == 0x3560);
crates/openwa-core/src/game_timer.rs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// GameTimer β€” timing subsystem.
2
+ ///
3
+ /// Constructor: FUN_0053E950, usercall(ESI=this, EAX=init_val), plain RET.
4
+ /// Size: 0x30 bytes (header). Constructor allocates 2 Γ— 0x20E0 internal buffers.
5
+ ///
6
+ /// Created by GameEngine__InitHardware (always).
7
+ /// Stored at GameSession+0xBC.
8
+ ///
9
+ /// OPAQUE: Internal layout not yet mapped.
10
+ #[repr(C)]
11
+ pub struct GameTimer {
12
+ pub _data: [u8; 0x30],
13
+ }
14
+
15
+ const _: () = assert!(core::mem::size_of::<GameTimer>() == 0x30);
crates/openwa-core/src/input_ctrl.rs ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// InputCtrl β€” input controller subsystem.
2
+ ///
3
+ /// Initializer: FUN_0058C0D0, usercall(ESI=this) + stdcall(4 params), RET 0x10.
4
+ /// Vtable: 0x66B3FC.
5
+ /// Size: 0x1800 bytes.
6
+ ///
7
+ /// Created by GameEngine__InitHardware when param4 != 0.
8
+ /// Stored at GameSession+0xB8.
9
+ ///
10
+ /// OPAQUE: Internal layout not yet mapped beyond vtable and a few known fields.
11
+ #[repr(C)]
12
+ pub struct InputCtrl {
13
+ /// 0x000: Vtable pointer (0x66B3FC)
14
+ pub vtable: *mut u8,
15
+ pub _unknown_004: [u8; 0xD74 - 4],
16
+ /// 0xD74: Set to 0x3F9 during inline construction.
17
+ pub _field_d74: u32,
18
+ pub _unknown_d78: [u8; 0x1800 - 0xD78],
19
+ }
20
+
21
+ const _: () = assert!(core::mem::size_of::<InputCtrl>() == 0x1800);
crates/openwa-core/src/lib.rs CHANGED
@@ -29,3 +29,9 @@ pub mod rebase;
29
  pub mod wa;
30
  pub mod wa_call;
31
  pub mod wa_alloc;
 
 
 
 
 
 
 
29
  pub mod wa;
30
  pub mod wa_call;
31
  pub mod wa_alloc;
32
+ pub mod display_gfx;
33
+ pub mod input_ctrl;
34
+ pub mod game_timer;
35
+ pub mod streaming_audio;
36
+ pub mod ddnetgame_wrapper;
37
+ pub mod game_stats;
crates/openwa-core/src/streaming_audio.rs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// StreamingAudio β€” streaming audio playback subsystem (music).
2
+ ///
3
+ /// Constructor: FUN_0058BC10, usercall(ESI=this) + 2 stack params, RET 0x8.
4
+ /// Size: 0x354 bytes.
5
+ ///
6
+ /// Created by GameEngine__InitHardware when speech is enabled
7
+ /// (GameInfo+0xDAA4 != 0) and DSSound initialized successfully.
8
+ /// Stored at GameSession+0xB4.
9
+ ///
10
+ /// OPAQUE: Internal layout not yet mapped.
11
+ #[repr(C)]
12
+ pub struct StreamingAudio {
13
+ pub _data: [u8; 0x354],
14
+ }
15
+
16
+ const _: () = assert!(core::mem::size_of::<StreamingAudio>() == 0x354);
crates/openwa-wormkit/src/replacements/config.rs CHANGED
@@ -196,10 +196,12 @@ unsafe extern "stdcall" fn hook_load_options(gi_ptr: u32) {
196
  gi.detail_level = read_profile_int("Options", "DetailLevel", 5) as u8;
197
  gi._zeroed_f3f0 = 0;
198
 
199
- // Copy 5 DWORDs from globals (indices 0..5 of config block)
200
  let src = rb(va::G_CONFIG_DWORDS_F3B4) as *const u32;
201
- for i in 0..5 {
202
- gi._config_block_f3b4[i] = *src.add(i);
 
 
203
  }
204
 
205
  // Conditional copy: 4 DWORDs if guard == 0
@@ -219,10 +221,10 @@ unsafe extern "stdcall" fn hook_load_options(gi_ptr: u32) {
219
 
220
  gi.energy_bar = read_profile_int("Options", "EnergyBar", 1) as u8;
221
 
222
- // 3 DWORDs from globals (overwrite indices 4..7 of config block)
223
  let src_c4 = rb(va::G_CONFIG_DWORDS_F3C4) as *const u32;
224
  for i in 0..3 {
225
- gi._config_block_f3b4[4 + i] = *src_c4.add(i);
226
  }
227
 
228
  gi.info_transparency = read_profile_int("Options", "InfoTransparency", 0) as u8;
 
196
  gi.detail_level = read_profile_int("Options", "DetailLevel", 5) as u8;
197
  gi._zeroed_f3f0 = 0;
198
 
199
+ // Copy 5 DWORDs from globals (display_width, display_height, + 3 more)
200
  let src = rb(va::G_CONFIG_DWORDS_F3B4) as *const u32;
201
+ gi.display_width = *src;
202
+ gi.display_height = *src.add(1);
203
+ for i in 0..3 {
204
+ gi._config_dwords_f3bc[i] = *src.add(i + 2);
205
  }
206
 
207
  // Conditional copy: 4 DWORDs if guard == 0
 
221
 
222
  gi.energy_bar = read_profile_int("Options", "EnergyBar", 1) as u8;
223
 
224
+ // 3 DWORDs from globals (overwrite indices 2..5 of _config_dwords_f3bc)
225
  let src_c4 = rb(va::G_CONFIG_DWORDS_F3C4) as *const u32;
226
  for i in 0..3 {
227
+ gi._config_dwords_f3bc[i + 2] = *src_c4.add(i);
228
  }
229
 
230
  gi.info_transparency = read_profile_int("Options", "InfoTransparency", 0) as u8;
crates/openwa-wormkit/src/replacements/hardware_init.rs CHANGED
@@ -5,7 +5,7 @@
5
  //!
6
  //! ## Calling convention
7
  //!
8
- //! `__thiscall`: ECX = `*mut GameInfo` (β‰₯0xF914 bytes), 3 stack params
9
  //! (hwnd, param3, param4), `RET 0xC`. Returns 1 on success, 0 on failure.
10
  //!
11
  //! The naked entry trampoline captures ECX, pops the return address β†’ `SAVED_RET`,
@@ -21,21 +21,21 @@
21
  //! IF param4 != 0:
22
  //! input ctrl (0x1800 bytes, inline vtable, FUN_0058C0D0 usercall ESI=this) β†’ session+0xB8
23
  //!
24
- //! IF GameInfo+0xF914 == 0 (normal mode):
25
  //! DisplayGfx (0x24E28, stdcall ctor) β†’ session+0xAC
26
  //! DDDisplay::Init retry loop (configured β†’ 1024Γ—768 β†’ 800Γ—600 β†’ 640Γ—480)
27
  //! screen center / cursor setup
28
  //! DDKeyboard (0x33C, inline) β†’ session+0xA4
29
  //! Palette (0x28, inline) β†’ session+0xB0
30
  //! DSSound (0xBE0, usercall ctor + DirectSoundCreate + coop level) β†’ session+0xA8
31
- //! IF GameInfo+0xDAA4 != 0 AND DSSound OK: streaming audio β†’ session+0xB4
32
  //!
33
  //! ELSE (headless):
34
  //! GameStats (0x3560, stdcall ctor + vtable override) β†’ session+0xAC
35
  //! session+0xA4/0xA8/0xB0/0xB4 = null
36
  //!
37
  //! ALWAYS:
38
- //! session+0x28 = (GameInfo+0xF3B0 != 0) ? 1 : 0
39
  //! DDGameWrapper (0x6F10) β†’ session+0xA0 [via game_session::construct_ddgame_wrapper]
40
  //! Palette vtable[4/3/2] calls + DDKeyboard poll (normal mode only)
41
  //! DDNetGameWrapper (0x2C, stdcall ctor) β†’ session+0xC0
@@ -43,11 +43,16 @@
43
 
44
  use openwa_core::address::va;
45
  use openwa_core::rebase::rb;
 
46
  use openwa_core::game_session::GameSession;
47
  use openwa_core::ddgame_wrapper::DDGameWrapper;
 
48
  use openwa_core::dddisplay::DDDisplay;
49
  use openwa_core::dssound::DSSound;
50
  use openwa_core::palette::Palette;
 
 
 
51
  use openwa_core::wa_alloc::WABox;
52
  use crate::hook;
53
  use crate::log_line;
@@ -88,7 +93,7 @@ static mut DSSOUND_INIT_EAX: u32 = 0;
88
  /// Timer constructor: `usercall(ESI=timer_ptr, EAX=d778_val)`, plain RET.
89
  /// Returns whatever EAX holds after the call.
90
  #[unsafe(naked)]
91
- unsafe extern "cdecl" fn call_timer_ctor(_timer_ptr: *mut u8, _d778_val: u32) -> u32 {
92
  core::arch::naked_asm!(
93
  // [esp+0]=bridge_ret, [esp+4]=timer_ptr, [esp+8]=d778_val
94
  "pushl %esi",
@@ -108,7 +113,7 @@ unsafe extern "cdecl" fn call_timer_ctor(_timer_ptr: *mut u8, _d778_val: u32) ->
108
  /// IMPORTANT: FUN_00573D50 clobbers ECX (MOV ECX, 0x1F4 for REP STOSD).
109
  /// Do NOT use ECX to save bridge_ret β€” read it from the stack instead.
110
  #[unsafe(naked)]
111
- unsafe extern "cdecl" fn call_dssound_ctor(_dssound: *mut u8) {
112
  core::arch::naked_asm!(
113
  // [esp+0]=bridge_ret, [esp+4]=dssound
114
  "movl 4(%esp), %eax", // EAX = dssound (bridge_ret stays on stack untouched)
@@ -129,19 +134,19 @@ static mut DSSOUND_INIT_SAVED_RET: u32 = 0;
129
  /// Caller sets `DSSOUND_INIT_EAX = dssound` before calling.
130
  #[unsafe(naked)]
131
  unsafe extern "stdcall" fn call_dssound_init_buffers(
132
- _out_0x10: *mut u32,
133
- _out_0x0c: *mut u32,
134
  ) -> u32 {
135
  core::arch::naked_asm!(
136
- // Entry: ESP=E, [E+0]=bridge_ret, [E+4]=out_0x10, [E+8]=out_0x0c
137
  "movl {eax_val}, %eax", // EAX = dssound
138
  "movl (%esp), %ecx", // ECX = bridge_ret (temp)
139
  "movl %ecx, {saved_ret}", // save to static (ECX clobbered by FUN_00573E50)
140
- "movl 4(%esp), %ecx", // ECX = out_0x10
141
- "movl 8(%esp), %edx", // EDX = out_0x0c
142
  "addl $0xC, %esp", // ESP = E+12 (discard bridge_ret + 2 args)
143
- "pushl %edx", // ESP = E+8, [E+8] = out_0x0c
144
- "pushl %ecx", // ESP = E+4, [E+4] = out_0x10
145
  "calll *({fn})", // ESP = E, [E] = cont
146
  // FUN_00573E50 RET 0x8: pops cont β†’ E+4, +8 β†’ E+12
147
  // At cont: ESP = E+12; EAX = return value
@@ -242,43 +247,48 @@ unsafe extern "stdcall" fn call_ddisplay_init(
242
  // ─── Implementation ────────────────────────────────────────────────────��──────
243
 
244
  unsafe extern "cdecl" fn impl_init_hardware(
245
- game_info: *mut u8,
246
  hwnd: u32,
247
  param3: u32,
248
  param4: u32,
249
  ) -> u32 {
250
  let _ = log_line("[hardware_init] GameEngine::InitHardware");
251
  let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
252
- let game_info_d778 = *(game_info.add(0xD778) as *const u32);
 
253
 
254
  // ── Input controller (if param4 != 0) ────────────────────────────────────
255
  if param4 == 0 {
256
  (*session).input_ctrl = core::ptr::null_mut();
257
  } else {
258
- let ctrl_ptr = WABox::<u8>::alloc(0x1800, 0x17E0).leak();
259
- // puVar3[0x35d] = 0x3f9 (original sets this; offset 0xD74)
260
- *(ctrl_ptr.add(0xD74) as *mut u32) = 0x3F9;
261
- *(ctrl_ptr as *mut u32) = rb(va::INPUT_CTRL_VTABLE);
262
- (*session).input_ctrl = ctrl_ptr;
263
-
264
- INPUT_CTRL_ESI = ctrl_ptr as u32;
265
- let ok = call_input_ctrl_init(game_info.add(4), param3, param4, game_info_d778);
 
 
 
 
266
  if ok == 0 {
267
- let vtbl = *(ctrl_ptr as *const *const usize);
268
  let dtor: unsafe extern "thiscall" fn(*mut u8, u32) =
269
  core::mem::transmute(*vtbl);
270
- dtor(ctrl_ptr, 1);
271
  (*session).input_ctrl = core::ptr::null_mut();
272
  return 0;
273
  }
274
  }
275
 
276
  // ── Timer object (ALWAYS) ─────────────────────────────────────────────────
277
- let timer_ptr = WABox::<u8>::alloc(0x30, 0x30).leak();
278
- call_timer_ctor(timer_ptr, game_info_d778);
279
- (*session).timer_obj = timer_ptr;
280
 
281
- let headless = *(game_info.add(0xF914) as *const u32) != 0;
282
 
283
  if !headless {
284
  // ── DisplayGfx ───────────────────────────────────────────────────────
@@ -288,11 +298,9 @@ unsafe extern "cdecl" fn impl_init_hardware(
288
  (*session).display_gfx = display_gfx;
289
 
290
  // ── DDDisplay::Init retry loop ────────────────────────────────────────
291
- // DDDisplay::Init is usercall(ECX=height) + stdcall(display_gfx, hwnd, width, flags).
292
- // Use call_ddisplay_init which sets ECX from DDISPLAY_INIT_ECX before the tail-jump.
293
- let flags = *(game_info.add(0xF374) as *const u32);
294
- let w0 = *(game_info.add(0xF3B4) as *const u32);
295
- let h0 = *(game_info.add(0xF3B8) as *const u32);
296
 
297
  DDISPLAY_INIT_ECX = h0;
298
  let mut init_ok = call_ddisplay_init(display_gfx, hwnd, w0, flags) != 0;
@@ -304,8 +312,8 @@ unsafe extern "cdecl" fn impl_init_hardware(
304
  (0x280, 0x1E0), // 640Γ—480
305
  ];
306
  for &(w, h) in &fallbacks {
307
- *(game_info.add(0xF3B4) as *mut u32) = w;
308
- *(game_info.add(0xF3B8) as *mut u32) = h;
309
  DDISPLAY_INIT_ECX = h;
310
  if call_ddisplay_init(display_gfx, hwnd, w, flags) != 0 {
311
  init_ok = true;
@@ -329,9 +337,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
329
  let h = GetSystemMetrics(SM_CYSCREEN);
330
  (w / 2, h / 2)
331
  } else {
332
- let w = *(game_info.add(0xF3B4) as *const i32);
333
- let h = *(game_info.add(0xF3B8) as *const i32);
334
- (w / 2, h / 2)
335
  };
336
 
337
  (*session).screen_center_x = cx;
@@ -348,8 +354,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
348
  let hwnd_val: HWND = *(rb(va::G_FRONTEND_HWND) as *const HWND);
349
  let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 };
350
  GetClientRect(hwnd_val, &mut rect);
351
- // MapWindowPoints via IAT thunk (avoids adding a new windows-sys feature).
352
- let map_fn_ptr = *(rb(0x0061_A588) as *const usize);
353
  let map_fn: unsafe extern "stdcall" fn(HWND, HWND, *mut RECT, u32) -> i32 =
354
  core::mem::transmute(map_fn_ptr);
355
  map_fn(hwnd_val, core::ptr::null_mut(), &mut rect, 2);
@@ -358,53 +363,54 @@ unsafe extern "cdecl" fn impl_init_hardware(
358
  }
359
 
360
  // ── DDKeyboard (inline construction) ──────────────────────────────────
361
- let kb_ptr = WABox::<u8>::alloc(0x33C, 0x31C).leak();
362
- *(kb_ptr as *mut u32) = rb(va::DDKEYBOARD_VTABLE);
363
- *(kb_ptr.add(4) as *mut u32) = game_info.add(0xF918) as u32;
364
- *(kb_ptr.add(8) as *mut u32) = 1;
365
- core::ptr::write_bytes(kb_ptr.add(0x11C), 0, 0x100);
366
- core::ptr::write_bytes(kb_ptr.add(0x21C), 0, 0x100);
367
- *(kb_ptr.add(0x14) as *mut u32) = 0;
368
- *(kb_ptr.add(0x18) as *mut u32) = 0;
369
- (*session).keyboard = kb_ptr;
370
 
371
  // ── Palette (inline construction) ─────────────────────────────────────
372
- let pal_ptr = WABox::<u8>::alloc(0x28, 0).leak();
373
- *(pal_ptr as *mut u32) = rb(va::PALETTE_VTABLE_MAYBE);
374
- *(pal_ptr.add(4) as *mut u32) = 0xFFFF_FFFF;
375
- (*session).palette = pal_ptr;
376
 
377
  // ── DSSound ───────────────────────────────────────────────────────────
378
  (*session).sound = core::ptr::null_mut();
379
  {
380
- let snd_mem = WABox::<u8>::alloc(0xBE0, 0xBC0).leak();
381
- call_dssound_ctor(snd_mem);
382
- *(snd_mem.add(4) as *mut u32) = hwnd;
383
- // Store DSSound pointer unconditionally β€” original does this before init checks.
384
- // snd_mem+0xBBC = 1 only if all 3 COM init steps succeed; WA code checks this flag.
385
- (*session).sound = snd_mem;
386
 
387
  let ds_create: unsafe extern "stdcall" fn(*const u8, *mut *mut u8, *const u8) -> i32 =
388
  core::mem::transmute(rb(va::DIRECTSOUND_CREATE) as usize);
389
- let hr = ds_create(core::ptr::null(), snd_mem.add(8) as *mut *mut u8, core::ptr::null());
 
 
 
 
390
 
391
  if hr == 0 {
392
- DSSOUND_INIT_EAX = snd_mem as u32;
393
  let hr2 = call_dssound_init_buffers(
394
- snd_mem.add(0x10) as *mut u32,
395
- snd_mem.add(0x0C) as *mut u32,
396
  );
397
  if hr2 == 0 {
398
- // vtable[12] (offset 0x30) of the IDirectSoundBuffer* stored at snd_mem+0x10
399
- // by DSSOUND_INIT_BUFFERS. IDirectSoundBuffer::Play(this, 0, 0, DSBPLAY_LOOPING=1)
400
- let pds = *(snd_mem.add(0x10) as *const *const *const usize);
401
  let vtbl = *pds;
402
  let play: unsafe extern "stdcall" fn(
403
  *const *const usize, u32, u32, u32,
404
  ) -> i32 = core::mem::transmute(*vtbl.add(0x30 / 4));
405
  let hr3 = play(pds, 0, 0, 1);
406
  if hr3 == 0 {
407
- *(snd_mem.add(0xBBC) as *mut u32) = 1;
408
  }
409
  }
410
  }
@@ -413,24 +419,26 @@ unsafe extern "cdecl" fn impl_init_hardware(
413
  // ── Streaming audio ───────────────────────────────────────────────────
414
  (*session).streaming_audio = core::ptr::null_mut();
415
  if !(*session).sound.is_null() {
416
- let speech_flag = *(game_info.add(0xDAA4) as *const u8);
417
- if speech_flag != 0 {
418
  let stream_mem = WABox::<u8>::alloc(0x354, 0x334).leak();
419
- let dssound = (*session).sound;
420
- let ids = *(dssound.add(8) as *const *mut u8);
421
- call_streaming_audio_ctor(stream_mem, ids, game_info.add(0xD9E0));
 
 
 
 
422
  (*session).streaming_audio = stream_mem;
423
  }
424
  }
425
  } else {
426
  // ── Headless / stats mode ─────────────────────────────────────────────
427
- let stats_mem = WABox::<u8>::alloc(0x3560, 0x3560).leak();
428
  let gamestats_ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
429
  core::mem::transmute(rb(va::GAMESTATS_CTOR) as usize);
430
- gamestats_ctor(stats_mem);
431
- *(stats_mem as *mut u32) = rb(va::GAMESTATS_VTABLE);
432
- let stats_ptr = stats_mem;
433
- (*session).display_gfx = stats_ptr;
434
  (*session).keyboard = core::ptr::null_mut();
435
  (*session).sound = core::ptr::null_mut();
436
  (*session).palette = core::ptr::null_mut();
@@ -438,37 +446,33 @@ unsafe extern "cdecl" fn impl_init_hardware(
438
  }
439
 
440
  // ── Windowed/fullscreen flag β†’ session+0x28, and session+0x24 = 1 ──────────
441
- let fullscreen_word = *(game_info.add(0xF3B0) as *const u16);
442
  *((session as *mut u8).add(0x28) as *mut u32) = (fullscreen_word != 0) as u32;
443
- // Original always sets session+0x24 = 1 unconditionally (both normal + headless).
444
  *((session as *mut u8).add(0x24) as *mut u32) = 1;
445
 
446
  // ── DDGameWrapper (ALWAYS) ────────────────────────────────────────────────
447
- // The 7 explicit args mirror the session fields:
448
- // display_gfx (DisplayGfx*), sound (DSSound*), keyboard (DDKeyboard* β€” called "gfx"),
449
- // palette, streaming_audio ("music"), input_ctrl ("network").
450
  let wrapper = game_session::construct_ddgame_wrapper(
451
- game_info,
452
  WABox::<DDGameWrapper>::alloc(0x6F10, 0x6EF0).leak(),
453
  (*session).display_gfx as *mut DDDisplay,
454
- (*session).sound as *mut DSSound,
455
- (*session).keyboard,
456
- (*session).palette as *mut Palette,
457
  (*session).streaming_audio,
458
  (*session).input_ctrl,
459
  );
460
- (*session).ddgame_wrapper = wrapper as *mut u8;
461
 
462
  // ── Palette vtable[4/3/2] + keyboard poll (normal mode only) ─────────────
463
  if !headless {
464
  let pal = (*session).palette;
465
  if !pal.is_null() {
466
  let vtbl = *(pal as *const *const usize);
467
- let vt4: unsafe extern "thiscall" fn(*mut u8) =
468
  core::mem::transmute(*vtbl.add(4));
469
- let vt3: unsafe extern "thiscall" fn(*mut u8) =
470
  core::mem::transmute(*vtbl.add(3));
471
- let vt2: unsafe extern "thiscall" fn(*mut u8, u32) =
472
  core::mem::transmute(*vtbl.add(2));
473
  vt4(pal);
474
  vt3(pal);
@@ -477,7 +481,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
477
 
478
  let kb = (*session).keyboard;
479
  if !kb.is_null() {
480
- let kb_poll: unsafe extern "stdcall" fn(*mut u8) =
481
  core::mem::transmute(rb(va::DDKEYBOARD_POLL_KEYBOARD_STATE) as usize);
482
  kb_poll(kb);
483
  }
 
5
  //!
6
  //! ## Calling convention
7
  //!
8
+ //! `__thiscall`: ECX = `*mut GameInfo` (β‰₯0xF91C bytes), 3 stack params
9
  //! (hwnd, param3, param4), `RET 0xC`. Returns 1 on success, 0 on failure.
10
  //!
11
  //! The naked entry trampoline captures ECX, pops the return address β†’ `SAVED_RET`,
 
21
  //! IF param4 != 0:
22
  //! input ctrl (0x1800 bytes, inline vtable, FUN_0058C0D0 usercall ESI=this) β†’ session+0xB8
23
  //!
24
+ //! IF GameInfo.headless_mode == 0 (normal mode):
25
  //! DisplayGfx (0x24E28, stdcall ctor) β†’ session+0xAC
26
  //! DDDisplay::Init retry loop (configured β†’ 1024Γ—768 β†’ 800Γ—600 β†’ 640Γ—480)
27
  //! screen center / cursor setup
28
  //! DDKeyboard (0x33C, inline) β†’ session+0xA4
29
  //! Palette (0x28, inline) β†’ session+0xB0
30
  //! DSSound (0xBE0, usercall ctor + DirectSoundCreate + coop level) β†’ session+0xA8
31
+ //! IF GameInfo.speech_enabled != 0 AND DSSound OK: streaming audio β†’ session+0xB4
32
  //!
33
  //! ELSE (headless):
34
  //! GameStats (0x3560, stdcall ctor + vtable override) β†’ session+0xAC
35
  //! session+0xA4/0xA8/0xB0/0xB4 = null
36
  //!
37
  //! ALWAYS:
38
+ //! session+0x28 = (GameInfo.home_lock != 0) ? 1 : 0
39
  //! DDGameWrapper (0x6F10) β†’ session+0xA0 [via game_session::construct_ddgame_wrapper]
40
  //! Palette vtable[4/3/2] calls + DDKeyboard poll (normal mode only)
41
  //! DDNetGameWrapper (0x2C, stdcall ctor) β†’ session+0xC0
 
43
 
44
  use openwa_core::address::va;
45
  use openwa_core::rebase::rb;
46
+ use openwa_core::game_info::GameInfo;
47
  use openwa_core::game_session::GameSession;
48
  use openwa_core::ddgame_wrapper::DDGameWrapper;
49
+ use openwa_core::ddkeyboard::DDKeyboard;
50
  use openwa_core::dddisplay::DDDisplay;
51
  use openwa_core::dssound::DSSound;
52
  use openwa_core::palette::Palette;
53
+ use openwa_core::input_ctrl::InputCtrl;
54
+ use openwa_core::game_timer::GameTimer;
55
+ use openwa_core::game_stats::GameStats;
56
  use openwa_core::wa_alloc::WABox;
57
  use crate::hook;
58
  use crate::log_line;
 
93
  /// Timer constructor: `usercall(ESI=timer_ptr, EAX=d778_val)`, plain RET.
94
  /// Returns whatever EAX holds after the call.
95
  #[unsafe(naked)]
96
+ unsafe extern "cdecl" fn call_timer_ctor(_timer_ptr: *mut GameTimer, _d778_val: u32) -> u32 {
97
  core::arch::naked_asm!(
98
  // [esp+0]=bridge_ret, [esp+4]=timer_ptr, [esp+8]=d778_val
99
  "pushl %esi",
 
113
  /// IMPORTANT: FUN_00573D50 clobbers ECX (MOV ECX, 0x1F4 for REP STOSD).
114
  /// Do NOT use ECX to save bridge_ret β€” read it from the stack instead.
115
  #[unsafe(naked)]
116
+ unsafe extern "cdecl" fn call_dssound_ctor(_dssound: *mut DSSound) {
117
  core::arch::naked_asm!(
118
  // [esp+0]=bridge_ret, [esp+4]=dssound
119
  "movl 4(%esp), %eax", // EAX = dssound (bridge_ret stays on stack untouched)
 
134
  /// Caller sets `DSSOUND_INIT_EAX = dssound` before calling.
135
  #[unsafe(naked)]
136
  unsafe extern "stdcall" fn call_dssound_init_buffers(
137
+ _out_primary_buffer: *mut *mut u8,
138
+ _out_primary_caps: *mut u32,
139
  ) -> u32 {
140
  core::arch::naked_asm!(
141
+ // Entry: ESP=E, [E+0]=bridge_ret, [E+4]=out_primary_buffer, [E+8]=out_primary_caps
142
  "movl {eax_val}, %eax", // EAX = dssound
143
  "movl (%esp), %ecx", // ECX = bridge_ret (temp)
144
  "movl %ecx, {saved_ret}", // save to static (ECX clobbered by FUN_00573E50)
145
+ "movl 4(%esp), %ecx", // ECX = out_primary_buffer
146
+ "movl 8(%esp), %edx", // EDX = out_primary_caps
147
  "addl $0xC, %esp", // ESP = E+12 (discard bridge_ret + 2 args)
148
+ "pushl %edx", // ESP = E+8, [E+8] = out_primary_caps
149
+ "pushl %ecx", // ESP = E+4, [E+4] = out_primary_buffer
150
  "calll *({fn})", // ESP = E, [E] = cont
151
  // FUN_00573E50 RET 0x8: pops cont β†’ E+4, +8 β†’ E+12
152
  // At cont: ESP = E+12; EAX = return value
 
247
  // ─── Implementation ────────────────────────────────────────────────────��──────
248
 
249
  unsafe extern "cdecl" fn impl_init_hardware(
250
+ game_info: *mut GameInfo,
251
  hwnd: u32,
252
  param3: u32,
253
  param4: u32,
254
  ) -> u32 {
255
  let _ = log_line("[hardware_init] GameEngine::InitHardware");
256
  let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
257
+ let gi = &mut *game_info;
258
+ let d778_val = gi.crosshair_overflow_threshold as u32;
259
 
260
  // ── Input controller (if param4 != 0) ────────────────────────────────────
261
  if param4 == 0 {
262
  (*session).input_ctrl = core::ptr::null_mut();
263
  } else {
264
+ let ctrl = WABox::<InputCtrl>::alloc(0x1800, 0x17E0).leak();
265
+ (*ctrl)._field_d74 = 0x3F9;
266
+ (*ctrl).vtable = rb(va::INPUT_CTRL_VTABLE) as *mut u8;
267
+ (*session).input_ctrl = ctrl as *mut u8;
268
+
269
+ INPUT_CTRL_ESI = ctrl as u32;
270
+ let ok = call_input_ctrl_init(
271
+ (game_info as *mut u8).add(4),
272
+ param3,
273
+ param4,
274
+ d778_val,
275
+ );
276
  if ok == 0 {
277
+ let vtbl = *(ctrl as *const *const usize);
278
  let dtor: unsafe extern "thiscall" fn(*mut u8, u32) =
279
  core::mem::transmute(*vtbl);
280
+ dtor(ctrl as *mut u8, 1);
281
  (*session).input_ctrl = core::ptr::null_mut();
282
  return 0;
283
  }
284
  }
285
 
286
  // ── Timer object (ALWAYS) ─────────────────────────────────────────────────
287
+ let timer = WABox::<GameTimer>::alloc(0x30, 0x30).leak();
288
+ call_timer_ctor(timer, d778_val);
289
+ (*session).timer_obj = timer as *mut u8;
290
 
291
+ let headless = gi.headless_mode != 0;
292
 
293
  if !headless {
294
  // ── DisplayGfx ───────────────────────────────────────────────────────
 
298
  (*session).display_gfx = display_gfx;
299
 
300
  // ── DDDisplay::Init retry loop ────────────────────────────────────────
301
+ let flags = gi.display_flags;
302
+ let w0 = gi.display_width;
303
+ let h0 = gi.display_height;
 
 
304
 
305
  DDISPLAY_INIT_ECX = h0;
306
  let mut init_ok = call_ddisplay_init(display_gfx, hwnd, w0, flags) != 0;
 
312
  (0x280, 0x1E0), // 640Γ—480
313
  ];
314
  for &(w, h) in &fallbacks {
315
+ gi.display_width = w;
316
+ gi.display_height = h;
317
  DDISPLAY_INIT_ECX = h;
318
  if call_ddisplay_init(display_gfx, hwnd, w, flags) != 0 {
319
  init_ok = true;
 
337
  let h = GetSystemMetrics(SM_CYSCREEN);
338
  (w / 2, h / 2)
339
  } else {
340
+ (gi.display_width as i32 / 2, gi.display_height as i32 / 2)
 
 
341
  };
342
 
343
  (*session).screen_center_x = cx;
 
354
  let hwnd_val: HWND = *(rb(va::G_FRONTEND_HWND) as *const HWND);
355
  let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 };
356
  GetClientRect(hwnd_val, &mut rect);
357
+ let map_fn_ptr = *(rb(va::IAT_MAP_WINDOW_POINTS) as *const usize);
 
358
  let map_fn: unsafe extern "stdcall" fn(HWND, HWND, *mut RECT, u32) -> i32 =
359
  core::mem::transmute(map_fn_ptr);
360
  map_fn(hwnd_val, core::ptr::null_mut(), &mut rect, 2);
 
363
  }
364
 
365
  // ── DDKeyboard (inline construction) ──────────────────────────────────
366
+ let kb = WABox::<DDKeyboard>::alloc(0x33C, 0x31C).leak();
367
+ (*kb).vtable = rb(va::DDKEYBOARD_VTABLE) as *mut u8;
368
+ (*kb).game_info_input_ptr = &raw mut gi.input_state_f918 as u32;
369
+ (*kb)._field_008 = 1;
370
+ core::ptr::write_bytes((*kb).key_state.as_mut_ptr(), 0, 0x100);
371
+ core::ptr::write_bytes((*kb).prev_state.as_mut_ptr(), 0, 0x100);
372
+ (*kb)._field_014 = 0;
373
+ (*kb)._field_018 = 0;
374
+ (*session).keyboard = kb;
375
 
376
  // ── Palette (inline construction) ─────────────────────────────────────
377
+ let pal = WABox::<Palette>::alloc(0x28, 0).leak();
378
+ (*pal).vtable = rb(va::PALETTE_VTABLE_MAYBE) as *mut u8;
379
+ *((pal as *mut u8).add(4) as *mut u32) = 0xFFFF_FFFF;
380
+ (*session).palette = pal;
381
 
382
  // ── DSSound ───────────────────────────────────────────────────────────
383
  (*session).sound = core::ptr::null_mut();
384
  {
385
+ let snd = WABox::<DSSound>::alloc(0xBE0, 0xBC0).leak();
386
+ call_dssound_ctor(snd);
387
+ (*snd).hwnd = hwnd;
388
+ (*session).sound = snd;
 
 
389
 
390
  let ds_create: unsafe extern "stdcall" fn(*const u8, *mut *mut u8, *const u8) -> i32 =
391
  core::mem::transmute(rb(va::DIRECTSOUND_CREATE) as usize);
392
+ let hr = ds_create(
393
+ core::ptr::null(),
394
+ &mut (*snd).direct_sound,
395
+ core::ptr::null(),
396
+ );
397
 
398
  if hr == 0 {
399
+ DSSOUND_INIT_EAX = snd as u32;
400
  let hr2 = call_dssound_init_buffers(
401
+ &mut (*snd).primary_buffer,
402
+ &mut (*snd).primary_buffer_caps,
403
  );
404
  if hr2 == 0 {
405
+ // IDirectSoundBuffer::Play(this, 0, 0, DSBPLAY_LOOPING=1)
406
+ let pds = (*snd).primary_buffer as *const *const usize;
 
407
  let vtbl = *pds;
408
  let play: unsafe extern "stdcall" fn(
409
  *const *const usize, u32, u32, u32,
410
  ) -> i32 = core::mem::transmute(*vtbl.add(0x30 / 4));
411
  let hr3 = play(pds, 0, 0, 1);
412
  if hr3 == 0 {
413
+ (*snd).init_success = 1;
414
  }
415
  }
416
  }
 
419
  // ── Streaming audio ───────────────────────────────────────────────────
420
  (*session).streaming_audio = core::ptr::null_mut();
421
  if !(*session).sound.is_null() {
422
+ if gi.speech_enabled != 0 {
 
423
  let stream_mem = WABox::<u8>::alloc(0x354, 0x334).leak();
424
+ let snd = (*session).sound;
425
+ let ids = (*snd).direct_sound;
426
+ call_streaming_audio_ctor(
427
+ stream_mem,
428
+ ids,
429
+ gi.streaming_audio_config.as_mut_ptr(),
430
+ );
431
  (*session).streaming_audio = stream_mem;
432
  }
433
  }
434
  } else {
435
  // ── Headless / stats mode ─────────────────────────────────────────────
436
+ let stats = WABox::<GameStats>::alloc(0x3560, 0x3560).leak();
437
  let gamestats_ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
438
  core::mem::transmute(rb(va::GAMESTATS_CTOR) as usize);
439
+ gamestats_ctor(stats as *mut u8);
440
+ (*stats).vtable = rb(va::GAMESTATS_VTABLE) as *mut u8;
441
+ (*session).display_gfx = stats as *mut u8;
 
442
  (*session).keyboard = core::ptr::null_mut();
443
  (*session).sound = core::ptr::null_mut();
444
  (*session).palette = core::ptr::null_mut();
 
446
  }
447
 
448
  // ── Windowed/fullscreen flag β†’ session+0x28, and session+0x24 = 1 ──────────
449
+ let fullscreen_word = gi.home_lock;
450
  *((session as *mut u8).add(0x28) as *mut u32) = (fullscreen_word != 0) as u32;
 
451
  *((session as *mut u8).add(0x24) as *mut u32) = 1;
452
 
453
  // ── DDGameWrapper (ALWAYS) ────────────────────────────────────────────────
 
 
 
454
  let wrapper = game_session::construct_ddgame_wrapper(
455
+ game_info as *mut u8,
456
  WABox::<DDGameWrapper>::alloc(0x6F10, 0x6EF0).leak(),
457
  (*session).display_gfx as *mut DDDisplay,
458
+ (*session).sound,
459
+ (*session).keyboard as *mut u8,
460
+ (*session).palette,
461
  (*session).streaming_audio,
462
  (*session).input_ctrl,
463
  );
464
+ (*session).ddgame_wrapper = wrapper;
465
 
466
  // ── Palette vtable[4/3/2] + keyboard poll (normal mode only) ─────────────
467
  if !headless {
468
  let pal = (*session).palette;
469
  if !pal.is_null() {
470
  let vtbl = *(pal as *const *const usize);
471
+ let vt4: unsafe extern "thiscall" fn(*mut Palette) =
472
  core::mem::transmute(*vtbl.add(4));
473
+ let vt3: unsafe extern "thiscall" fn(*mut Palette) =
474
  core::mem::transmute(*vtbl.add(3));
475
+ let vt2: unsafe extern "thiscall" fn(*mut Palette, u32) =
476
  core::mem::transmute(*vtbl.add(2));
477
  vt4(pal);
478
  vt3(pal);
 
481
 
482
  let kb = (*session).keyboard;
483
  if !kb.is_null() {
484
+ let kb_poll: unsafe extern "stdcall" fn(*mut DDKeyboard) =
485
  core::mem::transmute(rb(va::DDKEYBOARD_POLL_KEYBOARD_STATE) as usize);
486
  kb_poll(kb);
487
  }