SRB2P and Modding

@nerdyminer.bsky.social

SRB2P and Modding

This is part 2 of a series on the game Sonic Robo Blast 2: Persona, if you didn't see the first part, you can check that out by typing my bluesky identifer on the home page of WhiteWind(@nerdyminer.bsky.social).

The experience of modding is highly dependent on what kind of game you are working with. Older games are a bit of a wild west, and require one to do the fundamental ground breaking and research needed to figure out whats what, if someone else hasn't done it before you. Newer games are often on engines like Godot, Unreal, and Unity, meaning theres a lot of skills that can be transferred from game to game. Where does that put SRB2P(and SRB2)?

SRB2 is based off the Doom Engine, more specifically a fork of the Doom Legacy engine. Doom was already quite extensible since you could load custom content through the WAD archive format, but SRB2 goes above and beyond and added a doozy of other features that WADs(and the zip-like PK3s!) can make use of, most importantly Lua Scripting.

If you ask yourself, "Just how much stuff can Lua mess with in SRB2?" the answer is SRB2P. Don't be fooled by the custom exe! That thing is pretty much only there for a few scant features like the custom font system and the main menu. ALL of the game logic runs inside of Lua scripts! Character stats are defined in Lua, skills are defined in Lua, dungeons are defined in Lua, the battle system is defined in Lua...I could go on and on, but the fact is that the Lua engine that SRB2 uses is doing much of the heavy lifting when SRB2P runs. It's quite literally a fantastic endeavour by the creator, Lat', and I have my utmost respect for them for working on this game since 2015/2016.

What one may find odd(besides a majority of SRB2P mods not being uploaded to the SRB2 Message Board), is that many mods outright override the base behavior of the game to expand it. Why is this so prevalent? What's unique about SRB2P and SRB2's Lua engine that allows this to work so well? That's what I aim to explore in this post.

I will also be using this blog post as an excuse for my future Lua job where I attempt to implement some of the admittedly unholy techniques I've learned in professional projects(unless if they just can't be bothered and want whatever it is to work).

