Render Snapshots and Feature Integration
Combine camera and player observations, graphics features, versioned profiles, and reversible hooks, including two exact-build Urban Terror wallhacks, without mixing frames or losing game state.
Cheatsheet for Lesson 8.8
A one-page summary of “Render Snapshots and Feature Integration” you can keep beside your notes. Plain Markdown.
Lesson 8.8 — Render Snapshots and Feature Integration: cheatsheet
- Remember: Sharing one captured set prevents features from using different copies; it does not prove the game stayed unchanged during capture.
- Also useful: Use one selector for mutually exclusive overlay modes, and separate switches for independent features that can operate together.
Key terms
PlayerSnapshot: In this excerpt,PlayerSnapshotis a local record of the player's identity, position, and other validated fields;Mat4holds the camera transform.Mat4: In this excerpt,PlayerSnapshotis a local record of the player's identity, position, and other validated fields;Mat4holds the camera transform.Vec<PlayerSnapshot>:Vec<PlayerSnapshot>is owned rather than borrowed, so nothing here points back into the game.None: If a profile has no address for a capability, that control must remain unavailable;Noneis not an invitation to reuse an address from another game.ebx: Inside the entity loop,ebxholds the current render entity at0x0052D2FD.- multihack: The complete dispatcher and feature lifecycles bring the separate effects into one DLL, called the multihack in the original course.
Review
- Q: Why should several diagnostic features consume one shared frame snapshot? A: They use the same captured values and avoid duplicate remote reads
- Q: A render hook switches the graphics API to 'always pass the depth test' for certain draw calls. Why must it switch the normal setting back afterwards? A: The setting stays in force until changed, so later draws would use it too
The tool-architecture lesson gave snapshots, commands, features, and cleanup separate owners. Rendering adds a timing constraint: positions and the camera that projects them must describe the same captured update. A well-organized tool can still draw incorrect markers if those values come from different moments.
This lesson applies that architecture to radar, overlays, aim analysis, and render hooks. The snippets describe interfaces inside a combined tool; the complete dispatcher and patch owners are linked below.
One frame, one snapshot
Section titled “One frame, one snapshot”Read and validate target data once per update. Features consume the same owned
snapshot. In this excerpt, PlayerSnapshot is a local record of the player’s
identity, position, and other validated fields; Mat4 holds the camera
transform. Neither type borrows a pointer into the game:
struct FrameSnapshot { local: PlayerSnapshot, players: Vec<PlayerSnapshot>, matrix: Mat4, viewport: (f32, f32),}Every feature receives the same owned set of fields from one bounded observation pass. If the radar reads the player list on one pass while the overlay independently reads the camera matrix a few milliseconds later, the features describe different updates, and markers can land where enemies used to be. Bundling the fields means both features use the same captured values. It does not make the reads atomic: the game can update an entity between two reads within that pass. Validate the resulting snapshot and, if the game exposes a tick or generation counter, check it before and after the copy or retry when it changes.
The camera matrix and viewport travel with the players for that exact reason. Projecting this frame’s positions through last frame’s camera is wrong in a way that looks almost right — everything is drawn slightly behind where it belongs, worst when you turn quickly, which makes it easy to blame the projection maths instead of the timing.
Vec<PlayerSnapshot> is owned rather than borrowed, so nothing here points back
into the game. A feature holding a reference into an entity the game then frees
is the stale-pointer failure from Lesson 1.8, arriving through the back door.
Copy once, then let every feature read from the copy.
This board needs JavaScript to move things around. Here is the lesson’s sorting:
- Travels in the snapshot
- The local player record (PlayerSnapshot). A local record of identity, position and other validated fields. It borrows no pointer into the game.
- The list of players (an owned Vec). Owned rather than borrowed, so nothing points back into the game and every feature reads the same copy.
- The camera matrix from the same update. Positions and the camera that projects them must describe the same update, so the matrix travels with the players.
- The viewport size. It travels with the matrix and players so a feature can project this frame’s positions without asking the game again.
- Keep out of it
- A reference into a game entity. If the game frees the entity, the reference is the stale-pointer failure from Lesson 1.8. Copy once, then read from the copy.
- Last frame’s camera matrix. Projecting this frame’s positions through last frame’s camera draws everything slightly behind where it belongs, worst when you turn quickly.
- A camera matrix the overlay reads on its own, later. If the radar reads players now and the overlay reads the camera a few milliseconds later, the two describe different updates and markers land where enemies used to be.
Give render features a lifecycle
Section titled “Give render features a lifecycle”A radar overlay needs a different per-frame action from a recoil patch, but both must answer the same lifecycle questions: when is it enabled, what does it do with this frame’s snapshot, and how does it undo its own changes? The interface below makes those moments explicit for every feature:
trait Feature { fn name(&self) -> &'static str; fn enable(&mut self) -> anyhow::Result<()>; fn update(&mut self, frame: &FrameSnapshot) -> anyhow::Result<()>; fn disable(&mut self) -> anyhow::Result<()>;}Only patching or hooking features need setup or restoration in enable and
disable. A pure observer still implements those required methods, but they
can return Ok(()); its useful work lives in update. The owner checks the
result before reporting that a toggle succeeded.
Model configuration with enums
Section titled “Model configuration with enums”Three booleans named show_teammates, show_opponents, and
show_everyone could all become true after a sequence of clicks. Yet the
overlay can display only one of those modes at a time. Store that choice as
one enum value, alongside the independent radar and debug switches:
#[derive(Clone, Copy, Debug, Eq, PartialEq)]enum OverlayMode { Off, Teammates, Opponents, Everyone,}
#[derive(Debug)]struct Settings { overlay: OverlayMode, show_radar: bool, show_debug_values: bool,}An enum prevents impossible combinations such as three different overlay booleans being true at the same time.
Route input as commands
Section titled “Route input as commands”A key press should request a change, not let the input thread reach into a
feature while the update loop is using it. The input side can send a small
command; the update loop then applies it to its own Settings. For example,
CycleOverlay takes Off to Teammates and eventually back to Off:
enum Command { ToggleRadar, CycleOverlay, ToggleDebugValues, Stop,}
// The update loop uses true to leave the loop and enter cleanup.fn apply_command(settings: &mut Settings, command: Command) -> bool { match command { Command::ToggleRadar => settings.show_radar = !settings.show_radar, Command::CycleOverlay => { settings.overlay = match settings.overlay { OverlayMode::Off => OverlayMode::Teammates, OverlayMode::Teammates => OverlayMode::Opponents, OverlayMode::Opponents => OverlayMode::Everyone, OverlayMode::Everyone => OverlayMode::Off, }; } Command::ToggleDebugValues => { settings.show_debug_values = !settings.show_debug_values; } Command::Stop => return true, } false}The input thread sends commands through a channel. The update loop owns and
changes the settings, avoiding shared mutable globals. A true return means
it should stop taking frames and enter cleanup. The menu receives an
acknowledgement after an enabled feature has actually changed state.
Contain a render feature’s failure
Section titled “Contain a render feature’s failure”If a frame lacks a valid projection matrix, the overlay may fail while the
radar and debug reader can continue. Each feature’s update returns a result.
The owner records the error, marks that feature disabled, and calls its
restoration path. Subsequent updates skip it; an error message alone would
leave it running again on the next frame.
That local decision is valid only when the rest of the snapshot remains sound. A closed process or an invalid entity list stops every consumer. A missing camera may disable only screen projection. If a failing feature cannot restore its hook or patch, keep that failure visible and enter the shared shutdown path rather than treating a disabled checkbox as successful cleanup.
Shut down in reverse order
Section titled “Shut down in reverse order”Do not unload code while another thread may still execute it. Restoration must also happen on the thread or render boundary that owns the changed state. Stop the worker, wait for it to finish, and remove only the hooks this tool installed. A render callback may still be executing after the menu closes.
Keep verified graphics facts with their target build
Section titled “Keep verified graphics facts with their target build”An offset is a fact about one supported executable, not a universal property of an overlay. A profile joins build identity, module-relative locations, object layouts, and expected instruction bytes. Each feature receives verified facts from that profile rather than embedding another copy of an address.
A profile should make the supported game and version obvious. These constants join the concrete labs used by this combined tool; the complete project uses Option<usize> for facts a game does not need.
const ASSAULTCUBE_1202: TargetFacts = TargetFacts { process: "ac_client.exe", local_player_ptr: Some(0x0050_9B74), entity_list_ptr: Some(0x0050_F4F8), entity_count: Some(0x0050_F500), trigger_call: Some(0x0040_AD9D), no_recoil_patch: Some(0x0045_BAAD), radar_patch: Some(0x0040_9FB3), esp_hook: Some(0x0040_BE7E), draw_elements_hook_offset: None,};
const URBAN_TERROR_434: TargetFacts = TargetFacts { process: "Quake3-UrT.exe", local_player_ptr: None, entity_list_ptr: None, entity_count: None, trigger_call: None, no_recoil_patch: None, radar_patch: None, esp_hook: None, draw_elements_hook_offset: Some(0x16),};At startup, verify an original instruction signature at every address you plan to use. The finished DLL dispatches by the current executable and exposes only hotkeys that belong to that exact game:
| Target | F1 | F2 | F3 | F4 | F5 |
|---|---|---|---|---|---|
| Wesnoth 1.14.9 | terrain gold cave | second-player stat text | reveal map | — | — |
| AssaultCube 1.2.0.2 | triggerbot | no recoil | reveal radar | aimbot | internal ESP |
| Urban Terror 4.3.4 | memory wallhook | OpenGL wallhack | OpenGL chams | — | — |
End stops the worker and restores every active patch. Flare and Wyrmsun, the offline automation targets from Chapter 4, use their dedicated in-game loops instead of this function-key table.
Load one supported profile after fingerprinting the target. Features receive
verified addresses and layouts instead of hardcoding them. If a profile has
no address for a capability, that control must remain unavailable; None is
not an invitation to reuse an address from another game.
The complete dispatcher and lifecycles are in dll.rs. The patch-owning primitives are in local_patch.rs; dropping each LocalPatch restores the bytes it captured only after an exact pre-patch verification.
dll.rs
Show the codeHide the code
//! DLL entry points and the small hotkey worker used by the course labs.
#![cfg(target_arch = "x86")]
use std::{
ffi::c_void,
path::Path,
sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
time::Duration,
};
use anyhow::{Context, Result};
use windows::Win32::{
Foundation::{HINSTANCE, HMODULE},
System::{LibraryLoader::DisableThreadLibraryCalls, SystemServices::DLL_PROCESS_ATTACH},
UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_END, VK_F1, VK_F2, VK_F3, VK_F4, VK_F5},
};
use windows::core::BOOL;
use super::{
LocalPatch, OPENGL_CHAMS, OPENGL_OFF, OPENGL_WALLHACK, TARGET_UNDER_CROSSHAIR,
assaultcube_aim_once, assaultcube_update_esp, enable_assaultcube_no_recoil,
enable_assaultcube_show_all_radar, enable_wesnoth_map_reveal, install_assaultcube_esp_hook,
install_assaultcube_trigger_hook, install_urban_terror_memory_wallhook,
install_urban_terror_opengl_hook, install_wesnoth_stat_hook, install_wesnoth_terrain_hook,
run_flare_farming_bot, run_wyrmsun_macro, send_left_mouse, set_urban_terror_opengl_mode,
write_wesnoth_gold,
};
static STARTED: OnceLock<()> = OnceLock::new();
static STOP: AtomicBool = AtomicBool::new(false);
fn pressed_once(virtual_key: i32) -> bool {
// SAFETY: GetAsyncKeyState accepts any virtual-key integer and returns a
// value. The low bit means the key was pressed since the previous query.
unsafe { GetAsyncKeyState(virtual_key) & 1 != 0 }
}
fn toggle(
slot: &mut Option<LocalPatch>,
install: impl FnOnce() -> Result<LocalPatch>,
) -> Result<()> {
if let Some(mut patch) = slot.take() {
patch.restore()?;
} else {
*slot = Some(install()?);
}
Ok(())
}
fn current_process_name() -> Result<String> {
let executable = std::env::current_exe().context("Windows did not report the game path")?;
let name = Path::new(&executable)
.file_name()
.context("the game path has no file name")?
.to_string_lossy()
.into_owned();
Ok(name)
}
fn run_assaultcube() -> Result<()> {
let mut trigger = None;
let mut no_recoil = None;
let mut radar = None;
let mut esp = None;
let mut aimbot_enabled = false;
let mut mouse_down = false;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
if trigger.is_some() && mouse_down {
send_left_mouse(false)?;
mouse_down = false;
}
toggle(&mut trigger, install_assaultcube_trigger_hook)?;
if trigger.is_none() {
TARGET_UNDER_CROSSHAIR.store(false, Ordering::Release);
}
}
if pressed_once(VK_F2.0.into()) {
toggle(&mut no_recoil, enable_assaultcube_no_recoil)?;
}
if pressed_once(VK_F3.0.into()) {
toggle(&mut radar, enable_assaultcube_show_all_radar)?;
}
if pressed_once(VK_F4.0.into()) {
aimbot_enabled = !aimbot_enabled;
}
if pressed_once(VK_F5.0.into()) {
toggle(&mut esp, install_assaultcube_esp_hook)?;
}
if aimbot_enabled {
assaultcube_aim_once()?;
}
if esp.is_some() {
assaultcube_update_esp()?;
}
let wants_mouse_down = trigger.is_some() && TARGET_UNDER_CROSSHAIR.load(Ordering::Acquire);
if wants_mouse_down != mouse_down {
send_left_mouse(wants_mouse_down)?;
mouse_down = wants_mouse_down;
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(8));
}
if mouse_down {
send_left_mouse(false)?;
}
// Dropping these values restores the original instructions in reverse
// lexical order after the worker has stopped using them.
drop(esp);
drop(radar);
drop(no_recoil);
drop(trigger);
Ok(())
}
fn run_wesnoth() -> Result<()> {
write_wesnoth_gold(999)?;
let mut terrain = None;
let mut stat = None;
let mut map = None;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
toggle(&mut terrain, install_wesnoth_terrain_hook)?;
}
if pressed_once(VK_F2.0.into()) {
toggle(&mut stat, install_wesnoth_stat_hook)?;
}
if pressed_once(VK_F3.0.into()) {
toggle(&mut map, enable_wesnoth_map_reveal)?;
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(20));
}
drop(map);
drop(stat);
drop(terrain);
Ok(())
}
fn run_urban_terror() -> Result<()> {
let mut memory_wallhook = None;
let mut opengl_hook = None;
let mut opengl_mode = OPENGL_OFF;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
toggle(&mut memory_wallhook, install_urban_terror_memory_wallhook)?;
}
if pressed_once(VK_F2.0.into()) {
if opengl_mode == OPENGL_WALLHACK {
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook.take());
opengl_mode = OPENGL_OFF;
} else {
if opengl_hook.is_none() {
opengl_hook = Some(install_urban_terror_opengl_hook(OPENGL_WALLHACK)?);
}
set_urban_terror_opengl_mode(OPENGL_WALLHACK)?;
opengl_mode = OPENGL_WALLHACK;
}
}
if pressed_once(VK_F3.0.into()) {
if opengl_mode == OPENGL_CHAMS {
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook.take());
opengl_mode = OPENGL_OFF;
} else {
if opengl_hook.is_none() {
opengl_hook = Some(install_urban_terror_opengl_hook(OPENGL_CHAMS)?);
}
set_urban_terror_opengl_mode(OPENGL_CHAMS)?;
opengl_mode = OPENGL_CHAMS;
}
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(8));
}
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook);
drop(memory_wallhook);
Ok(())
}
fn run_lab() -> Result<()> {
let process = current_process_name()?.to_ascii_lowercase();
match process.as_str() {
"wesnoth.exe" => run_wesnoth(),
"ac_client.exe" => run_assaultcube(),
"quake3-urt.exe" => run_urban_terror(),
"flare.exe" => run_flare_farming_bot(&STOP),
"wyrmsun.exe" => run_wyrmsun_macro(&STOP),
_ => anyhow::bail!("{process:?} is not a configured course target"),
}
}
/// Windows calls this under the loader lock. Do only the one documented,
/// loader-safe optimization here; the injector calls `gha_start` afterward.
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(instance: HINSTANCE, reason: u32, _reserved: *mut c_void) -> BOOL {
if reason == DLL_PROCESS_ATTACH {
// SAFETY: `instance` is the module Windows is currently attaching.
let _ = unsafe { DisableThreadLibraryCalls(HMODULE(instance.0)) };
}
BOOL::from(true)
}
/// Exported with the Windows thread-procedure signature so the injector can
/// call it safely after LoadLibraryW has returned and released the loader lock.
#[unsafe(no_mangle)]
pub extern "system" fn gha_start(_argument: *mut c_void) -> u32 {
if STARTED.set(()).is_err() {
return 1; // already running
}
STOP.store(false, Ordering::Release);
let started = thread::Builder::new()
.name("gha-tool-worker".into())
.spawn(|| {
if let Err(error) = run_lab() {
eprintln!("game lab stopped: {error:#}");
}
});
if started.is_err() {
return 2;
}
0
}
#[unsafe(no_mangle)]
pub extern "system" fn gha_stop(_argument: *mut c_void) -> u32 {
STOP.store(true, Ordering::Release);
0
}local_patch.rs
Show the codeHide the code
use std::{ffi::c_void, mem::size_of, ptr};
use anyhow::{Context, Result};
use windows::Win32::System::{
Diagnostics::Debug::FlushInstructionCache,
Memory::{PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS, VirtualProtect},
Threading::GetCurrentProcess,
};
/// A patch applied inside the current process.
///
/// The original bytes are owned by this value. Calling `restore`, or simply
/// dropping the value, puts those bytes back.
#[derive(Debug)]
pub struct LocalPatch {
address: usize,
original: Vec<u8>,
active: bool,
}
fn read_local(address: usize, count: usize) -> Result<Vec<u8>> {
anyhow::ensure!(address != 0, "cannot read address zero");
anyhow::ensure!(count != 0, "cannot read an empty instruction span");
let mut bytes = vec![0_u8; count];
// SAFETY: callers use a supported-game profile. The bytes are copied
// immediately into owned memory and are never exposed as a borrowed slice.
unsafe {
ptr::copy_nonoverlapping(address as *const u8, bytes.as_mut_ptr(), count);
}
Ok(bytes)
}
fn write_local_code(address: usize, bytes: &[u8]) -> Result<()> {
anyhow::ensure!(address != 0, "cannot patch address zero");
anyhow::ensure!(!bytes.is_empty(), "cannot write an empty patch");
let mut old_protection = PAGE_PROTECTION_FLAGS::default();
// SAFETY: the supported-game profile supplies the live instruction range;
// Windows validates the page and returns its previous protection.
unsafe {
VirtualProtect(
address as *const c_void,
bytes.len(),
PAGE_EXECUTE_READWRITE,
&mut old_protection,
)?;
}
// SAFETY: VirtualProtect made the verified instruction range writable.
unsafe {
ptr::copy_nonoverlapping(bytes.as_ptr(), address as *mut u8, bytes.len());
FlushInstructionCache(
GetCurrentProcess(),
Some(address as *const c_void),
bytes.len(),
)?;
}
let mut ignored = PAGE_PROTECTION_FLAGS::default();
// SAFETY: restore the protection returned for the exact same range.
unsafe {
VirtualProtect(
address as *const c_void,
bytes.len(),
old_protection,
&mut ignored,
)?;
}
Ok(())
}
impl LocalPatch {
/// Applies a patch only when every original byte matches.
pub fn apply(address: usize, expected: &[u8], replacement: &[u8]) -> Result<Self> {
anyhow::ensure!(
expected.len() == replacement.len(),
"expected and replacement instruction spans differ"
);
let found = read_local(address, expected.len())?;
anyhow::ensure!(
found == expected,
"bytes at {address:#010x} do not match the supported game build"
);
write_local_code(address, replacement)?;
Ok(Self {
address,
original: found,
active: true,
})
}
/// Applies a patch with a mask. `Some(byte)` must match; `None` captures
/// the live byte without guessing it. This is useful for a branch whose
/// opcode is known but whose four-byte relative destination is recorded at
/// runtime and restored exactly.
pub fn apply_masked(
address: usize,
expected: &[Option<u8>],
replacement: &[u8],
) -> Result<Self> {
anyhow::ensure!(
expected.len() == replacement.len(),
"mask and replacement instruction spans differ"
);
let found = read_local(address, expected.len())?;
for (index, (actual, wanted)) in found.iter().zip(expected).enumerate() {
if let Some(wanted) = wanted {
anyhow::ensure!(
actual == wanted,
"byte {index} at {address:#010x} is {actual:#04x}, expected {wanted:#04x}"
);
}
}
write_local_code(address, replacement)?;
Ok(Self {
address,
original: found,
active: true,
})
}
/// Restores the captured bytes. Calling this more than once is harmless.
pub fn restore(&mut self) -> Result<()> {
if self.active {
write_local_code(self.address, &self.original)
.with_context(|| format!("could not restore patch at {:#010x}", self.address))?;
self.active = false;
}
Ok(())
}
}
impl Drop for LocalPatch {
fn drop(&mut self) {
if let Err(error) = self.restore() {
eprintln!("warning: {error:#}");
}
}
}
/// Builds the five bytes used by an x86 near jump (`E9` plus `rel32`).
pub fn near_jump(from: usize, to: usize) -> Result<[u8; 5]> {
relative_instruction(0xE9, from, to)
}
/// Builds the five bytes used by an x86 near call (`E8` plus `rel32`).
pub fn near_call(from: usize, to: usize) -> Result<[u8; 5]> {
relative_instruction(0xE8, from, to)
}
fn relative_instruction(opcode: u8, from: usize, to: usize) -> Result<[u8; 5]> {
let next = from.checked_add(5).context("jump address overflowed")?;
let difference = (to as i128) - (next as i128);
let displacement = i32::try_from(difference).context("jump is outside x86 rel32 range")?;
let mut instruction = [0_u8; 5];
instruction[0] = opcode;
instruction[1..].copy_from_slice(&displacement.to_le_bytes());
Ok(instruction)
}
/// Returns the size Windows expects when an [`INPUT`](windows::Win32::UI::Input::KeyboardAndMouse::INPUT) value is sent.
#[must_use]
pub fn input_structure_size() -> i32 {
i32::try_from(size_of::<windows::Win32::UI::Input::KeyboardAndMouse::INPUT>())
.expect("INPUT fits in an i32 on Windows")
}
#[cfg(test)]
mod tests {
use super::{near_call, near_jump};
#[test]
fn near_jump_handles_forward_and_backward_destinations() {
assert_eq!(near_jump(0x1000, 0x1010).unwrap(), [0xE9, 0x0B, 0, 0, 0]);
assert_eq!(
near_jump(0x1010, 0x1000).unwrap(),
[0xE9, 0xEB, 0xFF, 0xFF, 0xFF]
);
assert_eq!(near_call(0x1000, 0x1010).unwrap(), [0xE8, 0x0B, 0, 0, 0]);
}
}Build the Urban Terror 4.3.4 memory wallhack
Section titled “Build the Urban Terror 4.3.4 memory wallhack”The flag trace and frame-local state model in Lesson 7.2 can be understood with the debugger alone. This pinned in-process implementation applies the verified detour and DLL lifecycle from Lesson 8.3 to the rendering boundary. Keep those ownership and restoration steps intact while following the exact build-specific addresses below.
Exact-build reference: the Urban Terror 4.3.4 memory wallhack
The r_drawentities trace in this exact build leads through the call at 0x0052F71F. Inside the entity loop, ebx holds the current render entity at 0x0052D2FD. The field at [ebx+4] cycles through values such as 0x0D, 0x40, 0x82, and 0x83.
Hook this exact point:
hook: 0x0052_D2FDresume: 0x0052_D303change: write 0x0D to dword ptr [ebx+4]replay: mov dword ptr [0x0102_AE98], ebxA 32-bit detour cave has the same shape as the Wesnoth cave:
#[cfg(target_arch = "x86")]#[unsafe(naked)]unsafe extern "C" fn render_flag_cave() { core::arch::naked_asm!( "pushfd", "pushad", "mov dword ptr [ebx + 4], 0x0D", "popad", "popfd", "mov dword ptr [0x0102_AE98], ebx", "jmp {resume}", resume = const 0x0052_D303, );}Install a six-byte verified detour at 0x0052D2FD, enter a local game, enable cg_thirdperson, and add bots. The local player and bots should render through walls, as in
Lesson 7.2’s depth-test experiment. Values 0x83 and 0x40 should not create the same effect; that comparison is how you prove 0x0D is the relevant render state.
Restore the original six hook bytes when the feature is disabled. This memory-based wallhack is intentionally kept as a full target-specific lab before the more general OpenGL approach below.
The DLL already contains the complete verified implementation. Build and inject it into Quake3-UrT.exe, enter a local bot match, and press F1 to toggle this memory wallhook. Press End to stop and restore all six bytes. See install_urban_terror_memory_wallhook and the Urban Terror hotkey worker in dll.rs.
game_hooks.rs
Show the codeHide the code
//! Exact x86 hooks used by the version-pinned course targets.
#![cfg(target_arch = "x86")]
use std::{
ptr,
sync::atomic::{AtomicBool, Ordering},
};
use anyhow::{Context, Result};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEINPUT, SendInput,
};
use super::{LocalPatch, input_structure_size, near_jump};
const WESNOTH_PLAYER_ROOT: usize = 0x017E_ED18;
const WESNOTH_GAME_OFFSET: usize = 0x0A90;
const WESNOTH_GOLD_OFFSET: usize = 0x0004;
const WESNOTH_TERRAIN_HOOK: usize = 0x00CC_AF8A;
const WESNOTH_TERRAIN_RESUME: usize = 0x00CC_AF90;
const AC_TRIGGER_HOOK: usize = 0x0040_AD9D;
const AC_TRIGGER_ORIGINAL: usize = 0x0046_07C0;
const AC_TRIGGER_RESUME: usize = 0x0040_ADA2;
const AC_RECOIL_STORE: usize = 0x0045_BAAD;
const AC_RADAR_FILTER: usize = 0x0040_9FB3;
const URT_RENDER_HOOK: usize = 0x0052_D2FD;
const URT_RENDER_RESUME: usize = 0x0052_D303;
pub static TARGET_UNDER_CROSSHAIR: AtomicBool = AtomicBool::new(false);
/// Resolves Wesnoth's two pointers and writes the live gold field.
///
/// # Safety
/// Call only inside 32-bit Wesnoth 1.14.9 after a local match has started.
unsafe fn set_wesnoth_gold(amount: u32) -> Result<()> {
// SAFETY: the caller verified the target build; volatile access makes the
// actual memory operation visible at this exact point.
let player = unsafe { ptr::read_volatile(WESNOTH_PLAYER_ROOT as *const u32) } as usize;
anyhow::ensure!(
player != 0,
"start a Wesnoth match before using the terrain hook"
);
let side_pointer_address = player
.checked_add(WESNOTH_GAME_OFFSET)
.context("Wesnoth side-pointer address overflowed")?;
// SAFETY: the verified player structure owns a 32-bit pointer at +0xA90.
let side = unsafe { ptr::read_volatile(side_pointer_address as *const u32) } as usize;
anyhow::ensure!(side != 0, "Wesnoth side pointer is null");
let gold_address = side
.checked_add(WESNOTH_GOLD_OFFSET)
.context("Wesnoth gold address overflowed")?;
// SAFETY: the verified side structure owns its four-byte gold field at +4.
unsafe { ptr::write_volatile(gold_address as *mut u32, amount) };
Ok(())
}
pub fn write_wesnoth_gold(amount: u32) -> Result<()> {
// SAFETY: the DLL worker calls this only after matching wesnoth.exe. The
// pointer resolver still rejects the normal "no active match" null state.
unsafe { set_wesnoth_gold(amount) }
}
extern "C" fn wesnoth_cave_body() {
// A naked cave cannot return `Result`, so the worker verifies the target and
// this tiny body simply leaves the game unchanged if the match is not ready.
// SAFETY: the cave is installed only after the exact Wesnoth hook bytes
// match; the helper checks every nullable pointer in the live chain.
let _ = unsafe { set_wesnoth_gold(888) };
}
#[unsafe(naked)]
unsafe extern "C" fn wesnoth_terrain_cave() {
core::arch::naked_asm!(
"pushfd",
"pushad",
"call {body}",
"popad",
"popfd",
// Replay the behavior of the six bytes replaced at the hook site.
"mov eax, dword ptr [ecx]",
"lea esi, [esi]",
"jmp {resume}",
body = sym wesnoth_cave_body,
resume = const WESNOTH_TERRAIN_RESUME,
);
}
extern "C" fn record_crosshair_target(value: u32) {
TARGET_UNDER_CROSSHAIR.store(value != 0, Ordering::Release);
}
#[unsafe(naked)]
unsafe extern "C" fn assaultcube_trigger_cave() {
core::arch::naked_asm!(
// The removed instruction was this call. Run it first so EAX contains
// the game's real target-under-crosshair result.
"call {original}",
"pushfd",
"pushad",
"push eax",
"call {record}",
"add esp, 4",
"popad",
"popfd",
"jmp {resume}",
original = const AC_TRIGGER_ORIGINAL,
record = sym record_crosshair_target,
resume = const AC_TRIGGER_RESUME,
);
}
#[unsafe(naked)]
unsafe extern "C" fn urban_terror_render_cave() {
core::arch::naked_asm!(
"pushfd",
"pushad",
"mov dword ptr [ebx + 4], 0x0D",
"popad",
"popfd",
// Replay the complete instruction that the detour replaced.
"mov dword ptr [0x0102AE98], ebx",
"jmp {resume}",
resume = const URT_RENDER_RESUME,
);
}
fn six_byte_detour(hook: usize, cave: usize) -> Result<[u8; 6]> {
let jump = near_jump(hook, cave)?;
let mut replacement = [0x90_u8; 6];
replacement[..5].copy_from_slice(&jump);
Ok(replacement)
}
pub fn install_wesnoth_terrain_hook() -> Result<LocalPatch> {
// mov eax,[ecx] ; lea esi,[esi+eiz]
const ORIGINAL: [u8; 6] = [0x8B, 0x01, 0x8D, 0x74, 0x26, 0x00];
let cave = wesnoth_terrain_cave as *const () as usize;
let replacement = six_byte_detour(WESNOTH_TERRAIN_HOOK, cave)?;
LocalPatch::apply(WESNOTH_TERRAIN_HOOK, &ORIGINAL, &replacement)
}
pub fn install_assaultcube_trigger_hook() -> Result<LocalPatch> {
// call 0x004607C0, encoded relative to the end of this instruction.
const ORIGINAL: [u8; 5] = [0xE8, 0x1E, 0x5A, 0x05, 0x00];
let cave = assaultcube_trigger_cave as *const () as usize;
let replacement = near_jump(AC_TRIGGER_HOOK, cave)?;
LocalPatch::apply(AC_TRIGGER_HOOK, &ORIGINAL, &replacement)
}
pub fn install_urban_terror_memory_wallhook() -> Result<LocalPatch> {
// mov dword ptr [0x0102AE98],ebx
const ORIGINAL: [u8; 6] = [0x89, 0x1D, 0x98, 0xAE, 0x02, 0x01];
let cave = urban_terror_render_cave as *const () as usize;
let replacement = six_byte_detour(URT_RENDER_HOOK, cave)?;
LocalPatch::apply(URT_RENDER_HOOK, &ORIGINAL, &replacement)
}
pub fn enable_assaultcube_no_recoil() -> Result<LocalPatch> {
// fstp dword ptr [ebx+0x44] -> fstp st(0), nop
LocalPatch::apply(AC_RECOIL_STORE, &[0xD9, 0x5B, 0x44], &[0xDD, 0xD8, 0x90])
}
pub fn enable_assaultcube_show_all_radar() -> Result<LocalPatch> {
// 0F 85 is a six-byte near JNE. Capture its four-byte destination exactly,
// NOP the full branch, and let LocalPatch restore all six captured bytes.
LocalPatch::apply_masked(
AC_RADAR_FILTER,
&[Some(0x0F), Some(0x85), None, None, None, None],
&[0x90; 6],
)
}
pub fn send_left_mouse(pressed: bool) -> Result<()> {
let flags = if pressed {
MOUSEEVENTF_LEFTDOWN
} else {
MOUSEEVENTF_LEFTUP
};
let input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: 0,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
};
// SAFETY: `input` is a fully initialized INPUT value and the structure size
// is the one expected by this build of Windows.
let sent = unsafe { SendInput(&[input], input_structure_size()) };
anyhow::ensure!(sent == 1, "SendInput sent {sent} of 1 mouse event");
Ok(())
}dll.rs
Show the codeHide the code
//! DLL entry points and the small hotkey worker used by the course labs.
#![cfg(target_arch = "x86")]
use std::{
ffi::c_void,
path::Path,
sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
time::Duration,
};
use anyhow::{Context, Result};
use windows::Win32::{
Foundation::{HINSTANCE, HMODULE},
System::{LibraryLoader::DisableThreadLibraryCalls, SystemServices::DLL_PROCESS_ATTACH},
UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_END, VK_F1, VK_F2, VK_F3, VK_F4, VK_F5},
};
use windows::core::BOOL;
use super::{
LocalPatch, OPENGL_CHAMS, OPENGL_OFF, OPENGL_WALLHACK, TARGET_UNDER_CROSSHAIR,
assaultcube_aim_once, assaultcube_update_esp, enable_assaultcube_no_recoil,
enable_assaultcube_show_all_radar, enable_wesnoth_map_reveal, install_assaultcube_esp_hook,
install_assaultcube_trigger_hook, install_urban_terror_memory_wallhook,
install_urban_terror_opengl_hook, install_wesnoth_stat_hook, install_wesnoth_terrain_hook,
run_flare_farming_bot, run_wyrmsun_macro, send_left_mouse, set_urban_terror_opengl_mode,
write_wesnoth_gold,
};
static STARTED: OnceLock<()> = OnceLock::new();
static STOP: AtomicBool = AtomicBool::new(false);
fn pressed_once(virtual_key: i32) -> bool {
// SAFETY: GetAsyncKeyState accepts any virtual-key integer and returns a
// value. The low bit means the key was pressed since the previous query.
unsafe { GetAsyncKeyState(virtual_key) & 1 != 0 }
}
fn toggle(
slot: &mut Option<LocalPatch>,
install: impl FnOnce() -> Result<LocalPatch>,
) -> Result<()> {
if let Some(mut patch) = slot.take() {
patch.restore()?;
} else {
*slot = Some(install()?);
}
Ok(())
}
fn current_process_name() -> Result<String> {
let executable = std::env::current_exe().context("Windows did not report the game path")?;
let name = Path::new(&executable)
.file_name()
.context("the game path has no file name")?
.to_string_lossy()
.into_owned();
Ok(name)
}
fn run_assaultcube() -> Result<()> {
let mut trigger = None;
let mut no_recoil = None;
let mut radar = None;
let mut esp = None;
let mut aimbot_enabled = false;
let mut mouse_down = false;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
if trigger.is_some() && mouse_down {
send_left_mouse(false)?;
mouse_down = false;
}
toggle(&mut trigger, install_assaultcube_trigger_hook)?;
if trigger.is_none() {
TARGET_UNDER_CROSSHAIR.store(false, Ordering::Release);
}
}
if pressed_once(VK_F2.0.into()) {
toggle(&mut no_recoil, enable_assaultcube_no_recoil)?;
}
if pressed_once(VK_F3.0.into()) {
toggle(&mut radar, enable_assaultcube_show_all_radar)?;
}
if pressed_once(VK_F4.0.into()) {
aimbot_enabled = !aimbot_enabled;
}
if pressed_once(VK_F5.0.into()) {
toggle(&mut esp, install_assaultcube_esp_hook)?;
}
if aimbot_enabled {
assaultcube_aim_once()?;
}
if esp.is_some() {
assaultcube_update_esp()?;
}
let wants_mouse_down = trigger.is_some() && TARGET_UNDER_CROSSHAIR.load(Ordering::Acquire);
if wants_mouse_down != mouse_down {
send_left_mouse(wants_mouse_down)?;
mouse_down = wants_mouse_down;
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(8));
}
if mouse_down {
send_left_mouse(false)?;
}
// Dropping these values restores the original instructions in reverse
// lexical order after the worker has stopped using them.
drop(esp);
drop(radar);
drop(no_recoil);
drop(trigger);
Ok(())
}
fn run_wesnoth() -> Result<()> {
write_wesnoth_gold(999)?;
let mut terrain = None;
let mut stat = None;
let mut map = None;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
toggle(&mut terrain, install_wesnoth_terrain_hook)?;
}
if pressed_once(VK_F2.0.into()) {
toggle(&mut stat, install_wesnoth_stat_hook)?;
}
if pressed_once(VK_F3.0.into()) {
toggle(&mut map, enable_wesnoth_map_reveal)?;
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(20));
}
drop(map);
drop(stat);
drop(terrain);
Ok(())
}
fn run_urban_terror() -> Result<()> {
let mut memory_wallhook = None;
let mut opengl_hook = None;
let mut opengl_mode = OPENGL_OFF;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
toggle(&mut memory_wallhook, install_urban_terror_memory_wallhook)?;
}
if pressed_once(VK_F2.0.into()) {
if opengl_mode == OPENGL_WALLHACK {
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook.take());
opengl_mode = OPENGL_OFF;
} else {
if opengl_hook.is_none() {
opengl_hook = Some(install_urban_terror_opengl_hook(OPENGL_WALLHACK)?);
}
set_urban_terror_opengl_mode(OPENGL_WALLHACK)?;
opengl_mode = OPENGL_WALLHACK;
}
}
if pressed_once(VK_F3.0.into()) {
if opengl_mode == OPENGL_CHAMS {
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook.take());
opengl_mode = OPENGL_OFF;
} else {
if opengl_hook.is_none() {
opengl_hook = Some(install_urban_terror_opengl_hook(OPENGL_CHAMS)?);
}
set_urban_terror_opengl_mode(OPENGL_CHAMS)?;
opengl_mode = OPENGL_CHAMS;
}
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(8));
}
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook);
drop(memory_wallhook);
Ok(())
}
fn run_lab() -> Result<()> {
let process = current_process_name()?.to_ascii_lowercase();
match process.as_str() {
"wesnoth.exe" => run_wesnoth(),
"ac_client.exe" => run_assaultcube(),
"quake3-urt.exe" => run_urban_terror(),
"flare.exe" => run_flare_farming_bot(&STOP),
"wyrmsun.exe" => run_wyrmsun_macro(&STOP),
_ => anyhow::bail!("{process:?} is not a configured course target"),
}
}
/// Windows calls this under the loader lock. Do only the one documented,
/// loader-safe optimization here; the injector calls `gha_start` afterward.
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(instance: HINSTANCE, reason: u32, _reserved: *mut c_void) -> BOOL {
if reason == DLL_PROCESS_ATTACH {
// SAFETY: `instance` is the module Windows is currently attaching.
let _ = unsafe { DisableThreadLibraryCalls(HMODULE(instance.0)) };
}
BOOL::from(true)
}
/// Exported with the Windows thread-procedure signature so the injector can
/// call it safely after LoadLibraryW has returned and released the loader lock.
#[unsafe(no_mangle)]
pub extern "system" fn gha_start(_argument: *mut c_void) -> u32 {
if STARTED.set(()).is_err() {
return 1; // already running
}
STOP.store(false, Ordering::Release);
let started = thread::Builder::new()
.name("gha-tool-worker".into())
.spawn(|| {
if let Err(error) = run_lab() {
eprintln!("game lab stopped: {error:#}");
}
});
if started.is_err() {
return 2;
}
0
}
#[unsafe(no_mangle)]
pub extern "system" fn gha_stop(_argument: *mut c_void) -> u32 {
STOP.store(true, Ordering::Release);
0
}What the experiment proves
Section titled “What the experiment proves”The comparison isolates how one CPU-side flag changes GPU behavior. It does not prove that every build uses the same flag, address, or rendering path, so keep the executable fingerprint and original bytes beside the result.
Turn the wrapper into the actual Urban Terror wallhack
Section titled “Turn the wrapper into the actual Urban Terror wallhack”The API signature, draw-count interpretation, and sampling design in Lesson 7.3 stand on their own. The exact-build detour below applies the DLL and patch lifecycle from Lesson 8.3 to one verified OpenGL instruction. Recheck the displaced instruction span and restoration path before running it.
Exact-build reference: the Urban Terror 4.3.4 OpenGL wallhack
In Urban Terror 4.3.4, resolve glDrawElements from opengl32.dll. The historical build hooks the instruction at glDrawElements + 0x16 and resumes six bytes later. This is not a function-entry wrapper: it is a real code cave in the middle of the OpenGL function. At this point in this 32-bit build, EBX holds count.
Resolve these two OpenGL functions with GetProcAddress:
type GlDepthFunc = unsafe extern "system" fn(function: u32);type GlDepthRange = unsafe extern "system" fn(near: f64, far: f64);
const GL_LEQUAL: u32 = 0x0203;const GL_ALWAYS: u32 = 0x0207;Lesson 7.3 explains why this pair matters:
glDepthRange(0.0, 0.0) flattens every highlighted fragment to the nearest depth,
and GL_ALWAYS stops the depth test from rejecting it.
The six bytes we replace are one whole x86 instruction:
8B B6 18 0A 00 00 mov esi, dword ptr [esi + 0xA18]Before and after the patch, the six bytes at glDrawElements + 0x16 look like
this:
esi from [esi + displacement]0xA18, lowest byte first18 0A 00 00 is 0x18 + 0x0A × 256 = 24 + 2,560 = 2,584, which is 0xA18.jmp to the cave: E9 plus a 4-byte distancenop?? bytes hold the distance from +0x1B, the byte right after the jump (0x16 + 5 = 0x1B), to the cave, so they depend on where the DLL loaded. The cave returns to +0x1C (0x16 + 6 = 22 + 6 = 28 = 0x1C), so the nop never runs; it only fills the sixth byte so no fragment of the old instruction is left behind.The installer refuses to patch if those bytes do not match. It writes a five-byte near jump plus one NOP, and LocalPatch owns the saved bytes so dropping the patch restores them:
let draw_elements = resolve(opengl, s!("glDrawElements"))?;let hook = draw_elements.checked_add(0x16) .context("glDrawElements hook address overflowed")?;DRAW_RETURN.store(hook + 6, Ordering::Release);
const ORIGINAL: [u8; 6] = [0x8B, 0xB6, 0x18, 0x0A, 0x00, 0x00];let jump = near_jump(hook, urban_terror_opengl_cave as *const () as usize)?;let mut replacement = [0x90_u8; 6];replacement[..5].copy_from_slice(&jump);let patch = LocalPatch::apply(hook, &ORIGINAL, &replacement)?;The cave saves the CPU flags and all general-purpose registers, passes the live EBX value to an ordinary helper, replays the displaced instruction, and returns to the byte immediately after it:
#[unsafe(naked)]unsafe extern "C" fn urban_terror_opengl_cave() { core::arch::naked_asm!( "pushfd", "pushad", "push ebx", "call {apply}", "add esp, 4", "popad", "popfd", "mov esi, dword ptr [esi + 0xA18]", "jmp dword ptr [{resume}]", apply = sym apply_urban_terror_gl_state, resume = sym DRAW_RETURN, );}Why save everything? The original OpenGL function expects its registers and flags to have particular values when it resumes. The compiler is free to use registers while apply_urban_terror_gl_state runs. Saving and restoring them makes the helper almost invisible to the interrupted function.
The helper changes the live OpenGL state. A later non-highlighted draw restores normal depth state:
extern "C" fn apply_urban_terror_gl_state(count: u32) { let highlighted = OPENGL_MODE.load(Ordering::Acquire) != OPENGL_OFF && count > 500;
// 🛡️ SAFETY: these pointers were resolved from this process's opengl32.dll, // and the game reached the cave on its OpenGL render thread. unsafe { if highlighted { depth_range(0.0, 0.0); depth_func(GL_ALWAYS); } else { depth_range(0.0, 1.0); depth_func(GL_LEQUAL); } }}Build the DLL for 32-bit Windows, inject it into Quake3-UrT.exe, enter a local match, and press F2. Geometry with count > 500 is forced to the near plane and uses GL_ALWAYS, producing the player/weapon-through-wall result shown below. Press F2 again or End to restore normal state and the original six code bytes.
cargo build --release --target i686-pc-windows-msvc.\target\i686-pc-windows-msvc\release\injector.exe ` Quake3-UrT.exe ` .\target\i686-pc-windows-msvc\release\gha_windows_labs.dllThe complete compiled implementation—including export lookup, the naked cave, exact-byte verification, both graphics modes, and restoration—is in opengl_hooks.rs. The hotkey worker is in dll.rs.
opengl_hooks.rs
Show the codeHide the code
//! Urban Terror 4.3.4 OpenGL wallhack/chams hook from the original course lab.
#![cfg(target_arch = "x86")]
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use anyhow::{Context, Result};
use windows::{
Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress},
core::{s, w},
};
use super::{LocalPatch, near_jump};
pub const OPENGL_OFF: u32 = 0;
pub const OPENGL_WALLHACK: u32 = 1;
pub const OPENGL_CHAMS: u32 = 2;
const GL_LEQUAL: u32 = 0x0203;
const GL_ALWAYS: u32 = 0x0207;
const GL_COLOR_MATERIAL: u32 = 0x0B57;
const GL_COLOR_ARRAY: u32 = 0x8076;
const GL_TEXTURE_COORD_ARRAY: u32 = 0x8078;
static OPENGL_MODE: AtomicU32 = AtomicU32::new(OPENGL_OFF);
static DRAW_RETURN: AtomicUsize = AtomicUsize::new(0);
static DEPTH_FUNC: AtomicUsize = AtomicUsize::new(0);
static DEPTH_RANGE: AtomicUsize = AtomicUsize::new(0);
static COLOR_4F: AtomicUsize = AtomicUsize::new(0);
static ENABLE: AtomicUsize = AtomicUsize::new(0);
static DISABLE: AtomicUsize = AtomicUsize::new(0);
static ENABLE_CLIENT_STATE: AtomicUsize = AtomicUsize::new(0);
static DISABLE_CLIENT_STATE: AtomicUsize = AtomicUsize::new(0);
type GlDepthFunc = unsafe extern "system" fn(u32);
type GlDepthRange = unsafe extern "system" fn(f64, f64);
type GlColor4f = unsafe extern "system" fn(f32, f32, f32, f32);
type GlState = unsafe extern "system" fn(u32);
fn address(slot: &AtomicUsize) -> Option<usize> {
let value = slot.load(Ordering::Acquire);
(value != 0).then_some(value)
}
// This mid-glDrawElements cave cannot wrap one whole draw, so the non-highlight
// branch restores the defaults observed in the pinned Urban Terror 4.3.4 build.
// It is deliberately not advertised as a general OpenGL state snapshot. A
// reusable hook should capture/query the exact previous state at a boundary
// where it can restore that state immediately after forwarding one draw.
extern "C" fn apply_urban_terror_gl_state(count: u32) {
let mode = OPENGL_MODE.load(Ordering::Acquire);
let (Some(depth_func), Some(depth_range)) = (address(&DEPTH_FUNC), address(&DEPTH_RANGE))
else {
return;
};
// SAFETY: install_urban_terror_opengl_hook resolved these opengl32 exports
// and stored function pointers with their documented Windows ABI.
let depth_func: GlDepthFunc = unsafe { std::mem::transmute(depth_func) };
// SAFETY: same proof as above for glDepthRange.
let depth_range: GlDepthRange = unsafe { std::mem::transmute(depth_range) };
let highlighted = mode != OPENGL_OFF && count > 500;
// SAFETY: a current Urban Terror render context called this hook.
unsafe {
if highlighted {
depth_range(0.0, 0.0);
depth_func(GL_ALWAYS);
} else {
depth_range(0.0, 1.0);
depth_func(GL_LEQUAL);
}
}
if mode != OPENGL_CHAMS {
return;
}
let (Some(color), Some(enable), Some(disable), Some(enable_client), Some(disable_client)) = (
address(&COLOR_4F),
address(&ENABLE),
address(&DISABLE),
address(&ENABLE_CLIENT_STATE),
address(&DISABLE_CLIENT_STATE),
) else {
return;
};
// SAFETY: each address was resolved from the matching named OpenGL export.
let color: GlColor4f = unsafe { std::mem::transmute(color) };
// SAFETY: `enable` is the address returned for the glEnable export.
let enable: GlState = unsafe { std::mem::transmute(enable) };
// SAFETY: `disable` is the address returned for the glDisable export.
let disable: GlState = unsafe { std::mem::transmute(disable) };
// SAFETY: this address came from glEnableClientState, which has this ABI.
let enable_client: GlState = unsafe { std::mem::transmute(enable_client) };
// SAFETY: this address came from glDisableClientState, which has this ABI.
let disable_client: GlState = unsafe { std::mem::transmute(disable_client) };
// SAFETY: these are standard OpenGL 1.1 state calls on the render thread.
unsafe {
if highlighted {
disable_client(GL_TEXTURE_COORD_ARRAY);
disable_client(GL_COLOR_ARRAY);
enable(GL_COLOR_MATERIAL);
color(1.0, 0.6, 0.6, 1.0);
} else {
enable_client(GL_TEXTURE_COORD_ARRAY);
enable_client(GL_COLOR_ARRAY);
disable(GL_COLOR_MATERIAL);
color(1.0, 1.0, 1.0, 1.0);
}
}
}
#[unsafe(naked)]
unsafe extern "C" fn urban_terror_opengl_cave() {
core::arch::naked_asm!(
"pushfd",
"pushad",
// At this point in glDrawElements, EBX holds the index count. This is
// the value the original C++ course cave recovered from saved EBX.
"push ebx",
"call {apply}",
"add esp, 4",
"popad",
"popfd",
// Replay all six bytes replaced at glDrawElements + 0x16.
"mov esi, dword ptr [esi + 0xA18]",
"jmp dword ptr [{resume}]",
apply = sym apply_urban_terror_gl_state,
resume = sym DRAW_RETURN,
);
}
fn resolve(
module: windows::Win32::Foundation::HMODULE,
name: windows::core::PCSTR,
) -> Result<usize> {
// SAFETY: the module is live and every caller passes a static export name.
let function = unsafe { GetProcAddress(module, name) }
.context("required opengl32 export was not found")?;
Ok(function as *const () as usize)
}
pub fn install_urban_terror_opengl_hook(mode: u32) -> Result<LocalPatch> {
anyhow::ensure!(
matches!(mode, OPENGL_WALLHACK | OPENGL_CHAMS),
"invalid OpenGL lab mode"
);
// SAFETY: the name is a static, zero-terminated UTF-16 string.
let opengl = unsafe { GetModuleHandleW(w!("opengl32.dll")) }
.context("opengl32.dll is not loaded yet; enter a rendered game")?;
DEPTH_FUNC.store(resolve(opengl, s!("glDepthFunc"))?, Ordering::Release);
DEPTH_RANGE.store(resolve(opengl, s!("glDepthRange"))?, Ordering::Release);
COLOR_4F.store(resolve(opengl, s!("glColor4f"))?, Ordering::Release);
ENABLE.store(resolve(opengl, s!("glEnable"))?, Ordering::Release);
DISABLE.store(resolve(opengl, s!("glDisable"))?, Ordering::Release);
ENABLE_CLIENT_STATE.store(
resolve(opengl, s!("glEnableClientState"))?,
Ordering::Release,
);
DISABLE_CLIENT_STATE.store(
resolve(opengl, s!("glDisableClientState"))?,
Ordering::Release,
);
let draw_elements = resolve(opengl, s!("glDrawElements"))?;
let hook = draw_elements
.checked_add(0x16)
.context("glDrawElements hook address overflowed")?;
DRAW_RETURN.store(hook + 6, Ordering::Release);
OPENGL_MODE.store(mode, Ordering::Release);
// mov esi,dword ptr [esi+0xA18]
const ORIGINAL: [u8; 6] = [0x8B, 0xB6, 0x18, 0x0A, 0x00, 0x00];
let jump = near_jump(hook, urban_terror_opengl_cave as *const () as usize)?;
let mut replacement = [0x90_u8; 6];
replacement[..5].copy_from_slice(&jump);
LocalPatch::apply(hook, &ORIGINAL, &replacement)
}
pub fn set_urban_terror_opengl_mode(mode: u32) -> Result<()> {
anyhow::ensure!(
matches!(mode, OPENGL_OFF | OPENGL_WALLHACK | OPENGL_CHAMS),
"invalid OpenGL lab mode"
);
OPENGL_MODE.store(mode, Ordering::Release);
Ok(())
}dll.rs
Show the codeHide the code
//! DLL entry points and the small hotkey worker used by the course labs.
#![cfg(target_arch = "x86")]
use std::{
ffi::c_void,
path::Path,
sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
time::Duration,
};
use anyhow::{Context, Result};
use windows::Win32::{
Foundation::{HINSTANCE, HMODULE},
System::{LibraryLoader::DisableThreadLibraryCalls, SystemServices::DLL_PROCESS_ATTACH},
UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_END, VK_F1, VK_F2, VK_F3, VK_F4, VK_F5},
};
use windows::core::BOOL;
use super::{
LocalPatch, OPENGL_CHAMS, OPENGL_OFF, OPENGL_WALLHACK, TARGET_UNDER_CROSSHAIR,
assaultcube_aim_once, assaultcube_update_esp, enable_assaultcube_no_recoil,
enable_assaultcube_show_all_radar, enable_wesnoth_map_reveal, install_assaultcube_esp_hook,
install_assaultcube_trigger_hook, install_urban_terror_memory_wallhook,
install_urban_terror_opengl_hook, install_wesnoth_stat_hook, install_wesnoth_terrain_hook,
run_flare_farming_bot, run_wyrmsun_macro, send_left_mouse, set_urban_terror_opengl_mode,
write_wesnoth_gold,
};
static STARTED: OnceLock<()> = OnceLock::new();
static STOP: AtomicBool = AtomicBool::new(false);
fn pressed_once(virtual_key: i32) -> bool {
// SAFETY: GetAsyncKeyState accepts any virtual-key integer and returns a
// value. The low bit means the key was pressed since the previous query.
unsafe { GetAsyncKeyState(virtual_key) & 1 != 0 }
}
fn toggle(
slot: &mut Option<LocalPatch>,
install: impl FnOnce() -> Result<LocalPatch>,
) -> Result<()> {
if let Some(mut patch) = slot.take() {
patch.restore()?;
} else {
*slot = Some(install()?);
}
Ok(())
}
fn current_process_name() -> Result<String> {
let executable = std::env::current_exe().context("Windows did not report the game path")?;
let name = Path::new(&executable)
.file_name()
.context("the game path has no file name")?
.to_string_lossy()
.into_owned();
Ok(name)
}
fn run_assaultcube() -> Result<()> {
let mut trigger = None;
let mut no_recoil = None;
let mut radar = None;
let mut esp = None;
let mut aimbot_enabled = false;
let mut mouse_down = false;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
if trigger.is_some() && mouse_down {
send_left_mouse(false)?;
mouse_down = false;
}
toggle(&mut trigger, install_assaultcube_trigger_hook)?;
if trigger.is_none() {
TARGET_UNDER_CROSSHAIR.store(false, Ordering::Release);
}
}
if pressed_once(VK_F2.0.into()) {
toggle(&mut no_recoil, enable_assaultcube_no_recoil)?;
}
if pressed_once(VK_F3.0.into()) {
toggle(&mut radar, enable_assaultcube_show_all_radar)?;
}
if pressed_once(VK_F4.0.into()) {
aimbot_enabled = !aimbot_enabled;
}
if pressed_once(VK_F5.0.into()) {
toggle(&mut esp, install_assaultcube_esp_hook)?;
}
if aimbot_enabled {
assaultcube_aim_once()?;
}
if esp.is_some() {
assaultcube_update_esp()?;
}
let wants_mouse_down = trigger.is_some() && TARGET_UNDER_CROSSHAIR.load(Ordering::Acquire);
if wants_mouse_down != mouse_down {
send_left_mouse(wants_mouse_down)?;
mouse_down = wants_mouse_down;
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(8));
}
if mouse_down {
send_left_mouse(false)?;
}
// Dropping these values restores the original instructions in reverse
// lexical order after the worker has stopped using them.
drop(esp);
drop(radar);
drop(no_recoil);
drop(trigger);
Ok(())
}
fn run_wesnoth() -> Result<()> {
write_wesnoth_gold(999)?;
let mut terrain = None;
let mut stat = None;
let mut map = None;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
toggle(&mut terrain, install_wesnoth_terrain_hook)?;
}
if pressed_once(VK_F2.0.into()) {
toggle(&mut stat, install_wesnoth_stat_hook)?;
}
if pressed_once(VK_F3.0.into()) {
toggle(&mut map, enable_wesnoth_map_reveal)?;
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(20));
}
drop(map);
drop(stat);
drop(terrain);
Ok(())
}
fn run_urban_terror() -> Result<()> {
let mut memory_wallhook = None;
let mut opengl_hook = None;
let mut opengl_mode = OPENGL_OFF;
while !STOP.load(Ordering::Acquire) {
if pressed_once(VK_F1.0.into()) {
toggle(&mut memory_wallhook, install_urban_terror_memory_wallhook)?;
}
if pressed_once(VK_F2.0.into()) {
if opengl_mode == OPENGL_WALLHACK {
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook.take());
opengl_mode = OPENGL_OFF;
} else {
if opengl_hook.is_none() {
opengl_hook = Some(install_urban_terror_opengl_hook(OPENGL_WALLHACK)?);
}
set_urban_terror_opengl_mode(OPENGL_WALLHACK)?;
opengl_mode = OPENGL_WALLHACK;
}
}
if pressed_once(VK_F3.0.into()) {
if opengl_mode == OPENGL_CHAMS {
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook.take());
opengl_mode = OPENGL_OFF;
} else {
if opengl_hook.is_none() {
opengl_hook = Some(install_urban_terror_opengl_hook(OPENGL_CHAMS)?);
}
set_urban_terror_opengl_mode(OPENGL_CHAMS)?;
opengl_mode = OPENGL_CHAMS;
}
}
if pressed_once(VK_END.0.into()) {
break;
}
thread::sleep(Duration::from_millis(8));
}
set_urban_terror_opengl_mode(OPENGL_OFF)?;
drop(opengl_hook);
drop(memory_wallhook);
Ok(())
}
fn run_lab() -> Result<()> {
let process = current_process_name()?.to_ascii_lowercase();
match process.as_str() {
"wesnoth.exe" => run_wesnoth(),
"ac_client.exe" => run_assaultcube(),
"quake3-urt.exe" => run_urban_terror(),
"flare.exe" => run_flare_farming_bot(&STOP),
"wyrmsun.exe" => run_wyrmsun_macro(&STOP),
_ => anyhow::bail!("{process:?} is not a configured course target"),
}
}
/// Windows calls this under the loader lock. Do only the one documented,
/// loader-safe optimization here; the injector calls `gha_start` afterward.
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(instance: HINSTANCE, reason: u32, _reserved: *mut c_void) -> BOOL {
if reason == DLL_PROCESS_ATTACH {
// SAFETY: `instance` is the module Windows is currently attaching.
let _ = unsafe { DisableThreadLibraryCalls(HMODULE(instance.0)) };
}
BOOL::from(true)
}
/// Exported with the Windows thread-procedure signature so the injector can
/// call it safely after LoadLibraryW has returned and released the loader lock.
#[unsafe(no_mangle)]
pub extern "system" fn gha_start(_argument: *mut c_void) -> u32 {
if STARTED.set(()).is_err() {
return 1; // already running
}
STOP.store(false, Ordering::Release);
let started = thread::Builder::new()
.name("gha-tool-worker".into())
.spawn(|| {
if let Err(error) = run_lab() {
eprintln!("game lab stopped: {error:#}");
}
});
if started.is_err() {
return 2;
}
0
}
#[unsafe(no_mangle)]
pub extern "system" fn gha_stop(_argument: *mut c_void) -> u32 {
STOP.store(true, Ordering::Release);
0
}
The threshold is a confirmed property of this lesson build, not a universal player detector. Log counts while toggling third-person and adding/removing bots so you can see exactly which models it includes.
OpenGL state stays set until another call changes it. This middle-of-function cave cannot surround one typed glDrawElements call, so it restores the normal state when the next non-highlighted draw arrives and when the feature is disabled. Pressing End also disables the mode before removing the code patch. That ordering prevents the game from being left with GL_ALWAYS active.
Check the combined render path
Section titled “Check the combined render path”The complete dispatcher and feature lifecycles bring the separate effects into one DLL, called the multihack in the original course. Test the connections as well as each effect:
- radar and overlay consumers use the same captured player values;
- camera and viewport changes cannot silently mix with an older frame;
- a disabled feature is skipped until the user deliberately enables it again;
- each render callback restores the graphics state it changed;
- each hook restores only its own verified original bytes;
- an unsupported game or build exposes no active feature controls;
- shutdown waits for workers and callbacks before unloading code.
Keep a log of target, build, feature, address, expected bytes, found bytes, action, and restoration result, separate from the visible menu. Those records let a failed cleanup remain inspectable even after its UI closes.
The next lesson draws the menu inside the game’s frame. Each feature still needs one owner, commands that report their outcome, and verified restoration of any changed bytes.
Unify feature inputs
Why should several diagnostic features consume one shared frame snapshot?
An owned immutable snapshot gives each feature the same captured values. It does not make a multi-field read atomic: validate the copy and check a tick or generation counter when available.
Finished Lesson 8.8?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.