From a9b3baab8b672c312164537c845292ef4faeb849 Mon Sep 17 00:00:00 2001 From: Laurens de Voogd Date: Thu, 6 Aug 2026 18:36:31 +0200 Subject: [PATCH 1/2] Add StarMapAfterSave and StarMapAfterLoad hooks A mod that keeps per-save state has no way to be told when a save happens. The only signal available today is polling the save file's timestamp, which leaves a window: save and load inside it and the mod writes nothing for that save, then writes over it once the poll finally fires. Shortening the interval narrows the window without closing it. Both hooks pass the save's DirectoryInfo, because a save is a directory and what a mod usually wants is to put a file beside universe.xml -- so it is deleted, copied and renamed along with the save it belongs to. Patched on UniverseData.WriteTo(DirectoryInfo) and UniverseData.LoadFrom, both public. WriteTo is overloaded, so the patch names the argument type. Verified against KSA 2026.8.5.5168; the repository pins 2026.7.6.4939 for its reference package, which I could not restore here, so the method shapes are worth a second look against that build. --- StarMap.API/README.md | 28 ++++++++ StarMap.API/SaveAttributes.cs | 77 +++++++++++++++++++++ StarMap.Core/Patches/UniverseDataPatcher.cs | 47 +++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 StarMap.API/SaveAttributes.cs create mode 100644 StarMap.Core/Patches/UniverseDataPatcher.cs diff --git a/StarMap.API/README.md b/StarMap.API/README.md index 0a90ade..41880b9 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -220,3 +220,31 @@ Methods marked with this attribute will be called after `KSA.Program.OnFrame` is [StarMapAfterOnFrame] public void ModMethod(double currentPlayerTime, double dtPlayer); ``` + +#### StarMapAfterSave + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called after KSA has written a save, with the directory +it was written to. A mod that keeps per-save state can write its own file beside `universe.xml`. + +```csharp +[StarMapAfterSave] +public void ModMethod(System.IO.DirectoryInfo saveDirectory); +``` + +#### StarMapAfterLoad + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called after KSA has loaded a save, with the directory +it was read from. + +```csharp +[StarMapAfterLoad] +public void ModMethod(System.IO.DirectoryInfo saveDirectory); +``` diff --git a/StarMap.API/SaveAttributes.cs b/StarMap.API/SaveAttributes.cs new file mode 100644 index 0000000..706941b --- /dev/null +++ b/StarMap.API/SaveAttributes.cs @@ -0,0 +1,77 @@ +using System.IO; +using System.Reflection; + +namespace StarMap.API +{ + /// + /// Methods marked with this attribute will be called after KSA has written a save. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(System.IO.DirectoryInfo saveDirectory); + /// + /// + /// Parameter requirements: + /// + /// + /// + /// is the folder the save was written to, so a mod can + /// put its own file beside universe.xml. + /// + /// + /// + /// + /// Requirements: + /// + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public sealed class StarMapAfterSaveAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 1 && + method.GetParameters()[0].ParameterType == typeof(DirectoryInfo); + } + } + + /// + /// Methods marked with this attribute will be called after KSA has loaded a save. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(System.IO.DirectoryInfo saveDirectory); + /// + /// + /// Parameter requirements: + /// + /// + /// + /// is the folder the save was read from, so a mod can + /// read its own file from beside universe.xml. + /// + /// + /// + /// + /// Requirements: + /// + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public sealed class StarMapAfterLoadAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 1 && + method.GetParameters()[0].ParameterType == typeof(DirectoryInfo); + } + } +} diff --git a/StarMap.Core/Patches/UniverseDataPatcher.cs b/StarMap.Core/Patches/UniverseDataPatcher.cs new file mode 100644 index 0000000..b4a9110 --- /dev/null +++ b/StarMap.Core/Patches/UniverseDataPatcher.cs @@ -0,0 +1,47 @@ +using System.IO; +using HarmonyLib; +using KSA; +using StarMap.API; + +namespace StarMap.Core.Patches +{ + /// + /// Tells mods when a save is written or read, and where. + /// + /// A save is a directory, so a mod that keeps per-save state can put a file beside + /// universe.xml. Without a hook the only way to notice is to poll that file's timestamp, + /// which leaves a window: save and load inside it and the mod writes nothing for that save, + /// then writes over it afterwards. + /// + [HarmonyPatch(typeof(UniverseData))] + internal static class UniverseDataPatcher + { + private const string WriteToMethodName = "WriteTo"; + private const string LoadFromMethodName = "LoadFrom"; + + // WriteTo is overloaded; the DirectoryInfo one is the save on disk. + [HarmonyPatch(WriteToMethodName, [typeof(DirectoryInfo)])] + [HarmonyPostfix] + public static void AfterWriteTo(DirectoryInfo directory) + { + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; + + foreach (var (_, @object, method) in methods) + { + method.Invoke(@object, [directory]); + } + } + + [HarmonyPatch(LoadFromMethodName)] + [HarmonyPostfix] + public static void AfterLoadFrom(UncompressedSave uncompressedSave) + { + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; + + foreach (var (_, @object, method) in methods) + { + method.Invoke(@object, [uncompressedSave.Directory]); + } + } + } +} From eacf529c6ff14bf8c6234ff53a8abc4e3e407000 Mon Sep 17 00:00:00 2001 From: Laurens de Voogd Date: Thu, 6 Aug 2026 18:39:54 +0200 Subject: [PATCH 2/2] Trim the comments to what the code needs The motivation for the hooks belongs in the pull request, not in the source. --- StarMap.API/SaveAttributes.cs | 6 ++---- StarMap.Core/Patches/UniverseDataPatcher.cs | 7 +------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/StarMap.API/SaveAttributes.cs b/StarMap.API/SaveAttributes.cs index 706941b..6f8bb70 100644 --- a/StarMap.API/SaveAttributes.cs +++ b/StarMap.API/SaveAttributes.cs @@ -17,8 +17,7 @@ namespace StarMap.API /// /// /// - /// is the folder the save was written to, so a mod can - /// put its own file beside universe.xml. + /// is the folder the save was written to. /// /// /// @@ -53,8 +52,7 @@ public override bool IsValidSignature(MethodInfo method) /// /// /// - /// is the folder the save was read from, so a mod can - /// read its own file from beside universe.xml. + /// is the folder the save was read from. /// /// /// diff --git a/StarMap.Core/Patches/UniverseDataPatcher.cs b/StarMap.Core/Patches/UniverseDataPatcher.cs index b4a9110..4bae1ee 100644 --- a/StarMap.Core/Patches/UniverseDataPatcher.cs +++ b/StarMap.Core/Patches/UniverseDataPatcher.cs @@ -6,12 +6,7 @@ namespace StarMap.Core.Patches { /// - /// Tells mods when a save is written or read, and where. - /// - /// A save is a directory, so a mod that keeps per-save state can put a file beside - /// universe.xml. Without a hook the only way to notice is to poll that file's timestamp, - /// which leaves a window: save and load inside it and the mod writes nothing for that save, - /// then writes over it afterwards. + /// Tells mods when a save is written or read, and which directory it is in. /// [HarmonyPatch(typeof(UniverseData))] internal static class UniverseDataPatcher