Single-Page Docs

Introduction

Quickstart

Prerequisites

  1. 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.
  2. 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.
  3. 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”.

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.

meta_info.ini

[general]
mod_enabled = 0
[meta_info]
mod_0 = sniper_bnuy.ini

Any mods you want to load should be placed under the [meta_info] header and must have indexes in order.

[general]
mod_enabled = 1
[meta_info]
mod_0 = my_mod.ini
mod_1 = myOtherMod.ini
mod_2 = "With spaces.ini"
mod_4 = NotLoaded.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 = enabled
instance = enabled

[controller_events]
room_start = "i = 0; while ..."

[instance_events]
create = "self.parent ..."
draw = "if ..."
step = "if ..."

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.

You can visit the RMML Quickstart page to get started with RMML.

Final Notes

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”.

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.

[general]
mod_enabled = 1
[meta_info]
mod_0 = my_mod.ini

Rusted Moss Mod Loader

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.

sniper_bnuy.ini

[object_list]
controller = enabled
instance = enabled

[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; }"

[instance_events]
create = "self.parent = instance_nearest(self.x,self.y,oenemy_flame_sniper);"
draw = "if ( 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 = "if ( instance_exists(self.parent) ) { if ( self.parent.legs == smaya_legs_run )  { self.parent.vsp = self.parent.vsp -4; } }"

This a mod file, and contains the Catspeak code that runs. Let’s break it down.

Object List

[object_list]
controller = enabled
instance = enabled

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.

  1. 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.
  2. Gamemaker Functions: instance_number, instance_find, and instance_create_depth are all Gamemaker functions. Rusted Moss conveniently exposes every Gamemaker function for us, which is cool. However, because of boring technical reasons, some of these functions just don’t work.
  3. 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.
  4. 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.
  5. 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:

omod_instance Code

[instance_events]
create = "self.parent = instance_nearest(self.x,self.y,oenemy_flame_sniper);"
draw = "if ( 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 = "if ( instance_exists(self.parent) ) { if ( self.parent.legs == smaya_legs_run )  { self.parent.vsp = self.parent.vsp -4; } }"

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" Instance
if ( 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.

global.hello = fun(msg) {
  show_message(msg)
}
global.hello("Howdy!")

Undertale Mod Tool

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.

Catspeak and GML

Overview

Catspeak is very similar to Gamemaker Language (GML), and both are high-level, dynamically-typed, procedural programming languages featuring first-class functions. Both run on top of the Gamemaker engine, which is object-oriented with an event-driven model. If those buzzwords confuse you, ignore them, and let’s write some code.

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.

CatspeakGML
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 = 0
let 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
CatspeakTruthy-nessDescription
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
CatspeakGMLDescription
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 = 0
while _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
CatspeakGML
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 modding
let foo = 0
// A GameMaker Language code block, the language of source code
var foo = 0
# A `.ini` file used for Rusted Moss configuration or modding
# Event code is highlighted as Catspeak
code = 'let foo = 0'
A `.md` file used by Rusted Moss Mod Loader
Event 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 engine
inst.sprite_index = splayer_offscreen
-- setting a custom Instance variable
inst.name = "Fern"

Some Instance variables are managed by the engine for various functions.

Events

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 name
create = 'self.name = "Maya"'
# `draw` event, draw our name each frame
draw = '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.

PathNameR/WUsed For
SteamLibrary\steamapps\common\MoseSteam FolderRead-onlyRM Files, mods
%LocalAppData%\Rusted_MossSave FolderRead/WriteChangeable files, persistent data
%LocalAppData%\TempTemp FolderRead/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 NameNameParentNotes
omod_basicbasicpar_basic
omod_controllercontroller-Created at game start, doesn’t pause, persistent
omod_enemyenemypar_enemyHas HP, can be grappled to
omod_hitboxhitboxpar_hitbox
omod_instanceinstance-Doesn’t pause (they lied)
omod_playerplayeroplayerHard 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  < par_hitbox
   ^
par_draw
   ^
par_basic < oplayer
   ^
par_enemy

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.

Game ObjectEvents
basiccreate, step, draw
enemycreate, destroy, step, draw, draw_end, alarm_0, user_0, user_1
hitboxcreate, destroy, step, draw, alarm_0, alarm_1*, user_0, user_1, animation_end, cleanup
playercreate, step, step_end, draw, draw_end, draw_gui_begin, alarm_0, animation_end, cleanup

The hitbox alarm_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.

instance_change(oplayer, false)
event_inherited()
instance_change(omod_player, false)

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.

self.x  -- invalid
with oplayer {
  self.x  -- correct
}
with omod_player {
  self.x  -- correct, only gets modded players
}
let plr = instance_find(oplayer, 0)
plr.x  -- correct, doesn't set `self`

Moddable Events

Moddable Events

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}).

