Skip to content
Lesson 7.5Camera Geometry and Rendering APIs

Camera Rays, Collisions, and Crosshairs

See how a camera ray, collision query, and occlusion test become a game's crosshair target, and how the same ray from a mouse cursor picks what is under it.

Lesson 7.5
Read or listenPrint chapter or book
About 34 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 7.5Key ideas, reference and questions

A one-page summary of “Camera Rays, Collisions, and Crosshairs” you can keep beside your notes. Plain Markdown.

Lesson 7.5 — Camera Rays, Collisions, and Crosshairs: cheatsheet

  • Remember: No selected entity is an ordinary result. Keep it distinct from a failed read or an invalid observation.
  • Also useful: A cursor is a movable crosshair: first turn its screen position into a camera ray, then apply the same nearest-valid-hit reasoning.

Key terms

  • t: If direction has length one, t is distance in world units.
  • m: m is the vector from the sphere's centre to the ray's origin.
  • b: b is the negative of how far along the normalized ray the centre projects: the closest point is at t = -b when that value is nonnegative.
  • Picking: Picking is the question "which object is under the cursor?", and games use it to select units, press buttons, drag items, and click on the world.
  • edi: When the crosshair is over a player, eax/edi is non-zero.
  • edge detection: This comparison is edge detection.

Formulas and patterns

  • point(t) = origin + t × direction, where t ≥ 0

Review

  • Q: Before acting on the game's target-under-crosshair result, what should a tool check? A: That the identifier is valid for the current frame and still refers to an allowed target
  • Q: A ray is written as point(t) = origin + t × direction. When is t a distance in world units? A: When the direction has length one
Download cheatsheet (.md)

Use AssaultCube 1.2.0.2. Enable nametags, start a single-player deathmatch with eight bots, open the console with ~, and run idlebots 1. AssaultCube displays a name when the crosshair passes over a player, which means the game already computes a target-under-crosshair result.

AssaultCube showing a target name

AssaultCube showing a target's name.

Searching for the visible name and breaking on reads can lead from text back to the target-selection logic.

Searching for a player name

Searching memory for a player's name.

For a centered crosshair with no weapon offset, the geometric query begins at the camera position and travels along the camera’s forward direction. A ray is:

point(t) = origin + t × direction, where t ≥ 0

If direction has length one, t is distance in world units. If it is not normalized, t is merely a scale parameter. Keep that contract consistent when comparing hit distances.

This builder needs JavaScript. Here is the lesson’s worked example.

origin x + t × direction x: with the lesson’s numbers, 2 + 10 × 0.6, which gives 8. The ray starts at x = 2 and each unit of t moves it 0.6 in x, so after t = 10 it is at x = 8. With a direction of length one, an x of 0.6 pairs with a y of ±0.8 (0.6² + 0.8² = 1), so t really is a distance in world units.

#[derive(Clone, Copy, Debug)]
struct Ray {
origin: Vec3,
direction: Vec3, // invariant: finite and normalized
}
impl Ray {
fn point_at(self, distance: f32) -> Vec3 {
Vec3 {
x: self.origin.x + self.direction.x * distance,
y: self.origin.y + self.direction.y * distance,
z: self.origin.z + self.direction.z * distance,
}
}
}

Crosshairs away from screen center require unprojection: transform a near and far screen point back through the inverse view-projection matrix, divide each by its homogeneous w, then normalize the direction between them. A rendered weapon muzzle may not be the ray origin; many games aim from the camera but spawn a projectile from the weapon, then reconcile the two paths.

Ray casting is a nearest-valid-hit problem

Section titled “Ray casting is a nearest-valid-hit problem”

A collision system rarely tests every triangle directly. It commonly uses:

Camera ray
Broad phase: spatial tree / bounding volumes
Small candidate set
Narrow phase: shapes / triangles
Nearest accepted hit
Semantic filters: team, alive, material

The broad phase quickly rejects distant regions. The narrow phase computes more exact intersections. The query then chooses the smallest nonnegative hit parameter and filters hits by collision masks and excluded entities.

A sphere intersection illustrates the math. For sphere center c, radius r, ray origin o, and normalized direction d, substitute o + td into the sphere equation:

m = o - c
b = dot(m, d)
c_term = dot(m, m) - r²
discriminant = b² - c_term

Each symbol has a plain-English meaning worth holding on to. m is the vector from the sphere’s centre to the ray’s origin. b is the negative of how far along the normalized ray the centre projects: the closest point is at t = -b when that value is nonnegative. c_term compares the origin’s distance from the centre against the radius, so it is negative exactly when the ray starts inside the sphere.

The discriminant then answers one geometric question: how close does the ray pass to the centre, compared with the radius?

discriminant < 0 the ray passes wide of the sphere -> no hit
discriminant = 0 the ray grazes the surface -> one touch point
discriminant > 0 the ray passes through it -> enters and exits

That is why two roots appear when it is positive — one where the ray enters and one where it leaves. The nearer ray hit is t = -b - sqrt(discriminant), provided t ≥ 0. If the origin is inside the sphere, the nearer root may be negative and the farther root must be considered. Production engines use several shape types and acceleration structures, but this same “generate candidates, reject invalid roots, choose nearest” reasoning applies.

A mouse cursor is a crosshair that can move. Picking is the question “which object is under the cursor?”, and games use it to select units, press buttons, drag items, and click on the world. It works like the centred crosshair, with one extra step to find the ray.

Step 1: pixel to normalized coordinates. This reverses the conversion at the end of Lesson 7.1. On an 800 × 600 window, a cursor at pixel (600, 150) has

ndc_x = 2 × 600 ÷ 800 − 1 = 0.5
ndc_y = 1 − 2 × 150 ÷ 600 = 0.5

Step 2: build the ray. For a perspective camera at the origin looking along +z, with a 90° vertical field of view (tan 45° = 1) and aspect 800 ÷ 600 = 1.3333, the ray leaves the camera in the direction

x = 0.5 × 1.3333 × 1 = 0.6667
y = 0.5 × 1 = 0.5
z = 1
length = √(0.6667² + 0.5² + 1²) = √1.6944 = 1.3017
normalized = (0.5122, 0.3841, 0.7682)

A camera that is turned adds one more step, rotating that direction by the camera’s own axes (Lesson 7.1). An orthographic camera is simpler: every ray points straight forward, and the ray’s origin is the world point under the cursor, such as the (5, 3) that Lesson 7.1 found for the pixel (600, 180).

Step 3: test the candidates. The broad phase keeps the objects whose bounding shapes the ray might cross, and each is tested, nearest hit wins. For a box that lines up with the axes the test works one axis at a time. In a flat example, a ray starts at (0, 0) and heads in the direction (1, 0.5), and a box spans x from 4 to 6 and y from 1.5 to 3:

x range: t from 4 ÷ 1 = 4 to 6 ÷ 1 = 6
y range: t from 1.5 ÷ 0.5 = 3 to 3 ÷ 0.5 = 6
enters at the later start = max(4, 3) = 4
leaves at the earlier end = min(6, 6) = 6
4 ≤ 6, so the ray hits the box, first at t = 4, the point (4, 2)

Move the box up so that y spans 3.5 to 5 and the y range becomes t from 7 to 10, so the ray would have to enter at 7 but has already left at min(6, 10) = 6. Entering after leaving means a miss.

Two other methods are common:

  • Colour picking. Each object is drawn into an off-screen texture in its own flat colour that encodes its number. Object 300 is 300 = 0 × 65536 + 1 × 256 + 44, so its colour is (0, 1, 44). Reading the one pixel under the cursor and decoding the colour gives the object, already corrected for anything in front of it. It costs an extra pass and a read-back from the GPU.
  • Sprite picking. In 2D, convert the cursor to a world point as in Lesson 7.1 and test whether it falls inside each sprite’s rectangle, taking the topmost layer.

Dragging builds on the same query. When the button goes down, the game picks the object. While the mouse moves, it casts the ray again each frame and moves the object to where the ray meets a floor or a plane. When the button comes up, it drops the object. Many engines also have a debug view that draws the ray and the thing it hit, which is the quickest way to see why a click picked the wrong object.

