RimWorld

RimWorld

35 ratings
[For Modders] Multi-Version support
By NightmareCorporation
This guide will detail how to use one code-base to compile assemblies for multiple versions and other details that are required to provide support for multiple versions.

The guide is meant for mod-developers that have C# mods and don't want to abandon the previous version by locking its code at any given release - or go back to their older version of the solution to re-apply changes to the latest version.
3
   
Award
Favorite
Favorited
Unfavorite
From Ludeons Mouth
Few modders know that Ludeon has published its own guide on how to provide multi-version support. You can find the file in your installation path under
SteamLibrary\steamapps\common\RimWorld\ModUpdating.txt
with a docs link to
https://docs.google.com/document/d/1_DmcLpIvHIQ5AxVLYrn9_iwkOqArlgcWcyE_6RSDG6M/edit

It goes into details on how the folder structure of the mod should look like to load files conditionally depending on your version.

You can either create a LoadFolders.xml to control your own folder loading, or let the automatic system take over for you.

It would be redundant to go into details on the LoadFolders here.
Preamble
The common approach to how modders handle multi-version support is to only build their assembly for the latest version and shelve the previous assembly, so that it doesn't get any of the newly developed features.

The alternative to that is to constantly maintain two different copies of your solution, which is just not a good way to handle code.

This guide will illustrate how you can use a single codebase with shared code for multiple versions and how to define sections of code that are only used for any given version. I am using Visual Studio 2022 Community Edition, but this should work for any given version, although the UI will vary.
For illustration, I will be updating a personal mod from 1.3 to 1.4.

Setting up build configurations
On the tool ribbon, click the current build config, if you never touched these, this will most likely be "Debug" or "Release" - you can remove those if you want to, because we will make new build configs here.


In the Configuration Manager we can select which of our projects we want to build, ignore the "Common" project, that's a personal sub-project to keep all the XML files in VS.

You won't be doing much here, just click on the active solution configuration and create a new config with an appropriate name


Compilation Symbols
Now you have the ability to switch between these two build configurations, the next step is to set up configuration-specific compilation symbols:
Navigate to your project properties and switch to the "Build" tab on the right.

Define a symbol with an appropriate name and make sure your output path loads into a proper versioned folder, if you use LoadFolders.xml, make sure that any exotic paths are matching here!

At the top, switch to the old 1.3 config and define another compiler symbol.


OPTIONAL: Installing the krafs package through the package manager
Next comes the conditional assembly management, I personally recommend to use the NuGet package for the RimWorld assemblies, because they cut down on needless reference management. Harmony and HugsLib (if you use it) also have NuGet packages you can use for easier reference management!

If you want to, you can skip these steps and just insert the .csproj insertions posted later, Visual Studio automatically imports untracked NuGet packages, so just building should do these steps, I will go over it because understanding how to normally install packages may be helpful.


The package you want is called
krafs.rimworld.ref
and updates automatically to whatever version you need - because we'll set it up to do exactly that in the next step:
ALTERNATIVE to NuGet
You can choose not to use NuGet Packages and apply the following steps to your manually referenced assemblies, the only difference will be the name, the .csproj is luckily very similar for packages and normal references.
Conditional assembly loading
After installing the package you'll need to navigate into your mods source folder (for me that's
SteamLibrary\steamapps\common\RimWorld\Mods\Archaeology\Source\Archaeology.csproj
and open the .csproj file with any editor that is not Visual Studio (because that would open the solution) - I use Visual Studio Code.

In this file you can see the various references and packages you have installed for the given solution, we are going to be modifying the krafs package my replacing some text. The order of the <ItemGroup> is irrelevant here, but i like moving these near the top.
Your setup for the packages will look something like this
<ItemGroup> <PackageReference Include="Krafs.Rimworld.Ref" Version="*"> <ExcludeAssets>runtime</ExcludeAssets> </PackageReference> </ItemGroup>

We are going to change that to this format, which is going to add conditionally loaded elements in the .csproj, that means some packages are only loaded if the 1.3 build config is active, while others are only loaded if the 1.4 config is.
<Choose> <When Condition="'$(Configuration)' == '1.3'"> <ItemGroup> <PackageReference Include="Krafs.Rimworld.Ref" Version="1.3.*"> <ExcludeAssets>runtime</ExcludeAssets> </PackageReference> </ItemGroup> </When> <When Condition="'$(Configuration)' == '1.4'"> <ItemGroup> <PackageReference Include="Krafs.Rimworld.Ref" Version="1.4.*"> <ExcludeAssets>runtime</ExcludeAssets> </PackageReference> </ItemGroup> </When> </Choose>

You can add the Condition on any XML node here for it to work, experiment if you are brave or use git. We also provided a proper version number in the PackageReference node, which means NuGet will only ever pull the highest 1.3 or 1.4 version - the * is a placeholder for "highest possible". (You can even target a beta branch like an unstable 1.4 version by using "1.4.*-*"). Using "*" means NuGet will always pull the absolute latest stable version of the given package, you can use that for things like Harmony, which are never bound to the games version.

A complete .csproj in the "new format" would look like this:
(If you named your configurations after the exact versions you can even shortcut some of the paths by directly using the configuration in the path)
(This example is taken from my DBH Bad Hygiene / Hotsprings compatibility)
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net481</TargetFramework> <OutputPath>..\$(Configuration)\Assemblies\</OutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <Configurations>Debug;Release;1.3;1.4;1.5</Configurations> </PropertyGroup> <!-- version specific constants --> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == '1.3|AnyCPU'"> <DefineConstants>$(DefineConstants);v1_3</DefineConstants> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == '1.4|AnyCPU'"> <DefineConstants>$(DefineConstants);v1_4</DefineConstants> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == '1.5|AnyCPU'"> <DefineConstants>$(DefineConstants);v1_5</DefineConstants> </PropertyGroup> <!-- nuget packages --> <ItemGroup> <PackageReference Include="Krafs.Rimworld.Ref" Version="$(Configuration).*-*"> <ExcludeAssets>runtime</ExcludeAssets> </PackageReference> <PackageReference Include="Lib.Harmony" Version="*"> <ExcludeAssets>runtime</ExcludeAssets> </PackageReference> </ItemGroup> <!-- local references --> <ItemGroup> <Reference Include="DubsBadHygiene"> <HintPath>..\..\..\..\..\workshop\content\294100\836308268\$(Configuration)\Assemblies\BadHygiene.dll</HintPath> <Private>False</Private> </Reference> <Reference Include="VFEC"> <HintPath>..\..\..\..\..\workshop\content\294100\2787850474\$(Configuration)\Assemblies\VFEC.dll</HintPath> <Private>False</Private> </Reference> <Reference Include="StandaloneHotSpring"> <HintPath>..\..\..\..\..\workshop\content\294100\2205980094\$(Configuration)\Assemblies\StandaloneHotSpring.dll</HintPath> <Private>False</Private> </Reference> </ItemGroup> <!-- kijin was re-released for 1.4 --> <Choose> <When Condition="'$(Configuration)' == '1.3'"> <ItemGroup> <Reference Include="Kijin3"> <HintPath>..\..\..\..\..\workshop\content\294100\1918120749\1.3\Assemblies\Kijin2.dll</HintPath> <Private>False</Private> </Reference> </ItemGroup> </When> <When Condition="'$(Configuration)' != '1.3'"> <ItemGroup> <Reference Include="Kijin3"> <HintPath>..\..\..\..\..\workshop\content\294100\2884551646\$(Configuration)\Assemblies\Kijin3.dll</HintPath> <Private>False</Private> </Reference> </ItemGroup> </When> </Choose> </Project>
Applying the .csproj changes
Going back to Visual Studio, you will get this message, click "Reload All". Do note that the package is not IMMEDIATELY loaded, you need to build the assembly first.
Before building






After that you hopefully have a successful build on your previous version. Now you can remove the old RW-based assemblies and any UnityEngine assembly, because the Package will pull them automatically for you.


Code Example
Now that our assemblies can dynamically switch for the build config, we can create code-sections that are version specific, in this example we have a golden bullet that kills anything it hits, in 1.3 the signature of Impact is
void Impact(Thing)
in 1.4 the signature has changed to
void Impact(Thing, bool)
Here the class that our golden bullet used before we added multi-version support.
public class GoldenBullet : Bullet { protected override void Impact(Thing hitThing) { base.Impact(hitThing); hitThing.Kill(); } }
We will be using a compiler symbol to enable specific code sections for each version like this:
public class GoldenBullet : Bullet { #if v1_3 protected override void Impact(Thing hitThing) #else protected override void Impact(Thing hitThing, bool blockedByShield = false) #endif { base.Impact(hitThing); hitThing.Kill(); } }

Using #if allows us to check if that symbol is currently active - and we already set up the connected compiler symbol when we set up our build config in an earlier step. You can also use #elif v1_4 to check specifically for 1.4, but it's generally more prudent to assume that future versions will use the same signature.

You can use these compiler checks in any code, no matter where, you can make using statements conditional, method declarations, variables, there are no limits.
Notes
Using multiple build configs during development
Do note that changing a build config does not instantly swap out the assembly! You will need to build once to prompt NuGet to replace the package references and actually see suggestions and errors relating to the current configs assembly.

Batch-Building
Also batch-build sadly does not work with this approach due to an issue in VS2022, I have no knowledge if other versions work, but it is definitely a sorely missed feature. As a result you will have to manually switch to each build config and compile each on their own when you want to update multiple versions.
1 Comments
Alexis Popcorn 28 Oct, 2022 @ 1:12pm 
It works, thanks!