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

WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

Re: Halls Of Torment - Cheat Scripting Practice Log

Post by WanderingNovice »

Topic Update: Volume 2 has been released!

Recap

It has been a learning experience for me in trying to decipher the anomalous assembly line that writes to in-game total values. These values I found are updated frequently during notable game events (level up, level up reward choices, etc.), and finding the opcodes that write to them was easy - HOWEVER:

  • These opcodes write to many multiple other values in-game. This is likely Godot architecture at work in minimizing the arithmetic assembly lines that are unpacked per game session. It may also explain away why most Godot games I have had access to are so lightweight and stable. Good on them, hard for me, hahahahaha!
  • There are two opcode regions that handle value read-and-writes for our specific scope of values to find: one for handling float values (the default value containers found in-game) and another for handling 4-Byte value containers. This one was a real work to go through, and the documentation later will explain why.
  • There are two other opcode regions that are read-only for our values (float & 4-Byte) and these opcodes are very useful for parsing and identifying the new addresses that the were reserved for our character's values. I mostly used these "On Status Screen" readers to find addresses in my pointer scanning process. Sooooooo many pointers, goodness me. And who knows, maybe the next update breaks those pointers. Hahahahahaha... shit.

Anyways, I will be posting the cheat table, text file copies of the actual script involved (both streamlined and annotated), and some documentation on how to use it as well as understanding how we got to writing it.

Cheers!

Last edited by WanderingNovice on Wed Aug 05, 2026 1:37 pm, edited 1 time in total.

Tags:

WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Halls of Torment — Volume Two Scripts: Usage Guide

Version 1.7 (2026-08-05) · Released under CC0 1.0 Universal (public domain)

This guide accompanies Halls-Of-Torment-Vol2-Scripts-(Version_1.7).CT. An annotated edition of the table and standalone commented script files are published alongside it for readers who want to study the internals.


How to Use

Before you start (requirements)

RequirementDetail
GameHalls of Torment (Steam). Single-player only — there is no anti-cheat and no online play to disrupt.
Tested againstSteam build 22764619 ("HoT Fixes" update, 2026-04-14) — the public build current as of 2026-08-05. Later builds may work but are unverified; a game update can break the table (see "If something goes wrong"). Check your build against the game's build history on SteamDB — this table's build is 22764619.
Cheat EngineVersion 7.5. Other versions are untested.
Character scopeBuilt and validated on the Swordsman with the Zweihänder equipped. Defensive and miscellaneous stats are shared player stats and should work on any character, but only the Swordsman is validated. The weapon block affects the Zweihänder only.
Coverage14 stats: Max Health, Health Regen, Block Strength, Defense, Movement Speed, Experience Gain, and 8 Zweihänder stats (Damage, Range, Cone Angle, Attack Speed, Crit Chance, Crit Bonus, Multistrike, Knockback).

Step-by-step

  1. Launch the game first, then open the .CT file. Cheat Engine starts and offers to attach to HallsOfTorment.exe — accept. If it doesn't ask, click the computer icon (top-left) and select the process manually.
  2. Expand the table: click "Click Here to access Volume Two Scripts", then open the Main Content section (green).
  3. Activate the script: tick the checkbox on >>> Unified Multiplier Script (Vol2) <<<.
    • No dialog appears = success. The script verified all four hook signatures and installed cleanly.
    • A dialog naming a hook site means the game version doesn't match — nothing was written to memory, and the table needs an update.
  4. Start a run. Multiplier values resolve once your character exists in a level; in menus they may show ?? or 0 — that is normal.
  5. Set multipliers under Simplified View:
    • Whole category at once: use the dropdown on a category header (Defensive Values / Miscellaneous / Swordsman - Main Weapon - Zweihänder). Presets run 1× to 1000×; every stat in the category takes the value.
    • Single stat: edit any row's value directly, or use its own dropdown. [ DEFAULT = 1 ] means "no change".
  6. Deactivate by unticking the script's checkbox. A confirmation dialog reports a clean teardown: original game code is restored, all memory is freed, and stats return to normal on their next refresh.

Things worth knowing

  • Changes apply on stat refresh events — level-up, trait pick, weapon swap, level load — not always instantly. Health Regen refreshes constantly, so it reacts first.
  • Multipliers scale the game's own computed total (base + all in-game bonuses). They don't edit base values or upgrade math, which is why everything returns to normal when the script is off.
  • Floors and ceilings: every stat has a floor (its base-stat default) and a ceiling. Scaled results are clamped into that window. Both are editable per stat under Detailed View.
  • Detailed View also shows each stat's "Original Total" readout — the raw value the game computed before your multiplier — useful for checking what the game thinks your build is worth.
  • Range and Movement Speed look 18× too big in raw form: the game stores them in internal units where raw = displayed × 18.
  • Debug Central (magenta "Advanced Users Section") is diagnostics only — safe to ignore during normal play.
  • The Expand/Collapse entry at the top of the table only hides Cheat Engine's scanner panels for a cleaner window. It never touches the game.

If something goes wrong

SymptomMeaningFix
"…hook signature not found" dialog on activationThe game was updated and a hook site moved. Nothing was modified.Wait for / request an updated table.
Values show ??Script inactive, or CE not attached to the game.Attach to HallsOfTorment.exe, activate the script.
Multiplied stat didn't changeNo refresh event has fired yet.Level up, swap weapons, or reload a level.
A stat ignores an extreme valueThe sanity window or ceiling clamped it.Raise the ceiling in Detailed View; sanity windows protect against corrupt writes.

What This Script Actually Does

Plain-language version

Halls of Torment recalculates each of your stats whenever something about your build changes — you level up, pick a trait, swap a weapon. At the end of each recalculation, the game stores the finished number (your base stat plus every bonus) into that stat's slot in memory.

This script places a small detour at the exact moments those stores happen — four locations in the game's code cover all 14 stats. When the game is about to store a stat the table covers, the detour:

  1. checks the number is plausible (a corrupted value is let through untouched and counted, never scaled);
  2. records the original number so the table can show it to you;
  3. multiplies it by your chosen multiplier;
  4. clamps the result between a floor and a ceiling so it can't go absurd or negative;
  5. hands the result back to the game's own store instruction.

The game then carries on, believing this was the number it computed. Because the change happens at the moment of storage — not by overwriting memory afterwards — it survives every recalculation, and turning the script off restores the game's code exactly as it was.

One safety system deserves mention: the script must know which memory belongs to your character's stats, and that memory moves around (each run, each level, sometimes mid-play). A background task re-locates all 14 stat containers twice per second and hands fresh addresses to the detours. If a container temporarily doesn't exist — main menu, death screen, loading — the address is set to zero and the detours simply do nothing. Stale memory is never touched, which is what makes the script stable across restarts and long sessions.

Technical version

Injection. Four AOB signatures are located with aobscanmodule against HallsOfTorment.exe — three integer store sites and one float store site, each ending at a mov [rdi+168],eax / movss [rdi+168],xmm0 that commits a finished stat total into a Godot stat-container object. Each site is hooked with a 5-byte jmp into an allocated code cave (alloc near the target), with stolen bytes (22/15/15/17) re-executed at the top of the cave and preserved via readmem into an allocated backup buffer for byte-exact restoration on disable.

Preflight. Before any patching, a {$lua} block runs all four signature scans with AOBScan. Any miss aborts activation with a message naming the failed site (error() unwinds the [ENABLE] section), guaranteeing all-or-nothing installation.

