Skip to content
Lesson 4.7Game State and Bounded Decisions

From Coordinates to Target Selection

Trace player and enemy positions in offline Flare, then choose actions from validated coordinates rather than fixed timings.

Lesson 4.7
Read or listenPrint chapter or book
About 21 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 4.7Key ideas, reference and questions

A one-page summary of β€œFrom Coordinates to Target Selection” you can keep beside your notes. Plain Markdown.

Lesson 4.7 β€” From Coordinates to Target Selection: cheatsheet

  • Remember: Treat a position as an observation you refresh, not a fact you keep. Choose a target from fresh, validated numbers every time.
  • Also useful: Picture drawing a circle around the player and growing it until it first touches an enemy. That enemy is the nearest one.

Key terms

  • Stop: An invalid player position means Stop; no valid enemy means Wait; otherwise the nearest enemy becomes MoveTo(position).
  • Wait: An invalid player position means Stop; no valid enemy means Wait; otherwise the nearest enemy becomes MoveTo(position).

Formulas and patterns

  • observe -> estimate state -> choose an intent -> act -> observe again

Review

  • Q: Why should a coordinate bot first copy game data into a validated snapshot? A: Decision logic can work from one bounded, internally consistent model
  • Q: When searching memory for a character's position, why move in just one direction at a time at first? A: A single-axis change shows which candidates follow that axis
Download cheatsheet (.md)

Coordinates give a position meaning, and subtracting two positions gives the direction and distance between them. Now we can use those ideas to choose an action in a moving game. The earlier Wyrmsun macro reacted to a discrete result: a unit was recruited or it was not. In Flare, an enemy keeps moving. A fixed instruction such as β€œhold right for one second” can be correct at the start and wrong a moment later. Position becomes an observation the bot must refresh before choosing its next action.

Use Flare 1.12 in an offline test area. Stand at an easy location, move only left and right, and scan for a changing floating-point value.

Searching for the player position

Searching Flare's memory for the player's position.

Then hold the first coordinate steady and move only up and down. Two independent experiments help distinguish x from y.

Narrowing the coordinate candidates

Candidates narrowed by moving along one axis only.

Validate each candidate at several positions. Teleporting a candidate a tiny distance in a disposable lab can confirm it, but restore the original value immediately.

Once the player position is known, inspect nearby fields and the code that reads them. Repeat the same process for:

  • one enemy position;
  • the current cursor or destination position;
  • an β€œis alive” or active flag.

Inspecting nearby position data

Nearby bytes: more position data and flags around the coordinate.

Do not assume every object shares the same layout. Confirm offsets separately.

Copy each confirmed position into a small Vec2 value, then group the player, enemies, and cursor in one WorldSnapshot. This is the input to decision logic: the bot works from local values captured together and does not keep a remote pointer alive while it chooses what to do.

#[derive(Clone, Copy, Debug)]
struct Vec2 {
x: f32,
y: f32,
}
#[derive(Debug)]
struct WorldSnapshot {
player: Vec2,
enemies: Vec<Vec2>,
cursor: Vec2,
}

The bot should make decisions from owned snapshots. It should not hold long-lived raw pointers into game memory.

A useful bot repeats four jobs:

observe -> estimate state -> choose an intent -> act -> observe again

The action does not prove success. Movement may be blocked, the target may disappear, or the snapshot may already be stale. After every request, wait for a specific observation that confirms or rejects it before choosing the next action.

Keep time in the model. Store when the snapshot was captured, when the action was requested, and how long confirmation remains valid. A perfect position from half a second ago can be worse than a slightly noisy current position.

Reject impossible floats:

impl Vec2 {
fn is_reasonable(self) -> bool {
self.x.is_finite()
&& self.y.is_finite()
&& self.x.abs() < 100_000.0
&& self.y.abs() < 100_000.0
}
fn distance_to(self, other: Self) -> f32 {
let dx = other.x - self.x;
let dy = other.y - self.y;
dx.hypot(dy)
}
}

NaN and infinity can poison comparisons, so is_finite matters.

Range checks prove only that a coordinate is plausible, not that it belongs to the intended object. Combine them with identity, active/alive flags, expected movement continuity, and repeated reads.

