Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 8.3 Listening edition

Detours and Reversible Patches

Understand how a detour redirects execution, preserves displaced instructions, verifies target bytes, and restores the original path.

About 44 minutes

Section: Typed code manages the machine-code boundary.

A hook changes running machine code, so the final write is necessarily low-level. Typed code keeps the surrounding lifecycle explicit:

exact ownership of original bytes;

checked address math;

supported-version checks;

a single apply/restore state;

cleanup in Drop;

clear errors.

The Wesnoth debugger-only detour introduced in Lesson 2.10

Section: Define the patch’s safety model first.

In an offline game, the main danger is self-inflicted: patching the wrong build, splitting an instruction, losing restoration bytes, or unloading while the cave still runs. Write the safety model before writing the jump:

Lesson table

This is a game-hacking safety contract, not an enterprise threat model. Its job is to prevent an experiment from turning uncertainty into a write.

The trusted core should stay small: target-profile selection, instruction decoding, the patch writer, and the restoration record. A menu theme, log formatter, or hotkey label should not be able to bypass byte verification. When more code can validate a patch, there are more places where a bug can create a wrong write.

Section: Represent a patch as data.

Before writing to a process, describe exactly which address will change, which bytes must already be there, and which same-length bytes will replace them. A PatchPlan is only that description: constructing or validating it cannot modify the game. Equal lengths also mean the planned overwrite cannot silently spill beyond the verified instruction span:

Alternative: A patch plan is a work order, not the work itself: it describes the exact starting bytes and replacement before touching memory.

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Restoration needs the bytes that were replaced.

The planner performs no raw memory access. A disassembler should determine the whole-instruction span; never choose it by counting five arbitrary bytes.

Section: Verify before writing.

The plan’s expected bytes are a build identity check at the precise hook site. Copy that many live bytes, compare them before any write, and return an error if the running executable disagrees with the chosen profile:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Do not turn a mismatch into a force option:

The original helper writes the replacement immediately, then runs the feature. It neither checks the live instructions nor retains an owner that can restore them when the feature stops or returns an error. This is a teaching wrapper excerpt: AppliedPatch::apply must capture verified original bytes and install through a code-writing boundary; the buildable in-process lab below uses LocalPatch for that role.

Before: write a detour with no owned restoration

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Validate before writing, then hold an AppliedPatch owner for the whole enabled period. Explicit restoration reports a failure; dropping the owner provides a final best-effort attempt if an earlier operation fails.

Improved code

Code example. The code’s comments explain: Keep `applied` alive for the entire period when the detour is enabled.

Show the diff

Code example. The code’s comments explain: Keep `applied` alive for the entire period when the detour is enabled.

Takeaway: reject mismatched instructions and keep their original bytes owned until restoration finishes.

The expected bytes are a tiny build fingerprint. If they differ, the planned jump may split a different instruction or replay the wrong behavior. Equal lengths keep the resume address stable. Finally, the AppliedPatch value represents the fact that the process is currently changed; when that value is restored or dropped, it still has the original bytes needed to undo the patch.

If bytes differ, stop. Do not add a “force” switch to a beginner tool.

Section: Own the applied state.

Once the replacement is installed, cleanup needs the original bytes even if the caller returns early. AppliedPatch owns that restoration record. An explicit restore can report an error, while Drop makes one final best-effort attempt when the guard leaves scope:

Code example. The code’s comments explain: 🔁 Restore bytes before clearing the flag. If the write fails, `active` stays true so Drop can make one final best-effort attempt. 🧹 `restore` is idempotent: explicit cleanup sets active=false, so this fallback cannot accidentally write the original bytes twice.

Drop is a final safety net, not the only cleanup path. Call restore explicitly and report failures while the program can still react.

Narration: A cleanup error can otherwise disappear during destruction. Explicit restoration gives the tool a chance to report which code remains changed.

Section: Calculate jumps carefully.

A 5-byte relative jump stores a signed 32-bit displacement measured from the end of the jump: rel32 equals to minus (from + 5), where from is the jump’s own address and to is the cave’s.

This helper turns that formula into five instruction bytes. It checks that the destination fits the jump’s signed 32-bit displacement instead of truncating an out-of-range address.

Code example. The code’s comments explain: 🧭 The C P U, central processing unit, measures a near jump from the address *after* its five bytes. 📏 Use a wider signed type for subtraction so targets below `from` work without unsigned wraparound; the final conversion enforces rel32 range.

If the destination is too far away, rel32 returns None. Do not truncate the number.

rel32 and the six hook bytes.

Section: Architecture matters.

