<?xml version="1.0" encoding="utf-8"?>
<CheatTable CheatEngineTableVersion="45">
  <CheatEntries>
    <CheatEntry>
      <ID>18</ID>
      <Description>"Building Blocks Section (For Advanced Users Only!!!)"</Description>
      <Options moHideChildren="1"/>
      <Color>808080</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>10</ID>
          <Description>"Pointer Addresses"</Description>
          <Options moHideChildren="1"/>
          <Color>808080</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>11</ID>
              <Description>"XP Gems Pointer (3 Offsets) "</Description>
              <Color>808080</Color>
              <VariableType>4 Bytes</VariableType>
              <Address>"HallsOfTorment.exe"+0396BC20</Address>
              <Offsets>
                <Offset>5C0</Offset>
                <Offset>28</Offset>
                <Offset>68</Offset>
              </Offsets>
            </CheatEntry>
            <CheatEntry>
              <ID>12</ID>
              <Description>"XP Gems Pointer (4 Offsets) "</Description>
              <Color>808080</Color>
              <VariableType>4 Bytes</VariableType>
              <Address>"HallsOfTorment.exe"+0396BD00</Address>
              <Offsets>
                <Offset>5C0</Offset>
                <Offset>28</Offset>
                <Offset>68</Offset>
                <Offset>170</Offset>
              </Offsets>
            </CheatEntry>
            <CheatEntry>
              <ID>13</ID>
              <Description>"XP Gems Pointer (4 Offsets) "</Description>
              <Color>808080</Color>
              <VariableType>4 Bytes</VariableType>
              <Address>"HallsOfTorment.exe"+0396BD00</Address>
              <Offsets>
                <Offset>5C0</Offset>
                <Offset>28</Offset>
                <Offset>68</Offset>
                <Offset>278</Offset>
              </Offsets>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>21</ID>
          <Description>"Original Cheat Script Codes (REVIEW ONLY - DO NOT ACTIVATE SCRIPTS BELOW)"</Description>
          <Options moHideChildren="1"/>
          <Color>0000FF</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>22</ID>
              <Description>"XP Gem Multiplier (Original Code) (Godot 4.2)"</Description>
              <Options moHideChildren="1"/>
              <Color>FFFF00</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]

// ============================================================
// 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,"HallsOfTorment.exe"+24CCB01)     // reserve 2048 bytes of memory near the 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 Torment Shards address
registersymbol(multiplier)                          // expose "multiplier" so it can be added to the cheat table
registersymbol(targetaddr)                          // expose "targetaddr" for optional inspection in the cheat table

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 Torment Shards
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 spend/refund)

// ============================================================
// DATA
// "multiplier" is what you will see and edit directly in the
// cheat table (e.g. change 2 to 10 for a 10x shard gain).
// "targetaddr" is internal bookkeeping only — it always holds
// the freshly resolved memory address of the Torment Shards
// value, refreshed on every hook call.
// ============================================================
multiplier:
dd (int)2      // default multiplier: doubles every genuine shard gain

targetaddr:
dq 0           // placeholder until the first successful pointer-chain resolution

// ============================================================
// HOOK INSTALLATION
// Overwrites the start of the shared Variant-write routine
// (used by 158+ fields, not just Torment Shards) 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.
// ============================================================
"HallsOfTorment.exe"+24CCB01:
jmp newmem
nop
nop
nop
nop
returnhere:

/*
HallsOfTorment.exe+24CCAF0 - 48 8B 43 10           - mov rax,[rbx+10]
HallsOfTorment.exe+24CCAF4 - 48 89 47 10           - mov [rdi+10],rax
HallsOfTorment.exe+24CCAF8 - 48 8B 43 08           - mov rax,[rbx+08]
HallsOfTorment.exe+24CCAFC - 48 8B 74 24 38        - mov rsi,[rsp+38]
-------------------------- &gt; Hook Start Here Below &lt; ----------------
HallsOfTorment.exe+24CCB01 - 48 89 47 08           - mov [rdi+08],rax
HallsOfTorment.exe+24CCB05 - 48 8B 5C 24 40        - mov rbx,[rsp+40]
-------------------------- &gt; Hook End Here Above   &lt; ----------------
HallsOfTorment.exe+24CCB0A - 48 83 C4 20           - add rsp,20
HallsOfTorment.exe+24CCB0E - 5F                    - pop rdi
HallsOfTorment.exe+24CCB0F - C3                    - ret
*/

// ============================================================
// 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 shard system may
// not exist yet (loading screens, menus).
// ============================================================
newmem:

// --- Step 1: re-resolve the live Torment Shards address ---
// Walks "HallsOfTorment.exe"+396BC20 -&gt; +68 -&gt; +28 -&gt; +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.
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 Torment Shards? ---
// The routine we hooked writes to many different fields; this
// compares the CURRENT write's destination (rdi+08) against our
// resolved shard 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 shard 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 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 (a spend
// or a refund at the Scriptor) 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 being gained or spent)
cmp rdx,0                // is this actually a gain?
jle skipmultiply         // if it's zero or negative (a spend/refund), 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 two original instructions at the hook site and
// releases everything we allocated and registered.
// ============================================================
"HallsOfTorment.exe"+24CCB01:
mov [rdi+08],rax
mov rbx,[rsp+40]

unregistersymbol(multiplier)
unregistersymbol(targetaddr)
dealloc(newmem)
dealloc(multiplier)
dealloc(targetaddr)
</AssemblerScript>
              <CheatEntries>
                <CheatEntry>
                  <ID>24</ID>
                  <Description>"XP Gem Amount"</Description>
                  <Color>808000</Color>
                  <VariableType>4 Bytes</VariableType>
                  <Address>"HallsOfTorment.exe"+0396BC20</Address>
                  <Offsets>
                    <Offset>5C0</Offset>
                    <Offset>28</Offset>
                    <Offset>68</Offset>
                  </Offsets>
                </CheatEntry>
                <CheatEntry>
                  <ID>28</ID>
                  <Description>"XP Gem Multiplier Value"</Description>
                  <ShowAsSigned>0</ShowAsSigned>
                  <Color>808000</Color>
                  <VariableType>4 Bytes</VariableType>
                  <Address>multiplier</Address>
                </CheatEntry>
              </CheatEntries>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>30</ID>
      <Description>"Note: Table updated for Steam version [Build ID 22764619 — "HoT Fixes | 2026-04-14"]"</Description>
      <Color>FF00FF</Color>
      <GroupHeader>1</GroupHeader>
    </CheatEntry>
    <CheatEntry>
      <ID>32</ID>
      <Description>"Originally Published on [opencheattables.com]"</Description>
      <Color>FF00FF</Color>
      <GroupHeader>1</GroupHeader>
    </CheatEntry>
    <CheatEntry>
      <ID>31</ID>
      <Description>"Make sure you are attached to Halls of Torment process before going further."</Description>
      <Color>FF00FF</Color>
      <GroupHeader>1</GroupHeader>
    </CheatEntry>
    <CheatEntry>
      <ID>19</ID>
      <Description>"&gt;--------------------Cheat Scripts Here--------------------&lt;"</Description>
      <Options moHideChildren="1"/>
      <Color>4080FF</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>27</ID>
          <Description>"XP Gem Multiplier (AoB version 2.0)"</Description>
          <Options moHideChildren="1"/>
          <Color>FFFF00</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>// ============================================================
// 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 -&gt; +68 -&gt; +28 -&gt; +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)
</AssemblerScript>
          <CheatEntries>
            <CheatEntry>
              <ID>23</ID>
              <Description>"XP Gem Multiplier Value"</Description>
              <ShowAsSigned>0</ShowAsSigned>
              <Color>808000</Color>
              <VariableType>4 Bytes</VariableType>
              <Address>multiplier</Address>
            </CheatEntry>
            <CheatEntry>
              <ID>29</ID>
              <Description>"XP Gem Amount"</Description>
              <Color>808000</Color>
              <VariableType>4 Bytes</VariableType>
              <Address>"HallsOfTorment.exe"+0396BC20</Address>
              <Offsets>
                <Offset>5C0</Offset>
                <Offset>28</Offset>
                <Offset>68</Offset>
              </Offsets>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
  </CheatEntries>
  <CheatCodes>
    <CodeEntry>
      <Description>Original Code - Changes multiple values other than souls :mov [rdi+08],rax</Description>
      <AddressString>HallsOfTorment.exe+24CCB01</AddressString>
      <Before>
        <Byte>48</Byte>
        <Byte>8B</Byte>
        <Byte>74</Byte>
        <Byte>24</Byte>
        <Byte>38</Byte>
      </Before>
      <Actual>
        <Byte>48</Byte>
        <Byte>89</Byte>
        <Byte>47</Byte>
        <Byte>08</Byte>
      </Actual>
      <After>
        <Byte>48</Byte>
        <Byte>8B</Byte>
        <Byte>5C</Byte>
        <Byte>24</Byte>
        <Byte>40</Byte>
      </After>
    </CodeEntry>
  </CheatCodes>
  <UserdefinedSymbols/>
  <Comments>============================================================
HALLS OF TORMENT — XP GEM MULTIPLIER
============================================================

WHAT THIS DOES
Hooks the shared Variant-write routine used by 158+ in-game
fields, isolates the live XP counter (the value that fills up
from every XP Gem pickup and drives in-run Level Ups), and
multiplies only the amount actually gained on each write by an
editable "multiplier" value. Non-gain writes (decreases or
resets) are left untouched, so nothing else about the counter's
normal behavior changes.

TARGET
- Pointer chain: "HallsOfTorment.exe"+396BC20 -&gt; +68 -&gt; +28 -&gt; +5C0
- Hook site: AOB-scanned
  (48 89 47 08 48 8B 5C 24 40 48 83 C4 20 5F C3),
  self-relocates automatically if a game update shifts the
  executable's code layout.

VERIFIED AGAINST
Build ID 22764619 — "HoT Fixes | 2026-04-14"
(pushed live 14 April 2026, per SteamDB:
https://steamdb.info/patchnotes/22764619/)
If Halls of Torment has since updated past this build, re-check
that the pointer chain and AOB pattern still resolve correctly
before trusting the multiplier's output.

HOW TO USE
1. Enable this entry's checkbox in the cheat table.
2. Edit "multiplier" (default: 2) to whatever multiplier you
   want each XP Gem's effective value to be scaled by.
3. Disable the checkbox at any time to cleanly restore the
   original game code byte-for-byte — safe to toggle repeatedly.

SAFETY NOTES
- The scaling multiply is a 32-bit signed operation (imul edx),
  so an extremely high multiplier combined with a very large
  single gem pickup could in theory overflow. Keep the
  multiplier at a modest value (low double digits or under) to
  stay well clear of that ceiling.
- All hook-site bytes are captured live at enable-time and
  restored exactly on disable — no permanent changes are ever
  made to the game's files on disk.</Comments>
</CheatTable>