Given a player at (0, 0) and enemies at (3, 4) and (1, 0), the nearest valid target is (1, 0). The function below first discards implausible coordinates, then compares distance for the survivors. An empty or entirely invalid enemy list returns None rather than inventing a target.

fn nearest_enemy(player: Vec2, enemies: &[Vec2]) -> Option<Vec2> {
enemies.iter()
.copied()
.filter(|enemy| enemy.is_reasonable())
.min_by(|a, b| {
player.distance_to(*a)
.total_cmp(&player.distance_to(*b))
})
}

This decision code has no raw memory access and can be unit-tested with made-up coordinates.

Choosing the nearest valid enemy. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

let mut best = None;
let mut best_d = f32::MAX;
for e in enemies {
    if !e.is_reasonable() { continue; }
    let d = player.distance_to(e);
    if d < best_d { best = Some(e); best_d = d; }
}
best

The loop is over, so the answer is Some((1, 0)), the enemy 1 unit away. Only coordinates that passed the check were ever compared.

Final values: enemies = (3, 4) (1, 0); best = Some((1, 0)); best_d = 1; e = (1, 0); is_reasonable = true; d = 1.

Read every step of the lesson’s example
  1. Step 1, line 1. The player is at (0, 0) and the list holds 2 enemies: (3, 4), (1, 0). Nothing has been chosen yet, so best starts as None.

    enemies = (3, 4) (1, 0); best = None.

  2. Step 2, line 2. best_d holds the shortest distance seen so far. It starts as the largest possible f32 so that the first valid enemy always beats it.

    enemies = (3, 4) (1, 0); best = None; best_d = f32::MAX.

  3. Step 3, line 3. Take the next enemy, (3, 4).

    enemies = (3, 4) (1, 0); best = None; best_d = f32::MAX; e = (3, 4).

  4. Step 4, line 4. (3, 4) passes the plausibility check: both coordinates are finite and under 100000.

    enemies = (3, 4) (1, 0); best = None; best_d = f32::MAX; e = (3, 4); is_reasonable = true.

  5. Step 5, line 5. Distance from (0, 0): sqrt(3*3 + 4*4) = sqrt(25) = 5.

    enemies = (3, 4) (1, 0); best = None; best_d = f32::MAX; e = (3, 4); is_reasonable = true; d = 5.

  6. Step 6, line 6. 5 is smaller than the best distance so far, so (3, 4) becomes the best.

    enemies = (3, 4) (1, 0); best = Some((3, 4)); best_d = 5; e = (3, 4); is_reasonable = true; d = 5.

  7. Step 7, line 3. Take the next enemy, (1, 0).

    enemies = (3, 4) (1, 0); best = Some((3, 4)); best_d = 5; e = (1, 0); is_reasonable = true; d = 5.

  8. Step 8, line 4. (1, 0) passes the plausibility check: both coordinates are finite and under 100000.

    enemies = (3, 4) (1, 0); best = Some((3, 4)); best_d = 5; e = (1, 0); is_reasonable = true; d = 5.

  9. Step 9, line 5. Distance from (0, 0): sqrt(1*1 + 0*0) = sqrt(1) = 1.

    enemies = (3, 4) (1, 0); best = Some((3, 4)); best_d = 5; e = (1, 0); is_reasonable = true; d = 1.

  10. Step 10, line 6. 1 is smaller than the best distance so far, so (1, 0) becomes the best.

    enemies = (3, 4) (1, 0); best = Some((1, 0)); best_d = 1; e = (1, 0); is_reasonable = true; d = 1.

  11. Step 11, line 8. The loop is over, so the answer is Some((1, 0)), the enemy 1 unit away. Only coordinates that passed the check were ever compared.

    enemies = (3, 4) (1, 0); best = Some((1, 0)); best_d = 1; e = (1, 0); is_reasonable = true; d = 1.

decide turns a snapshot into an intent, never an input event. An invalid player position means Stop; no valid enemy means Wait; otherwise the nearest enemy becomes MoveTo(position). The platform-specific action layer can later decide how to carry out that intent.

