Skip to main content

Advice to SoR2 modders

· 9 min read
Abbysssal

(there was a lovecraftian-type story here that I wrote, but I was left unsatisfied with it, so I removed it; if you want, you can still find it and read it through GitHub's repo commit history)

Several weeks prior to the demo, I thought it'd be fun to play a bingo game, centered around SoR2's code. I came up with a bunch of spaces, about both the good and the bad code. I announced that in SoR's Discord, and started slowly revealing the spaces on the cards. At first, one space a day, and then two spaces per day, as the demo's release date approached.

note

Update (30/12/24): Ignore the "Directions are enums" space being checked on the left. At the time, I was just so happy to see an enum in the code, that I didn't even look at how that enum was used. I was not aware it was possible to misuse enums that badly...

Bingo space explanations

When I was in the process of revealing bingo card spaces, I had plenty of time, so I provided detailed explanations for some of them.

Good spaces are green (left card), Bad spaces are red (right card).

  • X and Y instead of Vector2. The entire purpose of structures is to group similar and relevant data together. This way the processor can address the data much faster. Another thing that structures can do - is align the fields' padding with the processor's architecture (that's what the JIT compiler does) for more efficient access. Structures can also be copied much quicker than individual components, - that's just the value type semantics. And, of course, Vector2 is much more readable and easier to understand than just a pair of separate components.

  • Argument validation. Validating input data and throwing exceptions. Invalid state is the main cause of errors that are caused by other errors. An exception throw in a correct place (or even just a conditional statement) would stop the program, before it does anything that corrupts state. Sure, I understand that it'd be weird for a game to crash after just a single exception, but that's what exception handling is for (try-catch block).

  • Loop-switch sequence. This one has a Wikipedia article, so I'll suggest you read it instead. Examples in SoR1: Agent.LoadDialogue(), AgentHitbox.SetupBodyStrings(), InvSlot.SortItems(), InvSlot.SortUseItems(), LevelEditor.RefreshCustomCharacters() (x2), LevelEditor.SaveChunkData(), LevelEditor.OpenLoadExtra(), MouseCursorSets.SetupCursors() (x2), PoolsScene.SetupWalls() (x2), PoolsScene.DoInstantiate().

  • SPANS?!?!?. Now this is quite a reach, I know. The purpose of Span<T> is to reduce the amount of unnecessary array/string allocations in synchronous operations. The performance improvement would be noticeable, and memory usage would go down by a lot, but that's only if all the libraries involved can handle this sort of data, and Unity doesn't know how to handle spans (or even Memory<T>). This is more of a "new .NET" thing, rather than a general code improvement. If one were to be looking for a .NET library for something, they'd definitely give more preference to ones that use spans. SoR doesn't have much of a "back-end" that could use this sort of improvement, so I wouldn't expect it.

  • Improper list population w/ excessive copying. There is a way to populate a list very inefficiently, that involves Insert. And that's pretty much all there is to it. It's very inefficient, forcing the list to recopy the entire array just one item over, every time an item is added. Also, another few small, related things: Add in a loop is much less efficient than AddRange, - lists are good at copying collections. And lastly, lists shouldn't be used as queues, queues should be used as queues.

  • Version control (free!). A free space! We already know that Matt is using version control (Unity's kind, but version control nonetheless). Version control is an important element in development of any project, even if you're the only developer. It helps keep track of the changes you made since the last release, allows you to easily backtrack and look back at old code, makes you think in terms of features and components instead of just doing whatever works and going along with it, and makes collaboration with others easy.

  • Enormous if-chain. A lot of ifs chained one after another (more than, like, 10). It never makes sense to have that many chained ifs, - you either don't know what a switch statement is, or you messed up really badly with your program's structure.

  • Directions are enums. In SoR1 directions are strings, - the most inefficient method anyone could ever think of. A beginner programmer's instinct should be to use small integers to represent directions, not strings! The overhead here is enormous even on the latest versions of .NET: comparison is 3x slower, memory usage is 2x greater (Note: interned strings in the SOH are negligible, but still...).

  • isAgent, isItem, isFire, isBullet, isObjectReal fields. Instead of type-checking an object (is expression), Matt first uses one of these 11 fields, defined on PlayfieldObject, and only then casts an object to the needed type. It's completely unnecessary, as this doesn't eliminate the type-check, - the runtime still needs to be sure the object is of the correct type, and throws an exception if it isn't. These fields add an overhead of 11 bytes to each object, and don't do anything besides these unnecessarily complex type casts. It's just a bad, uninformed design decision, nothing too serious, since modders can still use type-checks in a correct way.

Festive, huh?

And that's all the explanations. I only had the time to explain 9 random spaces, before the demo dropped, and now I'm not nearly as motivated to do the rest, since I've got nothing to hype up.

The evil code prevails

Needless to say, none of the good spaces, listed or otherwise, ended up being checked in the bingo. Aside from the 3 FREE good spaces, that is, but even they're under scrutiny (especially OOP).

I couldn't confirm a few bad spaces on my bingo card, since those ones required a deeper and more thorough investigation. It'd be easier to list the ones that were confirmed as NOT checked:

  • Unrelated code. This time only SoR2's stuff was added to the game. No irrelevant snippets or fragments of code blindly copied from elsewhere for unknown reasons.

  • Data clumps. I couldn't find any methods that this term would apply to, since it specifically refers to variables and method parameters. Some parts of the code certainly do feel full of data-clumps, but they're all already grouped into classes.

The rest of the bad spaces either were checked or are unconfirmed.

State of the game

I really did want to mod SoR2, but the current state of the code doesn't allow it. The excuse "It's just a demo/playtest" doesn't work here. I didn't test how optimized or buggy the game is, I only looked at the code itself. And it's a mess. It's a mess that I don't want to deal with.

Regarding modding tooling that Matt promises: that would be completely different from the modding we did with SoR1. Waiting until Matt adds support for something is very different from implementing the thing yourself. He's an only developer, and his hands are already full with just the base game.

And finally, for me personally, the most interesting thing about modding is creating something, that's vastly different from the original. Something weird, extrinsic, grand, silly, weird. For something of that degree of peculiarity, you'd at least have to make use of C#'s more advanced constructs. And Matt outright refuses to use any, other than classes, methods and fields.

If you want to mod SoR2, then... good luck.

A few last tidbits

Apparently, in SoR2, gender is a bool variable (see bool Agent.genderF).

The only thing separating animals from humans is an isAnimal flag.

Boats are just water cars in SoR2's world. (all vehicles are instances of the Car class)

The code initializing outfit colors just couldn't be more wet or hardcoded.

Direction enums in SoR2 are awful and completely non-mathable. Take a look at this:

public enum DirectionType { None, N, S, E, W, NE, SE, NW, SW, Wait }

North (0°), then South (180°), then East (90°), and then West (270°)... WTF.
It's all out of order AND misaligned as well, due to None taking up the 0.

Speaking of directions... The vehicles... They're.. uh... Their directions are both strings and numbers... There are no switches. Just ifs... Wet. It's so wet... It's painful to describe. It's arguably one of the most disgusting pieces of code in the game.

No signs of generics, interfaces, exception handling, structures, properties, events, reflection, attributes, try-finally, asynchronicity, object disposal, or any other above-basic-level constructs.

👋

Once again, good luck trying to mod this. I'll be taking my leave here.