paavohtl Claude Opus 4.6 commited on
Commit
b3cb49f
·
1 Parent(s): 8191e6e

refactor: add GameInfo type and use typed field access in LoadOptions

Browse files

New GameInfo struct in openwa-types with #[repr(C)] layout covering
known fields at offsets 0xDAE8 and 0xF3A0-0xF4C4. Conservative size
0xF500 with _unknown padding for unmapped regions.

Refactors hook_load_options to cast the raw pointer to &mut GameInfo,
replacing all manual pointer arithmetic with named field access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

crates/openwa-types/src/game_info.rs ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// GameInfo — large game configuration/session struct.
2
+ ///
3
+ /// Created by `GameInfo__InitSession` (0x4608E0), populated by
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-0xDAE7: Unknown
12
+ pub _unknown_0000: [u8; 0xDAE8],
13
+
14
+ // --- Cluster 1: data paths ---
15
+
16
+ /// 0xDAE8: Config DWORD (copied from global 0x88E390)
17
+ pub _config_dword_dae8: u32,
18
+ /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
19
+ pub land_dat_path: [u8; 14],
20
+
21
+ /// 0xDAFA-0xF39F: Unknown
22
+ pub _unknown_dafa: [u8; 0x18A6],
23
+
24
+ // --- Cluster 2: game options (populated by LoadOptions) ---
25
+
26
+ /// 0xF3A0: Unknown config byte (from global 0x7C0D38)
27
+ pub _config_byte_f3a0: u8,
28
+ /// 0xF3A1: Detail level (registry: DetailLevel, default 5)
29
+ pub detail_level: u8,
30
+ /// 0xF3A2: Energy bar display (registry: EnergyBar, default 1)
31
+ pub energy_bar: u8,
32
+ /// 0xF3A3: Info transparency (registry: InfoTransparency, default 0)
33
+ pub info_transparency: u8,
34
+ /// 0xF3A4: Info spy enabled (registry: InfoSpy, default 1, bool coerced)
35
+ pub info_spy: u8,
36
+ /// 0xF3A5: Chat pinned (registry: ChatPinned, default 0)
37
+ pub chat_pinned: u8,
38
+ /// 0xF3A6: Unknown
39
+ pub _unknown_f3a6: [u8; 2],
40
+ /// 0xF3A8: Chat line count (registry: ChatLines, default 0)
41
+ pub chat_lines: u32,
42
+ /// 0xF3AC: Pinned chat lines (registry: PinnedChatLines, default 0xFFFFFFFF)
43
+ pub pinned_chat_lines: u32,
44
+ /// 0xF3B0: Home lock (registry: HomeLock, default 0)
45
+ pub home_lock: u8,
46
+ /// 0xF3B1: Unknown
47
+ pub _unknown_f3b1: [u8; 3],
48
+ /// 0xF3B4: Config DWORDs from globals (7 consecutive u32s).
49
+ /// LoadOptions writes 5 DWORDs from G_CONFIG_DWORDS_F3B4 at indices 0..5,
50
+ /// then 3 DWORDs from G_CONFIG_DWORDS_F3C4 at indices 4..7 (overlapping).
51
+ pub _config_block_f3b4: [u32; 7],
52
+ /// 0xF3D0: Unknown (not written by LoadOptions)
53
+ pub _unknown_f3d0: [u8; 4],
54
+ /// 0xF3D4: Config DWORD (from global 0x88E3B0[0])
55
+ pub _config_dword_f3d4: u32,
56
+ /// 0xF3D8: Config DWORD (from global 0x88E3B0[1])
57
+ pub _config_dword_f3d8: u32,
58
+ /// 0xF3DC: Capture transparent PNGs flag (registry, default 0)
59
+ pub capture_transparent_pngs: u32,
60
+ /// 0xF3E0: Camera unlock mouse speed (registry, clamped to 0xB504 then squared)
61
+ pub camera_unlock_mouse_speed: u32,
62
+ /// 0xF3E4: Config DWORD (from global 0x88E44C)
63
+ pub _config_dword_f3e4: u32,
64
+ /// 0xF3E8: Background debris parallax (registry, fixed-point 16.16)
65
+ pub background_debris_parallax: u32,
66
+ /// 0xF3EC: Topmost explosion onomatopoeia flag (registry, default 0)
67
+ pub topmost_explosion_onomatopoeia: u32,
68
+ /// 0xF3F0: Zeroed at init
69
+ pub _zeroed_f3f0: u16,
70
+ /// 0xF3F2: Unknown
71
+ pub _unknown_f3f2: [u8; 2],
72
+ /// 0xF3F4: Conditional config block (4 DWORDs from global 0x88E3B8, only if guard==0)
73
+ pub _conditional_config_f3f4: [u32; 4],
74
+ /// 0xF404: Speech directory path (null-terminated, up to 129 bytes)
75
+ pub speech_path: [u8; 0x81],
76
+ /// 0xF485: Config data block (64 bytes copied from global 0x88DFF3)
77
+ pub _config_block_f485: [u8; 64],
78
+
79
+ /// 0xF4C5-0xF4FF: Unknown remainder
80
+ pub _unknown_f4c5: [u8; 0x3B],
81
+ }
82
+
83
+ const _: () = assert!(core::mem::size_of::<GameInfo>() == 0xF500);
crates/openwa-types/src/lib.rs CHANGED
@@ -11,5 +11,6 @@ pub mod landscape;
11
  pub mod dssound;
