Skip to content

Custom skills (talents)

A hero’s skills are the talents on the level-up cards and in the Skill Menu. The skill kind makes a custom talent visible by overriding the per-hero text bank, and can optionally edit the hero definition’s skill list. Guess

Author one

skill is a guess kind, so the mod must opt in with Mod(experimental=True) (lint enforces this for non-confirmed kinds).

build.py
from rsmm import sdk
with sdk.Mod("MySkills", experimental=True) as mod:
mod.skill(
"rsmm_lightning_dash",
hero="Aladdin",
source="Attack Dive", # an existing skill to repurpose
name="Lightning Dash", # shown on the card / menu
description="A dash that crackles with lightning.",
# mode="relabel" is the default
)

Equivalent declarative form:

mods/MySkills/manifest.toml
[mod]
id = "MySkills"
experimental = true
[[content]]
kind = "skill"
id = "rsmm_lightning_dash"
hero = "Aladdin"
source = "Attack Dive"
name = "Lightning Dash"
description = "A dash that crackles with lightning."
mode = "relabel"

Run ./rsmm apply to write the text override into the live install. The display name resolves against the hero’s text bank, so apply must see a Ravenswatch installemit errors out if no install is reachable.

Modes

modeWhat it editsStatusUse it for
relabel (default)Hero text bank values only (Skill_<src>_Name / _Desc)Works — visible & count-neutralRepurposing an existing talent’s name/description
repointHerodef row + text values: remints the source row’s identity GUID in placeExperimentalGiving an existing skill a fresh identity for behaviour binding
cloneWould add a NET-NEW herodef row + appended text keyDisabled — bricks the hero(blocked — use runtime injection)

Bind behaviour

Relabelling changes only the text. To make the talent do something custom, bind logic in a loader Lua script keyed on the talent’s identity:

mods/MySkills/scripts/lightning_dash.lua
R.talent.on_pick("rsmm_lightning_dash", function(ctx)
-- runs when this talent card is picked
end)
-- or define on a hero:
R.talent.define{ hero = "Aladdin", pickable = true, on_pick = function(ctx) ... end }

Behaviour binds on POWER_UP_COLLECT_REQUEST via the gameplay event bus — it does not require any herodef edit. See Mod hooks for the identity-offset details.

Net-new skills (runtime injection)

Because a data-cloned row bricks the hero, a genuinely additive skill is added at runtime by the loader, the same way custom items grow the item pool:

flowchart LR
    L["herodef load<br/>(FUN_14031e630)"] --> H["loader detour"]
    H --> G["grow skill vector @ +0x7d0"]
    G --> A["append cloned SkillController"]
    A --> M["menu enumerates runtime vector → shows"]

The hook (hook_skills.cpp) is default read-only: it logs the live herodef and skill-vector count/cap so the layout can be confirmed in-game. The injection itself is gated behind RSMM_ENABLE_SKILL_INJECT and currently clones the last skill into a spare slot as a proof-of-path (self-guarded: no realloc, count bounds-checked). Growing the vector with a distinct controller and refcounting it is the open work.

How a skill is wired

LayerWherePurpose
Display name / descText/Hero_<Hero>_Common~GAM.xls.LocalText.gen, keys Skill_<Suffix>_Name / _DescWhat the player reads on the card / menu
Skill row (identity)Definitions/Heroes/<Hero>.herodef.ot.DtHeroDefinition.genThe list of skills the hero owns + each row’s GUID
Behaviourgameplay event bus (POWER_UP_COLLECT_REQUEST)Custom code on pick
Net-new rowloader hook_skills.cpp, herodef skill vector @ +0x7d0Adds a slot the deserialiser never validates

The herodef deserialiser (FUN_14031e630) is a strict versioned, ordered field reader: the skill collection’s count sits behind a polymorphic header, so a spliced row desyncs every later field read — which is why data-clone is disabled and runtime injection is the correct layer.

Going deeper