I stream Indie and Retro Games
she/her
pfp by @cuppa
pngtuber by @qwarq


stream discord
discord.gg/xE4xkHwtMM

BappyDeerHooves
@BappyDeerHooves

i should write about the fun experience of making the first mod loader for a game sometime


BappyDeerHooves
@BappyDeerHooves

i played this cool puzzle game called

Opus Magnum

where you make machines to do alchemy for you, by placing down the inputs, outputs, arms, and various glyphs for transmutation and bonding elements.

and then i made a mod loader and i think it's pretty cool. also holy shit i've been doing this for 3 years? that's gotta be my longest running project ever wow. but here's how i made it, hopefully easy enough to read without being a Real Programmer, but still interesting if you do want to see all the details.


Opus Magnum

when you finish the puzzle, you get a nice looping GIF,

A GIF showing a solution for the puzzle "Rocket Propellant", where 4 arms grab and move marble-like atoms to build an output.

and your solution is rated on three different metrics: cost, cycles, and area or instructions:

  • every component you place has an associated cost, measured in Guilder (and abbreviated as G); there's no cost limit of any kind, but a solution with more expensive components (and more of them) will have a higher cost score.
  • your solutions are expected to loop forever, and the game makes that pretty easy to achieve, but it only has to produce 6 of every output to count as a win1. the number of steps (or Cycles) it takes to produce that many outputs is the cycles score, with faster solutions getting lower cycles.
  • the space taken up by every component you place, plus the area that's ever covered by an arm or atom, is tracked as the solution is run. the total number of hexes covered like this is the area score. note that this is also counted at win-time; it's very easy to make solutions with ever-increasing area.
  • some epilogue and bonus puzzles force you to work within limited area. in these, the area score isn't tracked, and gets replaced by instructions, the total number of instructions that every arm has2.

there's a lot of depth to optimizing these! and the community has gone very far, figuring out the theory behind optimal solutions and filling out extensive leaderboards of best solutions for various combinations of metrics, including entirely made up ones; plus the pareto frontier of solutions that aren't the best in any single metric, but can't be improved without worsening another. some new metrics (like rate or width) even complicate the process of measuring other ones, introducing concepts like "area at infinity".

here's a solution of the same puzzle optimized for CGA (cycles, tie-broken by guilder/cost, tie-broken by area), with a full score of 225g/​33c/​37a/​58i/​8h/​6.5w/​L@V 225g/​5r/​37a/​58i/​8h/​6.5w@∞, still fairly tame:

A GIF showing a solution to "Rocket Propellant"


Altering the Game

and that's fun but not what i'm here for. i get a shiny game --> i mod the shiny game

here's a shiny (but very unoptimized3) GIF with uranium and a fancy stabilizer glyph i made:

A GIF showing a solution to "Reactor Reagents". This one has two inputs, one of which consists of a custom atom type, covered by a glowing effect.

and other people have done some even more impressive things, like crossovers with other Zachtronics games (both by isaac wass):

A GIF showing a solution to the puzzle "Curious Lipstick", using components from & themed after the game Molek Syntez.

A GIF showing a solution to the puzzle "Alcohol Separation", using components from & themed after the game Infinifactory.

how does this all work? well, you install the mod loader Quintessential, then you download a zip file of the mod and put it in the Mods folder conveniently created next to the game's executable. now, when you run the modded version of the game, new stuff magically shows up, as if it was vanilla! a scroll menu on the puzzle editor, a mods list button on the pause menu, options to switch between campaigns (and soon journals, containing bonus puzzles)... and the little marker on the GIF that helps you know it's custom.

how does this all work if you want to make a mod? well, you start with a folder in Mods, make a quintessential.yaml file that holds its name (and other info), throw your assets in the Content subfolder, and maybe even make a C# project to make further additions or changes, which quintessential will handle loading.

how does this work before you have a mod loader? well, you flounder around a lot before making one.


Poking Around

the first step is to figure out what you're even working with. initially, after a bit of searching, i used a tool called Detect It Easy (DIE) to figure out that the game executable wasn't even machine code, just a pile of CIL with a thin wrapper, though i hope i could figure it out myself nowadays.

not all programming languages get turned into machine code, even among the ones that get compiled to a binary format ahead-of-time. one such language is C#, which gets turned into CIL (for Common Intermediate Language) instead, which is a made-up instruction set for an imaginary computer. when you run a C# program, a "virtual machine" handles executing this CIL, often translating it into optimized machine code on-demand. (java does this too; jar files store java bytecode, it's binary format.)

it might seem silly, but it has a number of advantages, including being far easier to work with for modding. to the point that i could simply throw premade libraries at any game written in C# or java to get half the work done for me.

which i eventually did, but at this stage, i didn't really know what i was doing. also, it was obfuscated, so it wouldn't have worked yet anyways. just like That Block Game, the final executable you get on your computer has its contents made hard to read: names scrambled, strings of text mangled in a finnicky & fragile binary format, code messy and difficult to follow (or maybe that's just game dev).

decompiling the game into a recreation of it's source code works, but you couldn't read it given everything looks like #=qQ3boY4a6o2O2sPtKvJtj_Q6y77XoLuLRv$4EsOcRQr4=4 & 5, with almost absolutely nothing to go off, no text or anything i could recognize.

well, except a couple things. none of the actual code had names, but some of the classes - which you can think of as containers for code - did. for example, AtomType and Molecule and HexIndex gives you a good reference for code that deals with atoms. the most important one was GameLogic6, a sprawling class that, importantly, includes the actual startup logic of the game - the kind of place that's most important for a mod loader to edit, and a nice point of reference.

i looked to other mod loaders for inspiration, and in particular, Minecraft Forge (from That Block Game), which i had worked with for a long while by then. it's installation is roughly a four step process:

  • make a copy of the game with all names replaced with good, readable ones,
  • decompile the game into a rough recreation of it's code,
  • apply a number of hand-written patches to make it valid/functional, and insert it's own logic and hooks,
  • compile that code into a new version of the game.

Adventures in Recompiling

and hey, The 2D Block Game mod loader also does that, how bad can it be? i quickly put together a tool for doing just that, and slowly charted out some of the code,

  • giving everything some basic generated names, like class_13 or method_1917,
  • updating it to work with a newer version of Steam code,
  • fixing various other nitpicks and decompilation artefacts until it eventually compiles,
  • it won't run,
  • why won't it run,

i tried to figure out how the game stores text for a long time, but it's obfuscated, and the code it uses to un-mangle it is sensitive to a lot of factors (e.g. the executable's metadata) and is very difficult to read or understand. i couldn't get it to work when recompiling, so i tried an alternative idea, patching.

when patching the game, you make more precise adjustments to it; instead of destroying the executable and recreating it from pieces, you make smaller targeted additions or changes, leaving it mostly intact. this is what Minecraft Fabric (and every other That Block Game mod loader), Celeste's Everest, and also Terraria's tModLoader does. that last one is a bit weird but it has good reasons.

