diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs new file mode 100644 index 0000000..f5df406 --- /dev/null +++ b/StarMap.API/BaseAttributes.cs @@ -0,0 +1,108 @@ +using KSA; +using System.Reflection; + +namespace StarMap.API +{ + /// + /// Marks the main class for a StarMap mod. + /// Only attributes on methods within classes marked with this attribute will be considered. + /// + [AttributeUsage(AttributeTargets.Class)] + public class StarMapModAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public abstract class StarMapMethodAttribute : Attribute + { + public abstract bool IsValidSignature(MethodInfo info); + } + + /// + /// Methods marked with this attribute will be called immediately when the mod is loaded. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(KSA.Mod definingMod); + /// + /// + /// Parameter requirements: + /// + /// + /// + /// the KSA.Mod instance that is being loaded. + /// + /// + /// + /// + /// Requirements: + /// + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public class StarMapImmediateLoadAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 1 && + method.GetParameters()[0].ParameterType == typeof(Mod); + } + } + + /// + /// Methods marked with this attribute will be called when all mods are loaded. + /// This is to be used for when the mod has dependencies on other mods. + /// + /// + /// Methods using this attribute must follow this signature: + /// + /// + /// public void MethodName(); + /// + /// + /// Specifically: + /// + /// No parameters are allowed. + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public class StarMapAllModsLoadedAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 0; + } + } + + /// + /// Methods marked with this attribute will be called when KSA is unloaded + /// + /// + /// Methods using this attribute must follow this signature: + /// + /// + /// public void MethodName(); + /// + /// + /// Specifically: + /// + /// No parameters are allowed. + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public class StarMapUnloadAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 0; + } + } +} diff --git a/StarMap.API/IStarMapInterface.cs b/StarMap.API/IStarMapInterface.cs deleted file mode 100644 index 0d1d329..0000000 --- a/StarMap.API/IStarMapInterface.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StarMap.API -{ - public interface IStarMapInterface - { - } -} diff --git a/StarMap.API/IStarMapMod.cs b/StarMap.API/IStarMapMod.cs deleted file mode 100644 index 7bfe317..0000000 --- a/StarMap.API/IStarMapMod.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace StarMap.API -{ - public interface IStarMapMod : IStarMapInterface - { - bool ImmediateUnload { get; } - void OnImmediatLoad(); - void OnFullyLoaded(); - void Unload(); - } -} diff --git a/StarMap.API/IStarMapOnUi.cs b/StarMap.API/IStarMapOnUi.cs deleted file mode 100644 index 1b590ca..0000000 --- a/StarMap.API/IStarMapOnUi.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StarMap.API -{ - public interface IStarMapOnUi : IStarMapInterface - { - void OnBeforeUi(double dt); - void OnAfterUi(double dt); - } -} diff --git a/StarMap.API/OnGuiAttributes.cs b/StarMap.API/OnGuiAttributes.cs new file mode 100644 index 0000000..f2e17bb --- /dev/null +++ b/StarMap.API/OnGuiAttributes.cs @@ -0,0 +1,76 @@ +using System.Reflection; + +namespace StarMap.API +{ + /// + /// Methods marked with this attribute will be called before KSA starts creating its ImGui interface. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(double dt); + /// + /// + /// Parameter requirements: + /// + /// + /// + /// + /// + /// + /// + /// + /// Requirements: + /// + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public sealed class StarMapBeforeGuiAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 1 && + method.GetParameters()[0].ParameterType == typeof(double); + + } + } + + /// + /// Methods marked with this attribute will be called when KSA has finished creating its ImGui interface. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(double dt); + /// + /// + /// Parameter requirements: + /// + /// + /// + /// + /// + /// + /// + /// + /// Requirements: + /// + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public sealed class StarMapAfterGuiAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 1 && + method.GetParameters()[0].ParameterType == typeof(double); + + } + } +} diff --git a/StarMap.API/README.md b/StarMap.API/README.md new file mode 100644 index 0000000..edfc959 --- /dev/null +++ b/StarMap.API/README.md @@ -0,0 +1,16 @@ +# StarMap API + +This package provides the API for mods to interface with the [StarMap](https://github.com/StarMapLoader/StarMap) modloader. +The main class of the mod should be marked by the StarMapMod attribute. +Then methods within this class can be marked with any of the StarMapMethod attributes. +At the initialization of the mod within KSA, an instance of the StarMapMod is created, only the first class that has this attribute will be considered. +Any method within this class that has any of the attributes will used, so if two methods use StarMapImmediateLoad, both will be called. + +## Attributes + +- StarMapMod: Main attribute to mark the mod class +- StarMapImmediateLoad: Called immediatly when the mod is loaded in KSA +- StarMapAllModsLoaded: Called once all mods are loaded, can be used when this mod has a dependency on another mod +- StarMapUnload: Called when KSA is unloaded +- StarMapBeforeGui: Called just before KSA starts drawing its Ui +- StarMapAfterGui: Called after KSA has drawn its Ui diff --git a/StarMap.API/StarMap.API.csproj b/StarMap.API/StarMap.API.csproj index 125f4c9..0f34d76 100644 --- a/StarMap.API/StarMap.API.csproj +++ b/StarMap.API/StarMap.API.csproj @@ -6,4 +6,15 @@ enable + + + runtime + + + + + + runtime + + diff --git a/StarMap.Core/ModRepository/LoadedModRepository.cs b/StarMap.Core/ModRepository/LoadedModRepository.cs index 4d13ce9..c1e0b3a 100644 --- a/StarMap.Core/ModRepository/LoadedModRepository.cs +++ b/StarMap.Core/ModRepository/LoadedModRepository.cs @@ -1,5 +1,4 @@ -using HarmonyLib; -using KSA; +using KSA; using StarMap.API; using System.Reflection; using System.Runtime.Loader; @@ -9,29 +8,34 @@ namespace StarMap.Core.ModRepository internal class LoadedModRepository : IDisposable { private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly IEnumerable _registeredInterfaces = []; + private readonly Dictionary _registeredMethodAttributes = []; private readonly ModRegistry _mods = new(); public ModRegistry Mods => _mods; + private (string attributeName, StarMapMethodAttribute attribute)? ConvertAttributeType(Type attrType) + { + if ((Activator.CreateInstance(attrType) as StarMapMethodAttribute) is not StarMapMethodAttribute attrObject) return null; + return (attrType.Name, attrObject); + } + public LoadedModRepository(AssemblyLoadContext coreAssemblyLoadContext) { _coreAssemblyLoadContext = coreAssemblyLoadContext; - var baseInterface = typeof(IStarMapInterface); - Assembly starMapTypes = baseInterface.Assembly; + Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; - _registeredInterfaces = starMapTypes + _registeredMethodAttributes = coreAssembly .GetTypes() - .Where( - t => t.IsInterface && - baseInterface.IsAssignableFrom(t) && - t != baseInterface); - } - - private bool IsStarMapMod(Type type) - { - return typeof(IStarMapMod).IsAssignableFrom(type) && !type.IsInterface; + .Where(t => + typeof(StarMapMethodAttribute).IsAssignableFrom(t) && + t.IsClass && + !t.IsAbstract && + t.GetCustomAttribute()?.ValidOn.HasFlag(AttributeTargets.Method) == true + ) + .Select(ConvertAttributeType) + .OfType<(string attributeName, StarMapMethodAttribute attribute)>() + .ToDictionary(); } public void LoadMod(Mod mod) @@ -46,45 +50,54 @@ public void LoadMod(Mod mod) var modLoadContext = new ModAssemblyLoadContext(mod, _coreAssemblyLoadContext); var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = mod.Name }); - var modClass = modAssembly.GetTypes().FirstOrDefault(IsStarMapMod); + var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.IsDefined(typeof(StarMapModAttribute), inherit: false)); if (modClass is null) return; - if (modClass.CreateInstance() is not IStarMapInterface modObject) return; + var modObject = Activator.CreateInstance(modClass); + if (modObject is null) return; - foreach (var interfaceType in _registeredInterfaces) + var classMethods = modClass.GetMethods(); + var immediateLoadMethods = new List(); + + foreach (var classMethod in classMethods) { - if (interfaceType.IsAssignableFrom(modClass)) + var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); + foreach (var stringAttr in stringAttrs) { - _mods.Add(interfaceType, modObject); - } - } + var attr = _registeredMethodAttributes[stringAttr]; - if (modObject is not IStarMapMod starMapMod) return; + if (!attr.IsValidSignature(classMethod)) return; - starMapMod.OnImmediatLoad(); + if (attr.GetType() == typeof(StarMapModAttribute)) + { + immediateLoadMethods.Add(classMethod); + } + + _mods.Add(attr, modObject, classMethod); + } + } - if (starMapMod.ImmediateUnload) + foreach (var method in immediateLoadMethods) { - modLoadContext.Unload(); - return; + method.Invoke(modObject, [mod]); } - Console.WriteLine($"Loaded mod: {mod.Name}"); + Console.WriteLine($"StarMap - Loaded mod: {mod.Name}"); } public void OnAllModsLoaded() { - foreach (var mod in _mods.Get()) + foreach (var (_, @object, method) in _mods.Get()) { - mod.OnFullyLoaded(); + method.Invoke(@object, []); } } public void Dispose() { - foreach (var mod in _mods.Get()) + foreach (var (_, @object, method) in _mods.Get()) { - mod.Unload(); + method.Invoke(@object, []); } _mods.Dispose(); diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 1a7cabd..ffd9c0b 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -1,53 +1,42 @@ using StarMap.API; +using System.Reflection; namespace StarMap.Core.ModRepository { internal sealed class ModRegistry : IDisposable { - private readonly Dictionary> _map = new(); + private readonly Dictionary> _map = new(); - public void Add(Type iface, IStarMapInterface instance) + public void Add(StarMapMethodAttribute attribute, object @object, MethodInfo method) { - // --- type-safety checks --- - if (!typeof(IStarMapInterface).IsAssignableFrom(iface) || !iface.IsInterface) - throw new ArgumentException($"{iface} is not an interface inheriting {typeof(IStarMapInterface).Name}"); - - if (instance is null) - throw new ArgumentNullException(nameof(instance)); - - Type implType = instance.GetType(); - - if (!iface.IsAssignableFrom(implType)) - throw new ArgumentException( - $"{implType.Name} does not implement {iface.Name}" - ); + var attributeType = attribute.GetType(); // --- add instance --- - if (!_map.TryGetValue(iface, out var list)) + if (!_map.TryGetValue(attributeType, out var list)) { list = []; - _map[iface] = list; + _map[attributeType] = list; } - list.Add(instance); + list.Add((attribute, @object, method)); } - public IReadOnlyList Get() - where TInterface : IStarMapInterface + public IReadOnlyList<(StarMapMethodAttribute attribute, object @object, MethodInfo method)> Get() + where TAttribute : Attribute { - if (_map.TryGetValue(typeof(TInterface), out var list)) + if (_map.TryGetValue(typeof(TAttribute), out var list)) { - return list.Cast().ToList(); + return list.Cast<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>().ToList(); } - return Array.Empty(); + return Array.Empty<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>(); } - public IReadOnlyList Get(Type iface) + public IReadOnlyList<(StarMapMethodAttribute attribute, object @object, MethodInfo method)> Get(Type iface) { return _map.TryGetValue(iface, out var list) ? list - : Array.Empty(); + : Array.Empty<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>(); } public void Dispose() diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index 260fc94..69b79ca 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -13,11 +13,11 @@ internal static class ProgramPatcher [HarmonyPrefix] public static void BeforeOnDrawUi(double dt) { - var mods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; - foreach (var mod in mods) + foreach (var (_, @object, method) in methods) { - mod.OnBeforeUi(dt); + method.Invoke(@object, [dt]); } } @@ -25,11 +25,11 @@ public static void BeforeOnDrawUi(double dt) [HarmonyPostfix] public static void AfterOnDrawUi(double dt) { - var mods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; - foreach (var mod in mods) + foreach (var (_, @object, method) in methods) { - mod.OnAfterUi(dt); + method.Invoke(@object, [dt]); } } } diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 702b31a..0690665 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -24,9 +24,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) public void Init() { - _harmony.PatchAll(); - // Currently needed to force patch in release mode - Harmony.GetAllPatchedMethods(); + _harmony.PatchAll(typeof(StarMapCore).Assembly); } public void DeInit() diff --git a/StarMap.Types/LoaderConfig.cs b/StarMap.Types/LoaderConfig.cs index f7646b6..aede82b 100644 --- a/StarMap.Types/LoaderConfig.cs +++ b/StarMap.Types/LoaderConfig.cs @@ -1,10 +1,4 @@ -using StarMap.Types.Proto.IPC; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; +using System.Text.Json; namespace StarMap.Types { @@ -15,36 +9,36 @@ public bool TryLoadConfig() { if (!File.Exists("./StarMapConfig.json")) { - Console.WriteLine("Please fill the StarMapConfig.json and restart the program"); + Console.WriteLine("StarMap - Please fill the StarMapConfig.json and restart the program"); File.WriteAllText("./StarMapConfig.json", JsonSerializer.Serialize(new LoaderConfig(), new JsonSerializerOptions { WriteIndented = true })); return false; } - + var jsonString = File.ReadAllText("./StarMapConfig.json"); var config = JsonSerializer.Deserialize(jsonString); - + if (config is null) return false; - + if (string.IsNullOrEmpty(config.GameLocation)) { - Console.WriteLine("The 'GameLocation' property in StarMapConfig.json is either empty or points to a non-existing file."); + Console.WriteLine("StarMap - The 'GameLocation' property in StarMapConfig.json is either empty or points to a non-existing file."); return false; } - + string path = config.GameLocation; - + if (Directory.Exists(path)) { path = Path.Combine(path, "KSA.dll"); } - + if (!File.Exists(path)) { - Console.WriteLine("Could not find KSA.dll. Make sure the folder or file path is correct:"); + Console.WriteLine("StarMap - Could not find KSA.dll. Make sure the folder or file path is correct:"); Console.WriteLine(path); return false; } - + GameLocation = path; return true; } diff --git a/StarMap.Types/Pipes/PipeClient.cs b/StarMap.Types/Pipes/PipeClient.cs index c9d182a..6941bde 100644 --- a/StarMap.Types/Pipes/PipeClient.cs +++ b/StarMap.Types/Pipes/PipeClient.cs @@ -28,7 +28,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken) options: PipeOptions.Asynchronous); - Console.WriteLine($"Connecting to pipe {_pipeName}..."); + Console.WriteLine($"StarMap - Connecting to pipe {_pipeName}..."); await _client.ConnectAsync(cancellationToken); _readingCts = new CancellationTokenSource(); diff --git a/StarMap/Program.cs b/StarMap/Program.cs index de2ac49..3e68e45 100644 --- a/StarMap/Program.cs +++ b/StarMap/Program.cs @@ -10,15 +10,15 @@ static void Main(string[] args) { if (args.Length < 1) { - Console.WriteLine("StarMapLoader - Running Starmap in solo mode!"); + Console.WriteLine("StarMap - Running Starmap in solo mode!"); SoleModeInner(); return; } - Console.WriteLine("Running Starmap in loader mode."); + Console.WriteLine("StarMap - Running Starmap in loader mode."); var pipeName = args[0]; - Console.WriteLine($"Connection to pipe: {pipeName}"); + Console.WriteLine($"StarMap - Connection to pipe: {pipeName}"); MainInner(pipeName).GetAwaiter().GetResult(); } @@ -39,7 +39,7 @@ static void SoleModeInner() var gameSurveyer = new GameSurveyer(dumbFacade, gameAssemblyContext, gameConfig.GameLocation); if (!gameSurveyer.TryLoadCoreAndGame()) { - Console.WriteLine("Unable to load mod manager and game in solo mode."); + Console.WriteLine("StarMap - Unable to load mod manager and game in solo mode."); return; } diff --git a/StarMapLoader/ModRepository.cs b/StarMapLoader/ModRepository.cs index b118b00..a1441be 100644 --- a/StarMapLoader/ModRepository.cs +++ b/StarMapLoader/ModRepository.cs @@ -1,7 +1,6 @@ -using System.Collections; -using System.Text.Json; -using StarMap.Index.API; +using StarMap.Index.API; using StarMap.Types.Proto.IPC; +using System.Text.Json; namespace StarMapLoader { @@ -107,7 +106,7 @@ public void ApplyModUpdates() } catch (Exception ex) { - Console.WriteLine($"Unable to apply update for mod: {modChange.Mod.Name} to version {modChange.AfterVersion?.Version ?? ""}: {ex}"); + Console.WriteLine($"StarMap - Unable to apply update for mod: {modChange.Mod.Name} to version {modChange.AfterVersion?.Version ?? ""}: {ex}"); } }