Halls Of Torment - Steampowered Games Store Page
Halls of Torment / Godot Engine 4.2
Relevant Details:
Halls Of Torment is released on the online game library platform on Steam at September 24, 2024.
This post is meant for posterity and open discussion. Education and reference is encouraged.
Initially, the post will focus on one scripting journey - creating a multiplier for XP Gems gain.
Overtime, either via requests or new contributions, there will be new cheat scripts featured in the main Cheat Table.
Writing scripts for Cheat Engine use is similar to game development learning. You are dismantling software and reassembling it your liking. Take your time.
Lastly, just have fun. Prioritize the in-game effects that you want to modify into your desired outcome.
Here inside is some documentation for you nerds out there that want to chew on more info about the script's creation process:
Relevant Descriptions:
Cheat Engine: Your primary tool for modifying values or game code in memory.
Pointers: Set of memory addresses or code that lead to a certain address containing your desired value (usually).
Godot Engine: The game's engine that computes for its effects. Your biggest friend and obstacle.
XP Gems: The game's primary and temporary resource for determining level-ups per run.
Volume 01: Halls of Torment - Script for XP Gems Multiplier
Here below inside this spoiler container is our code for the above title:
Code: Select all
[ENABLE]
aobscanmodule(hookaob,HallsOfTorment.exe,48 89 47 08 48 8B 5C 24 40 48 83 C4 20 5F C3)
alloc(newmem,2048,hookaob)
alloc(multiplier,4)
alloc(targetaddr,8)
alloc(originalbytes,9)
registersymbol(multiplier)
registersymbol(targetaddr)
registersymbol(originalbytes)
label(returnhere)
label(originalcode)
label(nomatch)
label(resolvedone)
label(skipmultiply)
multiplier:
dd (int)2
targetaddr:
dq 0
originalbytes:
readmem(hookaob,9)
hookaob:
jmp newmem
nop
nop
nop
nop
returnhere:
newmem:
push rax
mov rax,["HallsOfTorment.exe"+396BC20]
test rax,rax
je resolvedone
mov rax,[rax+68]
test rax,rax
je resolvedone
mov rax,[rax+28]
test rax,rax
je resolvedone
add rax,5C0
mov [targetaddr],rax
resolvedone:
pop rax
push rbx
lea rbx,[rdi+08]
cmp rbx,[targetaddr]
pop rbx
jne nomatch
push rcx
push rdx
mov rcx,[rdi+08]
mov rdx,rax
sub rdx,rcx
cmp rdx,0
jle skipmultiply
imul edx,[multiplier]
skipmultiply:
add rcx,rdx
mov rax,rcx
pop rdx
pop rcx
nomatch:
originalcode:
mov [rdi+08],rax
mov rbx,[rsp+40]
jmp returnhere
[DISABLE]
hookaob:
readmem(originalbytes,9)
unregistersymbol(multiplier)
unregistersymbol(targetaddr)
unregistersymbol(originalbytes)
dealloc(newmem)
dealloc(multiplier)
dealloc(targetaddr)
dealloc(originalbytes)
And here is the code with annotations:
Code: Select all
// ============================================================
// HALLS OF TORMENT — XP GEM MULTIPLIER
// Summary of Script intention: hooks the shared Variant-write
// routine used by 158+ game fields, filters for writes landing
// on the live XP counter address (resolved via a 3-hop
// pointer chain), and scales only the positive delta by
// "multiplier" (editable live in the cheat table). Since XP
// Gems add their value directly onto this counter, scaling the
// counter's gains effectively multiplies every XP Gem picked
// up, speeding up in-run Level Ups. AOB-scanned and
// self-restoring, so it's safe to enable/disable repeatedly and
// self-relocates if a game update shifts the executable's code
// layout.
// ============================================================
[ENABLE]
// ============================================================
// RESILIENCE — AOB SCAN FOR THE HOOK SITE
// Instead of trusting the hardcoded address 24CCB01, this scans
// the actual bytes of the hook site and its surroundings and
// binds whatever address matches to "hookaob". If a future game
// update shifts the executable's layout, this re-locates the
// same instructions automatically instead of silently hooking
// the wrong (or no) address. The pattern covers 5 instructions
// (write, restore, epilogue, pop, ret) starting exactly at our
// hook target, so hookaob IS the hook address — no offset math
// needed.
// ============================================================
aobscanmodule(hookaob,HallsOfTorment.exe,48 89 47 08 48 8B 5C 24 40 48 83 C4 20 5F C3)
// ============================================================
// SETUP
// Reserves memory for our injected code and our two editable
// values, then registers their names so the cheat table and
// the rest of this script can refer to them.
// ============================================================
alloc(newmem,2048,hookaob) // reserve 2048 bytes of memory near the scanned hook site for our injected code
alloc(multiplier,4) // reserve 4 bytes to hold our editable multiplier value
alloc(targetaddr,8) // reserve 8 bytes to hold the resolved XP counter address
alloc(originalbytes,9) // reserve 9 bytes to hold a live snapshot of the hook site's real bytes
registersymbol(multiplier) // expose "multiplier" so it can be added to the cheat table
registersymbol(targetaddr) // expose "targetaddr" for optional inspection in the cheat table
registersymbol(originalbytes) // required so DISABLE's readmem() can resolve this address by name (not for table display)
label(returnhere) // where execution resumes in the original code after our injected logic runs
label(originalcode) // marks the replayed original instructions inside newmem
label(nomatch) // where execution goes when the write isn't targeting the XP counter
label(resolvedone) // where the pointer-chain walk jumps to if it hits a null link partway through
label(skipmultiply) // where execution goes when the value change is not a gain (e.g. a decrease or reset)
// ============================================================
// DATA
// "multiplier" is what you will see and edit directly in the
// cheat table (e.g. change 2 to 10 for a 10x XP gain per gem).
// "targetaddr" is internal bookkeeping only — it always holds
// the freshly resolved memory address of the character's XP
// counter, refreshed on every hook call.
// ============================================================
multiplier:
dd (int)2 // default multiplier: doubles every genuine XP gain
targetaddr:
dq 0 // placeholder until the first successful pointer-chain resolution
// ============================================================
// BACKUP — LIVE SNAPSHOT OF THE ORIGINAL HOOK-SITE BYTES
// Reads whatever 9 bytes are actually sitting at the hook site
// right now, before we touch anything, and stores an exact copy
// in "originalbytes". DISABLE restores this literal captured
// copy rather than us retyping equivalent-looking instructions,
// so the restore is guaranteed byte-for-byte identical to
// whatever was really there — no transcription risk at all.
// ============================================================
originalbytes:
readmem(hookaob,9) // copy the live 9 bytes at hookaob into our buffer, right here, right now
// ============================================================
// HOOK INSTALLATION
// Overwrites the start of the shared Variant-write routine
// (used by 158+ fields, not just the XP counter) with a jump
// into our own code. 5 bytes for the jmp plus 4 NOPs covers
// the full 9 bytes of the two original instructions it replaces,
// so nothing downstream gets corrupted. "hookaob" is wherever
// the AOB scan above found our signature — normally the same
// address as 24CCB01, but self-locating if the game updates.
// The backup just above already captured these bytes live, so
// this overwrite is fully reversible regardless of exactly what
// these two instructions turn out to be on any given game build.
// ============================================================
hookaob:
jmp newmem
nop
nop
nop
nop
returnhere:
// ============================================================
// INJECTED LOGIC
// Runs on every single Variant write in the game (any of the
// 158+ fields sharing this routine), so everything here must
// both identify OUR field specifically and tolerate firing
// constantly, including at moments when the XP system may
// not exist yet (loading screens, menus).
// ============================================================
// ============================================================
// --- Step 1: re-resolve the live XP counter address ---
// Walks "HallsOfTorment.exe"+396BC20 -> +68 -> +28 -> +5C0 fresh
// on every call, so it self-corrects if the underlying container
// ever moves. Bails out early (keeping the last known-good
// address) the moment any link in the chain is null, instead of
// crashing on an invalid dereference during loading/menus.
// NOTE: +396BC20 stays a hardcoded static module offset. An
// AOB-based upgrade (same idea as "hookaob" above) was
// investigated but hit a dead end: no in-game action was found
// to trigger any real game-code instruction reading this address
// (only our own script's compiled code touched it). Since this
// offset is itself static and reliable across restarts, leaving
// it hardcoded is the correct call rather than forcing an AOB fix.
// ============================================================
newmem:
push rax // preserve the value the game is about to write; rax gets reused below
mov rax,["HallsOfTorment.exe"+396BC20] // hop 0: dereference the static base pointer
test rax,rax // check the result isn't null before going further
je resolvedone // bail out safely if it is
mov rax,[rax+68] // hop 1: apply Offset 0 and dereference
test rax,rax
je resolvedone
mov rax,[rax+28] // hop 2: apply Offset 1 and dereference
test rax,rax
je resolvedone
add rax,5C0 // hop 3: apply the final offset (no more dereferencing)
mov [targetaddr],rax // store the freshly resolved address
resolvedone:
pop rax // restore the value the game was about to write
// ============================================================
// --- Step 2: is this write actually targeting the XP counter? ---
// The routine we hooked writes to many different fields; this
// compares the CURRENT write's destination (rdi+08) against our
// resolved XP counter address before touching anything.
// ============================================================
push rbx
lea rbx,[rdi+08] // rbx = the destination this particular write is aiming at
cmp rbx,[targetaddr] // does it match our XP counter address?
pop rbx
jne nomatch // if not, skip straight to replaying the original write untouched
// ============================================================
// --- Step 3: scale only the amount actually gained ---
// The field holds the running XP total, not a delta, so
// multiplying the whole value (rax) would compound the total on
// every write. Instead we recover the real delta (new total
// minus what's currently stored) and multiply only that, then
// add it back onto the untouched old total. A non-positive
// delta (the game decreasing or resetting the counter itself,
// e.g. bookkeeping around a Level Up, rather than a genuine gem
// pickup) is left alone entirely.
// The multiply uses edx (32-bit), not rdx (64-bit): multiplier
// was only ever allocated as 4 bytes, so a 64-bit read here
// would spill into the adjacent targetaddr allocation and
// splice a chunk of a real memory address into the multiplier,
// producing an astronomically inflated result. Using the 32-bit
// register keeps the read correctly bounded to those 4 bytes.
// ============================================================
push rcx
push rdx
mov rcx,[rdi+08] // old total, still sitting in memory before this write lands
mov rdx,rax // new total the game intended to write
sub rdx,rcx // rdx = delta = new - old (the real amount of XP being gained)
cmp rdx,0 // is this actually a gain?
jle skipmultiply // if it's zero or negative (a decrease/reset), leave it untouched
imul edx,[multiplier] // scale only the gained amount (32-bit read, matches multiplier's true size)
skipmultiply:
add rcx,rdx // old total + (possibly scaled) delta
mov rax,rcx // this is what actually gets written below
pop rdx
pop rcx
// ============================================================
// --- Step 4: replay the original instructions ---
// These are the two real instructions our jmp overwrote; they
// must run exactly as before so the game's own logic (and the
// stack layout the function expects) stays intact.
// ============================================================
nomatch:
originalcode:
mov [rdi+08],rax // the original write, now carrying our (possibly scaled) value
mov rbx,[rsp+40] // the second original instruction our jmp also overlapped
jmp returnhere // resume normal execution right after our hook
[DISABLE]
// ============================================================
// DISABLE
// Restores the exact bytes captured in "originalbytes" back onto
// the hook site and releases everything we allocated and
// registered. Uses "hookaob" (the scanned address) rather than
// the raw address, since the two must always match whatever the
// scan found. Restoring the literal captured snapshot instead of
// retyped instructions guarantees a perfect, byte-identical undo.
// ============================================================
hookaob:
readmem(originalbytes,9) // paste the exact bytes we captured at enable-time straight back
unregistersymbol(multiplier)
unregistersymbol(targetaddr)
unregistersymbol(originalbytes)
dealloc(newmem)
dealloc(multiplier)
dealloc(targetaddr)
dealloc(originalbytes)
Then here, we have the cheat table material as referenced by this discussion:
Halls_Of_Torment_Windows_Steam_Published_Version_(Update_2026-April-14)_(XP_Gems_Multiplier).CT
- XP Gems Multiplier Script + Pointers + Manipulatable Multiplier Address Field
- (28.38 KiB) Downloaded 1 time
And of course, in the replies section with be individual chapters for breakdown of the script's journey. This is meant to allow google and other search engines (or aggregators like Artificial Intelligence searching algorithms) to immediately pick up on the context of the discussion in this thread. Again, this post will serve as posterity and accessibility to those who want to go further with their Cheat Engine journeys.
May we see plenty of cheat scripts proliferate everywhere! (So that I don't have to write my own for every game I want to mess around with, lol).
