Creating Your First Mod
Getting Started produces a mod that loads and writes one line to the log. This page explains the files generated by the template and extends the mod with a setting, a scene event handler and a Harmony patch.
Project File
<Project Sdk="Lustral.Sdk/0.1.0">
<PropertyGroup>
<LustralGame>Lethal Company</LustralGame>
<ModId>you.betterhud</ModId>
<ModName>BetterHud</ModName>
<Version>0.1.0</Version>
<Description></Description>
<Authors></Authors>
</PropertyGroup>
</Project>Sdk="Lustral.Sdk/0.1.0"makes this a Lustral mod project. The SDK locates the game, targets .NET 10 (IL2CPP) or .NET Standard 2.1 (Mono), references the game's assemblies, Lustral's API and HarmonyX, and deploys the mod to the game on every build. The SDK is a NuGet package: on the first build, .NET downloads the specified version from nuget.org and caches it, so it does not need to be installed separately. The template sets the version to that of the installed template; to move a project to a newer Lustral version, change the version in this attribute.ModId,ModName,Version,DescriptionandAuthorsdescribe the mod. The SDK writes them to the mod's manifest (mod.toml), which Lustral uses to identify the mod. The manifest is generated on every build and should not be edited directly.
The template's comments list further options; all properties are documented in SDK Properties.
Mod Class
using HarmonyLib;
using Lustral;
namespace BetterHud;
public sealed class ModEntry : Mod
{
internal static ModEntry Instance { get; private set; } = null!;
public ModEntry() => Instance = this;
public override void OnLoad()
{
Harmony.PatchAll(typeof(ModEntry).Assembly);
Events.SceneLoaded += scene => Log.Info($"Scene loaded: {scene}");
Log.Info($"{Info.Name} {Info.Version} loaded in {Game.Name} {Game.Version}");
}
public override void OnGameReady()
{
// Unity's APIs can be used from this point on.
}
}Each mod contains exactly one class deriving from Mod. Lustral instantiates it when the game starts and calls its lifecycle methods:
OnLoadruns before the game's first scene, early enough to patch the game before its code runs. Unity is not initialized at this point; use it for patches, settings and event subscriptions.OnGameReadyruns after the first scene has loaded. Unity's APIs are available from this point.OnShutdown(not included in the template) runs when the game exits.
The class exposes Lustral's services through the properties Log, Harmony, Config, Events, Coroutines, Assets, Info, Game and Mods, described in Mod Lifecycle.
The static Instance property gives code outside the class, such as static Harmony patch classes, access to the mod: ModEntry.Instance.Log.Info("...").
Adding a Setting
Users configure mods through settings files. Define the settings as a class:
using System.ComponentModel;
namespace BetterHud;
public sealed class Settings
{
[Description("Write a line to the log for every scene the game loads.")]
public bool LogScenes { get; set; } = true;
}and bind it in OnLoad:
public Settings Settings { get; private set; } = null!;
public override void OnLoad()
{
Settings = Config.Bind<Settings>();
if (Settings.LogScenes)
Events.SceneLoaded += scene => Log.Info($"Scene loaded: {scene}");
}On the first launch, Lustral writes Lustral/config/you.betterhud.toml, with each description as a comment:
# Settings for BetterHud.
# Changes take effect the next time the game starts. New settings are added when the mod is updated.
# Write a line to the log for every scene the game loads.
log_scenes = trueSee Configuration for lists, enumerations and nested sections.
Patching the Game
Mods modify game behavior by patching the game's methods with Harmony, running code before, after or instead of the original method. To find a method to patch:
- Mono games: open
<game>_Data/Managed/Assembly-CSharp.dllin a decompiler such as ILSpy or dnSpy. - IL2CPP games: the game's code is native, but its classes and method signatures are available in the interop assemblies in
Lustral/interop. OpenAssembly-CSharp.dllfrom that folder to browse them. Method bodies are not included; a native disassembler or Cpp2IL can be used to inspect the implementation.
For example, given a game class HUDManager with a method DisplayTip(string header, string body), the following postfix runs after it (on IL2CPP games, the parameter type is Il2CppSystem.String; see Mono and IL2CPP):
using HarmonyLib;
namespace BetterHud;
[HarmonyPatch(typeof(HUDManager), nameof(HUDManager.DisplayTip))]
internal static class DisplayTipPatch
{
private static void Postfix(string header) => ModEntry.Instance.Log.Info($"The game showed a tip: {header}");
}Harmony.PatchAll in OnLoad applies every [HarmonyPatch] class in the mod. If the method name or parameters are incorrect, the build reports a warning (LUS0101).
Next Steps
- Mod Lifecycle: the lifecycle in detail and error handling.
- Harmony Patching: prefixes, postfixes,
__instanceand patching on IL2CPP. - Mono and IL2CPP: strings, collections and other differences on IL2CPP games.
- Debugging: using breakpoints while the game runs.