Migrating from stock KSP2

There have been many significant changes to SpaceWarp and the general modding architecture from stock KSP2 to KSP2 Redux, and old code mods will not work without some modifications. This page serves as a general overview of the changes and how to upgrade your mods.

Overview

  • BepInEx removal: BepInEx has been completely removed. Equivalents for most of its functionality are now located in the ReduxLib or SpaceWarp2 namespaces.

  • Harmony version: HarmonyX has been replaced with Harmony2. A CreateAndPatchAll utility method is now provided within the mod classes.

  • Part modules: Classes extending PartComponentModule now need to explicitly subscribe to updates if they implement the OnUpdate method.

  • API cleanup: Deprecated APIs from SpaceWarp 1 have been removed. A legacy package for porting old mods is available at https://github.com/KSP2Community/SpaceWarpLegacy, though it is not recommended for new projects.

  • Namespace changes: The namespace SpaceWarp has been replaced with SpaceWarp2. Additionally, namespaces previously named as SpaceWarp.API.<Module> are now named SpaceWarp2.<Module>.API.

  • Campaign features: An installed mod is not on in all campaigns. Each campaign has its own feature set of features. If the default feature of a mod is off in a campaign, the mod must do nothing in that campaign. See Mods that are off in a campaign.

BepInEx API

BaseUnityPlugin and BaseSpaceWarpPlugin

The base mod classes BaseUnityPlugin and BaseSpaceWarpPlugin have been replaced with 3 classes, along with an interface that can be used to define more.

For all of these types, the mod lifecycle virtual methods from SpaceWarp 1 are the same, so OnPreInitialized, OnInitialized, and OnPostInitialized remain the same with the same semantics.

SpaceWarp2.API.Mods.ISpaceWarpMod

This is the base mod interface that can be used to define more mod types if desired. It provides three common properties:

  • **SWLogger** - logging interface for your mod,

  • **SWConfiguration** - adding and reading mod configuration values,

  • **SWMetadata** - provides metadata about your mod.

SpaceWarp2.API.Mods.GeneralMod

This is the recommended mod class for mods that don’t need the Unity lifecycle (e.g. for mods that only run some initialization code or provide part component classes, and don’t need Update or FixedUpdate).

SpaceWarp2.API.Mods.MonoBehaviourMod

This is the closest equivalent to BaseUnityPlugin. It is an ISpaceWarpMod that is also a MonoBehaviour, and as such, has the entire Unity component lifecycle. It gets instantiated as soon as possible upon loading the game.

Redux.ExtraModTypes.KerbalMod

This is the closest equivalent to BaseSpaceWarpPlugin. It includes Game, Assets, Messages and CFXSystem accessors in addition to everything provided by MonoBehaviourMod.

Mods that are off in a campaign

Each Redux campaign uses a feature set of campaign features. Each mod has a default feature. This feature is on when the mod is on.

A mod that is off in the loaded campaign is still loaded:

  • Its assembly loads.

  • OnPreInitialized, OnInitialized and OnPostInitialized run as before.

For a mod that is off, Redux disables its patches, assets, campaign hooks and app bar buttons. Code that your mod runs directly continues to run, for example Harmony patches.

All three base classes supply these members:

  • Enabled is true while the default feature of your mod is on in the active feature set. Check it before code that must run only while your mod is on.

  • OnCampaignLoaded() and OnCampaignUnloading() run only for campaigns that have your mod on. Set up per-campaign behaviour in OnCampaignLoaded(). Remove it in OnCampaignUnloading().

  • [Feature("Id")] on a bool field makes the field show if one of the features of your mod is on.

For all of the members, see Code modding guides.

Logging

The BepInEx logging system has been replaced with the ReduxLib.Logging namespace. To access your mod’s provided logger, use the SWLogger property in your mod class. To create a new logger, you can call ReduxLib.ReduxLib.GetLogger(name).

Log output from here goes to redux.log in the base folder of the install. There is an option in the game’s Settings → Mods → Redux to also mirror the output into Unity’s logs (and console).