#[derive(Debug)]
enum BotIntent {
Wait,
MoveTo(Vec2),
Stop(String),
}
fn decide(snapshot: &WorldSnapshot) -> BotIntent {
if !snapshot.player.is_reasonable() {
return BotIntent::Stop("invalid player position".into());
}
nearest_enemy(snapshot.player, &snapshot.enemies)
.map(BotIntent::MoveTo)
.unwrap_or(BotIntent::Wait)
}

The action layer may simulate a click in the local test or call a verified in-game function. The decision layer should not know how that happens.

Stop when:

  • the match or level is no longer active;
  • any base pointer becomes null;
  • coordinates are non-finite or out of bounds;
  • the player has not moved after several attempts;
  • the user requests stop;
  • the target version check fails.

A later coordinate-validation step

A later check that validates the coordinates before they are used.

Create snapshots in unit tests:

#[test]
fn chooses_the_nearest_enemy() {
let player = Vec2 { x: 0.0, y: 0.0 };
let enemies = [Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 3.0, y: 4.0 }];
assert_eq!(nearest_enemy(player, &enemies).unwrap().x, 3.0);
}

The most valuable bot codeβ€”geometry, state, timing, and failure handlingβ€”does not need unsafe and does not need the target running.

Windows VM

The snapshot, target selection, and tests above require no injected library. The following pinned Flare implementation uses the verified detour and DLL lifecycle developed in Lesson 8.3. Read it now as a worked mapping from three observed positions to an action, then run it after that boundary has been taught.

This is the original course’s farming bot: it walks toward a nearby enemy and attacks while a key is held.

The original 32-bit lab identified three calls. Their absolute addresses came from a run where flare.exe loaded at 0x00830000; use module-relative offsets in the injected DLL because the base can move.

Capture Observed call Hook offset Original target offset
mouse 0x0091CBC8 0xECBC8 0x54210
player 0x0083CAC4 0xCAC4 0x20840
shared entity loop 0x0089BA94 0x6BA94 0x6B180

At those sites, the useful fields are:

  • mouse X/Y: [ebp + 0x664] and [ebp + 0x668];
  • player X/Y: [ecx + 0x240] and [ecx + 0x244];
  • current entity X/Y: [ebx - 4] and [ebx].

Store the captured addresses as atomics or copy the values into an owned snapshot. Do not let the worker hold a pointer while a hook is changing it. The historical action loop used screen positions 490/560 for left/right and 270/330 for up/down, then sent a left-button input while the M key was held:

fn cursor_for_target(player: Vec2, enemy: Vec2) -> (i32, i32) {
let x = if enemy.x < player.x { 490 } else { 560 };
let y = if enemy.y > player.y { 270 } else { 330 };
(x, y)
}

These cursor numbers depend on the window size and UI scale. Confirm them by hovering the four attack directions and watching the captured mouse fields. In an enemy area, hold M: the player should run toward the moving enemy and attack when close. Releasing M must stop the bot immediately. On unload, stop the worker first and restore all three original five-byte calls.

strategy_hooks.rs

windows-labs/src/windows_impl/ Β· Rust, 415 lines

Show the codeHide the code
//! Complete Wyrmsun and Flare hooks ported from the original course projects.

#![cfg(target_arch = "x86")]

use std::{
    cell::UnsafeCell,
    ffi::c_void,
    ptr,
    sync::atomic::{AtomicBool, AtomicUsize, Ordering},
    thread,
    time::Duration,
};

use anyhow::{Context, Result};
use windows::{
    Win32::{
        System::LibraryLoader::GetModuleHandleW,
        UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_END},
    },
    core::w,
};

use super::{LocalPatch, near_call, near_jump, send_left_mouse};

fn module_base(name: windows::core::PCWSTR) -> Result<usize> {
    // SAFETY: the passed names are static zero-terminated strings.
    let module = unsafe { GetModuleHandleW(name) }?;
    Ok(module.0 as usize)
}

fn five_byte_detour(hook: usize, cave: usize) -> Result<[u8; 5]> {
    near_jump(hook, cave)
}

fn key_is_down(key: i32) -> bool {
    // SAFETY: Windows accepts any virtual-key integer. The high bit means down.
    unsafe { (GetAsyncKeyState(key) as u16 & 0x8000) != 0 }
}

fn end_pressed() -> bool {
    // SAFETY: VK_END is a valid virtual-key code. The low bit is an edge.
    unsafe { GetAsyncKeyState(VK_END.0.into()) & 1 != 0 }
}

