Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions StarMap.API/SaveAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.IO;
using System.Reflection;

namespace StarMap.API
{
/// <summary>
/// Methods marked with this attribute will be called after KSA has written a save, including an
/// overwrite and a save-as. Not called when the write fails.
/// </summary>
/// <remarks>
/// Methods using this attribute must match the following signature:
///
/// <code>
/// public void MethodName(System.IO.DirectoryInfo saveDirectory);
/// </code>
///
/// Parameter requirements:
/// <list type="bullet">
/// <item>
/// <description>
/// <paramref name="saveDirectory"/> is the folder the save was written to.
/// </description>
/// </item>
/// </list>
///
/// Requirements:
/// <list type="bullet">
/// <item><description>Return type must be <see cref="void"/>.</description></item>
/// <item><description>Method must be an instance method (non-static).</description></item>
/// </list>
/// </remarks>
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);
}
}

/// <summary>
/// Methods marked with this attribute will be called after KSA has loaded a save and rebuilt the
/// world from it. Not called when the save could not be read.
/// </summary>
/// <remarks>
/// Methods using this attribute must match the following signature:
///
/// <code>
/// public void MethodName(System.IO.DirectoryInfo saveDirectory);
/// </code>
///
/// Parameter requirements:
/// <list type="bullet">
/// <item>
/// <description>
/// <paramref name="saveDirectory"/> is the folder the save was read from.
/// </description>
/// </item>
/// </list>
///
/// Requirements:
/// <list type="bullet">
/// <item><description>Return type must be <see cref="void"/>.</description></item>
/// <item><description>Method must be an instance method (non-static).</description></item>
/// </list>
/// </remarks>
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);
}
}
}
2 changes: 2 additions & 0 deletions StarMap.Core/ModRepository/ModsLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace StarMap.Core.ModRepository
{
internal interface IModsLoader : IDisposable
{
ModRegistry ModRegistry { get; }

void Init();
}

Expand Down
2 changes: 2 additions & 0 deletions StarMap.Core/ModRepository/RuntimeMod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ public bool InitializeMod(ModRegistry modRegistry, IPatchManager patchManager, I
{
case StarMapBeforeMainAttribute:
case StarMapUnloadAttribute:
case StarMapAfterSaveAttribute:
case StarMapAfterLoadAttribute:
modRegistry.AddModMethod(ModId, attr, modObject, classMethod);
break;
case StarMapAllModsLoadedAttribute:
Expand Down
104 changes: 104 additions & 0 deletions StarMap.Core/Patches/SaveLoadPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using HarmonyLib;
using KSA;
using StarMap.API;
using StarMap.API.Services;
using StarMap.Core.ModRepository;
using System.Reflection;

namespace StarMap.Core.Patches
{
internal interface ISaveLoadPatches
{
void Init();
}

internal class SaveLoadPatches : ISaveLoadPatches
{
private readonly IPatchManager _patchManager;
private readonly IModsLoader _modsLoader;

// UncompressedSave.Load returns early when the save cannot be read, so its postfix alone
// would report loads that never happened. OnGameLoaded runs only once the world is rebuilt.
private bool _worldRebuilt;

public SaveLoadPatches(IPatchManager patchManager, IModsLoader modsLoader)
{
_patchManager = patchManager;
_modsLoader = modsLoader;
}

public void Init()
{
// Every save, overwrite and save-as goes through Make.
var make = AccessTools.Method(typeof(UncompressedSave), nameof(UncompressedSave.Make), [typeof(string)]);
var load = AccessTools.Method(typeof(UncompressedSave), nameof(UncompressedSave.Load), Type.EmptyTypes);
var gameLoaded = AccessTools.Method(typeof(Program), nameof(Program.OnGameLoaded), Type.EmptyTypes);

if (make is null)
{
Console.WriteLine("StarMap - UncompressedSave.Make(string) not found; StarMapAfterSave will not be called");
}
else
{
_patchManager.RegisterStaticPatch<GameSave?>(IPatchService.PatchType.Postfix, null, make, AfterMake);
}

if (load is null || gameLoaded is null)
{
Console.WriteLine("StarMap - UncompressedSave.Load() or Program.OnGameLoaded() not found; StarMapAfterLoad will not be called");
return;
}

_patchManager.RegisterInstanceVoidPatch<UncompressedSave>(IPatchService.PatchType.Prefix, null, load, BeforeLoad);
_patchManager.RegisterInstanceVoidPatch<UncompressedSave>(IPatchService.PatchType.Postfix, null, load, AfterLoad);
_patchManager.RegisterStaticVoidPatch(IPatchService.PatchType.Postfix, null, gameLoaded, AfterGameLoaded);
}

private bool AfterMake(object[] args, ref GameSave? result)
{
if (result is UncompressedSave save)
Dispatch<StarMapAfterSaveAttribute>(save.Directory);

return true;
}

private bool BeforeLoad(object[] args, UncompressedSave save)
{
_worldRebuilt = false;
return true;
}

private bool AfterGameLoaded(object[] args)
{
_worldRebuilt = true;
return true;
}

private bool AfterLoad(object[] args, UncompressedSave save)
{
if (!_worldRebuilt)
return true;

_worldRebuilt = false;
Dispatch<StarMapAfterLoadAttribute>(save.Directory);
return true;
}

// These run inside KSA's save and load, so one mod's exception must not reach the game
// or stop the mods after it.
private void Dispatch<TAttribute>(DirectoryInfo directory) where TAttribute : StarMapMethodAttribute
{
foreach (var (_, modObject, method) in _modsLoader.ModRegistry.Get<TAttribute>())
{
try
{
method.Invoke(modObject, [directory]);
}
catch (Exception e)
{
Console.WriteLine($"StarMap - {typeof(TAttribute).Name} in {method.DeclaringType?.FullName} threw: {(e as TargetInvocationException)?.InnerException ?? e}");
}
}
}
}
}
3 changes: 2 additions & 1 deletion StarMap.Core/Services/CoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ internal interface ICoreService
void Init();
}

internal class CoreService(IStarMapPatches _starmapPatches, IModsLoader _modsLoader) : ICoreService
internal class CoreService(IStarMapPatches _starmapPatches, ISaveLoadPatches _saveLoadPatches, IModsLoader _modsLoader) : ICoreService
{
public void Init()
{
_starmapPatches.Init();
_saveLoadPatches.Init();
_modsLoader.Init();
}
}
Expand Down
1 change: 1 addition & 0 deletions StarMap.Core/StarMapCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext)
serviceCollection.AddSingleton<IPatchManager, PatchManager>();
serviceCollection.AddSingleton<IModsLoader, ModsLoader>();
serviceCollection.AddSingleton<IStarMapPatches, StarMapPatches>();
serviceCollection.AddSingleton<ISaveLoadPatches, SaveLoadPatches>();
serviceCollection.AddSingleton<IModMenu, ModMenu>();
serviceCollection.AddSingleton<IMediator, Mediator>();
serviceCollection.AddSingleton<IModServices, ModServices>();
Expand Down
2 changes: 2 additions & 0 deletions docs/modding/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Mark the entry class with `[StarMapMod]`. StarMap creates one instance for the m
| `[StarMapBeforeGui]` | `void Method(double dt)` | Before KSA's GUI frame callback. |
| `[StarMapAfterGui]` | `void Method(double dt)` | After KSA's GUI drawing pass. |
| `[StarMapAfterOnFrame]` | `void Method(double currentPlayerTime, double dtPlayer)` | After KSA's `Program.OnFrame`. |
| `[StarMapAfterSave]` | `void Method(System.IO.DirectoryInfo saveDirectory)` | After KSA writes a save, including an overwrite or a save-as. Not called if the write fails. |
| `[StarMapAfterLoad]` | `void Method(System.IO.DirectoryInfo saveDirectory)` | After KSA loads a save and rebuilds the world from it. Not called if the save could not be read. |

The callback methods return `void`. Use the exact parameter types and counts; methods with a different signature are ignored.

Expand Down