12
  pub mod opengl;
13
  pub mod frontend;
 
14
  pub mod scheme;
15
  pub mod address;
 
11
  pub mod dssound;
12
  pub mod opengl;
13
  pub mod frontend;
14
+ pub mod game_info;
15
  pub mod scheme;
16
  pub mod address;
crates/openwa-wormkit/src/replacements/config.rs CHANGED
@@ -134,30 +134,28 @@ unsafe extern "stdcall" fn hook_registry_clean_all(struct_ptr: u32) {
134
  ///
135
  /// Reads game options from the Windows registry and copies various globals
136
  /// into the GameInfo struct at known offsets.
137
- unsafe extern "stdcall" fn hook_load_options(gi: u32) {
138
  use openwa_lib::wa::registry::read_profile_int;
 
 
 
139
 
140
  let _ = log_line("[Config] LoadOptions: loading game options from registry");
141
 
142
  // Format speech path: "%s\\user\\speech"
143
  let base_dir = rb(va::G_BASE_DIR) as *const u8;
144
- let speech_dest = (gi + 0xF404) as *mut u8;
145
  let base_str = std::ffi::CStr::from_ptr(base_dir as *const i8);
146
  let speech_path = format!("{}\\user\\speech\0", base_str.to_string_lossy());
147
- core::ptr::copy_nonoverlapping(
148
- speech_path.as_ptr(),
149
- speech_dest,
150
- speech_path.len(),
151
- );
152
 
153
- // Copy 64 bytes from global 0x88DFF3 → GameInfo+0xF485
154
  core::ptr::copy_nonoverlapping(
155
  rb(va::G_GAMEINFO_BLOCK_F485) as *const u8,
156
- (gi + 0xF485) as *mut u8,
157
  64,
158
  );
159
 
160
- // Format streams directory and randomize stream indices
161
  let streams_dest = rb(va::G_STREAMS_DIR) as *mut u8;
162
  let streams_path = format!("{}\\streams\0", base_str.to_string_lossy());
163
  core::ptr::copy_nonoverlapping(
@@ -179,78 +177,57 @@ unsafe extern "stdcall" fn hook_load_options(gi: u32) {
179
  // Stream volume: 0x10 if flag set, else 0
180
  let stream_vol_addr = rb(va::G_STREAM_INDICES_END) as *mut u8;
181
  *stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 { 0x10 } else { 0 };
182
- // Secondary volume byte
183
  *(rb(va::G_STREAM_VOLUME) as *mut u8) = 0x4B;
184
 
185
- // Copy "data\land.dat" string (14 bytes) → GameInfo+0xDAEC
186
  core::ptr::copy_nonoverlapping(
187
  rb(va::G_LAND_DAT_STRING) as *const u8,
188
- (gi + 0xDAEC) as *mut u8,
189
  14,
190
  );
191
 
192
- // Copy byte from global → GameInfo+0xF3A0
193
- *((gi + 0xF3A0) as *mut u8) = *(rb(va::G_CONFIG_BYTE_F3A0) as *const u8);
194
 
195
- // Read registry values from "Options" section
196
- let detail = read_profile_int("Options", "DetailLevel", 5);
197
- *((gi + 0xF3A1) as *mut u8) = detail as u8;
198
 
199
- // Zero 2 bytes at +0xF3F0
200
- *((gi + 0xF3F0) as *mut u16) = 0;
201
-
202
- // Copy 5 DWORDs from globals → GameInfo+0xF3B4..+0xF3D0
203
  let src = rb(va::G_CONFIG_DWORDS_F3B4) as *const u32;
204
- for i in 0u32..5 {
205
- let offset = 0xF3B4 + i * 4;
206
- *((gi + offset) as *mut u32) = *src.add(i as usize);
207
  }
208
 
209
  // Conditional copy: 4 DWORDs if guard == 0
210
  if *(rb(va::G_CONFIG_GUARD) as *const u32) == 0 {
211
  let src = rb(va::G_CONFIG_DWORDS_F3F4) as *const u32;
212
- for i in 0u32..4 {
213
- let offset = 0xF3F4 + i * 4;
214
- *((gi + offset) as *mut u32) = *src.add(i as usize);
215
  }
216
  }
217
 
218
  // Single DWORDs from globals
219
- *((gi + 0xDAE8) as *mut u32) = *(rb(va::G_CONFIG_DWORD_DAE8) as *const u32);
220
 
221
  let src_d4 = rb(va::G_CONFIG_DWORDS_F3D4) as *const u32;
222
- *((gi + 0xF3D4) as *mut u32) = *src_d4;
223
- *((gi + 0xF3D8) as *mut u32) = *src_d4.add(1);
224
 
225
- // EnergyBar
226
- let energy = read_profile_int("Options", "EnergyBar", 1);
227
- *((gi + 0xF3A2) as *mut u8) = energy as u8;
228
 
229
- // 3 DWORDs from globals → +0xF3C4..+0xF3CC
230
  let src_c4 = rb(va::G_CONFIG_DWORDS_F3C4) as *const u32;
231
- for i in 0u32..3 {
232
- let offset = 0xF3C4 + i * 4;
233
- *((gi + offset) as *mut u32) = *src_c4.add(i as usize);
234
  }
235
 
236
- // Remaining registry values
237
- let info_trans = read_profile_int("Options", "InfoTransparency", 0);
238
- *((gi + 0xF3A3) as *mut u8) = info_trans as u8;
239
-
240
- let info_spy = read_profile_int("Options", "InfoSpy", 1);
241
- *((gi + 0xF3A4) as *mut u8) = if info_spy != 0 { 1 } else { 0 };
242
-
243
- let chat_pinned = read_profile_int("Options", "ChatPinned", 0);
244
- *((gi + 0xF3A5) as *mut u8) = chat_pinned as u8;
245
-
246
- let chat_lines = read_profile_int("Options", "ChatLines", 0);
247
- *((gi + 0xF3A8) as *mut u32) = chat_lines;
248
-
249
- let pinned_lines = read_profile_int("Options", "PinnedChatLines", 0xFFFFFFFF);
250
- *((gi + 0xF3AC) as *mut u32) = pinned_lines;
251
-
252
- let home_lock = read_profile_int("Options", "HomeLock", 0);
253
- *((gi + 0xF3B0) as *mut u8) = home_lock as u8;
254
 
255
  // BackgroundDebrisParallax: clamp to i16 range, then << 16
256
  let mut parallax = read_profile_int("Options", "BackgroundDebrisParallax", 0x50);
@@ -262,13 +239,10 @@ unsafe extern "stdcall" fn hook_load_options(gi: u32) {
262
  parallax = 0x7FFF;
263
  }
264
  }
265
- *((gi + 0xF3E8) as *mut u32) = parallax << 16;
266
-
267
- let onomatopoeia = read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0);
268
- *((gi + 0xF3EC) as *mut u32) = onomatopoeia;
269
 
270
- let capture_png = read_profile_int("Options", "CaptureTransparentPNGs", 0);
271
- *((gi + 0xF3DC) as *mut u32) = capture_png;
272
 
273
  // CameraUnlockMouseSpeed: clamp to max 0xB504, then square
274
  let mut mouse_speed = read_profile_int("Options", "CameraUnlockMouseSpeed", 0x10);
@@ -279,10 +253,9 @@ unsafe extern "stdcall" fn hook_load_options(gi: u32) {
279
  mouse_speed = 0xB504;
280
  }
281
  }
282
- *((gi + 0xF3E0) as *mut u32) = mouse_speed * mouse_speed;
283
 
284
- // Final global DWORD
285
- *((gi + 0xF3E4) as *mut u32) = *(rb(va::G_CONFIG_DWORD_F3E4) as *const u32);
286
 
287
  let _ = log_line("[Config] LoadOptions completed (Rust)");
288
  }
 