// ---------------------------------------------------------------------------
// Flare 1.12 farming bot

static FLARE_MOUSE_CALL: AtomicUsize = AtomicUsize::new(0);
static FLARE_MOUSE_RETURN: AtomicUsize = AtomicUsize::new(0);
static FLARE_PLAYER_CALL: AtomicUsize = AtomicUsize::new(0);
static FLARE_PLAYER_RETURN: AtomicUsize = AtomicUsize::new(0);
static FLARE_LOOP_CALL: AtomicUsize = AtomicUsize::new(0);
static FLARE_LOOP_RETURN: AtomicUsize = AtomicUsize::new(0);

static FLARE_MOUSE_X: AtomicUsize = AtomicUsize::new(0);
static FLARE_MOUSE_Y: AtomicUsize = AtomicUsize::new(0);
static FLARE_PLAYER_X: AtomicUsize = AtomicUsize::new(0);
static FLARE_PLAYER_Y: AtomicUsize = AtomicUsize::new(0);
static FLARE_ENEMY_X: AtomicUsize = AtomicUsize::new(0);
static FLARE_ENEMY_Y: AtomicUsize = AtomicUsize::new(0);

extern "C" fn capture_flare_mouse(ebp: usize) {
    FLARE_MOUSE_X.store(ebp.saturating_add(0x664), Ordering::Release);
    FLARE_MOUSE_Y.store(ebp.saturating_add(0x668), Ordering::Release);
}

extern "C" fn capture_flare_player(ecx: usize) {
    FLARE_PLAYER_X.store(ecx.saturating_add(0x240), Ordering::Release);
    FLARE_PLAYER_Y.store(ecx.saturating_add(0x244), Ordering::Release);
}

extern "C" fn capture_flare_enemy(ebx: usize) {
    FLARE_ENEMY_X.store(ebx.saturating_sub(4), Ordering::Release);
    FLARE_ENEMY_Y.store(ebx, Ordering::Release);
}

#[unsafe(naked)]
unsafe extern "C" fn flare_mouse_cave() {
    core::arch::naked_asm!(
        "call dword ptr [{original}]",
        "pushfd",
        "pushad",
        "push ebp",
        "call {capture}",
        "add esp, 4",
        "popad",
        "popfd",
        "jmp dword ptr [{resume}]",
        original = sym FLARE_MOUSE_CALL,
        capture = sym capture_flare_mouse,
        resume = sym FLARE_MOUSE_RETURN,
    );
}

#[unsafe(naked)]
unsafe extern "C" fn flare_player_cave() {
    core::arch::naked_asm!(
        "pushfd",
        "pushad",
        "push ecx",
        "call {capture}",
        "add esp, 4",
        "popad",
        "popfd",
        "call dword ptr [{original}]",
        "jmp dword ptr [{resume}]",
        original = sym FLARE_PLAYER_CALL,
        capture = sym capture_flare_player,
        resume = sym FLARE_PLAYER_RETURN,
    );
}

#[unsafe(naked)]
unsafe extern "C" fn flare_loop_cave() {
    core::arch::naked_asm!(
        "pushfd",
        "pushad",
        "push ebx",
        "call {capture}",
        "add esp, 4",
        "popad",
        "popfd",
        "call dword ptr [{original}]",
        "jmp dword ptr [{resume}]",
        original = sym FLARE_LOOP_CALL,
        capture = sym capture_flare_enemy,
        resume = sym FLARE_LOOP_RETURN,
    );
}