Dispatch. All 14 stats share the four hooks. Parameters live in two allocated tables (integer: 5 records; float: 9 records), one 0x30-byte record per stat: multiplier (float), floor, ceiling, sanity min/max, intended-value readout, per-site reject counters, and an 8-byte container-pointer cache. Each hook walks its table comparing rdi (the container being written) against each record's cached pointer — a match identifies which stat is being stored right now; no match means the write belongs to a stat we don't cover and passes through untouched.

Scaling path (integer sites). Sanity-check eax against the record's window → store raw total to the readout field → cvtsi2ss / mulss by the multiplier / cvttss2si back → clamp to floor and ceiling via compare-and-move → fall through to the original store. The float site does the same in SSE registers directly (ucomiss checks, mulss, maxss/minss clamps).

Pointer cache. A single CE createTimer (500 ms) resolves all 14 pointer chains from the static root HallsOfTorment.exe+396BD00 using readPointer hops. A nil/0 at any hop poisons that chain's cache with 0 for the tick, so hooks compare against 0 and never dereference stale memory — game code never walks pointer chains itself. The timer self-destructs if it detects the script's symbols are gone, and destroys any legacy timers from older table versions on startup.

Teardown. [DISABLE] restores all four sites' original bytes from the backups, unregisters all 127 symbols, deallocates every buffer, destroys the timer, and confirms with a dialog. Because hooks scale values only in flight, no persistent memory edit outlives deactivation; stats revert on their next natural refresh.

Diagnostics. Sanity-window rejections increment per-site counters exposed as table rows under Debug Central, alongside cached-container address rows — enough to distinguish "signature broke" from "chain broke" from "value out of window" when triaging a report.


For the fully commented source, see:

  • Halls-of-Torment-CheatScript-Unified-Multiplier-Annotated.txt
  • Halls-of-Torment-CheatScript-Compact-View-Toggle-Annotated.txt
  • Halls-Of-Torment-Vol2-Scripts-(Version_1.7-Annotated).CT — the same table with commentary embedded in the script bodies

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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

Re: Halls Of Torment - Cheat Scripting Practice Log

Post by WanderingNovice »

Volume 2 Chapters to expect:

So, um, after sinking hours (days) into writing a baseline documentation for Volume 2, I ended up writing a Reference Edition. Useful for detail-hungry nerds, not so useful for teaching someone how to follow the journey that made the scripts stable. There are writing nuances that separate works into their respective would-be readers, and if I screw it up too early in this post... well... I am bound to filter out 99% of readers from ever being engaged. So I did something a sane person would do: rewrite it again after consulting with humans (who were annoyed with me putting some random document in their face), consult with Grammarly, then consult with Claude Fable and Sonnet.

And I ended up writing a Learner's Edition. Not much changed except for style? And the way the information is presented as impersonal. Good for academic publication, maybe a little dense and too nerdy for OCTF. Haha. Hahaha. HAHAHAHA. AAAAAAH. (This is my first time working on a project this big (for a hobby) and making sure it is polished enough to survive in OCTF. I really want my baby to be justified in its existence).

So I did another sane approach that only sane people would do who are definitely, totally normal enough to be spending their leisure time and vacation period in regular ways: I started writing a Forum Friendly Edition. Fanfare!

Well, expect Volume 2 to have plenty of chapters. But the most important is Chapter 0, which explains each code block and its function in a narrative style featuring figures from the Greek Pantheon. (Grammarly corrected me 4 times before I finished that sentence. "Everyone's a critic", as Squidward says).

Enjoy my:

:mrgreen: Replies of Torment! :mrgreen:


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Volume 2: Chapter 0 – The Pantheon of One Script

Chapter 0 introduces the "pantheon" behind the Unified Multiplier script: eight named blocks, each playing a specific role in the architecture, from the first preflight check to final cleanup.

Quick map:

  • Zeus – Header & Architecture Summary
  • Athena – Preflight Guard (Lua AOB checks)
  • Hephaestus – Alloc & Symbol Table
  • Demeter – Parameter Tables (per-stat records)
  • Ares – Integer Hook Blocks (three commit sites)
  • Apollo – Float Hook Block
  • Hermes – Cache Timer & Pointer Chains
  • Hestia – Disable & Cleanup

Zeus – Architecture in One Paragraph

Every script in this volume has a head of the table. For the Unified Multiplier script, that role belongs to Zeus. Zeus is not code but a contract written in one place. His job is to say clearly what the script is allowed to touch, how it is allowed to touch it, and what "enabled" must mean.

In this case, Zeus' contract is short enough to memorize. The script hooks four commit sites — three integer paths and one float path — all ending in the same two instructions: mov [rdi+168],eax for integers and movss [rdi+168],xmm0 for floats. Those commits write into stat container objects at offset +168, and the script treats them as the only place it may intervene. Fourteen stats are in scope: five integers (Max Health, Block Strength, Defense, Zweihänder Damage, Zweihänder Range) and nine floats (Health Regen, Movement Speed, Experience Gain, and the Zweihänder float block). Everything else in the game is deliberately ignored.

Zeus also fixes the tool and the rules. The target is Halls of Torment on Godot 4.2, Steam build ID 22764619, and the tool is Cheat Engine 7.5 on Windows only. No other CE version is supported; every AOB signature, Lua call, and .CT structural claim in this volume assumes 7.5. Activation is all-or-nothing: either all four hooks are present and armed, or none are. No partial installs, no "works for some stats but not others," no "probably fine."

The rest of the pantheon works inside those boundaries. When later chapters talk about "adding a new row to the table" or "moving more work into Lua," they are not moving Zeus. They are moving everyone else around the fixed outline he drew at the top of the script.


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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Athena – Preflight and Refusal

If Zeus decides what the world looks like, Athena decides whether it is safe to go to war in it. Her entire job lives in the first {$lua} block under [ENABLE], and she does one thing that turns out to matter more than most of the more glamorous code that follows: she refuses to arm the script if the battlefield has changed underneath it.

When you tick the script's checkbox for real — not during Cheat Engine's syntax check pass, but on an actual enable — Athena runs four tests in Lua. Each one is an AOBScan aimed at a specific commit sequence in HallsOfTorment.exe: one for INT SITE A, one for INT SITE B, one for INT SITE C, and one for FLOAT SITE A. She is not looking for a rough match. She is looking for the exact byte patterns the script was written and tested against, including the mov [rdi+168],eax or movss [rdi+168],xmm0 that the hook will eventually overwrite.

If any scan comes back empty, Athena does not negotiate. She shows you a precise error message — "INT SITE A hook signature not found. The game may have been updated — script was NOT enabled." — and then throws a Lua error() that aborts activation on the spot. No code caves are patched, no jumps are written, no allocs are left lying around. The script stays disabled. The table looks unchanged except for the message box it just raised.

This is the all-or-nothing rule enforced in code rather than documentation. In a naive table, it is easy to imagine a half-enabled state: two hooks find their sites, two do not, and the user spends the next hour wondering why some stats scale and others do not. Athena's contract forbids that state entirely. Either every site passes its preflight, or nothing happens. That means a game update that moves even one commit site fails loudly at enable time, not quietly after thirty minutes of "why is Defense still normal while Max Health is at x10."

One detail is worth carrying forward because it will show up again when we talk about Lua timers. Athena runs only when syntaxcheck is false. Cheat Engine does a dry run over every script as you type and when you first open a table, with syntaxcheck true, to make sure the Auto Assembler can parse it. You do not want preflight scans and message boxes firing during that pass. Athena respects that boundary. She wakes only during a real enable, checks her four sites, and either blesses the rest of the script to proceed or vetoes it entirely.

