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

Preparing read-aloud controls…

Lesson 8.9 Listening edition

Tool Architecture, Commands, and Cleanup

Organize a local tool around owned snapshots, typed commands, feature lifecycles, version checks, and dependable cleanup.

About 26 minutes

Several small labs can work on their own and still fail when combined. The input thread changes settings while a worker is reading them. Two features install a patch at the same address. Closing a menu leaves a worker running inside a D L L, dynamic link library, that is about to unload. These failures come from the connections between components, even when each component’s main calculation is correct.

The D L L, hook, input, and menu lessons have supplied the pieces. This lesson gives each piece an owner and a place in the tool’s control flow. We will use a small monitor of a local player’s health and gold to express the design; the same separation of ownership, observations, and commands later supports camera snapshots and rendering features.

Section: Combining code exposes hidden problems.

Five prototypes can each work alone and still fail when combined. Common causes include:

each feature reads the same memory separately;

two features try to own the same patch;

global booleans get out of sync;

shutdown happens while a worker is still running;

one feature’s error crashes every other feature.

Adding conditions to each feature does not settle who owns shared resources. An explicit architecture does: it names the stages, the data crossing between them, and the component responsible for cleanup.

Section: Give each module one game-hacking job.

A module is cohesive when its pieces belong to the same job. Modules are coupled when one needs to know another’s internal details. In plain English: keep related work together, and keep the number of secret dependencies small.

One useful project split is:

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

Each file has one main reason to change. A new game build mostly changes target.rs. A redesigned menu mostly changes ui.rs. An additional check before restoring changed bytes belongs in apply.rs, not in every checkbox handler.

The main path should show the tool’s control flow clearly:

Lesson diagram

That outline is intentionally free of addresses and Windows calls. Those facts live behind smaller interfaces, so you can review the tool’s overall logic without mentally executing every unsafe operation.

Section: Observe once, then decide from owned values.

An observer captures the few fields this monitor needs. It resolves live game objects, copies the fields, checks their ranges, and returns ordinary Rust values. Other features receive this local value rather than borrowing pointers into the game:

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

capture_id identifies a tool observation; it does not claim that the game stopped updating while those two fields were read. If the target supplies a tick or generation counter, the observer can check it before and after the copy and retry when it changes. Without that mechanism, validate what was captured and describe its limits honestly.

A health display and a gold-change logger can now consume the same values. If each reads independently, they may describe different moments. Copying also separates lifetimes: once the snapshot exists, its fields remain valid even if the game destroys the object from which they came.

Section: Give every feature a lifecycle.

A value logger and a guarded patch do different work, but both must answer the same lifecycle questions: when is the feature enabled, what does it do with this observation, and how does it undo its own changes? The interface below makes those moments explicit:

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

Only patching or hooking features need setup or restoration in enable and disable. A pure observer still implements those required methods, but they can simply return Ok(()). Its useful work lives in update. Returning a Result lets the owner distinguish a successful toggle from a failed one.

Section: Model configuration with enums.

Three booleans named show_health, show_gold, and show_both could all become true after several clicks. Yet the display should select one mode. Store that choice as an enum alongside independent switches:

Alternative: A display enum is like a radio-button group: exactly one mode is selected. Independent features behave like separate checkboxes.

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

One DisplayMode value cannot hold two variants at once. The independent watch_health and log_changes switches may both be enabled because those activities can run together. The enum permits only one display mode at a time, while separate switches permit both activities. Each handler receives a state the types can represent without contradictory display modes.

Section: Route input as commands.

A key press should request a change, not let the input thread reach into a feature while the worker is using it. The input side sends a small command; the worker applies it to its own settings:

In brief: Let one worker change settings from queued commands, so input handlers cannot alter a feature halfway through its update.

Code example. The code’s comments explain: Called by the worker; true requests an orderly stop after this command.

CycleDisplay advances one variant at a time and wraps back to Off. Stop returns true, which the worker uses to leave its update loop and start cleanup. A stop command is a lifecycle decision; changing the label on a menu cannot stop a thread.

The input thread sends commands through a channel. The worker owns and changes the settings, avoiding shared mutable globals. If a command enables a patch, the worker sends an acknowledgement only after the patch owner reports success. The menu can then distinguish requested state from applied state.

Section: Contain feature failures.

A malformed gold observation may stop the gold display while a valid health display can continue. A closed target invalidates every feature. Decide whether an error belongs to one feature or to the shared observation before choosing what to stop.

Keep an enabled flag beside each feature. This update helper disables a failing feature, so the next pass cannot silently run it again:

Code example. The code’s comments explain: A cleanup failure is not safe to hide behind a disabled checkbox.

Box<dyn Feature> owns an implementation behind the common interface. The loop skips disabled slots. An update error is logged and the slot is disabled before restoration starts. If restoration succeeds, other features continue; if it fails? returns that error to the owner, which enters the tool’s shutdown path. The owner must retain resources needed to retry cleanup rather than unload code that may still be reachable through a hook.

Section: Shut down in reverse order.

Shutdown order: right and wrong.

Do not unload code while another thread may still execute it. Stop accepting new commands first, request worker shutdown, and wait for those workers to finish before restoring their patches and releasing handles. A restoration failure must remain visible; a vanished window is not proof that cleanup completed.

Narration: The window may disappear before a worker exits. Waiting for that worker keeps its final operations from reaching already-released code or handles.

Section: Keep version facts outside feature logic.

An offset is a fact about one supported game build, not a universal property of a health or gold feature. Put build identity and discovered locations together in a profile, so changing the supported build does not silently change feature logic:

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

The excerpt’s ModuleOffsets, Layouts, and Signatures are the project’s records for module-relative locations, field layouts, and expected instruction bytes. Their exact fields depend on the supported target. Keep those facts together instead of scattering them through display and input code.

Load one profile only after fingerprinting the executable. At every address that will be patched, compare the live bytes with the expected original instruction signature before enabling the feature. A changed build must produce a clear rejection, not a plausible-looking value from the wrong field.

Section: Reuse without surrendering judgment.

Reusing proven work is normal engineering. Rebuilding everything wastes time; adding a dependency for every five-line problem creates a different kind of fragility. Make the decision in this order:

Can the standard library or an existing project abstraction do it clearly?

Is there a maintained crate whose A P I, application programming interface,, license, and safety model fit?

Is there a small licensed example you can adapt and test?

If not, what is the smallest implementation you can own confidently?

When a dependency is large, isolate it behind a small interface. When copied functionality needs heavy modification in several places, either make one reusable project module or choose a library that already owns that problem. When configuration changes more often than low-level mechanics, a scripting layer such as Lua may be a better boundary than recompiling the whole tool.

Reading other projects shows how several tools model the same problem. Test their assumptions against your target, then write the smallest design that fits. When code is actually reused, preserve its required license notices and attribution; when only an idea is reused, explain it in your own structure and words.

Porting a small licensed tool can be good practice. Begin with its tests or observable behavior, reproduce one behavior at a time, and explain why your ownership and error model differ. This teaches the boundary instead of rewarding blind copy-and-paste.

Section: Test patch decisions without launching the game.

Do not make every test launch AssaultCube and call ReadProcessMemory. Put a small trait between the instruction-byte comparison and the Windows memory-read A P I. The real tool uses a Process-backed reader; a fast test uses bytes stored inside the test:

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

This test answers a game-hacking question: “Does the tool refuse to patch when the live instruction bytes do not match the supported build?” It does not need a running match to answer that. Separate Windows-only checks can verify handle ownership and byte copying. When one fails, you know whether the problem is the comparison with the supported build or the ReadProcessMemory call. 🧪

A one-shot memory scanner can print its matches directly. A long-running in-process menu should log the same fields for every state change: target_game, target_build, feature, address, expected_bytes, found_bytes, action, and restored. Those log entries show which patch was enabled and whether its original bytes were restored. Keep them separate from the visible menu so closing or redesigning the UI does not erase the log needed to debug a failed cleanup.

Section: Know when the combined tool is ready.

Combining separate features into one D L L is what the original course called the multihack. The useful engineering question is whether the combination preserves each feature’s resource owner and completes its shutdown and restoration.

A multifeature tool is ready when:

starting it twice does not double-hook;

every feature can be toggled repeatedly;

disabling restores its changes;

target closure stops the tool cleanly;

unknown versions are rejected;

one feature error does not corrupt the rest;

the offline target behaves normally after exit.

The next hook lesson examines the live control-flow changes that this owner must verify and restore. Chapter 7 then applies the same architecture to rendering: the camera, player positions, and viewport must belong to the same captured update, and every draw must leave the game’s graphics state intact.