Modding NameRole
step_beginRuns before Alarm Events.
step
step_end
draw_beginFirst Event that allows drawing to the screen.
draw
draw_end
draw_gui_startFirst Event that uses screen coordinates instead of world coordinates.
draw_gui
draw_gui_endOnly Event that doesn’t have the biome shader. Perfect for GUI elements.

Next are 12 special Events, that only run under certain conditions.

Modding NameDescription
alarm_0, alarm_1, alarm_2Three Alarm events for indexes 0, 1, and 2.
animation_endRuns when an Instance’s Sprite animation ends.
cleanupRuns immediately after an Instance is removed from a Room (for any reason), after the destroy Event runs.
createRuns immediately for an Instance when an Instance is created.
destroyRuns when an Instance is destroyed, typically through instance_destroy(...)

These Events are also available, however have an other_ midfix for global.mod_map.

Modding NameDescription
room_startRuns when a Room is first entered. It happens after the create Event for Instances that are part of the Room.
room_endRuns when a Room is left, such as with room_goto. The current Event finishes, and then this Event is called.
user_0, user_1, user_2Three User Events for indexes 0, 1, and 2.

Alarm Events

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.

## create
```
self.my_alarm = 5
```

## step_begin
```
self.my_alarm -= 1
if self.my_alarm == 0 {
  show_message("Hello World")
}
```

User Events

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.

EventOrder (left first)Formal
room_start and room_endAlice, David, Bobby, CarolLowest depth, then oldest
All step EventsAlice, Bobby, Carol, DavidOldest to youngest
All draw EventsCarol, Bobby, David, AliceHighest depth, then youngest
with statementDavid, Carol, Bobby, AliceYoungest 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).

# controller

## room_start
```
let person = fun(depth, name) {
  let inst = instance_create_depth(0,0, depth, omod_instance)
  inst.name = name
  return inst
}

if !global.inited {
  global.inited = true
  person( 0, "Alice").persistent = true
  person(10, "Bobby").persistent = true
  person(10, "Carol").persistent = true
  person( 0, "David").persistent = true
}

person(  0, "Erica")
person( 10, "Frank")
person( 10, "Janet")
person(  0, "Harry")

with omod_instance {
  show_message(self.name)
}
```
RoomOrder (left first)Formal
InitialHarry, Janet, Frank, Erica,
David, Carol, Bobby, Alice
Youngest to oldest, ignoring persistent tag
SubsequentHarry, 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.

RoomOrder (left first)Formal
InitialAlice, Bobby, Carol, David,
Erica, Frank, Janet, Harry
Oldest to youngest, ignoring persistent tag
SubsequentAlice, 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
KeywordInternal 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.

// gml_Object_oplayer_Create_0
...
  sprite_dead = 595
  sprite_hit = 1369
  hair_start_col = 3026478
  hair_end_col = 6707256
  hair_number = 3
  hair_size = 2.5
  hair_alt = -1
...

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

```sp
global.ripper = {}
global.ripper.type = 2
global.ripper.saved_index = 0
global.ripper.max = 100
```

## step

```sp
let log = file_text_open_append("asset_list.txt")

let i = 0
while 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)
```

GML Reference

TODO

refs

Rusted Moss, the Code

Mod Loading

From Text to Events

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_controller create Event.