From this point on, when later chapters say "Athena failed" or "Athena passed and handed control to Hephaestus," they are talking about this very small, very strict Lua block at the top of [ENABLE]. It is easy to skim past in the annotated script. It is the reason the rest of the work can assume that every hook it thinks exists actually does.

Athena – Preflight Guard (Lua AOB Checks)

Athena lives entirely in the first {$lua} block under [ENABLE]. The excerpt below is the full preflight — all four AOB scans in the exact order they run. Every scan follows the same three-step pattern: verify the signature, abort activation on failure, and destroy the scan handle on success. The repetition is the point.

Code: Select all

{$lua}
if not syntaxcheck then  -- Athena: skip CE's parse-time dry run; only arm on a real enable

  -- Athena, INT SITE A: verify Ares' first door still exists
  local multiplierSiteAPreflight = AOBScan('48 8D 97 70 01 00 00 E8 82 9A FF FF F3 0F 2C C0 89 87 68 01 00 00', '+X')
  if multiplierSiteAPreflight == nil then
    showMessage('Unified Multiplier Script: INT SITE A hook signature not found. The game may have been updated - script was NOT enabled.')
    error('INT SITE A signature missing.')  -- Athena's veto: all-or-nothing, no partial hooks
  end
  multiplierSiteAPreflight.destroy()

  -- Athena, INT SITE B: verify Ares' second door still exists
  local multiplierSiteBPreflight = AOBScan('E8 D6 10 B2 02 F3 0F 2C C0 89 87 68 01 00 00', '+X')
  if multiplierSiteBPreflight == nil then
    showMessage('Unified Multiplier Script: INT SITE B hook signature not found. The game may have been updated - script was NOT enabled.')
    error('INT SITE B signature missing.')  -- Athena's veto: all-or-nothing, no partial hooks
  end
  multiplierSiteBPreflight.destroy()

  -- Athena, INT SITE C: verify Ares' third door still exists
  local multiplierSiteCPreflight = AOBScan('E8 3C 11 B2 02 F3 0F 2C C0 89 87 68 01 00 00', '+X')
  if multiplierSiteCPreflight == nil then
    showMessage('Unified Multiplier Script: INT SITE C hook signature not found. The game may have been updated - script was NOT enabled.')
    error('INT SITE C signature missing.')  -- Athena's veto: all-or-nothing, no partial hooks
  end
  multiplierSiteCPreflight.destroy()

  -- Athena, FLOAT SITE A: verify Apollo's only door still exists
  local multiplierFloatSiteAPreflight = AOBScan('E8 05 74 3D 02 48 63 45 B7 F3 0F 11 87 68 01 00 00', '+X')
  if multiplierFloatSiteAPreflight == nil then
    showMessage('Unified Multiplier Script: FLOAT SITE A hook signature not found. The game may have been updated - script was NOT enabled.')
    error('FLOAT SITE A signature missing.')  -- Athena's veto: all-or-nothing, no partial hooks
  end
  multiplierFloatSiteAPreflight.destroy()

end
{$asm}

A few things to notice in shape:

  • The if not syntaxcheck then ... end wrapper is Athena's boundary. Inside it, she scans; outside it during CE's parse-time dry run, she is silent.
  • Every scan is destroyed immediately after success. Athena does not hold onto scan handles between checks — each one is a single-use test.
  • On failure, error() aborts the whole [ENABLE] before any code in the {$asm} block below can run. This is where the all-or-nothing rule stops being a promise and becomes enforcement.
  • The three integer sites verify Ares' doors; the single float site verifies Apollo's. Athena does not know or care about Hephaestus, Demeter, Hermes, or Hestia — her job ends the moment control leaves this block.

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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Hephaestus – Forging the Script's Tools

Once Athena has decided the battlefield is still where we left it, the next god to act is Hephaestus. His domain is not strategy or numbers but hardware. Hephaestus is the smith of the script: the part that forges every code cave, lays out every block of data, and gives names to everything the rest of the pantheon will touch.

In the annotated script, Hephaestus lives in the first big cluster of alloc, label, and registersymbol lines that follow Athena's preflight. This is where the script reserves memory for its working parts:

  • Code caves for each hook site, such as multiplierSiteANewMem, multiplierSiteBNewMem, and multiplierSiteCNewMem for the three integer paths and multiplierFloatSiteANewMem for the float path.
  • Backups for stolen bytes, like multiplierSiteAOriginalBytes, multiplierSiteBOriginalBytes, multiplierSiteCOriginalBytes, and multiplierFloatSiteAOriginalBytes, so Hestia can put the original instructions back exactly as they were on disable.
  • The parameter tables themselves, multiplierStatParams and multiplierFloatStatParams, which will shortly be handed to Demeter to fill in.

Hephaestus does not stop at carving space. He also gives every important address a name and publishes those names to the rest of Cheat Engine. Every per-stat label in this block — maxHealthMultiplier, blockStrengthSafetyClamp, zweihanderDamageIntendedValue, zweihanderCritBonusContainerCache, and so on — is followed by a registersymbol that makes that label visible to the rest of the table and to Lua. That is why you can point a memory record at maxHealthMultiplier instead of hardcoding an address, and why the cache timer later can look up movementSpeedContainerCache by name.

The naming scheme is boring on purpose. Each stat gets a small family of symbols that read like a description of a table row:

  • maxHealthMultiplier, maxHealthSafetyClamp, maxHealthOutputCap, maxHealthSanityMin, maxHealthSanityMax, maxHealthIntendedValue, plus reject counters and maxHealthContainerCache.
  • The same pattern repeats for Block Strength, Defense, the Zweihänder integer stats, and all the float stats in their own table.

That repetition is not duplication. It is the interface. Ares and Apollo do not need to know which stat they are handling at compile time; they need a guarantee that, once they have picked a row, "multiplier is at offset 0, floor at +4, ceiling at +8, sanity window at +0xC/+0x10, intended value at +0x14, reject counters at +0x18 and up, container cache at the end." Hephaestus is the one who enforces that layout by allocating enough room and binding the names in a consistent pattern.

The Learner Edition makes a passing remark that the v1.7 script unregisters 127 symbols on disable and that this is a real audited number, not a marketing flourish. That count comes directly from Hephaestus' work: however many names he registers here are how many Hestia is required to clean up later. When you see that number in a later chapter, remember that it is not an abstraction. It is the length of the list in this block.

Hephaestus – Alloc & Symbol Table

Hephaestus begins only after Athena has passed the four doors. This excerpt shows the declaration pattern rather than every repeated stat: four code caves, their restoration buffers, two parameter tables, and the public names for one integer row and one float row. The shape matters because Demeter, Ares, Apollo, Hermes, and Hestia can all rely on these forged addresses without hardcoding a single raw location.

Code: Select all

// Hephaestus: forge code cave for Ares' first door
alloc(multiplierSiteANewMem,2048,multiplierSiteAAob)
// Hephaestus: reserve Demeter's integer parameter table
alloc(multiplierStatParams,256)
// Hephaestus: back up original bytes for Hestia to restore later
alloc(multiplierSiteAOriginalBytes,22)

