Back to list

Modding Architecture

The summer is almost over for me, so the crunch is on. Before university starts back up for me, I need to finish up big projects, or at least get them to a point where smaller iterative updates are sufficient. This is also the last opportunity I'll have until next summer to make any big changes to Exmods things. Once September arrives, everything from this website to Pixi will be updated on an as-needed basis so I can't procrastinate doing school work too much. So my last medium/big project for this summer was updating GCIPA.

You may recognise GCIPA from the (seemingly) random IPA executable that is used to patch Gamecraft to get mods working. GCIPA is the underlying technology that makes modding possible. GCIPA's mod architecture does two important things: patch Gamecraft to load mods, and load mods when Gamecraft starts up. The GCIPA patcher makes it so that Gamecraft loads GCIPA like a regular piece of game code while Gamecraft starts up. In turn, that activates the built-in mod loader and starts up all mods. While Gamecraft is running, GCIPA's mod loader also propogates certain engine messages to mods, to be handled.

Up until now, GCIPA was just the latest version of Eusth's IPA. IPA was created to mod a game called Illusion (or was the game company called Illusion?), hence the name Illusion Plugin Architecture. Despite that, the program is actually supports mods for any game built on the Unity game engine. The modding community for Beat Saber adapted IPA to their own needs and created BSIPA, which is where I originally heard about it. Unfortunately BSIPA had some functionality specific to Beat Saber, so I was stuck with using the regular old IPA for Gamecraft. The last official IPA release also wouldn't work with Gamecraft's newer version of the Unity engine, but luckily a few changes after the release fixed that issue. I initially started modding Gamecraft in a Windows virtual machine, so it was no big deal to compile and package IPA myself. Seeing a mod finally load with that version of IPA was an exciting day for me.

Little did I know that IPA was using some Windows-specific libraries that don't work with my current Linux-based development system. That was fine so long as I was happy with IPA functionality, but Unity and Gamecraft had so much more to offer that could only be accessed by changing IPA. A mod's root class, or "Plugin" as IPA calls them, acts a lot like a Unity MonoBehaviour, but with a bunch of methods and useful tools stripped away. If I wanted to have access to all of MonoBehaviour's message methods, I'd have to update GCIPA myself. That day finally came in late August 2020.

Before I actually started writing code for my GCIPA update, I wanted to start out with a confirmed working version of IPA compiled on my Linux computer. My Windows virtual machine was on standby just in case, but I was hoping I wouldn't have to boot up a completely separate operating system just to work on one thing. I immediately ran into issues with GCIPA, mainly because it was foreign to me. To get it to even compile on Linux, I had to remove the usage of Windows Forms (strange that IPA uses a GUI library but doesn't have a GUI) and update the whole project to use a newer version of .NET/C#. Removing that dependency was quick, since there was only a single simple use, which was easy to replace. Upgrading to .NET 4.7.2 wasn't much of a hassle either. All-in-all, this allows GCIPA to be developed on any modern platform, instead of being stupidly tied to legacy Windows functionality. Since that was an easy first hurdle, I hoped the rest was going to be that straight-forward as well.

Upgrading GCIPA with more functionality was the challenging part of this project. To be able to send more Unity engine messages to mods, I needed to first understand how IPA was handling the ones it already supported. That understanding required a deep dive into exactly how the GCIPA technology stack operates, from patching to mod handling. Here's what I found out:

Patching any Unity game is pretty trivial. GCIPA adds its library, which contains a MonoBehaviour implementation, to UnityEngine's and Assembly-Csharp's library references. Once the old DLLs for UnityEngine and Assembly-Csharp are replaced with the new version with the extra reference, the Unity engine will automatically load and handle the GCIPA MonoBehaviour like any other MonoBehaviour in the game code. The actual reference modification is done by Mono.Cecil.

Mod loading works by loading all C# libraries ("assemblies") in the Plugins folder, and then scanning them for GCIPA's IPlugin interface. Every IPlugin-compatible interface found is then intialised and hooked up to GCIPA's single MonoBehaviour for Unity engine messages. Despite being (relatively) slow, C#'s reflection functionality comes in really handy here. Without reflection, this simple mod loading system would not work. Since Unity's high-performance ahead-of-time compiled version using IL2CPP doesn't support a lot of C# reflection, Gamecraft mods will be completely broken if Gamecraft ever converts to IL2CPP.

GCIPA engine message functionality is literally just a fancy wrapping around Unity's MonoBehaviour built-in functionality. This was great to discover, since it means it's trivial to add new MonoBehaviour methods to add more engine messages to GCIPA. I also managed to remove that pesky warning at startup about a deprecated method on a MonoBehaviour. Every MonoBehaviour method in GCIPA's injected MonoBehaviour just iterates over every loaded IPlugin and calls the corresponding IPlugin method.

So, all that work updating GCIPA to compile on my Linux system wasn't for nothing! After a good few hours of work, I had managed to add support for almost every MonoBehaviour message method in Unity's documentation. A bit of testing followed, just to confirm my changes actually worked, but I had completed my goal so I was happy.

But there was just one more thing to consider: backwards compatibility. I knew I'd broken current mods in a big way, so I needed to create a way for modders to fix that easily. I'd also added a lot of extra methods that were required to be coded into every mod but were probably only going to be used by 1% of mods. Two problems with one common solution: abstraction. Object-oriented languages like Java and C# have this concept of interface and abstract classes. Interfaces are a blueprint for what functions an object has, but the implementation is not included. Abstract classes are like partially-complete blueprints: some functions are implemented, but some things are not. GCIPA actually had a second plugin interface called IEnhancedPlugin, which extended and enhanced existing IPlugin functionality. To make mod upgrades easy, I converted IEnhancedPlugin into an abstract class and implemented all IPlugin functions. This offered the best of both worlds: no useless methods lying around in mods and no big changes for anyone to workaround. Once I'd coded those changes, my GCIPA upgrade project was done.

And with that, the last thing to finish before school starts up was also done. Well, that's what I thought until I woke up this morning and opened Discord. Now I just need to wind down my enthusiasm with modding so I can focus on courses and my other priorities. The modding community is about as healthy as I'd expect it to be right now, considering the popularity of the game, so I'm not worried it will suffer without my full attention. With GCIPA underlying everything, and GCMM wrapping it all in a nice user-friendly bow, the current modding architecture is here to stay.