PART 1 [In Which I Explain The Quirks Of SRB2P's Lua Implementation]

When SRB2 first got it's Lua implementation(idk when), the maintainers decided that base Lua was lacking some features, and decided to use a fork of Lua called BLUA. BLUA, otherwise known as Bloated Lua, is a bastardised clobbering of Lua that somehow still works. The notable features are as follows:

  • BLUA has bitwise operators (&, |, ^^, ~/!, <<, >>)
  • BLUA has != as a valid form of Not Equals
  • BLUA allows concatenation with +, and allows concatenation within strings with \$...\
  • BLUA supports // and /*...*/ as comment syntax

SRB2 modifies BLUA a bit more to keep in line with other portions of the engine:

  • The numerical data type is signed 32-bit integer.
  • The only way to access decimals is through the fixed point FRACUNIT system.
  • A number of functions have been stripped away or modified for security purposes(mostly stuff from the io library)

Besides those things, the Lua implementation is (hopefully)pretty similar to Lua 5.1. SRB2P's Lua engine is practically the same as SRB2's so there's nothing more we need to consider here.

PART 2 [In Which I Explain How Mods Interact With Lua]

So, its good to know what we are working with, but we still don't know WHY SRB2P is modified so easily by mods. If your thoughts turn to Lua's global variables, that is indeed the secret recipie! In fact, rawset(_G, ...) is probably the most important factor in why SRB2P is so extensible.

When SRB2(P) loads a mod with Lua files, the files are only ever executed ONCE. Im pretty sure if you put a while true loop willy nilly in the script, you'll probably freeze the game and have to restart it. The main purpose of a Lua file for SRB2(P) is to setup whatever values it needs to in the appropriate places.

Defining a character means creating an entry into a global table charStats, and creating an entry for their persona in the personaList global table. Skills go into attackDefs, weapons go into weaponsList...so on and so forth. There's a lot of stuff that gets put into all these tables, but what I want to discuss is not relevant to this, so for now, just consider what each table is used for rather than the specifics.

Of course, there's still an unanswered question...how does SRB2P know what Lua functions to run, and when? This is where hooks come onto the scene. Throughout the engine, SRB2 has a number of precise moments where it kicks the Lua interpreter into gear and says "Run whatever has registered itself to run here!"

The amount of hooks that SRB2 has is admittedly insane. You have hooks for running stuff before the logic runs, after the logic runs, during the logic running, when someone joins, leaves, dies, quits their game...you can find the entire list of hooks here.

So you have the SRB2 defined hooks that take your code directly to the engine, but what about SRB2P related functions? They actually have their own set of hooks as well! The syntax is slightly different, but creating a hook with SRB2P_addHook("hookname", function(...)) will allow you to create custom functionality that runs at a specific part of SRB2P's code. The SRB2 hooks end up being used mostly for defining things that either need to be running every tic like a custom menu implmentation or hud graphics, or for a system that needs to run at a very specific point in time in the engine.

Of course...there's a catch to all this, hooks are created under the notion of what people need right now. If there's a new place where people would like to run their code, you'd have to update the game to account for it. SRB2P is probably not going to get another update for a good while(at least until Episode 1 is released), so we're on our own hook wise...but there is an ace up our sleeve. I actually had a bit of a hand in making SRB2P quite extensible, so I can finally get to what makes SRB2P really insane modding wise.

PART 3 [In Which I Discuss The Different Levels of Modding Acessibility]

You can do a lot in SRB2P, quite a lot in fact. I'm actually still figuring out just how much you can do with the game. Since there's an official way to load mods, however, you give up on a number of features that make code related mods in other games extremely versatile. Case in point: you cannot create user defined mid-function code injections. It's just not possible in Lua, since the language is interpreted on the fly and you can't really make use of exe mods since this is a online multiplayer game and you'd risk massive desyncs.

So if you can't do the usual code injection, how do mods get by? It's time to introduce you to the Acessibility Iceberg...

The Peak [Globalized Functions]

I stated earlier that the rawset function was the most important part of how extensible and malleable SRB2P is. A majority of game functions are global, from the battle system, rendering functions, and other general things. If there's something you need that's not already doable through the hooks, you can simply redefine it. So long as the function still does the things its supposed to by the time it needs to return anything(if at all), you'll be good to go.

A beginner modder creating more advanced mods may just decide to completely redefine every single global function that they need for their mechanics. While its definitely possible to do this, and occasionally necessary, you increase the amount of maintenance you have to do on said code...

What if the game updates and that specific function is modified? What if a mod comes out that edits the same function, but becomes really popular and widely used? What if you need to edit that function again, but for a different reason in a separate mod? I felt this pain when SRB2P updated from 1.3.3 to 1.3.4, then 1.3.5, then 1.3.6...Thank goodness I was already familiar with Github/Gitlab by that point, since I used it heavily to figure out what things were patched and how on the public repository for the game.

Once you realize that you should minimize the amount of base game edits you have to do, you start coming up with some really unique tricks to get what you want.

Pre and Post Injections

Sometimes, you need to run something within a function, but you really only care about handling something BEFORE or AFTER the code runs. That's quite simple to do:

local globalFuncOLD = globalFunc

globalFunc = function(...)
    -- Do whatever you need to do before... 
    globalFuncOLD(...)
    -- or after the function runs!
    --[[ ...or you can just decide to make that function not run at all!
       (not usually recommended though but sometimes theres a reason for it) ]]--
end

The fact that Lua just lets you make a copy of the function so easily is the reason why this works so well. If whatever you need to do doesn't depend on precision point injection, then use this trick to minimize the maintenance you have to do for your mod.

Making New Hooks

Sometimes, I'll have to modify a function to change some interim behavior. It's unavoidable the more advanced and universally applicable the mechanic I want to make, but it can happen if I feel like it. How can I ensure that I would only need a single override for the forseeable future? What if I told you that you can expand the list of available hooks?

globalFunc = function(...)
	-- Some base code here
	SRB2P_runHook("HookName", args, ...)
	-- Rest of base code
end

It does take a bit more setup than what I show here, but as long as the purpose of the hook is flexible enough, you wouldn't need to worry about making any more changes in the future. Finally, its only at most a single line change(unless this is a hook that overrides behavior), so it's not too bad to manage whenever an update happens.

Just Above the Water Line [Local Functions Tied to Global Functions]

Before 1.3.4, a good amount of functions were local, but tied to a global function. The Battle System was like this, meaning you had to copy the entirety of the battle code for something as small as a single line change. That's annoying! That's annoying to maintain! However, that's unfortunately what you must do with these functions. It's still good that you have a way to edit them, but again, it relies on a global function being there to act as your gateway to them.

These mostly appear in fringe parts of the code, but they've been vastly reduced in numbers after 1.3.4, and aren't exactly things people would need to edit anyways.

The Depths [Local Functions Added To SRB2 Thinkers]

You can't edit these.

Once a Lua function is sent to an SRB2 thinker, you cant touch it anymore. The function has to access something global for it to even be REMOTELY modifiable, but the rest of the logic? You can't edit it.

This mainly applies to status effect renderers, and maybe the dungeon shadows? If there's a bug in one of these, good luck trying to get around it, you're better off just making another thinker and handling whatever you need to in that. The Hunger status effect was actually broken from 1.2 to 1.3.3(That's a good 2 years right there), but because it was in a thinker, another thinker had to be made to run the correct behavior of that thinker.

PART 3 [In Which I Discuss How Mods Interact With Each Other]

One would imagine that because its so easy for mods to interact and modify the game, the modding scene would be in crisis with all the conflicts that would inevitably pop up. That's certainly possible, but SRB2P's community has managed to sidestep that with a couple of techniques that you may find interesting:

Just Making One Person Manage Everything

This somehow happened, and I ended up being the person that has to play manager. I currently manage a triage of popular, game modifying mods, and they ALL conflict with each other on their own:

  • Teamsize(Made by Spectra): A mod that unhardcodes the party size so you can have more than or less than 4 party members during a run. Modifies netcode, menus, and the battle system.
  • Press Turns(Made by Helius Universe): A gameplay modification that replaces the "One-More" system from the Persona series with the "Press-Turn" system from the Shin Megami Tensei series. Modifies the battle system.
  • Echo Fighters(Made by Zoraxua): A mod that allows characters to be purely Lua based, bypassing the engine's character limit by avoiding skin definitions. Modifies netcode, menus, the battle system, and other stuff that shouldn't have to be edited but needs to be because of object creation.

I realized early on that the best way to manage these systems was to merge Echo Fighters with Teamsize, then create another version that merges that combination with Press Turns. It does mean that I have to manage four separate mods, but the combo deal means that people only need to play with ONE of the mods loaded at any time. I'm not planning to take on any more mods to manage though, so how can people coordinate to avoid conflicts?

#pragma once For Lua

Suppose you have a Lua script for some mechanic, and you know that multiple mods will want to include this file so they do not have a dependency. What can you do to ensure only one version of this file is loaded at any time? Well, remember how I said that SRB2 only runs Lua files once?...

if not myMechanic return end
rawset(_G, "myMechanic", true)
--[[ ... ]]--

Yep, its basically just #pragma once! It's quite funny how a usually non-standard preprocessor directive has a spritual relative that is the golden standard for handling this problem in this specific context. This also leaves you with a constant that you can check, sort of like a compiler flag, that allows you to modify things to account for this. Bionis Fields, a campaign mod that adds 6 new elements, makes use of this for the purpose of letting mods know that Bionis related features are available to them upon loading.

Of course, this does imply that load-order is an unaccounted for variable. What happens when you load your mod with this file, then a bigger file comes along and wipes out what this does with its own implementation?

Enforcing A Standard Load Order

At last, we've come to the theorhetically hardest to implement, but somehow-feasible-within-SRB2P's-community part of how interesting modding for this game is. Essentially, the community has agreed on a standard for loading mods that allows for the least amount of mod conflicts where the mods with the largest impact on the base game are loaded first:

[Load Order Starts at 1]
...
n+1: Mod with no edits to base game
n: Mod with no edits to base game
n-1: Mod with slightly lesser amount of edits
...
7: Mod with slightly lesser amount of edits
6: Mod with slightly lesser amount of edits
5: Mod with Large amount of edits
1 - 4: Base Game files

It's kindof like a converging series, each mod that comes after can take into account more of what is already edited when deciding how to implement its own changes. More often than not though, you can do so much with hooks and pre/post-injection that the amount of mods loaded that actually change the base game stay at a low amount.

Conclusion

Modding breeds ingenuity. Whether the limitations are artifically or community imposed, you develop an eye for critical thinking along with a slew of other skills that I think you could transfer to your career. As I begin to document the development process of my mods in future posts, I hope I can show just how much this medium forces you to think, be creative, and collaborate. See you next time!

nerdyminer.bsky.social
NerdyMiner

@nerdyminer.bsky.social

Programmer, Game Modder, and Reverse Engineering Hobbyist.

Post reaction in Bluesky

*To be shown as a reaction, include article link in the post or add link card

Reactions from everyone (0)