// Hephaestus: give Max Health's integer row its private names
label(maxHealthMultiplier)  // Hephaestus: name Max Health's multiplier for Ares to read
label(maxHealthSafetyClamp)  // Hephaestus: name Max Health's floor
label(maxHealthOutputCap)  // Hephaestus: name Max Health's ceiling
label(maxHealthSanityMin)  // Hephaestus: name Max Health's sanity-window floor
label(maxHealthSanityMax)  // Hephaestus: name Max Health's sanity-window ceiling
label(maxHealthIntendedValue)  // Hephaestus: name Ares' unscaled readout
label(maxHealthSiteASanityRejectCount)  // Hephaestus: name Ares' first-door rejection tally
label(maxHealthSiteBSanityRejectCount)  // Hephaestus: name Ares' second-door rejection tally
label(maxHealthSiteCSanityRejectCount)  // Hephaestus: name Ares' third-door rejection tally
label(maxHealthContainerCache)  // Hephaestus: name the cache Hermes will refresh

// Hephaestus: publish Max Health's names to the table
registersymbol(maxHealthMultiplier)  // Hephaestus: publish the multiplier
registersymbol(maxHealthSafetyClamp)  // Hephaestus: publish the floor
registersymbol(maxHealthOutputCap)  // Hephaestus: publish the ceiling
registersymbol(maxHealthSanityMin)  // Hephaestus: publish the sanity-window floor
registersymbol(maxHealthSanityMax)  // Hephaestus: publish the sanity-window ceiling
registersymbol(maxHealthIntendedValue)  // Hephaestus: publish Ares' readout
registersymbol(maxHealthSiteASanityRejectCount)  // Hephaestus: publish Ares' first-door tally
registersymbol(maxHealthSiteBSanityRejectCount)  // Hephaestus: publish Ares' second-door tally
registersymbol(maxHealthSiteCSanityRejectCount)  // Hephaestus: publish Ares' third-door tally
registersymbol(maxHealthContainerCache)  // Hephaestus: publish Hermes' cache address

// ... (remaining 4 integer stats: Block Strength, Defense, Zweihänder Damage, Zweihänder Range follow the same label + registersymbol pattern)

// Hephaestus: expose Ares' first restoration buffer for Hestia
registersymbol(multiplierSiteAOriginalBytes)

// Hephaestus: forge code cave for Ares' second door
alloc(multiplierSiteBNewMem,2048,multiplierSiteBAob)
// Hephaestus: back up original bytes for Hestia to restore later
alloc(multiplierSiteBOriginalBytes,15)
// Hephaestus: expose Ares' second restoration buffer for Hestia
registersymbol(multiplierSiteBOriginalBytes)

// Hephaestus: forge code cave for Ares' third door
alloc(multiplierSiteCNewMem,2048,multiplierSiteCAob)
// Hephaestus: back up original bytes for Hestia to restore later
alloc(multiplierSiteCOriginalBytes,15)
// Hephaestus: expose Ares' third restoration buffer for Hestia
registersymbol(multiplierSiteCOriginalBytes)

// Hephaestus: forge code cave for Apollo's only door
alloc(multiplierFloatSiteANewMem,2048,multiplierFloatSiteAAob)
// Hephaestus: reserve Demeter's float parameter table
alloc(multiplierFloatStatParams,1024)
// Hephaestus: back up original bytes for Hestia to restore later
alloc(multiplierFloatSiteAOriginalBytes,17)

// Hephaestus: give Health Regen's float row its private names
label(healthRegenMultiplier)  // Hephaestus: name Health Regen's multiplier for Apollo to read
label(healthRegenSafetyClamp)  // Hephaestus: name Health Regen's floor
label(healthRegenOutputCap)  // Hephaestus: name Health Regen's ceiling
label(healthRegenSanityMin)  // Hephaestus: name Health Regen's sanity-window floor
label(healthRegenSanityMax)  // Hephaestus: name Health Regen's sanity-window ceiling
label(healthRegenIntendedValue)  // Hephaestus: name Apollo's unscaled readout
label(healthRegenSiteASanityRejectCount)  // Hephaestus: name Apollo's rejection tally
label(healthRegenContainerCache)  // Hephaestus: name the cache Hermes will refresh

// Hephaestus: publish Health Regen's names to the table
registersymbol(healthRegenMultiplier)  // Hephaestus: publish the multiplier
registersymbol(healthRegenSafetyClamp)  // Hephaestus: publish the floor
registersymbol(healthRegenOutputCap)  // Hephaestus: publish the ceiling
registersymbol(healthRegenSanityMin)  // Hephaestus: publish the sanity-window floor
registersymbol(healthRegenSanityMax)  // Hephaestus: publish the sanity-window ceiling
registersymbol(healthRegenIntendedValue)  // Hephaestus: publish Apollo's readout
registersymbol(healthRegenSiteASanityRejectCount)  // Hephaestus: publish Apollo's rejection tally
registersymbol(healthRegenContainerCache)  // Hephaestus: publish Hermes' cache address

// ... (remaining 8 float stats: Movement Speed, Experience Gain, Zweihänder Cone Angle, Zweihänder Attack Speed, Zweihänder Crit Chance, Zweihänder Crit Bonus, Zweihänder Multistrike, Zweihänder Knockback follow the same label + registersymbol pattern)

// Hephaestus: expose Apollo's hook address to the table
registersymbol(multiplierFloatSiteAAob)
// Hephaestus: expose Apollo's restoration buffer for Hestia
registersymbol(multiplierFloatSiteAOriginalBytes)

A few things to notice in shape:

  • The four alloc(...NewMem,2048,...) lines give Ares three independent integer caves and Apollo one float cave. The cave size stays the same even though the stolen-byte buffers do not.
  • Each OriginalBytes allocation preserves exactly the bytes Hestia must restore: 22 at Site A, 15 at Sites B and C, and 17 at Float Site A.
  • multiplierStatParams is 256 bytes for Demeter's five integer rows; multiplierFloatStatParams is 1024 bytes for her nine float rows. The allocation sizes reserve more room than those 0x30-byte rows consume; the per-row shape does not change.
  • A stat's label family and matching registersymbol family are deliberately parallel. Demeter lays out the row, Ares or Apollo uses it, Hermes refreshes its cache, and Cheat Engine can still address every slot by name.
  • The ellipses hide repetition, not new design. Every omitted stat receives the same family of names and the same publication step before any hook begins using it.

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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Demeter – Fields of Parameters

Hephaestus forges the tools; Demeter plants the fields. Her territory is the long data-only region that starts at multiplierStatParams: and multiplierFloatStatParams: — the place where each stat gets a row of parameters that describe how it should be treated when it passes through a hook.

From Demeter's point of view, the script is not a forest of instructions. It is a neat rectangular farm. Each integer stat gets exactly 0x30 bytes in multiplierStatParams, arranged like this:

  • At +0: Multiplier as a 32-bit float.
  • At +4: Floor as a 32-bit integer.
  • At +8: Ceiling as a 32-bit integer.
  • At +0xC and +0x10: SanityMin and SanityMax, defining a window of plausible totals; anything outside that window is recorded and passed through untouched.
  • At +0x14: IntendedValue, a readout slot where the hook records the unscaled total the game tried to store.
  • At +0x18, +0x1C, and +0x20: three per-site reject counters, one for each integer hook (Sites A, B, and C), so you can see which arm is voting "this value looked implausible."
  • At +0x28: ContainerCache, an 8-byte pointer field where Hermes will later write the address of this stat's container object, resolved from the static root.

The annotated script shows this most clearly in the Max Health row:

Code: Select all

maxHealthMultiplier:              dd (float)1.0
maxHealthSafetyClamp:             dd (int)1
maxHealthOutputCap:               dd (int)999999
maxHealthSanityMin:               dd (int)300
maxHealthSanityMax:               dd (int)5000
maxHealthIntendedValue:           dd (int)0
maxHealthSiteASanityRejectCount:  dd (int)0
maxHealthSiteBSanityRejectCount:  dd (int)0
maxHealthSiteCSanityRejectCount:  dd (int)0
dd (int)0                         // padding
maxHealthContainerCache:          dq 0