Configuration

The BepInEx configuration system has been replaced with the ReduxLib.Configuration namespace. It functions slightly differently than BepInEx’s, the following snippet illustrates a general use case:

/* This is a set of code snippets for defining a config value */

// Define a config section and value in your mod class.
// The "string" type can be replaced by any type that can be serialized to a JSON
[ConfigSection("My Section", loc: "MyMod/Settings/MySection")]
[ConfigValue(
    "Some Config Name",
    "Some Config Description",
    nameLoc: "MyMod/Settings/Name/SomeConfigValue",
    descLoc: "MyMod/Settings/Desc/SomeConfigValue"
)]
public ConfigValue<string> SomeConfigValue = new ConfigDescription<string>("DEFAULT VALUE");

// If you only need a one-way binding (no callback on value change, etc), you can
// directly use the raw type for the config value object
[ConfigValue(
    "Some Other Config Name",
    "Some Other Config Description",
    nameLoc: "MyMod/Settings/Name/SomeOtherConfigValue",
    descLoc: "MyMod/Settings/Desc/SomeOtherConfigValue"
)]
public bool SomeOtherConfigValue = true;

// ...

public override void OnPreInitialized()
{
    // We bind the configs defined in the class to the SWConfiguration object
    // (inherited from the Mod base class)
    SWConfiguration.Bind(this);

    // Register an action to call when the value changes
    SomeConfigValue.RegisterCallback(SomeConfigCallback);
}

private static void SomeConfigCallback(string oldValue, string newValue)
{
    SWLogger.LogInfo($"I changed from {oldValue} to {newValue}");
}

Keybinds

New in KSP2 Redux, ReduxLib provides classes for binding KeyboardShortcuts and Keybinds. When you create config values with these types, they will be automatically added into the Settings menu.

ReduxLib.Input.Keybind

Represents a single key action

ReduxLib.Input.KeyboardShortcut

Represents a keyboard shortcut consisting of modifier(s) and a single key

HarmonyX

HarmonyX has been replaced with a modified version of Harmony2 in order to allow it to run in the Unity Editor. Unfortunately, this means that some features were removed.

CreateHarmonyAndPatchAll is a helper method provided in most mod classes, which replaces Harmony.CreateAndPatchAll. It will create a harmony instance and apply all patches from your mod’s assembly.

Part Modules

The OnUpdate method in classes extending KSP.Sim.impl.PartComponentModule no longer runs automatically for performance reasons. If you need it to run, you will now need to explicitly register and unregister for updates using KSP.Sim.impl.PartComponent’s RegisterModuleForUpdates and UnregisterModuleForUpdates methods. Following is an example:

public class PartComponentModule_MyModule : PartComponentModule
{
    public override void OnStart(double universalTime)
    {
        Part.RegisterModuleForUpdates(this);
    }
    
    public override void OnUpdate(double universalTime, double deltaUniversalTime)
    {
        // My update code will run with this setup
    }
    
    public override void OnShutdown()
    {
        Part.UnregisterModuleForUpdates(this);
    }
}

Deprecations

The following APIs have been deprecated and removed in Redux:

  • The AssetManager API from SpaceWarp 1 has been deprecated and removed in favor of Unity’s Addressables, which the Redux SDK can automatically build.

  • The IMGUI API and skin from SpaceWarp 1 has been deprecated and removed in favor of UI Toolkit, which the Unity Editor has a convenient editor for. The UitkForKsp2.Unity Unity package provides a KSP2 theme, fonts and other assets.

If you need either of the two above deprecated APIs, a legacy port is available in the SpaceWarpLegacy Unity package.

  • The Lua modding support from SpaceWarp 1 was completely removed without any replacements, as it never got used by any mods.

Namespace Changes

