Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions crates/zipfixup/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ time = "0.3"
version = "0.3.9"
default-features = false
features = ["debugapi", "libloaderapi"]

[build-dependencies]
cc = "1.2"
2 changes: 2 additions & 0 deletions crates/zipfixup/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
63 changes: 27 additions & 36 deletions crates/zipfixup/src/dbg.rs
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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<u16> = s.encode_utf16().collect();
let p: *const u16 = v.as_ptr();
unsafe { OutputDebugStringW(p) };
Expand All @@ -48,33 +51,21 @@ fn encode_ascii(s: &str) -> Vec<i8> {
/// 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<i8> = 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;
20 changes: 11 additions & 9 deletions crates/zipfixup/src/dll_main.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -16,27 +18,27 @@ 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 => (),
_ => (),
}
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<bool> = Mutex::new(false);

fn on_thread_attach_inner() -> Result<()> {
fn load_fixup_inner() -> Result<()> {
output!("Fixup loaded ({})", VERSION);

let mut installed = INSTALLED.lock().unwrap();
Expand All @@ -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");
}
Expand Down
16 changes: 16 additions & 0 deletions crates/zipfixup/src/err_msg.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include <stdarg.h>
#include <stdio.h>

// 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);
}
19 changes: 19 additions & 0 deletions crates/zipfixup/src/err_msg.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
2 changes: 2 additions & 0 deletions crates/zipfixup/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
10 changes: 5 additions & 5 deletions crates/zipfixup/src/mech3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)?;
Expand All @@ -37,7 +37,7 @@ pub(crate) fn install_hooks() -> Result<()> {
&DRAW_DASHED_LINE_HOOK,
)?;

output!("Installed hooks");
output!("Installed");
Ok(())
}

Expand Down
18 changes: 15 additions & 3 deletions crates/zipfixup/src/overrides.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
use std::sync::OnceLock;
use std::time::Instant;
use winapi::shared::minwindef::DWORD;

static START_TIME: OnceLock<Instant> = 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
}
28 changes: 28 additions & 0 deletions crates/zipfixup/src/patch/byte.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
54 changes: 54 additions & 0 deletions crates/zipfixup/src/patch/call.rs
Original file line number Diff line number Diff line change
@@ -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 <rel>` 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 <rel>
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(())
}
Loading
Loading