Block Strength, Defense, Zweihänder Damage, and Zweihänder Range repeat the same pattern immediately after, each with their own floor, ceiling, and sanity window values tuned to the stat's natural range. Demeter does not care how those numbers were chosen — that story lives in later chapters — only that once chosen they are laid out on the same stride so Ares' loop can treat them interchangeably.

The float side, multiplierFloatStatParams, follows the same idea with type-appropriate changes. Health Regen's row, for instance, replaces the integer fields with floats and moves the cache pointer to a slightly different offset, but keeps the conceptual slots: multiplier, floor, ceiling, sanity window, intended value, reject counter, cache pointer. Apollo's hook body knows that if it lands on Health Regen's record, the multiplier is at [r14+0], the floor at [r14+4], the ceiling at [r14+8], the sanity window at [r14+0xC] and [r14+0x10], and the cache pointer at [r14+0x20].

The key property Demeter enforces is the stride. Each record is exactly 0x30 bytes wide, no more, no less. That is why Ares and Apollo can walk the table with nothing more than mov r14,multiplierStatParams and add r14,30 in their compare loops, and why they can track how many records remain with a simple mov r15,05 or mov r15,09. From their perspective, Demeter has laid out a row of identical plots; all they have to do is walk along the fence, compare rdi to each ContainerCache, and, on a match, apply whatever parameters they find in that plot.

When later chapters talk about "adding a new stat" to the script, what they mean at the lowest level is adding one more record to Demeter's table and one more comparison in Ares or Apollo's loop. The hard work is in finding the right container and deciding on sane multipliers and clamps. Once those are known, Demeter's job is straightforward: plant another row and keep the field regular so the gods who walk it never have to special-case a thing.

Demeter – Parameter Tables (Per-Stat Records)

Demeter's fields begin at the two parameter-table declarations below. The tables contain no executable instructions; they are regular 0x30-byte plots of values that the hooks can locate via a cached container address. Max Health shows the five-record integer field first; Health Regen then shows the nine-record float field and its slightly different cache placement. The labels and stored values are the script's own; the comments name the god who reads or tends each plot.

Code: Select all

// ---------- Demeter's integer parameter table: 5 records, 0x30 bytes each ----------
// layout: +00 multiplier(float) +04 floor +08 ceiling +0C sanityMin
//         +10 sanityMax +14 intendedValue +18/+1C/+20 reject counters
//         +24 padding +28 containerCache(8 bytes)
multiplierStatParams:

maxHealthMultiplier:
dd (float)1.0  // Demeter, +0x00: Multiplier; Ares reads it before scaling Max Health
maxHealthSafetyClamp:
dd (int)1  // Demeter, +0x04: Floor/SafetyClamp; Ares reads the lower bound
maxHealthOutputCap:
dd (int)999999  // Demeter, +0x08: Ceiling/OutputCap; Ares reads the upper bound
maxHealthSanityMin:
dd (int)300  // Demeter, +0x0C: lower edge of Ares' sanity gate
maxHealthSanityMax:
dd (int)5000  // Demeter, +0x10: upper edge of Ares' sanity gate
maxHealthIntendedValue:
dd (int)0  // Demeter, +0x14: IntendedValue readout written by Ares
maxHealthSiteASanityRejectCount:
dd (int)0  // Demeter, +0x18: Site A reject counter written by Ares
maxHealthSiteBSanityRejectCount:
dd (int)0  // Demeter, +0x1C: Site B reject counter written by Ares
maxHealthSiteCSanityRejectCount:
dd (int)0  // Demeter, +0x20: Site C reject counter written by Ares
dd (int)0  // Demeter, +0x24: padding that preserves the 0x30-byte stride
maxHealthContainerCache:
dq 0  // Demeter, +0x28: ContainerCache written by Hermes for Ares to match

// ... (Block Strength, Defense, Zweihänder Damage, Zweihänder Range follow the same 0x30-byte pattern)

// ---------- Demeter's float parameter table: 9 records, 0x30 bytes each ----------
// layout: +00 multiplier +04 floor +08 ceiling +0C sanityMin +10 sanityMax
//         +14 intendedValue +18 reject counter +1C padding
//         +20 containerCache(8 bytes) +28 padding
multiplierFloatStatParams:

healthRegenMultiplier:
dd (float)1.0  // Demeter, +0x00: Multiplier; Apollo reads it before scaling Health Regen
healthRegenSafetyClamp:
dd (float)1.0  // Demeter, +0x04: Floor/SafetyClamp; Apollo reads the lower bound
healthRegenOutputCap:
dd (float)99999.0  // Demeter, +0x08: Ceiling/OutputCap; Apollo reads the upper bound
healthRegenSanityMin:
dd (float)0.0  // Demeter, +0x0C: lower edge of Apollo's sanity gate
healthRegenSanityMax:
dd (float)10000.0  // Demeter, +0x10: upper edge of Apollo's sanity gate
healthRegenIntendedValue:
dd (float)0.0  // Demeter, +0x14: IntendedValue readout written by Apollo
healthRegenSiteASanityRejectCount:
dd (int)0  // Demeter, +0x18: reject counter written by Apollo
dd (int)0  // Demeter, +0x1C: padding that preserves the 0x30-byte stride
healthRegenContainerCache:
dq 0  // Demeter, +0x20: ContainerCache written by Hermes for Apollo to match
dq 0  // Demeter, +0x28: padding that completes the 0x30-byte record

// ... (Movement Speed, Experience Gain, Zweihänder Cone Angle, Zweihänder Attack Speed, Zweihänder Crit Chance, Zweihänder Crit Bonus, Zweihänder Multistrike, and Zweihänder Knockback follow the same 0x30-byte pattern)

A few things to notice in shape:

  • Demeter keeps both fields on the same 0x30-byte stride so that Ares can traverse five integer records and Apollo can traverse nine float records by advancing one fixed plot at a time.
  • The first three fields are Ares' integer controls — Multiplier, Floor/SafetyClamp, and Ceiling/OutputCap — with Apollo reading the corresponding float fields in the second table; the distinct field types are the reason the two tables stay separate.
  • The sanity window sits before the +0x14 readout. Ares or Apollo writes IntendedValue only after the raw total survives that window; an implausible total instead increments the appropriate reject counter and passes through untouched.
  • Hermes supplies the ContainerCache pointer that lets each hook identify a plot. Its integer-table address is +0x28; its float-table address is +0x20, leaving the rest of each 0x30-byte record as padding.

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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Ares – Three Doors into the Integer Setter

Once Demeter has laid out the fields, Ares takes his post at the doors. The Unified Multiplier script does not have a dedicated hook for "Max Health" or "Defense" in the way a C++ game might. It has three hooks on the engine's universal integer setter, and every integer stat in the game passes through one of those hooks when it is committed. Ares' job is to stand at those doors, recognize which stat is walking through, and decide whether to touch the value.

The three doors are the three INT SITES you will see in the annotated script:

  • INT SITE A: a longer signature that includes a lea into rdi+170, a call, the cvttss2si that turns the float result into an integer in eax, and the mov [rdi+168],eax commit.
  • INT SITE B and INT SITE C: shorter signatures that both call into the same helper and then perform the same cvttss2si and mov [rdi+168],eax sequence, differing only in the relative displacement of the preceding call.

