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/