So one thing led to another and then today turned out to be a giant rabbit hole to figure out how to actually get a game changing mod working in Humankind.
(Note this post might get semi-deep into how Unity & C#/.NET work. I'll try to keep things clear without getting too wordy. If something doesn't make sense, please feel free to ask for clarification.)
Some context first!
When trying to decide where to build an outpost, I'm really annoyed by having to hover my mouse over every hex in the area to see which one has the resources I want (sometimes I don't want the suggested location that the game hints at). I thought it would be neat to have a terrain overlay that gave an overview of what hexes were better. I was thinking of something like the Appeal Lens in Civ VI.
Anyways, this led me to start digging into what it would take to add some sort of mod to support this. Since modding support hasn't been officially released yet, we need to start poking around to figure out what works so far. It turns out there's a fair bit of code to handle loading mods and it also looks like the base game itself is using that code for some content/logic.
Step One: Get Humankind to load something.. anything..
From the game's Main Menu there's a "Extra" -> "Community" item. Which takes you to a page where you can manage mods. The first goal was to get something to show up here. Since Humankind is made with Unity, we can probably look through the code they are using to load mods into that page. This is because Unity takes advantage of the .NET framework (more specifically Mono, but for what we're doing it doesn't matter). All of the logic for Unity games is compiled into Assemblies (.dll files) that are shipped with the game that can be easily decompiled back into C# and inspected. We can load up our favorite Assembly decompiler (dotPeek, for instance) and start digging through all the .dlls that are shipped with the game (found in "<install dir>\Humankind\Humankind_Data\Managed".
After a fair bit of searching around (searching for terms like "Plugin", "Mod", or "Assembly") we can find that there's this class called RuntimeModule that gets used for loading mods. This looks to be the important class for defining all the metadata for a mod. It has fields for Author, Description, Website, Loading Screen, etc... so a promising place to start.
RuntimeModule is used in a class called RuntimeManager, specifically in a method called "DoLoadRuntimeModuleDatabase". Reading through that function it looks like the game has three places it could load a RuntimeModule from. In all cases it needs to exist inside a file with the ".assetbundle" extension.
First it looks for assetbundles in an AssetBundles directory next to the installed Humankind.exe. Ideally we don't use this way of loading a module. Putting files into the installation directory of the game isn't a very clean way of doing things. Steam (or whatever you use to install the game) should be the authority of what's there.
Next it looks like it tries to load asset bundles in the same way from two other locations. The second "Community" location is quite interesting.
If we go into the EnumerateRuntimeModules method, we see that the Community folder is someplace quite nice for us to play with.
GameDirectory points to the Humankind directory in your User's Documents directory (on Windows). So this entire thing would resolve to something like "C:\Users\<User>\Documents\Humankind\Community\". This is a great candidate for putting an assetbundle file that Humankind can try to load. But first we need to create this assetbundle file. Unity uses assetbundles as a way to put game content/assets into separate files for loading. What we need to do is create an asset bundle that contains this RuntimeModule, so Humankind can load it.
Let's start by creating an empty new Unity Project. (I just used the latest version of Unity, as I don't know what version of Unity Amplitude is using, it seemed to work fine). In order to create a RuntimeModule asset, we'll need the definition for it which is inside the Amplitude.Framework Assembly. In dotPeek we can see that this assembly requires a few of the other ones to be brought in, Amplitude, Amplitude.Core, and Amplitude.IO (and Amplitude.IO requires LZ4). So lets copy all of those assemblies directly from the game installation to the Unity project.
Great! Now we have access to a bunch of API to the game. RuntimeModule is a ScriptableObject type, this means that it can exist as an asset. Amplitude probably has editor scripts to create/edit/manipulate these assets, but editor scripts aren't shipped with the game, so we'll need to make our own. We can make an Editor folder and put a new cs file in there with the following:
This will put a new menu item in Unity's main menu bar to create a RuntimeModule. Click it!
Now we have a RuntimeModule named AwesomeMod in our project! We can add some data to it now too! Important: Change the Runtime Module Type to "Extension". Standalone will crash the game, since the game is currently assuming there's only one Standalone module, which it is already using and loading.
Next we need to get this into an assetbundle file. First we need to make this RuntimeModule to be in the awesomemod assetbundle. This is done at the bottom of the inspector.
Next we need to build the actual assetbundle. To do this, we'll need to add another menu item. And then click it!
That should have put some files into an AssetBundles directory in the Unity project folder. We can copy the awesomemod.assetbundle and awesomemod.assetbundle.manifest to that Community directory from before. Then let's start Humankind!
Hooray!!! Now we're getting somewhere. We can even select it, click Apply and load it. Though it won't do much, because we haven't done anything with it yet.
Step Two: Load a custom Assembly!
There are a few ways for RuntimeModules to modify the game. The RuntimeModule class has a list of RuntimePlugins that it can have. There are already a few RuntimePlugin implementations that look interesting. AssemblyPlugin looks to be able to load an assembly that sits next to the assetbundle. This would be very useful for adding logic to the mod, which I think will be necessary for what I'm wanting to do. DatabasePlugin looks like a way to load more assets into the game. I'm not entirely sure how this will be expected to be used in the future, but there's definitely some mod loading logic that expects it (we'll get back to this later). And then LocalizationPlugin definitely looks like a way to load more languages into the game. In our case, we'll definitely need an AssemblyPlugin to run our own code in the game, so let's start there.
AssemblyPlugin is a ScriptableObject like RuntimeModule. So we'll need another menu item to create it. Additionally, we'll need to add the Assembly file name that we expect to make in the inspector, as well as selecting the awesomemod AssetBundle.
We also need to assign this plugin to the RuntimeModule asset. Annoyingly, the RuntimePlugins list on that asset is hidden in the inspector, so normally we'd need to write an editor script to assign it and then save the asset. However, there's a little known trick in Unity. Open the Help -> About Window and then just type "internal". This puts Unity into a bit of a debug mode (used by developers at Unity). This gives access to a new inspector mode "Debug-Internal", which shows all object data, regardless of if it has a hidden attribute. Now we can add an element to the RuntimePlugins array and drag the AssemblyPlugin we created to that new element.
Now we need to create the actual assembly. Unity has a useful utility class called AssemblyBuilder we can use for this for now. We can create a directory to hold our cs files and then we can build an assembly from those files with AssemblyBuilder. For now, lets create a really simple script in a folder and then build it with AssemblyBuilder in yet another menu item.
After building the Assemblies, the .dll and the .pdb files in the Assemblies directory in the Unity Project can be moved to the Community directory from before. Additionally, we need to rebuild the Asset Bundles and copy them to the Community directory too. Then we can run Humankind again and try to load the Awesome Mod again! Unfortunately, now it fails to load (you won't immediately see this, but if you go back to the modding page again from the main menu, you'll see that Awesome Mod isn't loaded). If we take a look at the latest diagnostics logs that Humankind is dumping into the Documents\Humankind\TemporaryFiles\ directory, we see some errors. And we hit our first major snag in mod development for Humankind in its current state.
The mod loading logic is complaining that we're using a disallowed type in our assembly. In the simple assembly we used, we have a Debug.Log call. This is Unity api for logging to the player.log for the game. It seems that there's a pretty restrictive allow list of types we can actually use in our assembly. It's found near the RuntimeModule loading code in RuntimeManager we looked at before. What's allowed now doesn't give us much flexibility in manipulating the game state/logic. It looks like all we can really do is work with Amplitude's AI API and practically nothing from Unity at all. This means that there's not enough exposed to do what I set out to do in the beginning, but there's still more to discover today!
Fortunately, we are allowed access to the Amplitude.Diagnostics.Log api, so we'll use that instead of Debug.Log for now, just to see if we can get an assembly to do something. After rebuilding assemblies, copying the files to the Community directory, restarting Humankind and loading the assembly, everything seems to load now! This means that the code we've written is now sitting in memory ready to run. All that needs to happen now is something to call it to run it. Unfortunately, this is the next big barrier in modding support right now. There doesn't seem to be a way to call into the assembly with only a mod loaded from the Community directory. Looking at the implementation of AssemblyPlugin, it only loads the Assembly, but then does absolutely nothing with it after it is loaded.
Step Three: Getting our code to run!
(I won't go into super detail of each part for this step as it gets really messy. But I'll see if I can put together a unity project that people can poke at to see what we did.)
However, if we dig deeper into the code that exists for loading assemblies specifically for AI, there are some interesting scans for specific types that maybe we can leverage. The AI.Brain logic scans assemblies for types that implement various interfaces like IAnalysisPass. I'm assuming these are passes in resolving the AI that modders could implement to create their own AI behavior. (It would be really interesting to see a future where modders pit their AIs against each other!) So in theory, we could implement one of these interfaces to get an entry point into running our own logic. Unfortunately, it's a bit of a headache to get the AI.Brain to scan our specific assembly. The assemblies it scans are assemblies that are referenced by an AIModDefinition object. Which is a ScriptableObject like RuntimeModule and AssemblyPlugin before. However, if we try putting this into the assetbundle like before, it won't really work. AIModDefinition needs a whole host of other types created to be properly loaded. We'll need a DatabasePlugin, AssetBundleContentDescriptor (important that the asset is named ContentDescriptor for some reason), AssetBundleManifest, and AIConfigurationDefinitionCollection all next to the AIModDefinition. Doublely unfortunate, the AssetBundleManifest is currently hardcoded to only work in assetbundles that are in the AssetBundles directory in the game installation directory (next to the Humankind.exe). Triplely unfortunate, AssemblyPlugins don't load properly when defined in asset bundles in that AssetBundles directory in the installation directory, so we can't just use that directory instead, even if it is less clean to do. However, it's still doable!
First, we need to create a second assetbundle that contains the AIModDefinition along with all of it's dependent assets. We create the assets in the same way we've done for all the other ScriptableObjects (more menu items!). We also create another RuntimeModule for a new mod called something like "AwesomeModData". The DatabasePlugin needs to reference the AssetBundleManifest asset in the inspector. The AssetBundleManifest needs to have the new "awesomemoddata" name added to it's Asset Bundle Name property in the inspector. And the AIModDefintion asset needs to have the full assembly name ("AwesomeMod, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null") of our assembly added. We add all of those assets to the awesomemoddata assetbundle and then we build the assetbundles. This assetbundle (and manifest file) we add to a new awesomemoddata directory we created in the AssetBundles directory in the Humankind installation directory.
Secondly, we need to change the script to implement one of the interfaces that the AI.Brain is looking for. For instance, we can create a empty class that inherits MinorObjectives, and then do an Diagnostics.Log in its constructor. In order to reference AI.Brain.MinorObjectives, we need to pull in the Amplitude.Mercury.AI.Brain.dll assembly. Unfortunately, this requires a bunch of other assemblies, so there are going to be a bunch of assemblies we need to pull into our Unity project now. But after that's done, we can rebuild our AwesomeMod assembly and move it to the Community directory.
Thirdly, we'll need to launch Humankind with command line arguments to tell it to load the awesomemoddata.assetbundle from the installation AssetsBundles directory. The argument we need to pass is "-m awesomemoddata". After all of this we can launch Humankind and load our mods and start a game!
If we keep an eye on the diagnostics logs we'll see something amazing! Each of the empire's "brains" are saying "It's Alive!" when they start up! That's our code running in game!
Conclusion!
That's as far as we've gotten so far. I think the next is to investigate what these different AI.Brain interfaces do and if we can make use of them to manipulate AI behavior with what's available today.
Otherwise, I think we'll need to wait for more support to be exposed to do what I originally set out to do. I think a fair bit could be accomplished if we were allowed more types to work with (I assume its restricted so that people don't do malicious things with mods, which is totally reasonable), and some sort of entry point being added for non-AI mods. I'm looking forward to seeing what comes in the future.
I hope someone finds this interesting, and I hope that this shows the devs at Amplitude that we're eager to start digging into modding when they're ready!
One of my suspicion is that it's possible to load a mod the same way they did for their other game. I'm most interested in modifying the DB actually because most thing we will want to do as modders will come from the DB anyway I'm investigating on that side :)
CapnRat
in Disguise
CapnRat
in Disguise
2 400g2g ptsReport comment
Why do you report CapnRat?
Are you sure you want to block CapnRat ?
BlockCancelAre you sure you want to unblock CapnRat ?
UnblockCancelAztq
Newcomer
Aztq
Newcomer
1 000g2g ptsReport comment
Why do you report Aztq?
Are you sure you want to block Aztq ?
BlockCancelAre you sure you want to unblock Aztq ?
UnblockCancelTouhma
Harmonic
Touhma
Harmonic
24 700g2g ptsReport comment
Why do you report Touhma?
Are you sure you want to block Touhma ?
BlockCancelAre you sure you want to unblock Touhma ?
UnblockCancelEnchanteur
Senior
Enchanteur
Senior
24 700g2g ptsReport comment
Why do you report Enchanteur?
Are you sure you want to block Enchanteur ?
BlockCancelAre you sure you want to unblock Enchanteur ?
UnblockCancel