Developing CSharp Mods

*The Planet Crafter* can be modded in a straightforward manner by using the BepInEx modding framework.

This guide will walk through the steps to create a C# solution that will host your various mods.

## Licensing

We recommend you develop your mods as open source software under a permissive license, such as Apache 2.0. This enables other prospective modders to look into your code and learn from it. Also it can serve as a springboard for a new maintainer in case you decide to stop developing your mods further.

## Preparation

You'll have to install a couple of software first.

### Install the latest .NET Developer Pack/SDK from Microsoft

As of writing, this will be .NET 7.0. https://dotnet.microsoft.com/en-us/download/visual-studio-sdks

### Install Visual Studio Community

https://visualstudio.microsoft.com/vs/community/

This IDE is free and fully capable of helping you develop the mods. It has integrated Git support.

### Install dnSpy

We use *dnSpy*, more specifically *dnSpyEx* to reverse engineer the game's `Assembly-csharp.dll`. It has syntax highlight, code decompilation and more importantly, can search for usages of methods and fields.

Note that the original *dnSpy* has ended development and there is now a new group developing it further under the name *dnSpyEx*.

https://github.com/dnSpyEx/dnSpy/releases

To use it, open the `Assembly-csharp.dll` from the game's directory, `The Planet Crafter\The Planet Crafter_Data\Managed\`.

### Install BepInEx for the game

If you haven't already, install the BepInEx framework into the game's directory. See Modding.

### Prepare BepInEx

To bootstrap your solution, first we have to tell .NET about a template the BepInEx framework uses to create new projects.

In Visual Studio's menu, select Tools>Command Line>Developer Command Prompt

On the command line, run the following:

```
dotnet new -i BepInEx.Templates --nuget-source https://nuget.bepinex.dev/v3/index.json

```

### Create a solution

Next, we create an empty solution from command line too. Make sure you are in the right directory (i.e., `%USERPROFILE%\git\MyPlanetCrafterMods`)

```
dotnet new sln --name MySolutionNameHere

```

### Create the first mod project

Next, we create a project within the solution directory above by utilizing the BepInEx template we installed before:

```
dotnet new bepinex5plugin -n MyModNameHere -T netstandard2.1 -U 2023.1.9
dotnet restore MyModNameHere

```

If all done right, you should see a new directory with `MyModNameHere`, having 3 files: `MyModNameHere.csproj`, `NuGet.cfg` and `Plugin.cs`.

If you get an error "Object reference not set to an instance of an object", it means the BepInEx template is not compatible with your .NET runtime installed. As of writing, the template works with .NET 7. To fix the issue above

* Install .NET SDK 7 too from https://dotnet.microsoft.com/en-us/download/dotnet/7.0
* Go to the target solution directory, then execute

```
dotnet new globaljson --sdk-version 7.0.404

```

This will create a `global.json`. Now you should be able to run the template code above.

For some reason, Visual Studio has difficulty picking up the NuGet repositories from within project, therefore, I suggest you copy `NuGet.cfg` one directory up into the solution folder. If this doesn't work, go to the *Tools > NuGet Package Manager > Package Manager Settings* dialog, select *Package sources* and add `https://nuget.bepinex.dev/v3/index.json` as a new source.

### Referencing the game files

The steps above create an independent mod with the BepInEx framework. In order to work with *The Planet Crafter*, we have to tell the project where it can find the game files.

In case you develop multiple mods, setting each project up in detail might be cumbersome. Therefore, we'll create some helper files first.

#### Creating solution\_private.targets

Go to the solution root directory and create a text file named `solution_private.targets` and open it with a text editor.

Inside it, put these:

```
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 <PropertyGroup>
 <GameDir>c:\Program Files (x86)\Steam\steamapps\common\The Planet Crafter\</GameDir>
 <GameManaged>$(GameDir)The Planet Crafter_Data\Managed\</GameManaged>
 <PluginsFolder>$(GameDir)\BepInEx\plugins</PluginsFolder>
 </PropertyGroup>
 <PropertyGroup>
 <AuthorName>YourNameHere</AuthorName>
 <Destination>$(PluginsFolder)\$(AuthorName) - $(Description)</Destination>
 </PropertyGroup>
</Project>

```

