This isn’t a programming tutorial. I’m going to keep the jargon to a minimum, but I’m not going to explain how loops work, or what a string is, or how functions work.
This isn’t a GML tutorial. If you’ve never used a modern game engine before, you’ll want to read over the official manual (or my shorthand) before continuing.
You want a code editor. My (completely unbiased) suggestion is Visual Studio Code, but just use your favorite. There are also other tools you might find useful.
What does it do?
Rusted Moss exposes six Game Objects we can write unrestricted* GameMaker Event code for. This means we can do (almost) anything we want, including completely replacing all of Rusted Moss’s code (!!!). However, most of this documentation will explain how to stay within the bounds of the game’s systems.
Where are my Files?
Mods live in Rusted Moss’s Steam directory, Steam/steamapps/common/Mose. You can easily find this by right-clicking on the game and clicking “Browse local files”.
In that folder, you’ll find the data.win file (for Undertale Mod Tool), but we’re mainly after the mods folder. Inside, we’ll find the mod configuration file meta_info.ini and the example mod sniper_bnuy.ini.
Because of boring technical reasons, you should only use one mod (mod_0) at a time. Each mod name references a .ini file in the Mods directory, so let’s look at sniper_bnuy.ini for what they look like.
sniper_bnuy.ini
[object_list]controller = enabledinstance = enabled[controller_events]room_start = "i = 0; while ..."[instance_events]create = "self.parent ..."draw = "if ..."step = "if ..."
[object_list]: This header lists which Game Objects we want to write code for.
{OBJECT} = enabled: If an object isn’t listed as {OBJECT_NAME} = enabled, then its Event code won’t be loaded. Rusted Moss looks for enabled exactly, not a truthy value.
[{OBJECT}_events]: The key-value pairs after this header are associated with the Game Object in the header.
{EVENT} = {SOME_CODE}: The value {SOME_CODE} is interpreted as one line of Catspeak code for the Event listed as the key. I recommend using single quotes ' to wrap your Event code so you can use double quotes " for strings.
If you want a more in-depth breakdown of this mod, check out the Example Mod page.
To RMML or not to RMML
Rusted Moss Mod Loader (or RMML) is a mod created by me (Harlem512) to aid with developing, installing, and distributing mods. It comes with Rusted Moss Mod Manager (RMMM), a mod that lets you download mods off the internet. My (completely unbiased) recommendation is to install Rusted Moss Mod Loader and use that instead of trying to use the “standard” modding tools.
Modded Event code is run immediately when the Event for a particular Instance fires, and modded Game Objects don’t implement any other behavior on their own. This means calling event_inherited() on your own.
controller is a non-pausing, persistent Game Object that is created when the menu loads, and is destroyed when you return to the menu. Most of your code can live in this object, with other Instances accessed with clever with blocks. instance Instances don’t pause (the Steam page lies).
If you’re doing serious modding work, I recommend bypassing the normal mod loading process using Rusted Moss Mod Loader or your own mod loader.
Installing Mods
The Discord
Most mods are stored in the official Discord in the # mod-share channel or in the rm-mod-database Github repository. Many mods reference RMML or Rusted Moss Mod Loader, which is also available in the # mod-share channel or on Github.
Rusted Moss Mod Loader is required to use .md and index.csv mods and is optional for .ini mods. RMML also comes installed with RMMM, which greatly simplifies installing and managing mods.
Steam Directory
Mods live in Rusted Moss’s Steam directory, Steam/steamapps/common/Mose. You can easily find this by right-clicking on the game and clicking “Browse local files”.
From here, you’ll find the mods folder. Some mod installation guides call this folder Mose/mods.
From there, you follow the mod’s install instructions. For non-RMML mods this usually means unzipping the install zip into the mods folder, making sure meta_info.ini is replaced.
If an install zip isn’t provided, then you need to open the meta_info.ini file in the mods folder and set mod_enabled to 1 and replace sniper_bnuy.ini with the file name of the mod.
Rusted Moss Mod Loader (RMML) replaces the normal mod installation and loading process to fix a few issues. RMML comes with Rusted Moss Mod Manager, which lets you download mods from the internet, manage your mod list, and automatically disables mods in the event of startup crash.
RMML downloads are available in the # mod-share channel in the official Discord or on Github. You install by unzipping rmml_X_X.zip into the mods folder, replacing meta_info.ini. After starting the game, you can use the gear icon on the menu to open the GUI and download other mods.
If you want to install a mod manually, place it in the mods/rmml folder (in either the Steam or saves directories) and either enable it with RMMM or add your mod (as a relative path from mods/rmml) to modlist.txt in the mods folder.
If you are using RMML, do not install other .ini mods or modify meta_info.ini.
Example Mod
What
For more context, I recommend reading the Quickstart article first. We’re going to take a more in-depth look at sniper_bnuy.ini, Rusted Moss’s example mod.
This section tells Rusted Moss what Game Objects to run code for. In this case, controller and instance Game Objects are enabled.
Object Events
[controller_events]room_start = "i = 0; while( i < instance_number(oenemy_flame_sniper)){ c = instance_find(oenemy_flame_sniper,i); instance_create_depth(c.x,c.y,30,omod_instance); i += 1; }"
This is where we define the Catspeak code that runs on each Event. The header ([controller_events]) tells us what Game Object to run code for, and the key (room_start) tells us the Event. Inside the " quotes, as the value for room_start, is our Catspeak code. It’s not very readable, so I’ll add some line breaks for now and friendly comments.
i = 0-- For each `oenemy_flame_sniper` in the current room ...while( i < instance_number(oenemy_flame_sniper)){ -- ... find that Instance ... c = instance_find(oenemy_flame_sniper, i); -- ... and make a new `omod_instance` Instance at its position. instance_create_depth(c.x, c.y, 30, omod_instance); i += 1;}
Pretty simple, we’re making an omod_instance Instance for each oenemy_flame_sniper in the Room, when we first enter a Room. There’s a few other notes for this code.
Line Breaks: Because of boring technical reasons, we cannot add line breaks to our Catspeak source code. There are ways around it, but for now we’ll need to remove all of the line breaks. For clarity, I’ll be including line breaks in code examples.
oenemy_flame_sniper: This is one of Rusted Moss’s Game Objects, specifically the enemies found in certain Living Quarters rooms. Similar to Gamemaker functions, Rusted Moss exposes every Game Object as well. Undertale Mod Tool lists every Game Object for our convenience.
room_start: This Event runs when we first enter a Room. A single omod_controller Instance is created for us when the game starts, so we don’t need to do anything else for this event to run.
omod_instance: This is a special Game Object, since we get to write code for it! Let’s see what it’s code looks like:
Since the game makes an omod_controller for us, we don’t need to create a controller Instance. However, instance Game Objects must be created by our code (in this example, the create event creates our Instances). There are also three events this time, so let’s break it down.
-- Create event, add an Instance variable for the closest `oenemy_flame_sniper`self.parent = instance_nearest(self.x, self.y, oenemy_flame_sniper);
-- Draw event, draw bunny ears on the head of each "parent" Instanceif(instance_exists(self.parent)){ draw_sprite_ext( spuck_bunny_ears, 0, self.parent.x, self.parent.y - 8, 1, 1, 0, c_white, 1)}
-- Step event, if the parent is "running", make it "jump"if(instance_exists(self.parent)){ if(self.parent.legs == smaya_legs_run ){ self.parent.vsp = self.parent.vsp - 4;}}
Each omod_instance Instance is responsible for managing a single oenemy_flame_sniper Instance, drawing its ears and jumping. There are a few things to note here.
self.parent: This is an Instance variable on self, which is the currently scoped Instance. In most cases, this will be the moddable Instance you’e writing code for, however you can use the with statement to change the current scope.
legs and vsp: These are custom Instance variables on oenemy_flame_sniper Instances. To find out how what they do, you’ll want to investigate the source code using Undertale Mod Tool.
spuck_bunny_ears: This is a Sprite, all of which are exposed for modding. Because of boring technical reasons, our ability to modify Sprites is slightly limited, but there are hundreds of existing Sprites we can use.
create, draw, and step Events: Unlike controller, instance Game Object need to be created first (since there is no Instance for the Event to run on). create runs immediately after creation (halting execution). draw and step both happen each frame, with draw triggering after step.
Tools and Resources
Code Editor
Code editors let you write code more efficiently than a normal text editor. While you can use Windows notepad, you really want something with line numbers and integrated access to the file system. The specific code editor you use doesn’t really matter, but my recommendation is Visual Studio Code.
Syntax highlighting
Naturally, you probably want pretty colors for your code editor. If you’re using VS Code, you can use my vsc-catspeak extension, which is also used by this site. TabularElf has a highlighter for Notepad++ if that’s better for you.
Undertale Mod Tool (UTMT or UMT) is a tool for inspecting the source code of Rusted Moss (and other GameMaker games). While you can use Undertale Mod Tool to make mods, we don’t need to (and there are boring technical reasons why we can’t). I recommend reading my docs for more information.
GameMaker Long Term Service
Rusted Moss runs on an older version of GameMaker (2022.9.X), which is very similar to the current long term service version. Instead of accessing the normal monthly version of the manual, you’ll want to use the LTS manual. It’s not significantly different, but there are functions that aren’t available.
I highly recommend installing GameMaker as a way to verify behavior. Because of boring technical reasons, it can be useful to sanity-check GameMaker functionality in a more isolated environment.
Catspeak Documentation
Catspeak is slightly different than GML, so I recommend skimming over the language documentation at least once. Also, Catspeak has a handy list of every GameMaker function we have access to. This is especially useful since some variables, like room, are function calls (room_get()) in Catspeak.
I highly recommend installing Catspeak in an empty GameMaker project if you’re running into weird behavior. There are a lot of abstraction layers between the mod code we write and the engine; having an environment that removes a few of those layers is valuable.
Official Discord
The official Discord is probably the only English-speaking place on the internet where you can find other Rusted Moss modders. If you want to download mods, either for playing with or modding examples, this is the place. Mods are in the # mod-share channel.
Rusted Moss Mod Loader
Rusted Moss Mod Loader (RMML) is a tool built by me. It solves a few technical issues with modding and generally makes everything easier. You can also use RMML as a working example of how to bypass the normal mod loading system.
If you want more information, I recommend looking at the GameMaker and Catspeak documentation sites. If you’ve used Javascript, a lot of the basics will be similar.
Semicolons
Both GML and Catspeak use semicolons (;) to separate statements within a single line.
Catspeak
GML
let i = 0; i += 2
var i = 0; i += 2
Because of boring technical reasons, all modded code must fit within one line, using semicolons to separate statements.
The with Statement and self
The with statement is GML and Catspeak’s most powerful scope-changing and Instance-finding tool. When used with a struct-like structure, it changes self to reference that struct.
let struct ={ "name": "Fern", "sprite": splayer_offscreen,}with struct { if self.name == "Fern"{ -- runs}}
Naturally, this isn’t very useful. What is useful though, is when you use it with an Instance.
let player = instance_find(oplayer, 0)with player { self.y -= 10 draw_self()}
The highlighted function draws whatever self is, which we get to manipulate using a with statement. We can also use with statements with Game Objects (or the all keyword) to loop over Game Objects.
with par_enemy { instance_destroy(self)}
Instead of using instance_number, instance_find, and a while loop, we just use a with statement. Keep in mind that if you’re using modded Game Objects with a with statement, you’ll want to filter for only your Instances.
Advanced Scoping
If you’ve come from Javascript or other similar languages, then you might expect something like this to work.
let _i = 0let count = fun(a,b){ _i += 1 return _i}let a = count()
You would be disappointed, because line 3 throws an error. In other languages, the variable _i would be available to the function, however in Catspeak functions are far more isolated, so you crash when you try to add 1 to undefined.
Catspeak Truthy Values
When using boolean expressions, certain values can be coerced into true or false. For example, the following code works.
if 123{show_message("Hello World!")}
This is because 123 can be coerced into a true-like value. Here is a list of keywords that do or don’t coerce into true-like values.
Truthy Values
Catspeak
Truthy-ness
Description
123
true
Positive numbers
0
false
Zero
-123
false
Negative numbers (!!!)
NaN
false
Not-A-Number
undefined
false
Undefined keyword
"Hello"
-- runtime error
Strings of any kind (!!!)
{ }
true
Structs of any kind
This probably also applies to GML, but I’ve only tested it on Catspeak. Fun fact: !"Hello World" is the simplest “Hello World” Catspeak program, since the unary boolean operator causes an exception and prints the string in the error message.
Differences
This is a condensed list of the major differences between the two languages. If you want something else added, slap that contribute button.
Catspeak and GML Comparisons
Catspeak
GML
Description
let x = 1
var x = 1
Variable assignment
fn = fun(a) { ... }
fn = function(a) { ... }
Function declaration
self.x
x
Implicit/explicit self
a // b; a //= b
a div b; a = a div b
Modulo division operator
a and b or c
a && b || c
Boolean operators
a = if b { c } else { d }
a = b ? c : d
Ternary operator
-- comment
// comment
Comments
x += 1
x++
Unary increment/decrement
r = room_get()
r = room
Special variable access
instance_find(oplayer).state
oplayer.state
Singleton Instance access
c = 'A'
c = ord("A")
Character shorthand
let _i = 0while _i < 4 { ... _i += 1}
repeat 4 { ... }
Repeat loops
match a { case 0 { b = 1 } else { b = 0 }}
switch a { case 0: b = 1 break default: b = 0}
Switch Statement
Constructors
Catspeak also lacks constructor functions, however you can emulate them using normal structs and functions. Catspeak doesn’t preserve self scoping in the same way that GML does, so you can’t use self properly.
Constructor Comparison
Catspeak
GML
let Vec = {}vec.make = fun (x, y) { return { "x": x, "y": y }}vec.add = fun (v1, v2) { v1.x += v2.x v1.y += v2.y}let a = Vec.make(2, 3)let b = Vec.make(4, 5)Vec.add(a, b)
function Vec(_x, _y) constructor { x = _x; y = _y; static add = function(v2) { x += v2.x y += v2.y }}var a = new Vec(2, 3)var b = new Vec(4, 5)a.add(b)
Understanding the Docs
Who does this guy think he is?
I’m not a GameMaker programmer, actually. I know a lot about software development and have learned a lot about GameMaker by modding Rusted Moss, but I’m far from an expert. GameMaker is very beginner-focused, which is good for people who want to learn programming, but really bad for someone trying to figure out advanced engine behavior.
If something in the docs is wrong, please let me know so I can fix it and educate myself. Or fix it yourself.
Usage
There are four topics. The Index pages give an overview of each article in a topic. When an article is opened, its headings appear as numbers in the navigation sidebar, and can be clicked to jump around. Links in an article are highlighted in red and always open in the same tab (you can control+click to open in a new tab). An arrow signifies a docs link, and the link icon signifies an external link.
The Single-Page link combines every article into one large page, for easier searching. If you want to find something specific, the Single-Page view can be faster than checking each article individually.
Certain GameMaker keywords are Capitalized, such as Game Object, Instance, Event, Sprite, and Room. This is done to differentiate between Instances, the GameMaker data structure, and instance, the moddable Game Object (omod_instance) referenced as instance in mod files. When “Player” is used by the docs, it refers specifically to oplayer, the Rusted Moss Game Object, not to be confused with player, the moddable omod_player Game Object. It also helps clarify the difference between Game Objects, the GameMaker object-oriented programming class equivalent, and objects, an object-oriented programming structure similar to Instances.
Code Blocks
Inline code blocks are used for code-like segments, such as literal text, files.md, and code = Examples(). Brackets ({ }) in a code block are sometimes used as placeholders, such as create = '{CODE_HERE}' and always use MACRO_CASE. Larger code blocks use syntax highlighting and include the language name and line numbers:
-- A Catspeak code block, the language of moddinglet foo = 0
// A GameMaker Language code block, the language of source codevar foo = 0
# A `.ini` file used for Rusted Moss configuration or modding# Event code is highlighted as Catspeakcode = 'let foo = 0'
A `.md` file used by Rusted Moss Mod LoaderEvent code is highlighted as Catspeak## code```let foo = 0```
Some code segments use an ellipsis (...) to truncate code or use line highlighting for emphasis.
let fn = fun(a, b){ -- ellipsis instead of more code ...}
All multi-line code blocks should use two spaces for indentation. GML code blocks are allowed to use the Allman brace style, since it’s what UTMT exports, but all other code should use the One True Brace style. Variable names should use snake_case, if possible.
Comments in code should only be in lowercase, except for special keywords (ie Room). Pseudocode can be used in comments to simplify unnecessary logic, but must use MACRO_CASE and start with an @ (ie // @ IF ( CONDITION ) THEN).
Most of the larger mod examples use Rusted Moss Mod Loader’s syntax, since it supports multiple-line Catspeak code, which is easier for humans to read.
Contribute
The easiest way to contribute is to just message me (Harlem512) on Discord with whatever input you wish to provide, and I’ll probably listen to you. If you want to contribute more directly, you can submit an issue or a pull request. The raw documentation files are here and served from this directory. You can add new articles by creating new mdx files under a directory, however they must have a title, desc (description), and order (used for ordering on the left) field in their frontmatter (the part with ---).
New topics can be made by adding a new directory and an index.mdx file, however I’ll probably reject new topics unless you have a really good idea.
If you like this (or hate it), also reach out. I’d love to know if anyone is actually reading any of this.
Markdown and Magic
The documentation is powered by MDX files, which are basically Markdown with support for embedded Javascript. This makes changing things very easy, since Markdown is basically just plain text; you don’t even need a development environment.
These files then get parsed by Astro, a web framework, which compiles them into raw HTML and generates the actual site you can read.
The docs are best used as a half-width (for a 1920x1080 screen) Firefox browser tab and most of my design decisions are focused on that.
Syntax Highlighting
Syntax highlighted is powered by TextMate grammars, made by me (except for GML highlighting, stolen from Butterscotch Shenanigans). If you’re ambitious, you can take the grammars and make a syntax highlighting extension for your favorite code editor. I leave that as an exercise for the reader (or ask me nicely).
GameMaker, the Engine
Engine Architecture
Engine Overview
GameMaker is event-driven and object-oriented; code exists as Events, which operate on Instances as defined by their Game Object. If you’re familiar with object-oriented programming, then this will sound familiar: Game Objects are classes, Instances are objects, and Events are special class methods called by the GameMaker engine.
Game Objects
Game Objects define structure and relationships. They determine what Events are registered and initial Instance variables. Similar “things” can be grouped together for easier code reuse and extension. You can call parent Events using event_inherited() and find the parent using object_get_parent(o).
For example, all pickups share a common parent, par_pickup. Instead of checking for each pickup type individually, we just check for the parent.
// gml_Object_oplayer_Step_2.gml...if global.trinket_active_[5]{ var do_fx_ = false if instance_exists(par_pickup) do_fx_ = true ...}...
Rusted Moss provides six Game Objects we can easily write Event code for. Our ability to manipulate Game Objects is limited by boring technical reasons.
Instances
Instances are mindless data. They’re assigned a Game Object (using object_index), which determines their behavior; every Instance of a Game Object runs the same Events. Instances are usually made using instance_create_depth(...). Most Instances are destroyed when a new Room is loaded, however the persistent variable overrides this behavior.
let inst = instance_create_depth(10, 10, 0, omod_instance)-- `sprite_index` is managed by the engineinst.sprite_index = splayer_offscreen-- setting a custom Instance variableinst.name = "Fern"
Events are code, and implement behavior. Most Events happen each frame, in a specific order, but some are called depending on certain conditions, like when a Room is entered or when an Instance is created. Events can be called using the event_perform(...).
[instance_events]# `create` event, set namecreate = 'self.name = "Maya"'# `draw` event, draw our name each framedraw = 'draw_text(0, 0, self.name)'
We can only write code for twenty-five Events, however most of the classics are moddable. You can check the official documentation for a full list of all Events.
Rooms
Rooms are collections of Instances, tile map data, and other data. The game is always operating with a loaded Room. Everything that happens inside a Room is isolated and non-persistent, meaning anything that happens to a Room or inside a Room is lost when the Room is unloaded (unless explicitly saved).
In Rusted Moss, most of the time the screen fades to black the current Room is being unloaded and a new Room is being loaded. We also get a visual example of Room behavior: every time the same Room is loaded, enemies are in the same place, tiles are in the same place, money on the ground is gone, etc. If you want persistent Room data, you can use the room_start Event with conditional logic based on global variables.
We have a lot of tools to manipulate Rooms through modding, from room_add(...) to room_assign(...). Because of boring technical reasons we can’t just use room_goto(...) to load Rusted Moss’s Rooms.
Sprites
Sprites are images, and are used to render most things to the screen. Anything that isn’t extremely fluidly animated is probably a Sprite. GameMaker gives us a lot of control for manipulated Sprites, however there are performance issues if you load too many new sprites.
Folders
There are three main folders for modding.
Path
Name
R/W
Used For
SteamLibrary\steamapps\common\Mose
Steam Folder
Read-only
RM Files, mods
%LocalAppData%\Rusted_Moss
Save Folder
Read/Write
Changeable files, persistent data
%LocalAppData%\Temp
Temp Folder
Read/Write*
Temporary files
When reading a file, the game first checks the Save folder for a matching file, then it checks the Steam folder. The Steam folder forbids writing, so any if you write to file, it always writes to the Save folder.
Reading from or writing to the Temp folder requires prefixing the path with temp_directory_get(). Files in the Temp directory can be freely deleted at any time, so you should only use it for temporary files.
Globals
The global namespace can be accessed using the global struct, and contains many persistent data structures and variables, such as Player data (ie global.ameli_mode_, global.hp, etc), UI state (global.gamestate), and other data.
Unlike Instances, which are cleared, global variables are not reset. This is especially important when using Gamemaker’s data structures or loading assets (ie room_add, sprite_add, etc).
Moddable Game Objects
Game Object Table
Rusted Moss provides six Game Objects that we can write Event code for. The following table can be used for quick reference. Name is used for object headers ([{NAME}_events] and # {NAME}) while GML Name is the GML Game Object name (for instance_create_depth, with, etc). Parent is the GML parent Game Object.
GML Name
Name
Parent
Notes
omod_basic
basic
par_basic
omod_controller
controller
-
Created at game start, doesn’t pause, persistent
omod_enemy
enemy
par_enemy
Has HP, can be grappled to
omod_hitbox
hitbox
par_hitbox
omod_instance
instance
-
Doesn’t pause (they lied)
omod_player
player
oplayer
Hard limit of one oplayer, not persistent
95% of modded code can exist with just controller and instance Instances, and I recommend avoiding the rest unless absolutely necessary.
Inheritance
Most Rusted Moss Game Objects inherit from something on this short inheritance tree. The arrows point from a Game Object to its parent.
par_game is the simplest Game Object that pauses and does almost nothing else. par_draw calls draw_self in its draw Event, and similarly does little else. The other Game Objects here have moddable versions and are discussed in their sections.
event_inherited
The Game Objects with parents don’t run their parent’s code automatically. This will lead to weird crashes, as unrelated Game Objects attempt to do stuff with Instance variables that were never initialized. This table is every Event you should add an event_inherited() call in.
The hitboxalarm_1 Event probably crashes Catspeak, since it’s the code that implements hitlag. Maybe don’t use event_inherited on that one.
basic
basic is a child of par_basic, which is the simplest Game Object that pauses. Naturally, it’s still quite complicated, performing tilemap collision resolution and simple rendering. Most things that collide with walls and feature gravity are descendants of par_basic.
controller
This is the most important Game Object, and is the only one Rusted Moss makes for you. Any other Instances you want created must be made in a controller’s Event code. Like all other Instances, it gets destroyed when you go back to the main menu.
You should use a controller for any logic that doesn’t need to abuse Event ordering, or needs to be a parent of another Game Object.
enemy
Rusted Moss has a lot of Enemies, most of which extend from par_enemy. Notably, this Game Object tracks a self.hp Instance variable and destroys itself when that value reaches zero. Enemies can also be grappled to, which can be useful.
hitbox
par_hitbox is mostly used for bullets and other projectiles that need to interact with the game’s tile system. The par_ prefix is somewhat misleading, par_hitbox is a parent for only two Game Objects; instead, par_hitbox is created directly. There is a lot of code encased in this Game Object, most of which only applies conditionally depending on initial state.
instance
A big motivation for why I’m writing this is the instance Game Object. The official patch notes for the modding support call instance a “a basic object that pauses when the game pauses.” In reality, instance Game Objects don’t pause, probably because their parent was never set to par_game. For most purposes, this doesn’t matter. If all you want is an Instance that pauses, there are alternatives.
instance Instances are very useful for modifying existing Event code by abusing execution order or other techniques. Any code that needs to be run at a specific time within an Event
player
oplayer is Rusted Moss’s main Player Game Object, and manages the Player’s position and handles rendering. The Player Game Object is deeply entwined with Rusted Moss’s core logic and messing with it can have weird side effects. If you must use player, I recommend using instance_change to change the Instance into an oplayer and back again before calling event_inherited.
Unless you want to completely replace Player logic, I recommend just using a with block in a controller Instance.
with oplayer { -- player code here}
self
When using INI mods with the player Game Object, self is not set. If you want access to the Player Instance, you must wrap your Event code in a with statement, or use instance_find.
Rusted Moss allows us to write Event code for 21 Events. We get almost everything we’d need, but there are some exceptions, notably higher-index Alarm and User Events. If you want more information about each Event, you can check out the official documentation. If you want technical details on how modded Events operate, here you go.
First are the twelve Events that run each frame, in the following order. The Modding Name is used in mod files ({MODDING_NAME} = ... or ## {MODDING_NAME}).
Alarm Events run after a specified number of frames, set using alarm_set({INDEX}, {DELAY}). You can use alarm_get({INDEX}) to read an Alarm’s current delay. There are twelve Alarm Events, however we only get to mod three of them. The rest can be detected using clever logic in the step_begin Event.
## step_begin```with oplayer { -- alarm is zero when it should run if alarm_get(9)== 0{ show_message("Hello World")}}```
Since Alarms don’t decrement if there isn’t any Event code, we can’t use arbitrary Alarms, but we can emulate them using code.
User Events must be called manually and run immediately.
with otile_switcher { event_perform(ev_other, ev_user_11)}
In normal GameMaker, they’re useful for running code that can be easily modified by a Game Object’s children. Similar to Alarms, there are more User Events (16) than we have moddable access to (3). I am not aware of any way to get around this, which is a limitation.
Execution Order
For games, a lot of logic depends on earlier code within the same frame; simulation code typically needs to happen before rendering, so the graphics aren’t a frame behind Player inputs. Usually you want even more granularity; a bullet killing an enemy in a frame should destroy that enemy before it has a chance to attack in a later part of the frame.
For most use cases, the nine per-frame Events are good enough for ensuring the right code runs before or after other code. If you think you need more than 9, you probably want to rethink how your code is structured. However, modding doesn’t have this luxury.
A lot of the time, you need code to run immediately before or immediately after some Instance’s Event to modify behavior. Maybe you want to intercept draw calls using surface_set_target, or abuse object_index, or you need to re-create an Instance that was destroyed before the game crashes. For all of these cases, we must learn about the order GameMaker chooses Instances to run Events for.
Depth
There are two factors main that GameMaker uses: an Instance’s depth and when it was created. depth is a GameMaker-managed Instance variable used to manipulate draw order, and is just a number.
Before we continue, let’s play a game. Think about each Event, and try to guess how those two would affect each Event.
Order Table
Let’s start with four example objects, created in the following order and with varying depths:
let person = fun(depth, name){ instance_create_depth(0,0, depth, omod_instance).name = name}person(0, "Alice")person(10, "Bobby")person(10, "Carol")person(0, "David")
We can combine this with a simple piece of code we can copy into each Event.
show_message(self.name)
Here’s the results. How’d you do? From what I understand, this execution order is extremely consistent to the point of total reliability. Here is more information about how to abuse this behavior.
Event
Order (left first)
Formal
room_start and room_end
Alice, David, Bobby, Carol
Lowest depth, then oldest
All step Events
Alice, Bobby, Carol, David
Oldest to youngest
All draw Events
Carol, Bobby, David, Alice
Highest depth, then youngest
with statement
David, Carol, Bobby, Alice
Youngest to oldest
Persistent Objects
The ordering gets weird with persistent Instances.
For with statements, all Instances created in the current Room run (in youngest to oldest order), then all persistent Instances run in the draw Event order (highest depth, then youngest).
Harry, Janet, Frank, Erica, David, Carol, Bobby, Alice
Youngest to oldest, ignoring persistent tag
Subsequent
Harry, Janet, Frank, Erica Carol, Bobby, David, Alice
Youngest to oldest non-persistent; then highest depth, then youngest for persistent
Step Events also exhibit similar behavior.
Room
Order (left first)
Formal
Initial
Alice, Bobby, Carol, David, Erica, Frank, Janet, Harry
Oldest to youngest, ignoring persistent tag
Subsequent
Alice, David, Bobby, Carol, Erica, Frank, Janet, Harry
Lowest depth, then oldest for persistent; then oldest to youngest for non-persistent
I am not sure how this pattern continues for other Events, or how Instances created by Rooms fit into this model. If you know, feel free to reach out.
Internal Identifiers
A Story
If you’ve used GameMaker for a bit, you might know what’s wrong with this.
instance_create_depth(omod_instance, 0, 0, 10)
The arguments are in the wrong order, but instead of providing a helpful error message, GameMaker accepts the arguments and keeps running. The reason why GameMaker doesn’t reject this function call is because omod_instance is actually just a number. This is what GameMaker actually sees:
instance_create_depth(329, 0, 0, 10)
“Pointers”
If you’ve taken a low-level programming course or come from other game engines, then this probably isn’t too unexpected. Because of boring performance reasons, it’s better to use a single number than an entire string, at the cost of making things slightly more difficult.
Basically everything the game tracks is secretly just a number. This includes Game Objects, Instances, Sprites, Rooms, audio assets, shaders, constants (ie ev_create, vk_home, etc ), some GML language keywords (self, all, true, etc), functions, data structures (buffers, ds_maps, etc), and probably more I haven’t run into yet.
Here’s a few common keywords and their internal numbers.
Internal Keywords
Keyword
Internal Value
true
1
false
0
self
-1
other
-2
all
-3
noone
-4
For most use cases, this information is irrelevant. However, if you’re using json_stringify for serialization or debugging purposes, you should use json_encode with GML data structures.
Undertale Mod Tool
When using Undertale Mod Tool, eventually you’ll run into code that looks like this.
Despite all seven variables being initialized to numbers, their actual purpose varies dramatically. The first two, sprite_dead and sprite_hit, are referencing Sprite indexes for splayer_maya_dead and splayer_maya_hit respectively. hair_start_col and hair_end_col are hex code colors, 2E 2E 2E and 66 58 38 in RGB. hair_alt is -1 so it evaluates as false for conditional statements, but is normally an array. hair_number and hair_size are the only two “normal” numbers here.
When using Undertale Mod Tool, you can right click on one of these weird numbers and get a list of possible assets it could be. If you’ve exported the code or are looking for references to a Game Object or Sprite, you might want a list of every internal identifier.
Identifier Dump Mod
The key technology here are the _get_name functions, which take an identifier and return the name of the asset. Additionally, these identifiers are not stable. If possible, you should use the actual names of the asset, or find the index at runtime.
# controller## create```spglobal.ripper ={}global.ripper.type = 2global.ripper.saved_index = 0global.ripper.max = 100```## step```splet log = file_text_open_append("asset_list.txt")let i = 0while i < global.ripper.max{ let name let next let exists = false match global.ripper.type{ case 0{ name = sprite_get_name(global.ripper.saved_index) exists = sprite_exists(global.ripper.saved_index) next = "room"} case 1{ name = room_get_name(global.ripper.saved_index) exists = room_exists(global.ripper.saved_index) next = "object"} case 2{ name = object_get_name(global.ripper.saved_index) exists = object_exists(global.ripper.saved_index) next = "END"} case 4{ file_text_close(log) !"Done"}} if !exists { file_text_write_string(log, next + "\n") global.ripper.saved_index = -1 global.ripper.type += 1}else{ file_text_write_string(log, string(global.ripper.saved_index)+ "\t:" + name + "\n")} i += 1 global.ripper.saved_index += 1}file_text_close(log)```
Disappointingly, GameMaker and Catspeak can’t turn a text file directly into Event code. Instead, we must use the power of code to do what we want. Thankfully, Catspeak does the hard part of turning text into code, all we need to do is run that code when we want.
Modded code gets loaded by the omod_meta Game Object, during its create Event. The code gets stored in a ds_map named global.mod_map, which maps an internal Event name to a Catspeak function. Modded Game Objects look at global.mod_map and call the function stored for that Event’s key.
Calling an Event
The following is the entire Event code, annotated and [fixed(/rm-docs/modding/limitations#decompilation), for the omod_controllercreate Event.
// gml_Object_omod_controller_Create_0// check if the function existsif ds_map_exists(global.mod_map, "controller_events_create"){ // ensure `self` gets set to this Game Object ds_map_find_value(global.mod_map, "controller_events_create").setSelf(self) // call the stored function ds_map_find_value(global.mod_map, "controller_events_create")()}
setSelf is a Catspeak function that sets the self inside the function to what you specify. You’ll also notice it’s reading from a concatenated key based on the Game Object’s name and the Event.
omod_meta
Here’s a shortened and annotated version of the code that loads mods into global.mod_map. See the Quickstart for information on what the referenced meta_info.ini and mod .ini files look like.
First up is some initialization code.
// gml_Object_omod_meta_Create_0// clear `mod_map`global.mod_map = ds_map_create()...// the list of mods to loadfile_list = -1
Next, we build the internal mod list. Note that it finds mods based on string concatenation and exact keys. If you have three mods, mod_0, mod_2, and mod_3, only mod_0 will be loaded, since mod_1 doesn’t exist.
// gml_Object_omod_meta_Create_0// open `meta_info` fileini_open("mods/meta_info.ini")i = 0while(ini_read_string("meta_info", ("mod_" + string(i)), "")!= ""){ // read mods from mod list file_list[i]= ini_read_string("meta_info", ("mod_" + string(i)), "") i++}ini_close()// if no mods, stop loadingif(file_list == -1) return;
The next block actually does the mod loading. For each mod in the mod list, we open the ini file and start reading. We first check that a Game Object is enabled on line 6, then make sure that it has Event code on line 9 before being parsed on line 11.
Line 9 (and the loading code) is duplicated for all 21 Events, and the entire section starting on line 6 is duplicated for all 6 Game Objects.
// gml_Object_omod_meta_Create_0// for each mod in mod list, ...for(i = 0; i< array_length(file_list); i++){ ini_open("mods/" + file_list[i]) // if that mod has a Game Object enabled, ... if(ini_read_string("object_list", "controller", "")== "enabled"){ // and if it has a key for an Event, ... if(ini_read_string("controller_events", "create", "")!= "") // then add the parsed Catspeak code to `global.mod_map` ds_map_set(global.mod_map, "controller_events_create", global.__catspeak__.compileGML( global.__catspeak__.parseString( ini_read_string("controller_events", "create", "")))) ...} ... ini_close()}
global.mod_map
As shown, global.mod_map is the main data structure that relates Event names to Catspeak function calls. As a ds_map, you need to use ds_map functions instead of simple [ ] syntax (and it’s actually a number). The keys for this map are a concatenated string based on the Game Object name and Event name.
Map Key
Description
Example
{OBJECT}_events_{EVENT}
Normal map key for most Events
controller_events_create
{OBJECT}_events_other_{EVENT}
Map key for room and user Events
instance_events_other_user_1
If you’re paying attention, you might notice a few issues. We can only store one function per Event, there isn’t any sandboxing for Instances (so different mods’ code can run on the same Game Object), and code must be one line, since .ini files don’t support multi-line strings. These are limitations that we must work around, by using your own code or Rusted Moss Mod Loader.
Catspeak Codegen
I want to briefly talk about how to load your own Catspeak code. For more in-depth information, check out Catspeak’s offical docs.
Catspeak code generation happens in two steps. Parsing takes in a buffer (parse) or a string (parseString) and outputs an intermediate data structure. The second step, compilation (compile), takes the resulting intermediate structure and outputs a GML function we can store and call later. compileGML can also be used, but is deprecated as of Catspeak 3.0.2.
-- our source codelet str = "!\"Hello World\""let hir = global.__catspeak__.parseString(str)let function = global.__catspeak__.compile(hir)-- call our functionfunction()
Once we have a function, we can store it in global.mod_map and have Rusted Moss run it automatically, or store it some other structure for later use. omod_meta also provides with us a simple function (compile_mod_event) that takes an input file and fully compiles it for us.
-- load our code from a text filelet function = compile_mod_event("mods/codeFile.txt")-- attach the function to the `controller`'s `create` Eventds_map_set(global.mod_map, "controller_events_create", function)
Using this technology, we are no longer limited by Rusted Moss’s mod loading and can substitute our own mod loader. On thing to keep in mind is that compiling code takes a long time, long enough that you can easily start running into the 1000ms limitation or cause noticeable slow-downs. This can be mitigated by storing code in a global and only compiling on startup.
Rooms and Maps
Strange Errors and the Backrooms
If you’ve tried using room_goto to go to a Room, you’ve probably run into some strange behavior. Weird crashes, a room that you can never leave, ending up in unexpected locations, what’s happening?
oroom_transition
While GameMaker does have Rooms, it doesn’t have a standardized method of linking Rooms together. So how does Rusted Moss do it? The main example is the oroom_transition Game Object, which is responsible for the standard transition between adjacent rooms. Let’s investigate.
// gml_Object_oroom_transition_Create_0...// convert `image_angle` from degrees to a range [0-3]dir =((image_angle + 45)% 360)div 90// store two mystery globals (we'll need these later)cx = global.player_map_x_cy = global.player_map_y_// get adjacent coordinate based on the `image_angle`switch dir{ case 0: // `jump_dis` is specified by the Room editor cx -= jump_dis break case 1: cy += jump_dis break case 2: cx += jump_dis break case 3: cy -= jump_dis break}// read from a mystery data structure, using our new coordinatesm = ds_grid_get(global.map_grid_, cx, cy)if(m != 0) // set a `target_room` based on another data structure, ... target_room = global.map_data_[m][0].romelse // ... or use the backrooms as a fallback target_room = rm_sea_8...
First is some initialization code. This generates three Instance variable outputs: cx, cy, and target_room. Let’s see what they’re used for.
// gml_Object_oroom_transition_Draw_75...// @ IF ( READY_TO_TRANSITION ) THEN // set those two mystery globals using the values from earlier global.player_map_x_ = cx global.player_map_y_ = cy // store the player's velocity for later global.player_map_hsp_ = oplayer.hsp global.player_map_vsp_ = oplayer.vsp // hey, it's how the climb hardmode works, neat if(force_room != 0 &&(instance_exists(oclimb_hardmode)|| force_push_room)){ global.player_map_x_ = global.map_data_[force_room][0].xx global.player_map_y_ = global.map_data_[force_room][0].yy target_room = force_room} // go to the target Room room_goto(target_room) // keeps this Instance around after the Room transition persistent = true// @ END IF...
This code is what actually moves the Player between Rooms. Notice that we’re not just using room_goto directly, we’re also setting the globals player_map_x_ and player_map_y_. If we don’t then the oroom_transition Instances in the next room won’t find the correct adjacent Rooms, leading to a desync.
If you’re curious how the Player is created in the next room (since the Player isn’t peristent), check out the room_start Event for oroom_transition (in gml_Object_oroom_transition_Other_4).
You’ll notice that there are two load-bearing data structures here, global.map_data_ and global.map_grid_. Both of these are necessary for oroom_transition to work properly, so let’s inspect them.
global.map_data_
global.map_data_ is two-dimensional array that maps internal Room indexes to an array of sub-rooms, each containing Room metadata.
[ ...[-1.0], // no data, this Room index is unused[ // a single room{ // internal room index "rom": 2.0, // x/y coordinates for `player_map_[x/y]_` "xx": 47.0, "yy": 31.0, // map rendering metadata (for the tab menu) "index": 2.0, "angle": 90.0, "color": 6, // pickup metadata "extra": 6.0, "found": 0.0,}],[ // a two-high room{ "rom": 4.0, "xx": 49.0, "yy": 30.0, "index": 5.0, "angle": 90.0, "color": 6, "extra": false, "found": 0.0,},{ "rom": 4.0, "xx": 49.0, "yy": 29.0, "index": 5.0, "angle": -90.0, "color": 6, "extra": false, "found": 0.0,}], ...]
global.map_grid_
This is a much simpler data structure. It is a ds_grid, and translates the xx and yy coordinates in global.map_data_ to a Room index.
let room = rm_testlet room_meta = global.map_data_[rm_test][0]let xx = room_meta.xxlet yy = room_meta.yy-- read from maplet roomFromMap = ds_grid_get(global.map_grid_, xx, yy)if room == roomFromMap { show_message("Success!")}
This structure is mostly empty. It also contains many rows that similarly completely empty.
Drawing the Map
An additional upside is that we can draw the game’s map very easily. The map_draw() function in gml_GlobalScript_map_scripts contains the code that draws the map, using the metadata from global.map_data_.
Just give me the Code
Ok, ok, enough yapping. Here’s how to go to any standard Room.
-- takes us to the chosen roomglobal.room_goto_fix = fun(room){ let room_meta = global.map_data_[room][0] global.player_map_x_ = room_meta.xx global.player_map_y_ = room_meta.yy room_goto(room)}
If you want to go to an arbitrary Room (such as one made by room_add or a Room that isn’t part of map_data_), you’ll need to populate global.map_data_ and global.map_grid_ with fake data to trick oroom_transition Instances, or implement your own Room transition logic.
You might want to dump global.map_grid_, so here’s some code to JSON stringify a ds_grid.
global.map_stringify = fun(grid){ let w = ds_grid_width(grid) let h = ds_grid_height(grid) let str = "[" let x = 0 while x < w { let y = 0 str += "[" while y < h { str += string(ds_grid_get(grid, x, y)) y += 1 if y != h { str += ","}} str += "]" x += 1 if x != w { str += ","}} str += "]" return str}
Miscellaneous
Articles that haven’t been written yet.
Screen size is natively 444x250 pixels
Shaders
Delayed Initialization
The Player
Pausing
Save File
Where persistent data is saved and how to manipulate it.
Dialogue
Make characters say whatever you want.
Modding Info
Undertale Mod Tool
What
Undertale Mod Tool (UTMT or UMT) is a tool for inspecting the source code of Rusted Moss (and other GameMaker games). This includes room layouts, sprites, Game Objects, Event code, and more. To inspect code, open the data.win file inside the Steam install directory with Undertale Mod Tool and watch the magic happen.
Undertale Mod Tool is a powerful but imperfect tool, and we can improve our lives slightly.
Close the function cache
When first opening the data.win file, you’ll be presented with a “Building the cache of all sub-functions…” popup, which can be safely closed. From what I understand, this doesn’t significantly affect anything.
ExportAllCode.csx
Instead of using the default “Find in Code” search, you can export all of the game’s code and use a code editor like Visual Studio Code to search the source files. Event code files start with gml_Object_{GAME_OBJECT_NAME} and global script files start with gml_GlobalScript_{SCRIPT_NAME}.
Using Undertale Mod Tool to search for functions is very slow, and looking at multiple code files at the same time isn’t supported.
Where the Code At
Most of the game’s code is in Events, which are accessed from the “Game objects” tab in Undertale Mod Tool. Find the Game Object you want, and open the Event to see the source code. Some Event code references script functions, which are functions inside global script files in the Code tab.
As a practical example, we can look at the weapon pickup Game Object, opickup_gun. The draw event looks like this:
As a file or in the Code section, this is gml_Object_opickup_gun_Draw_0 and the gml_Object_* prefix tells us it’s Event code. While gpu_set_fog is a GameMaker function, scr_draw_weapon_pickup is a script function:
...function scr_draw_weapon_pickup(argument0)//gml_Script_scr_draw_weapon_pickup{ if global.ameli_mode_ draw_sprite_ext(...) else draw_sprite_ext(...)}
Undertale Mod Tool lets us jump directly to this file by right clicking, or we can search for the function name. This function is located in gml_GlobalScript_scr_draw_pickup, a global script starting with gml_GlobalScript_*.
Use a fork
A fork describes a modification of software that diverges from the “normal” version. The normal version of Undertale Mod Tool doesn’t properly understand certain code.
However, some code isn’t readable by the Pizza Tower fork, and future versions of the normal version (or possibly some other fork) might work better.
Error Comments
If you go digging for long enough, eventually you’ll find something like this.
/**WARNING: Recursive script decompilation (for member variable name resolution) failed for gml_Script_active_create_ability_init_gml_GlobalScript_abilitiesSystem.Exception: Unable to find the var name for anonymous code object gml_Script_active_create_ability_init_gml_GlobalScript_abilities at UndertaleModLib.Decompiler.Decompiler.<DecompileFromBlock>g__FindActualNameForAnonymousCodeObject|3_2(DecompileContext context, UndertaleCode anonymousCodeObject) in UndertaleModTool\UndertaleModTool\UndertaleModLib\Decompiler\Decompiler.cs:line 587*/...
For reasons beyond me, Undertale Mod Tool can’t properly decompile parts of Rusted Moss’s code, which (mostly) prevents us from using Undertale Mod Tool to rewrite code. Understanding what’s missing requires either improving Undertale Mod Tool or reading the raw bytecode, so we’re not going to do that.
Limitations
What Limits?
Rusted Moss’s modding support is very powerful. Writing arbitrary code as if we had access to the full source code means we don’t have the same restrictions as other modding systems. Instead of being limited by modding support or external tools, we’re limited by Rusted Moss’s interesting codebase and GameMaker’s runtime limitations; if you can make it in normal GameMaker, you can make 99% of it on top of Rusted Moss’s modding system.
Writing Gamemaker Language Code
Normally, all code we write must use Catspeak. This is usually fine, since Catspeak doesn’t impose many limitations. However, you might want to write GML code either to use a feature that Catspeak doesn’t support. In these cases, we can utilize the Gamemaker Live extension, which comes installed with Rusted Moss. You can do a lot with it, but here’s a short example.
live_snippet_call(live_snippet_create("// GML code hereglobal.hello_from_gml = function (msg) { show_message(msg)}"))global.hello_from_gml("Hello World!")
I don’t recommend using this frequently, since it’s probably less efficient than just using Catspeak.
Shaders
New shaders are impossible to make. This is a limitation of GameMaker and cannot be bypassed. Rusted Moss does offer a few shaders that we can use, and we can emulate a lot of shader behavior using software rendering. Just keep in mind that you will take a performance hit for emulating shader behavior with software.
Upon further review, shaders might be possible. Rusted Moss contains shader-replace-simple, which can probably be used to compile shaders at runtime. Getting this to work is an exercise left to the viewer.
One Thousand Milliseconds
If you’ve ever code like this, then you’re familiar with this error.
let i = 0while i < 10{ draw_text(0, i * 10, "hello")}
Catspeak limits execution time to one thousand milliseconds (one second), starting when you first enter Catspeak. For our purposes, this means that every Event we write must fully complete within that time frame.
For almost all purposes, this is plenty of time. Most code is limited to 16ms anyway (the duration of one frame), so you’ll only run into this limit if you’re doing something bad (like trying to build a mod loader). However, just because a limit exists doesn’t mean we need to follow it.
# controller## create```let s = method_get_self(ds_map_find_value(global.mod_map,"controller_events_create"))s.callTime = infinitywhile true{}```
The limitation exists per-function call, so you would need to set the callTime for every function in the stack.
Decompilation
TODO
LTS Bugs
TODO
Sprites
GameMaker has a lot of support for Sprite manipulation. Almost anything you can do with Sprites in the editor you can do at runtime. However, there are GPU performance considerations for creating a lot of new Sprites. Each
Game Object Sadness
GameMaker’s runtime support for Game Objects is limited. We can’t modify a Game Object’s parent, add new Game Objects, or change default values for Game Objects. Some of these can be bypassed, but others are impossible to change.
”New” Game Objects
We can emulate new Game Objects using a custom Instance variable. We lose most of the convenience, and our “new” Game Objects aren’t isolated in with blocks.
Rusted Moss Mod Loader uses a similar technique (a struct-based lookup table, on self.mod_name) to isolate each mod, and has the same issues.
Missing Events
Modifying Existing Code
Depth Manipulation
Debugging, Pitfalls, and Errors
Using the controllercreate Event for Evil
This mod has an issue. Can you spot it?
# controller## create```let inst = instance_create_depth(0,0, 0, omod_instance)inst.persistent = true```# instance## create```spself.name = "Fern"```## step```sp-- should print "Fern", but it doesn't do anything ???show_message(self.name)```
Here’s the issue.
# controller## create...
If you make Instances during a controller’s create Event, the instancecreate Event fails to run. To get around this, you can set an Alarm for one frame later, or use the room_start Event instead.
Using Data Structures as Booleans
Can you spot the issue with this code? Your hint is that global.save_data is a ds_map.
# controller## room_start```if !global.save_data{ return}-- do stuff if a save is loaded```
The problem is a sneaky combination of Catspeak’s boolean coercion and function pointers. What we want is to quickly determine if global.save_data is a valid ds_map without calling ds_exists, so we abuse the default value (-1) as a boolean (false). Normally this works correctly with no issues, however, GameMaker can assign 0 as a data structure identifier, which is also a false value. The end result is a false negative; the save file is treated as if it doesn’t exist, causing issues.
How do we fix this? In the simple case, we can compare to -1 instead, or just do it the right way.
-- compare to -1if global.save_data == -1{ return}-- check if existsif !ds_exists(global.save_data, ds_type_map){ return}
Creating instances inside with blocks
There’s an issue with this code. Can you spot it?
# controller## room_start```spwith self{ -- create an instance inside a `with` block instance_create_depth(0,0,0, omod_instance)}```# instance## step```spshow_message("Never runs :c")```
Creating instances (of any kind) while inside a with block is a problem. In RMML 6, this causes an immediate crash and leads to all kinds of funny issues.
This is resolved by later Catspeak releases, however Rusted Moss still uses an affected version.
Performance
What
As with all high-level languages, Catspeak (and GML in general)‘s ease-of-use features come at a performance cost. The performance hit is difficult to predict and will change depending on your hardware, but there are a few rules we can follow to make things faster.
Why does this matter? Most game code needs to fit within 16ms, or the game’s framerate will drop below 60fps. This problem is made worse by GameMaker’s linking of rendering and game logic. If the game starts to lag, the entire game slows down, which can quickly lead to unfun times. As modders, we can write faster code to minimize, but never eliminate, the overhead of modding.
For the following demonstrations, we’ll use this example mod.
# controller## draw_gui_end```let offscreenArray =[{id: 0, name: "Fern", sprite: splayer_offscreen},{id: 1, name: "Maya", sprite: splayer_maya_offscreen},{id: 2, name: "Ameli", sprite: splayer_ameli_offscreen},]let findOffscreen = fun(name, offscreenArray){ let index = 0 let found = undefined while index < array_length(offscreenArray){ if offscreenArray[index].name == name { found = offscreenArray[index].sprite} index = index + 1} return found}draw_sprite(findOffscreen("Maya", offscreenArray), 0, 10, 10)```
Premature Optimization
You might’ve heard that “premature optimization is the root of all evil”, and while this is absolutely true, there is some merit to developing with performance in mind. Your initial data layout can determine a lot of the performance characteristics of later code, and some optimizations are invasive enough to reward starting early.
80% Optimizations
When optimizing code, you want to look for “80% optimizations”; ways to increase your code’s execution time by huge amounts. As commonly stated, 90% of time is spent in 10% of the code, and by optimizing that 10% we can achieve much better gains than by micro-optimizing the remaining 90% of the code.
Measure Everything
Performance is extremely difficult to predict. Modern computers areverycomplicated. Changing code without first testing its performance can lead to failure, as what you expect to be faster turns out to be slower. Thankfully, measuring performance is easy:
Naturally, there is some overhead here. For most purposes, we can safely ignore it, or do our performance test multiple times.
let start = get_timer()let i = 0while i < 1000{ -- code goes here i += 1}let duration = get_timer()- startshow_message(string(duration))
Of course, the loop also adds overhead, but anything worth measuring will drown out the noise from the loop. There’s a lot of variance, so let’s smooth it out.
Letting the test run gives us a baseline of about 76 microseconds. Let’s see if we can go faster.
Catspeak Operations
Every “operation” in Catspeak is performed internally as a function call. Since function calls are expensive, we can greatly improve performance by removing unnecessary work. This usually means leveraging language features or GameMaker functions to offload work off of Catspeak and onto faster parts of the engine. Every performance increase is primarily concerned with reducing the number of operations Catspeak needs to perform.
... let len = array_length(offscreenArray)-- store array length while index < len {-- read from variable ... index += 1 -- use += instead of + and =}
The += operator performs the addition and assignment in the same Catspeak function call, and saves about 4 microseconds.
A while loop runs its check operation every time the loop executes and calling an expensive function like array_length can cause a performance hit. Adding or removing array elements inside the loop will now cause unexpected behavior, but it’s worth the performance gain. This change saves about 8 additional microseconds.
You might think we can do this for the array item as well (array[index]), but we actually see an increase in execution time. Since assigning a local variable is still a function call, we would need to use our local variable at least three times to see a benefit.
Caching
Caching is a technique where you precompute a value and reference that value, instead of recalculating it every time. In our case, we’re recreating the array array every frame, even though it doesn’t change. Let’s fix that, and put it inside a global variable during the create Event.
# controller## create```-- store the arrayglobal.offscreenArray =[{id: 0, name: "Fern", sprite: splayer_offscreen},{id: 1, name: "Maya", sprite: splayer_maya_offscreen},{id: 2, name: "Ameli", sprite: splayer_ameli_offscreen},]```## draw_gui_end```...-- read from the arraydraw_sprite(findOffscreen("Maya", global.offscreenArray), 0, 10, 10)```
Storing the array in a global also means we can read from it directly, instead of passing it as an argument. However, this is a performance regression; reading from a global costs more than storing it in a local variable first, which the function argument is doing for us.
Caching the array like this saves about 16 microseconds, bring our total to about 48 microseconds. In real code, caching an expensive operation can provide very noticeable gains, especially for expensive operations like reading and writing files or rendering.
Caching an expensive function can easily pass our 80% test by only running code once.
Data Layout
For normal software, memory layout can have a huge impact on performance. However, we don’t really care about that. Instead, we’re concerned with the number of operations required to convert from raw data to our desired output.
In the example mod, we’re iterating over an array for a matching element. Instead of an array, what if we switch to a struct?
At just 11 microseconds, we’ve cut 11 lines of code and 85% of our execution time. In reality, our data isn’t always this nicely lined up, but we can still cache a faster data structure.
Lazy Loading
Lazy Loading is a technique from web development. Instead of trying to do everything at once (ie in a create Event), we do things slower over a longer duration. In practice, we can use alarm or step Events to defer loading to later frames.
# instance## create```alarm_set(0, 5)-- set an alarm for five frames later```## alarm_0```-- expensive code here```
Many Rusted Moss objects have partial create and room_start Events. Instead of trying to do everything immediately, non-critical code can wait one or more frames when the load is lower. Remember, we get 16 milliseconds to render a frame, and not every frame needs all 16. The difference between 8 and 12 milliseconds per frame is invisible, the difference between 16 and 20 is a negative steam review.
Conclusion
Writing code is easy. Writing code that’s fast, easy to maintain, and doing it quickly enough to ship a product is impossible. Rusted Moss has performance issues, but it’s sold more copies than any game you or I will probably make. We’re also using microseconds, which we get over ten thousand of; the 4 microseconds from inlining a function isn’t worth making the code unreadable.
You also might’ve spotted some other optimizations. An early return after finding an element, or inlining the function call, or something else I missed. But don’t waste time on simple stuff; look for the 80% cut.
Throughout this documentation, there have been a lot of references to Rusted Moss Mod Loader (RMML). RMML is a tool created by me to aid in my personal modding journey (not that it can’t be used by others), and its design is heavily influenced by its history. I highly recommend using it, or building something similar, if you are going to do serious modding.
This article contains detail on how to install RMML and the basics for making mods with it.
Install
RMML is available on the rm-mod-manager Github page. Download the latest release’s rmml_{VERSION}.zip file, and unzip it into the Steam mods folder, replacing meta_info.ini.
You can write mods using the “standard” .ini syntax or RMML’s .md syntax (or index.csv). Mods written with RMML installed should be placed in the mods/rmml folder and enabled with Rusted Moss Mod Manager in-game, or added to the modlist.
For more information about the syntax, check out the full page.
RMML also uses the save folder for storing logs and other information. If you’re using Rusted Moss Mod Manager (which comes installed with RMML), any mods you download are also in that folder. On Windows, this is %LocalAppData%/RustedMoss. For more information about how RMML works, see the full article.
Rusted Moss Mod Manager
RMML comes installed with Rusted Moss Mod Manager (RMMM). This mod lets you download mods off the internet, manage your mod installation, and adds safety features.
modlist.txt
RMML completely bypasses Rusted Moss’s mod loading when loading other mods, and doesn’t read the mod list in meta_info.ini. Instead, the file modlist.txt in the mods directory stores a list of mods to load.
# put your mods here (or disable them with #)# mods are loaded (and run) in the order written herermml_modlister.mdccw_shark.mdcc_shark.mdmy_mod_here.mdmy_other_mod.iniyour_mod_folder/mod_name.md
If you’re using RMMM, this file is located in the saves folder instead of the Steam folder. If you’re using RMMM, I recommend ignoring this file, and using RMMM to manage your mods in-game instead.
Features
Differences
For most use cases, RMML is an otherwise invisible middleman that sits between your mod’s code and Rusted Moss. However, there are a things to keep in mind when using RMML.
Game Objects, global.rmml_current_mod, and self.mod_name
Because of boring technical reasons, RMML can’t just make a omod_controller_my_mod Game Object and attach your code to it. Instead, it uses self.mod_name as a key into a struct pairing your mod’s name with a Catspeak function call.
RMML tracks the currently running mod with global.rmml_current_mod, which should be compared against when using with or instance_find.
with omod_instance { if self.mod_name != global.rmml_current_mod{ continue} ...}
You can exploit this to create instances for other mods or use sub-mods.
Rusted Moss Mod Loader creates a controller Instance for each mod that is loaded. This lets you use alarm Events and Instance variables normally. However, you can’t use instance_find or with directly, and need to check for the mod name as demonstrated above.
player
In INI modding, omod_player Events don’t set self to the Player Instance. RMML sets self to the current omod_player Instance automatically. Additionally, the Player Instance is sandboxed like other Instances; if you want to use the Player, you need to set self.mod_name on the Instance.
with oplayer { instance_change(omod_player, false) self.mod_name = global.rmml_current_mod}
You also can’t have two mods installed that try to modify the Player, since only one mod’s Event code can run. This is a solvable problem, but goes against my design goals with RMML since it would be too invasive. You’d need to support three types of Player mods: mods that runs before event_inherited(), mods that replace the normal inheritance, and mods that run after event_inherited(). And make sure the Player instance is properly converted into an omod_player in all cases.
Mod File Support
RMML doesn’t just support .md files. It can interpret .ini, .meow, and index.csv files. All mods loaded by RMML receive its main benefits, unless otherwise limited.
.md: The standard mod file, simplified Markdown. A classic one-file solution for small- to medium-sized mods.
.ini: The standard mod file, like sniper_bnuy.ini. Still suffers from single-line coding. Included for easy porting of existing mods.
.meow: A Catspeak source file, run immediately after loading. This happens during the mod loading process, letting you modify RMML’s behavior or load your mod manually. Largely super-seeded by the game start script and index.csv.
index.csv: A .csv file that associates Catspeak source files with a Game Object and Event (and optional sub-mod). Features faster parsing and better ergonomics.
Internally, all mods (except for .meow scripts, which run immediately) are eventually inserted into global.rmml_map.
.ini
The original .ini syntax. This is the only mod syntax that Rusted Moss supports directly. Support for .ini mods is only included for legacy purposes, I highly recommend not using it.
The classic RMML syntax, supporting all features. This is recommended for most mods, as it simplifies development and distribution. In short, it’s a simplified version of Markdown, with each code fence interpreted as Catspeak code and H1 and H2 headers used to specify Game Object and Event names.
a game object header# controlleran event header## room_starta code fence, interpreted as Catspeak```-- some codeinstance_create_depth(0,0, 0, omod_instance)```another event## draw_gui_end```...```a second game object# instance...
Game Objects are specified using a single H1 header (ie # controller), with all following code and H2 headers applied to that Game Object. H2 headers (ie ## room_start) are Event names and affect the code block immediately following it. Event code for an Event goes between sets of three backticks ``` at the start of a line and is interpreted as Catspeak code. These code fences can include line breaks, comments, and both types of quotes.
All other text in a .md file is ignored. If you need explicit comments (such as for commenting out code fences), you can use HTML comments (<!-- Comment -->).
If you’re using my VS Code highlighter, you’ll want to add catspeak, sp, or meow after the three backticks (ie ````catspeak) to specify the language.
index.csv
A file named index.csv inside a folder in the mods/rmml directory can also be interpreted as a mod. This is a csv, without a header. Each row is in the following order.
If your file names contain spaces, you should wrap the entire path in double quotes ". Inline code blocks containing double quotes should be wrapped in double quotes, with double quotes in the code replaced with two double quotes (ie ""string""). Inline code blocks should be kept to a minimum, because otherwise you’re just using an ini mod.
I recommend using index.csv format for large mods. They load much faster (by bypassing RMML’s slow .md parser) and have better developer ergonomics compared to managing a 1000+ line file.
Game Start Script
If you use a .md mod and don’t use a Game Object and Event header, the resulting code is run during the mod loading process, and only once per game start. You can use this to load sprites or initialize global scripts, without needing to check a global or parse extra Catspeak.
no object or event header```sp-- this code only runs the first time the mod is loadedglobal.my_sprite = sprite_add("mods/rmml/my_mod", 1, false, false, 0, 0)```# controller## draw_gui_endhas headers```spdraw_sprite(global.my_sprite, 0, 0, 0)```
index.csv mods also support the game start script, by using empty Game Object and Event columns.
RMML implements multiple safety features to prevent mods from crashing your game on start up, which would prevent you from accessing RMMM’s UI. This is a list of changes Dev mode currently implements. You can implement your own Dev features in your own mods.
Better line numbers for .md mods. Counting starts from the start of the file, instead of the start of the fence.
Mods are fully loaded when you return to the main menu, instead of loading from a cache.
RMMM resets the mod list if a startup crash (ie syntax or room_start error) is detected
The easiest way to enable dev mode is with a game start script:
Game Start Scripts
.md Mod
index.csv
INI File
```global.rmml.dev = true```# controller...
,,"do { global.rmml.dev = true }"...
Unsupported :)
JaySpeak
JaySpeak is a dirty hack, the pinnacle of my lack of planning. The idea is that Javascript code is similar enough to Catspeak, so why not convert Javascript code (which has powerful developer tools) into Catspeak code?
INI Mod VS RMML Mod
JaySpeak
Catspeak
// commentlet x = function(a, b, c) { return a || b && type(c) == "string"}
-- commentlet x = fun(a, b, c) { return a or b and typeof(c) == "string"}
In practice, this sucks. RMML reads JaySpeak blocks ( ```js) and does simple string replacement on the resulting code. This leads to all sorts of weird, difficult to debug errors as parts of your code are silently converted into illegal function calls and misnamed variables. It’s also slow, since the entire code fence must be placed into a string for replacement.
If you still want to use it (for some reason), you can add js as the .md code fence language.
# controller## create```js// commentlet x = function(a, b, c){ return a || b && type(c)== "string"}```
I will not be adding JaySpeak support to index.csv mods. Implementing a Typescript -> Javascript -> Catspeak pipeline is an exercise left to the reader.
Finding Mod Files
modlist.txt entries are treated as partial paths, which are added to mods/rmml. The partial path also determines the mod’s internal mod name, used by global.rmml_current_mod and self.mod_name.
Name on modlist.txt
Full Path
Internal Mod Name
my_mod.md
mods/rmml/my_mod.md
my_mod
my_mod.ini
mods/rmml/my_mod.ini
my_mod
my_mod.meow
mods/rmml/my_mod.meow
my_mod
path/to/mod.md
mods/rmml/path/to/mod.md
path/to/mod
Instead of a file, you can specify a folder. If you add my_mod to modlist.txt, RMML will look for the following files, in order. In all cases, the internal mod name will be the name of the folder (my_mod).
Source File Partial Path
Full Path
my_mod.md
mods/rmml/my_mod/my_mod.md
index.md
mods/rmml/my_mod/index.md
index.csv
mods/rmml/my_mod/index.csv
If you want RMMM to properly find your mod, it should either be a single file (.md, .ini, or .meow) or one of the three folder options. Additionally, if your mod doesn’t fit in a single file (because of external sprite assets, or split source files, or other external data) all of your mod’s files should be placed in your main folder.
Here’s an example layout, using index.csv to split code into multiple files.
By default, all modded code you write is associated with your mod’s internal name. .md and index.csv mods support registering Event code with other mods instead of the standard mod.
The text after the Game Object header (# instance) is interpreted as a sub-mod, and supports all of the standard RMML features. Internally, sub-mods are their own mod, featuring their own mod name (ie my_mod_a and my_mod_b). This means you can set mod_name to that mod’s name and run its code, which lets you emulate creating new Game Objects.
Note that these sub-mods have no relation to the file they’re in. Using global.rmml.unload on my_mod won’t unload my_mod_a or my_mod_b. While you can specify sub-mod controllers, RMML won’t make a controller with that name automatically. I also recommend adding a common prefix to your mods, to prevent overriding someone else’s mod (unless this is desired).
Publishing to the Database
The rm-mod-database, which RMMM uses to store mods, isn’t just for me. If you have a mod you want added, you can either submit a Pull Request updating manifest.json or contact me (Harlem512) on Discord in the # modding-discusson or DMs and I’ll do the hard part. I do have a few submission requirements, however.
Host on a Permanent Link. Because I don’t want to distribute malware, all links in manifest.json must be permanent (enough) or managed by me. I will basically only accept raw Git-based links (Github, Gitlab, BitBucket, etc) pointing to a blob, since these cannot be changed unless the site itself goes down. Additionally, it must be a raw link; clicking it should immediately download the file without user input. If you need help, I can store your mod in the repo directly (crediting you, of course).
Include a proper name/description/author/version. manifest.json stores what your mod is named and its description. These fields should be filled out, and should describe your mod. Descriptions also support Scribble formatting, except for color.
Use the mod list standards. Your entire mod must either be a single .md or .ini file, or a folder containing one of the folder-based options. All file references must accept that the mod will be placed in the mods/rmml folder. If you need help converting a .ini mod or packing everything into a single directory, I can work with that.
Don’t cause permanent damage. Your mod should be able to be uninstalled without preventing Rusted Moss or another mod from working (unless it’s a dependency). The only exception is save files: save files changed while your mod is installed are free game, but your impact should be minimized. If you need to clean up your resources, you can use an uninstall script to run code when your mod is uninstalled.
Have common human decency. Don’t make anything offensive, please. I am One Person doing this in their free time. If something is delayed or broken, please don’t lash out at me or the official devs.
Uninstall Script
If you’re looking to publish to the RMMM and are messing with the file system, you might want to clean up your mod’s files. Most mods can put all of their files in their mod’s folder, but you might need to mess with Rusted Moss’s files more directly. In these cases, you can use an uninstall script, which runs after your mod is uninstalled.
The code inside a file named uninstall.meow in your mod’s folder will be executed when your mod is uninstalled.
This feature doesn’t come with all RMMM installations, so if you’re relying on this behavior (such as to replace Fern_Custom), you’ll want to check the current RMMM version using >.
if global.rmmm_version> 1{ -- behavior exists, continue}
API and Technical
Caching
global.rmml_map
global.rmml.register
Runtime
Most of RMML’s code is concerned with populating global.rmml_map as quickly as possible. However, something needs to read that map and actually run modded Event code. RMML’s runtime has changeddrastically between versions, so let’s go over the latest.
Rusted Moss runs modded Event code using Events that look like this.
// gml_Object_omod_controller_Create_0// check if the function existsif ds_map_exists(global.mod_map, "controller_events_create"){ // ensure `self` gets set to this Game Object ds_map_find_value(global.mod_map, "controller_events_create").setSelf(self) // call the stored function ds_map_find_value(global.mod_map, "controller_events_create")()}
Each Event has its own key, based on the Game Object and Event names. global.mod_map is a ds_map, and maps these event keys to Catspeak function calls.
Design and History
Design Goals
RMML’s design is heavily influenced by its history: it is, first and foremost, a tool designed to be as invisible as possible. Modding without RMML should feel the same as with RMML installed, not just for portability but also for debugging. Understanding how Rusted Moss works is difficult enough, having to also understand an intermediate layer adds too much complexity.
With that being said, RMML has features that separate itself from standard modding. However, all additions to RMML’s scope must align with the rules: do not make modding different.
RMMM is an example of this design goal. Integrating the mod manager into the mod loader is likely a good idea, and there are features of RMMM that would benefit from tighter integration with RMML, however it breaks the design goal. RMMM adds a significant amount of complexity, making heavy use of the saves folder instead of the Steam folder. It also adds another point of failure: RMML is well-tested and relatively simple in execution, RMMM is over 600 lines of complex UI logic.
With that being said, I’m open to feature ideas for RMML or RMMM. A good example is the Player. Figuring out how to properly support multiple mods that try to use omod_player is an open problem, and while it’s probably best served as a library mod, I’m open to a good RMML-only idea.
Pain Points
Rusted Moss Modding primarily features five major pain points. RMML was created to solve these issues, however the last two have dropped out of RMML’s scope.
Coding in One Line: ini mods only support one line of Catspeak, limiting program size and increasing mental load.
The most obvious issue is that all Catspeak code must fit in one line. This is a byproduct of mods existing as .ini files, instead of something more purpose-built.
This is the create function for one of my mods (Ameli Palette) and condenses 100 commented lines into 2000 characters. Obviously, your code doesn’t need to be this dense; this code is generated using a script that removes all unnecessary characters. But there’s an even bigger issue than unreadable, unmaintainable code.
Because of boring technical reasons, Catspeak is limited to about 4000 characters per line of code. Since all Rusted Moss Event code is a single line, this essentially places a hard limit on the size of a potential mod.
RMML loads Event code from parts a normal text file, which lets us use line breaks.
One Function per Event
Lets say I have these two mods. What do you expect to happen?
If you’re used to other modding systems, you might expect to see both “Foo” and “Bar”. In Rusted Moss, this isn’t the case. Because of boring technical reasons, only a single Catspeak function can be stored per Event, so it only prints “Bar”, the message from the mod loaded second.
RMML gets around this by storing Event code in its own data structure and providing global.mod_map a function that reads from that data structure and calls all code for the Event, instead of only the last.
No Instance Sandboxing
Here’s another two mods. What do you expect to happen?
You might expect that each mod would act independently; name.ini would show “Fern” in the top left corner, and float.ini would create a bunch of Instances that slowly float downwards. Instead, you get a runtime error.
Rusted Moss doesn’t differentiate between Instances created by different mods, which means every instance Instance runs the exact same code. This can cause errors, since you can’t predict what other mods will do with their Instances. Sadly, truly fixing this problem is impossible because of boring technical reasons since it would involve creating new Game Objects (and adding code to their Events) at runtime, which GameMaker doesn’t support.
RMML half solves this issue by attaching a mod_name field to all Instances, and only running modded code if the mod_name on the Instance matches the mod_name of the mod. This sandboxes each Instance, ensuring that Event code only runs on Instances that are expecting it.
Of course, this isn’t a flawless system. Using instance_change to turn a normal Instance into a moddable Instance won’t apply the mod_name field, causing it to run no Event code. with blocks and instance_find (and similar) can’t read Instance variables, so each Instance must be checked individually.
with omod_instance { -- `rmml_current_mod` tracks the current mod, so this filters out any Instances that weren't created by this mod if self.mod_name != global.rmml_current_mod{ continue} ...}
This issue is the core reason why I felt Rusted Moss Mod Loader needed to go from a private script to a public tool other humans can use. This effectively means that only one mod can be loaded at once, which greatly limits what you can do with mods.
Annoying Distribution
If you look in the mods folder or investigate the source code, you’ll see references to a few files that are seemingly absent from the documentation. asset_list.ini, constant_list.ini, function_list.ini, and method_list.ini were used to tell Catspeak what data should be exposed for modding. Distributing a mod required also distributing these files, however merging these lists between mods would be a problem.
Early versions of RMML featured a ## config header, which replaced the need for managing these .ini lsits. Thankfully, Rusted Moss enables the super-secret Catspeak option that exposes everything for modding, which makes all of this obsolete.
Rusted Moss also lacks an easy-to-use modlist. The meta_info.ini file stores the standard modlist, however it is sensitive to naming changes. In order for the game to load mod_1, a mod_0 entry must exist, which makes disabling mods tedious and prone to errors.
Of course, you still want to share mods with other users. Rusted Moss doesn’t support using Steam Workshop for mods, and until recently mods were distributed using a channel in the official Discord. RMMM exists to solve the distribution problem by letting users download a single mod (RMMM) and download the rest in-game from the internet.
I don’t expect RMMM to completely standardize mod distribution, simply because I am One Person doing this in their free time, but I digress.
No Syntax Highlighting
Syntax highlighting adds colors to various language constructs and generally makes things easier. It also makes programming more fun, which I value more than anything else.
-- misspelling, wrong color = easy to fixsel.name = self.name + True - true-- wrong boolean operatorif a && b and c {...}
Originally, syntax highlighting was provided using a dirty hack that let you use Javascript developer tools. After getting burned by my own hack (it also converts syntax inside strings, including the strings that do the conversion), I set out to make a Visual Studio Code extension which provides basic RegEx syntax highlighting. The same highlighting is used by the docs, which is why everything looks pretty.
History
When I first started work on what would become RMML, I didn’t anticipate it being this large of a project. I want to talk a little bit about the history and future of RMML, because this is the kind of thing I find interesting.
Legacy versions of RMML are available on the Github, but I don’t recommend reading them.
Version Zero
RMML version zero was a Jupyter notebook that self-referenced its own Markdown blocks as RMML syntax, outputting a reformated .ini mod that removed all the line breaks. Markdown is almost perfect for this purpose, as a mix of visually-distinct markup (# headings) and code (```) sections. A Python script in the notebook could easily read the Markdown blocks and convert them into something Rusted Moss can read directly. Despite being literally the first thing I thought of, Markdown’s syntax has served well and I would use the same syntax again if I could start from scratch.
Initially, this arrangement seemed ok. The 4000 character limit still existed, but that’s a lot of code to write before it’s an issue. However, as stated, there are two biggerissues. These are both unsolvable with a simple md to ini converter, and need something more powerful. More aware of how Rusted Moss actually loads code.
Public Release
The first versions of RMML are (probably) lost to time, but the core has remained the same. Instead of Rusted Moss calling your mod’s code directly, it calls RMML’s code, which then performs its intermediate logic before calling your mod’s code. Naturally, there’s a lot of hidden complexity there and a lot of weird scoping issues that needed to be ironed out.
I’ve also referenced a few minor issues that were resolved. The first was a lack of syntax highlighting, which is basically a personal issue. RMML is (barely) capable of converting Javascript code fences (```js) into Catspeak using basic string replacement, which I abused for syntax highlighting before abandoning it in favor of a real Catspeak syntax highlighter using TextMate grammars (after wasting too long trying to figure out why function in a string was replaced with fun). This is probably going to be cut in a future release, since it hurts mod loading performance and adds unnecessary complexity.
Another minor issue had to do with sharing mods. In earlier versions of Rusted Moss, you needed to manage the other .ini files in the mods folder, adding function and asset names to the lists so Catspeak can expose them. This causes issues, since there isn’t an easy way to distribute these without overwriting other mods. RMML solved this by reading a special header in the .md file and exposing them using Catspeak’s API. In the latest versions, this is unnecessary. The Rusted Moss devs enabled the super secret setting which exposes everything, which also makes managing these files unnecessary.
Future
So what’s next? RMML is surprisingly simple in its execution. It is solely concerned with loading mods and ensuring all three major issues are resolved to the best of its ability, while minimizing the performance overhead. RMML is also reaching the limitations of its architecture, and deserves a proper redesign using my updated knowledge. A future version will likely include a better user interface, probably as an additional mod, and feature improvements to every part of the process.
Official tools
When modding support was initially announced, there was some discussion from the developers about switching away from .ini files towards something easier to work with. It has been long enough since the last update on this for me to say that I don’t think it’s coming. I don’t know if this is because RMML exists and solves these issues, or if it’s because there are less than 10 people who have attempted modding the game and valuable developer time is better allocated, or some mystery third option, but regardless I will continue to maintain this project into the indeterminate future.
RMML Runtime History
Most of RMML’s code is for parsing, taking a source file and converting it into Catspeak functions. The runtime, what reads those functions and calls them based on the running mod, has evolved dramatically. Here’s a funny comparison between RMML, as it existed a month after modding support was released and now.
RMML 1
RMML 6
-- executes the passed event on the object that called itglobal.rmml_exec = function (event_name) { let is_controller = string_starts_with(event_name, "controller") -- if this instance isn"t a controller and hasn"t called its create, do that first if (!is_controller && !self.rmml_created) { self.rmml_created = true -- call the create event on this object let new_event = string(string_split(event_name, "_")[0]) + "_events_create" global.rmml_exec(new_event) } -- check if we have events to run let mod_cs = ds_map_find_value(global.rmml_modmap, event_name) if (mod_cs == undefined) { return } -- loop over all registered events let i = 0 while (i < array_length(mod_cs)) { -- unpack modid and code let elem = mod_cs[i] let _mod_name = elem[0] let _catspeak = elem[1] -- controllers get to speedrun their code if (is_controller) { _catspeak() -- no mod_name set } else if (self.mod_name == undefined) { -- create events get a pass, since they don"t have mod_names set yet, -- unless this is the manual create event if (!string_ends_with(event_name, "create") || self.rmml_created == true) { -- this throws a runtime exception NON_CONTROLLER_INSTANCES_MUST_HAVE_A___mod_name__() } -- run our code if the mod_names match } else if (self.mod_name == _mod_name) { _catspeak() } i += 1 }}
-- get event out of map, checking mod namee = global.rmml_map["INJECT"][self.mod_name]if e { c = global.rmml_current_mod global.rmml_current_mod = self.mod_name e.setSelf(self) -- run code e() global.rmml_current_mod = c}
RMML 1’s runtime is 40 lines and includes breaking behavior (delayed create Events), a while loop (slow), and multiple string operations. There also wasn’t any mechanism to “turn off” this function; if a mod didn’t use draw_end, most of this code would run regardless.
RMML 6’s runtime is 10 lines, packing extra features (setting global.rmml_current_mod) and finding modded code with a simple lookup operation. It’s also fast enough (after some forbidden techniques) to be completely inlined, rather than relying on a function call.
Migration to RMML 6
RMML 6 and RMMM
Rusted Moss Mod Loader 6 is the future. Featuring performance improvements, a new syntax, safety features, and an included Mod Manager, modding has never looked better. With this update, RMML’s core API has essentially locked in; any future updates cannot modify behavior without breaking existing mods or creating dependency hell.
Naturally, “the last” RMML update means breaking some things. This page details what has changed, but here’s the short version for players of mods:
For Players
After (optionally) backing up your mods folder, you can download RMML 6 and install it like normal. If you have existing mods, you can either download them again with Rusted Moss Mod Manager in-game, or put your old mods in the mods/rmml folder. Downloaded mods are located in the Saves folder. Any mod configuration (such as Maya/Ameli palette options) should be placed in the mod.
The old mods/rmml/modlist.txt (in the Steam directory) has been to mods/modlist.txt in the Saves folder. If you have manually installed mods, they must be re-added to the mod list. RMMM can do this for you using the in-game UI, or you can find the new list in the Saves folder. The mod list in the Steam directory is only used if the game crashes.
For Mod Makers
Some of you already have RMML 6 Beta installations, which can be safely updated with RMMM. If you have an existing RMML 5 (or lower) installation, you can follow the for players section and come back.
Here’s the quick changed stuff:
Steam mods/rmml/modlist.txt -> Saves mods/modlist.txt: You shouldn’t need to modify either file, since RMMM will manage them.
rmml_log.txt -> mods/log.txt: Logging functions have changed file paths. If you need to use the old filename, you can set global.rmml.log_name like normal.
Each mod makes an omod_controller: Each mod has its own omod_controller Instance; Alarms and instance variables will work as expected, with and instance_number will not.
global.rmml.current_mod -> global.rmml_current_mod: The currently running mod. self.mod_name and how mod names are determined is unchanged.
Dev Mode: (Some) startup errors are caught, no hot-reloading, etc. See the link for more details.
global.rmml.modmap -> global.rmml_map: Structure has changed. Still shouldn’t modify this directly, use global.rmml.register instead.
global.rmml.exec, global.rmml.purge_fn, and global.rmml.as: They’re gone, never to return again (probably).
No more duplicate Game Object and Event pairs: Before, you could specify multiple controllercreate (for example) code fences. This is now an error. Unless you’re abusing RMML’s specifications, this shouldn’t change anything.
And here’s the new (technical) stuff:
global.rmml.try(try, catch, ...args): Wraps the passed function in a try-catch block using magic powers.
global.rmml.warn(msg) and global.rmm.warnings: Warnings thrown during the mod loading process (or user programs). Most errors from before are warnings now.
global.rmml.clone(struct): Shallow copies a struct of structs. Used for RMML’s internals (please don’t modify it <3).
global.rmml.modmap -> global.rmml_map
The structure of RMML’s mod map, used for associating Events, mod names, and Catspeak functions, has changed. In RMML 5, it was a struct containing an array containing a flattened tuple of a mod name and a function.
You probably weren’t modifying this structure directly (use global.rmml.register), but there is one user-facing change. RMML no longer supports multiple code blocks for the same Game Object and Event. For example, the following fails.
# controller## create```-- this doesn't run-- (and causes a startup error)show_message("Hello")``````-- this DOES runshow_message("World!")```
Having multiple code blocks for the same Event-Game-Object pair was counter-intuitive and could lead to bugs, such as if you forgot an Event header.