so this patched version is roughly, Opus Magnum But It Writes Every Piece Of Text That Exists To A File And Dies, which is similar enough to Opus Magnum that hey it works! all of the text has a number associated with it (that's how the game picks out the right one), so then my cool shiny tool can go through and replace all the "get text #1000" with actual text!

then it ran! or at least, started up, got to "restart if this wasn't run by steam", died and ran the vanilla version. there was a sort of "master switch" for steam support laying around, but it only did part of the work - likely a holdover from early development, sitting in the same class as a bunch of other debugging switches that can never be enabled under normal conditions.7

the advantage of recompiling like this is that i get to pretend i'm the game developer and i can just modify all the code at will. want something commented out, or made conditional? want to use some fancy language features? i can just do that like normal. so getting a running build of the game wasn't actually that bad!

so that ends up being pretty much Opus Magnum But Without Steam Integration And Maybe Slightly Faster8. woooo


Actually Loading Mods

it feels like it should be pretty well-defined, but, what does it actually mean to "load a mod". what is "a mod"?

now, i'm firmly in the camp of "modding should let me make completely arbitrary changes to the game". loading some JSON files and sprites to add content isn't worth it, dinky lua interop isn't worth it, because then you're always limited by what someone before you has decided you should be able to do, and that's exactly what i try to break with modding.

so, really, i'd like

  • to let the mod author write C# code that changes the game's code, letting them add or change anything,
  • then compile that to a file, and put it in a zip file with some metadata,
  • then a user can just put that in some designated Mods folder,
  • on startup, the mod loader should pull that code into memory and run it.

the only hard part is step 1. over in java land, a lovely library called Mixins allows you to modify other code with code, in a fairly easy way, that also avoids mods exploding on contact with eachother's changes. this is Good and i would like that, or an equivalent; by this point, i had also gotten quite experienced with Celeste modding, which uses the somewhat-equivalent MonoMod library for the same purpose.

then i thought about it for a bit, looked at Everest's code a bunch, and noticed that MonoMod had dedicated tools for patching ahead of time too. and like wait i should just use this for everything instead of the weird janky setup i had before.

so the answer to "how do i load mods" ends up being "just copy code from Everest", which works because the two games (Celeste and Opus Magnum) share a lot of important charactaristics, and the core logic of loading mods is fairly reusable in any case. (that's where quintessential.yamls and QuintessentialMods and ModMetas come from, for the curious.)


Where We Are

so that all works out! i made a proper installing tool that handles all the patching and extracting and renaming and-

the mod loader itself is named after quintessence, the only atom added to the game post-release, and probably my favourite. it's pretty! the marker on the GIFs earlier is it's symbol, which was also partially chosen because it took a long time to get "including new assets with the mod loader" working.9

after you give mods arbitrary power to do arbitrary things, you sort of get to sit back and add things to the loader at your own pace. it's main task is to manage mods, so a UI for looking at mods is useful, and then a configuration system for them. mod interop is the next thing - combining mods shouldn't explode - and the game was not designed for flexibility, so a lot of work was needed there.

another version of me might have chosen to leave it at just that, loading mods and nothing more, and include anything else inside some "standard API" mod"; which is fairly common, and has a number of advantages, but also has some downsides, and switching to something like that would be a pain now.10

skinmods are a no-brainer - textures in mods should override the vanilla game (and the mod's dependencies), which is also applied to "actual" content mods to load all of their assets. adding a placeholder texture instead of crashing outright was also kind of necessary.

stuff like "custom campaigns and journals" are a bit of a luxury, perhaps, but in a way, it's just another mod format, one consisting only of puzzles and dialogue and YAMLs, much more palatable for the average player.

installation is still technical and finnicky, and things are held together with glue and a prayer. i have started work on a proper GUI tool to make things nicer, but that still needs a lot of work.


Curse of Discord

The Modding Scene is more-or-less just me and whatever 0-3 other people happen to be motivated at any given time. if i'm currently distracted, or busy, or upset, then nobody will get anything done. making a wiki or keeping up documentation or archiving conversations is a lot or work, hopefully unsurprisingly, so a lot of discussion and context just ends up lost inside the random thread on a random channel in a semi-random discord. take whatever energy you might have used to complain and help document things instead.1


Afterword

yeah there's that. i put a lot of time and effort into it & will continue to do so for a long time, hopefully. still a lot of things to do, but it looks like my brain will let me keep up one (1) long-term project, and i'm happy with that. grateful i managed to get as far as i did, even.

sidenote, i did get to speak to zach once about it, and they were not particularly pleased by it, just a "well, i can't really stop you, but i won't make it any easier either", which was particularly annoying when the game's most recent (and almost certainly final) update broke a number of things - at least they left the older version on the steam "betas".

but i wouldn't worry much, given zachtronics kind of doesn't exist anymore, but also definitely still does? idk


Footnotes

1: some puzzles increase the requirement to a higher multiple of 6; usually limited space ones, that want you to properly use the Glyph of Disposal to destroy unnecessary/waste atoms instead of piling it up in a corner, or custom puzzles that try to make optimization more difficult. also, some puzzles have a fancy polymer output instead, but this is roughly the same as making 6 of the output but stuck together.

2: with infinite space, you can solve any puzzle in 3 or 4 instructions (except for possibly certain custom puzzles?), by programming one arm with "move along the track you're on and pick up anything you pass over", and encoding the solution with a complex arrangement of tracks. it's generally not considered an interesting primary metric, but it can be improved with extra restrictions (e.g. "trackless instructions" produces fairly elegant solutions) or used as a tie-breaker.

A GIF showing a solution to "Rocket Propellant", where one hex-arm moves on a looping path, taking atoms across various other components to solve the puzzle.

3: i have changes planned for these mechanics, so optimized solutions won't live (hopefully) very long, though it does help to playtest. plus, it's a lot harder to figure out what's optimal when working outside of what's established. and these definitely aren't just excuses

4: it's not just base64, or if it is, there's something else on top of that (though it would be convenient!).

5: this sort of name is not valid in C# code, but it is allowed in CIL, which is designed for other languages to also take advantage of (like F# and anything with .NET plastered on it) which might have wildly different naming conventions.

6: zachtronics have a mini game engine that use across multiple games; a lot of the infrastructure i've made can be copied almost verbatim for their last game, Last Call BBS. in both, GameLogic does a lot of work to e.g. tie together screens/scenes and handle ongoing state. (they have released a "minimalist game framework" that i might mess with someday, but it seems different enough to not count.)

7: these include "display the phrase I Can Eat Glass in every font", "export previews of every puzzle to a folder on your desktop with incorrect lighting", and various ways of crashing the game. all of these are static final on the C# side - set when the game is compiled and not changeable through normal (or even some weird) means, though i can still alter it ahead-of-time.

8: i guess you could make a cracked version of the game if you really wanted to, but it's literally on GOG and you need to buy it either way, so that's on you.

9: having a logo, or branding, or any coherent sort of Look is a bit expensive sorry.

10: some things are easier in a mod (e.g. direct CIL modification), some things are harder or semi-impossible (e.g. adding fields to structs), often for annoying technical detail reasons. more generally, having everything of substance in a mod means you get to reuse all mod tooling for it too, while putting it in the mod loader means more special casing (and some brittleness, e.g. when mods try to modify mod loader code thinking it to be equivalent to base game code).

11: see https://discord.gg/TvgDkJ26.


You must log in to comment.

in reply to @BappyDeerHooves's post:

at that point it just turns into ordinary Software Maintainance + community drama, and that's ultra boring. like you already have all the tools setup? and the knowledge and freedom to actually design things, instead of running around in the dark figuring out how much stuff you can change before the string decoder spontaniously combusts? you have to ask modders "hey can you stop using the old thing and try using the new thing" and ask players to stop being annoying about multiple modloaders- not fun

in reply to @BappyDeerHooves's post:

I hope that one day MadMaster's Handicap element will be implemented in its full glory.

(I also hope that one day there's a modloader for Copy Kitty, cuz I got a heckin sweet mod idea)

it doesn't print it, it covers the screen in it

another interesting thing is a hidden way to generate a website with the game's story/dialogue, but it seems to have been broken (either over time or by my changes).

also some interesting scrapped content, like combined ("docked") parts