Athena has already verified that all three signatures are present before Ares is allowed to act. Hephaestus has already forged a code cave and an original-byte buffer for each site. Ares uses that infrastructure in the same pattern every time:

  1. At the hook site, the script overwrites the original instruction bytes with a jmp to Ares' code cave and pads the remainder of the stolen instructions with NOPs so the total length matches exactly. The original bytes themselves are safely stored in multiplierSite*OriginalBytes for Hestia to replay later.

  2. Inside the code cave — for example, multiplierSiteANewMem for Site A — Ares starts by replaying the work he just stole: the lea, the call that computes the finished total into xmm0, and the cvttss2si that converts it to an integer in eax. At this point, eax holds exactly the value the game intended to write and rdi still points at the stat container being written.

  3. Only then does the dispatch begin. Ares saves the registers he intends to borrow and sets up two pointers: r14 to the start of Demeter's integer table and r15 to the number of integer records to test — five in v1.7. In the loop that follows, he compares the container pointer in rdi against the ContainerCache field at the end of each record, stepping r14 by 0x30 bytes after each mismatch and decrementing r15 until he either finds a match or runs out of records.

If no record's cache matches rdi, Ares restores his borrowed registers and falls straight into the original store: mov [rdi+168],eax. In that path, the write is untouched. The stat being committed is outside Volume 2's scope, and the hook behaves as if it were not there. Given how many integer stats share these commit paths, this is the overwhelmingly common outcome.

If a record does match — if rdi equals, say, maxHealthContainerCache — Ares steps into the scaling block instead. There he does four things in order, using only the fields Demeter laid out:

  1. Sanity window: compare eax against the per-stat SanityMin and SanityMax. If it falls outside the window, increment that site's reject counter ([r14+0x18] for Site A, [r14+0x1C] or [r14+0x20] for B and C) and skip scaling entirely, writing the original value through.

  2. Readout: copy the unscaled total into IntendedValue ([r14+0x14]), which is what you see in the table as the "raw" stat the game tried to store.

  3. Scaling: convert eax to a float in xmm1, multiply by the per-stat Multiplier at [r14+0], then convert back to an integer in eax.

  4. Floor and ceiling: compare eax against the SafetyClamp floor and OutputCap ceiling at [r14+4] and [r14+8], clamping up or down as needed so no scaled value can underflow the intended baseline or overflow into absurd territory.

After that, the flow rejoins the original code. The mov [rdi+168],eax that follows is still the game's own commit; Ares has only changed what eax contains when it arrives. On a non-matching container, eax is the game's total. On a matching one, it is the multiplied and clamped variant that this volume is about.

Sites B and C share the same structure with smaller stolen regions and different reject-counter slots, but the working picture is identical: three doors, one dispatch surface, and one set of Demeter-backed parameters. When later chapters talk about "teaching Ares a new stat," what they mean is adding a row to Demeter's integer table, making sure Hermes fills its cache, and adding that stat to the compare loop — nothing more exotic than that.

Ares – Integer Hook Blocks (Three Commit Sites)

Ares owns the three integer commit sites: the last narrow corridor between the game calculating a stat total and the game storing it. Site A below is shown in full, from its 22-byte door patch through its code cave and Hestia's byte backup; Sites B and C repeat this same discipline at their own doors.

Code: Select all

// ---------- INT SITE A (22 stolen bytes): Ares' first commit door ----------
aobscanmodule(multiplierSiteAAob,HallsOfTorment.exe,48 8D 97 70 01 00 00 E8 82 9A FF FF F3 0F 2C C0 89 87 68 01 00 00)

alloc(multiplierSiteANewMem,2048,multiplierSiteAAob)  // Ares' code cave beside the first integer commit site
alloc(multiplierSiteAOriginalBytes,22)  // Hestia's 22-byte restoration reliquary
registersymbol(multiplierSiteAOriginalBytes)  // Hestia can find this backup during the return journey

label(multiplierSiteAReturnHere)
label(multiplierSiteAOriginalCode)
label(multiplierSiteACompareLoop)
label(multiplierSiteANoMatch)
label(multiplierSiteAApplyScale)
label(multiplierSiteAFloorOk)
label(multiplierSiteACapOk)
label(multiplierSiteASanityReject)
label(multiplierSiteASkipScale)

// Hestia's backup MUST be captured before the patch is assembled, or readmem would
// snapshot the injected jmp+NOPs instead of the game's original 22 bytes
multiplierSiteAOriginalBytes:
readmem(multiplierSiteAAob,22)  // Hestia's backup of the 22 bytes that Ares is about to replace

multiplierSiteAAob:
jmp multiplierSiteANewMem  // Ares diverts the 22-byte doorway into his cave
nop  // the remaining stolen-byte space is deliberately quiet
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop

// Ares returns to the game's road here after the original store
multiplierSiteAReturnHere:

multiplierSiteANewMem:

// Ares: replay stolen instructions
lea rdx,[rdi+00000170]  // restore the first stolen instruction before intervening
call HallsOfTorment.exe+F1B20  // restore the game's total-calculation call
cvttss2si eax,xmm0  // restore the conversion: eax is the integer total about to be committed

// Ares preserves the two registers he borrows, then receives Demeter's table
push r15
push r14
mov r14,multiplierStatParams  // r14 begins at Demeter's integer parameter table
mov r15,05  // five integer records stand in Demeter's table

// Ares: dispatch loop over Demeter's integer table
multiplierSiteACompareLoop:
cmp rdi,[r14+28]  // Demeter: ContainerCache field
// Hermes wrote this ContainerCache 500ms ago
je multiplierSiteAApplyScale  // the current container belongs to this record
add r14,30  // advance one 0x30-byte Demeter record
dec r15  // count down the remaining records
jne multiplierSiteACompareLoop  // continue until every integer record has been tested

// Ares found no Demeter record for this container; its commit remains untouched
multiplierSiteANoMatch:
pop r14
pop r15
jmp multiplierSiteASkipScale

// Ares: sanity window check
multiplierSiteAApplyScale:
cmp eax,[r14+0C]  // Demeter: SanityMin field
jl multiplierSiteASanityReject
cmp eax,[r14+10]  // Demeter: SanityMax field
jg multiplierSiteASanityReject

// Ares: readout to Demeter's IntendedValue slot
mov [r14+14],eax  // Demeter: IntendedValue field records the unscaled game total

// Ares: scale by Demeter's Multiplier
cvtsi2ss xmm1,eax
mulss xmm1,[r14]  // Demeter: Multiplier field
cvttss2si eax,xmm1

// Ares: clamp against Demeter's floor and ceiling
cmp eax,[r14+04]  // Demeter: floor field
jge multiplierSiteAFloorOk
mov eax,[r14+04]
multiplierSiteAFloorOk:
cmp eax,[r14+08]  // Demeter: ceiling field
jle multiplierSiteACapOk
mov eax,[r14+08]
multiplierSiteACapOk:
pop r14
pop r15
jmp multiplierSiteASkipScale

// Ares rejects implausible totals without changing the game's intended write
multiplierSiteASanityReject:
inc dword ptr [r14+18]  // Demeter: Site A sanity-reject counter
pop r14
pop r15

// Ares: fall through to original store
multiplierSiteASkipScale:

multiplierSiteAOriginalCode:
mov [rdi+00000168],eax  // Ares delivers either the scaled value or the untouched game value
jmp multiplierSiteAReturnHere

