use crate::interfaces::Cvar;
use std::marker::PhantomData;
use std::rc::Rc;
#[must_use = "keep the patch guard until the plugin restores normal time scaling"]
pub struct HostTimescalePatch {
#[cfg(target_os = "windows")]
patch: windows::Patch,
_not_thread_safe: PhantomData<Rc<()>>,
}
impl HostTimescalePatch {
pub unsafe fn locate(cvar: Cvar<'_>) -> Result<Self, PatchError> {
#[cfg(target_os = "windows")]
let patch = {
unsafe { windows::Patch::locate(cvar) }?
};
#[cfg(not(target_os = "windows"))]
let _ = cvar;
Ok(Self {
#[cfg(target_os = "windows")]
patch,
_not_thread_safe: PhantomData,
})
}
pub unsafe fn enable(&mut self) -> Result<(), PatchError> {
#[cfg(target_os = "windows")]
{
unsafe { self.patch.enable() }
}
#[cfg(not(target_os = "windows"))]
Ok(())
}
pub fn is_active(&self) -> bool {
#[cfg(target_os = "windows")]
return self.patch.active;
#[cfg(not(target_os = "windows"))]
false
}
pub unsafe fn restore(&mut self) -> Result<(), PatchError> {
#[cfg(target_os = "windows")]
{
unsafe { self.patch.restore() }
}
#[cfg(not(target_os = "windows"))]
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum PatchError {
#[error("the engine does not register `{0}`")]
MissingVariable(&'static str),
#[error("engine.dll has an unsupported or inaccessible PE image")]
InvalidImage,
#[error("engine.dll does not contain the verified TF2 time-scale gate")]
UnsupportedEngine,
#[error("engine.dll contains more than one verified TF2 time-scale gate")]
AmbiguousGate,
#[error("another component changed the time-scale gate's instruction")]
InstructionChanged,
#[error("{operation} failed: {source}")]
Windows {
operation: &'static str,
#[source]
source: std::io::Error,
},
}
#[cfg(any(target_os = "windows", test))]
mod scan {
use super::PatchError;
use crate::sigscan;
use std::ops::Range;
pub(super) const ORIGINAL: u8 = 0x75;
pub(super) const PATCH_OFFSET: usize = 28;
const PATTERN: [Option<u8>; 63] = [
Some(0x48),
Some(0x8b),
Some(0x0d),
None,
None,
None,
None,
Some(0x0f),
Some(0x2f),
Some(0x71),
Some(0x54),
Some(0x0f),
Some(0x83),
None,
None,
None,
None,
Some(0x48),
Some(0x8b),
Some(0x05),
None,
None,
None,
None,
Some(0x83),
Some(0x78),
Some(0x58),
Some(0x00),
Some(ORIGINAL),
Some(0x1c),
Some(0x48),
Some(0x8b),
Some(0x0d),
None,
None,
None,
None,
Some(0x48),
Some(0x8b),
Some(0x01),
Some(0xff),
Some(0x50),
Some(0x30),
Some(0x84),
Some(0xc0),
Some(0x0f),
Some(0x84),
None,
None,
None,
None,
Some(0x48),
Some(0x8b),
Some(0x0d),
None,
None,
None,
None,
Some(0xf3),
Some(0x0f),
Some(0x10),
Some(0x71),
Some(0x54),
];
pub(super) const REPLACEMENT: u8 = 0xeb;
pub(super) struct CodeSection {
pub offset: usize,
pub bytes: Vec<u8>,
}
pub(super) fn find_gate(
base: usize,
code: &[CodeSection],
data: &[Range<usize>],
timescale_parent: usize,
cheats_parent: usize,
) -> Result<usize, PatchError> {
let mut found = None;
for section in code {
for index in sigscan::find_all(§ion.bytes, &PATTERN) {
let bytes = §ion.bytes[index..index + PATTERN.len()];
let Some(offset) = section.offset.checked_add(index) else {
continue;
};
let Some(address) = base.checked_add(offset) else {
continue;
};
let Some(reject) = address.checked_add(198) else {
continue;
};
let Some(section_start) = base.checked_add(section.offset) else {
continue;
};
let Some(section_end) = section_start.checked_add(section.bytes.len()) else {
continue;
};
if !(section_start..section_end).contains(&reject)
|| relative_target(address, 7, bytes, 3) != Some(timescale_parent)
|| relative_target(address, 24, bytes, 20) != Some(cheats_parent)
|| relative_target(address, 58, bytes, 54) != Some(timescale_parent)
|| relative_target(address, 17, bytes, 13) != Some(reject)
|| relative_target(address, 51, bytes, 47) != Some(reject)
{
continue;
}
let Some(demo) = relative_target(address, 37, bytes, 33) else {
continue;
};
let Some(demo_end) = demo.checked_add(size_of::<usize>()) else {
continue;
};
if !data
.iter()
.any(|range| range.start <= demo && demo_end <= range.end)
{
continue;
}
if found.replace(offset + PATCH_OFFSET).is_some() {
return Err(PatchError::AmbiguousGate);
}
}
}
found.ok_or(PatchError::UnsupportedEngine)
}
fn relative_target(
address: usize,
instruction_end: usize,
bytes: &[u8],
displacement: usize,
) -> Option<usize> {
let value = i32::from_le_bytes(bytes.get(displacement..displacement + 4)?.try_into().ok()?);
address
.checked_add(instruction_end)?
.checked_add_signed(value as isize)
}
#[cfg(test)]
mod tests {
use super::*;
const BASE: usize = 0x180000000;
const CHEATS: usize = BASE + 0x2100;
const DEMO: usize = BASE + 0x2200;
const TIMESCALE: usize = BASE + 0x2000;
#[test]
fn candidates_at_nonzero_section_offsets_are_found() {
let mut code = CodeSection {
offset: 0x100,
bytes: vec![0x90; 0x80],
};
code.bytes.extend(section(0x180).bytes);
assert_eq!(find(&[code]).unwrap(), 0x180 + PATCH_OFFSET);
}
#[test]
fn every_candidate_in_a_section_is_validated() {
let mut code = section(0x100);
code.bytes.extend(section(0x200).bytes);
assert!(matches!(
find(std::slice::from_ref(&code)),
Err(PatchError::AmbiguousGate)
));
put_relative(&mut code.bytes, 3, 7, CHEATS, BASE + code.offset);
assert_eq!(find(&[code]).unwrap(), 0x200 + PATCH_OFFSET);
}
fn find(code: &[CodeSection]) -> Result<usize, PatchError> {
find_gate(
BASE,
code,
&[BASE + 0x2000..BASE + 0x3000],
TIMESCALE,
CHEATS,
)
}
fn put_relative(bytes: &mut [u8], at: usize, end: usize, target: usize, address: usize) {
let delta = i32::try_from(target as i128 - (address + end) as i128).unwrap();
bytes[at..at + 4].copy_from_slice(&delta.to_le_bytes());
}
#[test]
fn rejection_branches_and_replacement_destination_are_checked() {
for (at, end) in [(13, 17), (47, 51)] {
let mut code = section(0x100);
put_relative(
&mut code.bytes,
at,
end,
BASE + code.offset + 199,
BASE + code.offset,
);
assert!(matches!(find(&[code]), Err(PatchError::UnsupportedEngine)));
}
let mut code = section(0x100);
code.bytes[PATCH_OFFSET + 1] = 0x1d;
assert!(matches!(find(&[code]), Err(PatchError::UnsupportedEngine)));
}
#[test]
fn relocation_targets_must_be_the_registered_variables() {
for (at, end, wrong) in [
(3, 7, CHEATS),
(20, 24, TIMESCALE),
(54, 58, CHEATS),
(33, 37, BASE + 0x100),
] {
let mut code = section(0x100);
put_relative(&mut code.bytes, at, end, wrong, BASE + code.offset);
assert!(matches!(find(&[code]), Err(PatchError::UnsupportedEngine)));
}
}
fn section(offset: usize) -> CodeSection {
let address = BASE + offset;
let mut bytes = vec![0x90; 256];
for (out, input) in bytes.iter_mut().zip(PATTERN) {
*out = input.unwrap_or_default();
}
put_relative(&mut bytes, 3, 7, TIMESCALE, address);
put_relative(&mut bytes, 20, 24, CHEATS, address);
put_relative(&mut bytes, 54, 58, TIMESCALE, address);
put_relative(&mut bytes, 33, 37, DEMO, address);
put_relative(&mut bytes, 13, 17, address + 198, address);
put_relative(&mut bytes, 47, 51, address + 198, address);
CodeSection { offset, bytes }
}
#[test]
fn selects_only_the_verified_gate() {
assert_eq!(find(&[section(0x100)]).unwrap(), 0x100 + PATCH_OFFSET);
assert!(matches!(
find(&[section(0x100), section(0x500)]),
Err(PatchError::AmbiguousGate)
));
}
#[test]
fn truncated_or_already_patched_code_is_refused() {
let mut code = section(0x100);
code.bytes.truncate(63);
assert!(matches!(find(&[code]), Err(PatchError::UnsupportedEngine)));
let mut code = section(0x100);
code.bytes[PATCH_OFFSET] = REPLACEMENT;
assert!(matches!(find(&[code]), Err(PatchError::UnsupportedEngine)));
}
}
}
#[cfg(target_os = "windows")]
mod windows {
use super::{PatchError, scan};
use crate::interfaces::Cvar;
use std::ffi::c_void;
use std::mem::{MaybeUninit, offset_of};
use std::ops::Range;
use std::ptr::{self, NonNull};
const _: () = assert!(offset_of!(MemoryInformation, region_size) == 24);
const _: () = assert!(offset_of!(sys::ConVar, m_fValue) == 0x54);
const _: () = assert!(offset_of!(sys::ConVar, m_nValue) == 0x58);
const _: () = assert!(offset_of!(sys::ConVar, m_pParent) == 56);
const _: () = assert!(size_of::<MemoryInformation>() == 48);
const IMAGE_SCN_MEM_EXECUTE: u32 = 0x20000000;
const IMAGE_SCN_MEM_READ: u32 = 0x40000000;
const MEM_COMMIT: u32 = 0x1000;
const MEM_IMAGE: u32 = 0x1000000;
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
#[repr(C)]
struct MemoryInformation {
base: *mut c_void,
allocation_base: *mut c_void,
allocation_protection: u32,
partition_id: u16,
region_size: usize,
state: u32,
protection: u32,
kind: u32,
}
struct Module(NonNull<c_void>);
impl Module {
fn engine() -> Result<Self, PatchError> {
const NAME: [u16; 11] = [101, 110, 103, 105, 110, 101, 46, 100, 108, 108, 0];
let mut module = ptr::null_mut();
if unsafe { GetModuleHandleExW(0, NAME.as_ptr(), &mut module) } == 0 {
return Err(os_error("GetModuleHandleExW(engine.dll)"));
}
NonNull::new(module)
.map(Self)
.ok_or(PatchError::InvalidImage)
}
fn base(&self) -> usize {
self.0.as_ptr() as usize
}
fn copy(&self, offset: usize, len: usize) -> Result<Vec<u8>, PatchError> {
self.readable(offset, len)?;
let mut bytes = vec![0; len];
unsafe {
ptr::copy_nonoverlapping(
(self.base() + offset) as *const u8,
bytes.as_mut_ptr(),
len,
)
};
Ok(bytes)
}
fn readable(&self, offset: usize, len: usize) -> Result<(), PatchError> {
let mut address = self
.base()
.checked_add(offset)
.ok_or(PatchError::InvalidImage)?;
let end = address.checked_add(len).ok_or(PatchError::InvalidImage)?;
while address < end {
let mut information = MaybeUninit::uninit();
if unsafe {
VirtualQuery(
address as *const c_void,
information.as_mut_ptr(),
size_of::<MemoryInformation>(),
)
} != size_of::<MemoryInformation>()
{
return Err(PatchError::InvalidImage);
}
let information = unsafe { information.assume_init() };
if information.allocation_base != self.0.as_ptr()
|| information.state != MEM_COMMIT
|| information.kind != MEM_IMAGE
|| information.protection & 0x101 != 0
|| information.protection & 0xee == 0
{
return Err(PatchError::InvalidImage);
}
let next = (information.base as usize)
.checked_add(information.region_size)
.ok_or(PatchError::InvalidImage)?;
if next <= address {
return Err(PatchError::InvalidImage);
}
address = next.min(end);
}
Ok(())
}
fn sections(&self) -> Result<(Vec<scan::CodeSection>, Vec<Range<usize>>), PatchError> {
let dos = self.copy(0, 64)?;
if dos[..2] != *b"MZ" {
return Err(PatchError::InvalidImage);
}
let pe = usize::try_from(i32::from_le_bytes(dos[60..64].try_into().unwrap()))
.map_err(|_| PatchError::InvalidImage)?;
if !(64..=0x100000).contains(&pe) {
return Err(PatchError::InvalidImage);
}
let coff = self.copy(pe, 24)?;
if coff[..4] != *b"PE\0\0"
|| u16::from_le_bytes(coff[4..6].try_into().unwrap()) != 0x8664
{
return Err(PatchError::InvalidImage);
}
let count = u16::from_le_bytes(coff[6..8].try_into().unwrap()) as usize;
let optional_size = u16::from_le_bytes(coff[20..22].try_into().unwrap()) as usize;
if !(1..=96).contains(&count) || !(112..=4096).contains(&optional_size) {
return Err(PatchError::InvalidImage);
}
let optional = self.copy(pe + 24, optional_size)?;
if u16::from_le_bytes(optional[..2].try_into().unwrap()) != 0x20b {
return Err(PatchError::InvalidImage);
}
let image_size = u32::from_le_bytes(optional[56..60].try_into().unwrap()) as usize;
let table = pe + 24 + optional_size;
if !(table + count * 40..=0x40000000).contains(&image_size) {
return Err(PatchError::InvalidImage);
}
let sections = self.copy(table, count * 40)?;
let mut code = Vec::new();
let mut data = Vec::new();
for section in sections.chunks_exact(40) {
let size = u32::from_le_bytes(section[8..12].try_into().unwrap()) as usize;
let offset = u32::from_le_bytes(section[12..16].try_into().unwrap()) as usize;
let flags = u32::from_le_bytes(section[36..40].try_into().unwrap());
let end = offset.checked_add(size).ok_or(PatchError::InvalidImage)?;
if offset > image_size || end > image_size {
return Err(PatchError::InvalidImage);
}
if size == 0 || flags & IMAGE_SCN_MEM_READ == 0 {
continue;
}
if flags & IMAGE_SCN_MEM_EXECUTE != 0 {
code.push(scan::CodeSection {
offset,
bytes: self.copy(offset, size)?,
});
} else {
self.readable(offset, size)?;
data.push(self.base() + offset..self.base() + end);
}
}
Ok((code, data))
}
}
impl Drop for Module {
fn drop(&mut self) {
unsafe { FreeLibrary(self.0.as_ptr()) };
}
}
pub(super) struct Patch {
_module: Module,
location: NonNull<u8>,
original_block: [u8; 63],
pub active: bool,
cache_dirty: bool,
protection: Option<u32>,
}
impl Patch {
pub(super) unsafe fn locate(cvar: Cvar<'_>) -> Result<Self, PatchError> {
let timescale = cvar
.find_var(c"host_timescale")
.ok_or(PatchError::MissingVariable("host_timescale"))?;
let cheats = cvar
.find_var(c"sv_cheats")
.ok_or(PatchError::MissingVariable("sv_cheats"))?;
let module = Module::engine()?;
let (code, data) = module.sections()?;
let timescale_parent = (timescale.as_ptr() as usize)
.checked_add(offset_of!(sys::ConVar, m_pParent))
.ok_or(PatchError::InvalidImage)?;
let cheats_parent = (cheats.as_ptr() as usize)
.checked_add(offset_of!(sys::ConVar, m_pParent))
.ok_or(PatchError::InvalidImage)?;
if ![timescale_parent, cheats_parent].iter().all(|address| {
data.iter().any(|range| {
range.start <= *address
&& address.checked_add(8).is_some_and(|end| end <= range.end)
})
}) {
return Err(PatchError::UnsupportedEngine);
}
let offset =
scan::find_gate(module.base(), &code, &data, timescale_parent, cheats_parent)?;
let location = NonNull::new((module.base() + offset) as *mut u8)
.ok_or(PatchError::InvalidImage)?;
let original_block = module
.copy(offset - scan::PATCH_OFFSET, 63)?
.try_into()
.map_err(|_| PatchError::InvalidImage)?;
Ok(Self {
_module: module,
location,
original_block,
active: false,
cache_dirty: false,
protection: None,
})
}
pub(super) unsafe fn enable(&mut self) -> Result<(), PatchError> {
if !unsafe {
self.matches_block(if self.active {
scan::REPLACEMENT
} else {
scan::ORIGINAL
})
} {
return Err(PatchError::InstructionChanged);
}
if self.active {
self.finish_write()
} else {
unsafe { self.replace(scan::ORIGINAL, scan::REPLACEMENT, true) }
}
}
fn finish_write(&mut self) -> Result<(), PatchError> {
let cache_error = if self.cache_dirty {
if unsafe {
FlushInstructionCache(GetCurrentProcess(), self.location.as_ptr().cast(), 1)
} == 0
{
Some(os_error("FlushInstructionCache(time-scale gate)"))
} else {
self.cache_dirty = false;
None
}
} else {
None
};
let protection_error = if let Some(protection) = self.protection {
let mut old = 0;
if unsafe { VirtualProtect(self.location.as_ptr().cast(), 1, protection, &mut old) }
== 0
{
Some(os_error(
"VirtualProtect(time-scale gate, original protection)",
))
} else {
self.protection = None;
None
}
} else {
None
};
match cache_error.or(protection_error) {
Some(error) => Err(error),
None => Ok(()),
}
}
unsafe fn matches_block(&self, opcode: u8) -> bool {
self.original_block
.iter()
.enumerate()
.all(|(index, expected)| {
let expected = if index == scan::PATCH_OFFSET {
opcode
} else {
*expected
};
(unsafe {
self.location
.as_ptr()
.sub(scan::PATCH_OFFSET)
.add(index)
.read_volatile()
}) == expected
})
}
unsafe fn replace(
&mut self,
expected: u8,
value: u8,
active: bool,
) -> Result<(), PatchError> {
if unsafe { self.location.as_ptr().read_volatile() } != expected {
return Err(PatchError::InstructionChanged);
}
let mut old = 0;
if unsafe {
VirtualProtect(
self.location.as_ptr().cast(),
1,
PAGE_EXECUTE_READWRITE,
&mut old,
)
} == 0
{
return Err(os_error("VirtualProtect(time-scale gate, writable)"));
}
self.protection.get_or_insert(old);
unsafe { self.location.as_ptr().write_volatile(value) };
self.active = active;
self.cache_dirty = true;
self.finish_write()
}
pub(super) unsafe fn restore(&mut self) -> Result<(), PatchError> {
if self.active {
if unsafe { self.location.as_ptr().read_volatile() } == scan::ORIGINAL {
self.active = false;
return self.finish_write();
}
if !unsafe { self.matches_block(scan::REPLACEMENT) } {
return Err(PatchError::InstructionChanged);
}
unsafe { self.replace(scan::REPLACEMENT, scan::ORIGINAL, false) }
} else {
self.finish_write()
}
}
}
impl Drop for Patch {
fn drop(&mut self) {
let _ = unsafe { self.restore() };
}
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn FlushInstructionCache(process: *mut c_void, address: *const c_void, size: usize) -> i32;
fn FreeLibrary(module: *mut c_void) -> i32;
fn GetCurrentProcess() -> *mut c_void;
fn GetModuleHandleExW(flags: u32, name: *const u16, module: *mut *mut c_void) -> i32;
fn VirtualProtect(
address: *const c_void,
size: usize,
protection: u32,
old: *mut u32,
) -> i32;
fn VirtualQuery(
address: *const c_void,
information: *mut MemoryInformation,
length: usize,
) -> usize;
}
fn os_error(operation: &'static str) -> PatchError {
PatchError::Windows {
operation,
source: std::io::Error::last_os_error(),
}
}
}