pub fn install_flare_hooks() -> Result<Vec<LocalPatch>> {
    let base = module_base(w!("flare.exe"))?;
    let profiles = [
        (
            0x0E_CBC8,
            0x05_4210,
            flare_mouse_cave as *const () as usize,
            &FLARE_MOUSE_CALL,
            &FLARE_MOUSE_RETURN,
        ),
        (
            0x00_CAC4,
            0x02_0840,
            flare_player_cave as *const () as usize,
            &FLARE_PLAYER_CALL,
            &FLARE_PLAYER_RETURN,
        ),
        (
            0x06_BA94,
            0x06_B180,
            flare_loop_cave as *const () as usize,
            &FLARE_LOOP_CALL,
            &FLARE_LOOP_RETURN,
        ),
    ];

    let mut patches = Vec::with_capacity(profiles.len());
    for (hook_offset, call_offset, cave, call_slot, return_slot) in profiles {
        let hook = base
            .checked_add(hook_offset)
            .context("Flare hook overflowed")?;
        let target = base
            .checked_add(call_offset)
            .context("Flare call overflowed")?;
        call_slot.store(target, Ordering::Release);
        return_slot.store(hook + 5, Ordering::Release);

        let expected = near_call(hook, target)?;
        let replacement = five_byte_detour(hook, cave)?;
        patches.push(LocalPatch::apply(hook, &expected, &replacement)?);
    }
    Ok(patches)
}

pub fn run_flare_farming_bot(stop: &AtomicBool) -> Result<()> {
    let patches = install_flare_hooks()?;
    let mut mouse_down = false;

    while !stop.load(Ordering::Acquire) && !end_pressed() {
        let mouse_x = FLARE_MOUSE_X.load(Ordering::Acquire);
        let mouse_y = FLARE_MOUSE_Y.load(Ordering::Acquire);
        let player_x = FLARE_PLAYER_X.load(Ordering::Acquire);
        let player_y = FLARE_PLAYER_Y.load(Ordering::Acquire);
        let enemy_x = FLARE_ENEMY_X.load(Ordering::Acquire);
        let enemy_y = FLARE_ENEMY_Y.load(Ordering::Acquire);
        let ready = [mouse_x, mouse_y, player_x, player_y, enemy_x, enemy_y]
            .iter()
            .all(|address| *address != 0)
            && player_x != enemy_x
            && player_y != enemy_y;
        let active = ready && key_is_down(b'M' as i32);

        if active {
            // SAFETY: all six addresses came from the three verified live hooks.
            let (px, py, ex, ey) = unsafe {
                (
                    ptr::read_volatile(player_x as *const f32),
                    ptr::read_volatile(player_y as *const f32),
                    ptr::read_volatile(enemy_x as *const f32),
                    ptr::read_volatile(enemy_y as *const f32),
                )
            };
            anyhow::ensure!(
                [px, py, ex, ey].iter().all(|value| value.is_finite()),
                "Flare produced a non-finite coordinate"
            );
            let target_x = if ex < px { 490_u32 } else { 560_u32 };
            let target_y = if ey < py { 270_u32 } else { 330_u32 };
            // SAFETY: the mouse hook identified two live u32 fields.
            unsafe {
                ptr::write_volatile(mouse_x as *mut u32, target_x);
                ptr::write_volatile(mouse_y as *mut u32, target_y);
            }
        }

        if active != mouse_down {
            send_left_mouse(active)?;
            mouse_down = active;
        }
        thread::sleep(Duration::from_millis(1));
    }

    if mouse_down {
        send_left_mouse(false)?;
    }
    drop(patches); // restore all three calls after the worker stops using them
    Ok(())
}

// ---------------------------------------------------------------------------
// Wyrmsun 5.0.1 macro bot

const WORKER_RECORD_SIZE: usize = 0x110;
static WYRM_BASE: AtomicUsize = AtomicUsize::new(0);
static WYRM_RECRUIT_CALL: AtomicUsize = AtomicUsize::new(0);
static WYRM_RECRUIT_RETURN: AtomicUsize = AtomicUsize::new(0);
static WYRM_LOOP_CALL: AtomicUsize = AtomicUsize::new(0);
static WYRM_LOOP_RETURN: AtomicUsize = AtomicUsize::new(0);
static WYRM_OUTER_POINTER: AtomicUsize = AtomicUsize::new(0);
static WYRM_UNIT_RECORD: AtomicUsize = AtomicUsize::new(0);
static WYRM_CAPTURED: AtomicBool = AtomicBool::new(false);

struct SharedWorkerBuffer(UnsafeCell<[u8; WORKER_RECORD_SIZE]>);
// SAFETY: Wyrmsun's two hook sites run on its main game thread. The captured
// flag uses release/acquire ordering before any consumer reads the bytes.
unsafe impl Sync for SharedWorkerBuffer {}
static WYRM_WORKER_BYTES: SharedWorkerBuffer =
    SharedWorkerBuffer(UnsafeCell::new([0_u8; WORKER_RECORD_SIZE]));