You'll have to change `<GameDir>` to point to your installation directory.

If you are like me and have multiple machines with different locations for the game install, you can make the `PropertyGroup` above conditional:

```
<PropertyGroup Condition="Exists('c:\Program Files (x86)\Steam\steamapps\common\The Planet Crafter')">
 <GameDir>c:\Program Files (x86)\Steam\steamapps\common\The Planet Crafter\</GameDir>
 <GameManaged>$(GameDir)The Planet Crafter_Data\Managed\</GameManaged>
 <PluginsFolder>$(GameDir)\BepInEx\plugins</PluginsFolder>
</PropertyGroup>
<PropertyGroup Condition="Exists('e:\Steam\steamapps\common\The Planet Crafter')">
 <GameDir>e:\Steam\steamapps\common\The Planet Crafter\</GameDir>
 <GameManaged>$(GameDir)The Planet Crafter_Data\Managed\</GameManaged>
 <PluginsFolder>$(GameDir)\BepInEx\plugins</PluginsFolder>
</PropertyGroup>

```

The `$(AuthorName) - $(Description)` setting is a convention I suggest with mods. The mods themselves are usually just DLL files but it is much better to have them in separate folders under the `BepInEx\plugins` directory. The directory uses your author's name and the short description of the mod for easy handling by the end-users.

#### Referencing solution\_private.targets

Now go into your mod directory and open the `.csproj` file.

Add the following line, perhaps after the first `PropertyGroup` entry:

```
<PropertyGroup>
 <!-- ... -->
</PropertyGroup>
<Import Project="$(SolutionDir)\solution\_private.targets" />

```

#### Adding the project to the solution

In the *Solution Explorer*, double-click on the solution (filename ending in .sln). Right-click on it and select *Add > Existing Project...*. Navigate to the `.csproj`, select it and approve the dialog.

(Remark: maybe there is an automatic way of doing this with the command line, but I don't know how. You'll have to do this file selection for every new project.)

#### Naming your mod

While we are looking at the `.csproj` file, check out the first `PropertyGroup` section:

```
<PropertyGroup>
 <TargetFramework>netstandard2.1</TargetFramework>
 <AssemblyName>YourModsDLLName</AssemblyName>
 <Description>(Category) Mod Short Description</Description>
 <Version>1.0.0.0</Version>
 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
 <LangVersion>latest</LangVersion>
</PropertyGroup>

```

Here, the `AssemblyName` will be your DLL name when you build the project. I recommend including a short category name with it, such as `CheatInfiniteResources` or `UIDarkMode`.

Next you can describe what the mod does. I suggest a short description that includes a category, such as `(Cheat) Infinite Resources` or `(UI) Dark Mode`. If you followed the `solution_private.targets` steps above, this short description will also be the deployed directory name for your mod.

Lastly. You'll have to bump the `Version` up every time a new version of the mod is ready to release. This will ensure BepInEx will pick the latest of your mod.

#### Referencing Game DLLs

Beyond the naming and versioning, we'll have to reference the game's and Unity's DLL files. Add these to the `.csproj` below the `PropertyGroups`

```
<ItemGroup>
 <Reference Include="Assembly-CSharp">
 <SpecificVersion>False</SpecificVersion>
 <HintPath>$(GameManaged)\Assembly-CSharp.dll</HintPath>
 <Private>False</Private>
 </Reference>
 <Reference Include="UnityEngine.UI">
 <SpecificVersion>False</SpecificVersion>
 <HintPath>$(GameManaged)unityengine.ui.dll</HintPath>
 <Private>False</Private>
 </Reference>
 <Reference Include="UnityEngine.InputSystem">
 <SpecificVersion>False</SpecificVersion>
 <HintPath>$(GameManaged)Unity.InputSystem.dll</HintPath>
 <Private>False</Private>
 </Reference>
</ItemGroup>

```

