If I had one of those great American bucks for every time @DraB had asked me some mundane question about whether or not I'd be able to add a great Apache Attack Helicopter to Legacy, just like those fancy ones we have in our Lord and Savior of all games, Rust Experimental, I'd probably have enough bucks to buy the source code for Legacy, and more Apache Attack 'Choppers than the US Army.
You see, adding a new asset into an already-compiled and shipped game without access to the source or a Unity editor is about as hard as it sounds. In Legacy (along with almost all multiplayer games) there are two parts to the game: the client, and the server. If you want to spawn a helicopter, you have to spawn it on both the client and the server. One of which (the server) we have control over, and one of which (the client) we don't. Yet. In theory, all we'd need is to be able to run a few bits of code on the client, and all of a sudden we'd have more ladders and gun skins than CS:GO and your local hardware store combined. To make this good though, we're gonna want to have full control over the mods - we'll probably want them streamed from the server. Join me in my journey, and we'll see what we can do here...
So this is where our story begins. A murky grey afternoon in the United Kingdom, and another plea for a guns-blazing naked-Newman-shooing helicopter.
I think it was after speaking with the one and only KahunaElGrande from the Cracked Games Servers project that I realised the power of WebSocket-Sharp - an easy to use C# library for creating websockets allowing applications to communicate over the 'net. Kahuna actually wrote the built-in RCON server in Oxide, which was a great help in getting up and running with a basic WebSocket implementation (thanks Kahuna!). For the uninitiated, let me explain. Imagine you have a garden - this is our internet. On one side of the garden is our house, this is our gameserver (in this case, a Rust Legacy server). On the other side of our internet field is a bathtub - this is our game. Why is there a bathtub in a field? I don't know. But I'd like to think it's a metaphor for the randomness and sporadic-ness of our awesome players. Anyway - we're presented with a dilemma: master @DraB wants all the bathtubs in his field to each have mods - a set of files which alter the game, be it adding new weapons, building parts, or even, Apache Attack Helicopters. The question is, how do we get all these fancy mods from our house, to the 150 bathtubs scattered around the field? The answer is hosepipe. Or should I say - websockets. In essence, we host a websocket server (think of it like a tap) on our gameserver, and all our players (ya' bathtubs) hook up a bit of hosepipe so you can all get some of that gushing wet mod-dy goodness. How do we achieve this? Simple. In two bitesized chunks: a serverside mod manager (the tap and connected piping), and a clientside mod manager (the hosepipe and bathtub faucet).
To get anywhere, we're going to need a very basic implementation of both. Let's get started.
The Serverside: PsychoMod Plugin & PsychoMod Extension
Luckily for us we already have an amazing, open source, popular, secure, reliable, well maintained, well supported, modding platform on the serverside: OxideMod. As I'm sure you know, Oxide supports the ability to install plugins. These allow you to modify the game, add chat commands, remover tools, auto kits, timed events, in-game events like GunGame, etc. This is great, and 99% of the time a plugin alone will fit our needs. However, what you probably didn't know, is that there are some restrictions to plugins. For example, a plugin may not access System.IO: a library in C# that allows you to modify the filesystem (deleting, editing, moving, creating, and anything and everything to do with files). This is generally a good thing - nobody wants a rogue plugin deleting System32 off their server machine now do they? But in this case this poses a problem: we're going to need libraries like System.IO to make our modding system work. This is where Oxide's extensions come in. Extensions are dll's which are loaded automatically by the game at runtime, and allow you full access to do anything and everything on the server and system. This means we can really get our hands dirty with the filesystem, file compilation, file watching, and, most important of all - WebSockets.
There are two things we need in this extension to get up and running with a basic implementation on the serverside, access to System.IO (we'll call this File I/O), and the ability to start, stop, and talk to a WebSocket server. On the File I/O side we expose about 10 functions, varying from reading and creating files to checking if directories exist. The WebSocket side is a little more interesting: here we have the ability to start and stop the server, and broadcast messages to all clients. Once we have these basic functions added in we are able to hook this up to a plugin. The plugin is what's going to act as the overarching brain in all this, the plugin will be pulling all the ties between the extension, the gameserver server, the websocket server, the client, and the modding platform on the client, together.
Now in theory there is only one main job the plugin needs to do right now: when a player connects to the websocket, read all the mods in the "mods" folder off the disk, and send them to the client in a byte array. We actually wrap this byte array within a nice little class which specifies things like the location of the loader class, which also means we can load custom dll's (e.g. hacks like DizzyHack, but only for testing ofcourse). Obviously things will get a little more complicated than that when we have to worry about clients dropping connection, and worry about nicely shutting down the WebSocket to stop frivolous errors clientside, but I'll worry about that, and let you guys focus on the less boring stuff.
The Clientside: PsychoMod
You better get used to the word PsychoMod now, because by the time this thing goes live you're going to be hearing it more than I've heard DraB moan about wanting a grenade launcher added to the game.
The clientside is where things get a little more funky. You see, unlike on the serverside, we don't have a pretty little modding platform like Oxide, which means we have to write our own. Fun? Right? Actually, not bad.
You see one interesting trick about Unity is the Managed folder. The Managed folder basically contains a big group of libraries that contain all the code for the game. The main code is usually in Assembly-CSharp.dll, however there are also a few other bits and pieces hanging around in the flurry of other libraries that are to be found within this folder. But here's the trick: at runtime, all files within the Managed folder are loaded up into the main AppDomain of the application. Neat right? Well, one problem: our mod isn't self aware, it doesn't realise it's been loaded. We need a 3rd party to come in and tell it to load up and start doing things. How do we achieve this? Well, the same way Oxide does: by adding an initialization hook into the Assembly-CSharp file which tells our mod to load up and start working. How do we add that hook? Well, also the same way Oxide does: with the Oxide Patcher.
An Oxide What?
If you're familiar with writing plugins and using Oxide, you'll know that there are a great variety of hooks for you to use in your plugins. Hooks are functions that are called when a specific event happens in-game, perhaps a player spawning or a helicopter crashing to the ground and burning into a lump of molten polygonal 3D-rendered vertices. These hooks are actually added directly into the game's code (remember that Assembly-CSharp file I mentioned? Yeah, that), via the method of IL-editing. Basically, code that you see in your editor gets crumbled down by a compiler into a more computer-friendly set of instructions (add one! Times by 3! Subtract 6! Square roooooottt!). Unfortunately for us, IL editing is a bit of a pain. You see machines don't tend to communicate in plain-English like us humanoids do, and diving straight into the deep end of IL editing can quickly leave you in a mess of IL_006b and Sub(x -- 9)'s. This is where the Oxide Patcher comes in (praise Mughisi!). The Oxide Patcher allows you to quickly and easily add in new hooks to the file, and let's you place these hooks wherever you please, with whatever arguments you wish.
But Psycho! The Oxide Patcher is made for Oxide! Not PsychoMod!!!11! What do??
What do indeed. You see, Oxide Patcher happens to be open-source (praise Mughisi!), which means we can download our own copy and modify it to fit our needs. With some quick find and replace's from "Oxide" to "PsychoMod" and some shuffling of dll's, we are now able to patch in new hooks clientside. Sounds easy right? Trust me, it wasn't.
Now, after me telling you all this in such great detail, you'll probably be disappointed to hear that in theory we could cut all this once the clientside modding project gets mashed together with FShield. But alas, for now it's necessary, and it sure was a great trip down the old IL-patching lane to get us going on the clientside. Now, back to WebSockets.
So as you saw in that second snippet there, I was able to add an "OnPlayerConnect" hook to the ClientConnect:DoConnect method (the function responsible for connecting you to the server). This means PsychoMod knows when you're trying to connect to a server, and gives it a chance to connect to the WebSocket. The beauty of the Oxide Patcher is that it allows us to specify a return value - all we have to do is return a boolean and the function will return there and then. Therefore, by putting this hook at the top of the DoConnect method, I am able to stop you from connecting if PsychoMod wasn't able to connect to the WebSocket for whatever reason.
Now if you read the first section on the serverside of this, you'll know that the server is set up to instantly send back all the mods to a user when the connect on the websocket. This means all we have to do is hook up our websocket's output hole to a Json deserializer. Why a json deserializer? Well, since I didn't mention earlier, let me explain. When sending data over our websocket, WebSocket-Sharp gives us two options: a string, or a byte array. Now, a byte array is generally a little trickier to work with. I'd probably have to write my own wrapper around it which would turn all our data into a byte array, and then spit it back out correctly on the other end. Or, we can take advantage of being able to send strings. You see, Json.NET makes it really easy to convert pretty much any data type into a string. In one line we can convert an entire class into a string, and in another we can spit it back out perfectly, every time, no fuss:
The only drawback to this approach (really, the only drawback), is that Json.NET has about 6 and a half million different dependencies. No big deal, we'll just chuck them all in your Managed folder for you, but certainly gave me a headache when I was trying to demonstrate to Kahuna an early version of the system, and it kept crashing as he was missing a few of those aforementioned dependencies.
Awesome. So we've read our files on the server, sent them across the web, and now have them in a big pile of "ModInfo" on the client. Where next? Good question. An easy one, too.
You see, since we already have our dll's in byte format after reading it off the disk all the way over on the server, we can simply chuck all those bytes into Assembly.Load(byte) and hey presto! We have our assembly loaded on the main AppDomain! Now there's only one task left: call the function from within the assembly which spawns the main game object and allows the mod to work all it's magic.
Now, in my infinite wisdom I decided to add a loader class to each mod. This means all I have to do is track down the location of the Load and Unload functions, which luckily are all under the main namespace in a class called "Loader", and call them. Easy!
Now, if you were looking at the code with your own eyes you could expect to see way more error checking at each stage, but in essence, our loader is about 5 lines of code. We actually save all the parts into that "module" variable so we can access them all later. This includes invoking both our Load and Unload methods.
Whaddya know, whaddya say, looks like we've got some mods doesn't it?
Congratulations on making it this far - all two of you that cared to read this. I can tell you now, I've spent way too long writing this, but eh, I'm sure future me will look back on it some time and self indulge in how beautiful this whole system is (/s).
Nevertheless, I've also been putting together short 2-3 minute videos demonstrating the functionality of the project as we go. Here's the first, which demonstrates pretty much everything that I've explained here in a real world context:
If you have any questions about the project please feel free to leave them in the comments section below, or you can always come and find me on the LegionRust TeamSpeak server - I hang around there often.
Expect more of these updates as time goes on, and hopefully within a few months you'll be able to try out PsychoMod for yourself!