// gml_Object_omod_controller_Create_0
// check if the function exists
if 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 load
file_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` file
ini_open("mods/meta_info.ini")
i = 0
while (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 loading
if (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 KeyDescriptionExample
{OBJECT}_events_{EVENT}Normal map key for most Eventscontroller_events_create
{OBJECT}_events_other_{EVENT}Map key for room and user Eventsinstance_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 code
let str = "!\"Hello World\""
let hir = global.__catspeak__.parseString(str)
let function = global.__catspeak__.compile(hir)
-- call our function
function()

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 file
let function = compile_mod_event("mods/codeFile.txt")
-- attach the function to the `controller`'s `create` Event
ds_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 coordinates
m = 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].rom
else
  // ... 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_test
let room_meta = global.map_data_[rm_test][0]
let xx = room_meta.xx
let yy = room_meta.yy
-- read from map
let 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 room
global.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.

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:

gpu_set_fog(true, c_white, -1, 0)
x -= 1
scr_draw_weapon_pickup(index)
x += 2
scr_draw_weapon_pickup(index)
x -= 1
y += 1
scr_draw_weapon_pickup(index)
y -= 2
scr_draw_weapon_pickup(index)
y += 1
gpu_set_fog(false, c_black, 0, 0)
scr_draw_weapon_pickup(index)

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.

if (ini_read_string("controller_events", "create", "") != "")
  ds_map_set(global.mod_map, "controller_events_create",
    ini_read_string("controller_events", "create", "")
      .ini_read_string("controller_events", "create", "")
      .parseString(global.__catspeak__)
      .ini_read_string("controller_events", "create", "")
      .ini_read_string("controller_events", "create", "")
      .parseString(global.__catspeak__)
      .compileGML(global.__catspeak__)
  )

However, there is a fork by Pizza Tower modders that can understand this code better.

if (ini_read_string("controller_events", "create", "") != "")
  ds_map_set(global.mod_map, "controller_events_create",
    global.__catspeak__.compileGML(
      global.__catspeak__.parseString(
        ini_read_string("controller_events", "create", "")
      )
    )
  )

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_abilities

System.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 here
global.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 = 0
while 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 = infinity
while 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.

# controller
## create
```
instance_create_depth(0,0, 0, omod_instance, {
  "type": 0
})
instance_create_depth(0,0, 0, omod_instance, {
  "type": 1
})
```

# instance
## create
```
match self.type {
  case 0 {
    self.sprite_index = splayer_offscreen
  }
  case 1 {
    self.sprite_index = splayer_maya_offscreen
  }
}
```

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 controller create 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
```sp
self.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 instance create 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 -1
if global.save_data == -1 {
  return
}

-- check if exists
if !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
```sp
with self {
  -- create an instance inside a `with` block
  instance_create_depth(0,0,0, omod_instance)
}
```

# instance
## step
```sp
show_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 are very complicated. 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:

let start = get_timer()

-- code goes here

let duration = get_timer() - start
show_message(string(duration))

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 = 0
while i < 1000 {
  -- code goes here
  i += 1
}

let duration = get_timer() - start
show_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.

if global._timer_iterations == undefined {
  global._timer_iterations = 0
  global._timer = 0
}
let start = get_timer()

-- code goes here

let duration = get_timer() - start
global._timer += duration
global._timer_iterations += 1
if global._timer_iterations % 600 == 0 {
  show_message(string(global._timer / global._timer_iterations))
}

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 array
global.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 array
draw_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?

# controller
## create
```
global.offscreenMap = {
  "Fern": splayer_offscreen,
  "Maya": splayer_maya_offscreen,
  "Ameli": splayer_ameli_offscreen,
}
```
## draw_gui_end
```
draw_sprite(global.offscreenMap["Maya"], 0, 10, 10)
```

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.

# controller
## draw_gui_end
```
sprite_draw(splayer_maya_offscreen, 0, 10, 10)
```

RMML

Quickstart

Paid Shill

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.

mods/
├── asset_list.ini
├── constant_list.ini
├── function_list.ini
├── meta_info.ini
├── method_list.ini
├── modlist.txt
├── rmml/
   ├── rmml.meow
   └── rmmm.md
├── rmml.ini
└── sniper_bnuy.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.

INI Mod VS RMML Mod
INI File.md Mod
[object_list]
controller = enabled
instance = enabled
[controller_events]
create='show_message("Foo")'
room_start='show_message("Bar")'
[instance_events]
create='self.y += self.vsp; self.vsp *= 0.5; if self.vsp < 0.1 { instance_destroy(self) }'
# controller
## create
```
show_message("Foo")
```

## room_start
```
show_message("Bar")
```

# instance
## create
```
self.y += self.vsp
self.vsp *= 0.5
if self.vsp < 0.1 {
  instance_destroy(self)
}
```

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 here
rmml_modlister.md
ccw_shark.md
cc_shark.md

my_mod_here.md
my_other_mod.ini
your_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.

instance_create_depth(0,0, 0, omod_instance, { mod_name: "your_mod" })

controller

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.

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.

[object_list]
controller = enabled
instance = enabled

[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; }"

[instance_events]
create = "self.parent = instance_nearest(self.x,self.y,oenemy_flame_sniper);"
draw = "if ( 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 = "if ( instance_exists(self.parent) ) { if ( self.parent.legs == smaya_legs_run )  { self.parent.vsp = self.parent.vsp -4; } }"

.md

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
# controller

an event header
## room_start

a code fence, interpreted as Catspeak
```
-- some code
instance_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.