This will grant access to the game's code, Unity's UI and Input system code, required by most mods.

Certain mods may require interacting with the rich text presenter or the color picker:

```
<ItemGroup>
 <Reference Include="Unity.TextMeshPro">
 <SpecificVersion>False</SpecificVersion>
 <HintPath>$(GameManaged)unity.textmeshpro.dll</HintPath>
 <Private>False</Private>
 </Reference>
 <Reference Include="Unity.HSVPicker">
 <SpecificVersion>False</SpecificVersion>
 <HintPath>$(GameManaged)HSVPicker.dll</HintPath>
 <Private>False</Private>
 </Reference>
</ItemGroup>

```

#### Local Deployment

If you build your project, you'll get a DLL in one of the inner directories. It is very inconvenient to copy this every time into the `BepInEx\plugins` directory. Luckily, we can add some settings to the `.csproj` and have the build system take care of it:

```
<Target Name="CopyDLLs" AfterTargets="Build">
 <Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(Destination)" />
</Target>

```

If you have assets or other files, you'll have to copy them too:

```
<Target Name="CopyDLLs" AfterTargets="Build">
 <Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(Destination)" />
 <Copy SourceFiles="$(SourceDir)MyAsset.bin" DestinationFolder="$(Destination)" />
 <Copy SourceFiles="$(SourceDir)labels.txt" DestinationFolder="$(Destination)" />
 <Copy SourceFiles="$(TargetDir)Some3rdParty.dll" DestinationFolder="$(Destination)" />
</Target>

```

In the example above, we copy a binary and text file from the source directory, your mod project. In addition, 3rd party dlls also tend to end up the same place where your own assembly DLL, so we copy those too.

#### Releasing the mod

You could now, in principle just put up your mod DLL somewhere, but usually it is better to have it (and any accompanying files your mod needs) zipped together, ready to be uploaded to your site or mod repository.

The easiest way is to have the build system, again, create a zip file of your files and put it somewhere on your system. I tend to have it put into the solution folder itself.

To accomplish this, simply go to the `.csproj` again and within to the `CopyDLLs` we just set up and add the following into the `Target`:

```
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(TargetDir)\$(AssemblyName)\$(AuthorName) - $(Description)" />
<ZipDirectory SourceDirectory="$(TargetDir)\$(AssemblyName)" DestinationFile="$(OutputPath)\..\..\..\..\$(AuthorName)-$(AssemblyName).zip" Overwrite="true" />

```

First, it will create a subdirectory with the *author name - description* specified in previous sections, then zips this directory and puts the file into the solution directory.

(Remark. If you are using Git, don't forget to suppress `*.zip` files via `.gitignore` if you don't want to clutter your source tree with binaries like this.)

#### Packing up all mods

If you have many mods, it is a good idea to create an all encompassing zip file with all your mods, so users can cherry pick with convenience. This is slightly more involved to setup.

First, you can add another `Copy` task to the aforementioned `CopyDLLs` from the previous sections:

```
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(OutputPath)\..\..\..\..\$(AuthorName)-all\$(AuthorName) - $(Description)" />

```

Now you have to create a new "fake" mod the same way as described so far. I tend to name this project **ZipAll**. However, instead of setting up all the deployment like with normal mods, we set up the `CopyDLLs` task to zip our *all directory* we are creating with the normal mods:

```
<Target Name="CopyDLLs" AfterTargets="Build">
 <ZipDirectory
 SourceDirectory="$(OutputPath)\..\..\..\..\$(AuthorName)-all"
 DestinationFile="$(OutputPath)\..\..\..\..\$(AuthorName)-all.zip"
 Overwrite="true" />
</Target>

```

As the last step, we have to tell the solution that our **ZipAll** project depends on all of our normal projects, so the build order is fine. To do this, go to the solution properties (right click on the solution and pick *Properties*). Click on *Project Dependencies* and from the *Projects* drop-down, pick **ZipAll**. Now check every other project and click *Ok* at the end. Don't forget to save your solution (*File > Save All*).

