Author: A human being.
Co-author: An artificial intelligence.
Proofreader: A human being.
Formatter: A human being.
Documentation for: Volume 02: Halls of Torment — Script for Unified Multiplier
Reflection: What the Versions Taught
Eight versions across two days (not counting the time spent building version 1.0), and the same handful of ideas keep reappearing in different clothes. This chapter names them, pointing backward at specific versions rather than asserting abstractions.
"Guard it" versus "structurally can't happen"
This is the single most portable idea in the document, and it is worth stating in the general form before the specific one: when a failure mode cannot be detected, do not defend against it — remove the operation that exposes you to it.
The Volume 02 trace of that idea runs through two chapters. In Chapter 1 (Act I, Proof of Concept, v1.0), a guard-based defense was built and thoroughly instrumented: null checks, alignment checks, canonical-range checks, strict-range-reject counters, strict-unmapped-reject counters, hop captures, and a 100 ms validity poller. In Chapter 3 (Act III, the Cache Era, v1.2 through v1.4), that defense was proven not to work, with a guard-trip counter that read 0 after a repeat crash — the guards had not rejected the fatal pointer because the property that made it fatal, that its target had been freed, is not present in the pointer's bits. Then the dereference was deleted instead of improving the guard, and the resolution was moved into a Cheat Engine-side Lua timer, where a failure returns nil.
The general principle of Cheat Engine scripting is that a guard can only test locally observable properties. Null, alignment, range, and magnitude are locally observable. Liveness is not. If correctness depends on a property that cannot be observed from inside the hook, guarding is theater, and the counter attached to the guard will read 0 while the process dies.
The corollary is that "crash-proof, not guarded" is a claim that can actually be checked. Count the dereferences of foreign memory in a hook. If the count is zero, the bug class is gone. If it is not zero, the result is a probability, not a guarantee, and its magnitude is unknown.
Fixes discovered once get reapplied later, deliberately
The cache architecture was invented in v1.2 for the float script and retrofitted onto the integer script in v1.4. Two versions of lag, during which the integer half kept its in-hook pointer walks and kept occasionally crashing.
The lag is the interesting part. At the moment the fix was found, there was a proven mechanism and a working implementation, and the integer script's identical exposure was fully visible — the same static root, the same hub walks, the same class of teardown window. It was not generalized at that moment. The retrofit was prompted in v1.4 by a user report of seldom-but-recurring random crashes, not by an architectural review that noticed the other half of the codebase had the same hole.
Architectural lessons are not self-propagating. A fix arrives attached to the specific bug that motivated it, and detaching it — asking "what else in this codebase has this shape?" — is a separate deliberate act that nothing in the workflow prompts. Chapter 3 (Act III)'s throughline is "one bug, found twice, fixed once as a pattern," and the reason it had to be found twice is that after the first fix, nobody asked the second question.
All-or-nothing as a recurring safety idea
The same principle shows up at three scales.
In Chapter 3 (Act III, v1.4) it is four independent preflight checks with showMessage dialogs, one per hook site across two scripts, each aborting its own script's activation with a named failure. In Chapter 5 (Act V, Unification, v1.6) it becomes one gate: all four signatures verified in one {$lua} block before any memory is written anywhere, and the guarantee that a failed activation modified nothing. In Chapter 6 (Act VI, Publication, v1.7) it becomes a documented behavior — "no dialog appears = success" in the Quick Start, and all-or-nothing activation listed openly under Known Limitations rather than buried.
The design instinct underneath all three is that partial success is worse than clean failure. A table with three of four hooks installed works for some stats and not others, and presents as unreliable rather than as incomplete. The same instinct produced the disable-confirmation dialog in v1.2, for the opposite-facing reason: after the v1-era silent restore failure, silence could mean either outcome, so the success case was made loud. Never let silence be ambiguous. Pick which state is silent, document it, and make the other one speak.
UX maturity arriving in dedicated versions
Count how often "no script logic changed" appears in this document's own version summaries. v1.5 is a table-only release with byte-identical script bodies. v1.7 is a table-only release with a byte-identical script body. The v1.0 arrangement pass was explicitly verified byte-identical before and after. Large parts of v1.6 — the cascade redesign, the navigation overhaul, the section banners, the self-documentation — touched no assembly at all.
Roughly a third of the Volume 02 version history is presentation work in versions that changed nothing else, and that is a choice rather than a slow patch. Interface changes and architecture changes fail differently, are verified differently, and want different review attention. A version that only reorders the table can be validated by comparing bytes in the script body and performing a visual pass. A version that does both cannot, and when something breaks in it, two hypotheses are being debugged at once.
There is a second benefit that only shows up in a document like this one: the version history is legible. "v1.5 is UX" and "v1.6 is unification plus navigation" are sentences that describe what happened. A history where every version changes a bit of everything cannot be summarized at all.
Surprises worth naming plainly
The freed-but-plausible pointer defeated naive guards twice, on two independent code paths, with two independent evidence trails — video plus reject counters in Chapter 1 (Act I), Event Viewer fault offsets plus a guard counter in Chapter 3 (Act III). That it took two full investigations to close is not a comment on either investigation. The first one produced the mechanism and could not act on it; the second one produced a proof of impossibility, which is what made deletion obviously correct rather than merely appealing.
Knockback turned out not to be a shared hub, as Chapter 3 (Act III) covers: believed at first to be the first hook of a shared hop-50 container, later disproven by live edit testing elsewhere. Knockback is per-weapon, on a second hop that differs by weapon, with the same tail shape across all of them. Volume 02's Zweihänder chain was correct; the category it was filed under was wrong. Raw address proximity is not a structural relationship, and a chain that resolves and a correct chain are different claims — only an independent live edit test separates them.
Site B's garbage-pointer signature was never fully explained by any single hop in isolation, as the eight diagnostic recordings of Chapter 1 (Act I) show — different runs caught the corruption on different hops, or as a reject-count climb with every hop looking clean. Two competing mechanisms fit the clearest recording, but neither is distinguishable. That remains open. It stopped mattering once the hook stopped dereferencing anything, which is a real resolution and not the same thing as an explanation.
And the reject counter itself, threaded through Chapter 1 (Act I): a signal that correlated with crashes across multiple runs in both directions, with a plausible mechanism and no counterexamples, was measuring the project's own polling code re-reading its own stale snapshots. Correlated is not causal, and the specific way to get fooled is to instrument one of two effects of a common cause.
What would be done differently
Two honest answers, neither of which is "nothing."
The cache-timer pattern was available at v1.0. Every ingredient existed: Cheat Engine's Lua layer, createTimer, readPointer, and allocated parameter memory the hook already owned. Nothing about the pattern required the two crash investigations to invent it; what they provided was the motivation — specifically the guard-counter-at-zero proof that made "guard harder" visibly futile. Would it have been adopted at v1.0 without that proof? Honestly, probably not. In-hook chain walking with null guards is the pattern every tutorial teaches, and it is a real fix for null containers during loading screens — it just does not address freed-but-plausible pointers at all, and there was no way to know that mattered before something crashed. The transferable version of this regret is smaller and more useful than "this should have been known upfront": if a hook dereferences foreign memory and the code can be restructured so it does not, do it for structural reasons before there is a crash to justify it.
The int/float split should probably have been unified before v1.6. The natural moment was v1.4, when the integer script was already being rewritten to the cache architecture, and after that rewrite the two scripts had identical architecture — the v1.6 merge was consolidation rather than design. Doing it in v1.4 would have meant one timer instead of two from the start and no defensive-legacy-timer-destruction code needed for users upgrading across v1.4 and v1.5. Against that, v1.4 was already a substantial rewrite of both parameter layouts, and merging in the same pass would have meant that, if anything broke, the cause could have been either the cache conversion or the merge. Splitting them kept each version's failure surface small. The split was defensible, and v1.5 was the version that should have merged them.
What transfers to other Cheat Engine projects
Six things, stated as takeaways rather than conclusions.
Hook the store site, not the address. Scanning per-stat addresses fights the game's recalculation cycle forever. Intervening at the commit instruction cooperates with it: the game computes, the value is scaled in flight, the game stores what it believes it produced, and every future recalculation carries the change without further work. On an engine like Godot 4.2, this is not merely better; it is the only available approach because there is no per-stat code to hook into.
Separate resolution from mutation whenever a pointer chain crosses a teardown boundary. Resolve in Cheat Engine's Lua, where failure is a nil return. Mutate in the hook, using only registers the game has guaranteed are live and memory allocated by the script itself. The 500 ms staleness this introduces costs a missed scale on one write; the alternative costs the process.
Treat "no dialog on activation" as the success signal, and make the failure path speak. A preflight that names the exact failing signature converts a broken table into an actionable report. Pick which state is silent, document that choice, and never let the same silence mean both outcomes — the v1-era silent restore failure is what that rule is made of.
Mechanical symbol naming is an architecture decision, not a style preference. Volume 02 rewrote its parameter memory layout twice. It merged two scripts into one while preserving every symbol name and relative offset, so the table's wiring never had to be rebuilt. It is also why a 127-symbol unregister list could be audited by pattern.
Verify before you refactor, especially when the refactor is trivial. Three checks — no hotkey ID links, no getMemoryRecordByID, no dropdown link-mode references — cost minutes and were the entire reason renumbering 238 entries was safe. The bugs that refactor could have introduced are silent at load and surface as a control that mysteriously does nothing.
Reach for tools outside your tool when your tool is what's crashing. The most consequential evidence in Volume 02's crash investigations was not produced by Cheat Engine. It came from Windows Event Viewer's Application log, whose fault-offset field decoded to a specific instruction — mov r15,[r15+1C0], hop 1 of the in-hook chain walk — twice, on two independent crashes, with no ambiguity. Cheat Engine's own diagnostic surface could not produce that reading because the fault occurs within CE-allocated code that the OS reports as "unknown module" (Chapter 3, Act III). The general form of the lesson: when the code being injected is the code that's dying, the process that's dying is the process reporting the crash, and neither is a good position from which to observe. Move up one level, to the operating system's own crash-reporting layer. On Windows that is Event Viewer's Application log, filtered to Application Error entries, with the fault offset decoded against the module base. On Linux, the equivalents are /var/log/syslog and dmesg for kernel-visible faults, coredumpctl list/coredumpctl gdb for user-space cores, journalctl for anything the systemd journal caught, and strace on a controlled repro. On macOS, Console's crash reports and os_log play the same role. None of these are Cheat Engine, and that is the point: any code-injection project in any tool inherits this asymmetry, and reaching past the injecting tool to the OS's own logs the moment something crashes is worth more than any amount of in-tool instrumentation.
