• ⚠️ Mod Release Rules now apply to this board.

    All mods must include a license, source code (for executable mods), and proper attribution.

    Read the full rules here before posting.

Save/Load mod data

SaphireFalcon

New Member
Dec 21, 2025
6
2
This is a small explanation how I got loading and saving data for my mods working with the in-game save/load button.
1. Create `public` data class that has XML serializable fields.
2. Have this data stored like `public static ModDataType data = new ModDataType();` in you mod main class, e.g. the one that has `[StarMapAllModsLoaded]` function.
3. Create `UniverseDataPatchWriteTo` class that uses Harmony to patch the `UniverseData.WriteTo` function.
4. Create `UncompressedSavePatchLoadFrom` class that uses Harmony to patch the `UncompressedSave.Load` function.

Notes:
Based of this code: https://gist.github.com/SafeShows/97ec8d3da8e64c7cc3ae220b3d64bef5

`UniverseData.LoadFrom` is not called when loading a save game. Likely that happens before patching happens. The table with the save-files in the save/load window is filled from `UncompressedSave.DrawInTable()`. That will call the `UncompressedSave.Load` function.
This gives a challenge, because that function has no arguments. Using Harmony's `__instance` it is however possible to get access to the `UncompressedSave` instance that populated the table. The path of where the `UncompressedSave` is stored is accessible through `__instance.Directory.FullName`.

When loading the xml file, the `Deserialize()` function will create a volatile `ModDataType` instance. That means if that class has types serialized such as lists and objects. These need to be deep copied. This can be done by creating `Clone` functions for each type.

Code files:
1. Example `ModDataType.cs`:
C#:
using System.Xml.Serialization;

namespace MyMod  // Update
{
    public class ModDataType
    {
        // XML serializable fields
        [XmlElement("myint")]
        public int MyInt { get; set; } = 42;

        // etc.

        private static XmlSerializer DataXmlSerializer = new XmlSerializer(typeof(ModDataType));

        // XML serialization needs a public constructor
        public ModDataType() { }

        // Load data from save file (pointed at by `UncompressedSave.Directory`)
        public void LoadFrom(string savePath)
        {
            string filePathXmlFile = Path.Combine(uncompressedSave.Directory.FullName, "mymod.xml");
            if (!File.Exists(filePathXmlFile))
            {
                throw new NullReferenceException("My mod file '" + filePathXmlFile + "' does not exist");
            }

            StreamReader streamReader = new StreamReader(filePathXmlFile);
            if (!(DataXmlSerializer.Deserialize(streamReader) is ModDataType loadedData))
            {
                streamReader.Close();
                throw new NullReferenceException("loaded data is null");
            }

            // Copy data, for referencable classes make sure to create deep copy!
            this.MyInt = loadedData.MyInt;

            streamReader.Close();
        }

        // Write data to save file
        public void WriteTo(DirectoryInfo directory)
        {
            XmlHelper.SerializeWithoutNaN(_contractManagerDataXmlSerializer, this, Path.Combine(directory.FullName, "mymod.xml"));
        }
    }
}

2. Example `MyMod.cs`:
C#:
namespace MyMod
{
[StarMapMod]
public class MyMod
{
    public static ModDataType data = new ModDataType();
   
    [StarMapAllModsLoaded]
    public void OnFullyLoaded()
    {
        Console.WriteLine("OnFullyLoaded");
        Patches.UniverseDataPatchWriteTo.Patch();
        Patches.UncompressedSavePatchLoad.Patch();
    }

    [StarMapUnload]
    public void Unload()
    {
        Patches.UniverseDataPatchWriteTo.Unload();
        Patches.UncompressedSavePatchLoad.Unload();
    }
}
}

3. Example `UniverseDataPatch.cs`:
C#:
using HarmonyLib;
using KSA;

namespace Patches
{
    [HarmonyPatch]
    internal static class UniverseDataPatchWriteTo
    {
        private static Harmony? _harmony = new Harmony("MyMod"); // Update to your mod name in mod.toml

        public static void Patch()
        {
            Console.WriteLine("Patching UniverseData...");
            _harmony?.PatchAll(typeof(UniverseDataPatchWriteTo).Assembly);
        }

        public static void Unload()
        {
            _harmony?.UnpatchAll(_harmony.Id);
            _harmony = null;
        }

        [HarmonyPatch(typeof(KSA.UniverseData), nameof(KSA.UniverseData.WriteTo), typeof(DirectoryInfo))]
        [HarmonyPostfix]
        public static void WriteToPostfix(DirectoryInfo directory)
        {
            Console.WriteLine("UniverseDataPatchWriteTo.WriteToPostfix(directory)");
            ContractManager.data.WriteTo(directory);
        }
    }
}

4. Example `UncompressedSavePathc.cs`:
C#:
using HarmonyLib;
using KSA;

namespace Patches
{
    [HarmonyPatch]
    internal static class UncompressedSavePatchLoad
    {
        private static Harmony? _harmony = new Harmony("MyMod"); // Update to your mod name in mod.toml

        public static void Patch()
        {
            Console.WriteLine("Patching UncompressedSave...");
            _harmony?.PatchAll(typeof(UncompressedSavePatchLoad).Assembly);
        }

        public static void Unload()
        {
            _harmony?.UnpatchAll(_harmony.Id);
            _harmony = null;
        }

        [HarmonyPatch(typeof(KSA.UncompressedSave), nameof(KSA.UncompressedSave.Load))]
        [HarmonyPostfix]
        public static void LoadPostfix(ref KSA.UncompressedSave __instance)
        {
            Console.WriteLine("UncompressedSavePatchLoad.LoadPostfix()");
            ContractManager.data.LoadFrom(__instance.Directory.FullName);
        }
    }
}
 
Last edited: