diff --git a/StarMap.Core/ModRepository/RuntimeMod.cs b/StarMap.Core/ModRepository/RuntimeMod.cs index 62bf7a8..59b8a2f 100644 --- a/StarMap.Core/ModRepository/RuntimeMod.cs +++ b/StarMap.Core/ModRepository/RuntimeMod.cs @@ -159,17 +159,18 @@ public bool InitializeMod(ModRegistry modRegistry, IPatchManager patchManager, I modRegistry.AddModMethod(ModId, attr, modObject, classMethod); break; case StarMapAllModsLoadedAttribute: - var allModsLoadedPatch = patchManager.RegisterStaticVoidPatch(IPatchService.PatchType.Postfix, this, typeof(KSA.ModLibrary).GetMethod(nameof(ModLibrary.LoadAll))!, + var allModsLoadedPatch = patchManager.RegisterStaticVoidPatch(IPatchService.PatchType.Postfix, this, typeof(KSA.ModLibrary).GetMethod(nameof(ModLibrary.LoadAll)), args => { - classMethod.Invoke(modObject, args); + // The callback takes no parameters, whatever LoadAll takes. + classMethod.Invoke(modObject, []); return true; }); Patches.Add(allModsLoadedPatch); break; case StarMapImmediateLoadAttribute: - var immediateLoadPatch = patchManager.RegisterInstanceVoidPatch(IPatchService.PatchType.Prefix, this, typeof(KSA.Mod).GetMethod(nameof(Mod.PrepareSystems))!, + var immediateLoadPatch = patchManager.RegisterInstanceVoidPatch(IPatchService.PatchType.Prefix, this, typeof(KSA.Mod).GetMethod(nameof(Mod.LoadSettings)), (args, mod) => { if (mod.Id.Equals(ModId)) diff --git a/StarMap.Core/Patches/PatchManager.cs b/StarMap.Core/Patches/PatchManager.cs index 9d91f2f..c6ee34e 100644 --- a/StarMap.Core/Patches/PatchManager.cs +++ b/StarMap.Core/Patches/PatchManager.cs @@ -9,10 +9,10 @@ namespace StarMap.Core.Patches { internal interface IPatchManager : IDisposable { - IDisposable RegisterInstancePatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.InstanceCallback callback); - IDisposable RegisterInstanceVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.InstanceVoidCallback callback); - IDisposable RegisterStaticPatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.StaticCallback callback); - IDisposable RegisterStaticVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.StaticVoidCallback callback); + IDisposable RegisterInstancePatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.InstanceCallback callback); + IDisposable RegisterInstanceVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.InstanceVoidCallback callback); + IDisposable RegisterStaticPatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.StaticCallback callback); + IDisposable RegisterStaticVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.StaticVoidCallback callback); } internal class PatchManager : IPatchManager @@ -34,25 +34,42 @@ public PatchManager(ILogger logger) _instance = this; } - public IDisposable RegisterInstancePatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.InstanceCallback callback) + public IDisposable RegisterInstancePatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.InstanceCallback callback) { + if (method is null) return MissingTarget(mod); return GetOrCreateMethodPatch(method).RegisterPatch(patchType, mod, new InstanceCallback(callback), _logger); } - public IDisposable RegisterInstanceVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.InstanceVoidCallback callback) + public IDisposable RegisterInstanceVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.InstanceVoidCallback callback) { + if (method is null) return MissingTarget(mod); return GetOrCreateMethodPatch(method).RegisterPatch(patchType, mod, new InstanceVoidCallback(callback), _logger); } - public IDisposable RegisterStaticPatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.StaticCallback callback) + public IDisposable RegisterStaticPatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.StaticCallback callback) { + if (method is null) return MissingTarget(mod); return GetOrCreateMethodPatch(method).RegisterPatch(patchType, mod, new StaticCallback(callback), _logger); } - public IDisposable RegisterStaticVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase method, IPatchService.StaticVoidCallback callback) + public IDisposable RegisterStaticVoidPatch(PatchType patchType, RuntimeMod? mod, MethodBase? method, IPatchService.StaticVoidCallback callback) { + if (method is null) return MissingTarget(mod); return GetOrCreateMethodPatch(method).RegisterPatch(patchType, mod, new StaticVoidCallback(callback), _logger); } + // A KSA update can remove a method StarMap hooks. Skipping the one callback keeps the loader + // and every other mod running. + private IDisposable MissingTarget(RuntimeMod? mod) + { + _logger.Warning($"StarMap - A hook target is missing from this KSA build; a callback of {mod?.ModId ?? "StarMap"} will not run"); + return new NoPatch(); + } + + private sealed class NoPatch : IDisposable + { + public void Dispose() { } + } + private MethodPatch GetOrCreateMethodPatch(MethodBase method) { if (!_patches.TryGetValue(method, out var methodPatch)) diff --git a/StarMap.Core/UI/ConfirmRestart/ConfirmRestart.cs b/StarMap.Core/UI/ConfirmRestart/ConfirmRestart.cs index 59d904b..4258d86 100644 --- a/StarMap.Core/UI/ConfirmRestart/ConfirmRestart.cs +++ b/StarMap.Core/UI/ConfirmRestart/ConfirmRestart.cs @@ -1,4 +1,5 @@ -using Brutal.GlfwApi; +using Brutal.Collections; +using Brutal.GlfwApi; using Brutal.ImGuiApi; using Brutal.VulkanApi; using Brutal.VulkanApi.Abstractions; diff --git a/docs/modding/lifecycle.md b/docs/modding/lifecycle.md index 70c9dcd..f1b930b 100644 --- a/docs/modding/lifecycle.md +++ b/docs/modding/lifecycle.md @@ -7,7 +7,7 @@ Mark the entry class with `[StarMapMod]`. StarMap creates one instance for the m | Attribute | Required method signature | Called | | --- | --- | --- | | `[StarMapBeforeMain]` | `void Method()` | After the mod is initialized and before KSA starts. | -| `[StarMapImmediateLoad]` | `void Method(KSA.Mod definingMod)` | Immediately before KSA prepares that mod's systems. | +| `[StarMapImmediateLoad]` | `void Method(KSA.Mod definingMod)` | When KSA registers that mod, before its settings and any content load. | | `[StarMapAllModsLoaded]` | `void Method()` | After KSA's `ModLibrary.LoadAll` completes. | | `[StarMapUnload]` | `void Method()` | When KSA unloads. Dispose mod-owned subscriptions and menu items here. | | `[StarMapBeforeGui]` | `void Method(double dt)` | Before KSA's GUI frame callback. |