Lesson 7.9 runs the opposite way, from a world point to a pixel. The “target under the crosshair” value this lesson hunts for is the result of this computation.

Rendering visibility and collision visibility differ

Section titled “Rendering visibility and collision visibility differ”

The renderer may omit a distant object, draw a transparent surface, or use a different level-of-detail mesh. A gameplay ray can still collide with its collision shape. The reverse can also occur: foliage may be visible but configured not to block shots.

Question Likely data source
what pixel is visible? depth/render buffers
what geometry blocks a shot? physics/collision query
which entity gets a nametag? UI selection result
which entity receives damage? weapon/gameplay logic

These results may agree most of the time without being the same variable.

Compare the value the query returns while aiming at:

  • empty space;
  • a wall;
  • yourself, if possible;
  • a teammate;
  • an opponent;
  • a dead player.

One non-zero value does not automatically mean “valid opponent.”

Also vary distance, partial cover, transparent surfaces, camera mode, and weapon. The purpose is to learn the query’s contract: maximum range, ignored object types, collision mask, and whether it returns the nearest hit or the first accepted entity.

The crosshair may point at empty space, a teammate, or an opponent. A single boolean such as enemy_under_cursor would hide which step failed. Record the selected entity, its relationship to the player, and the two properties that matter for a visible, living target:

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct EntityId(u32);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Relation {
SelfPlayer,
Teammate,
Opponent,
Unknown,
}
#[derive(Debug)]
struct CrosshairObservation {
target: Option<EntityId>,
relation: Relation,
alive: bool,
visible: bool,
}

Use Option<EntityId> because “nothing selected” is a normal state, not an error.

Once the observation has been named, the decision can be a small pure function. For an opponent that is both alive and visible, it returns a candidate entity ID; for every other combination it returns Hold. No input is sent to the game in this step:

#[derive(Debug, Eq, PartialEq)]
enum TriggerIntent {
Hold,
Candidate(EntityId),
}
fn decide_trigger(observation: &CrosshairObservation) -> TriggerIntent {
match (
observation.target,
observation.relation,
observation.alive,
observation.visible,
) {
(Some(id), Relation::Opponent, true, true) => TriggerIntent::Candidate(id),
_ => TriggerIntent::Hold,
}
}

This function does not press a button. It turns a CrosshairObservation into a testable intent. Keeping action separate makes accidental behavior less likely.

The visible field needs an exact definition. It might mean “the game’s existing crosshair query selected this entity,” “a collision ray reached its hitbox,” or “its projected point is inside the viewport.” Name the field after the check that actually produced it; those definitions are not interchangeable.

The ray, collision, and target-observation model above can be tested without injection. The pinned hook below applies the DLL lifecycle from Lesson 8.3 and the Windows input boundary from Lesson 6.9. Follow the target-specific evidence before writing to its memory.

Search Cheat Engine for one idle bot’s name. For each remaining text address, use Find out what accesses this address, look away, then look back at the bot. The useful address is accessed heavily only while the nametag is visible.

In x64dbg, the relevant 1.2.0.2 code is:

0x0040_AD9D call 0x0046_07C0 ; returns target-under-crosshair in eax
0x0040_ADA2 ... ; resume here after the five-byte call
0x0040_ADA6 conditional jump used by the nametag path

When the crosshair is over a player, eax/edi is non-zero. Looking at empty space produces zero. Replace the five-byte call at 0x0040AD9D with a verified detour, execute the original call inside the cave, then publish the result to a typed handler:

use std::sync::atomic::{AtomicBool, Ordering};
static TARGET_UNDER_CROSSHAIR: AtomicBool = AtomicBool::new(false);
extern "C" fn record_target(value: u32) {
TARGET_UNDER_CROSSHAIR.store(value != 0, Ordering::Release);
}
#[cfg(target_arch = "x86")]
#[unsafe(naked)]
unsafe extern "C" fn trigger_cave() {
core::arch::naked_asm!(
"call {original}",
"pushfd",
"pushad",
"push eax",
"call {record}",
"add esp, 4",
"popad",
"popfd",
"jmp {resume}",
original = const 0x0046_07C0,
record = sym record_target,
resume = const 0x0040_ADA2,
);
}