Unfortunately, I don't know any other way so whenever you create a new mod project, you'll have to manually edit this dependency settings every time. Sorry about that.

## Developing an actual mod

### Naming and versioning

With the solution and project(s) set up, you can now open the `Plugin.cs`. You'll be greeted with a minimal mod class with some attributes.

```
using BepInEx;

namespace ExampleMod
{
 [BepInPlugin(PluginInfo.PLUGIN\_GUID, PluginInfo.PLUGIN\_NAME, PluginInfo.PLUGIN\_VERSION)]
 public class Plugin : BaseUnityPlugin
 {
 private void Awake()
 {
 // Plugin startup logic
 Logger.LogInfo($"Plugin {PluginInfo.PLUGIN\_GUID} is loaded!");
 }
 }
}

```

The BepInEx template is so nice it sets up the required parameters for BepInEx by dynamically referencing the `.csproj` attributes `AssemblyName`, `Description` and `Version` of it so no need to manually change these, especially the version number.

However, usually what's nice for a DLL name (i.e., `Assembly`) can be a problem as unique identifier for your mod (you can have the same DLL name in separate folders).

So instead, of using `PluginInfo.PLUGIN_GUID`, we'll change that parameter to a string that includes our name, the game name and the mod's name: `yourname.theplanetcraftermod.myfirstmod`

```
       [BepInPlugin("yourname.theplanetcraftermods.myfirstmod", PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]

```

This naming convention is more useful if you end up modding several games via BepInEx in the future.

### Mod configuration

Usually your mod will need some configuration options so users can tailor them to their needs. BepInEx stores your mod's config named as your <GUID>.cfg, such as `yourname.theplanetcraftermod.myfirstmod.cfg`. However, this file is only created when you actually interact with the configuration system.

To do this, simply create a `ConfigEntry<T>` field in your mod (recommend static field), then call `Config.Bind` within `Awake`

```
using BepInEx;
using BepInEx.Configuration;

namespace ExampleMod
{
 [BepInPlugin(PluginInfo.PLUGIN\_GUID, PluginInfo.PLUGIN\_NAME, PluginInfo.PLUGIN\_VERSION)]
 public class Plugin : BaseUnityPlugin
 {
 static ConfigEntry<bool> modEnabled;

 private void Awake()
 {
 // Plugin startup logic
 Logger.LogInfo($"Plugin {PluginInfo.PLUGIN\_GUID} is loaded!");

 modEnabled = Config.Bind("General", "Enabled", true, "Is the mod enabled?");
 }
 }
}

```

The layout is straightforward. With the category *General* and property name *Enabled*, defaulted to *true*, you'll automatically get said property loaded upon mod activation.

### Logging

The framework offers logging capabilities via the *Logger* instance API (which means you'll have to save it into a static field to be accessible from static methods). Logging via it will end up in the game's `Player.log` too (`%USERPROFILE%\AppData\LocalLow\MijuGames\The Planet Crafter`).

```
using BepInEx;
using BepInEx.Configuration;

namespace ExampleMod
{
 [BepInPlugin(PluginInfo.PLUGIN\_GUID, PluginInfo.PLUGIN\_NAME, PluginInfo.PLUGIN\_VERSION)]
 public class Plugin : BaseUnityPlugin
 {
 static ConfigEntry<bool> modEnabled;

 static ManualLogSource logger;

 private void Awake()
 {
 // Plugin startup logic
 Logger.LogInfo($"Plugin {PluginInfo.PLUGIN\_GUID} is loaded!");

 modEnabled = Config.Bind("General", "Enabled", true, "Is the mod enabled?");

 logger = Logger;
 }

 static void MyMethod()
 {
 logger.LogDebug("Hello world!");
 }
 }
}

```

Also if there is something wrong, crashes or your mod doesn't appear to be working, check out said `Player.log` for problems.

### Intercepting game code

This guide won't go into details how the patching framework, Harmony is working. Please see https://harmony.pardeike.net/articles/intro.html for the available features.

#### A quick primer on Harmony

Perhaps the easiest way to invoke the patching framework is to add methods to one's `Plugin` class, annotate them and point Harmony to perform the patching. The best place for this is at the end of a `Awake()` method:

```
void Awake()
{
 Harmony.CreateAndPatchAll(typeof(Plugin));
}

```

The framework allows intercepting method calls on classes, which can be done before and after said method executes. For this, you define a static method on `Plugin`, annotate it with `[HarmonyPrefix]` or `[HarmonyPostfix]`.

```
[HarmonyPrefix]
static void MyOverridingMethod()
{
}

```

Next, one has to tell Harmony which exact class and method needs to be intercepted via `[HarmonyPatch]`. This annotation has at least two arguments: the first is the target type, the next is the method name.

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), nameof(Intro.ShowMainMenu))
static void MyOverridingMethod()
{
}

```

As a tip, I recommend naming such patching methods based on the original method name for finding it easier, thus `Intro_ShowMainMenu`, `Patch_Intro_ShowMainMenu` or even `Patch_Intro_ShowMainMenu_Prefix`. This latter can come in handy if one needs to run code before and after the original method has been invoked.

It is preferreable to use `nameOf(Class.PublicMethod)` so that if the game's code changes, plugin code depending on it will generate an error during compile time. Unfortunately, one can't reference non-public code thus some overrides have to specify the method name as string:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), "Start")
static void MyOverridingMethod()
{
}

```

When implementing an overriding method, Harmony grants access to the current instance and any fields of the target class. To get the current instance, define a `T __instance` parameter (two underscores) on the method, where T is the class being targeted:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), "Start")
static void MyOverridingMethod(Intro \_\_instance)
{
}

```

To access fields, especially private fields, define a `U ___fieldName` parameter (three underscores):

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), "Start")
static void MyOverridingMethod(Intro \_\_instance, GameObject \_\_\_saveFilesContainer)
{
}

```

If you need to manipulate such fields, define the parameter with ref keyword:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), "Start")
static void MyOverridingMethod(Intro \_\_instance, ref GameObject \_\_\_saveFilesContainer)
{
 \_\_\_saveFilesContainer = new GameObject("example");
}

```

When the target method has arguments, you can list any of them with the exact same name and type. To overwrite them, use `ref` again:

```
[HarmonyPrefix]
[HarmonyPatch(typeof(Inventory), nameof(Inventory.SetSize))
static void MyOverridingMethod(Inventory \_\_instance, ref int \_inventorySize)
{
 \_\_\_\_inventorySize += 10;
}

```

If overriding a method that returns a value, you can use the `ref R __result` parameter, where R is the return type.

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Inventory), nameof(Inventory.IsFull))
static void MyOverridingMethod(Inventory \_\_instance, ref bool \_\_result)
{
 \_\_result = false;
}

```

One of the benefits of having `[HarmonyPrefix]` is that one can prevent the original code from running by declaring the overriding method with a `bool` return type and returning false. Note that in this case, one may have to provide a return value with the reference above.

```
[HarmonyPrefix]
[HarmonyPatch(typeof(Inventory), nameof(Inventory.IsFull))
static bool MyOverridingMethod(Inventory \_\_instance, ref bool \_\_result)
{
 if (\_\_instance.GetId() == 1)
 {
 \_\_result = false;
 return false;
 }
 return true;
}

```

Warning! One caveat to consider when trying to override methods. Some methods, typically getters and setters are so small the .NET Runtime inlines their code before patching occurs, thus the override has no effect at all. Strangly enough, this inlining may happen on some computers but not on others.

#### Finding game objects

There are generally 3 ways for locating game-related objects.

1. Using `GameObject.Find` where you'll have to know the name of the GameObject. Use Unity Explorer ingame to find those.
2. Using `UnityEngine.Object.FindObjectsByType<T>` when the type T is known but the object is not named trivially.
3. Using `Managers.GetManager<T>` where T is one of the game objects that exists when the player has loaded into a world. Otherwise it returns null.

#### Manipulating the Main Menu

When the game loads the `SpaceCraft.Intro` behavior is activated. The best is to intercept the `Start()` method:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), "Start")
static void Intro\_Start(Intro \_\_instance)
{
}

