Everything a mod can do through SlormAPI: every command with its fields and results, the events, draw programs and widgets, Lua mods, settings, data files, and the game facts you need to use them.
- How it works
- Making a mod — C++ · Lua · external tool (dev server)
- Values, paths and roots
- Commands
- Game state:
pinggetkeyssetcallinstancesobjectsscripts - Hooks:
hookunhookhookshooklog - Drawing and UI:
ui_addui_removeui_refreshoverlay - Events:
subscribeunsubscribeeventsevent_types - Mods:
register_modmodsmods_pagemod_enable - Settings:
register_settingssettingsset_setting - Data files:
mod_datamod_log - Game knowledge:
item_infostat_infostat_listhero_statsslot_infolegendary_infoskill_info - Legendaries:
register_legendaryunregister_legendarylegendarieslegendary_equippedlegendary_droplegendaries_waiting - Buffs:
buff_addbuff_removebuff_stacks - Other:
batchperfrunnerroutinesroutine_index
- Game state:
- Events
- Draw programs
- Widgets (UI kit)
- Custom legendaries
- Lua reference
- Items
- Game cheat sheet
- Rules of thumb
SlormAPI is an Aurie module built on YYToolkit 5. The Slormancer is a GameMaker game compiled with YYC: its logic is native code, but its variables, instances, scripts and built-in functions are reachable at run time, and that is what SlormAPI exposes.
Every request is one JSON object with a cmd; every response is
{"ok": true, "result": ...} {"ok": false, "error": "what went wrong"}The same commands are available in three ways:
| from | how | owner |
|---|---|---|
| a C++ Aurie module | SlormAPIInterface::Request(json) (+ Subscribe for events) |
whatever you pass as "owner" |
| a Lua mod | slorm.request{...} and the helpers on top of it |
forced to "lua:<file name>" |
| an external tool | newline-delimited JSON on 127.0.0.1:47950 (off by default) |
whatever you pass |
Everything runs on the game thread. Requests from C++ and Lua run immediately; TCP requests are processed between the game's object events (never inside a hooked script call). Events are delivered once per frame.
Owners. Many commands take "owner": your mod's name. It keeps mods apart — each owner has its own hook layer
on a script, its own captured-call log, its own event queue, its own UI programs and its own data file. Use the
same name as in register_mod, so the Mods page can attribute problems (e.g. a script your mod could not hook
after a game update) to your mod. Without owner you share the "" owner with development tools.
An Aurie module (a 64-bit DLL) in mods/aurie/. You need the Aurie / YYToolkit 5 SDK headers (in SlormAPI's
third_party/YYToolkit; BuildFilter's repository shows a complete project) and SlormAPI's one header,
source/SlormAPI_Interface.hpp:
struct SlormAPIInterface : public Aurie::AurieInterfaceBase
{
virtual const char* Request(const char* RequestJson) = 0; // -> {"ok":..., "result"/"error":...}
virtual uint64_t Subscribe(const char* Event, SlormEventCallback Callback, void* User) = 0; // 0 = unknown event
virtual void Unsubscribe(uint64_t SubscriptionId) = 0;
};
using SlormEventCallback = void (*)(const char* Event, const char* DataJson, void* User);A minimal mod:
#include <YYToolkit/YYTK_Shared.hpp>
#include "SlormAPI_Interface.hpp"
using namespace Aurie;
using namespace YYTK;
static YYTKInterface* g_Yytk = nullptr;
static SlormAPIInterface* g_Api = nullptr;
static void OnKill(const char* Event, const char* DataJson, void* User)
{
// DataJson: {"enemy": {"$inst": ..., "$object": ...}, "x": ..., "y": ..., "level": ..., "profile": "elite"}
}
// runs for every object event; we use the UI object's Step (once per frame, also in menus)
static void OnObjectEvent(FWCodeEvent& Context)
{
CCode* code = std::get<2>(Context.Arguments());
if (!code || !code->GetName() || strcmp(code->GetName(), "gml_Object_obj_ui_next_gen_Step_0") != 0) return;
if (!g_Api)
{
// modules load in any order: ask for SlormAPI from a game callback, not in ModuleInitialize
AurieInterfaceBase* base = nullptr;
if (!AurieSuccess(ObGetInterface(SLORMAPI_INTERFACE_NAME, base)) || !base) return;
g_Api = static_cast<SlormAPIInterface*>(base);
g_Api->Request(R"({"cmd":"register_mod","name":"MyMod","version":"1.0.0","author":"me",
"description":"What it does","requires":{"SlormAPI":"1.0.0"},"game":"1.0.2c"})");
g_Api->Subscribe("enemy_killed", OnKill, nullptr);
}
// ... per-frame work: g_Api->Request(...)
}
EXPORTED AurieStatus ModuleInitialize(IN AurieModule* Module, IN const fs::path& ModulePath)
{
g_Yytk = YYTK::GetInterface();
if (!g_Yytk) return AURIE_MODULE_DEPENDENCY_NOT_RESOLVED;
return g_Yytk->CreateCallback(Module, EVENT_OBJECT_CALL, reinterpret_cast<PVOID>(OnObjectEvent), 0);
}
EXPORTED AurieStatus ModuleUnload(IN AurieModule* Module, IN const fs::path& ModulePath)
{
if (g_Yytk) g_Yytk->RemoveCallback(Module, reinterpret_cast<PVOID>(OnObjectEvent));
return AURIE_SUCCESS;
}- Build: Visual Studio 2022, x64 Release DLL, C++20. Copy BuildFilter's
.vcxprojas a starting point. - The string
Requestreturns stays valid until your nextRequest; copy what you keep.DataJsonin an event callback is valid only during the call. - A callback that crashes is unsubscribed (and logged) instead of taking the game down.
- YYToolkit refuses two callbacks that are byte-identical functions; the linker's identical-code folding (
/OPT:ICF) can make two of yours identical. Keep their bodies different or turn ICF off. - Use
"owner": "MyMod"on your hooks, UI, events and data (the C++ side has no forced owner).
One text file in <The Slormancer>/mods/lua/, e.g. my_mod.lua. SlormAPI loads it while the game runs, reloads it
2 seconds after you save it, and lists it on the Mods page (switching it off renames it to my_mod.lua.disabled and
unloads it at once).
slorm.mod{ name = "Kill Counter", version = "1.0", author = "me", description = "Counts your kills" }
local kills = 0
slorm.on("enemy_killed", function(e)
kills = kills + 1
slorm.ui("main", { slorm.text(1300, 120, "Kills: " .. kills, "ft_standard_16") })
end)As a pack (for Vortex). A folder mods/lua/<folder>/ with a lua-pack.json and the script runs too, when the
manifest requires slorm.slormapi:
{ "schema_version": 1, "type": "slorm-lua", "id": "my_mod", "name": "My Mod", "version": "1.0.0",
"entry": "my_mod.lua", "requires": { "slorm.slormapi": "1.0.2" } }id(lower case,a-z 0-9 _ . -) names its files: its owner islua:<id>and its data filelua_<id>.json.name,authoranddescriptionare its row on the Mods page;nameis also the name its settings are kept under and whatslorm.display_name()returns. The script does not needslorm.mod{}(1.0.3+); what that sets wins.entryis the script (defaultmain.lua); only that file runs (norequireof other files).requires["slorm.slormapi"]is the minimum SlormAPI version; an older SlormAPI lists the pack with "Needs SlormAPI x+" and does not run it. A pack withoutslorm.slormapibelongs to another Lua runtime and is ignored.- Switching it off renames
lua-pack.jsontolua-pack.json.disabled; a pack Vortex deployed is switched in Vortex. - A pack and a single file with the same name: the pack runs (the log says so).
- Vortex installs only manifests with versions as
x.y.zand no other fields than these,author,description,$schemaandminimum_runtime.
Zip it in the game-folder layout (mods/lua/<id>/lua-pack.json, mods/lua/<id>/<entry>): The Slormancer's Vortex
extension installs it as a Lua pack (into mods/Lua/<id>/), and unzipping it into the game folder works too.
examples/lua_pack/ is a pack to start from; its pack.cmd checks lua-pack.json by the extension's rules and
makes that zip.
See Lua reference for every function and the sandbox rules. examples/lua/kill_counter.lua is a
small complete mod; BuildFilter's lua/BuildFilter.lua is a big one
(hooks, draw programs, clicks, item conversion), written next to its C++ twin.
For tools, tests and reverse engineering. Off for players; it starts when the environment variable
SLORMAPI_SERVER=1 (or SLORMAPI_PORT=<port>) is set, or an empty file SlormAPI.server is next to the DLL.
127.0.0.1:47950(loopback only), one JSON request per line, one JSON response per line, in order.- A request's
"id"is copied into its response. - Connections that start like an HTTP request are dropped (web pages cannot reach it); a line over 16 MB ends the connection.
- While it is on, any program on the PC can control the game.
import json, socket
s = socket.create_connection(("127.0.0.1", 47950)); f = s.makefile("rw")
f.write(json.dumps({"cmd": "get", "path": "gold"}) + "\n"); f.flush()
print(json.loads(f.readline())) # {'result': 749312, 'ok': True}JSON → game. Numbers, strings, booleans, null (undefined) and arrays as you expect; objects become structs,
except:
| JSON | becomes |
|---|---|
{"$inst": 100856} |
that instance (fails if it does not exist or is deactivated) |
{"$asset": "spr_menu_grey_button"} |
the asset's index (sprite, object, script, font, room, sound...) |
Game → JSON. Instances, arrays and structs are expanded to depth levels (default 1); below that:
| JSON | is |
|---|---|
{"$inst": id, "$object": "obj_loot", "$x": .., "$y": .., "$bbox": [l, r, t, b]} |
an instance (with its variables when expanded) |
{"$array": 12} |
an array of 12 entries, not expanded |
{"$struct": 5} |
a struct with 5 members, not expanded |
{"$method": "..."} |
a method / script reference |
{"$ptr": "0x..."}, {"$ref": "...", "id": n} |
pointers, references that are not instances |
"$truncated" / "$truncated": true |
the 500 000-value budget per response ran out |
Paths walk from a root: "a.b[3].c", or the same as a list ["a", "b", 3, "c"]. Names are variables (instance,
global or struct members) or engine built-ins (x, y, depth, object_index, visible, room,
instance_count, ...). Reading never creates variables.
Roots ("root", and "self" for call):
| root | means |
|---|---|
omitted / "global" |
the global instance (global.*) |
100856 |
the instance with that id |
{"$inst": 100856} |
the same |
"obj_parent_hero" |
the first instance of that object |
Field types: s string, n number, b boolean, a array, o object. Defaults in brackets.
→ {version, frame, code_events, wndproc_calls, uptime_s, room, fps_real, event_listeners: {callbacks: {event: count}, owners: [..]}, game_version, last_session: {crashed, during_startup}}. game_version is global.version (e.g.
"1.0.2c"); last_session.crashed is true when the previous game session did not end normally.
| field | |
|---|---|
path s/a |
what to read (empty = the root itself) |
root |
see roots |
depth n [1] |
how far arrays / structs / instances are expanded |
→ the value. {"cmd":"get","path":"inventory[0][0]","depth":3} = the current Knight's helmet (item array).
path, root → the member names of a struct or instance (instance variables, not built-ins).
| field | |
|---|---|
path s/a |
required |
value |
JSON → game as above |
root |
→ true. Array entries are written in place (the game's own array changes; set past the end grows it). A
missing variable is created. Built-ins (x, visible, ...) work too.
| field | |
|---|---|
fn s |
a game script (scr_..., with or without gml_Script_) or an engine function (instance_destroy, array_length, ...) |
args a [] |
JSON → game |
self |
the instance it runs as (a root; default the global instance) |
depth n [1] |
of the result |
→ the return value. Scripts that use self / other need the right self (e.g. scr_create_loot runs as the hero).
A GML runtime error inside the call comes back as an error, but can leave the game in a bad state: test unknown
scripts in a throwaway game.
| field | |
|---|---|
object |
an object name, index, or "all" (children count: obj_parent_hero finds the hero) |
vars a |
variables or built-ins to add to each (not expanded: depth 0) |
→ [{"$inst", "$object", "$x", "$y", "$bbox", <vars>...}, ...]. Deactivated instances are not listed.
→ every object name / every script name (the index in the list is the asset index).
A hook puts your layer on a game script: log its calls, change its result, skip it, change its arguments, set variables after it, or run a draw program inside it. Up to 256 different scripts can be hooked at once (layers of several mods on one script share a slot).
| field | |
|---|---|
owner s [""] |
your layer; hooking the same script again replaces your layer only |
script s / scripts a |
script name(s), with or without gml_Script_ |
capture b [true] |
log the calls (read them with hooklog) |
capture_args a |
log only calls whose arguments match this pattern (see below) |
depth n [1] |
expansion of logged arguments / results |
stack b [false] |
log the GML call stack too (slow) |
self_vars a |
variables of self to log with each call |
overrides a |
[{args, value, skip, set_args}], below |
after o |
a draw program run after every call (also after a skipped one), e.g. inside a menu bake |
post_set o |
{var: value} set on self after each call |
post_set_count n [-1] |
how many calls post_set applies to (-1 = always) |
ttl_ms n |
the layer's overrides / post_set / after stop applying unless you hook again within this time |
→ {"gml_Script_x": slot} per script, or "error: ..." for a script that could not be hooked (that error also shows
on the Mods page for the owner).
Patterns (capture_args, overrides[].args): one entry per argument, compared from the first: null = anything,
"abc*" = a string starting with abc, other strings / numbers / booleans = equal. Fewer entries than arguments = the
rest match anything.
Overrides (the first one whose args match is used):
{"args": [...], "value": v}— the script runs, then its result is replaced byv.{"args": [...], "skip": true, "value": v}— the script does not run; the call returnsv.{"args": [...], "set_args": {"2": "new text"}}— the script runs with those arguments replaced (keys are argument indices as strings).
Across mods: skip / set_args overrides are checked layer by layer and the first match of any layer wins; result
overrides, post_set and after programs then run for every layer in order.
owner, script (none = all of your hooks), all b (every owner's layers — a dev tool; refused for Lua) →
the scripts it removed layers from. A script without layers is un-hooked.
→ [{slot, script, calls, layers: [{owner, capture, depth, overrides, after?, after_error?, expired?}]}].
after_error is set (and the program dropped) when a layer's draw program failed.
| field | |
|---|---|
owner s [""] |
whose log |
max n [10000] |
at most this many entries, oldest first |
consume b [true] |
remove the returned entries |
→ {entries: [{seq, script, t_us, depth, self, args, result, us, stack?}], remaining, dropped}. self is an
instance stub (+ vars from self_vars); us is the call's duration. A mod's log keeps the newest 5000 entries.
Click pattern (what the UI kit does for you, and how BuildFilter does its own): give your clickable mba entries
the script scr_blacksmith_open_filter_from_inventory with an argument string of your own ("mymod:open"), and hook
that script with {"overrides": [{"args": ["mymod:*"], "skip": true}], "capture_args": ["mymod:*"]}: the game calls
it on a click, your override stops it, and the click lands in your hooklog.
| field | |
|---|---|
owner s |
|
menu s |
a game menu: main, inventory, settings, skills, elements, profile, ... (the program runs after scr_ui_menus_<menu>, the game's bake of that menu) |
program o |
a draw program; widgets work here |
→ true. Replaces your earlier program for that menu and redraws the menu. The program runs on every bake of the
menu (the game re-bakes on clicks and when asked), so it can read live values.
owner, menu → removes your program and your widgets' clickable entries from that menu.
→ true; asks the open menu to re-bake (when your program reads game variables that changed).
A single global draw program run after one object event every frame — a development tool (one overlay for everybody;
not available to Lua). Fields: event [gml_Object_obj_ui_next_gen_Draw_64], vars / when / calls (a
program; calls: [] clears it), owner, ttl_ms. → {calls, event, frames_drawn, error?}. Mods should use
ui_add or a hook's after.
owner, events a (names, "*" = all) → the owner's event list. Events are queued for the owner (up to 2000;
older ones drop) until read with events. C++ mods can use Subscribe instead (a callback per event).
owner, events a (none = all, and the queue is dropped) → true.
owner, max n [1000] → {events: [{seq, event, t_us, data}], remaining, dropped} (returned events are removed).
→ every event name. See Events.
| field | |
|---|---|
name s |
required; your mod's name (also use it as owner) |
version s |
shown on the Mods page |
author, description s |
shown on the Mods page |
file s [name + ".dll"] |
your DLL in mods/aurie (ties the row to the file and its on/off switch) |
requires o |
{"SlormAPI": "1.0.0", "OtherMod": "2.1"}: minimum versions; unmet ones show as problems |
game s |
the game version you made it for ("1.0.2c"); a different game version shows a note |
→ true. Registering again updates your row.
→ the Mods page's rows: SlormAPI, the DLLs of mods/aurie and its subfolders, as Aurie loads them (registered or
not, switched off ones too; file is relative to mods/aurie, e.g. "MyMod/MyMod.dll"), Lua mods, then the loader
parts. Each: {name, version, author, description, file, kind: "mod" | "lua" | "loader", loaded, enabled, locked, pending, problems: [..], note?, managed?, game?}. pending = switched this session, applies at the next
start; managed = deployed by a mod manager (Vortex hardlink: switched there, not here).
enabled b (turn SlormAPI's MODS button and page off / on), open b → {enabled, open}. Not for Lua.
file s ("X.dll", "Folder/X.dll", "x.lua" or "<folder>/lua-pack.json", as mods lists it), enabled b → true. DLLs are renamed to / from X.dll.disabled (applies
at the next start); Lua mods switch at once. Refused for SlormAPI, the loader and Vortex-managed files. Not for Lua.
Declare your settings once; SlormAPI shows them in Options > Mods (a tab per mod, drawn like the game's own settings), validates and saves them, and tells you when the player changes one.
| field | |
|---|---|
mod s |
your mod's name (the tab's title) |
settings a |
[{key, label, help, type, default, options, min, max, step}] |
Types: "bool" (a switch, default false), "choice" (one of options, any JSON values; default the first),
"number" (min [0] .. max [100] by step [1]; shown as a selector). → the current values {key: value}:
the saved ones that are still valid, else the defaults. Saved in mods/aurie/SlormAPI/settings/<mod>.json.
{"cmd": "register_settings", "mod": "MyMod", "settings": [
{"key": "enabled", "label": "Show the overlay", "type": "bool", "default": true},
{"key": "hotkey", "label": "Hotkey", "help": "While the game has the focus", "type": "choice",
"options": ["Off", "F5", "F6", "F7"], "default": "Off"},
{"key": "size", "label": "Size", "type": "number", "min": 50, "max": 200, "step": 10, "default": 100}]}mod → {key: value}.
mod, key, value → the stored value (numbers are clamped and snapped to the step; a bad value is an error).
Emits setting_changed. Lua mods can change only their own.
For anything that is not a setting (a mod's rules, counters, layouts). Files are in mods/aurie/SlormAPI/data/,
named after your owner (Lua: forced, lua_<file name>), else mod.
mod, value → saves value (any JSON, up to 4 MB; written whole or not at all) and returns true. Without
value → the saved value, or null.
mod, line s → appends HH:MM:SS line to <name>.log (a line up to 4096 characters; past 2 MB the file moves
to <name>.log.old).
Read from the game's own data files (dat_*.json next to the exe), so names follow game updates and nothing from
the game is redistributed. Loaded in the background at start.
One of: item (an item array), inst (an obj_loot instance: id or {"$inst"}), equipped (0-10, the
current hero's slot). →
{"kind": "equipment", "rarity": "epic", "slot": "glove", "slot_name": "Gloves", "level": 100,
"legendary": {"id": 52, "name": "...", "roll": 88} | null,
"stats": [{"tier": "E", "stat": "crit_chance_percent", "name": "Critical Strike Chance", "roll": 40, "value": 2, "pure": false}],
"extras": [{"type": "skill_mastery" | "attribute" | "reaper_affinity", "index": 3, "value": 1, "name": "..."}]}value is the stat's real value on that item level (what the game shows).
stat s (+ slot s, roll n, level n [100]) → {stat, name, score, epic_slots: [..], label?, value?}.
epic_slots: slots where it can roll as a gold (epic) line; label: its name on that slot ("Max Life %" when the
slot has a flat and a % version); value: the value of roll on an item of level.
slot s (helm body shoulder bracer glove boot ring amulet belt cape), hero n [current]
→ {slot, name, hero, epic_stats: [{stat, name}], legendaries: [{id, name}]} — what can roll there as gold lines,
and the lootable legendaries for that slot and hero, both sorted by name.
id n → {id, name, slot, hero, lootable, description, values, types}. hero: -1 any, 0-2 a hero, 99 legacy,
999 reaper. description is the game's text: @ = a value (from values), # = a line break.
→ every stat input of the game (176): [{stat, name, unit, item_affix}]. unit: flat, percent,
multiplier_percent (the *_global_mult multipliers) or seconds; item_affix: one of the 92 stats items can roll.
stat_info returns the same unit / item_affix for one stat.
→ the hero's current final stats, as the game computed them from items, skills, buffs and everything else:
{hero, instance, life, max_life, mana, max_mana, shield, max_shield, stats: {crit_chance: 51.3, cooldown_reduction: 45.6, attack_speed: ..., the_max_health: ..., ...}} (stats = the hero's final_* variables, without the prefix;
81 in 1.0.2c). Fails when there is no hero (e.g. in the main menu).
hero n [current], id n → {hero, id, name}.
New legendary items for the game - see Custom legendaries for how they work.
| field | |
|---|---|
owner s |
your mod (with id it makes the permanent id owner:id; Lua: lua:<file name>:id) |
id s |
keep it the same in every version: saved items find their legendary by it |
name, description s |
the item's name and text; @ in the text shows the next number of stats then values (scaled by the item's roll like the game's) |
slot s |
helm body shoulder bracer glove boot ring amulet belt cape |
hero n [-1] |
-1 every hero, 0 Knight, 1 Huntress, 2 Mage |
lootable b [true] |
drops (and appears in reforging) like the game's legendaries |
stats o |
{stat: value} while it is worn, scaled by its roll (75-100%); any key of stat_list except the two flat knockback ones |
values a |
more numbers for @ in the description (not stats) |
icon n |
use the icon of one of the game's legendaries (its number) |
effects a |
[{trigger, chance, cooldown, seconds, status_key, scale_with_roll, buff, heal}], below |
→ {id, ref} (ref = its number in the game's table for this session). Registering the same id again updates it
(e.g. a Lua mod saved again).
Effects (each runs only while the legendary is worn):
trigger:hit(every hero hit),crit,ancestral,kill,boss_kill,hero_hit,cast(a skill),status(a status applied to an enemy;status_keyto name one),every(eachseconds).chancen [100] in %,cooldownn [0] in seconds,scale_with_rollb [true].buff:{stats, duration (seconds), max_stacks, name, description, icon}- a game buff on the hero (each trigger adds a stack); withicon(one of the game's buff keys, e.g.slow,fire_burn) it shows in the buff bar.heal:{life, life_percent, mana, mana_percent, shield, shield_percent}- restores the hero (*_percentof the maximum life / mana;shield= Light Shield).
owner, id (none = all of the owner's) → true. Its row stays (items keep their number) but it no longer drops,
gives stats or runs effects; its name shows "(mod off)". A Lua mod that is switched off or removed does this itself.
→ [{id, ref, name, slot, owner, active, equipped}] (equipped: its roll while worn, else null).
id (or owner + id) → its roll (75-100) while it is worn, else null.
id, roll [100] → a drop of that legendary at the hero (an equipment drop of its slot, made that legendary): for
testing, or as a reward. → the obj_loot stub.
→ [{name, id}]: items in the inventory that were custom legendaries when they were saved and whose mod is not
loaded (or switched off) now - they are Epics until it is back.
The game's own buffs: stats while they last, stacks, timers, and (with an icon) a place in the buff bar.
| field | |
|---|---|
target |
a root; default the hero |
key s |
your buff's name (no comma or pipe); the same key again adds stacks / refreshes it |
stats o |
{stat: value} (stat_list keys) |
duration n [-1] |
seconds; -1 = until removed |
stacks n [1], max_stacks n [1] |
stacks to add, the most it can have |
icon s |
one of the game's buff keys (slow, fire_burn, ice_chill, ...): its icon; then it shows in the buff bar |
name, description s |
its tooltip in the buff bar (with icon) |
debuff b |
shown as a debuff |
→ its stacks now. Stats count in the target's stats like the game's own buffs.
target, key → true / its stacks (0 = none).
cmds a → [{ok, result | error}, ...], one per command, all in the same frame (reads are consistent). Lua mods
cannot nest batches.
enable b, reset b → SlormAPI's own cost per frame since the last reset: {enabled, frames, callbacks_us_per_frame, ticks_us_per_frame, modspage_us_per_frame, events_us_per_frame, lua_us_per_frame, detours_us_per_frame, object_events_per_frame, hooked_calls_per_frame}. Off unless enabled. Not for Lua.
Reverse-engineering helpers: RVAs of the runner's helper functions; names → RVAs of built-in functions; names →
their function-table indices. Not for Lua.
Delivered once per frame on the game thread, right after they happened. Script events install their hook only while someone listens; watchers compare state each frame (or a few times a second).
| event | data | source |
|---|---|---|
enemy_killed |
enemy {$inst, $object}, x, y, level, profile (trash / good / elite / boss) |
scr_hero_stats_on_kill |
boss_killed |
as enemy_killed |
a kill with profile boss |
hero_hit |
damage, attacker {$inst, $object}, life, max_life, shield, max_shield (Light Shield after the hit) |
scr_hero_proc_when_hit |
hit_dealt |
target {$inst, $object}, skill (id, or a name such as "overdrive"), strike (normal / critical / ancestral) |
every hero hit (scr_hero_proc_when_crit_brutal) |
damage_dealt |
target, damage (the health it lost, after its shields) |
scr_damage_shield (hits, procs and other damage to enemies) |
status_applied |
status (the game's key, e.g. fire_burn, ice_chill, knight_bleed, reaper_enfeeble), name ("Burn"), target, duration (-1 = until removed), stacks (after it) |
scr_buff_list_add on an enemy |
status_removed |
status, name, target, reason (ended, or target_gone when the enemy died / despawned) |
a status seen applied whose stacks are gone (checked 4×/s) |
cooldown_started |
skill, name, hero, cooldown (the game's cooldown value) |
scr_new_attack_state_start_cooldown |
stats_refreshed |
hero (at most once per frame; often several times a second in combat) |
the hero's scr_stats_refresh |
hero_died |
— (once per death) | scr_hero_dead |
item_picked_up |
items (what was picked up), picked (the script's result) |
scr_pick_up_item |
skill_cast |
skill (id), name, hero |
scr_hero_proc_when_any_cast |
run_started / run_ended |
— | scr_run_start / scr_run_end |
loot_dropped |
loot (the obj_loot stub), item (item array), info (as item_info) |
new obj_loot instances |
gold_changed |
gold, delta |
global.gold |
level_up |
level, previous, hero (0 Knight, 1 Huntress, 2 Mage) |
global.level[hero] |
item_equipped |
slot (key), index (0-10), hero, item, previous, info (also when a slot is emptied) |
the hero's 11 slots, 4×/s |
room_changed |
room, previous (room names) |
the current room |
menu_changed |
menu, previous (obj_ui_next_gen.menu_show) |
the open menu |
setting_changed |
mod, key, value |
set_setting / Options > Mods |
ui_clicked |
owner, id, action |
a click on a widget |
What already exists when you start listening is not reported (e.g. loot already on the ground).
Used by ui_add, a hook's after and overlay. A program runs at draw / bake time, so its values are live:
{"vars": [["bx", {"$op": "??", "args": [{"$op": "mba", "args": ["Filter Active", 2]}, 1537]}]],
"when": {"$op": "==", "args": [{"$self": "menu_show"}, "inventory"]},
"calls": [
{"fn": "draw_sprite", "args": [{"$asset": "spr_menu_grey_button"}, 0, {"$v": "bx"}, 904]},
{"fn": "scr_draw_text", "args": [{"$op": "+", "args": [{"$v": "bx"}, 84]}, 934, "Hello", 1, 1, "ft_standard_12", 1, 1, 0, 1, "white", "black", 1, false],
"when": {"$global": "gold"}}]}vars:[name, expression], evaluated in order.when: the whole program runs only if true. Each call:fn(an engine function, a game script, or a pseudo call below),args(expressions), optionalwhen.selfis the hooked call's / the event's instance (in menus:obj_ui_next_gen).- Positions: the game lays its menus out for 1920×1080 and multiplies every position and size by
self.ui_scale(from the resolution) as it draws. Widgets do that for you; a program's owndraw_*calls and$mba_pushentries must scale themselves, e.g.{"$op": "*", "args": [904, {"$self": "ui_scale"}]}, and draw sprites withdraw_sprite_extat that scale (scr_draw_textsizes its font itself).
Expressions: JSON literals (with $asset / $inst), arrays of expressions, and
| expression | value |
|---|---|
{"$v": name} |
a program variable |
{"$self": var} |
a variable of self |
{"$global": var} |
a global variable |
{"$arg": i} |
argument i of the hooked call (e.g. a panel's x / y) |
{"$fn": name, "args": [...]} |
a call's result |
{"$op": op, "args": [...]} |
+ (also joins strings) - * / == != < <= > >= and or not ? (cond, a, b) ?? (first defined) def (is defined) in_rect (px, py, x, y, w, h) mba (name, index: a field of the first self.mba entry with that name) |
Pseudo calls on self.mba, the game's list of clickable UI elements (each [name, sprite, x, y, w, h, text, script, argument, tooltip]; the game draws hover frames, plays sounds and on a click calls script(argument)):
| call | does |
|---|---|
$mba_push [entry] |
adds an entry (replaces one with the same name and position) |
$mba_set [name, index, value] |
sets field index of the entries called name |
$mba_remove [index, value] |
removes the entries whose field index equals value |
$mba_remove_in [x1, y1, x2, y2] |
removes the entries placed inside that rectangle |
$set [var, value] |
sets a variable on self |
$button $radio $selector $text |
widgets |
A program that fails (bad function, bad arguments) is dropped and its error is shown by hooks / overlay.
Text: the game's scr_draw_text takes fonts by name:
[x, y, text, 1, 1, font, halign, valign, 0, 1, color, outline, 1, false] — fonts such as ft_standard_11,
ft_standard_12, ft_standard_14, ft_standard_16, ft_standard_13_b, ft_standard_22_b, colors by name (white, grey, black, item_highlight, pure, ...), halign 0 left /
1 center / 2 right. Use 0, 1 for arguments 9-10: with 1, 0 the game defers the text and it may never be drawn.
{text} in the string with a 15th argument (a color name) marks words in that color.
The game's own controls in one call each, inside any draw program (usually ui_add). Clicks arrive as the event
ui_clicked {owner, id, action} (owner = the program's owner). Positions are the game's 1920×1080 layout; the
widgets follow the game's ui_scale (from the resolution) like its own controls.
| widget | args | actions |
|---|---|---|
$button |
id, x, y, text, sprite?, tooltip? (sprite default: spr_menu_thin_grey_button; size = the sprite's) |
click |
$radio |
id, x, y, label, on, tooltip? (the game's diamond switch) |
toggle |
$selector |
id, x, y, label, value, tooltip? (label above, ◀ value ▶ box, as in the game's settings) |
prev, next |
$text |
x, y, text, font? [ft_standard_12], color? [white], halign? [0] |
— |
Widgets you stop drawing disappear (each run replaces your previous entries); the tooltip is the game's own. Change
state → call ui_add again with the new program (or ui_refresh if the program reads game variables).
A mod can add legendaries to the game. They are rows of the game's own legendary table, so they drop like the game's (and appear in Friedrich's reforging), show their name, icon and description, and count as worn like the game's own. Stats and effects come from SlormAPI:
- Stats are a passive game buff on the hero while the legendary is worn, scaled by the item's legendary roll (75-100%), like the game's stat legendaries.
- Effects run on SlormAPI's events (hits, crits, kills, casts, statuses...) with the game's buffs and heals.
- From Lua,
slorm.legendary{...}also takes handlers (on_hit,on_kill, ...) for anything else.
slorm.legendary{
id = "windrunner", name = "Windrunner", slot = "boot", icon = 84,
description = "+@% Movement Speed. Critical strikes grant +@% Attack Speed for 3 seconds.",
stats = { the_speed_global_mult = 40 }, values = { 25 },
effects = { { trigger = "crit", cooldown = 0.5,
buff = { stats = { attack_speed_percent = 25 }, duration = 3, name = "Windrunner Haste", icon = "slow" } } },
on_kill = function(e, roll) ... end, -- only while it is worn
}examples/lua/legendary_windrunner.lua is a complete one.
Saves. A custom legendary never reaches a save file. When the game saves, each one is written as an Epic with
the same stats, and SlormAPI notes which Epic it was (mods/aurie/SlormAPI/data/legendary_items.json); after loading,
or when the legendary registers, the Epic becomes the legendary again. So:
- removing the mod (or SlormAPI) leaves an ordinary Epic, never a broken item;
- with SlormAPI installed but the mod missing, the player is asked before playing that save (CONTINUE, or a save
in Load Game): which legendaries wait for which mod, and not to reforge, upgrade or sell those Epics - a changed
Epic can no longer be recognized - with Load anyway and Back. If the save starts some other way, the same
warning shows over the game for 15 seconds; the Mods page lists them until the mod is back. SlormAPI knows what
each save slot holds from its last save (also in
legendary_items.json); - keep the
idthe same: the Epic finds its legendary by it; - a save copied to another PC (or Steam Cloud) has the Epic there; it becomes the legendary on a PC that has the
mod and its
legendary_items.json.
Custom legendaries cannot be placed in the game's legendary storage (it has one slot per game legendary); one that lands there is moved back to the bag.
Lua 5.5, one state per file, run on the game thread. slorm.request plus helpers:
| function | does |
|---|---|
slorm.mod{name, version, author, description} |
your row on the Mods page (call it first; in a pack optional: lua-pack.json fills in what it leaves out) |
slorm.request{cmd = ..., ...} |
any command above → its result (an error raises a Lua error) |
slorm.on(event, fn) → id / slorm.off(id) |
events: fn(data, event) |
slorm.every(seconds, fn) / slorm.after(seconds, fn) → id / slorm.cancel(id) |
timers (at most once per frame and every 10 ms; every(0, fn) = about every frame) |
slorm.get(path, root?, depth? [2]) / slorm.set(path, value, root?) |
variables |
slorm.call(fn, ...) / slorm.call_on(self, fn, ...) |
scripts and engine functions (result depth 2) |
slorm.instances(object, vars?) |
instances |
slorm.inst(id) / slorm.asset(name) |
{"$inst": id} / {"$asset": name} |
slorm.null |
JSON null where Lua has no nil, e.g. "any" in patterns: { slorm.null, "abc*" } |
slorm.item_info(item) / slorm.slot_info(slot, hero?) / slorm.stat_info(stat, slot?, roll?, level?) |
game knowledge |
slorm.hero_stats() / slorm.stat_list() |
the hero's final stats / every stat with its unit |
slorm.legendary{...} → {id, ref} |
a custom legendary (register_legendary fields) + handlers on_hit, on_kill, on_boss_kill, on_hero_hit, on_cast, on_status, on_damage, on_cooldown as fn(event_data, roll), called only while it is worn |
slorm.legendary_equipped(id) |
its roll while worn, else nil |
slorm.buff{key, stats, duration, max_stacks, icon, name, ...} / slorm.buff_remove(key, target?) / slorm.buff_stacks(key, target?) |
the game's buffs (buff_add) |
slorm.settings{ {key=..., ...}, ... } → values / slorm.setting(key) |
your settings (kept up to date) |
slorm.save_data(table) / slorm.load_data() |
your data file |
slorm.log_file(...) |
a line in your log file |
slorm.ui(menu, calls, when?) / slorm.ui_remove(menu) |
your program in a menu |
slorm.text(x, y, text, font?, color?, halign?) |
a $text call |
slorm.button(id, x, y, text, tooltip?, sprite?) / slorm.radio(id, x, y, label, on, tooltip?) / slorm.selector(id, x, y, label, value, tooltip?) |
widget calls |
slorm.on_click(function(id, action) ... end) |
clicks on your widgets |
slorm.clock() |
seconds since the game started |
slorm.log(...) / print(...) |
a line in Aurie's log |
slorm.display_name() |
your mod's name (as on the Mods page) |
slorm.name / slorm.owner / slorm.version |
the file name / your owner (lua:<file name>) / SlormAPI's version |
Tables become JSON: keys 1..n only (or empty) → an array, else an object. JSON null inside arrays becomes nil in
Lua (a hole) — use your own markers ("" / -1) in data you save.
The sandbox (a Lua mod is a text file, but it still must not be able to harm the PC):
- Libraries: base, coroutine, math, string, table, utf8. No io, os, package, debug;
loadtakes text only;string.dump,dofile,loadfileare gone. - Engine functions that reach outside the game are refused in
calland in draw programs: files (file_*,directory_*,ini_*,buffer_save/buffer_load,screen_save, ...), network (http_*,network_*,url_*,steam_*), the OS (execute_*,environment_*,os_*,external_*,shell_*), dialogs, the clipboard, ... - Commands not available:
mod_enable,mods_page,perf,overlay,runner,routines,routine_index; nested batches.set_settingonly for your own mod. Yourowneris alwayslua:<file name>andunhook'sallis ignored, so a Lua mod only ever touches its own hooks, UI, events and data. - Each call may run 200 ms and each mod may use 64 MB; past that it stops with an error.
- Errors are shown on the Mods page ("Lua error: ...") and in Aurie's log; fix the file and save to reload.
Items (drops' obj_loot.ar, inventory entries, loot_dropped.item, save files) are arrays of arrays:
[["epic", "glove", 100, "equipment", ...],
["E", "crit_chance_percent", "40", "0", "0"],
["M", "the_max_health_percent", "61", "0", "0"],
["L", 52, 88],
["MA", 3, 1]]- Header
[0]: rarity (normalmagicrareepiclegendarynether), slot (helmbodyshoulderbracerglovebootringamuletbeltcape;weapon), item level, kind (equipment,weapon/reaper, or a material / other pickup). For weapons / reapers field 4 is the reaper's id. Leave the other fields as they are. - Stat lines
[tier, stat, roll, pure, ...]: tierN/M/R/E(epic = gold, the hard ones to reforge) /D; the stat's id (e.g.crit_chance_percent,the_max_health_add); the roll (1-40 for gold lines; numbers may come as strings);pure> 0 = a pure stat. - Legendary
["L", id, roll](roll 75-100). Extras:["MA", skill id, n]+n skill mastery,["AT", attribute 0-7, n]+n attribute points (Toughness, Savagery, Fury, Determination, Zeal, Willpower, Dexterity, Bravery),["RP", reapersmith, n]+n affinity.
item_info turns any of these into names and real values; stat_info with roll and level gives one value.
Version 1.0.2c. Names are the game's own.
Globals (get / set without a root):
current_hero |
0 Knight, 1 Huntress, 2 Mage |
gold, level[hero], version |
gold, the heroes' levels, the game version |
inventory[hero][0..10] |
equipped items: helm, body, shoulder, bracer, glove, boot, ring, ring, amulet, belt, cape |
loot_filter[hero] |
the Filter-o-tron: [enabled, [5 rarity rows × 7], convert (1 fragments, 2 goldus)] |
skill_equipped[hero][skill] |
2-4 = the skill is equipped |
Objects:
obj_ui_next_gen |
all menus: menu_show (the open menu: none, main, inventory, settings, ...; read it, don't write it - set menu_wanted to open or close a menu: writing menu_show skips the game's menu switch and can crash it), menu_citizen (blacksmith = Friedrich's panel), mba (clickable elements), rebake_menu (set true to redraw), main_panel (0 Welcome, 1 New Game, 2 Load Game), settings_panel (0 Gameplay, 1 Settings, 2 Keybinds, 3 Codex) |
obj_parent_hero |
the hero: actual_state ("dead"), the_health, final_the_max_health, x, y |
obj_parent_enemy_NL |
enemies: enemy_level, the_health, final_the_max_health |
obj_loot |
a drop: ar (the item), loot_name (its label; set baked false after changing it), visible |
obj_gold_coin |
gold: value; set pick_up_animation true to make it fly to the hero |
Scripts (call with the right self):
scr_ui_menus_<menu> |
a menu's bake (hook it, or use ui_add) |
scr_draw_text |
text with the game's fonts (arguments) |
scr_create_loot(level, profile) |
self = hero: drops loot as if an enemy of that profile died there |
scr_pick_up_item(item, true, false, true) |
self = hero: puts an item into the inventory (false when full) |
scr_blacksmith_filter_mla(item) |
self = hero: the Filter-o-tron's conversion of one item with the current loot_filter (0 = converted) |
scr_save_load_save(slot) / scr_run_start() |
self = obj_ui_next_gen: load a save / start a run |
scr_ui_settings_change_panel(n) |
switch the Options tab |
Rooms: rm_procedural while in a run. GUI positions are the game's 1920×1080 layout times ui_scale (see
draw programs).
- Use your mod's name as
ownereverywhere, and register it withrequiresandgame. - Do work from events instead of every frame; when you poll, batch the reads (
batch) and do it a few times a second. - Never keep instance ids across rooms (instances are recreated); look them up again (
instances). - Hooks run inside the game's own code: keep
afterprograms small, and don't call scripts with side effects from them unless you mean to. - An error in a game script you call can leave the game in a bad state: try new calls in a test game.
- The game's
mbalist only changes when a menu is baked: after changing what your program draws,ui_addagain orui_refresh. - Test with the dev server: everything a mod does can be tried live from a Python prompt first.