IndexExampleWhat
0controllerA Game Object name
1createAn Event name
2filename.meow OR "do { show_message(""hello"") }"Either a relative path from mods/rmml/{MOD_NAME} or a single line of code (starting with do {)
3sub_modAn optional sub-mod name. Don’t include this to use your main mod.

Here’s what that might look like, with an example folder layout.

{OBJECT},{EVENT},{FILENAME_OR_CODE},{?SUB_MOD?}
controller,create,controller_create.meow
basic,step_end,my_basic_end.meow,inherited_mod
instance,draw,"do { show_message(""Hello World!"")}"
mods/rmml/my_mod/
├── controller_create.meow
├── index.csv
└── my_basic_end.meow

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 loaded
global.my_sprite = sprite_add("mods/rmml/my_mod", 1, false, false, 0, 0)
```

# controller
## draw_gui_end
has headers
```sp
draw_sprite(global.my_sprite, 0, 0, 0)
```

index.csv mods also support the game start script, by using empty Game Object and Event columns.

,,game_start.meow
,,"do { show_message(""Hello World!"")}"

Dev Mode

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.

The easiest way to enable dev mode is with a game start script:

Game Start Scripts
.md Modindex.csvINI 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
JaySpeakCatspeak
// comment
let x = function(a, b, c) {
  return a || b && type(c) == "string"
}
-- comment
let 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
// comment
let 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.txtFull PathInternal Mod Name
my_mod.mdmods/rmml/my_mod.mdmy_mod
my_mod.inimods/rmml/my_mod.inimy_mod
my_mod.meowmods/rmml/my_mod.meowmy_mod
path/to/mod.mdmods/rmml/path/to/mod.mdpath/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 PathFull Path
my_mod.mdmods/rmml/my_mod/my_mod.md
index.mdmods/rmml/my_mod/index.md
index.csvmods/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.

mods/
├── meta_info.ini
├── modlist.txt
└── rmml/
   ├── my_mod/
   ├── index.csv
   ├── sprites/
   ├── ally.png
   └── enemy.png
   └── src/
      ├── controller_create.meow
      ├── instance_ally_draw.meow
      └── instance_enemy_draw.meow
   ├── rmml.meow
   └── rmmm.md
controller,create,src/controller_create.meow
instance,draw,src/instance_ally_draw.meow,my_mod_ally
instance,draw,src/instance_enemy_draw.meow,my_mod_enemy

Sub-Mods

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.

# controller
## room_start
```
instance_create_depth(0,0, 0, omod_instance)
instance_create_depth(0,0, 0, omod_instance, { mod_name: "my_mod_a" })
instance_create_depth(0,0, 0, omod_instance, { mod_name: "my_mod_b" })
```

# instance
## create
```
show_message("Default Object!")
```

# instance my_mod_a
## create
```
show_message("Object A!")
```

# instance my_mod_b
## create
```
show_message("Object B!")
```

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.

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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.

mods/rmml/my_mod/
├── data.json
├── my_mod.md
└── uninstall.meow

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 changed drastically 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 exists
if 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.

  1. Coding in One Line: ini mods only support one line of Catspeak, limiting program size and increasing mental load.
  2. One Function per Event: Different mods cannot use the same Events without overwriting.
  3. No Instance Sandboxing: Every Game Object runs all Event code, ignoring what mod wrote it.
  4. Annoying Distribution: Rusted Moss’s internal modlist is very sensitive and other issues.
  5. No Syntax Highlighting: Catspeak doesn’t have a VS Code syntax highlighter. (I also didn’t know about the Notepad++ extension)

Coding in One Line

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.

create = 'while true { show_message("Hello World") }'

For short programs, this isn’t an issue. However, it quickly becomes unmanageable for larger mods.

create='if global.__ameli_hair{return}let pal_folder="mods/ameli_palette/";let palette=sprite_add(pal_folder+"palette.png",1,false,false,0,0)let surf=surface_create(1,1)surface_set_target(surf)draw_sprite(palette,0,-1,-55)let buff=buffer_create(8,buffer_fixed,1)buffer_get_surface(buff,surf,0)global.__ameli_hair=buffer_peek(buff,0,buffer_u32)surface_reset_target()surface_free(surf)buffer_delete(buff)shader_replace_simple_set_hook(shd_palette)if!global.player_use_shader{shader_set_uniform_f(shader_get_uniform(shd_palette,"col_num"),56)shader_set_uniform_f(shader_get_uniform(shd_palette,"pal_num"),2)shader_set_uniform_f(shader_get_uniform(shd_palette,"pal_index"),1)shader_set_uniform_f_array(shader_get_uniform(shd_palette,"palette_uvs"),[0,0,1,1])}texture_set_stage(shader_get_sampler_index(shd_palette,"palette"),sprite_get_texture(palette,0))let out_dir=temp_directory_get()+"rmap/";let f=file_find_first(pal_folder+"DO_NOT_TOUCH/*.png",0)while true{if f==""{break}let name=string_split(f,".png")[0]let index=asset_get_index(name)let width=sprite_get_width(index)let height=sprite_get_height(index)let subimage_number=sprite_get_number(index)let xoffset=sprite_get_xoffset(index)let yoffset=sprite_get_yoffset(index)let dest_img=out_dir+f;let pal_sprite=sprite_add(pal_folder+"DO_NOT_TOUCH/"+f,1,false,false,0,0)let surf=surface_create(width*subimage_number,height)surface_set_target(surf)draw_sprite(pal_sprite,0,0,0)surface_reset_target()surface_save(surf,dest_img)surface_free(surf)sprite_delete(pal_sprite)match index{case smaya_legs_idle{global.__ameli_idle=sprite_add(dest_img,subimage_number,false,false,xoffset,yoffset)}case smaya_legs_run{global.__ameli_run=sprite_add(dest_img,subimage_number,false,false,xoffset,yoffset)}case splayer_maya_legs_crouching{global.__ameli_crouch=sprite_add(dest_img,subimage_number,false,false,xoffset,yoffset)}else{sprite_replace(index,dest_img,subimage_number,false,false,xoffset,yoffset)}}f=file_find_next()}if!global.player_use_shader{shader_set_uniform_f_array(shader_get_uniform(shd_palette,"palette_uvs"),[0,0,0,0])}shader_replace_simple_reset_hook()sprite_delete(palette)'

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?

[general]
mod_enabled = 1
[meta_info]
mod_0 = foo.ini
mod_1 = bar.ini
foo.inibar.ini
[object_list]
controller = enabled
[controller_events]
create='show_message("Foo")'
[object_list]
controller = enabled
[controller_events]
create='show_message("Bar")'

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?

[general]
mod_enabled = 1
[meta_info]
mod_0 = name.ini
mod_1 = float.ini
name.inifloat.ini
[object_list]
controller = enabled
instance = enabled
[controller_events]
room_start='let inst = instance_create_depth(0,0,10, omod_instance); inst.name = "Fern"'
[instance_events]
draw_gui_end='draw_text(self.name, 0, 0)'
[object_list]
controller = enabled
instance = enabled
[controller_events]
step='let inst = instance_create_depth(100,0,10, omod_instance); inst.vsp = 2'
[instance_events]
step='self.y += self.vsp; self.vsp *= 0.5; if self.vsp < 0.1 { instance_destroy(self) }

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 fix
sel.name = self.name + True - true
-- wrong boolean operator
if 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 bigger issues. 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 1RMML 6
-- executes the passed event on the object that called it
global.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 name
e = 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:

And here’s the new (technical) stuff:

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.

global.rmml.modmap = {
  "controller_events_create": [
    "my_mod", fun () { ... },
    "your_mod", fun () { ... },
  ],
  "instance_events_other_room_start": [
    "my_mod", fun () { ... },
  ],
}

In RMML 6, this is a simpler struct of structs.

global.rmml_map = {
  "controller_events_create": {
    "my_mod": fun () { ... },
    "your_mod": fun () { ... },
  },
  "instance_events_other_room_start": {
    "my_mod": fun () { ... },
  },
}

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 run
show_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.