refactor: use typed GameInfo pointer throughout replay parser
Browse filesAdd 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-
|
| 39 |
-
pub _unknown_d947: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 73 |
if mode != 1 {
|
| 74 |
-
let orig: unsafe extern "stdcall" fn(
|
| 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(
|
| 87 |
let artclass_counter = *(rb(va::G_ARTCLASS_COUNTER) as *const i32);
|
| 88 |
if artclass_counter >= 0x34 { return Err(ReplayError::ArtClassLimit); }
|
| 89 |
-
|
| 90 |
-
|
| 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(
|
| 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 |
-
|
| 126 |
-
|
| 127 |
-
|
| 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 |
-
|
| 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 |
-
|
| 145 |
if first_dword >= -4 && first_dword < -2 {
|
| 146 |
-
*(
|
|
|
|
| 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(
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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(
|
| 181 |
wa_free(p2); guard.payload = ptr::null_mut();
|
| 182 |
|
| 183 |
match result {
|
| 184 |
Ok(()) => {
|
| 185 |
-
call_usercall_eax(
|
| 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(
|
| 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(
|
| 198 |
}
|
| 199 |
}
|
| 200 |
}
|
| 201 |
|
| 202 |
-
unsafe fn parse_version1(
|
| 203 |
-
delegate_to_original(
|
| 204 |
}
|
| 205 |
|
| 206 |
-
unsafe fn delegate_to_original(
|
| 207 |
-
let orig: unsafe extern "stdcall" fn(
|
| 208 |
-
let result = orig(
|
| 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 |
-
|
| 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 =
|
| 497 |
-
let process_colors: unsafe extern "stdcall" fn(
|
| 498 |
core::mem::transmute(rb(va::REPLAY_PROCESS_TEAM_COLORS));
|
| 499 |
-
process_colors(
|
| 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 =
|
| 505 |
-
let fun_45d640: unsafe extern "stdcall" fn(
|
| 506 |
core::mem::transmute(rb(va::REPLAY_PROCESS_STATE));
|
| 507 |
-
fun_45d640(
|
| 508 |
|
| 509 |
if map_seed == 0 || map_seed == 0xFFFF {
|
| 510 |
-
call_process_scheme_defaults(
|
| 511 |
}
|
| 512 |
// TODO: else branch β per-team weapon config reads from stream
|
| 513 |
|
| 514 |
-
// ValidateTeamSetup:
|
| 515 |
-
let validate_setup: unsafe extern "stdcall" fn(
|
| 516 |
core::mem::transmute(rb(va::REPLAY_VALIDATE_TEAM_SETUP));
|
| 517 |
-
validate_setup(
|
| 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
|
| 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 |
-
|
| 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(
|
| 595 |
use std::io::Write as IoWrite;
|
| 596 |
-
let
|
| 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 =
|
| 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:
|
| 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=
|
| 824 |
/// Reuses call_usercall_esi.
|
| 825 |
#[inline]
|
| 826 |
-
unsafe fn call_process_scheme_defaults(
|
| 827 |
-
call_usercall_esi(
|
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|