The following namespaces have been renamed or moved:

  • SpaceWarp → SpaceWarp2

  • SpaceWarp.API.Configuration → ReduxLib.Configuration

  • SpaceWarp.API.Loading → SpaceWarp2.API.Loading

  • SpaceWarp.API.Logging → ReduxLib.Logging

  • SpaceWarp.API.Mods → SpaceWarp2.API.Mods

  • SpaceWarp.API.Parts → SpaceWarp2.API.Parts

  • SpaceWarp.API.SaveGameManager → SpaceWarp2.API.SaveGameManager

  • SpaceWarp.API.Versions → SpaceWarp2.API.Versions

  • SpaceWarp.Patching.LoadingActions → SpaceWarp2.API.Loading.LoadingActions

  • SpaceWarp.API.Game.Messages → Redux.Messages

  • SpaceWarp.API.Game.Extensions → integrated into the target classes

  • SpaceWarp.API.Game.Waypoints → SpaceWarp2.Game.API.Waypoints

  • SpaceWarp.API.Messaging → ReduxLib.Messaging

  • SpaceWarp.API.Sound → SpaceWarp2.Sound.API

  • SpaceWarp.API.UI → SpaceWarp2.UI.API

UI Toolkit

From Unity 6.6 on, creating custom controls using the UxmlFactory and UxmlTraits pattern is no longer possible, so you will need to migrate to the newer way of using [UxmlElement] and [UxmlAttribute] attributes. For more info, see Unity’s documentation.

There have also been many changes made to the KerbalUI theme, so make sure your UI still looks good with these new styles.

Example before & after

Before:

using System.Reflection;
using BepInEx;
using JetBrains.Annotations;
using SpaceWarp;
using SpaceWarp.API.Assets;
using SpaceWarp.API.Mods;
using SpaceWarp.API.UI.Appbar;
using SpaceWarpModUI.UI;
using UitkForKsp2.API;
using UnityEngine;
using UnityEngine.UIElements;

namespace SpaceWarpModUI;