A few things to notice in shape:

  • Ares first replays every instruction displaced by the patch, then lets r14 walk Demeter's five 0x30-byte records while r15 bounds the search; only a matching ContainerCache may modify the value.
  • A matching total passes its sanity window, is copied to Demeter's IntendedValue readout, and scales through cvtsi2ss / mulss / cvttss2si.
  • The scaled integer is clamped to the record's floor and ceiling; every route then restores r14 and r15, performs mov [rdi+00000168],eax, and returns to game code.
  • Sites B and C are structurally identical, but each steals only 15 bytes instead of Site A's 22, so their replayed stolen regions are shorter.
  • Their otherwise identical sanity-reject paths increment [r14+1C] at Site B and [r14+20] at Site C, rather than Site A's [r14+18].

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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Apollo – Harmony on the Float Side

Where Ares deals in integers and brute commits, Apollo handles the softer side of the script: every stat the game tracks as a float. Health Regen, Movement Speed, Experience Gain, cone angles, crit percentages, and the rest of the Zweihänder's float block all go through Apollo's single hook. His site looks different at the disassembly level, but from the script's point of view, he plays the same role on a different type family.

Apollo's hook sits on what the annotated script calls FLOAT SITE A — a sequence that ends in movss [rdi+168],xmm0 instead of an integer store. As with Ares, Athena has already validated the AOB signature, and Hephaestus has carved out a code cave and backed up the original bytes. Apollo overwrites the site with a jmp, replays the stolen instructions inside the cave (a call into the float setter plus a load through [rbp-49]), and ends up with xmm0 holding the finished float total the engine intended to store.

From there, the pattern should feel familiar. Apollo pushes the registers he needs, points r14 at Demeter's float table, and sets r15 to the number of float records — nine in v1.7. His compare loop walks one 0x30-byte record at a time, checking whether rdi matches the ContainerCache pointer in that entry. On a mismatch, he advances r14 and decrements r15. On a match, he branches to a scaling block; if he runs out of records, he skips scaling entirely.

The difference lies in the operations he uses once he has a match. Instead of bouncing through an integer register, Apollo stays in float space:

  • He compares xmm0 against the sanity window using ucomiss against SanityMin and SanityMax in the record. Values outside that window increment the record's float-side reject counter and skip scaling; the game's original value goes through untouched.
  • He writes the unscaled float into IntendedValue as a diagnostic readout.
  • He multiplies xmm0 in place by the record's Multiplier using mulss.
  • He clamps the result with maxss and minss against the floor and ceiling, so a too-aggressive multiplier cannot push Movement Speed or Crit Chance into nonsense ranges.

At the end, just like Ares, Apollo hands control back to the original code. The movss [rdi+168],xmm0 that follows is the engine's own store; the only question is whether xmm0 still holds the raw value or the scaled one.

The important structural point is that Apollo never needs a custom path for "Health Regen" versus "Movement Speed." Demeter has already normalized those into records with the same layout. Hermes has already supplied fresh container addresses into their cache fields. Apollo only has to recognize which container he is looking at and then apply the same three-step dance: sanity, record, scale-and-clamp.

Apollo – Float Hook Block

Apollo is Ares' float-side mirror, but he guards one door only: FLOAT SITE A, whose original commit is movss [rdi+168],xmm0. He first restores the work displaced by the hook, then uses rdi to recognize a cached Demeter record and keeps the candidate total in xmm0 all the way through sanity checking, scaling, clamping, and the game's original store.

Code: Select all

// ---------- FLOAT SITE A (17 stolen bytes): Apollo's only commit door ----------
aobscanmodule(multiplierFloatSiteAAob,HallsOfTorment.exe,E8 05 74 3D 02 48 63 45 B7 F3 0F 11 87 68 01 00 00)

alloc(multiplierFloatSiteANewMem,2048,multiplierFloatSiteAAob)  // Apollo's code cave beside the float commit site
alloc(multiplierFloatSiteAOriginalBytes,17)  // Hestia's 17-byte restoration reliquary
registersymbol(multiplierFloatSiteAAob)  // Hestia can find the patch site during the return journey
registersymbol(multiplierFloatSiteAOriginalBytes)  // Hestia can find the backup during the return journey

label(multiplierFloatSiteAReturnHere)
label(multiplierFloatSiteAOriginalCode)
label(multiplierFloatSiteACompareLoop)
label(multiplierFloatSiteAApplyScale)
label(multiplierFloatSiteASanityReject)
label(multiplierFloatSiteADone)

// Hestia's backup MUST be captured before the patch is assembled, or readmem would
// snapshot the injected jmp+NOPs instead of the game's original 17 bytes
multiplierFloatSiteAOriginalBytes:
readmem(multiplierFloatSiteAAob,17)  // Hestia's backup of the 17 bytes that Apollo is about to replace

// Apollo's FLOAT SITE A patch: divert execution into the float code cave.
multiplierFloatSiteAAob:
jmp multiplierFloatSiteANewMem  // 5-byte jump to the hook (overwrites the store instruction)
nop  // nops pad the rest of the stolen bytes
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop

// execution resumes in game code here
multiplierFloatSiteAReturnHere:

multiplierFloatSiteANewMem:

// Apollo: replay stolen instructions
call HallsOfTorment.exe+24CF2A0  // stolen: game routine that computes the finished stat total into xmm0
movsxd rax,dword ptr [rbp-49]  // stolen: original sign-extended load re-executed here

push r14  // preserve the registers Apollo borrows from the game
push r15  // preserve the registers Apollo borrows from the game
mov r14,multiplierFloatStatParams  // r14 begins at Demeter's float parameter table
mov r15,09  // loop counter: 9 float stat records to test

// Apollo: dispatch loop over Demeter's float table
multiplierFloatSiteACompareLoop:
cmp rdi,[r14+20]  // Hermes writes this float table's ContainerCache at +20; does this record own rdi?
je multiplierFloatSiteAApplyScale  // match found: Apollo may shape this float stat
add r14,30  // no match: advance one Demeter record (0x30 bytes each)
dec r15  // one fewer record to check
jne multiplierFloatSiteACompareLoop  // more records to check
jmp multiplierFloatSiteADone  // no cached container matched: preserve xmm0 untouched

// container matched: Apollo now tests and shapes the total in xmm0
multiplierFloatSiteAApplyScale:
// Apollo: sanity window with ucomiss
ucomiss xmm0,[r14+0C]  // compare against Demeter's SanityMin at +0x0C
jb multiplierFloatSiteASanityReject  // below the window: reject
ucomiss xmm0,[r14+10]  // compare against Demeter's SanityMax at +0x10
ja multiplierFloatSiteASanityReject  // above the window: reject

// Apollo: readout to Demeter's IntendedValue slot
movss [r14+14],xmm0  // retain the raw total in IntendedValue at +0x14

// Apollo: scale by Demeter's Multiplier via mulss
mulss xmm0,[r14]  // apply the Multiplier at +0x00

// Apollo: clamp with maxss/minss
maxss xmm0,[r14+04]  // clamp up to Demeter's floor at +0x04
minss xmm0,[r14+08]  // clamp down to Demeter's ceiling at +0x08
jmp multiplierFloatSiteADone  // proceed to the original store with the scaled float

multiplierFloatSiteASanityReject:
inc dword ptr [r14+18]  // implausible total: increment Demeter's float-side reject counter at +0x18

multiplierFloatSiteADone:
pop r15  // restore the game register
pop r14  // restore the game register

// Apollo: fall through to original movss
multiplierFloatSiteAOriginalCode:
movss [rdi+00000168],xmm0  // the game's original write - xmm0 now carries the scaled (or untouched) value
jmp multiplierFloatSiteAReturnHere  // back to game code

A few things to notice in shape:

  • Hestia's multiplierFloatSiteAOriginalBytes buffer captures the complete 17-byte region before the jmp patch is installed. On disable, that backup is what restores the game's call, movsxd, and original movss path rather than merely removing Apollo's cave.
  • Apollo's dispatch is deliberately pointer-free inside game code: r14 advances through nine 0x30-byte Demeter records while r15 counts them down. The float record's ContainerCache is at +0x20 — not the integer table's +0x28 — and Hermes is the one who refreshes that cache slot outside the hook.
  • Once rdi matches a cache, the float row supplies every operand by offset: Multiplier +0x00, floor +0x04, ceiling +0x08, sanity bounds +0x0C and +0x10, IntendedValue +0x14, and the Site A reject counter +0x18.
  • A sanity rejection changes no float value: it increments the row's counter and lets the untouched xmm0 reach the original store. A valid match records the raw total first, then scales and clamps it in place.
  • The final movss [rdi+00000168],xmm0 remains the game's own commit. Apollo decides only which value arrives in xmm0; he does not replace the game's storage operation.

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


WanderingNovice
Cheater
Cheater
Posts: 21
Joined: Sat Jul 25, 2026 7:40 am
Answers: 0
x 10

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


Hermes – Walking the Chains So Hooks Don't

The last god to act during enable is Hermes. He never touches an instruction, never writes a jmp, and never runs inside the game's address space. His territory is Lua and pointer arithmetic, not assembly. His job is to walk the chains from the static root and deliver fresh container addresses into Demeter's cache fields on a fixed schedule, so that Ares and Apollo never again have to dereference a heap pointer in the middle of a hook.

This is not how the script started. Earlier versions did their pointer chasing inline, right inside the hook bodies. That design worked — until it didn't. The Learner Edition spends a full chapter on the crash-hunting arc: pointer slots in the chain briefly holding freed but plausible values during level transitions, hooks following those values, and the process dying in Cheat Engine-allocated code because there is no way for injected instructions to tell "valid object" from "freed memory that happens to look aligned." Hermes is the architectural answer to that failure.

In the final script, Hermes lives in the second {$lua} block under [ENABLE]. When Athena and Hephaestus have finished their work, Hermes does three things:

  1. He tears down any old timers that might be left over from prior versions — an integer-only cache, a float-only cache — so there is at most one cache timer alive at a time.

  2. He builds a chainTable: a 14-entry Lua table where each entry has a symbol (such as maxHealthContainerCache or zweihanderKnockbackContainerCache) and a list of offsets that describe the pointer chain from the static root to that stat's container. Every chain begins at HallsOfTorment.exe+396BD00 and then walks through hubs like +1C0 → +10 → +498 for defensive stats or +1C0 → +18 → +68 → +28 → +2C8 for weapon offense.

  3. He creates multiplierUnifiedCacheTimer, a Cheat Engine-side timer anchored to the main form, with a 500 ms interval and an OnTimer callback that does the real work.

The callback is deliberately simple. On each tick, Hermes:

  • First checks whether maxHealthContainerCache is still a valid symbol. If Hestia has already unregistered the symbols, the timer destroys itself and exits, so it does not outlive the script that created it.
  • Reads the static root pointer at HallsOfTorment.exe+396BD00.
  • For each entry in chainTable, starts from that root and follows the offsets one hop at a time with readPointer, bailing out to 0 if any hop yields nil or 0 — the "object is gone or not constructed yet" cases that used to be so dangerous inside hooks.
  • Looks up the target symbol's address with getAddressSafe. If that symbol still exists, writes whatever pointer it just resolved (or 0 on failure) into the ContainerCache field for that stat. If not, it does nothing and moves on.

The critical difference from earlier versions is where this work runs. Hermes' pointer walks run in Cheat Engine's own process, not in the game. A failed readPointer call here is just a nil in Lua. Writing a 0 into a cache is harmless. Ares and Apollo see that 0 in their compare loops and simply never match that record while the object does not exist, leaving all writes untouched. The worst that happens is a briefly unscaled stat during loading screens, not a crash.

When later chapters talk about "moving work across the CE–game boundary" or "deleting whole classes of pointer-safety diagnostics because the hook no longer dereferences pointers at all," they are talking about Hermes. Once his timer takes over all of the pointer-chasing burden, the hook code that used to be brittle becomes a simple cmp rdi,[r14+offset] against a value that was already validated, or safely zeroed, half a second ago.

Hermes – Cache Timer & Pointer Chains

Hermes owns the second {$lua} block under [ENABLE]: he retires any timer a prior table version left behind, describes the fourteen routes from the static root, and starts one replacement timer. Every 500 ms, that timer refreshes Demeter's ContainerCache fields outside the hooks, leaving Ares and Apollo with a cheap cached-pointer comparison rather than a dangerous heap walk at the moment the game commits a stat.

Code: Select all

if not syntaxcheck then
  -- Hermes: tear down any stale timer from prior versions
  if multiplierUnifiedCacheTimer then
    multiplierUnifiedCacheTimer.destroy()
    multiplierUnifiedCacheTimer = nil
  end
  if multiplierIntCacheTimer then
    multiplierIntCacheTimer.destroy()
    multiplierIntCacheTimer = nil
  end
  if multiplierFloatCacheTimer then
    multiplierFloatCacheTimer.destroy()
    multiplierFloatCacheTimer = nil
  end

  -- Hermes: chainTable — pointer walk recipes from static root to each Demeter cache slot
  local chainTable = {
    { symbol = 'maxHealthContainerCache', offsets = {0x1C0, 0x10, 0x498} },
    { symbol = 'healthRegenContainerCache', offsets = {0x1C0, 0x10, 0x4B8} },
    -- ... 12 more entries: block strength, defense, zweihänder stats, movement speed, XP gain, ...
  }

  -- Hermes: 500ms timer anchored to CE's main form
  multiplierUnifiedCacheTimer = createTimer(getMainForm())
  multiplierUnifiedCacheTimer.Interval = 500
  multiplierUnifiedCacheTimer.OnTimer = function(timerSelf)
    -- Hermes: self-destruct if Hestia has already unregistered symbols
    if getAddressSafe('maxHealthContainerCache') == nil then
      timerSelf.destroy()
      multiplierUnifiedCacheTimer = nil
      return
    end

    local rootPointer = readPointer('HallsOfTorment.exe+396BD00')
    for index = 1, #chainTable do
      local chainEntry = chainTable[index]
      local chainPointer = rootPointer
      -- Hermes: walk one hop, bail to 0 on nil/0
      for hopIndex = 1, #chainEntry.offsets do
        if chainPointer == nil or chainPointer == 0 then
          chainPointer = 0
          break
        end
        chainPointer = readPointer(chainPointer + chainEntry.offsets[hopIndex])
      end
      local cacheAddress = getAddressSafe(chainEntry.symbol)
      if cacheAddress ~= nil then
        -- Hermes: deliver pointer into Demeter's ContainerCache slot
        writeQword(cacheAddress, chainPointer or 0)
      end
    end
  end
end

A few things to notice in shape:

  • chainTable keeps the fourteen pointer recipes declarative: adding or correcting a route changes data, not injected hook code.
  • The pointer walk runs in Cheat Engine's Lua timer, not at an Ares or Apollo commit site. The hooks only compare rdi with the cache values Hermes last delivered.
  • A missing root or intermediate object resolves to 0; that makes the affected record fail to match safely instead of asking a hook to follow an unstable heap pointer.
  • Hestia's symbol cleanup is also Hermes' exit signal. Once getAddressSafe cannot find maxHealthContainerCache, the timer removes itself rather than surviving the disabled script.

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


Post Reply