Skip to content

Hookpoints

Distilled from a string-anchor sweep of out/strings.json and the out/decompiled_all/ corpus. Each row maps one of the community feature requests to the most promising candidate function(s) for rsmm.hook once the API ships.

Status legend:

  • anchor — we have a string constant inside the function, so the function clearly participates in this subsystem; deeper RE needed to confirm whether to hook the function itself, its caller, or one of its callees.
  • registry — function constructs the event-name table (or similar metadata); not itself a useful hookpoint, but its string-xrefs lead to the real dispatchers / consumers.
  • dispatcher — function emits events of this type; hookable directly (intercept arg) or in chain (replace pre/post).
  • consumer — function reacts to the event (likely the gameplay logic).

Glossary

The game’s internal vocabulary differs from the community’s:

Community wordEngine word
TalentSkill
ItemMagical Object (MO)
Curse / Curse itemMO with Cursed tag
Refugee / VendorRefugee Archetype (NPC archetype picker)
Aggro rangenot yet found — probably on oCEnemyCampDifficultyDefinition or similar
Difficulty/scalingCamp Difficulty Modifier + Difficulty Xp Modifier
Talent stackingSkill add — currently dedup-guarded somewhere downstream of “Skill selected”
Talent restrictionSkill rolltable filter (in Skill propose)

Anchors found so far

FeatureAnchor fn (link VA)Source string(s)Role
Game event-name registryFUN_1401d1850 (0x1401d1850)Skill propose, Skill selected, Level up, Pick Up Magical Object, Magical Object Destroyed, {Common,Rare,Epic,Legendary,Cursed} Magical Object Destroyed, Hero kill streak, Hero permadeath, Final victory never using any featherregistry
Game-state machine (modifiers)FUN_1401d6b10 (0x1401d6b10)Is in victory sequence, Is in defeat sequence, Difficulty Xp Modifier, Camp Difficulty Modifier, Camp Difficulty Modifier Chance To Apply, Half Cycle Count Before Boss Awakens Modifierdispatcher
Skill choice surface (Extra skill / Extra MO)FUN_1401d2e90 (0x1401d2e90)Extra skill choice, Extra MO Choicedispatcher
GameOptions ctor (Forced seed lives here)FUN_1401c6d60 (0x1401c6d60)Forced seedregistry — already used by mods/ExampleSeedPin
Run-end emitterFUN_1401f1a40 (0x1401f1a40)run_enddispatcher
Run-level emittersFUN_1401f3b90, FUN_1401f2c80level_reached, level_up_reachdispatcher
Run-abandon emitterFUN_140291190, FUN_1402836a0, FUN_140292090Abandondispatcher (multiple call sites — narrow before hooking)
Resource lookup (already hooked Phase 3)FUN_140487040 (0x140487040)n/aconsumer — every cooked entity-by-path resolution

Feature → action plan

The “next RE step” column points at the concrete work needed before each hook can be implemented. Most of them are short follow-ups on the anchor function (a single Ghidra xref pass, or a 10-minute decomp read).

Talents

User-requested featureAnchor / candidateNext RE step
Add talents (custom Lua-defined skill)Need: Skill propose consumerXref "Skill propose" from FUN_1401d1850; find the emitter; the row of three skills is built somewhere in that flow.
Restrict which talents can appearSame as aboveHook the skill-roll function; filter the candidate list before it’s returned to UI.
Stack same talent multiple timesFind the dedup guardXref "Skill selected" consumer; the function that records the chosen skill onto the hero almost certainly contains an if (already_has(skill_id)) reject; branch — patch / hook that.
Talent selection beyond lvl 10 (Heredos)FUN_1401d2e90 (Extra skill choice)Read decomp; find the level >= 10 guard or the cap-counter increment; either bump the cap or insert hook to grant.
”Activate all talents” buffNeed: skill-effect dispatcherFind the function that, on hero update, walks the hero’s skill list and applies each — call it from a Lua tick with every skill id.
”Pick one talent” itemNeed: Skill propose emitterSame flow as restrict; replace candidate list with a single deterministic id.
Add extra talents at level 11 (curse / legendary)FUN_1401d2e90 + skill-grantCombine: emit Extra skill choice on level 11 with a curse-tagged pool.

Magical Objects (items, curses, legendaries)

