// ============================================================
// 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 a hardcoded address, 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 2 instructions
// that get hooked (the write and the restore) plus a 3-
// instruction tail (epilogue add, pop, ret) that is scanned as
// part of the signature but is never hooked or touched — it
// exists purely to make the match unique. hookaob points at the
// start of the pattern, 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 — 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
// a 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)