The worker thread turns only state transitions into mouse events: send MOUSEEVENTF_LEFTDOWN when the atomic changes from false to true and MOUSEEVENTF_LEFTUP when it changes back. With the DLL loaded in the offline bot match, moving the crosshair onto a bot should fire; moving away should release the button.

Here is the complete windows-crate input boundary used by that worker:

use std::mem::size_of;
use anyhow::Result;
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE,
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEINPUT,
SendInput,
};
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,
},
},
};
let structure_size = i32::try_from(size_of::<INPUT>())?;
// SAFETY: `input` is fully initialized and the size matches this INPUT type.
let sent = unsafe { SendInput(&[input], structure_size) };
anyhow::ensure!(sent == 1, "SendInput sent {sent} of 1 event");
Ok(())
}

The actual worker remembers whether it previously pressed the button:

let mut mouse_down = false;
while !stop.load(Ordering::Acquire) {
let wants_mouse_down = hook_is_enabled
&& TARGET_UNDER_CROSSHAIR.load(Ordering::Acquire);
if wants_mouse_down != mouse_down {
send_left_mouse(wants_mouse_down)?;
mouse_down = wants_mouse_down;
}
std::thread::sleep(std::time::Duration::from_millis(8));
}
if mouse_down {
send_left_mouse(false)?;
}

This comparison is edge detection. Holding a target does not send thousands of repeated down events; only a false-to-true or true-to-false change produces input. The build-checked cave, original call bytes, detour installer, input function, and DLL worker are in game_hooks.rs and dll.rs.

game_hooks.rs

windows-labs/src/windows_impl/ · Rust, 202 lines

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

windows-labs/src/windows_impl/ · Rust, 249 lines

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
}

Always send the final mouse-up event when disabling or unloading the feature. Otherwise Windows can keep the button logically held down.

Still image accompanying Crosshair-triggered firing in AssaultCube
Crosshair-triggered firing in AssaultCubeSilent demonstration · 4 seconds

The original trigger demonstration connects a target under the crosshair to firing. Watch when the target enters the crosshair; the implementation above turns changes in that result into mouse-down and mouse-up edges.

Download Crosshair-triggered firing in AssaultCube video

A target result may flicker for a frame. Require repeated agreement:

struct StableTarget {
last: Option<EntityId>,
matching_frames: u8,
}
impl StableTarget {
fn observe(&mut self, current: Option<EntityId>) -> bool {
if current.is_some() && current == self.last {
self.matching_frames = self.matching_frames.saturating_add(1);
} else {
self.last = current;
self.matching_frames = u8::from(current.is_some());
}
self.matching_frames >= 3
}
}

The name-rendering path may use a cached or display-only target. Confirm it updates at the expected time and matches the game’s own hit logic.

Code associated with the name display

Code associated with the name display.

Trace the value’s producer, not only its final display. Reusing an internal result is more reliable than recreating a ray test from incomplete data.

When tracing the producer, identify:

  • where the ray origin and direction come from;
  • the maximum query distance;
  • collision layers or masks;
  • ignored entities, especially the local player;
  • whether the result is a pointer, index, handle, or cached UI record;
  • when during the frame the result becomes valid;
  • whether a later gameplay check can reject the UI selection.

Failure patterns reveal the wrong assumption

Section titled “Failure patterns reveal the wrong assumption”

Failure patterns and what they suggest

Symptom Check first
target appears selected through thin walls collision mask or stale cached result
selection is offset while moving camera snapshot timing
center works but edge crosshair does not missing screen unprojection
nearest target is skipped ignored layers, dead state, or hitbox mismatch
result points to a reused entity handle generation/lifetime
flicker at silhouette edges ray precision, animation, or frame-to-frame pose changes

The implementation is tied to the offline bot-match state model. A multiplayer server may own hit and firing decisions, so a crosshair result read from the local client does not decide what the server accepts.

The lasting skill is finding an existing semantic result—“the object under the crosshair”—and representing its normal, missing, and invalid states cleanly.

Knowledge check

Validate the target result

Before acting on the game's target-under-crosshair result, what should a tool check?

Finished Lesson 7.5?

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.

Chapter 7: 11 lessons

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.