Example mods
Every example below is a complete mod. Copy the files into mods/<id>/, set
enabled = true, and run ./rsmm apply. Source lives in
docs/ExampleMods/.
Pick a starting point
Section titled “Pick a starting point”Hello — smallest Lua mod
Section titled “Hello — smallest Lua mod”The minimum to prove the loader + SDK work: log once when the game is ready and
count ticks. No assets, just manifest.toml + init.lua.
[mod]id = "ExampleSdkHello"name = "Example: SDK hello"version = "0.2.0"author = "rsmm-examples"description = "Smallest possible mod using SDK v3. Logs once on ready + bumps a counter every tick."enabled = falsesdk_version = ">=3.0,<4"-- Smallest possible mod using SDK v3.-- Demonstrates: require "rsmm", R.health.checkpoint, R.on, R.log, R.kv.
local R = require "rsmm"R.health.checkpoint("per_mod:ExampleSdkHello")
R.on("ready", function() R.log("[Hello] loaded; SDK ready")end)
R.on("tick", function() local n = R.kv.inc("tick_count") if n == 1 or n % 20 == 0 then R.log("[Hello] tick #" .. n) endend)Magic item — declarative content
Section titled “Magic item — declarative content”A custom magical object, cloned-and-patched from a vanilla rare item. No
code — the [[content]] block tells the SDK to cook a new item end-to-end.
[mod]id = "ExampleMagicItem"name = "Example: Magic Item"version = "0.3.0"author = "rsmm-examples"description = "Declarative custom magical object via [[content]] block."enabled = falsesdk_version = ">=3.0,<4"
[[content]]kind = "item"id = "Iron_Crab_Hide"base = "Common/Armor_Per_Object"name = "Iron Crab Hide"description = "Bonus armor per rare object collected."rarity = "Common"The kind = "item" builder resolves the base donor, cooks the new item, and
rsmm apply syncs the game-side registration (versiondef + UsedRscCache). You
write the manifest; the SDK does the rest. See
Authoring mods for every [[content]] kind.
React to in-game events
Section titled “React to in-game events”The loader bridges the game’s named events to the Lua bus, so a mod subscribes
by name — enemy_killed, unlock_hero, game_start, level_up_reach, … (one
hook re-publishes every named analytics event; browse the list in the
event systems reference).
Needs the loader DLL. Both event buses are armed by default; the loader
skips publishing entirely when no mod has subscribed.
local R = require "rsmm"
R.on("enemy_killed", function(ev) R.kv.inc("kills") -- persists across restarts R.log(("kill #%d"):format(R.kv.get("kills")))end)
-- Take a whole family at once, and stop listening when you're done.local h = R.on_match("^gameplay:ABILITY_", function(ev, name) R.log("ability event:", name)end)R.once("run:end", function() R.off(h) end)Not sure what fires? Let the loader tell you — it records every event it sees with its payload keys:
R.on("ready", function() R.schedule.after(120, function() R.events.dump() end) -- play, then read _log.txtend)These events are observation-grade: they fire after the action and carry the event name + sequence, not a live entity handle.
Call engine functions
Section titled “Call engine functions”data/symbols.json maps semantic names to engine functions and records each
one’s cabi, so a mod calls them like methods — no addresses, no signatures.
Browse with rsmm symbols list or R.engine.names().
local res = R.engine.fn.Resource_LookupByPath(path, 0, 0, 0)-- equivalent: R.engine.call("Resource_LookupByPath", path, 0, 0, 0)Persistent state
Section titled “Persistent state”R.kv stores per-mod scalars (string/number/boolean) to
<mod_dir>/.rsmm_state, reloaded on the next launch. Loaded lazily, flushed
automatically on exit, or on demand with R.kv.save().
R.kv.inc("runs_played") -- survives a game restartR.kv.set("last_hero", "Scarlet")local n = R.kv.get("runs_played", 0)Recording what a playtest proved
Section titled “Recording what a playtest proved”Some questions can only be answered by the running game, and a playtest is
expensive: you play, then somebody reads the log. R.exp turns one run into
several answers by recording verdicts instead of prose.
-- answerable on the spotR.exp.run("tiles_registered", "does the engine register a mod tiledef?", function() local n = count_tiledefs() R.exp.observe("tiles_registered", "tiledefs", n) return n > 237, n .. " tiledefs"end)
-- the answer arrives later, from a handlerR.exp.case("tile_placed", "is a mod tile ever placed?")R.on("gameplay:OPEN_CHEST", function() R.exp.verdict("tile_placed", true, "seen") end)Read them back after the run:
./rsmm expEach case is PASS, FAIL, or NO-DATA — and NO-DATA is a result too: it
says the code path never ran. Verdicts are written as they resolve, so a crash
part-way through a run keeps everything answered up to that point. They are
stored in R.kv, so keep observations to a handful per case.
A mod with no Lua at all can still ask a question. Declare it in
manifest.toml and it shows up as NO-DATA before the run:
[[experiment]]id = "icon_replaced"question = "do teleporter minimap icons become the shrine icon?"Some answers are only ever on the screen. Quit the game — the loader rewrites the state file when it exits — and record what you saw:
./rsmm exp answer minimap-icon-test icon_replaced pass -m "shrine icon on every teleporter"Those are marked (reported by you) in the table, because a verdict somebody
typed is a different grade of evidence from one the game measured.
Guard R.exp if your mod ships to other people: a newer mod meets an older
planted SDK whenever someone updates the mod before running
rsmm update-loader, and an unguarded call stops the whole mod from loading.
local exp = R.exp or { case = function() end, observe = function() end, verdict = function(_, p) return p end, run = function(_, _, fn) pcall(fn) end }Apply any example
Section titled “Apply any example”- Copy the example folder into your
mods/directory. - Set
enabled = truein itsmanifest.toml. - Apply and launch:
Terminal window ./rsmm apply./rsmm run
More examples in the repo
Section titled “More examples in the repo”ExampleEvents— subscribe to in-game events (enemy_killed,unlock_hero, …) and count them.ExampleEngineCall— call named engine functions viaR.engine.fn/R.engine.call.ExampleSeedPin— pin the run RNG seed via theR._internalescape-hatch + per-mod config (R.config.get).ExampleSdkAll— exercises the full SDK surface.ExampleCustomSkill— relabels one of Aladdin’s skills into a custom talent (visible on his level-up cards & Skill Menu). See Custom skills.HyperAggro— a gameplay tweak mod.
Browse them all in docs/ExampleMods/.