[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
[BepInDependency(SpaceWarpPlugin.ModGuid, SpaceWarpPlugin.ModVer)]
public class SpaceWarpModUIPlugin : BaseSpaceWarpPlugin
{
    // Useful in case some other mod wants to use this mod a dependency
    [PublicAPI] public const string ModGuid = MyPluginInfo.PLUGIN_GUID;
    [PublicAPI] public const string ModName = MyPluginInfo.PLUGIN_NAME;
    [PublicAPI] public const string ModVer = MyPluginInfo.PLUGIN_VERSION;

    /// Singleton instance of the plugin class
    [PublicAPI] public static SpaceWarpModUIPlugin Instance { get; set; }

    // AppBar button IDs
    internal const string ToolbarFlightButtonID = "BTN-SpaceWarpModUIFlight";
    internal const string ToolbarOabButtonID = "BTN-SpaceWarpModUIOAB";
    internal const string ToolbarKscButtonID = "BTN-SpaceWarpModUIKSC";

    /// <summary>
    /// Runs when the mod is first initialized.
    /// </summary>
    public override void OnInitialized()
    {
        base.OnInitialized();

        Instance = this;

        // Load all the other assemblies used by this mod
        LoadAssemblies();

        // Load the UI from the asset bundle
        var myFirstWindowUxml = AssetManager.GetAsset<VisualTreeAsset>(
            // The case-insensitive path to the asset in the bundle is composed of:
            // - The mod GUID:
            $"{ModGuid}/" +
            // - The name of the asset bundle:
            "SpaceWarpModUI_ui/" +
            // - The path to the asset in your Unity project (without the "Assets/" part)
            "ui/myfirstwindow/myfirstwindow.uxml"
        );

        // Create the window options object
        var windowOptions = new WindowOptions
        {
            // The ID of the window. It should be unique to your mod.
            WindowId = "SpaceWarpModUI_MyFirstWindow",
            // The transform of parent game object of the window.
            // If null, it will be created under the main canvas.
            Parent = null,
            // Whether or not the window can be hidden with F2.
            IsHidingEnabled = true,
            // Whether to disable game input when typing into text fields.
            DisableGameInputForTextFields = true,
            MoveOptions = new MoveOptions
            {
                // Whether or not the window can be moved by dragging.
                IsMovingEnabled = true,
                // Whether or not the window can only be moved within the screen bounds.
                CheckScreenBounds = true
            }
        };

        // Create the window
        var myFirstWindow = Window.Create(windowOptions, myFirstWindowUxml);
        // Add a controller for the UI to the window's game object
        var myFirstWindowController = myFirstWindow.gameObject.AddComponent<MyFirstWindowController>();

        // Register Flight AppBar button
        Appbar.RegisterAppButton(
            ModName,
            ToolbarFlightButtonID,
            AssetManager.GetAsset<Texture2D>($"{ModGuid}/images/icon.png"),
            isOpen => myFirstWindowController.IsWindowOpen = isOpen
        );

        // Register OAB AppBar Button
        Appbar.RegisterOABAppButton(
            ModName,
            ToolbarOabButtonID,
            AssetManager.GetAsset<Texture2D>($"{ModGuid}/images/icon.png"),
            isOpen => myFirstWindowController.IsWindowOpen = isOpen
        );

        // Register KSC AppBar Button
        Appbar.RegisterKSCAppButton(
            ModName,
            ToolbarKscButtonID,
            AssetManager.GetAsset<Texture2D>($"{ModGuid}/images/icon.png"),
            () => myFirstWindowController.IsWindowOpen = !myFirstWindowController.IsWindowOpen
        );
    }

    /// <summary>
    /// Loads all the assemblies for the mod.
    /// </summary>
    private static void LoadAssemblies()
    {
        // Load the Unity project assembly
        var currentFolder = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory!.FullName;
        var unityAssembly = Assembly.LoadFrom(Path.Combine(currentFolder, "SpaceWarpModUI.Unity.dll"));
        // Register any custom UI controls from the loaded assembly
        CustomControls.RegisterFromAssembly(unityAssembly);
    }
}

After:

using JetBrains.Annotations;
using Redux.ExtraModTypes;
using SpaceWarp2.UI.API.Appbar;
using SpaceWarpModUI.UI;
using UitkForKsp2.API;
using UnityEngine;
using UnityEngine.UIElements;

namespace SpaceWarpModUI;

public abstract class SpaceWarpModUIPlugin : KerbalMod
{
    [PublicAPI] public const string ModGuid = "SpaceWarpModUI"; // ModGuid here
    [PublicAPI] public const string ModName = "SpaceWarp UI Mod"; 
    //[PublicAPI] public const string ModVer = MyPluginInfo.PLUGIN_VERSION;

    /// Singleton instance of the plugin class
    [PublicAPI] public static SpaceWarpModUIPlugin Instance { get; set; }

    // AppBar button IDs
    internal const string ToolbarFlightButtonID = "BTN-SpaceWarpModUIFlight";
    internal const string ToolbarOabButtonID = "BTN-SpaceWarpModUIOAB";
    internal const string ToolbarKscButtonID = "BTN-SpaceWarpModUIKSC";

    /// <summary>
    /// Runs before the game is loaded, as early as possible. 
    /// You would want to use this to register loading actions (like loading JSONs from addressables).
    /// </summary>

    public override void OnPreInitialized()
    {
        Debug.Log("Pre Initialized");
    }
    
    /// <summary>
    /// Runs after the game is loaded and after your mod assets are loaded.
    /// </summary>

    public override async void OnInitialized()
    {
        Debug.Log("Is Initializing"); // You can use Debug.Log to log things.
        Instance = this;

        // Start both asset operations before awaiting either one so that they
        // can load concurrently.
        var myFirstWindowUxmlHandle =
            Assets.LoadAssetAsync<VisualTreeAsset>(
                // The address given to the addressable asset:
                "/myfirstwindow/myfirstwindow.uxml"
            );

        var appbarIconHandle =
            Assets.LoadAssetAsync<Texture2D>(
                $"{ModGuid}/images/icon.png"
            );

        try
        {
            // Await the Tasks exposed by the Unity operation handles.
            // This does not block the Unity main thread.
            await Task.WhenAll(
                myFirstWindowUxmlHandle.Task,
                appbarIconHandle.Task
            );
        }
        catch (Exception exception)
        {
            Debug.LogError(
                $"{ModName}: An exception occurred while loading UI assets."
            );

            Debug.LogException(exception);
            return;
        }

        if (myFirstWindowUxmlHandle.Status !=
            AsyncOperationStatus.Succeeded)
        {
            Debug.LogError(
                $"{ModName}: Failed to load the window UXML asset. " +
                $"Status: {myFirstWindowUxmlHandle.Status}. " +
                $"Exception: {myFirstWindowUxmlHandle.OperationException}"
            );

            return;
        }

        if (appbarIconHandle.Status != AsyncOperationStatus.Succeeded)
        {
            Debug.LogError(
                $"{ModName}: Failed to load the AppBar icon. " +
                $"Status: {appbarIconHandle.Status}. " +
                $"Exception: {appbarIconHandle.OperationException}"
            );

            return;
        }

        // Both operations have completed successfully, so we can access Result now.
        var myFirstWindowUxml = myFirstWindowUxmlHandle.Result;
        var appbarIcon = appbarIconHandle.Result;

        // Create the window options object
        var windowOptions = new WindowOptions
        {
            // The ID of the window. It should be unique to your mod.
            WindowId = "SpaceWarpModUI_MyFirstWindow",
            // The transform of parent game object of the window.
            // If null, it will be created under the main canvas.
            Parent = null,
            // Whether the window can be hidden with F2.
            IsHidingEnabled = true,
            // Whether the UI will use the "Scale" option in the settings.
            UseStockScale = true,
            // Whether to disable game input when typing into text fields.
            DisableGameInputForTextFields = true,
            MoveOptions = new MoveOptions
            {
                // Whether the window can be moved by dragging.
                IsMovingEnabled = true,
                // Whether the window can only be moved within the screen bounds.
                CheckScreenBounds = true
            }
        };

        // Create the window
        var myFirstWindow = Window.Create(windowOptions, myFirstWindowUxml);
        // Add a controller for the UI to the window's game object
        var myFirstWindowController = myFirstWindow.gameObject.AddComponent<MyFirstWindowController>();

        // Register Flight AppBar button
        Appbar.RegisterAppButton(
            ModName,
            ToolbarFlightButtonID,
            appbarIcon,
            isOpen => myFirstWindowController.IsWindowOpen = isOpen
        );

        // Register OAB AppBar Button
        Appbar.RegisterOABAppButton(
            ModName,
            ToolbarOabButtonID,
            appbarIcon,
            isOpen => myFirstWindowController.IsWindowOpen = isOpen
        );

        // Register KSC AppBar Button
        Appbar.RegisterKSCAppButton(
            ModName,
            ToolbarKscButtonID,
            appbarIcon,
            () => myFirstWindowController.IsWindowOpen = !myFirstWindowController.IsWindowOpen
        );
    }

    /// <summary>
    /// Runs after all mods have done their 2nd stage initialization.
    /// You would want to use this to interact with game systems, since they're guaranteed to be loaded on this stage.
    /// </summary>

    public override void OnPostInitialized()
    {
        Debug.Log("Post Initialized");
    }


    // You can create controls directly in Unity. No need for this method anymore.
    //private static void LoadAssemblies()
    //{
    //    // Load the Unity project assembly
    //    var currentFolder = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory!.FullName;
    //    var unityAssembly = Assembly.LoadFrom(Path.Combine(currentFolder, "SpaceWarpModUI.Unity.dll"));
    //    // Register any custom UI controls from the loaded assembly
    //    CustomControls.RegisterFromAssembly(unityAssembly);
    //}
}

For more examples you can look at some of the "Reduxed" mods we've made for Redux here: https://github.com/orgs/KSP2Community/repositories?type=all