Skip to content

DLL Mod Quick Start

Sheep-y edited this page Jul 2, 2020 · 12 revisions

This short tutorial goes through how to create a DLL mod for Phoenix Point.

You are assumed to have mastered C#. If not, learn C# Programming first.

Table of Contents

How Mod Works

Most dll mods do either or both of the following:

  1. When called by mod loader, change some game data.
  2. When called by mod loader, change some game code.

Data is simple. [Find] and change it. If that is all, [PPDefModifier] is the first mod to facilitate simple data modding, while Modnix 3 introduces Action Mods for heavier data lifting.

For code, almost everyone use Harmony, which allows code to be changed on-the-fly multiple times back and forth. With it, multiple mods can modify the same method and works together. Harmony has two main types of patches: Prefix and Postfix.

Prefix runs before the patched method. It is mainly used to change input parameters and object states, but can also totally replace/bypass the method, preventing the original and other prefixes from being executed.

Postfix runs after the patched method. It is mainly used to change outputs, append logic, or clean up after Prefix. Postfix patches always run and cannot be disabled by Prefix patches.

Playing Detective

Data or Code, you need to know how the game works to change it.

Use dnSpy to read game code. Game code is in Assembly-CSharp.dll. Search "Assembly" in game folder and you will find it.

There is no one best place to start; it all depends on what you want to change. For example if you want to mess with air vehicles, start with GeoVehicle. Use the Search function in dnSpy to find types and members and manually check them.

Example:

  1. We want to change home screen version text.
  2. We "Search" "version" and found RuntimeBuildInfo.Version prpoerty.
    (Hide system assemblies to focus results on game code.)
  3. We "Analyse" it in dnSpy, found out it is used by UIModuleBuildRevision.SetRevisionNumber, which sets the version text.
  4. Our options:
    1. Override the return value of RuntimeBuildInfo.Version, which also affects telemetry, bug reporting, and other subsystems. (A prefix or postfix patch on the getter.)
    2. Replace SetRevisionNumber with our own code. (A prefix patch)
    3. Let SetRevisionNumber do its job, then (re)set the version text. (A postfix patch.)

New Solution, New Project, New Mod

Install and launch Visual Studio (not VS Code). During setup, pick the ".NET desktop development" workload (not Game development).

Create a new project of type "Class Library (.NET Framework)". (Not WPF, not App, not Control, not .NET Standard, not .NET Core.) When done, you should see a vanilla Class1.

On the left, in Solution Explorer, right-click "References", "Browse" Assembly-CSharp.dll and add it. Select the new reference and set "Copy Local" to False.

Still in Solution Explorer, right-click the project, select "Manage NuGet Packages". Find "Lib.Harmony" and Install v1.2.x. (v2 is incompatible.) Again set its Copy Local to False.

Optionally, rename Class1, change namespace, and fill in Assembly Information, such as mod title, your name, copyright etc. Build the project to make sure there is no error.

Canvas ready. Coding time.

Mod Initialisers

Add this method to your class. You will need to add Harmony to using directives.

   public static void MainMod ( Func<string,object,object> api ) {
      HarmonyInstance.Create( "your.mod.id" ).PatchAll();
      api( "log verbose", "Mod Initialised." );
   }

This method will be called by Modnix when the game launch. It asks Harmony to find and apply all patches in your mod, which we will create soon, and then log a verbose level message.

For PPML compatibility and Modnix 3 lazy load, use this code instead:

   // PPML entry point.
   public static void Init () => HomeMod();
   
   // Modnix 1 & 2 entry point. Skipped if any Modnix 3 phase exists, such as HomeMod (below).
   public static void MainMod ( Func<string,object,object> api ) => HomeMod( api );
   
   // Modnix 3 entry point.  HomeMod change the main menu (home screen).
   // Most mods can use GameMod, GeoscapeMod, or TacticalMod to enjoy lazy load.
   public static void HomeMod ( Func<string,object,object> api = null ) {
      HarmonyInstance.Create( "your.mod.id" ).PatchAll();
      api?.Invoke( "log verbose", "Mod Initialised." );
   }

This will work with PPML 0.1, PPML 0.3, and Modnix (all versions). This tutorial will keep things simple and not add the extra complexity.

Harmony Patches

The "easy" way to create a Harmony Patch is to create one class for each patched method and annotate them.

This is how to do a Postfix patch for UIModuleBuildRevision.SetRevisionNumber:

   // This "tag" allows Harmony to find this class and apply it as a patch.
   [ HarmonyPatch( typeof( UIModuleBuildRevision ), "SetRevisionNumber" ) ]
   
   // Class can be any name, but must be static.
   internal static class UIModuleBuildRevision_SetRevisionNumber {
   
      // Change version text _after_ SetRevisionNumber exits.
      private static void Postfix ( UIModuleBuildRevision __instance ) =>
         __instance.BuildRevisionNumber.text = "Hello World!";
   
   }

You will need to add the game class to using directives. Once done, the mod is functionally complete.

  • __instance is a magic parameter to get the instance of the method call.
  • __result is another magic parameter, usually used with ref for modification.
  • Other params allow you to read/write private fields, or pass info between Prefix and Postfix.
  • You can add static fields and non-patch methods to the patch class, such as data cache, mod state, or code reuse.

To patch multiple methods, simply create multiple patch classes.

Full Code

   using Harmony;
   using PhoenixPoint.Home.View.ViewModules;
   using System;
   
   namespace JohnDoe.QuickStartMod {
   
      public static class MyMod {
   
         public static void MainMod ( Func<string,object,object> api ) {
            HarmonyInstance.Create( "john.doe.quick.start" ).PatchAll();
            api( "log verbose", "Mod Initialised." );
         }
   
      }
   
      [ HarmonyPatch( typeof( UIModuleBuildRevision ), "SetRevisionNumber" ) ]
      internal static class UIModuleBuildRevision_SetRevisionNumber {
   
         private static void Postfix ( UIModuleBuildRevision __instance ) =>
            __instance.BuildRevisionNumber.text = "Hello World!";
   
      }
   
   }

Deploy

In Visual Studio's toolbar, change build config from "Debug" to "Release". This helps make your mod smaller and faster.

Build the project. If nothing goes wrong, right-click the project and open in File Explorer. Go to bin\Release, and you should see a DLL file.

This is your mod.

Add this file in Modnix and launch the game to see it works. (If there are lots of DLLs, you forgot to set "Copy Local" to False.)

After confirmation, you can zip and upload the dll to NexusMods.

Mod Info

Mod information helps Modnix, users, and other mod authors to identify your mod. It is done by adding an info file to the mod.

Back in Visual Studio, press Ctrl+Shift+A to add a new "Text File" named "mod_info.js".

Here is an example:

   {
      Id : "JohnDoe.QuickStart", // Must be unique. Same Id = one survives
      Lang : "en",
      Name : "Quick Start Hello World",
      Url  : "https://github.com/Sheep-y/Modnix/wiki/DLL-Mod-Quick-Start",
      Description : "This is my first mod. Please be gentle!",
   }

Check the new file's "Properties" and set "Build Action" to "Embedded Resource". Then rebuild the project and (re)add the new DLL to Modnix.

If the steps are correct, and there are no typos, the mod info will be displayed in Modnix.

Name, Version, and a short Description is preferably set in the project's Assembly Info. Assembly info is well supported on Windows, allows users to identify it even without Modnix.

Mod Config

Modnix make it easy to add configs to a mod.

  1. Create a config class.
  2. Specify config class in mod_info.
  3. Read config with api.

All public fields of the config class will be saved and read. You can use a hierarchy of classes, but stick with simple types for the leaf fields, such as string, int, bool, and their arrays.

Example mod code. 4 places changed.

   using Harmony;
   using PhoenixPoint.Home.View.ViewModules;
   using System;
   
   namespace JohnDoe.QuickStartMod {
   
      // New config class.
      internal class ModConfig {
         public string Version_Text = "Hello World!";
         public uint Config_Version = 1;
      }
   
      public static class MyMod {
   
         // New config field.
         internal static ModConfig Config;
   
         public static void MainMod ( Func<string,object,object> api ) {
            // Read config and assign to field.
            Config = api( "config", null ) as ModConfig ?? new ModConfig();
            HarmonyInstance.Create( "john.doe.quick.start" ).PatchAll();
            api( "log verbose", "Mod Initialised." );
         }
   
      }
   
      [ HarmonyPatch( typeof( UIModuleBuildRevision ), "SetRevisionNumber" ) ]
      internal static class UIModuleBuildRevision_SetRevisionNumber {
   
         // Rewrite to use configured value. User may set it to null.
         private static void Postfix ( UIModuleBuildRevision __instance ) =>
            __instance.BuildRevisionNumber.text = MyMod.Config.Version_Text ?? "Hello!";
   
      }
   }

Example mod_info.js. Added "ConfigType" and "Requires".

   {
      Id : "JohnDoe.QuickStart",
      Lang : "en",
      Name : "Quick Start Hello World",
      Url  : "https://github.com/Sheep-y/Modnix/wiki/DLL-Mod-Quick-Start",
      Description : "This is my first mod. Please be gentle!",
      Requires : { Id: "Modnix", Min: "2.0" },
      ConfigType : "JohnDoe.QuickStartMod.ModConfig", // Fullname with namespace
   }

That's it! Rebuild, re-add, and Modnix should show a "Config" tab.

Final Notes

Hope you are happy with your new little mod! Here is a grown-up version. ;)

  • The config code needs Modnix 2+. Remove "Requires" to support Modnix 1, but no config there.
  • "Config_Version" is unused now, but will make it easy to [expand] in the future.
  • Harmony's PatchAll throws on error. The Patch method allows far more control.
  • PPML compatibility is hell. You must pick between 0.1, 0.2, or 0.3, and the last two will crash 0.1.
  • Mods can embed readme, history, and license docs. File type can be .txt, .md, or .rtf.
  • Transpiler patch can modify a method's IL code. Useful for surgical patches.
  • Very short methods may be inlined on the fly; its patches would not run. Must modify every method that call it.
  • Most mods are open source, which you can copy and learn from or improve. Here are my mods.

Clone this wiki locally