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`:
2. Example `MyMod.cs`:
3. Example `UniverseDataPatch.cs`:
4. Example `UncompressedSavePathc.cs`:
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: