xenonauts 2 tutorial code - GoldhawkInteractive/X2-Modding GitHub Wiki
Welcome to Xenonauts 2 modding! In this tutorial, we’ll create a simple “Hello World” code mod from scratch. Xenonauts 2 supports C# code mods via a modding API and Harmony patching. We will use the official X2-Modding/code-skeleton-mod template as our starting point. This guide assumes you know C#, but no prior Xenonauts 2 modding experience is needed.
By the end of this tutorial, you will have a mod that:
-
Integrates with the game’s mod lifecycle via the
IModLifecycle
interface. -
Defines a new System that listens for a game event (using Artitas’s event system and the
[Subscriber]
attribute) and logs a message. -
Applies a sample Harmony postfix patch to demonstrate altering game behavior.
-
Builds into a
.dll
with a mod manifest, ready to load in Xenonauts 2.
Let’s get started!
Before writing code, set up your development environment:
-
Install Tools: Have Visual Studio 2022 (or JetBrains Rider) with the .NET Framework 4.8 SDK.
-
Get the Mod Template: Obtain the X2ExampleMod project (the “code-skeleton-mod” from the official modding repository). This provides a ready project structure:
X2-Example-Mod/ ├── Properties/ │ └── AssemblyInfo.cs (Assembly metadata) ├── Patches/ │ └── ExamplePatch.cs (Harmony patches example) ├── $NameModLifecycle.cs (Main mod entry point & lifecycle)
($NameModLifecycle.cs will be renamed for your mod; e.g. HelloModLifecycle.cs)
-
Link Game Libraries: Open the
Directory.Build.props
in the mod project and update paths to your Xenonauts 2 installation and mod folder. For example:<PropertyGroup> <!-- Path to Xenonauts 2 Managed DLLs --> <GameFolder>C:\Games\Xenonauts2\Xenonauts2_Data\Managed</GameFolder> <!-- Path to cached external DLLs (if using game code base) --> <CacheFolder>D:\X2ModdingRepo\binaries</CacheFolder> <!-- Output mod folder for auto-copy on build --> <ModInstanceFolder>C:\Users\<You>\Documents\My Games\Xenonauts 2\Mods\HelloWorldMod</ModInstanceFolder> </PropertyGroup>
Set GameFolder to the
Managed
directory of your Xenonauts 2 install (containingAssembly-CSharp.dll
,UnityEngine.dll
, etc.), and ModInstanceFolder to your mod’s target folder (we’ll create this folder later under the game’s Mods directory). This ensures the project can find game assemblies and will copy your built mod DLL into the Mods folder automatically.
With the project set up and references configured, you’re ready to implement your mod.
The core of every Xenonauts 2 code mod is a class that implements Common.Modding.IModLifecycle
. The game will detect and use this class to hook into your mod. The IModLifecycle
interface defines several methods that the game calls at specific times:
-
Create(Mod mod, Harmony patcher)
– Called once when the mod is loaded/enabled. Use this to initialize your mod (set up logging, etc).- Note that Harmony patches are automatically patched for you before this call!
-
Destroy()
– Called when the mod is unloaded or the game exits. Clean up resources if needed. -
OnWorldCreate(IModLifecycle.Section section, WeakReference<World> world)
– Called whenever the game creates a new “World” (game state) for a section (e.g. Strategy, GroundCombat, AirCombat). This is where you can inject your custom game logic into the world. -
OnWorldDispose(IModLifecycle.Section section, WeakReference<World> world)
– Called when a world is about to be disposed (e.g. leaving a game screen).
Let’s create our mod lifecycle class. In the template, this is a file like X2ExampleModLifecycle.cs; for this tutorial, we’ll name it HelloModLifecycle.cs. It should be a public class implementing IModLifecycle
.
Example – HelloModLifecycle.cs:
using Common.Modding;
using HarmonyLib;
using Artitas.Utils;
using log4net;
using System.Reflection;
namespace HelloWorldMod {
public class HelloModLifecycle : IModLifecycle {
// Set up a logger for our mod (optional but useful)
private static readonly ILog Log = ArtitasLogger.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public void Create(Mod mod, Harmony patcher) {
// Called when mod is loaded
Log.Warn("[HelloMod] Mod created and loaded!");
// (Harmony patches are applied automatically by the mod loader)
}
public void Destroy() {
// Called when mod is unloaded
Log.Warn("[HelloMod] Mod is being unloaded.");
}
public void OnWorldCreate(IModLifecycle.Section section, WeakReference<Artitas.World> worldRef) {
// Called when a new game world is created (strategy, ground combat, etc.)
if (!worldRef.TryGetTarget(out Artitas.World world)) return;
Log.Info($"[HelloMod] OnWorldCreate for section: {section}");
if (section == IModLifecycle.Section.Strategy) {
// Register our custom System in the Strategy world
world.RegisterSystem<HelloWorldSystem>();
Log.Info("[HelloMod] HelloWorldSystem added to Strategy world.");
}
}
public void OnWorldDispose(IModLifecycle.Section section, WeakReference<Artitas.World> worldRef) {
Log.Info($"[HelloMod] OnWorldDispose for section: {section}");
// No special cleanup required in this example
}
}
}
Let’s break down what this does:
-
In
Create()
, we log a message indicating our mod started. The mod loader provides a Harmony instance (patcher
) which has already been used to apply any[HarmonyPatch]
annotations in our assembly (more on patches soon). The template’s example uses logging inCreate
andDestroy
to verify the mod loaded. -
In
OnWorldCreate()
, we first get the actualWorld
object from theWeakReference
. We then check which game section is starting. Here we choose to act only on the Strategy section (the Geoscape). If the Strategy world is being created, we callworld.RegisterSystem<HelloWorldSystem>()
to add our custom game logic System into that world. We’ll defineHelloWorldSystem
next. (We log the action for confirmation.) -
OnWorldDispose()
simply logs when a world is ending. In a more complex mod, you might remove systems or perform cleanup here, but our simple mod doesn’t need to.
What is world.RegisterSystem
? This is the game’s way of injecting a System (part of the Artitas ECS framework) into the game world. The core game uses this to add its own systems – for example, when Xenonauts 2 creates a new Air Combat world, it registers all the Air Combat systems. By registering our own system, we hook our code into the game’s update loop and event system. The RegisterSystem<T>()
method will instantiate our HelloWorldSystem
, subscribe it to events, and include it in the world’s processing pipeline automatically.
Note: We used
ArtitasLogger
/log4net
for logging to integrate with the game’s logging system. You can also useUnityEngine.Debug.Log
for simple debugging, but using the game’s logger means your messages appear in the game’s log file with the proper format. The skeleton template even provides aModConstants
with a staticILog Log
you can use across classes. Logging is optional for functionality, but it helps confirm that your mod is running.
With our mod lifecycle class in place, the game will automatically find it (the mod loader scans all mod assemblies for IModLifecycle
implementations and instantiates them). Now, let’s create the HelloWorldSystem
that we registered in OnWorldCreate
.
Systems in Xenonauts 2 (using the Artitas ECS framework) are classes that contain game logic and respond to events. In fact, “logic which operates on a Family, triggered by an Event” is exactly how the World defines a System. The game continuously fires events (like lifecycle events, input events, etc.), and systems can subscribe to those events.
We’ll create a simple System that listens for a “world initialized” event and prints a hello-world message. To do this, we take advantage of Artitas’s event bus and the [Subscriber]
attribute.
Artitas provides [Subscriber]
to easily subscribe to events. If a System class (specifically, one that extends Artitas.Systems.EventSystem
) has a public method marked with [Subscriber]
and taking an IEvent
or subtype parameter, that method will be automatically wired to receive events of that type. The EventSystem base class scans for such methods in its constructor and registers them for you:
How it works: When an EventSystem is created, it uses
SubscriberAttribute.FindAllSubscriberMethods
to find methods with[Subscriber]
and builds a list of events they handle. It then adds those event types to the system’s subscription list. The world will route matching events to your method at runtime.
In our case, we want to know when the game’s world is fully initialized, so we’ll subscribe to the PostInitializeWorldLifecycleEvent
. This event is fired by the world after all systems (including ours) have been initialized. It’s a good moment to run any startup code.
For modding purposes, taking a look at all classes that inherit from IEvent
or ISystem
is a good start to discover a lot of functionality.
Example – HelloWorldSystem.cs:
using Artitas.Systems;
using Artitas.Utils;
using Artitas.Core.Events;
using Artitas;
using log4net;
using System.Reflection;
namespace HelloWorldMod {
// Extend EventSystem to automatically handle [Subscriber] methods
public class HelloWorldSystem : EventSystem {
private static readonly ILog Log = ArtitasLogger.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
// This method will be called when the world initialization is complete
[Subscriber]
public void OnWorldPostInit(PostInitializeWorldLifecycleEvent ev) {
Log.Warn("[HelloMod] HelloWorldSystem received PostInitialize event – World is initialized!");
// We can perform any post-initialization logic here.
// For example, create an entity, display a message, etc. (This simple mod just logs a message.)
}
}
}
Breaking it down:
-
HelloWorldSystem extends
Artitas.Systems.EventSystem
. By doing so, we inherit the functionality that finds[Subscriber]
methods. We don’t override anything else here. -
We subscribe to
PostInitializeWorldLifecycleEvent
by simply writing a methodOnWorldPostInit
that takes that event type as a parameter and tagging it with[Subscriber]
. -
In the method, we log a message announcing that the world has finished initializing. (In a real mod, this is where you might initialize your own gameplay elements, spawn entities, etc. For our hello-world, a log is enough to prove it worked.)
When Xenonauts 2 creates the Strategy world and calls world.Initialize()
, the sequence will be: our HelloWorldSystem
is registered (via OnWorldCreate
earlier), then during world init, all systems are initialized and finally the PostInitializeWorldLifecycleEvent is fired. Our system receives that event and logs the message. This will happen each time a new world is started for the sections we added the system to. In our case, we only added it to the Strategy section, so we’ll see the message when starting or loading a Strategy (geoscape) game.
Recap: At this point, our mod lifecycle will add HelloWorldSystem
to the game’s world, and the system will respond to a game event. That covers the basics of integrating new logic via the ECS. Now we’ll look at the other modding technique: Harmony patching.
Sometimes you’ll want to change or extend existing game code directly. Xenonauts 2 mods can do this with Harmony (a library for runtime method patching). The mod loader already prepares a Harmony instance for your mod and applies all patches when the mod is enabled. To use it, you create classes with Harmony attributes describing which game method to patch, and Harmony will inject your prefix/postfix code.
Note that given the nature that Xenonauts 2 uses Artitas, a lot of functionality can also be achieved by either unregistering Systems, or intercepting events.
Let’s walk through adding a simple postfix patch. A postfix runs after the original method, allowing you to observe or modify results. (A prefix can run before the original, possibly skipping it or changing input arguments.)
Harmony Patch Basics:
-
Mark a class with
[HarmonyPatch]
specifying the target type and method name (and optional method signature if needed) to patch. -
Inside, define a
static
method with[HarmonyPrefix]
or[HarmonyPostfix]
that matches the target method’s signature (plus special parameters for accessing original results, instance, etc., if needed). -
If the prefix method returns
false
, it will skip the original method entirely. Postfix runs after the original (and can optionally accept the original return value or exception as parameters).
The skeleton mod template already includes an example Harmony patch. For instance, it shows a prefix on GroundCombatScreen.OnSetup()
, logging a message and then allowing the original function to run (returning true in the prefix). We’ll create a postfix patch in our mod for demonstration.
Suppose we want to log a message right after the game’s main XenonautsMain.Start()
method runs (this is called once when the game initializes, after mods are loaded). Here’s how to do it:
Example – HelloPatch.cs (in the Patches/ folder):
using HarmonyLib;
using UnityEngine; // for Debug.Log
namespace HelloWorldMod.Patches {
[HarmonyPatch(typeof(Xenonauts.XenonautsMain), "Start")]
public static class HelloPatch {
private static readonly ILog Log = ArtitasLogger.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
[HarmonyPostfix]
public static void PostStart() {
// This code runs after XenonautsMain.Start() executes
Log.Warn("[HelloMod] XenonautsMain.Start has finished – Hello from the mod!");
}
}
}
A few things to note:
-
We specify the class and method to patch with
[HarmonyPatch]
. In this case,typeof(Xenonauts.XenonautsMain)
and method name"Start"
. This targets theXenonautsMain.Start()
method in the game. -
The patch class is
static
(Harmony requires patch classes to be static). We named itHelloPatch
, but the name doesn’t matter – you typically name it after what you’re patching. -
The postfix method
PostStart()
is static and returns void (since the originalStart
is void). We don’t need any parameters here because we’re just logging, but you could include parameters likeException __exception
to detect errors, orXenonautsMain __instance
to access the instance, etc., depending on your needs.
When the mod is enabled, the mod loader will invoke harmony.PatchAll()
on your assembly, which finds this class and applies the patch. So as soon as XenonautsMain.Start()
runs (during game launch), our postfix will execute and print the message.
Harmony patches are extremely powerful – you can change return values, adjust game behavior, or call your own code at specific points in the original code execution. Use them wisely (and be careful with prefixes that skip original methods or with altering critical logic). In our “Hello World” scenario, the postfix patch is just for illustration; our main functionality (logging on world init) was done via the mod lifecycle and event system. But if you wanted, for example, to modify a gameplay value (say, change soldier health on spawn), you might patch the method that creates soldiers, etc.
Now that we’ve written our mod code (the mod lifecycle class, the system, and a patch), it’s time to compile it and try it in the game.
Build the DLL:
-
In Visual Studio, select Build Solution. If the references and paths are set correctly, it should compile to a DLL (and possibly a
.pdb
if debug symbols are on). The default assembly name might be something like X2-Example-Mod.dll if you used the template without renaming – you can change the assembly name in project properties to something like HelloWorldMod.dll. -
Thanks to the
ModInstanceFolder
we set inDirectory.Build.props
, the built DLL should copy automatically to the target mod folder (e.g.My Games\Xenonauts 2\Mods\HelloWorldMod\
). If not, locate the DLL (e.g. inbin\Debug
) and copy it manually to your mod folder inassembly\common\
.
Create a Mod Manifest:
Each mod needs a manifest JSON file so the game recognizes it as a content pack. Create a file named manifest.json in your mod folder (same folder as the DLL). Use the template below, or copy from an example mod and edit it:
{
"version": "0.0.4",
"asset": {
"Name": "Hello World Mod",
"UID": "123e4567-e89b-12d3-a456-426614174000",
"Description": "A simple example mod that logs a greeting when the game starts.",
"Author": "YourName",
"Website": "",
"Tags": [ "Mod" ],
"Version": "1.0.0",
"$type": "Common.Content.DataStructures.ContentPackManifest"
},
"$type": "Common.Content.DataStructures.VersionedAsset"
}
Fill in the Name, Description, Author, etc. The UID should be a unique identifier (a GUID) for your mod – you can generate one using an online GUID generator or a Visual Studio tool. It’s important that each mod has a unique UID. The other fields like "version": "0.0.4"
and the $type
lines should be left as in the template (they relate to the content pack format and version). The manifest tells the game about your mod but does not list the DLL explicitly; the game will automatically load any assemblies in the mod folder.
Your mod folder should now contain at least:
-
manifest.json (as above)
-
YourMod.dll (the compiled assembly of your mod code)
-
(Optionally, any other assets your mod might include, but for our code-only mod, that’s it)
Enable the Mod in Game:
-
Launch Xenonauts 2. From the main menu, go to the Mods menu (the Mod Manager).
-
You should see “Hello World Mod” (or whatever Name you gave) in the list. Enable it (check it or toggle it on). If the mod isn’t listed, double-check that you placed the folder in the correct directory. By default, user mods should live in your
%USERPROFILE%\Documents\My Games\Xenonauts 2\Mods\
directory (the game’s external mods folder). The manifest’s presence is what causes the game to discover the mod. -
If prompted, restart the game or return to main menu to apply changes. (Enabling the mod in the menu will typically load it immediately, but a restart ensures everything is fresh.)
Once the mod is enabled, the game will load your assembly. The sequence (as derived from the code) is: the mod loader reads your manifest, loads your DLL, applies Harmony patches, and calls Create()
on your IModLifecycle
class. This should trigger our log messages.
Test the Mod:
-
When the game starts up (after you enable the mod), look at the game’s log output. You should see “[HelloMod] Mod created and loaded!” from our
Create()
method. Our Harmony postfix onXenonautsMain.Start
should also print “Hello from the mod!” to the Unity console/log when the main menu appears. -
Start a new game (or load a save) to go to the Strategy (Geoscape) screen. During the loading of the geoscape, our
OnWorldCreate
will run. The log should show “HelloWorldSystem added to Strategy world.” and once the world finishes initializing, ourHelloWorldSystem
will catch the post-init event and log “World is initialized!” as we coded. All these messages confirm the mod is functioning. -
If you start a Ground Combat mission, you won’t see the hello message, because we only injected the system for Strategy. That’s intentional in our example (we filtered by section), but you could easily support other sections by adjusting the code.
Congratulations – you’ve made your first Xenonauts 2 code mod! 🎉
In this tutorial, we created a very simple mod that demonstrates the fundamentals of Xenonauts 2 code modding:
-
We used the
IModLifecycle
interface to tie into the game’s mod loading process and register our content. -
We created a custom System and subscribed to a game event using Artitas’s
[Subscriber]
attribute to react during gameplay. -
We wrote a Harmony patch to illustrate how to hook into existing game methods.
-
We built and enabled the mod in-game, verifying it via log messages.
From here, you can expand on these concepts:
-
Use different game events (check the Artitas.Core.Events namespace for many event types) or create your own events and fire them via
world.HandleEvent(...)
orworld.QueueEvent(...)
. -
Add prefix/postfix patches to alter game logic – for example, adjust damage calculations, UI behavior, etc. (The Harmony library and Xenonauts 2 code are your reference – always test patches thoroughly.)
-
Integrate new Components or data: The Artitas ECS allows you to define Components and add them to entities, then Systems can act on those. Your mod could introduce new gameplay mechanics this way.
-
Package multiple classes or even assets with your mod. You can include new JSON content or images in the mod folder as needed; the manifest and ContentManager will handle them (beyond this tutorial’s scope, but the framework supports it).
For more information, you might check the official Harmony documentation (for advanced patching techniques), and the rest of this wiki for more in depth technical knowledge. The X2-Modding repository also contains other example mods (like the “basic_template_cover_mod”) which show simple gameplay data tweaks.