The target, D L L, dynamic link library,, pointer size, instruction decoder, and calling convention must agree: a 32-bit D L L loads only into a 32-bit process, and a 64-bit D L L only into a 64-bit one. Register roles, stack-slot widths, and alignment also differ, and Lesson 8.10 shows how a displaced instruction-pointer-relative operand changes meaning when it is copied. This is why copying a detour from a video or another game version is unreliable.

Section: A safer milestone.

Before any live patch, test:

rel32 with forward and backward jumps;

overflow rejection;

byte verification with a fake memory buffer;

applying and restoring in a practice process you wrote;

repeated enable/disable cycles.

After every restoration test, read the hook span again and compare it with the captured original bytes. “The feature looks disabled” does not show that the machine code returned to its starting state; comparing the bytes does.

In brief: Verify restoration by reading the original instruction span again; a disabled-looking feature is not enough evidence.

The first end-to-end target should be a tiny program with one known function.

Section: Apply it to Wesnoth 1.14.9.

After the practice target works, reproduce the original Wesnoth terrain-description hook:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

At this value, the supported 1.14.9 profile expects these exact six bytes:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

They contain the instructions represented as:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Labelled memory cells

The replacement has to fit in that same six-byte span:

Labelled memory cells

Section: Trace the jump and saved C P U state.

Suppose the D L L placed terrain_cave at this value. Then:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Labelled memory cells

Two things follow from that. Because the value is relative, the same five bytes mean different destinations at different addresses, so a working jump cannot be copied to another location and still land anywhere sensible. And because the value is signed, jumping backward is perfectly ordinary: a cave at a lower address simply produces a negative displacement.

The C P U therefore goes to our D L L instead. Our cave borrows control, preserves the interrupted computation, changes gold, performs the behavior we removed, and gives control back at this value. The original function never returns to the hook address, so there is no loop.

This is a tiny example of control-flow redirection, a computer-science idea used by debuggers, profilers, hot-patching systems, instrumentation tools, and game hooks.

The cave interrupts a function in the middle of its work. That function had values sitting in registers and a comparison result sitting in the flags, and it expects to find all of them untouched when execution resumes. Everything the cave does — including the ordinary Rust inside cave_body — is free to overwrite them. pushad copies all eight general-purpose registers onto the stack and pushfd copies the flags; popfd and popad restore them in the opposite order, so the interrupted function never notices that anything happened.

Labelled memory cells

There is no universal “save every register” rule. pushfd/pushad is easy to understand for this small 32-bit teaching cave. In a performance-sensitive hook, inspect which values are live and preserve exactly what the inserted code may change. The stack must also be balanced: every push needs a matching pop, and the called function’s convention must agree with the cave.

The cave now needs a complete owner: one function changes the gold field, one preserves and restores the interrupted C P U state, and one installs a verified, reversible jump.

This is the actual cave from the build-checked Windows project:

Code example. The code’s comments explain: / Follow Wesnoth's real pointer path and change the live gold field. / # Safety / Run only inside the documented 32-bit Wesnoth build with a local match open. 🛡️ SAFETY: the supported-game profile identifies this pointer root. 🛡️ SAFETY: the verified player record stores a 32-bit side pointer here. 🛡️ SAFETY: the current side record stores its four-byte gold value at +4. ⚠️ A naked assembly function cannot return Result. A missing match simply leaves gold unchanged; startup already verified the game executable. 📦 Save the flags and eight general-purpose 32-bit registers. 🛡️ The helper performs the pointer checks and gold write. 🔁 Put the interrupted thread back exactly as we found it. ✅ Replay the behavior of all instructions replaced at the hook. 🧭 Continue at the first untouched Wesnoth instruction. 🧹 LocalPatch refuses a different build, makes the code page writable, writes all six bytes, flushes the instruction cache, restores the page protection, and owns ORIGINAL so Drop can restore it later.

Here is the important difference between “the address worked once” and a hook whose assumptions are visible:

The original helper follows the lab’s local player and side pointers, then writes gold using unchecked integer addition. These constants describe one verified offline build. This is unsafe code: the caller must guarantee that every addressed field stays live, initialized, aligned and accessible for the entire operation.

Before: follow unchecked local object addresses

Code example. The code’s comments explain: / # Safety / Call only inside the verified 32-bit build while these local fields remain / initialized, live, aligned and accessible. Serialize this operation with / game updates and guarantee exclusive writable access to the gold field. SAFETY: the caller guarantees the root slot is live and aligned. SAFETY: the caller must guarantee this computed local slot is valid. SAFETY: the caller must guarantee the local gold field remains writable.