```

Note however that some elements may not have beein fully initialized at this moment in time, so you'll have to start a coroutine with a delay before attempting to manipulate the menu:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(Intro), "Start")
static void Intro\_Start(Intro \_\_instance)
{
 \_\_instance.StartCoroutine(My\_mainMenu\_Code());
}
static IEnumerator My\_MainMenu\_Code()
{
 yield return new WaitForSeconds(1.1f);

 // your customizations here.
}

```

#### Entering the world

When the player picks a world to load into, the process starts within the `SessionController.Start` method. You can intercept this method as postfix:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(SessionController), "Start")
static void SessionController\_Start(SessionController \_\_instance)
{
}

```

However, some world objects might not have been initialized when the vanilla `SessionController.Start` method finishes, so one has to intercept `PlanetLoader.HandleDataAfterLoad` method too.

```
[HarmonyPostfix]
[HarmonyPatch(typeof(PlanetLoader), "HandleDataAfterLoad")
static void PlanetLoader\_HandleDataAfterLoad(PlanetLoader \_\_instance)
{
}

```

Currently, there is only one planet in the game so we don't have to worry about which planet is being loaded.

#### Leaving the world

When the player quits to the main menu, some mods may need to clean up resources manually. You can intercept this by targeting `UiWindowPause.OnQuit`:

```
[HarmonyPostfix]
[HarmonyPatch(typeof(UiWindowPause), nameof(UiWindowPause.OnQuit))]
static void UiWindowPause\_OnQuit()
{
 someBuffer.Clear();
 someObject = null;
}

```

Generally, `GameObject`s will be automatically destroyed by the Unity Engine.

#### Save persistence

The game's save format is based on sections: world objects, inventories, story elements, terrain. It is somewhat rigid and would require patching the entire loading and save processes.

However, there is a simple method to get persistence without disrupting the save format: using the text attribute of a world object. The approach is as follows.

* Intercept entering the world.
* Create a world object with a known ID if it doesn't already exist
* Keep its text property up-to-date.

To create a world object, one will first need an item description, called `Group`. One can look for such groups via `GroupsHandler.GetGroupViaId`

```
const int myModStorageId = 221109;

static WorldObject myModStorage;

[HarmonyPostfix]
[HarmonyPatch(typeof(PlanetLoader), "HandleDataAfterLoad")
static void PlanetLoader\_HandleDataAfterLoad(PlanetLoader \_\_instance)
{
 myModStorage = WorldObjectsHandler.GetWorldObjectViaId(myModStorageId);
 if (myModStorage == null) {
 var gr = GroupsHandler.GetGroupViaId("Iron");
 myModStorage = WorldObjectsHandler.CreateNewWorldObject(gr, myModStorageId);
 myModStorage.SetDontSaveMe(false);
 }
}

```

This routine will find or create such a special world object. "Iron" is one of the most reliably present item type. `SetDontSaveMe(false)` ensures that changes to this world object will be saved.

You can use any `myModStorageId` value provided it is less than 200.000.000 (where the game's objects are allocated by default), does not start with 10 (those are the pre-placed objects identified by). So `3456789` is a good id, but `1023` is not. Note that some mods already use ids (4000 - hotbar, 6000 - pin recipe, 20-99 - multiplayer) so please consult the community on Discord first.

Then, whenever something needs to be persisted, either call `myModStorage.SetText()` at that point every time, or intercept and prefix `SavedDataHandler::SaveWorldData` and issue the call there once.

One important thing about the text to be stored is that it can't contain the at sign (@) or the pipe sign (|) because the game treats these as section and line separator in its save file. Therefore, you may have to invent an encoding scheme with escape characters for these, use hex, Base32 or Base64 encoding.

## Further help

The game is vast and complicated. If you are having trouble with the above or are wondering where to find things or get an explanation how they (likely) work, find us on Discord:

https://discord.gg/9YPjJ3C in the `#modding` channel.