134
  ///
135
  /// Reads game options from the Windows registry and copies various globals
136
  /// into the GameInfo struct at known offsets.
137
+ unsafe extern "stdcall" fn hook_load_options(gi_ptr: u32) {
138
  use openwa_lib::wa::registry::read_profile_int;
139
+ use openwa_types::game_info::GameInfo;
140
+
141
+ let gi = &mut *(gi_ptr as *mut GameInfo);
142
 
143
  let _ = log_line("[Config] LoadOptions: loading game options from registry");
144
 
145
  // Format speech path: "%s\\user\\speech"
146
  let base_dir = rb(va::G_BASE_DIR) as *const u8;
 
147
  let base_str = std::ffi::CStr::from_ptr(base_dir as *const i8);
148
  let speech_path = format!("{}\\user\\speech\0", base_str.to_string_lossy());
149
+ gi.speech_path[..speech_path.len()].copy_from_slice(speech_path.as_bytes());
 
 
 
 
150
 
151
+ // Copy 64 bytes from global 0x88DFF3
152
  core::ptr::copy_nonoverlapping(
153
  rb(va::G_GAMEINFO_BLOCK_F485) as *const u8,
154
+ gi._config_block_f485.as_mut_ptr(),
155
  64,
156
  );
157
 
158
+ // Format streams directory and randomize stream indices (global, not GameInfo)
159
  let streams_dest = rb(va::G_STREAMS_DIR) as *mut u8;
160
  let streams_path = format!("{}\\streams\0", base_str.to_string_lossy());
161
  core::ptr::copy_nonoverlapping(
 
177
  // Stream volume: 0x10 if flag set, else 0
178
  let stream_vol_addr = rb(va::G_STREAM_INDICES_END) as *mut u8;
179
  *stream_vol_addr = if *(rb(va::G_STREAM_FLAG) as *const u32) != 0 { 0x10 } else { 0 };
 
180
  *(rb(va::G_STREAM_VOLUME) as *mut u8) = 0x4B;
181
 
182
+ // Copy "data\land.dat" string (14 bytes)
183
  core::ptr::copy_nonoverlapping(
184
  rb(va::G_LAND_DAT_STRING) as *const u8,
185
+ gi.land_dat_path.as_mut_ptr(),
186
  14,
187
  );
188
 
189
+ // Copy byte from global
190
+ gi._config_byte_f3a0 = *(rb(va::G_CONFIG_BYTE_F3A0) as *const u8);
191
 
192
+ // Registry values from "Options" section
193
+ gi.detail_level = read_profile_int("Options", "DetailLevel", 5) as u8;
194
+ gi._zeroed_f3f0 = 0;
195
 
196
+ // Copy 5 DWORDs from globals (indices 0..5 of config block)
 
 
 
197
  let src = rb(va::G_CONFIG_DWORDS_F3B4) as *const u32;
198
+ for i in 0..5 {
199
+ gi._config_block_f3b4[i] = *src.add(i);
 
200
  }
201
 
202
  // Conditional copy: 4 DWORDs if guard == 0
203
  if *(rb(va::G_CONFIG_GUARD) as *const u32) == 0 {
204
  let src = rb(va::G_CONFIG_DWORDS_F3F4) as *const u32;
205
+ for i in 0..4 {
206
+ gi._conditional_config_f3f4[i] = *src.add(i);
 
207
  }
208
  }
209
 
210
  // Single DWORDs from globals
211
+ gi._config_dword_dae8 = *(rb(va::G_CONFIG_DWORD_DAE8) as *const u32);
212
 
213
  let src_d4 = rb(va::G_CONFIG_DWORDS_F3D4) as *const u32;
214
+ gi._config_dword_f3d4 = *src_d4;
215
+ gi._config_dword_f3d8 = *src_d4.add(1);
216
 
217
+ gi.energy_bar = read_profile_int("Options", "EnergyBar", 1) as u8;
 
 
218
 
219
+ // 3 DWORDs from globals (overwrite indices 4..7 of config block)
220
  let src_c4 = rb(va::G_CONFIG_DWORDS_F3C4) as *const u32;
221
+ for i in 0..3 {
222
+ gi._config_block_f3b4[4 + i] = *src_c4.add(i);
 
223
  }
224
 
225
+ gi.info_transparency = read_profile_int("Options", "InfoTransparency", 0) as u8;
226
+ gi.info_spy = if read_profile_int("Options", "InfoSpy", 1) != 0 { 1 } else { 0 };
227
+ gi.chat_pinned = read_profile_int("Options", "ChatPinned", 0) as u8;
228
+ gi.chat_lines = read_profile_int("Options", "ChatLines", 0);
229
+ gi.pinned_chat_lines = read_profile_int("Options", "PinnedChatLines", 0xFFFFFFFF);
230
+ gi.home_lock = read_profile_int("Options", "HomeLock", 0) as u8;
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
  // BackgroundDebrisParallax: clamp to i16 range, then << 16
233
  let mut parallax = read_profile_int("Options", "BackgroundDebrisParallax", 0x50);
 
239
  parallax = 0x7FFF;
240
  }
241
  }
242
+ gi.background_debris_parallax = parallax << 16;
 
 
 
243
 
244
+ gi.topmost_explosion_onomatopoeia = read_profile_int("Options", "TopmostExplosionOnomatopoeia", 0);
245
+ gi.capture_transparent_pngs = read_profile_int("Options", "CaptureTransparentPNGs", 0);
246
 
247
  // CameraUnlockMouseSpeed: clamp to max 0xB504, then square
248
  let mut mouse_speed = read_profile_int("Options", "CameraUnlockMouseSpeed", 0x10);
 
253
  mouse_speed = 0xB504;
254
  }
255
  }
256
+ gi.camera_unlock_mouse_speed = mouse_speed * mouse_speed;
257
 
258
+ gi._config_dword_f3e4 = *(rb(va::G_CONFIG_DWORD_F3E4) as *const u32);
 
259
 
260
  let _ = log_line("[Config] LoadOptions completed (Rust)");
261
  }