extern "C" fn capture_wyrmsun_worker(outer_pointer: usize) {
    if outer_pointer == 0 {
        return;
    }
    // SAFETY: the verified recruit hook supplies a pointer to the outer pointer.
    let record = unsafe { ptr::read_volatile(outer_pointer as *const u32) } as usize;
    if record == 0 {
        return;
    }
    // SAFETY: the original course trace established a 0x110-byte worker record.
    unsafe {
        ptr::copy_nonoverlapping(
            record as *const u8,
            (*WYRM_WORKER_BYTES.0.get()).as_mut_ptr(),
            WORKER_RECORD_SIZE,
        );
    }
    WYRM_OUTER_POINTER.store(outer_pointer, Ordering::Release);
    WYRM_UNIT_RECORD.store(record, Ordering::Release);
    WYRM_CAPTURED.store(true, Ordering::Release);
}

unsafe fn read_pointer32(address: usize) -> Option<usize> {
    if address == 0 {
        return None;
    }
    // SAFETY: caller supplies one link from the verified in-process chain.
    let value = unsafe { ptr::read_volatile(address as *const u32) } as usize;
    (value != 0).then_some(value)
}

extern "C" fn maybe_recruit_wyrmsun_worker() {
    if !WYRM_CAPTURED.load(Ordering::Acquire) {
        return;
    }
    // SAFETY: this is the verified Wyrmsun module-relative gold-root slot; the
    // helper turns an ordinary null pointer into `None`.
    let Some(mut pointer) =
        (unsafe { read_pointer32(WYRM_BASE.load(Ordering::Acquire).saturating_add(0x61_A504)) })
    else {
        return;
    };
    for offset in [0x78_usize, 0x4, 0x8, 0x4, 0x0] {
        let Some(next_address) = pointer.checked_add(offset) else {
            return;
        };
        // SAFETY: every address comes from the previous non-null link plus one
        // verified offset in the Wyrmsun 5.0.1 pointer chain.
        let Some(next) = (unsafe { read_pointer32(next_address) }) else {
            return;
        };
        pointer = next;
    }
    let Some(gold_address) = pointer.checked_add(0x14) else {
        return;
    };
    // SAFETY: the verified chain ends at the live four-byte gold field.
    let gold = unsafe { ptr::read_volatile(gold_address as *const u32) };
    if gold <= 3000 {
        return;
    }

    let record = WYRM_UNIT_RECORD.load(Ordering::Acquire);
    let outer = WYRM_OUTER_POINTER.load(Ordering::Acquire);
    let function = WYRM_RECRUIT_CALL.load(Ordering::Acquire);
    if record == 0 || outer == 0 || function == 0 {
        return;
    }
    // SAFETY: the captured destination remains the record used by the game's
    // recruit path; both hooks run on the game thread.
    unsafe {
        ptr::copy_nonoverlapping(
            (*WYRM_WORKER_BYTES.0.get()).as_ptr(),
            record as *mut u8,
            WORKER_RECORD_SIZE,
        );
    }

    // The original function receives `outer` in ECX and again as its stack
    // argument. Rust's x86 thiscall ABI expresses that exact call shape.
    type RecruitFunction = unsafe extern "thiscall" fn(*mut c_void, *mut c_void);
    // SAFETY: this address is the verified wyrmsun.exe + 0x2CF7 function.
    let recruit: RecruitFunction = unsafe { std::mem::transmute(function) };
    // SAFETY: the captured outer pointer was observed in a legitimate call.
    unsafe { recruit(outer as *mut c_void, outer as *mut c_void) };
}

#[unsafe(naked)]
unsafe extern "C" fn wyrmsun_recruit_cave() {
    core::arch::naked_asm!(
        "pushfd",
        "pushad",
        "push ecx",
        "call {capture}",
        "add esp, 4",
        "popad",
        "popfd",
        // Replay all eight original bytes semantically.
        "push ecx",
        "mov ecx, esi",
        "call dword ptr [{original}]",
        "jmp dword ptr [{resume}]",
        capture = sym capture_wyrmsun_worker,
        original = sym WYRM_RECRUIT_CALL,
        resume = sym WYRM_RECRUIT_RETURN,
    );
}