Checked arithmetic and null checks reject obvious invalid addresses before access. They do not prove pointer validity, prevent concurrent game writes or make volatile access safe; the caller’s lifetime and build guarantees remain necessary.

Improved code

Code example. The code’s comments explain: / # Safety / Call only inside the verified 32-bit build while these local fields remain / initialized, live, aligned and accessible. Serialize this operation with / game updates and guarantee exclusive writable access to the gold field. SAFETY: the caller guarantees the root slot is live and aligned. SAFETY: the caller guarantees this local slot remains live and aligned. SAFETY: the caller guarantees exclusive writable access to this live field.

Show the diff

Code example. The code’s comments explain: / # Safety / Call only inside the verified 32-bit build while these local fields remain / initialized, live, aligned and accessible. Serialize this operation with / game updates and guarantee exclusive writable access to the gold field. SAFETY: the caller guarantees the root slot is live and aligned. SAFETY: the caller must guarantee this computed local slot is valid. SAFETY: the caller guarantees this local slot remains live and aligned. SAFETY: the caller must guarantee the local gold field remains writable. SAFETY: the caller guarantees exclusive writable access to this live field.

Takeaway: arithmetic checks supplement an unsafe memory contract; they cannot replace it.

checked_add prevents a bad pointer plus offset from wrapping around to a believable low address. The null check turns “no active side” into an ordinary error. Volatile access tells the compiler that this memory can change outside the compiler’s view, but it does not make an invalid pointer safe. That is why the build check, pointer path, and narrow unsafe boundary are still required. LocalPatch then owns the separate responsibility of restoring the original instructions.

The complete cave above is buildable source. The definitions of LocalPatch and near_jump, including VirtualProtect, FlushInstructionCache, and automatic restoration, are part of the project. The exact cave above lives in game_hooks.rs.

Build it as 32-bit because the naked function uses x86 registers and the target stores four-byte pointers:

Terminal window

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Inject the resulting D L L with the course injector:

Terminal window

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Press F1 to install the cave. Right-click a tile and choose Terrain Description. Gold becomes 888 before the description is shown. Press F1 again to restore all six original bytes; repeat the menu action and gold should stay unchanged. Press End to stop the worker and restore any remaining patch.

Section: Reuse the same detour for a live text field.

The first hook changes a game value at a terrain-description event. The next case starts from a different observation: the game passes a live text pointer to its print function. The patch manager, checked relative jump, saved bytes, and verified restoration procedure stay the same; the cave changes what it reads and writes.

Now use the real target. On the Den of Onis map, right-click a Ford tile and open Terrain Description. Search for a distinctive part of the description, then change the first letter of each candidate until the displayed text changes. In the book’s run, the live string began at this value.

Set a read breakpoint on one byte of that string and open the description again. Step out until you reach this value. Here e d x leads to the text object, and the call at this value prints it. The original print target is this value; execution continues at this value.

For the debugger-only experiment, place a cave at this value, replace the five-byte call at this value with a relative jump, and assemble:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

This deliberately changes the first byte each time the description is drawn: A becomes B, then C, and so on. It is visible proof that the cave receives the live text pointer and still replays the original call.

The installer reuses the checked near_jump helper from the first hook. Before writing, verify that the five original bytes still encode the expected call. When disabling the hack, restore those exact saved bytes. Close and reopen the description between tests so each character change is obvious.

Section: Turn the text cave into the complete second-player gold patch.

The finished hook keeps the same five-byte patch point, original print call, and resume address. Instead of incrementing one letter, it follows Wesnoth’s verified in-process pointer chain and writes the second player’s gold as decimal text:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

The helper checks every pointer, caps the displayed value at 999, converts it into at most three A S C I I digits, and copies exactly three bytes into the existing buffer. The naked cave supplies the live e d x, preserves flags and registers, replays the original print call, and resumes the game:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Inject the 32-bit D L L into wesnoth.exe, open the terrain description during the two-player local scenario, and press F2. The first three characters change to the other player’s live gold. Press F2 again or End to restore the original call bytes.

The complete helper, decimal conversion, cave, and installer are in wesnoth_hooks.rs. The hotkey and patch lifetime are in dll.rs.

The first hook redirects a verified instruction span; the second redirects a verified call. Both restore exactly what they displaced. Lesson 8.4 changes a different boundary: an imported function pointer rather than instruction bytes.

To transfer either example to another build, start from the observation that led to the hook: which action reaches the instruction, and which register or pointer holds the value at that moment? Then re-measure the complete instruction span, expected bytes, cave reachability, displaced behavior, resume address, and restoration result in that build. The Wesnoth addresses above are evidence for one 32-bit version, not reusable offsets.