paavohtl Claude Opus 4.6 (1M context) commited on
Commit
5a60ed0
Β·
1 Parent(s): 3f2b67c

refactor: use typed GameInfo pointer throughout replay parser

Browse files

Add replay-related fields to GameInfo struct (replay_map_type,
replay_format_version, replay_active, replay_filename, terrain_flag,
etc.) by splitting unknown padding regions.

Replace all `state: u32` parameters with `*mut GameInfo` / `*const
GameInfo` across replay functions and bridge calls. Raw offset
arithmetic (`s.add(0xDB50)`) replaced with named field access
(`gi.replay_format_version`).

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

crates/openwa-core/src/engine/game_info.rs CHANGED
@@ -35,8 +35,12 @@ pub struct GameInfo {
35
  /// 0xD946: Network config byte 2 (copied to network object+0x29).
36
  pub net_config_2: u8,
37
 
38
- /// 0xD947-0xD9DF: Unknown
39
- pub _unknown_d947: [u8; 0xD9E0 - 0xD947],
 
 
 
 
40
 
41
  /// 0xD9E0: Streaming audio config data (path config passed to streaming audio ctor).
42
  /// Address of this field is passed as a pointer parameter.
@@ -59,8 +63,37 @@ pub struct GameInfo {
59
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
60
  pub land_dat_path: [u8; 14],
61
 
62
- /// 0xDAFA-0xF343: Unknown
63
- pub _unknown_dafa: [u8; 0xF344 - 0xDAFA],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  /// 0xF344: Sound start frame threshold (i32). Sound is suppressed when
66
  /// DDGame.frame_counter < this value. Checked by IsSoundSuppressed and
 
35
  /// 0xD946: Network config byte 2 (copied to network object+0x29).
36
  pub net_config_2: u8,
37
 
38
+ /// 0xD947-0xD98A: Unknown
39
+ pub _unknown_d947: [u8; 0xD98B - 0xD947],
40
+ /// 0xD98B: Terrain flag (set from map object during replay loading).
41
+ pub terrain_flag: u8,
42
+ /// 0xD98C-0xD9DF: Unknown
43
+ pub _unknown_d98c: [u8; 0xD9E0 - 0xD98C],
44
 
45
  /// 0xD9E0: Streaming audio config data (path config passed to streaming audio ctor).
46
  /// Address of this field is passed as a pointer parameter.
 
63
  /// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
64
  pub land_dat_path: [u8; 14],
65
 
66
+ // --- Replay configuration (populated by ReplayLoader) ---
67
+
68
+ /// 0xDAFA-0xDB1B: Unknown
69
+ pub _unknown_dafa: [u8; 0xDB1C - 0xDAFA],
70
+ /// 0xDB1C: Replay map sub-type (first DWORD of first payload).
71
+ /// >= 1: map stored in playback.thm. Negative: inline map data.
72
+ pub replay_map_type: i32,
73
+ /// 0xDB20: Replay payload field 2 (sub-type < 1 path).
74
+ pub replay_payload_2: i32,
75
+ /// 0xDB24: Replay payload extra data (variable-length, up to 0x24 bytes).
76
+ pub replay_payload_extra: [u8; 0x24],
77
+ /// 0xDB48: Replay active flag (set to 1 by ReplayLoader).
78
+ pub replay_active: u8,
79
+ /// 0xDB49-0xDB4F: Unknown
80
+ pub _unknown_db49: [u8; 0xDB50 - 0xDB49],
81
+ /// 0xDB50: Replay file format version.
82
+ pub replay_format_version: u32,
83
+ /// 0xDB54: Replay file format version (duplicate/backup).
84
+ pub replay_format_version_2: u32,
85
+ /// 0xDB58: Replay field (set to 0xFFFFFFFF during loading).
86
+ pub replay_field_db58: u32,
87
+ /// 0xDB5C-0xDB5F: Unknown
88
+ pub _unknown_db5c: [u8; 4],
89
+ /// 0xDB60: Replay filename buffer (C string, null-terminated).
90
+ pub replay_filename: [u8; 0x400],
91
+ /// 0xDF60-0xEF5F: Unknown
92
+ pub _unknown_df60: [u8; 0xEF60 - 0xDF60],
93
+ /// 0xEF60: Cleared to 0 during replay loading.
94
+ pub replay_field_ef60: u32,
95
+ /// 0xEF64-0xF343: Unknown
96
+ pub _unknown_ef64: [u8; 0xF344 - 0xEF64],
97
 
98
  /// 0xF344: Sound start frame threshold (i32). Sound is suppressed when
99
  /// DDGame.frame_counter < this value. Checked by IsSoundSuppressed and
crates/openwa-wormkit/src/replacements/replay.rs CHANGED
@@ -6,6 +6,7 @@
6
  use crate::hook;
7
  use crate::log_line;
8
  use openwa_core::address::va;
 
9
  use openwa_core::engine::replay::{self, ReplayError, ReplayStream, ReplayTeamEntry};
10
  use openwa_core::rebase::rb;
11
  use openwa_core::wa_alloc::{wa_free, wa_malloc};
@@ -69,9 +70,9 @@ impl Drop for PayloadGuard {
69
 
70
  // ─── ReplayLoader hook ──────────────────────────────────────────────────────
71
 
72
- unsafe extern "stdcall" fn hook_replay_loader(state: u32, mode: i32) -> u32 {
73
  if mode != 1 {
74
- let orig: unsafe extern "stdcall" fn(u32, i32) -> u32 =
75
  core::mem::transmute(REPLAY_LOADER_ORIG);
76
  return orig(state, mode);
77
  }
@@ -83,19 +84,16 @@ unsafe extern "stdcall" fn hook_replay_loader(state: u32, mode: i32) -> u32 {
83
 
84
  // ─── Main replay loader (mode 1) ────────────────────────────────────────────
85
 
86
- unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
87
  let artclass_counter = *(rb(va::G_ARTCLASS_COUNTER) as *const i32);
88
  if artclass_counter >= 0x34 { return Err(ReplayError::ArtClassLimit); }
89
-
90
- let s = state as *mut u8;
91
- *s.add(0xDB48) = 1;
92
- *(s.add(0xEF60) as *mut u32) = 0;
93
 
94
  // Open replay file using Rust's std::fs::File
95
  let replay_path = {
96
- // Build path: game data dir + replay filename from state+0xDB60
97
  let data_dir = CStr::from_ptr(rb(va::G_DATA_DIR) as *const i8);
98
- let file_name = CStr::from_ptr(s.add(0xDB60) as *const i8);
99
  let dir = data_dir.to_str().unwrap_or(".");
100
  let name = file_name.to_str().unwrap_or("");
101
  if Path::new(name).is_absolute() {
@@ -122,9 +120,9 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
122
  let version = header >> 16;
123
  if version == 0 || version > 20 { return Err(ReplayError::VersionTooNew); }
124
 
125
- *(s.add(0xDB50) as *mut u32) = version;
126
- *(s.add(0xDB54) as *mut u32) = version;
127
- *(s.add(0xDB58) as *mut u32) = 0xFFFFFFFF;
128
 
129
  // First payload
130
  let payload_size = read_u32(&mut file)?;
@@ -136,19 +134,26 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
136
  file.read_exact(payload_slice).map_err(|_| ReplayError::FileNotFound)?;
137
 
138
  let first_dword = *(payload as *const i32);
139
- *(s.add(0xDB1C) as *mut i32) = first_dword;
140
  if first_dword >= 1 {
141
  // Write payload to playback.thm using Rust File
142
  let _ = std::fs::write("data\\playback.thm", payload_slice);
143
  } else {
144
- *(s.add(0xDB20) as *mut i32) = *(payload.add(4) as *const i32);
145
  if first_dword >= -4 && first_dword < -2 {
146
- *(s.add(0xDB24) as *mut i32) = *(payload.add(8) as *const i32);
 
147
  } else if first_dword == -2 {
148
  let n = payload_size.saturating_sub(8) as usize;
149
  if n > 0x20 { return Err(ReplayError::InvalidFormat); }
150
- ptr::copy_nonoverlapping(payload.add(8), s.add(0xDB24), n);
151
- *s.add(0xDB1C + payload_size as usize) = 0;
 
 
 
 
 
 
152
  }
153
  }
154
  wa_free(payload); guard.payload = ptr::null_mut();
@@ -161,7 +166,7 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
161
 
162
  if version == 1 {
163
  drop(file);
164
- return parse_version1(state);
165
  }
166
 
167
  // ─── Version 2+: read second payload ─────────────────────────────────
@@ -177,16 +182,16 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
177
 
178
  // Parse and write to globals
179
  let data = core::slice::from_raw_parts(p2, second_size as usize);
180
- let result = parse_and_write_v2plus(state, data, version);
181
  wa_free(p2); guard.payload = ptr::null_mut();
182
 
183
  match result {
184
  Ok(()) => {
185
- call_usercall_eax(state, rb(va::REPLAY_PROCESS_FLAGS));
186
  let wa_log_file = *(rb(va::G_LOG_FILE_PTR) as *const *mut FILE);
187
  if !wa_log_file.is_null() {
188
  if let Some(mut log_file) = wa_file_to_rust(wa_log_file) {
189
- write_replay_log(state, &mut *log_file)?;
190
  }
191
  }
192
  let _ = log_line("[Replay] Rust replay loading complete");
@@ -194,18 +199,18 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
194
  }
195
  Err(e) => {
196
  let _ = log_line(&format!("[Replay] Parse failed ({e:?}), falling back to original"));
197
- delegate_to_original(state)
198
  }
199
  }
200
  }
201
 
202
- unsafe fn parse_version1(state: u32) -> Result<(), ReplayError> {
203
- delegate_to_original(state)
204
  }
205
 
206
- unsafe fn delegate_to_original(state: u32) -> Result<(), ReplayError> {
207
- let orig: unsafe extern "stdcall" fn(u32, i32) -> u32 = core::mem::transmute(REPLAY_LOADER_ORIG);
208
- let result = orig(state, 1);
209
  if result == 0 { Ok(()) } else { Err(ReplayError::FileNotFound) }
210
  }
211
 
@@ -219,7 +224,7 @@ unsafe fn wb(addr: u32, val: u8) { *(rb(addr) as *mut u8) = val; }
219
  unsafe fn wd(addr: u32, val: u32) { *(rb(addr) as *mut u32) = val; }
220
 
221
  unsafe fn parse_and_write_v2plus(
222
- state: u32, data: &[u8], version: u32,
223
  ) -> Result<(), ReplayError> {
224
  let mut s = ReplayStream::new(data);
225
 
@@ -493,28 +498,28 @@ unsafe fn parse_and_write_v2plus(
493
 
494
  wb(va::G_TEAM_COUNT, team_count);
495
 
496
- // ProcessTeamColors: stdcall(1 param = state). RET 0x4.
497
- let process_colors: unsafe extern "stdcall" fn(u32) =
498
  core::mem::transmute(rb(va::REPLAY_PROCESS_TEAM_COLORS));
499
- process_colors(rb(va::G_REPLAY_STATE));
500
 
501
  let map_seed = s.read_u16()?;
502
  wd(va::G_MAP_SEED, map_seed as u32);
503
 
504
- // FUN_0045d640: stdcall(1 param = state). 1032-line function.
505
- let fun_45d640: unsafe extern "stdcall" fn(u32) =
506
  core::mem::transmute(rb(va::REPLAY_PROCESS_STATE));
507
- fun_45d640(rb(va::G_REPLAY_STATE));
508
 
509
  if map_seed == 0 || map_seed == 0xFFFF {
510
- call_process_scheme_defaults(rb(va::G_REPLAY_STATE), rb(va::REPLAY_PROCESS_SCHEME_DEFAULTS));
511
  }
512
  // TODO: else branch β€” per-team weapon config reads from stream
513
 
514
- // ValidateTeamSetup: reads [ESP+0xBC] in prologue = stdcall(1 param = state)
515
- let validate_setup: unsafe extern "stdcall" fn(u32) =
516
  core::mem::transmute(rb(va::REPLAY_VALIDATE_TEAM_SETUP));
517
- validate_setup(rb(va::G_REPLAY_STATE));
518
 
519
  // Assembly: MOV ESI,[seed]; PUSH ESI; CALL srand; rand(); SHL<<16; rand(); ADD
520
  let current_seed = *(rb(va::G_RANDOM_SEED) as *const u32);
@@ -536,7 +541,7 @@ unsafe fn parse_and_write_v2plus(
536
  // The original loads it here via FUN_00447e80 + FUN_0044a9a0.
537
  // For positive sub-version (our test case), we need to:
538
  // Map loading: construct map object, load playback.thm, copy info, release.
539
- if *(state as *const i32).byte_add(0xDB1C) >= 1 {
540
  // Alloc: PUSH 0x29628; CALL 005C0AB8; ADD ESP,4 β€” cdecl(size)
541
  let alloc: unsafe extern "cdecl" fn(u32) -> u32 =
542
  core::mem::transmute(rb(va::WA_CRT_MALLOC));
@@ -570,7 +575,7 @@ unsafe fn parse_and_write_v2plus(
570
  call_usercall_esi(map_obj as u32, rb(va::MAP_COPY_INFO));
571
 
572
  // Terrain flag: CMP [ESI+0x29618],0; SETZ
573
- *(state as *mut u8).add(0xD98B) =
574
  (*(map_obj as *const u8).add(0x29618) == 0) as u8;
575
 
576
  // Release: PUSH 1; MOV ECX,ESI; CALL [vtable+4] β€” thiscall(this, 1)
@@ -591,9 +596,9 @@ unsafe fn parse_and_write_v2plus(
591
 
592
 
593
  /// Write the /getlog replay header to the log file.
594
- unsafe fn write_replay_log(state: u32, log_file: &mut File) -> Result<(), ReplayError> {
595
  use std::io::Write as IoWrite;
596
- let s = state as *const u8;
597
 
598
  // ── Date/time line ───────────────────────────────────────────────────
599
  // "Game Started at YYYY-MM-DD HH:MM:SS GMT"
@@ -643,7 +648,7 @@ unsafe fn write_replay_log(state: u32, log_file: &mut File) -> Result<(), Replay
643
  ptr as *const u8
644
  };
645
 
646
- let replay_ver = *(s.add(0xDB50) as *const u32);
647
  let format_ver_str = *(rb(0x6AC624 + replay_ver * 4) as *const u32) as *const u8;
648
 
649
  {
@@ -777,7 +782,7 @@ unsafe fn c_strlen(s: *const u8) -> usize {
777
 
778
  /// Bridge for usercall(EAX=value) + plain call. cdecl(eax_val, func_addr).
779
  #[unsafe(naked)]
780
- unsafe extern "cdecl" fn call_usercall_eax(_eax_val: u32, _func: u32) {
781
  core::arch::naked_asm!(
782
  "mov eax, [esp+4]", // eax_val
783
  "mov ecx, [esp+8]", // func_addr
@@ -820,11 +825,11 @@ unsafe extern "cdecl" fn call_register_observer(
820
  );
821
  }
822
 
823
- /// Bridge for ProcessSchemeDefaults: usercall(ESI=state).
824
  /// Reuses call_usercall_esi.
825
  #[inline]
826
- unsafe fn call_process_scheme_defaults(state: u32, func: u32) {
827
- call_usercall_esi(state, func);
828
  }
829
 
830
  // ─── ParseReplayPosition ─────────────────────────────────────────────────────
 
6
  use crate::hook;
7
  use crate::log_line;
8
  use openwa_core::address::va;
9
+ use openwa_core::engine::game_info::GameInfo;
10
  use openwa_core::engine::replay::{self, ReplayError, ReplayStream, ReplayTeamEntry};
11
  use openwa_core::rebase::rb;
12
  use openwa_core::wa_alloc::{wa_free, wa_malloc};
 
70
 
71
  // ─── ReplayLoader hook ──────────────────────────────────────────────────────
72
 
73
+ unsafe extern "stdcall" fn hook_replay_loader(state: *mut GameInfo, mode: i32) -> u32 {
74
  if mode != 1 {
75
+ let orig: unsafe extern "stdcall" fn(*mut GameInfo, i32) -> u32 =
76
  core::mem::transmute(REPLAY_LOADER_ORIG);
77
  return orig(state, mode);
78
  }
 
84
 
85
  // ─── Main replay loader (mode 1) ────────────────────────────────────────────
86
 
87
+ unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
88
  let artclass_counter = *(rb(va::G_ARTCLASS_COUNTER) as *const i32);
89
  if artclass_counter >= 0x34 { return Err(ReplayError::ArtClassLimit); }
90
+ (*gi).replay_active = 1;
91
+ (*gi).replay_field_ef60 = 0;
 
 
92
 
93
  // Open replay file using Rust's std::fs::File
94
  let replay_path = {
 
95
  let data_dir = CStr::from_ptr(rb(va::G_DATA_DIR) as *const i8);
96
+ let file_name = CStr::from_ptr((*gi).replay_filename.as_ptr() as *const i8);
97
  let dir = data_dir.to_str().unwrap_or(".");
98
  let name = file_name.to_str().unwrap_or("");
99
  if Path::new(name).is_absolute() {
 
120
  let version = header >> 16;
121
  if version == 0 || version > 20 { return Err(ReplayError::VersionTooNew); }
122
 
123
+ (*gi).replay_format_version = version;
124
+ (*gi).replay_format_version_2 = version;
125
+ (*gi).replay_field_db58 = 0xFFFFFFFF;
126
 
127
  // First payload
128
  let payload_size = read_u32(&mut file)?;
 
134
  file.read_exact(payload_slice).map_err(|_| ReplayError::FileNotFound)?;
135
 
136
  let first_dword = *(payload as *const i32);
137
+ (*gi).replay_map_type = first_dword;
138
  if first_dword >= 1 {
139
  // Write payload to playback.thm using Rust File
140
  let _ = std::fs::write("data\\playback.thm", payload_slice);
141
  } else {
142
+ (*gi).replay_payload_2 = *(payload.add(4) as *const i32);
143
  if first_dword >= -4 && first_dword < -2 {
144
+ *((*gi).replay_payload_extra.as_mut_ptr() as *mut i32) =
145
+ *(payload.add(8) as *const i32);
146
  } else if first_dword == -2 {
147
  let n = payload_size.saturating_sub(8) as usize;
148
  if n > 0x20 { return Err(ReplayError::InvalidFormat); }
149
+ ptr::copy_nonoverlapping(
150
+ payload.add(8),
151
+ (*gi).replay_payload_extra.as_mut_ptr(),
152
+ n,
153
+ );
154
+ // Null-terminate: write 0 at replay_map_type + payload_size offset
155
+ // (the original uses base+0xDB1C+payload_size which spans into replay_payload_extra)
156
+ *((gi as *mut u8).add(0xDB1C + payload_size as usize)) = 0;
157
  }
158
  }
159
  wa_free(payload); guard.payload = ptr::null_mut();
 
166
 
167
  if version == 1 {
168
  drop(file);
169
+ return parse_version1(gi);
170
  }
171
 
172
  // ─── Version 2+: read second payload ─────────────────────────────────
 
182
 
183
  // Parse and write to globals
184
  let data = core::slice::from_raw_parts(p2, second_size as usize);
185
+ let result = parse_and_write_v2plus(gi, data, version);
186
  wa_free(p2); guard.payload = ptr::null_mut();
187
 
188
  match result {
189
  Ok(()) => {
190
+ call_usercall_eax(gi, rb(va::REPLAY_PROCESS_FLAGS));
191
  let wa_log_file = *(rb(va::G_LOG_FILE_PTR) as *const *mut FILE);
192
  if !wa_log_file.is_null() {
193
  if let Some(mut log_file) = wa_file_to_rust(wa_log_file) {
194
+ write_replay_log(gi, &mut *log_file)?;
195
  }
196
  }
197
  let _ = log_line("[Replay] Rust replay loading complete");
 
199
  }
200
  Err(e) => {
201
  let _ = log_line(&format!("[Replay] Parse failed ({e:?}), falling back to original"));
202
+ delegate_to_original(gi)
203
  }
204
  }
205
  }
206
 
207
+ unsafe fn parse_version1(gi: *mut GameInfo) -> Result<(), ReplayError> {
208
+ delegate_to_original(gi)
209
  }
210
 
211
+ unsafe fn delegate_to_original(gi: *mut GameInfo) -> Result<(), ReplayError> {
212
+ let orig: unsafe extern "stdcall" fn(*mut GameInfo, i32) -> u32 = core::mem::transmute(REPLAY_LOADER_ORIG);
213
+ let result = orig(gi, 1);
214
  if result == 0 { Ok(()) } else { Err(ReplayError::FileNotFound) }
215
  }
216
 
 
224
  unsafe fn wd(addr: u32, val: u32) { *(rb(addr) as *mut u32) = val; }
225
 
226
  unsafe fn parse_and_write_v2plus(
227
+ gi: *mut GameInfo, data: &[u8], version: u32,
228
  ) -> Result<(), ReplayError> {
229
  let mut s = ReplayStream::new(data);
230
 
 
498
 
499
  wb(va::G_TEAM_COUNT, team_count);
500
 
501
+ // ProcessTeamColors: stdcall(1 param = game_info). RET 0x4.
502
+ let process_colors: unsafe extern "stdcall" fn(*mut GameInfo) =
503
  core::mem::transmute(rb(va::REPLAY_PROCESS_TEAM_COLORS));
504
+ process_colors(gi);
505
 
506
  let map_seed = s.read_u16()?;
507
  wd(va::G_MAP_SEED, map_seed as u32);
508
 
509
+ // FUN_0045d640: stdcall(1 param = game_info). 1032-line function.
510
+ let fun_45d640: unsafe extern "stdcall" fn(*mut GameInfo) =
511
  core::mem::transmute(rb(va::REPLAY_PROCESS_STATE));
512
+ fun_45d640(gi);
513
 
514
  if map_seed == 0 || map_seed == 0xFFFF {
515
+ call_process_scheme_defaults(gi, rb(va::REPLAY_PROCESS_SCHEME_DEFAULTS));
516
  }
517
  // TODO: else branch β€” per-team weapon config reads from stream
518
 
519
+ // ValidateTeamSetup: stdcall(1 param = game_info)
520
+ let validate_setup: unsafe extern "stdcall" fn(*mut GameInfo) =
521
  core::mem::transmute(rb(va::REPLAY_VALIDATE_TEAM_SETUP));
522
+ validate_setup(gi);
523
 
524
  // Assembly: MOV ESI,[seed]; PUSH ESI; CALL srand; rand(); SHL<<16; rand(); ADD
525
  let current_seed = *(rb(va::G_RANDOM_SEED) as *const u32);
 
541
  // The original loads it here via FUN_00447e80 + FUN_0044a9a0.
542
  // For positive sub-version (our test case), we need to:
543
  // Map loading: construct map object, load playback.thm, copy info, release.
544
+ if (*gi).replay_map_type >= 1 {
545
  // Alloc: PUSH 0x29628; CALL 005C0AB8; ADD ESP,4 β€” cdecl(size)
546
  let alloc: unsafe extern "cdecl" fn(u32) -> u32 =
547
  core::mem::transmute(rb(va::WA_CRT_MALLOC));
 
575
  call_usercall_esi(map_obj as u32, rb(va::MAP_COPY_INFO));
576
 
577
  // Terrain flag: CMP [ESI+0x29618],0; SETZ
578
+ (*gi).terrain_flag =
579
  (*(map_obj as *const u8).add(0x29618) == 0) as u8;
580
 
581
  // Release: PUSH 1; MOV ECX,ESI; CALL [vtable+4] β€” thiscall(this, 1)
 
596
 
597
 
598
  /// Write the /getlog replay header to the log file.
599
+ unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(), ReplayError> {
600
  use std::io::Write as IoWrite;
601
+ let gi = &*gi;
602
 
603
  // ── Date/time line ───────────────────────────────────────────────────
604
  // "Game Started at YYYY-MM-DD HH:MM:SS GMT"
 
648
  ptr as *const u8
649
  };
650
 
651
+ let replay_ver = gi.replay_format_version;
652
  let format_ver_str = *(rb(0x6AC624 + replay_ver * 4) as *const u32) as *const u8;
653
 
654
  {
 
782
 
783
  /// Bridge for usercall(EAX=value) + plain call. cdecl(eax_val, func_addr).
784
  #[unsafe(naked)]
785
+ unsafe extern "cdecl" fn call_usercall_eax(_eax_val: *mut GameInfo, _func: u32) {
786
  core::arch::naked_asm!(
787
  "mov eax, [esp+4]", // eax_val
788
  "mov ecx, [esp+8]", // func_addr
 
825
  );
826
  }
827
 
828
+ /// Bridge for ProcessSchemeDefaults: usercall(ESI=game_info).
829
  /// Reuses call_usercall_esi.
830
  #[inline]
831
+ unsafe fn call_process_scheme_defaults(gi: *mut GameInfo, func: u32) {
832
+ call_usercall_esi(gi as u32, func);
833
  }
834
 
835
  // ─── ParseReplayPosition ─────────────────────────────────────────────────────