Halls Of Torment - Cheat Scripting Practice Log

A context providing post for novices who wish to tackle Godot Engine with their custom scripts.

The main forum for Cheat Table database collections.


Moderator: Table Moderator

User avatar
WanderingNovice
Novice Hacker
Novice Hacker
Posts: 32
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 21

Re: Halls Of Torment - Cheat Scripting Practice Log

Post by WanderingNovice »

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.


License: https://creativecommons.org/publicdomain/zero/1.0/


Tags:

User avatar
WanderingNovice
Novice Hacker
Novice Hacker
Posts: 32
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 21

Re: Halls Of Torment - Cheat Scripting Practice Log

Post by WanderingNovice »

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


Appendices

Appendix A — Version-to-artifact map

Every frozen version and what the version was.

VersionPurpose
v1.0Three-site Max Health Multiplier (Sites A/B/C, shared multiplier/floor/ceiling/sanity symbols) plus the usability restructure and functional color scheme.
v1.1Multi-stat rdi dispatch on the same three sites — 5 integer stats sharing one scale routine.
v1.2First float stat (Health Regen) and the CE-side Lua cache architecture that removed all in-hook pointer walks.
v1.3Float multiplier scaled to full Volume 2 scope (9 float stats), including Zweihänder Knockback on its hop-50 chain.
v1.4Integer script converted to the CE-side cache architecture; showMessage preflight dialogs on all 4 hook sites.
v1.5Simplified/Detailed View UX restructure with cascade dropdowns; speedhack diagnostics section retired.
v1.6Unified 4-hook multiplier script, one merged cache timer, category cascades, in-game UI stat ordering, section banners.
v1.7Publication release: compact sequential IDs, public-facing Comments with quick start and troubleshooting, CC0 1.0.

Appendix B — Full annotated script

Not reproduced here. The line-by-line commented source is Halls-of-Torment-Vol2-CheatScript-Unified-Multiplier-Annotated.txt, whose code lines are byte-identical to the shipped table and which carries an architecture summary in its own header block. The same commentary is embedded in the script bodies of Halls-Of-Torment-Vol2-Scripts-(Version_1.7-Annotated).CT, an annotated edition strip-verified functionally identical to the frozen v1.7 table. The uncommented body is Halls-of-Torment-Vol2-CheatScript-Unified-Multiplier-Streamlined.txt. The Compact View Toggle ships annotated-only, as Halls-of-Torment-Vol2-CheatScript-Compact-View-Toggle-Annotated.txt.

Appendix C — Usage Guide

Not reproduced here. Anything concerning how to use the finished table — requirements, the build pin, activation steps, the Simplified and Detailed views, the cascade dropdowns, floors and ceilings, the ?? and refresh-timing behaviors, and the troubleshooting table — lives in Halls-of-Torment-Vol2-Documentation-Usage-Guide.md. That document also carries a plain-language and a technical description of what the script does at runtime. This document deliberately does not duplicate either.

Appendix D — Diagnostic run archive

Not reproduced here. Chapter 1 narrates the shape of the eight diagnostic recordings referenced throughout this document. The full frame-by-frame record and the source videos are held privately as project archive material and are not part of the published set; this document deliberately describes their structure and findings rather than reproducing the logs or footage.

Appendix E — Glossary

Site A / Site B / Site C — The three integer hook locations, one per code path of the game's universal integer setter, all reaching the same mov [rdi+168],eax commit. Each has its own AOB signature, code cave, byte backup, restore path, and per-stat sanity reject counters. Site C was added at the end of v1.0, after Sites A and B were already established during the diagnostics era described in Chapter 1. The float hook is separately named FLOAT Site A.

Hop 1–4 — The sequential pointer dereferences walked to resolve a stable container address from the static root. During the v1.0 diagnostics era, each hop was captured live into its own symbol so the chain could be inspected after a crash rather than only at a breakpoint. Those captures were stripped before the v1.0 freeze. In the shipped table, hops are walked only by the Lua cache timer, never inside a hook.

SanityRejectCount — A per-stat, per-site counter incremented whenever a hook's sanity window rejects an incoming value. Rejected values pass through to the game completely untouched and are never scaled. This is the only diagnostic counter family that survived into the published table, exposed as rows under Debug Central.

Strict Range Reject Count / Strict Unmapped Reject Count — Retired diagnostic counters from the v1.0 era that separately tracked rejections caused by a pointer value falling outside a plausible address range versus pointing at unmapped memory entirely. Their climbing behavior before crashes was the project's favored early-warning signal until it was found to be self-inflicted: the 100 ms Lua poller was re-reading its own stale snapshots and counting each as a rejection. Removed entirely before the v1.0 freeze.

Hub 1 (defensive hub) — The persistent PlayerStats container holding the shared defensive values, reached by HallsOfTorment.exe+396BD00 → 1C0 → 10 on a 0x10 stride: Max Health at 498, Block Strength at 4A8, Health Regen at 4B8, Defense at 4C8, each committing at +168. Its live memory order reflects modifier-registration call order, not the source declaration order of the game's native Health node.