FeatureAnchorNext step
Add new MOAsset-side (entity def)Already partially supported via cooked-asset overrides; new entity needs the .gen re-encoder (separate track).
Modify MO (effects)FUN_1401d1850 events + the MO defLive-modify via memory writes on the MO’s modifier table.
Legendaries/cursed limited to 1Need: MO pool filterXref "Legendary Magical Object Destroyed" / "Cursed Magical Object Destroyed" to find the picker; insert hook that rejects a second roll of the same rarity.
Extra MO choice (already exists as engine surface)FUN_1401d2e90 (Extra MO Choice)Same function as Extra skill choice — confirm both go through one dispatcher.

Scaling / difficulty

FeatureAnchorNext step
Act 2 / Act 3 harder than baselineFUN_1401d6b10 (Camp Difficulty Modifier, Difficulty Xp Modifier)Read decomp; modifiers are floats. Two paths: (a) hook the modifier getter and multiply, (b) write the underlying float to GlobalValues_Common/NGP_Enemy_*_Modifier.globalvalue cooked file. Path (b) ships today; (a) needs hooks.
4-player density (XP / HP / stagger scaled)SameThe same modifier struct includes XP. Hook the player-count → modifier-product function; bump density independently of player count.
Movement speed / attack speed / cooldown / aggro range (player or enemy)Need: stat-getterStats live in oIEntityValueModifierComputer (in strings.json). Hook its compute() and apply a tag-keyed multiplier.

Spawn / generation

FeatureAnchorNext step
All enemies = crabsNeed: enemy-template pickerEntityCpntMethodPicker, AliasPicker, RefugeeArchetypePicker (in strings.json) are pickers. The enemy version probably has a similar name; needs a Ghidra class-walk. Once found, hook the picker, force return = crab template id.
Modify NPCs (vendors / refugees)RefugeeArchetypePickerDecompile + hook to pin a chosen archetype.
Reward / chest content by difficultyNeed: reward rollerXref "_InitAllRewards" (FUN_1401e6030) — it’s a reward-type registrar; the actual roller will be a sibling. oCDtRewardDefinition is the entity class.

Seed / run

FeatureAnchorNext step
Per-chapter seed inputForced seed (already mapped)The bool/value pair is one global. Hook the chapter-init function (xref "Chapter 1 Complete" etc.) and reseed per chapter.
Quest selection (enable/disable)Need: quest rollerNo direct anchor yet. Look for quest / Quest strings; if absent, the quest system probably re-uses _InitAllRewards-style entity drop tables.

Run-end / stats

FeatureAnchorNext step
Win / Lose / Abandon counter per hero / difficulty / coopFUN_1401d6b10 (Is in victory sequence, Is in defeat sequence) + Abandon dispatchersHook the three dispatchers; record (hero_id, difficulty, coop?) to a JSON in mods/<id>/state.json. Read-only memory access — no game-state mutation needed.
Enable shapeshifter by default on ScarletNeed: hero-load functionNo Shapeshift string found. Likely encoded as a class name. Walk Scarlet’s cooked entity files for oCDtEntityCpnt*Shapeshift* and then xref.

Pick one of:

  1. Win / Lose / Abandon counter — pure read-side hooks on the three dispatchers in FUN_1401d6b10 + FUN_140291190. No mutation, can’t break a run, easiest to prove the API works.
  2. Difficulty multiplier hook — pick the modifier-getter inside FUN_1401d6b10, multiply by N from Lua. Visible in-game immediately (enemies harder), trivial to roll back.

Either choice would prove the full chain: rsmm.resolve(name) → rsmm.hook(va, "sig", cb) → callback fires → modifies behavior → unhook on mod disable.

Methodology used (so this is repeatable)

  1. python3 over out/strings.json bucketed by feature-keyword regex (see this commit’s _re/HOOKPOINTS.md for the list).
  2. grep -rE "<exact string>" out/decompiled_all/ to find the containing functions.
  3. Most “feature event” strings (Skill propose, Camp Difficulty Modifier, etc.) collect inside two functions:
    • a giant registry function (FUN_1401d1850, FUN_1401d6b10) that constructs the event/modifier name table at boot,
    • the actual dispatcher that takes the name and routes it. The registry is found first; the dispatcher is a Ghidra xref of the name string from outside the registry function.
  4. To get the dispatcher we need a small Ghidra script — docs/_re/scripts/xrefs_to.py already exists; run it for each anchor string. That gives the consumer functions per feature.

Next pass should be a script that takes the list of anchor strings from this doc and dumps decompiled bodies of every xrefing function into out/hookpoint_candidates/. That’s the menu we hook from.