diff --git a/Cargo.lock b/Cargo.lock index 1bffa3b..e60618a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,6 +393,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" name = "zipfixup" version = "0.1.2" dependencies = [ + "cc", "region", "retour", "time", diff --git a/README.md b/README.md index 376b311..006d6cf 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,7 @@ Obviously, this is an unofficial fan effort and not connected to the developers, ## Installation -1. Download [the latest release Zip file](https://github.com/TerranMechworks/ZipperFixup/releases), for example ` -ZipperFixup-0.1.2.zip`. +1. Download [the latest release Zip file](https://github.com/TerranMechworks/ZipperFixup/releases), for example `ZipperFixup-0.1.2.zip`. 2. Extract the Zip file. 3. Follow instructions of `Readme.txt` in Zip file. diff --git a/crates/zipfixup/Cargo.toml b/crates/zipfixup/Cargo.toml index cdbcfbf..07fe445 100644 --- a/crates/zipfixup/Cargo.toml +++ b/crates/zipfixup/Cargo.toml @@ -24,3 +24,6 @@ time = "0.3" version = "0.3.9" default-features = false features = ["debugapi", "libloaderapi"] + +[build-dependencies] +cc = "1.2" diff --git a/crates/zipfixup/build.rs b/crates/zipfixup/build.rs index d02acc3..91e56b0 100644 --- a/crates/zipfixup/build.rs +++ b/crates/zipfixup/build.rs @@ -19,4 +19,6 @@ fn main() { println!("cargo::rustc-link-arg-cdylib=/DEF:{path}"); } } + + cc::Build::new().file("src/err_msg.c").compile("err_msg"); } diff --git a/crates/zipfixup/src/dbg.rs b/crates/zipfixup/src/dbg.rs index d1d4d8b..72d6089 100644 --- a/crates/zipfixup/src/dbg.rs +++ b/crates/zipfixup/src/dbg.rs @@ -1,5 +1,19 @@ +//! [`output!`] macro to log messages via the Debug API, to be viewed in e.g. +//! [DebugView](https://learn.microsoft.com/en-us/sysinternals/downloads/debugview). use ::winapi::um::debugapi::{OutputDebugStringA, OutputDebugStringW}; +macro_rules! output { + ($fmt:literal $(, $args:expr)* $(,)?) => {{ + let msg: String = format!($fmt $(, $args)*); + $crate::dbg::output_debug_string_w(&msg); + }}; + (a $fmt:literal $(, $args:expr)* $(,)?) => {{ + let msg: String = format!($fmt $(, $args)*); + $crate::dbg::output_debug_string_a(&msg); + }}; +} +pub(crate) use output; + /// Output a Unicode debug string. /// /// OutputDebugStringW is... weird/standard Microsoft: @@ -13,18 +27,7 @@ use ::winapi::um::debugapi::{OutputDebugStringA, OutputDebugStringW}; /// Although you shouldn't log a lot of stuff, if you need to, the ASCII /// version may be slightly faster. pub(crate) fn output_debug_string_w(msg: &str) { - let now = time::OffsetDateTime::now_utc(); - let s = format!( - "[ZF {:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z] {msg}\0", - now.year(), - now.month(), - now.day(), - now.hour(), - now.minute(), - now.second(), - now.millisecond(), - ); - + let s = format_log_msg(msg); let v: Vec = s.encode_utf16().collect(); let p: *const u16 = v.as_ptr(); unsafe { OutputDebugStringW(p) }; @@ -48,33 +51,21 @@ fn encode_ascii(s: &str) -> Vec { /// Microsoft Unicode ineptness). #[allow(dead_code, reason = "Use Unicode version by default")] pub(crate) fn output_debug_string_a(msg: &str) { - let now = time::OffsetDateTime::now_utc(); - let s = format!( - "[ZF {:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z] {msg}\0", - now.year(), - now.month(), - now.day(), - now.hour(), - now.minute(), - now.second(), - now.millisecond(), - ); - + let s = format_log_msg(msg); let v: Vec = encode_ascii(&s); let p: *const i8 = v.as_ptr(); unsafe { OutputDebugStringA(p) }; - // paranoia: ensure `s` is valid until after `OutputDebugStringA` - drop(s); + // paranoia: ensure `v` is valid until after `OutputDebugStringA` + drop(v); } -macro_rules! output { - (a $fmt:literal $(, $args:expr)* $(,)?) => {{ - let msg: String = format!($fmt $(, $args)*); - $crate::dbg::output_debug_string_a(&msg); - }}; - ($fmt:literal $(, $args:expr)* $(,)?) => {{ - let msg: String = format!($fmt $(, $args)*); - $crate::dbg::output_debug_string_w(&msg); - }}; +fn format_log_msg(msg: &str) -> String { + let now = time::OffsetDateTime::now_utc(); + format!( + "[ZF {:02}:{:02}:{:02}.{:03}Z] {msg}\0", + now.hour(), + now.minute(), + now.second(), + now.millisecond(), + ) } -pub(crate) use output; diff --git a/crates/zipfixup/src/dll_main.rs b/crates/zipfixup/src/dll_main.rs index 712147e..c2dd5a9 100644 --- a/crates/zipfixup/src/dll_main.rs +++ b/crates/zipfixup/src/dll_main.rs @@ -1,3 +1,5 @@ +//! [`DllMain`] logic, also installs specific hooks/patches based on the +//! executable that loaded the DLL. use crate::{Result, output}; use std::sync::Mutex; use winapi::shared::minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE}; @@ -16,10 +18,11 @@ extern "system" fn DllMain(dll_module: HINSTANCE, call_reason: DWORD, _reserved: // we don't need them, and may help with spawning threads unsafe { DisableThreadLibraryCalls(dll_module) }; // it's unclear what is allowed to be done in DllMain. - // theoretically, even spawning a thread is not allowed: + // theoretically, even spawning a thread is not "recommended": // https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices // https://devblogs.microsoft.com/oldnewthing/20070904-00/?p=25283 - let _ = std::thread::spawn(on_thread_attach); + // however, we don't synchronize on the thread, so it's ok? + let _ = std::thread::spawn(load_fixup); } DLL_PROCESS_DETACH => (), _ => (), @@ -27,16 +30,15 @@ extern "system" fn DllMain(dll_module: HINSTANCE, call_reason: DWORD, _reserved: TRUE } -fn on_thread_attach() { - if let Err(e) = on_thread_attach_inner() { - output!("FATAL ERROR: {:?}", e); - panic!("FATAL ERROR"); +fn load_fixup() { + if let Err(e) = load_fixup_inner() { + output!("FATAL error when loading fixup: {:?}", e); } } static INSTALLED: Mutex = Mutex::new(false); -fn on_thread_attach_inner() -> Result<()> { +fn load_fixup_inner() -> Result<()> { output!("Fixup loaded ({})", VERSION); let mut installed = INSTALLED.lock().unwrap(); @@ -56,9 +58,9 @@ fn on_thread_attach_inner() -> Result<()> { match exe_size { // Mech3 v1.2 - 2384384 => crate::mech3::install_hooks()?, + 2384384 => crate::mech3::install()?, // Recoil - 1254912 | 1868288 => crate::recoil::install_hooks()?, + 1254912 | 1868288 => crate::recoil::install()?, _ => { output!("ERROR: Exe unknown"); } diff --git a/crates/zipfixup/src/err_msg.c b/crates/zipfixup/src/err_msg.c new file mode 100644 index 0000000..4bb91f0 --- /dev/null +++ b/crates/zipfixup/src/err_msg.c @@ -0,0 +1,16 @@ +#include +#include + +// Rust function to further process the buffer +void err_msg_log(int flags, char* path, int line, char* buffer); + +// C function to handle varargs +void err_msg_printf(int flags, char* path, int line, char* format, ...) +{ + char buffer[1020]; + va_list args; + va_start(args, format); + vsnprintf(buffer, 1020, format, args); + err_msg_log(flags, path, line, buffer); + va_end(args); +} diff --git a/crates/zipfixup/src/err_msg.rs b/crates/zipfixup/src/err_msg.rs new file mode 100644 index 0000000..bc1e44c --- /dev/null +++ b/crates/zipfixup/src/err_msg.rs @@ -0,0 +1,19 @@ +use crate::output; +use std::ffi::CStr; + +unsafe extern "C" { + fn err_msg_printf(); +} + +pub(crate) const ERR_MSG_ADDR: *const () = err_msg_printf as *const (); + +#[unsafe(no_mangle)] +extern "C" fn err_msg_log(flags: i32, path: *const i8, line: i32, buffer: *const i8) { + let path = unsafe { CStr::from_ptr(path) }; + let buffer = unsafe { CStr::from_ptr(buffer) }; + + let path = path.to_string_lossy(); + let buffer = buffer.to_string_lossy(); + + output!("{path}:{line} ({flags:#08X})\n{buffer}"); +} diff --git a/crates/zipfixup/src/lib.rs b/crates/zipfixup/src/lib.rs index 8cae322..5f3fb84 100644 --- a/crates/zipfixup/src/lib.rs +++ b/crates/zipfixup/src/lib.rs @@ -1,9 +1,11 @@ #![cfg(windows)] mod dbg; mod dll_main; +mod err_msg; mod hook; mod mech3; mod overrides; +mod patch; mod recoil; pub(crate) use dbg::output; diff --git a/crates/zipfixup/src/mech3.rs b/crates/zipfixup/src/mech3.rs index 0773bdf..7022141 100644 --- a/crates/zipfixup/src/mech3.rs +++ b/crates/zipfixup/src/mech3.rs @@ -11,10 +11,10 @@ const SCREEN_HEIGHT_PX: *const i32 = 0x8007d0 as _; // Hooked functions type DrawLineFn = extern "fastcall" fn(*mut (), i32, i32, i32, i32, i16); -const DRAW_LINE_ADDR: i32 = 0x00563b80; +const DRAW_LINE_ADDR: u32 = 0x00563b80; type DrawDashedLineFn = extern "fastcall" fn(*mut (), i32, i32, i32, i32, i16, i32); -const DRAW_DASHED_LINE_ADDR: i32 = 0x00563c50; +const DRAW_DASHED_LINE_ADDR: u32 = 0x00563c50; // Hooks @@ -23,8 +23,8 @@ decl_hooks! { DRAW_DASHED_LINE_HOOK: DrawDashedLineFn, } -pub(crate) fn install_hooks() -> Result<()> { - output!("Installing hooks... (MW)"); +pub(crate) fn install() -> Result<()> { + output!("Installing... (MW)"); let target: DrawLineFn = unsafe { std::mem::transmute(DRAW_LINE_ADDR) }; hook("DrawLine", target, draw_line, &DRAW_LINE_HOOK)?; @@ -37,7 +37,7 @@ pub(crate) fn install_hooks() -> Result<()> { &DRAW_DASHED_LINE_HOOK, )?; - output!("Installed hooks"); + output!("Installed"); Ok(()) } diff --git a/crates/zipfixup/src/overrides.rs b/crates/zipfixup/src/overrides.rs index de22646..31995cb 100644 --- a/crates/zipfixup/src/overrides.rs +++ b/crates/zipfixup/src/overrides.rs @@ -1,13 +1,25 @@ use std::sync::OnceLock; use std::time::Instant; -use winapi::shared::minwindef::DWORD; static START_TIME: OnceLock = OnceLock::new(); +/// Custom `GetTickCount` implementation for two reasons: +/// 1. The machine's uptime no longer matters, as the time base is when the DLL +/// was loaded. +/// 2. The resolution of [GetTickCount] is 10-16 milliseconds, which is too +/// coarse on modern/fast systems. Currently, Rust's [`Instant`] uses +/// [QueryPerformanceCounter], which has a higher resolution and also is +/// monotonic. +/// +/// Note that because the output is a [`u32`], it can still wrap once the +/// executable has been running for 49.7 days. +/// +/// [GetTickCount]: https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount +/// [QueryPerformanceCounter]: https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter #[unsafe(no_mangle)] #[allow(non_snake_case)] -extern "system" fn GetTickCount() -> DWORD { +extern "system" fn GetTickCount() -> u32 { let start_time = START_TIME.get_or_init(Instant::now); let elapsed = start_time.elapsed().as_millis(); - elapsed as DWORD + elapsed as u32 } diff --git a/crates/zipfixup/src/patch/byte.rs b/crates/zipfixup/src/patch/byte.rs new file mode 100644 index 0000000..f88ea50 --- /dev/null +++ b/crates/zipfixup/src/patch/byte.rs @@ -0,0 +1,28 @@ +use super::PatchError; +use region::Protection; + +/// Patch a single byte. +/// +/// **WARNING**: This is racy and must be run in a critical section. +#[expect(dead_code)] +pub(crate) fn byte(addr: u32, value: u8, expected: u8) -> Result<(), PatchError> { + let byte_addr = addr as *mut u8; + + // Check existing assembly (sanity) + let actual = unsafe { byte_addr.read_unaligned() }; + + if actual != expected { + return Err(PatchError::Byte { + offset: addr, + actual, + expected, + }); + } + + // Unprotect + unsafe { region::protect(byte_addr, 1, Protection::READ_WRITE_EXECUTE) }?; + // Write new value + unsafe { byte_addr.write_unaligned(value) }; + + Ok(()) +} diff --git a/crates/zipfixup/src/patch/call.rs b/crates/zipfixup/src/patch/call.rs new file mode 100644 index 0000000..4899be7 --- /dev/null +++ b/crates/zipfixup/src/patch/call.rs @@ -0,0 +1,54 @@ +use super::PatchError; +use region::Protection; + +fn rel_offset(call_site: u32, func_addr: *const ()) -> u32 { + let addr = func_addr as u32; + let eip = call_site.wrapping_add(5); + addr.wrapping_sub(eip) +} + +/// Patch a `CALL ` instruction. +/// +/// **WARNING**: This is racy and must be run in a critical section. +pub(crate) fn call( + call_site: u32, + func_addr: *const (), + expected_rel: u32, +) -> Result<(), PatchError> { + // Calculate new relative offset + let rel = rel_offset(call_site, func_addr); + + let call_base = call_site as *mut u8; + // Relative offset after CALL/e8 + let call_rel = call_site.wrapping_add(1); + let call_addr = call_rel as *mut u32; + + // Check existing assembly (sanity) + let actual = unsafe { call_base.read_unaligned() }; + + if actual != 0xe8 { + return Err(PatchError::Byte { + offset: call_site, + actual, + expected: 0xe8, + }); + } + + // Check existing relative offset (sanity) + let actual = unsafe { call_addr.read_unaligned() }; + + if actual != expected_rel { + return Err(PatchError::Dword { + offset: call_rel, + actual, + expected: expected_rel, + }); + } + + // Unprotect + unsafe { region::protect(call_addr, 4, Protection::READ_WRITE_EXECUTE) }?; + // Write new relative offset (very likely to be unaligned...) + unsafe { call_addr.write_unaligned(rel) }; + + Ok(()) +} diff --git a/crates/zipfixup/src/patch/error.rs b/crates/zipfixup/src/patch/error.rs new file mode 100644 index 0000000..4dec67b --- /dev/null +++ b/crates/zipfixup/src/patch/error.rs @@ -0,0 +1,50 @@ +use std::fmt; + +#[derive(Debug)] +pub(crate) enum PatchError { + Byte { + offset: u32, + actual: u8, + expected: u8, + }, + Dword { + offset: u32, + actual: u32, + expected: u32, + }, + Region(region::Error), +} + +impl fmt::Display for PatchError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Byte { + offset, + actual, + expected, + } => write!(f, "{:#02x} != {:#02x} at {:#08x}", actual, expected, offset), + Self::Dword { + offset, + actual, + expected, + } => write!(f, "{:#08x} != {:#08x} at {:#08x}", actual, expected, offset), + Self::Region(e) => fmt::Display::fmt(e, f), + } + } +} + +impl std::error::Error for PatchError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Byte { .. } => None, + Self::Dword { .. } => None, + Self::Region(e) => Some(e), + } + } +} + +impl From for PatchError { + fn from(value: region::Error) -> Self { + Self::Region(value) + } +} diff --git a/crates/zipfixup/src/patch/mod.rs b/crates/zipfixup/src/patch/mod.rs new file mode 100644 index 0000000..3f0b499 --- /dev/null +++ b/crates/zipfixup/src/patch/mod.rs @@ -0,0 +1,8 @@ +mod byte; +mod call; +mod error; + +#[expect(unused_imports)] +pub(crate) use byte::byte; +pub(crate) use call::call; +pub(crate) use error::PatchError; diff --git a/crates/zipfixup/src/recoil.rs b/crates/zipfixup/src/recoil.rs index 1a1c534..fb502cc 100644 --- a/crates/zipfixup/src/recoil.rs +++ b/crates/zipfixup/src/recoil.rs @@ -1,8 +1,21 @@ -use crate::{Result, output}; +use crate::err_msg::ERR_MSG_ADDR; +use crate::{Result, output, patch}; -pub(crate) fn install_hooks() -> Result<()> { - output!("Installing hooks... (RC)"); +const ERR_MSG_CALLS: &[(u32, u32)] = &[ + (0x004c2900, u32::from_be(0x7b_25_f4_ff)), // Interp: "No current node to enable cycle textures..." + (0x004c294a, u32::from_be(0x31_25_f4_ff)), // Interp: "ERROR no GFX data for cycled texture (%s)" + (0x004c2b02, u32::from_be(0x79_23_f4_ff)), // Interp: "Node (%s) has no graphics data for cycled texture\n" +]; - output!("Installed hooks"); +pub(crate) fn install() -> Result<()> { + output!("Installing... (RC)"); + + for (call_site, expected_rel) in ERR_MSG_CALLS.iter().copied() { + if let Err(e) = patch::call(call_site, ERR_MSG_ADDR, expected_rel) { + output!("{}", e); + } + } + + output!("Installed"); Ok(()) }