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.
Cheatsheet for Lesson 7.5
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: Ifdirectionhas length one,tis distance in world units.m:mis the vector from the sphere's centre to the ray's origin.b:bis the negative of how far along the normalized ray the centre projects: the closest point is att = -bwhen 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/ediis 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
Use an existing UI clue
Section titled “Use an existing UI clue”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.

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

A crosshair is a ray through the camera
Section titled “A crosshair is a ray through the camera”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 ≥ 0If 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:
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 - cb = dot(m, d)c_term = dot(m, m) - r²discriminant = b² - c_termEach 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 hitdiscriminant = 0 the ray grazes the surface -> one touch pointdiscriminant > 0 the ray passes through it -> enters and exitsThat 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.
Picking: the same ray, from the cursor
Section titled “Picking: the same ray, from the cursor”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.5ndc_y = 1 − 2 × 150 ÷ 600 = 0.5Step 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.6667y = 0.5 × 1 = 0.5z = 1length = √(0.6667² + 0.5² + 1²) = √1.6944 = 1.3017normalized = (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 = 6y range: t from 1.5 ÷ 0.5 = 3 to 3 ÷ 0.5 = 6enters at the later start = max(4, 3) = 4leaves at the earlier end = min(6, 6) = 64 ≤ 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.
Collect positive and negative cases
Section titled “Collect positive and negative cases”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.
Model the crosshair result
Section titled “Model the crosshair result”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.
Decide without acting
Section titled “Decide without acting”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.
Reproduce the actual AssaultCube hook
Section titled “Reproduce the actual AssaultCube hook”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 eax0x0040_ADA2 ... ; resume here after the five-byte call0x0040_ADA6 conditional jump used by the nametag pathWhen 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
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
}Always send the final mouse-up event when disabling or unloading the feature. Otherwise Windows can keep the button logically held down.

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 videoAdd debounce
Section titled “Add debounce”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 }}Verify the source
Section titled “Verify the source”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.

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.
Validate the target result
Before acting on the game's target-under-crosshair result, what should a tool check?
Selection results are time-sensitive game state. Validate identity, lifetime, and local rules immediately before using them.
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.
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.