Weapon-offense hub — The per-weapon stat container, reached by 396BD00 → 1C0 → 18 → 68 → 28 on a 0x18 stride. For the Zweihänder: Damage at 2C8, Range at 2E0, Cone Angle 2F8, Attack Speed 310, Crit Chance 328, Crit Bonus 340, an unused slot at 358, Multistrike 370. Rebuilt whenever the equipped weapon changes, which is why weapon rows read ?? when the Zweihänder is not equipped. Field order is per-weapon, repeating within an emitter-script family and diverging across families. Knockback is not in this block — the Zweihänder reaches it through second hop 50 instead of 18.

Variant stride — The flat 0x18 (24-byte) spacing between adjacent fields in a Godot stat container, equal to the size of a Godot Variant struct. The fields are a boxed Variant array populated at _ready() time by native createModifiedFloatValue / createModifiedIntValue calls in the order the weapon's own initialize_modifiers() invokes them. Once two fields of a container are confirmed by scan, the rest can be derived by arithmetic on this stride and confirmed by live value matching.

Static rootHallsOfTorment.exe+396BD00, the module-relative static base from which every confirmed pointer chain in this project descends. Its first hop, +1C0, is the shared currently-equipped-weapon container. Chapter 3's crashes were faults on the very first dereference from this slot, mov r15,[r15+1C0], during teardown windows, when the slot briefly holds a freed but still plausible pointer.

Cache timer — The Cheat Engine-side Lua timer that resolves every covered stat's pointer chain and writes the resulting container address into that stat's parameter block, so that hooks compare against a cached value instead of walking chains themselves. 500 ms period throughout. Introduced in v1.2 for the float script, added independently for the integer script in v1.4, and merged in v1.6 into a single unified timer, which also defensively destroys the two legacy timers and self-destructs if the script's symbols disappear. A nil or 0 at any hop writes 0 into that stat's cache slot rather than leaving a stale pointer.

CT ID — The integer ID attribute on each <CheatEntry> element in the .CT XML, auto-assigned by Cheat Engine at entry creation, unique within the table, and referenceable from hotkeys, Lua getMemoryRecordByID, and dropdown link-mode entries. Called "CT IDs" throughout this document to disambiguate from Cheat Engine's other numeric identifiers. In v1.7, the ID space was recompacted from a scattered range with gaps (running past 369) to a contiguous 0–237 sequence after three referenceability checks (Chapter 6) confirmed the renumber was safe.

alloc(name, size) — Cheat Engine Auto Assembler primitive that reserves size bytes of executable memory at script-enable time and binds name as a symbol pointing at that memory. The reserved region is the "code cave" where the hook body is assembled. Freed automatically when the script is disabled.

readmem(address, size) — Auto Assembler primitive that copies size bytes from address into the assembled output at assembly time, not at runtime. The bytes become part of the emitted code stream, which is why it can be used inside [DISABLE] blocks to restore original instructions without hardcoding a hex string: the pattern is readmem(<siteName>, <stolenByteCount>), and the bytes read are whatever was live at that address when the enable path last saved them.

registersymbol / unregistersymbol — Auto Assembler primitives that publish and remove entries in Cheat Engine's global symbol table. A registered symbol can be referenced by name from any .CT entry's address expression, from Lua, and from other Auto Assembler scripts in the same Cheat Engine session. Every symbol declared in an [ENABLE] block must be paired with an unregistersymbol in [DISABLE], or the next enable in the same session collides with the leftover. Volume 02's v1.6 unified script declares and unregisters 127 symbols, which is why the disable-path audit is performed using a naming pattern rather than by hand.

AOBScan / aobscanmodule — Two forms of the byte-pattern search Cheat Engine uses to locate hook sites without hardcoding addresses. AOBScan is the Lua-side form, returns a result object with a count and safe iteration, and raises no error on failure — an empty result is just an empty result. aobscanmodule is the Auto Assembler-side form, aborts the script with an assembly-time error if the pattern is not found or is not unique. The v1.6 preflight deliberately uses AOBScan for its non-fatal failure, so the script can report every missing site by name before touching memory (Chapter 5); the real hook installation uses aobscanmodule so that signature drift is a loud, immediate failure rather than a silent partial install.

All-or-nothing activation — The table's activation contract. All four hook signatures are verified by Lua AOBScan before any memory is written; any miss shows a dialog naming the exact failing site and aborts, having modified nothing. Consequently, no dialog on activation means success, and the integer and float halves cannot be enabled separately — a deliberate simplicity-over-flexibility choice made in v1.6 and documented openly under the v1.7 table's Known Limitations.

Appendix F — License

This document and the Volume 02 Cheat Engine table it describes are released into the public domain under the Creative Commons CC0 1.0 Universal Public Domain Dedication. To the extent possible under law, all copyright and related or neighboring rights are waived. No attribution is required. The full text of CC0 1.0 Universal is available at https://creativecommons.org/publicdomain/zero/1.0/.


License: https://creativecommons.org/publicdomain/zero/1.0/


Post Reply