Skip to content

Creating library mods

Artyom Zuev edited this page Dec 20, 2025 · 15 revisions

This article is a work in progress! Please check back later!

Custom libraries are an very powerful tool that can facilitate nearly any modification to Phantom Brigade. The game is powered by Unity/C# and reads IL (interpreted language) at runtime. The IL code is modifiable at runtime, which means that it's possible to patch game logic and execute new code from mods. This tutorial will cover the full process of creating a library mod, from setting up the project to building and exporting your mod.

Dependencies

We strongly recommend reading through the following articles before you begin:

You'll need an IDE software capable of creating and compiling solutions that are .NET 4.7.2 Class Libraries. We strongly recommend using JetBrains Rider (even if you're familiar with other IDEs). It's free for non-commercial use and features a built-in decompiler letting you explore C#/IL code in the Unity and Phantom Brigade assemblies, which is essential when developing mods.

Repository setup

First, we'll need to create a new Git repository on GitHub. It will help you keep track of the changes and give you a backup if things go south. The repository can start private but will have to be made public once you publish your mod to comply with the Modding Guidelines.

Start by following a the Quickstart for repositories tutorial from GitHub. We recommend the following settings:

You should end up with a ready to use project that looks like this, with some files already in place (like README.md and .gitignore):

Local repository

Next, we'll need to clone the project to your machine using Git. Choose an easily accessible folder close to disk root. For this tutorial, I'm going to use D:/Work/UnityProjects/PB_LibraryTutorial.

If you're not familiar with Git and are unsure how to download a project from GitHub, try installing the GitHub Desktop client and following this tutorial. You should end up with folder contents that match what you saw in the browser:

Create a subfolder with the desired mod ID within the root folder. The repository is intended to be used with the External projects feature of the SDK. Mod config and other files will be located under a subfolder to allow using the root as a target for Custom project folders in the mod project manager config. For this tutorial, I'll use PB_Library as the mod ID and solution name. This setup will also allow you to reuse the same repository for several mods:

Creating a solution

Start Rider and select File > New Solution. Select the following settings:

  • Project type: Class Library
  • Solution name & Project name: Same as the mod subfolder
  • Solution directory: Mod subfolder (e.g. D:/Work/UnityProjects/PB_LibraryTutorial/PB_Library in our case)
  • Put solution and project in the same directory: On
  • Create Git repository: Off
  • Target framework: v4.7.2
  • Language: C#
  • Type: Class Library
image

Confirm the project. You should end up with file hierarchy like this (solution folder within the mod folder within the root folder):

image

Solution setup

At the start, your solution will look like this in Rider:

image

Rename (right click in Solution tab, Edit > Rename) the Class1 .cs file you start with into ModLinkCustom and paste the following code into it:

using UnityEngine;
using HarmonyLib;
using PhantomBrigade.Mods;

namespace ModExtensions
{
    public class ModLinkCustom : ModLink
    {
        public static ModLinkCustom ins;
        
        public override void OnLoadStart()
        {
            ins = this;
            Debug.Log ($"OnLoadStart");
        }

        public override void OnLoad (Harmony harmonyInstance)
        {
            base.OnLoad (harmonyInstance);
            Debug.Log ($"OnLoad | Mod: {modID} | Index: {modIndexPreload} | Path: {modPath}");
        }
    }
}

You will see a lot of errors, which is normal at this stage:

This class is important for the modding system, and it can additionally serve as a quick test bed for confirming we have set up the project correctly. For as long as this example displays errors, the project is not fully set up. What's missing is various dependencies: external .dll files needed to make sense of your code. Let's add them.

Open Dependencies > Assemblies in your Solution tab, right click the header, select Reference, then Add From... and navigate to the installation folder for Phantom Brigade. Assuming it is installed through Steam, the path would be something like D:/Steam/steamapps/common/Phantom Brigade. In there, navigate to phantombrigade_Data/Managed/ subfolder. Pick the following .dll files from that folder:

  • 0Harmony.dll (code injection library)
  • Assembly-CSharp.dll (main game assembly)
  • Assembly-Csharp-firstpass.dll (plugins)
  • Entitas.dll (entity component system)
  • UnityEngine.dll (main engine assembly)
  • UnityEngine.CoreModule.dll (supplemental engine assembly with debug classes)
  • UnityUtilities.dll (secondary assets & utilities)

If you added everything correctly, you should see a list like this under your Solution tab, and the ModLinkCustom class should now compile without any errors:

image

Now that the project has no compilation errors and all dependencies are in place, make sure to commit your changes to the repository. If you are a beginner with Git and aren't sure how to upload your changes, follow this tutorial from GitHub (assuming you're using GitHub Desktop as your Git client).

Side note: Having solution files not pushed to the Git repository can be the reason why Rider highlights your Solution tab in red, even if you do not have any remaining compilation errors. The red highlights should disappear as soon as you upload your solution files alongside ModLinkCustom.cs.

Simple patch

Next, let's create a simple patch class to try out code injection with Harmony. Start by right clicking the solution and select Add > Class/Interface:

image

Let's choose a name pattern that'll let us keep creating classes of the same type, for example PatchesTests. Future classes can follow the same pattern and get names like PatchesOverworldUI, PatchesDamageLogic, PatchesPilotEconomy etc.

image

If Rider offers to add a file to Git for you, we recommend to decline and tick the "Do not ask again" checkbox. You are likely to be using a standalone Git client so Rider probably shouldn't make Git changes on your behalf. Once inside the new class, try filling in the following snippet.

Deciding what to patch, whether to use a Prefix patch or Postfix patch and deciding on a specific implementation of each patch is very context specific. There is no tutorial that can cover all possible cases, but what we can do is provide a few working examples and set you up with tools making it easier to find answers for a particular case. Let's start with a simple idea: tapping into a method refreshing the "active mods" label in the top right corner of the main menu, and appending some text at the end:

We'll cover where to look in a bit more detail further down, but for now, let's assume we already know that the class responsible for this label is called CIViewPauseFooter. This is where decompilation capabilities built into Rider (or standalone decompilation tools like ILSpy, if you're using another IDE) come in handy. We can browse through the code, find the method that interests us and learn how to approach patching it. Let's start with a simple patch class pointing to CIViewPauseFooter and fill in the rest a bit later:

using HarmonyLib;

namespace PB_Library
{
    [HarmonyPatch] // Tells Harmony this class contains patches
    public class PatchesTest
    {
        // HarmonyPatch (Type, string) shows what class and what method within that class to modify
        [HarmonyPatch (typeof (CIViewPauseFooter), "PatchedMethod")]
        public static void PatchContent ()
        {
        }
    }
}

With that in place, let's select CIViewPauseFooter you typed into the HarmonyPatch attribute and press F12 (navigate to definition). You should see the decompiler progress bar pop up and should see the class code open shortly after:

image

The code responsible for displaying the list of mods probably has the word "mods" somewhere in it. Searching for it reveals the method responsible (RefreshModDisplay) and the UI object we need to access to modify the text (text labelMods).

image

This is perfect material for your first patch, with the method and the field that we need to access both public. Private methods and fields are possible to access, but this involves additional steps best left for later patches, once you're confident with the fundamentals. Knowing the targets, we can finish our patch class:

    [HarmonyPatch]
    public class PatchesTest
    {
        // HarmonyPatch (Type, string) shows what class and what method within that class to modify
        // HarmonyPostfix indicates that code must be added after existing code
        [HarmonyPatch (typeof (CIViewPauseFooter), "RefreshModDisplay")]
        [HarmonyPostfix]
        public static void RefreshModDisplay_Postfix (CIViewPauseFooter __instance)
        {
            // __instance is a magic argument initialized by Harmony
            // If this is a local method, it'll reference the instance running the patched method
            if (__instance == null || __instance.labelMods == null)
                return;
            
            // Grab label from CIViewPauseFooter instance and modify it
            var label = __instance.labelMods;
            label.text += "\n\nMy patch is working!";
        }
    }

Do not forget to commit all your changes to Git at every step!

Building

With all of the above done, we can move to building the library and testing the project. If everything is set up right, this step should be very simple. Click the v next to the Build button (hammer icon) and select Release configuration. Then click the Build button.

image

If the build succeeds, you should see something like this in the Build Output window, including the path to the generated .dll:

image

...

Setting up mod project

...


The article is a work in progress Old text below

mod_content_libraries_01.png

  • Metadata flag: includesLibraries
  • Folder: Libraries/
  • Refer to the Including libraries to learn about making this type of modification with the SDK.

This is a very powerful type of mod content enabled by C#/Unity's reliance on editable interpreted language (IL): ability to load libraries that can patch game logic and execute new code.

  • This feature is partially enabled by a popular library called Harmony - we've already been using it, but for internal needs, like patching Unity Editor UI
  • It is recommended that you read Harmony documentation if you're a programmer interested in creating a library mod.
  • The mod system tries to discover all assemblies in Libraries folder, like Mods/[ModName]/Libraries/MyLibrary.dll
  • It then fetches types from that assembly and tries to find a class inheriting from special type ModLink - if found, it's instantiated, filled with data, and has a special entry method invoked
  • ModLink provides an entry point to modders: you can declare an override to OnLoad method in it, executing arbitrary code; you get access to full mod metadata (so you know where you are, what mod you're in, what other mods are loaded) and access to the Harmony patcher object, allowing you to modify code in PB.

mods_content_libraries_02.png

General setup of a new library goes as follows:

  • Create a .NET Class Library (.dll) project in Visual Studio Community, Rider or other .NET IDE, targeting .NET Framework 4.7.2
  • Add the following references to it from your install folder (e.g. SteamGames/Phantom Brigade/PhantomBrigade_Data/Managed/):
    • 0Harmony.dll
    • Assembly-CSharp.dll
    • Assembly-CSharp-firstpass.dll
    • Entitas.dll
    • System.dll
    • UnityEngine.dll
  • Create a ModLink.cs file
  • In it, wrap everything in your own custom namespace, ideally matching the id field in your metadata.yaml , e.g. ModTest
  • Declare a class inheriting from type ModLink, e.g. public class ModTestLink : ModLink
    • In that class, declare an override method OnLoad with this signature: public override void OnLoad (Harmony harmonyInstance)
    • If needed, implement any custom logic originating from this starting point. For instance, you have access to metadata field that can tell you what folder you're working with, what mod is this library loaded in, etc; access ModManager.loadedMods to interact with other mods, access any game classes, perform custom patching procedures using Harmony object passed into the method etc.
  • Declare a plain class called Patches
    • Declare any number of subclasses annotated with Harmony patch attributes within that class, for example: [HarmonyPatch (typeof (PhantomBrigade.GameController), MethodType.Normal), HarmonyPatch ("Initialize")]
    • All of these patches will be applied automatically unless you declare OnLoad override and comment out base.OnLoad (harmonyInstance) call in it.
  • Build the .dll, place it in Libraries folder of your mod
  • You are free to get into the logic setting up scenario units and changing how they are generated. You are also free to override how post-deserialization code in some data container works, changing how weapon efficiency is explained to AI, you are free to prefix, postfix or entirely replace any method you want modified - sky's the limit. Most mods changing logic or implementing new systems in Unity games such as Rimworld, Oxygen not Included, KSP and others operate on a similar principle.

Clone this wiki locally