#[unsafe(naked)]
unsafe extern "C" fn wyrmsun_loop_cave() {
    core::arch::naked_asm!(
        "pushfd",
        "pushad",
        "call {maybe_recruit}",
        "popad",
        "popfd",
        // Replay the five-byte call replaced at the loop hook.
        "call dword ptr [{original}]",
        "jmp dword ptr [{resume}]",
        maybe_recruit = sym maybe_recruit_wyrmsun_worker,
        original = sym WYRM_LOOP_CALL,
        resume = sym WYRM_LOOP_RETURN,
    );
}

pub fn install_wyrmsun_hooks() -> Result<Vec<LocalPatch>> {
    let base = module_base(w!("wyrmsun.exe"))?;
    WYRM_BASE.store(base, Ordering::Release);

    let recruit_hook = base + 0x22_3471;
    let recruit_function = base + 0x00_2CF7;
    WYRM_RECRUIT_CALL.store(recruit_function, Ordering::Release);
    WYRM_RECRUIT_RETURN.store(recruit_hook + 8, Ordering::Release);
    let original_call = near_call(recruit_hook + 3, recruit_function)?;
    let mut recruit_original = [0_u8; 8];
    recruit_original[..3].copy_from_slice(&[0x51, 0x8B, 0xCE]);
    recruit_original[3..].copy_from_slice(&original_call);
    let recruit_jump = near_jump(recruit_hook, wyrmsun_recruit_cave as *const () as usize)?;
    let mut recruit_detour = [0x90_u8; 8];
    recruit_detour[..5].copy_from_slice(&recruit_jump);
    let recruit_patch = LocalPatch::apply(recruit_hook, &recruit_original, &recruit_detour)?;

    let loop_hook = base + 0x38_5D34;
    let loop_function = base + 0x00_DBCA;
    WYRM_LOOP_CALL.store(loop_function, Ordering::Release);
    WYRM_LOOP_RETURN.store(loop_hook + 5, Ordering::Release);
    let loop_original = near_call(loop_hook, loop_function)?;
    let loop_detour = near_jump(loop_hook, wyrmsun_loop_cave as *const () as usize)?;
    let loop_patch = match LocalPatch::apply(loop_hook, &loop_original, &loop_detour) {
        Ok(patch) => patch,
        Err(error) => {
            drop(recruit_patch); // restore the first hook on partial installation
            return Err(error);
        }
    };
    Ok(vec![recruit_patch, loop_patch])
}

pub fn run_wyrmsun_macro(stop: &AtomicBool) -> Result<()> {
    let patches = install_wyrmsun_hooks()?;
    while !stop.load(Ordering::Acquire) && !end_pressed() {
        thread::sleep(Duration::from_millis(20));
    }
    drop(patches);
    Ok(())
}

The complete port is strategy_hooks.rs. Each site verifies its real E8 rel32 call before installing a detour. For example, the player cave captures ecx and then replays the original call:

#[unsafe(naked)]
unsafe extern "C" fn flare_player_cave() {
core::arch::naked_asm!(
"pushfd",
"pushad",
"push ecx",
"call {capture}",
"add esp, 4",
"popad",
"popfd",
"call dword ptr [{original}]",
"jmp dword ptr [{resume}]",
original = sym FLARE_PLAYER_CALL,
capture = sym capture_flare_player,
resume = sym FLARE_PLAYER_RETURN,
);
}

The worker rejects non-finite coordinates, writes the captured cursor fields, sends mouse input only when the active state changes, and always sends the final mouse-up before restoring all three calls. Inject into flare.exe, hold M near an enemy, and press End to stop.

Still image accompanying Following a target in Flare
Following a target in FlareSilent demonstration Β· 5 seconds

The original farming-bot recording shows the player moving toward an enemy and attacking. The motion is the visible result of repeatedly observing positions, choosing a direction, and sending a bounded input.

Download Following a target in Flare video
Knowledge check

Separate observation from decision

Why should a coordinate bot first copy game data into a validated snapshot?

Finished Lesson 4.7?

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 4: 10 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.