From 3b48e36a9ee2df2a51d60556a8d4f4d841970624 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Sat, 29 Nov 2025 12:41:01 +0100 Subject: [PATCH 01/14] Add dependencies to mods and use them for mod loading order --- StarMap.API/BaseAttributes.cs | 61 ++++ .../ModRepository/LoadedModRepository.cs | 156 ---------- .../ModRepository/ModAssemblyLoadContext.cs | 15 +- StarMap.Core/ModRepository/ModLoader.cs | 274 ++++++++++++++++++ StarMap.Core/ModRepository/ModRegistry.cs | 77 +++-- StarMap.Core/Patches/ModPatches.cs | 2 + StarMap.Core/Patches/ProgramPatcher.cs | 4 +- StarMap.Core/StarMapCore.cs | 4 +- 8 files changed, 407 insertions(+), 186 deletions(-) delete mode 100644 StarMap.Core/ModRepository/LoadedModRepository.cs create mode 100644 StarMap.Core/ModRepository/ModLoader.cs diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index d5ecb5a..b427812 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -12,6 +12,67 @@ public class StarMapModAttribute : Attribute { } + public class StarMapDependencyInfo + { + public required string ModId { get; init; } + public required bool Optional { get; init; } + } + + /// + /// Should mark a property that returns a list of mod IDs that this mod depends on. + /// Any mod in this list will be loaded before this mod is loaded, if a dependency is never loaded, this most will also not be loaded + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public static StarMapDependencyInfo[] PropertyName { get; } + /// + /// + /// Specifically: + /// + /// Return type must be . + /// Property must be a static property. + /// + /// + + [AttributeUsage(AttributeTargets.Property)] + public class StarMapDependenciesAttribute : Attribute + { + public bool IsValidSignature(PropertyInfo property) + { + return property.PropertyType == typeof(StarMapDependencyInfo[]); + } + } + + /// + /// Should mark a property that returns a list of assembly names other mods can access. + /// When other mods, that depend on this mod, try to load an assembly, they will first check this list and if it is in there, load it from this mods load context. + /// The assembly names should not contain any versions or .dll suffixes, just the raw assembly name. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public static string[] PropertyName { get; } + /// + /// + /// Specifically: + /// + /// Return type must be . + /// Property must be a static property. + /// + /// + + [AttributeUsage(AttributeTargets.Property)] + public class StarMapExportedAssemblyAttribute : Attribute + { + public bool IsValidSignature(PropertyInfo property) + { + return property.PropertyType == typeof(string[]); + } + } + [AttributeUsage(AttributeTargets.Method)] public abstract class StarMapMethodAttribute : Attribute { diff --git a/StarMap.Core/ModRepository/LoadedModRepository.cs b/StarMap.Core/ModRepository/LoadedModRepository.cs deleted file mode 100644 index b1177d1..0000000 --- a/StarMap.Core/ModRepository/LoadedModRepository.cs +++ /dev/null @@ -1,156 +0,0 @@ -using KSA; -using StarMap.API; -using System.Reflection; -using System.Runtime.Loader; - -namespace StarMap.Core.ModRepository -{ - internal class LoadedModRepository : IDisposable - { - private readonly AssemblyLoadContext _coreAssemblyLoadContext; - - private readonly Dictionary _registeredMethodAttributes = []; - private readonly Dictionary _attemptedMods = []; - private readonly Dictionary _modLoadContexts = []; - - 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; - - Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; - - _registeredMethodAttributes = coreAssembly - .GetTypes() - .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 Init() - { - PrepareMods(); - } - - private void PrepareMods() - { - var loadedManifest = ModLibrary.PrepareManifest(); - - if (!loadedManifest) return; - - var mods = ModLibrary.Manifest.Mods; - if (mods is null) return; - - string rootPath = "Content"; - string path = Path.Combine(new ReadOnlySpan(in rootPath)); - - foreach (var mod in mods) - { - var modPath = Path.Combine(path, mod.Id); - - if (!LoadMod(mod.Id, modPath)) - { - _attemptedMods[mod.Id] = false; - continue; - } - - if (_mods.GetBeforeMainAction(mod.Id) is (object @object, MethodInfo method)) - { - method.Invoke(@object, []); - } - _attemptedMods[mod.Id] = true; - } - } - - public void ModPrepareSystems(Mod mod) - { - if (!_attemptedMods.TryGetValue(mod.Id, out var succeeded)) - { - succeeded = LoadMod(mod.Id, mod.DirectoryPath); - } - - if (!succeeded) return; - - if (_mods.GetPrepareSystemsAction(mod.Id) is (object @object, MethodInfo method)) - { - method.Invoke(@object, [mod]); - } - } - - private bool LoadMod(string modId, string modDirectory) - { - var fullPath = Path.GetFullPath(modDirectory); - var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); - var assemblyExists = File.Exists(modAssemblyFile); - - if (!assemblyExists) return false; - - var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext); - var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId }); - - var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); - if (modClass is null) return false; - - var modObject = Activator.CreateInstance(modClass); - if (modObject is null) return false; - - _modLoadContexts.Add(modId, modLoadContext); - - var classMethods = modClass.GetMethods(); - var immediateLoadMethods = new List(); - - foreach (var classMethod in classMethods) - { - var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); - foreach (var stringAttr in stringAttrs) - { - var attr = _registeredMethodAttributes[stringAttr]; - - if (!attr.IsValidSignature(classMethod)) continue; - - if (attr.GetType() == typeof(StarMapImmediateLoadAttribute)) - { - immediateLoadMethods.Add(classMethod); - } - - _mods.Add(modId, attr, modObject, classMethod); - } - } - - Console.WriteLine($"StarMap - Loaded mod: {modId} from {modAssemblyFile}"); - return true; - } - - public void OnAllModsLoaded() - { - foreach (var (_, @object, method) in _mods.Get()) - { - method.Invoke(@object, []); - } - } - - public void Dispose() - { - foreach (var (_, @object, method) in _mods.Get()) - { - method.Invoke(@object, []); - } - - _mods.Dispose(); - } - } -} diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index d6dbd6f..78e6e69 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -9,6 +9,8 @@ internal class ModAssemblyLoadContext : AssemblyLoadContext private readonly AssemblyLoadContext _coreAssemblyLoadContext; private readonly AssemblyDependencyResolver _modDependencyResolver; + public ModInformation? ModInfo { get; set; } + public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadContext coreAssemblyContext) : base() { @@ -21,7 +23,7 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon protected override Assembly? Load(AssemblyName assemblyName) { - var existingInDefault = AssemblyLoadContext.Default.Assemblies + var existingInDefault = Default.Assemblies .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); if (existingInDefault != null) return existingInDefault; @@ -44,6 +46,17 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon } } + if (ModInfo is ModInformation modInfo && modInfo.DependencyContexts.Count > 0) + { + foreach (var context in modInfo.DependencyContexts) + { + var existsInDependencyContext = context.Assemblies + .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); + if (existsInDependencyContext != null) + return existsInDependencyContext; + } + } + var foundPath = _modDependencyResolver.ResolveAssemblyToPath(assemblyName); if (foundPath is null) return null; diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs new file mode 100644 index 0000000..c2862c4 --- /dev/null +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -0,0 +1,274 @@ +using KSA; +using StarMap.API; +using System.Reflection; +using System.Runtime.Loader; + +namespace StarMap.Core.ModRepository +{ + internal sealed class ModLoader : IDisposable + { + private readonly AssemblyLoadContext _coreAssemblyLoadContext; + + private readonly Dictionary _registeredMethodAttributes = []; + private readonly Dictionary> _modDependencyGraph = []; + private readonly HashSet _waitingMods = []; + + private readonly ModRegistry _modRegistry = new(); + public ModRegistry ModRegistry => _modRegistry; + + 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 ModLoader(AssemblyLoadContext coreAssemblyLoadContext) + { + _coreAssemblyLoadContext = coreAssemblyLoadContext; + + Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; + + _registeredMethodAttributes = coreAssembly + .GetTypes() + .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 Init() + { + PrepareMods(); + } + + private void PrepareMods() + { + var loadedManifest = ModLibrary.PrepareManifest(); + + if (!loadedManifest) return; + + var mods = ModLibrary.Manifest.Mods; + if (mods is null) return; + + string rootPath = "Content"; + string path = Path.Combine(new ReadOnlySpan(in rootPath)); + + foreach (var mod in mods) + { + var modPath = Path.Combine(path, mod.Id); + + if (!LoadMod(mod.Id, modPath)) + { + continue; + } + + if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.BeforeMainAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, []); + } + } + + var loadedMod = true; + + while (_modDependencyGraph.Count > 0 && loadedMod) + { + loadedMod = false; + foreach (var waitingMod in _waitingMods) + { + if (waitingMod.NotLoadedModDependencies.Count == 0 || waitingMod.NotLoadedModDependencies.Values.All(dependencyInfo => dependencyInfo.Optional)) + { + loadedMod = true; + _waitingMods.Remove(waitingMod); + + if (InitializeMod(waitingMod)) + { + Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + } + else + { + Console.WriteLine($"StarMap - Failed to load mod:{ waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + } + waitingMod.NotLoadedModDependencies.Clear(); + } + } + } + + if (_waitingMods.Count > 0) + { + foreach (var waitingMod in _waitingMods) + { + Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + } + } + } + + private bool LoadMod(string modId, string modDirectory) + { + var fullPath = Path.GetFullPath(modDirectory); + var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); + var assemblyExists = File.Exists(modAssemblyFile); + + if (!assemblyExists) return false; + + var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext); + var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId }); + + var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); + if (modClass is null) return false; + + var modInfo = new ModInformation() { + ModId = modId, + ModAssemblyLoadContext = modLoadContext, + ModType = modClass + }; + + modLoadContext.ModInfo = modInfo; + + _modRegistry.Add(modInfo); + + var dependencyPropertyField = modClass + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapDependenciesAttribute).Name)); + + if (dependencyPropertyField is PropertyInfo dependencyProperty) + { + object? value = dependencyProperty.GetValue(null); // null because static + if (value is StarMapDependencyInfo[] dependencies) + { + foreach (var dependency in dependencies) + { + if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) + { + modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); + if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) + { + dependents = []; + _modDependencyGraph[dependency.ModId] = dependents; + } + dependents.Add(modInfo); + } + else + { + modInfo.DependencyContexts.Add(modDependency.ModAssemblyLoadContext); + } + } + } + } + + if (modInfo.NotLoadedModDependencies.Count > 0) + { + Console.WriteLine($"StarMap - Delaying load of mod: {modInfo.ModId} due to missing dependencies: {string.Join(", ", modInfo.NotLoadedModDependencies.Keys)}"); + _waitingMods.Add(modInfo); + return false; + } + + if (!InitializeMod(modInfo)) + { + Console.WriteLine($"StarMap - Failed to initialize mod: {modInfo.ModId} from {modAssemblyFile}"); + return false; + } + + Console.WriteLine($"StarMap - Loaded mod: {modInfo.ModId} from {modAssemblyFile}"); + + if (_modDependencyGraph.TryGetValue(modInfo.ModId, out var modDependents)) + { + foreach(var modDependent in modDependents) + { + modDependent.DependencyContexts.Add(modInfo.ModAssemblyLoadContext); + if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) + { + _waitingMods.Remove(modDependent); + if (InitializeMod(modDependent)) + { + Console.WriteLine($"StarMap - Loaded mod: {modDependent.ModId} after loading {modInfo.ModId}"); + } + else + { + Console.WriteLine($"StarMap - Failed to load mod: {modDependent.ModId} after loading {modInfo.ModId}"); + } + } + } + _modDependencyGraph.Remove(modInfo.ModId); + } + + return true; + } + + private bool InitializeMod(ModInformation modInfo) + { + var modObject = Activator.CreateInstance(modInfo.ModType); + if (modObject is null) return false; + modInfo.ModInstance = modObject; + modInfo.Initialized = true; + + var classMethods = modInfo.ModType.GetMethods(); + var immediateLoadMethods = new List(); + + foreach (var classMethod in classMethods) + { + var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); + foreach (var stringAttr in stringAttrs) + { + var attr = _registeredMethodAttributes[stringAttr]; + + if (!attr.IsValidSignature(classMethod)) continue; + + if (attr.GetType() == typeof(StarMapImmediateLoadAttribute)) + { + immediateLoadMethods.Add(classMethod); + } + + _modRegistry.AddModMethod(modInfo.ModId, attr, modObject, classMethod); + } + } + + var exportedAssemblyAttribute = modInfo.ModType + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapExportedAssemblyAttribute).Name)); + + if (exportedAssemblyAttribute is PropertyInfo dependencyProperty) + { + object? value = dependencyProperty.GetValue(null); // null because static + if (value is string[] exportedAssemblies) + { + modInfo.ExportedAssemblies.AddRange(exportedAssemblies); + } + } + + return true; + } + + public void ModPrepareSystems(Mod mod) + { + if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.PrepareSystemsAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, [mod]); + } + } + + public void OnAllModsLoaded() + { + + + foreach (var (_, @object, method) in _modRegistry.Get()) + { + method.Invoke(@object, []); + } + } + + public void Dispose() + { + foreach (var (_, @object, method) in _modRegistry.Get()) + { + method.Invoke(@object, []); + } + + _modRegistry.Dispose(); + } + } +} diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 40e1b3b..18548e1 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -1,38 +1,75 @@ using KSA; using StarMap.API; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace StarMap.Core.ModRepository { + internal sealed class ModInformation + { + public required string ModId { get; init; } + public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } + public required Type ModType { get; init; } + public bool Initialized { get; set; } = false; + public object? ModInstance { get; set; } = null; + + public List ExportedAssemblies { get; set; } = []; + + public HashSet DependencyContexts = []; + public Dictionary NotLoadedModDependencies = []; + + public MethodInfo? BeforeMainAction { get; set; } = null; + public MethodInfo? PrepareSystemsAction { get; set; } = null; + } + internal sealed class ModRegistry : IDisposable { - private readonly Dictionary> _map = []; - private readonly Dictionary _beforeMainActions = []; - private readonly Dictionary _prepareSystemsActions = []; + private readonly Dictionary _mods = []; + private readonly Dictionary> _modMethods = []; + + public bool ModLoaded(string modId) => _mods.ContainsKey(modId); + + public bool TryGetMod(string modId, [NotNullWhen(true)] out ModInformation? modInfo) + { + return _mods.TryGetValue(modId, out modInfo); + } + + public void Add(ModInformation modInfo) + { + _mods.Add(modInfo.ModId, modInfo); + } + + public IEnumerable GetMods() + { + return _mods.Values; + } + - public void Add(string modId, StarMapMethodAttribute attribute, object @object, MethodInfo method) + public void AddModMethod(string modId, StarMapMethodAttribute methodAttribute, object @object, MethodInfo method) { - var attributeType = attribute.GetType(); + if (!_mods.TryGetValue(modId, out var modInfo)) return; + + var attributeType = methodAttribute.GetType(); - if (!_map.TryGetValue(attributeType, out var list)) + if (!_modMethods.TryGetValue(attributeType, out var list)) { list = []; - _map[attributeType] = list; + _modMethods[attributeType] = list; } - if (attribute.GetType() == typeof(StarMapBeforeMainAttribute)) - _beforeMainActions[modId] = (@object, method); + if (methodAttribute.GetType() == typeof(StarMapBeforeMainAttribute)) + modInfo.BeforeMainAction = method; - if (attribute.GetType() == typeof(StarMapImmediateLoadAttribute)) - _prepareSystemsActions[modId] = (@object, method); + if (methodAttribute.GetType() == typeof(StarMapImmediateLoadAttribute)) + modInfo.PrepareSystemsAction = method; - list.Add((attribute, @object, method)); + list.Add((methodAttribute, @object, method)); } public IReadOnlyList<(StarMapMethodAttribute attribute, object @object, MethodInfo method)> Get() where TAttribute : Attribute { - if (_map.TryGetValue(typeof(TAttribute), out var list)) + if (_modMethods.TryGetValue(typeof(TAttribute), out var list)) { return list.Cast<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>().ToList(); } @@ -42,24 +79,14 @@ public void Add(string modId, StarMapMethodAttribute attribute, object @object, public IReadOnlyList<(StarMapMethodAttribute attribute, object @object, MethodInfo method)> Get(Type iface) { - return _map.TryGetValue(iface, out var list) + return _modMethods.TryGetValue(iface, out var list) ? list : Array.Empty<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>(); } - public (object @object, MethodInfo method)? GetBeforeMainAction(string modId) - { - return _beforeMainActions.TryGetValue(modId, out var action) ? action : null; - } - - public (object @object, MethodInfo method)? GetPrepareSystemsAction(string modId) - { - return _prepareSystemsActions.TryGetValue(modId, out var action) ? action : null; - } - public void Dispose() { - _map.Clear(); + _modMethods.Clear(); } } } diff --git a/StarMap.Core/Patches/ModPatches.cs b/StarMap.Core/Patches/ModPatches.cs index 69ce5b0..8d01a8e 100644 --- a/StarMap.Core/Patches/ModPatches.cs +++ b/StarMap.Core/Patches/ModPatches.cs @@ -1,5 +1,7 @@ using HarmonyLib; using KSA; +using StarMap.Core.ModRepository; +using System.Reflection; namespace StarMap.Core.Patches { diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index 69b79ca..dd31775 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -13,7 +13,7 @@ internal static class ProgramPatcher [HarmonyPrefix] public static void BeforeOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { @@ -25,7 +25,7 @@ public static void BeforeOnDrawUi(double dt) [HarmonyPostfix] public static void AfterOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 4f54870..6a7997d 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -12,8 +12,8 @@ internal class StarMapCore : IStarMapCore private readonly Harmony _harmony = new("StarMap.Core"); private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly LoadedModRepository _loadedMods; - public LoadedModRepository LoadedMods => _loadedMods; + private readonly ModLoader _loadedMods; + public ModLoader LoadedMods => _loadedMods; public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) { From f12668e334ecb8301a9d5f58e7ba99c91dfe4d73 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Sat, 13 Dec 2025 21:45:05 +0100 Subject: [PATCH 02/14] Allow mods to define what assemblies they want to export --- StarMap.API/README.md | 21 +++++++++++++++++++ .../ModRepository/ModAssemblyLoadContext.cs | 20 ++++++++++++------ StarMap.Core/ModRepository/ModLoader.cs | 9 ++++---- StarMap.Core/ModRepository/ModRegistry.cs | 4 ++-- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/StarMap.API/README.md b/StarMap.API/README.md index 0ee0184..b1f14d7 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -14,3 +14,24 @@ Any method within this class that has any of the attributes will used, so if two - StarMapUnload: Called when KSA is unloaded. - StarMapBeforeGui: Called just before KSA starts drawing its Ui. - StarMapAfterGui: Called after KSA has drawn its Ui. + +## Dependencies + +Mods can define what mods they define on, as well as what assemblies they want to export themselves. +This only falls on assembly loading and load order within StarMap. +If a mod is set as a dependency, and it is present, it will be loaded before the mods that depends on it. +The dependent mod can then access any assembly that the dependency exposes, in addition to the main mod assembly, which is always exposed. + +This requires following attributes: + +- StarMapDependenciesAttribute: Describes what mods this mod dependends on. + + - This attribute should be placed on a static property that returns an array of StarMapDependencyInfo. + - The StarMapDependencyInfo provides info on the mod id of the mod that should be depended on, and if this dependency is optional. + - When a dependency is not optional, the mod will not be loaded when the dependency is not present. + - When a dependency is optional, the mod is loaded after all other mods are loaded, to ensure the dependency can be loaded before. + +- StarMapExportedAssemblyAttribute: Describes what assemblies are exposed by the mod. + - This attribute should be placed on a static property that returns an array of strings. + - The string should be the name of the assembly, with no ".dll" suffix. + - The main mod assembly (modid.dll) is always exposed, so this is only for other assemblies that other mods could depend on. diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index 78e6e69..6ec39b9 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -46,14 +46,22 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon } } - if (ModInfo is ModInformation modInfo && modInfo.DependencyContexts.Count > 0) + if (ModInfo is ModInformation modInfo && modInfo.Dependencies.Count > 0) { - foreach (var context in modInfo.DependencyContexts) + foreach (var dependency in modInfo.Dependencies) { - var existsInDependencyContext = context.Assemblies - .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); - if (existsInDependencyContext != null) - return existsInDependencyContext; + if (dependency.ModId == assemblyName.Name || dependency.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + { + try + { + var asm = dependency.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + if (asm != null) + return asm; + } + catch (FileNotFoundException) + { + } + } } } diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs index c2862c4..8b3ebc9 100644 --- a/StarMap.Core/ModRepository/ModLoader.cs +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -154,7 +154,7 @@ private bool LoadMod(string modId, string modDirectory) } else { - modInfo.DependencyContexts.Add(modDependency.ModAssemblyLoadContext); + modInfo.Dependencies.Add(modDependency); } } } @@ -179,7 +179,7 @@ private bool LoadMod(string modId, string modDirectory) { foreach(var modDependent in modDependents) { - modDependent.DependencyContexts.Add(modInfo.ModAssemblyLoadContext); + modDependent.Dependencies.Add(modInfo); if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) { _waitingMods.Remove(modDependent); @@ -236,7 +236,8 @@ private bool InitializeMod(ModInformation modInfo) object? value = dependencyProperty.GetValue(null); // null because static if (value is string[] exportedAssemblies) { - modInfo.ExportedAssemblies.AddRange(exportedAssemblies); + foreach (var assembly in exportedAssemblies) + modInfo.ExportedAssemblies.Add(assembly); } } @@ -253,8 +254,6 @@ public void ModPrepareSystems(Mod mod) public void OnAllModsLoaded() { - - foreach (var (_, @object, method) in _modRegistry.Get()) { method.Invoke(@object, []); diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 18548e1..e9f7cdd 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -13,9 +13,9 @@ internal sealed class ModInformation public bool Initialized { get; set; } = false; public object? ModInstance { get; set; } = null; - public List ExportedAssemblies { get; set; } = []; + public HashSet ExportedAssemblies { get; set; } = []; - public HashSet DependencyContexts = []; + public HashSet Dependencies = []; public Dictionary NotLoadedModDependencies = []; public MethodInfo? BeforeMainAction { get; set; } = null; From 7679c9b987e31f493704091a3b7b7e18fa2584f3 Mon Sep 17 00:00:00 2001 From: KlaasWhite <45828001+KlaasWhite@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:06:32 +0100 Subject: [PATCH 03/14] Add StarMap hook for after onFrame * Fix package release * Onframepatch clean (#47) * Do not try to unload the assembly load contexts (#44) * Program.OnFrame Harmony patch * Updated the API readme --------- Co-authored-by: KlaasWhite <45828001+KlaasWhite@users.noreply.github.com> --------- Co-authored-by: mihe --- StarMap.API/OnFrameAttributes.cs | 46 ++++++++++++++++++++++++++ StarMap.API/README.md | 1 + StarMap.Core/Patches/ProgramPatcher.cs | 13 ++++++++ 3 files changed, 60 insertions(+) create mode 100644 StarMap.API/OnFrameAttributes.cs diff --git a/StarMap.API/OnFrameAttributes.cs b/StarMap.API/OnFrameAttributes.cs new file mode 100644 index 0000000..5844277 --- /dev/null +++ b/StarMap.API/OnFrameAttributes.cs @@ -0,0 +1,46 @@ +using System.Reflection; + +namespace StarMap.API +{ + /// + /// Methods marked with this attribute will be called after KSA Program.OnFrame is called. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(double currentPlayerTime, double dtPlayer); + /// + /// + /// Parameter requirements: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Requirements: + /// + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public sealed class StarMapAfterOnFrameAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 2 && + method.GetParameters()[0].ParameterType == typeof(double) && + method.GetParameters()[1].ParameterType == typeof(double); + + } + } +} diff --git a/StarMap.API/README.md b/StarMap.API/README.md index 0ee0184..b414faa 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -14,3 +14,4 @@ Any method within this class that has any of the attributes will used, so if two - StarMapUnload: Called when KSA is unloaded. - StarMapBeforeGui: Called just before KSA starts drawing its Ui. - StarMapAfterGui: Called after KSA has drawn its Ui. +- StarMapAfterOnFrame: Called after KSA calls Program.OnFrame diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index 69b79ca..9bc2710 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -8,6 +8,7 @@ namespace StarMap.Core.Patches internal static class ProgramPatcher { private const string OnDrawUiMethodName = "OnDrawUi"; + private const string OnFrameMethodName = "OnFrame"; [HarmonyPatch(OnDrawUiMethodName)] [HarmonyPrefix] @@ -32,5 +33,17 @@ public static void AfterOnDrawUi(double dt) method.Invoke(@object, [dt]); } } + + [HarmonyPatch(OnFrameMethodName)] + [HarmonyPostfix] + public static void AfterOnFrame(double currentPlayerTime, double dtPlayer) + { + var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + + foreach (var (_, @object, method) in methods) + { + method.Invoke(@object, new object[] { currentPlayerTime, dtPlayer }); + } + } } } From 67418e069a3b30417a3e4a066cb2ad03f9159ed8 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 16 Dec 2025 22:24:24 +0100 Subject: [PATCH 04/14] Alter dependency config to work using the mod.toml instead of static properties --- StarMap.API/BaseAttributes.cs | 61 ---------------- StarMap.API/README.md | 20 +----- StarMap.Core/Config/StarMapConfig.cs | 26 +++++++ .../ModRepository/ModAssemblyLoadContext.cs | 27 ++++++- StarMap.Core/ModRepository/ModLoader.cs | 70 ++++++++----------- StarMap.Core/ModRepository/ModRegistry.cs | 7 +- StarMap.Core/StarMap.Core.csproj | 3 + 7 files changed, 90 insertions(+), 124 deletions(-) create mode 100644 StarMap.Core/Config/StarMapConfig.cs diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index b427812..d5ecb5a 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -12,67 +12,6 @@ public class StarMapModAttribute : Attribute { } - public class StarMapDependencyInfo - { - public required string ModId { get; init; } - public required bool Optional { get; init; } - } - - /// - /// Should mark a property that returns a list of mod IDs that this mod depends on. - /// Any mod in this list will be loaded before this mod is loaded, if a dependency is never loaded, this most will also not be loaded - /// - /// - /// Methods using this attribute must match the following signature: - /// - /// - /// public static StarMapDependencyInfo[] PropertyName { get; } - /// - /// - /// Specifically: - /// - /// Return type must be . - /// Property must be a static property. - /// - /// - - [AttributeUsage(AttributeTargets.Property)] - public class StarMapDependenciesAttribute : Attribute - { - public bool IsValidSignature(PropertyInfo property) - { - return property.PropertyType == typeof(StarMapDependencyInfo[]); - } - } - - /// - /// Should mark a property that returns a list of assembly names other mods can access. - /// When other mods, that depend on this mod, try to load an assembly, they will first check this list and if it is in there, load it from this mods load context. - /// The assembly names should not contain any versions or .dll suffixes, just the raw assembly name. - /// - /// - /// Methods using this attribute must match the following signature: - /// - /// - /// public static string[] PropertyName { get; } - /// - /// - /// Specifically: - /// - /// Return type must be . - /// Property must be a static property. - /// - /// - - [AttributeUsage(AttributeTargets.Property)] - public class StarMapExportedAssemblyAttribute : Attribute - { - public bool IsValidSignature(PropertyInfo property) - { - return property.PropertyType == typeof(string[]); - } - } - [AttributeUsage(AttributeTargets.Method)] public abstract class StarMapMethodAttribute : Attribute { diff --git a/StarMap.API/README.md b/StarMap.API/README.md index b1f14d7..cca3f60 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -17,21 +17,5 @@ Any method within this class that has any of the attributes will used, so if two ## Dependencies -Mods can define what mods they define on, as well as what assemblies they want to export themselves. -This only falls on assembly loading and load order within StarMap. -If a mod is set as a dependency, and it is present, it will be loaded before the mods that depends on it. -The dependent mod can then access any assembly that the dependency exposes, in addition to the main mod assembly, which is always exposed. - -This requires following attributes: - -- StarMapDependenciesAttribute: Describes what mods this mod dependends on. - - - This attribute should be placed on a static property that returns an array of StarMapDependencyInfo. - - The StarMapDependencyInfo provides info on the mod id of the mod that should be depended on, and if this dependency is optional. - - When a dependency is not optional, the mod will not be loaded when the dependency is not present. - - When a dependency is optional, the mod is loaded after all other mods are loaded, to ensure the dependency can be loaded before. - -- StarMapExportedAssemblyAttribute: Describes what assemblies are exposed by the mod. - - This attribute should be placed on a static property that returns an array of strings. - - The string should be the name of the assembly, with no ".dll" suffix. - - The main mod assembly (modid.dll) is always exposed, so this is only for other assemblies that other mods could depend on. +Mods can define what mods they depend on, and what assemblies they want to import from that mod. +They can as well define what assemblies they want exported to other mods. diff --git a/StarMap.Core/Config/StarMapConfig.cs b/StarMap.Core/Config/StarMapConfig.cs new file mode 100644 index 0000000..57efc81 --- /dev/null +++ b/StarMap.Core/Config/StarMapConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace StarMap.Core.Config +{ + internal class RootConfig + { + public required StarMapConfig StarMap { get; set; } + } + + + internal class StarMapConfig + { + public required string EntryAssembly { get; set; } + public List ExportedAssemblies { get; set; } = []; + public List ModDependencies { get; set; } = []; + } + + internal class StarMapModDependency + { + public required string ModId { get; set; } + public bool Optional { get; set; } = false; + public List ImportedAssemblies { get; set; } = []; + } +} diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index 6ec39b9..b6c419f 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -48,13 +48,34 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon if (ModInfo is ModInformation modInfo && modInfo.Dependencies.Count > 0) { - foreach (var dependency in modInfo.Dependencies) + foreach (var (dependencyInfo, importedAssemblies) in modInfo.Dependencies) { - if (dependency.ModId == assemblyName.Name || dependency.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + bool ShouldTryLoad() + { + var hasExportedAssemblies = dependencyInfo.ExportedAssemblies.Count > 0; + var hasImportedAssemblies = importedAssemblies.Count > 0; + + if (!hasImportedAssemblies && !hasExportedAssemblies) { + if (dependencyInfo.Config.EntryAssembly == assemblyName.Name) + return true; + + return false; + } + + if (hasExportedAssemblies && !dependencyInfo.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + return false; + + if (hasImportedAssemblies && !importedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + return false; + + return true; + } + + if (ShouldTryLoad()) { try { - var asm = dependency.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + var asm = dependencyInfo.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); if (asm != null) return asm; } diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs index 8b3ebc9..d4b4dd9 100644 --- a/StarMap.Core/ModRepository/ModLoader.cs +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -1,7 +1,9 @@ using KSA; using StarMap.API; +using StarMap.Core.Config; using System.Reflection; using System.Runtime.Loader; +using Tomlet; namespace StarMap.Core.ModRepository { @@ -61,8 +63,11 @@ private void PrepareMods() foreach (var mod in mods) { var modPath = Path.Combine(path, mod.Id); + var starMapConfig = TomletMain.To(File.ReadAllText(Path.Combine(modPath, "mod.toml"))); + if (starMapConfig.StarMap is null) + continue; - if (!LoadMod(mod.Id, modPath)) + if (!LoadMod(mod.Id, modPath, starMapConfig.StarMap)) { continue; } @@ -104,10 +109,11 @@ private void PrepareMods() { Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } + _waitingMods.Clear(); } } - private bool LoadMod(string modId, string modDirectory) + private bool LoadMod(string modId, string modDirectory, StarMapConfig config) { var fullPath = Path.GetFullPath(modDirectory); var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); @@ -124,39 +130,33 @@ private bool LoadMod(string modId, string modDirectory) var modInfo = new ModInformation() { ModId = modId, ModAssemblyLoadContext = modLoadContext, - ModType = modClass + ModType = modClass, + Config = config, }; - modLoadContext.ModInfo = modInfo; + foreach(var exportedAssembly in config.ExportedAssemblies) + { + modInfo.ExportedAssemblies.Add(exportedAssembly); + } + modLoadContext.ModInfo = modInfo; _modRegistry.Add(modInfo); - var dependencyPropertyField = modClass - .GetProperties(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapDependenciesAttribute).Name)); - - if (dependencyPropertyField is PropertyInfo dependencyProperty) + foreach (var dependency in config.ModDependencies) { - object? value = dependencyProperty.GetValue(null); // null because static - if (value is StarMapDependencyInfo[] dependencies) + if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) { - foreach (var dependency in dependencies) + modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); + if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) { - if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) - { - modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); - if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) - { - dependents = []; - _modDependencyGraph[dependency.ModId] = dependents; - } - dependents.Add(modInfo); - } - else - { - modInfo.Dependencies.Add(modDependency); - } + dependents = []; + _modDependencyGraph[dependency.ModId] = dependents; } + dependents.Add(modInfo); + } + else + { + modInfo.Dependencies.Add(modDependency, [.. dependency.ImportedAssemblies]); } } @@ -179,7 +179,8 @@ private bool LoadMod(string modId, string modDirectory) { foreach(var modDependent in modDependents) { - modDependent.Dependencies.Add(modInfo); + var dependencyInfo = modDependent.NotLoadedModDependencies[modInfo.ModId]; + modDependent.Dependencies.Add(modInfo, [.. dependencyInfo.ImportedAssemblies]); if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) { _waitingMods.Remove(modDependent); @@ -227,19 +228,8 @@ private bool InitializeMod(ModInformation modInfo) } } - var exportedAssemblyAttribute = modInfo.ModType - .GetProperties(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapExportedAssemblyAttribute).Name)); - - if (exportedAssemblyAttribute is PropertyInfo dependencyProperty) - { - object? value = dependencyProperty.GetValue(null); // null because static - if (value is string[] exportedAssemblies) - { - foreach (var assembly in exportedAssemblies) - modInfo.ExportedAssemblies.Add(assembly); - } - } + foreach (var assembly in modInfo.Config?.ExportedAssemblies ?? []) + modInfo.ExportedAssemblies.Add(assembly); return true; } diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index e9f7cdd..3a11a30 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -1,5 +1,6 @@ using KSA; using StarMap.API; +using StarMap.Core.Config; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -10,13 +11,15 @@ internal sealed class ModInformation public required string ModId { get; init; } public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } public required Type ModType { get; init; } + public required StarMapConfig Config { get; init; } + public bool Initialized { get; set; } = false; public object? ModInstance { get; set; } = null; public HashSet ExportedAssemblies { get; set; } = []; + public Dictionary> Dependencies { get; set; } = []; + public Dictionary NotLoadedModDependencies { get; set; } = []; - public HashSet Dependencies = []; - public Dictionary NotLoadedModDependencies = []; public MethodInfo? BeforeMainAction { get; set; } = null; public MethodInfo? PrepareSystemsAction { get; set; } = null; diff --git a/StarMap.Core/StarMap.Core.csproj b/StarMap.Core/StarMap.Core.csproj index cf2929a..2e14983 100644 --- a/StarMap.Core/StarMap.Core.csproj +++ b/StarMap.Core/StarMap.Core.csproj @@ -8,6 +8,9 @@ + + runtime + From 6f36cebf901925936e0a53c03b6bdf4db3823c4d Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 18:20:22 +0100 Subject: [PATCH 05/14] Add StarMap section to mod.toml and improve API documentation --- README.md | 2 +- StarMap.API/README.md | 216 ++++++++++++++++-- StarMap.API/StarMap.API.csproj | 37 +-- .../ModRepository/ModAssemblyLoadContext.cs | 35 +-- StarMap.Core/ModRepository/ModLoader.cs | 199 ++++------------ StarMap.Core/ModRepository/ModRegistry.cs | 31 +-- StarMap.Core/ModRepository/RuntimeMod.cs | 176 ++++++++++++++ StarMap.Core/Patches/ModLibraryPatches.cs | 10 +- StarMap.Core/Patches/ModPatches.cs | 8 +- StarMap.Core/Patches/ProgramPatcher.cs | 4 +- StarMap.Core/StarMapCore.cs | 10 +- 11 files changed, 484 insertions(+), 244 deletions(-) create mode 100644 StarMap.Core/ModRepository/RuntimeMod.cs diff --git a/README.md b/README.md index ac40813..2f08e97 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It makes use of Assembly Load Contexts to ensure mod dependencies are managed se ## Mod location -Mods should be installed in the mods folder in the KSA installation, any KSA mod that has a dll with the same name as the mod that impelements the IStarMapMod interface will be loaded. +Mods should be installed in the contents folder in the KSA installation, StarMap makes use of the ## Mod creation diff --git a/StarMap.API/README.md b/StarMap.API/README.md index cca3f60..d3604b5 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -1,21 +1,209 @@ # 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 +## How to create mods -- 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. +### General architecture -## Dependencies +A StarMap mod is in essence an extension of KSA mods. +Every mod will at minimum contain a mod.toml (which is also the case for KSA mods) as well as an entry assembly. -Mods can define what mods they depend on, and what assemblies they want to import from that mod. -They can as well define what assemblies they want exported to other mods. +#### mod.toml + +While it is not stricly neccesary, it is adviced to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: + +```toml +name = "StarMap.SimpleMod" + +[StarMap] +EntryAssembly = "StarMap.SimpleMod" +``` + +The name will be the modid of your mod, this should be the same name as the folder in which this lives in the Content folder. +The "StarMap" section is optional, if it is provided, at mimimum, it should provide the name of the assembly which StarMap will initially load (without the dll extension). +If it is not provided, StarMap will search for a [modid].dll in the mod folder. + +#### Entry assembly + +The above mentioned entry assembly will be the first thing that StarMap will load. +Within this assembly, StarMap will search for a class that has the [StarMapMod](#starmapmod) attribute. It will use the first that it finds. +This class will be the core part of the mod, any entry from StarMap will be done through this class. +To add functionality to the mod, methods with attributes can be added, which attributes are available and what their functionality is, can be found in the [Attributes API reference](#attributes). +These methods will be called as instance methods on the mod class, throughout the runtime of KSA, StarMap will create once instance of this class and reuse it. +The implemenation should follow the signatures shown in the example code. + +### Dependencies + +In many cases, mods will have dependencies on other mods, or will want to overwrite functionality of other mods. +To achieve this, some extra configuration is required, this configuration is confined to the mod.toml within the StarMap section. + +#### Exported assemblies + +First of all, mods can configure what assemblies should be exposed to other mods, by default all assemblies that are provided with the mod can be accessed from other mods, this can be changed with the ExportedAssemblies. +In below example, only the StarMap.SimpleMod.Dependency(.dll) assembly will be accessable from other mods (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). + +```toml +name = "StarMap.SimpleMod2" + +[StarMap] +EntryAssembly = "StarMap.SimpleMod2" +ExportedAssemblies = [ + "StarMap.SimpleMod.Dependency" +] +``` + +#### Mod dependency + +Then, mods can define what mods they want to define on, they can do this by adding a new ModDependencies list entry in the mod.toml + +```toml +name = "StarMap.SimpleMod" + +[StarMap] +EntryAssembly = "StarMap.SimpleMod" + +[[StarMap.ModDependencies]] +ModId = "StarMap.SimpleMod2" +Optional = false +ImportedAssemblies = [ + "StarMap.SimpleMod.Dependency" +] +``` + +In above example, it is provided that StarMap.SimpleMod wants to depend on StarMap.SimpleMod2, this dependency is not optional and the mod wants to access the StarMap.SimpleMod.Dependency assembly. +Following fields can be used + +- The ModId should be the same as is provided as the name field in the mod.toml of the dependency mod. +- The optional field (default false) defines if this dependency is optional, more info in the [loading strategy](#dependency-loading-strategy) +- The ImportedAssemblies field contains a list of assemblies that this mod intends to use from the dependency (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). + +#### Imported and exported assemblies + +The goal of the imported and exported assembly fields is to compile a list of assemblies that will be provided to a mod from a dependency, below is the behaviour depending on the content of both fields: + +- If both fields are not filled in, the list will contain the entry assembly of the dependency. +- If only 1 of the lists is filled in, it will use this list to provide the assemblies. +- If both lists are defined, the intersect of the two will be used. + +## Mod loading strategy + +When StarMap is started, it will start with loading the manifest.toml (in the same way KSA does it, only sooner), it will then start loading mods from top to bottom. +It will first load the mod.toml, if the file does not exists, mod loading will not work, if there is no StarMap section, it will use a default configuration. +Using the config, if there are dependencies, it will first check if these dependencies are already loaded. If they are all loaded, mod loading continues, +otherwise, it stores what dependencies are still needed and continues to the next mod. +It will then search for the entry assembly, if it does not exists, loading will be stopped, otherwise, it will load the assembly. Then will search for a class with the StarMapMod attribute and create an instance. With the instance, it goes over the known attributes and stores a reference to the methods, allowing for quick quering, it stores the StarMapBeforeMain and StarMapImmediateLoad methods seperatly because they are mod specific. +Once the mod is fully loaded, it will call the StarMapBeforeMain method, if there is any. + +Now that the mod has been loaded, it checks the list of mods that are waiting for dependencies, and if there are any that are waiting for this mod. If so, it removes itself from the waiting dependencies and checks if the mod can now be loaded, if so, the mod is loaded and the StarMapBeforeMain of that mod is called. + +It does this for all the mods in the manifest. +Once it has tried loading all the mods, it gets the mods that are stil waiting and checks them again. +If for a waiting mod, all its dependencies are optional, it will now load this mod. The implementation of the mod should ensure it can handle the optional dependency can be absent. +It keeps looping over the list of waiting mods until it has gone through the list once without being able to load a new mod, this indicates there are no more mods that can load with the provided mods, and gives up on loading these mods. + +Now StarMap will start KSA, which in turn will call StarMapImmediateLoad for each mod, if implemented. + +## Examples + +Some examples can be found in the [example mods repository](https://github.com/StarMapLoader/StarMap-ExampleMods) + +## API reference + +### Attributes + +#### StarMapMod + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Class + +Marks the main class for a StarMap mod. +Only attributes on methods within classes marked with this attribute will be considered. + +```csharp +[StarMapMod] +public class ModClass +``` + +#### StarMapBeforeMain + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called before KSA is started. + +```csharp +[StarMapBeforeMain] +public void ModMethod() +``` + +#### StarMapImmediateLoad + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called immediately when the mod is loaded by KSA. +It is called before the `KSA.Mod.PrepareSystems` method for each mod + +```csharp +[StarMapBeforeMain] +public void ModMethod(KSA.Mod mod) +``` + +#### StarMapAllModsLoaded + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called when all mods are loaded. +It is called after the `KSA.ModLibrary.LoadAll` method. + +```csharp +[StarMapAllModsLoaded] +public void ModMethod() +``` + +#### StarMapUnload + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called when KSA is unloaded + +```csharp +[StarMapUnload] +public void ModMethod() +``` + +#### StarMapBeforeGui + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called before KSA starts creating its ImGui interface. +It is called just before the `KSA.Program.OnDrawUi` method. + +```csharp +[StarMapBeforeGui] +public void ModMethod(double dt) +``` + +#### StarMapAfterGui + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called when KSA has finished creating its ImGui interface. +It is called just after the `KSA.Program.OnDrawUi` method. + +```csharp +[StarMapAfterGui] +public void ModMethod(double dt) +``` diff --git a/StarMap.API/StarMap.API.csproj b/StarMap.API/StarMap.API.csproj index 209a1cd..ff44d0d 100644 --- a/StarMap.API/StarMap.API.csproj +++ b/StarMap.API/StarMap.API.csproj @@ -1,19 +1,26 @@  - - net10.0 - enable - enable - + + net10.0 + enable + enable + true + 1591 + README.md + - - - + + + - - - compile; build; analyzers - all - - - + + + + + + + compile; build; analyzers + all + + + \ No newline at end of file diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index b6c419f..ccfb44b 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -9,15 +9,15 @@ internal class ModAssemblyLoadContext : AssemblyLoadContext private readonly AssemblyLoadContext _coreAssemblyLoadContext; private readonly AssemblyDependencyResolver _modDependencyResolver; - public ModInformation? ModInfo { get; set; } + public RuntimeMod? RuntimeMod { get; set; } - public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadContext coreAssemblyContext) + public ModAssemblyLoadContext(string modId, string entryAssemblyLocation, AssemblyLoadContext coreAssemblyContext) : base() { _coreAssemblyLoadContext = coreAssemblyContext; _modDependencyResolver = new AssemblyDependencyResolver( - Path.GetFullPath(Path.Combine(modDirectory, modId + ".dll")) + Path.GetFullPath(entryAssemblyLocation) ); } @@ -46,36 +46,15 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon } } - if (ModInfo is ModInformation modInfo && modInfo.Dependencies.Count > 0) + if (RuntimeMod is RuntimeMod modInfo && modInfo.Dependencies.Count > 0) { - foreach (var (dependencyInfo, importedAssemblies) in modInfo.Dependencies) + foreach (var (dependency, importedAssemblies) in modInfo.Dependencies) { - bool ShouldTryLoad() - { - var hasExportedAssemblies = dependencyInfo.ExportedAssemblies.Count > 0; - var hasImportedAssemblies = importedAssemblies.Count > 0; - - if (!hasImportedAssemblies && !hasExportedAssemblies) { - if (dependencyInfo.Config.EntryAssembly == assemblyName.Name) - return true; - - return false; - } - - if (hasExportedAssemblies && !dependencyInfo.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) - return false; - - if (hasImportedAssemblies && !importedAssemblies.Contains(assemblyName.Name ?? string.Empty)) - return false; - - return true; - } - - if (ShouldTryLoad()) + if (importedAssemblies.Contains(assemblyName.Name ?? string.Empty)) { try { - var asm = dependencyInfo.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + var asm = dependency.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); if (asm != null) return asm; } diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs index d4b4dd9..c0a8754 100644 --- a/StarMap.Core/ModRepository/ModLoader.cs +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -11,9 +11,8 @@ internal sealed class ModLoader : IDisposable { private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly Dictionary _registeredMethodAttributes = []; - private readonly Dictionary> _modDependencyGraph = []; - private readonly HashSet _waitingMods = []; + public static Dictionary RegisteredMethodAttributes = []; + private readonly ModRegistry _modRegistry = new(); public ModRegistry ModRegistry => _modRegistry; @@ -30,7 +29,7 @@ public ModLoader(AssemblyLoadContext coreAssemblyLoadContext) Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; - _registeredMethodAttributes = coreAssembly + RegisteredMethodAttributes = coreAssembly .GetTypes() .Where(t => typeof(StarMapMethodAttribute).IsAssignableFrom(t) && @@ -62,191 +61,85 @@ private void PrepareMods() foreach (var mod in mods) { - var modPath = Path.Combine(path, mod.Id); - var starMapConfig = TomletMain.To(File.ReadAllText(Path.Combine(modPath, "mod.toml"))); - if (starMapConfig.StarMap is null) - continue; - - if (!LoadMod(mod.Id, modPath, starMapConfig.StarMap)) + if (!mod.Enabled) { + Console.WriteLine($"StarMap - Nod loading mod: {mod.Id} because it is disable in manifest"); continue; } - if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.BeforeMainAction is MethodInfo action) - { - action.Invoke(modInfo.ModInstance, []); - } - } + var modPath = Path.Combine(path, mod.Id); - var loadedMod = true; + if (!RuntimeMod.TryCreateMod(mod, _coreAssemblyLoadContext, out var runtimeMod)) + continue; - while (_modDependencyGraph.Count > 0 && loadedMod) - { - loadedMod = false; - foreach (var waitingMod in _waitingMods) - { - if (waitingMod.NotLoadedModDependencies.Count == 0 || waitingMod.NotLoadedModDependencies.Values.All(dependencyInfo => dependencyInfo.Optional)) - { - loadedMod = true; - _waitingMods.Remove(waitingMod); + ModRegistry.Add(runtimeMod); - if (InitializeMod(waitingMod)) - { - Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); - } - else - { - Console.WriteLine($"StarMap - Failed to load mod:{ waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); - } - waitingMod.NotLoadedModDependencies.Clear(); - } + if (!runtimeMod.AllDependenciesLoaded(ModRegistry)) + { + Console.WriteLine($"StarMap - Delaying load of mod: {runtimeMod.ModId} due to missing dependencies: {string.Join(", ", runtimeMod.NotLoadedModDependencies.Keys)}"); + continue; } - } - if (_waitingMods.Count > 0) - { - foreach (var waitingMod in _waitingMods) + if (!runtimeMod.InitializeMod(ModRegistry)) { - Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + Console.WriteLine($"StarMap - Failed to initialize mod: {runtimeMod.ModId} from manifest"); + continue; } - _waitingMods.Clear(); - } - } - - private bool LoadMod(string modId, string modDirectory, StarMapConfig config) - { - var fullPath = Path.GetFullPath(modDirectory); - var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); - var assemblyExists = File.Exists(modAssemblyFile); - - if (!assemblyExists) return false; - - var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext); - var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId }); - - var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); - if (modClass is null) return false; - - var modInfo = new ModInformation() { - ModId = modId, - ModAssemblyLoadContext = modLoadContext, - ModType = modClass, - Config = config, - }; - foreach(var exportedAssembly in config.ExportedAssemblies) - { - modInfo.ExportedAssemblies.Add(exportedAssembly); - } - - modLoadContext.ModInfo = modInfo; - _modRegistry.Add(modInfo); + Console.WriteLine($"StarMap - Loaded mod: {runtimeMod.ModId} from manifest"); - foreach (var dependency in config.ModDependencies) - { - if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) + var dependentMods = runtimeMod.CheckForDependentMods(ModRegistry); + + foreach (var dependentMod in dependentMods) { - modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); - if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) + if (dependentMod.InitializeMod(ModRegistry)) { - dependents = []; - _modDependencyGraph[dependency.ModId] = dependents; + Console.WriteLine($"StarMap - Loaded mod: {dependentMod.ModId} after loading {runtimeMod.ModId}"); + } + else + { + Console.WriteLine($"StarMap - Failed to load mod: {dependentMod.ModId} after loading {runtimeMod.ModId}"); } - dependents.Add(modInfo); - } - else - { - modInfo.Dependencies.Add(modDependency, [.. dependency.ImportedAssemblies]); } } - if (modInfo.NotLoadedModDependencies.Count > 0) - { - Console.WriteLine($"StarMap - Delaying load of mod: {modInfo.ModId} due to missing dependencies: {string.Join(", ", modInfo.NotLoadedModDependencies.Keys)}"); - _waitingMods.Add(modInfo); - return false; - } - - if (!InitializeMod(modInfo)) - { - Console.WriteLine($"StarMap - Failed to initialize mod: {modInfo.ModId} from {modAssemblyFile}"); - return false; - } + TryLoadWaitingMods(); + } - Console.WriteLine($"StarMap - Loaded mod: {modInfo.ModId} from {modAssemblyFile}"); + private void TryLoadWaitingMods() + { + var loadedMod = true; - if (_modDependencyGraph.TryGetValue(modInfo.ModId, out var modDependents)) + while (ModRegistry.WaitingModsDependencyGraph.Count > 0 && loadedMod) { - foreach(var modDependent in modDependents) + loadedMod = false; + foreach (var waitingMod in ModRegistry.WaitingMods) { - var dependencyInfo = modDependent.NotLoadedModDependencies[modInfo.ModId]; - modDependent.Dependencies.Add(modInfo, [.. dependencyInfo.ImportedAssemblies]); - if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) + if (waitingMod.NotLoadedModDependencies.Count == 0 || waitingMod.NotLoadedModDependencies.Values.All(dependencyInfo => dependencyInfo.Optional)) { - _waitingMods.Remove(modDependent); - if (InitializeMod(modDependent)) + loadedMod = true; + ModRegistry.WaitingMods.Remove(waitingMod); + + if (waitingMod.InitializeMod(ModRegistry)) { - Console.WriteLine($"StarMap - Loaded mod: {modDependent.ModId} after loading {modInfo.ModId}"); + Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } else { - Console.WriteLine($"StarMap - Failed to load mod: {modDependent.ModId} after loading {modInfo.ModId}"); + Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } + waitingMod.NotLoadedModDependencies.Clear(); } } - _modDependencyGraph.Remove(modInfo.ModId); } - return true; - } - - private bool InitializeMod(ModInformation modInfo) - { - var modObject = Activator.CreateInstance(modInfo.ModType); - if (modObject is null) return false; - modInfo.ModInstance = modObject; - modInfo.Initialized = true; - - var classMethods = modInfo.ModType.GetMethods(); - var immediateLoadMethods = new List(); - - foreach (var classMethod in classMethods) + if (ModRegistry.WaitingMods.Count > 0) { - var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); - foreach (var stringAttr in stringAttrs) + foreach (var waitingMod in ModRegistry.WaitingMods) { - var attr = _registeredMethodAttributes[stringAttr]; - - if (!attr.IsValidSignature(classMethod)) continue; - - if (attr.GetType() == typeof(StarMapImmediateLoadAttribute)) - { - immediateLoadMethods.Add(classMethod); - } - - _modRegistry.AddModMethod(modInfo.ModId, attr, modObject, classMethod); + Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } - } - - foreach (var assembly in modInfo.Config?.ExportedAssemblies ?? []) - modInfo.ExportedAssemblies.Add(assembly); - - return true; - } - - public void ModPrepareSystems(Mod mod) - { - if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.PrepareSystemsAction is MethodInfo action) - { - action.Invoke(modInfo.ModInstance, [mod]); - } - } - - public void OnAllModsLoaded() - { - foreach (var (_, @object, method) in _modRegistry.Get()) - { - method.Invoke(@object, []); + ModRegistry.WaitingMods.Clear(); } } diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 3a11a30..04ae34f 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -6,48 +6,31 @@ namespace StarMap.Core.ModRepository { - internal sealed class ModInformation - { - public required string ModId { get; init; } - public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } - public required Type ModType { get; init; } - public required StarMapConfig Config { get; init; } - - public bool Initialized { get; set; } = false; - public object? ModInstance { get; set; } = null; - - public HashSet ExportedAssemblies { get; set; } = []; - public Dictionary> Dependencies { get; set; } = []; - public Dictionary NotLoadedModDependencies { get; set; } = []; - - - public MethodInfo? BeforeMainAction { get; set; } = null; - public MethodInfo? PrepareSystemsAction { get; set; } = null; - } - internal sealed class ModRegistry : IDisposable { - private readonly Dictionary _mods = []; + public Dictionary> WaitingModsDependencyGraph { get; } = []; + public HashSet WaitingMods { get; } = []; + + private readonly Dictionary _mods = []; private readonly Dictionary> _modMethods = []; public bool ModLoaded(string modId) => _mods.ContainsKey(modId); - public bool TryGetMod(string modId, [NotNullWhen(true)] out ModInformation? modInfo) + public bool TryGetMod(string modId, [NotNullWhen(true)] out RuntimeMod? modInfo) { return _mods.TryGetValue(modId, out modInfo); } - public void Add(ModInformation modInfo) + public void Add(RuntimeMod modInfo) { _mods.Add(modInfo.ModId, modInfo); } - public IEnumerable GetMods() + public IEnumerable GetMods() { return _mods.Values; } - public void AddModMethod(string modId, StarMapMethodAttribute methodAttribute, object @object, MethodInfo method) { if (!_mods.TryGetValue(modId, out var modInfo)) return; diff --git a/StarMap.Core/ModRepository/RuntimeMod.cs b/StarMap.Core/ModRepository/RuntimeMod.cs new file mode 100644 index 0000000..25bea2e --- /dev/null +++ b/StarMap.Core/ModRepository/RuntimeMod.cs @@ -0,0 +1,176 @@ +using KSA; +using StarMap.API; +using StarMap.Core.Config; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using Tomlet; + +namespace StarMap.Core.ModRepository +{ + internal class RuntimeMod + { + private static readonly string _rootContentPath = Path.Combine(["Content"]); + + public required string ModId { get; init; } + public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } + public required Type ModType { get; init; } + public required StarMapConfig Config { get; init; } + + public bool Initialized { get; set; } = false; + public object? ModInstance { get; set; } = null; + + public HashSet ExportedAssemblies { get; set; } = []; + public Dictionary> Dependencies { get; set; } = []; + public Dictionary NotLoadedModDependencies { get; set; } = []; + + public MethodInfo? BeforeMainAction { get; set; } = null; + public MethodInfo? PrepareSystemsAction { get; set; } = null; + + + public static bool TryCreateMod(ModEntry manifestEntry, AssemblyLoadContext coreALC, [NotNullWhen(true)] out RuntimeMod? runtimeMod) + { + runtimeMod = null; + + var modPath = Path.Combine(_rootContentPath, manifestEntry.Id); + var modTomlPath = Path.Combine(modPath, "mod.toml"); + if (!File.Exists(modTomlPath)) return false; + var tomlConfig = TomletMain.To(File.ReadAllText(modTomlPath)); + if (tomlConfig?.StarMap is not StarMapConfig starMapConfig) + { + starMapConfig = new StarMapConfig + { + EntryAssembly = manifestEntry.Id, + }; + } + + var modAssemblyFile = Path.Combine(modPath, $"{starMapConfig.EntryAssembly}.dll"); + var assemblyExists = File.Exists(modAssemblyFile); + + if (!assemblyExists) return false; + + var modLoadContext = new ModAssemblyLoadContext(manifestEntry.Id, modAssemblyFile, coreALC); + var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = starMapConfig.EntryAssembly }); + + var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); + if (modClass is null) return false; + + runtimeMod = new RuntimeMod + { + ModId = manifestEntry.Id, + ModAssemblyLoadContext = modLoadContext, + ModType = modClass, + Config = starMapConfig, + }; + + modLoadContext.RuntimeMod = runtimeMod; + + return true; + } + + public bool AllDependenciesLoaded(ModRegistry modRegistry) + { + foreach (var dependency in Config.ModDependencies) + { + if (!modRegistry.TryGetMod(dependency.ModId, out var modDependency)) + { + NotLoadedModDependencies.Add(dependency.ModId, dependency); + + if (!modRegistry.WaitingModsDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) + { + dependents = []; + modRegistry.WaitingModsDependencyGraph[dependency.ModId] = dependents; + } + dependents.Add(this); + } + else + { + Dependencies.Add(modDependency, [.. CalculateUseableAssemblies(modDependency, dependency)]); + } + } + + if (NotLoadedModDependencies.Count > 0) + { + + modRegistry.WaitingMods.Add(this); + return false; + } + + return true; + } + + private static IEnumerable CalculateUseableAssemblies(RuntimeMod dependency, StarMapModDependency dependencyInfo) + { + + var hasImportedAssemblies = dependencyInfo.ImportedAssemblies.Count > 0; + var hasExportedAssemblies = dependency.ExportedAssemblies.Count > 0; + + if (!hasImportedAssemblies && !hasExportedAssemblies) + { + return [dependency.Config.EntryAssembly]; + } + + if (hasImportedAssemblies && !hasExportedAssemblies) + return dependencyInfo.ImportedAssemblies; + + if (!hasImportedAssemblies && hasExportedAssemblies) + return dependency.ExportedAssemblies; + + return dependency.ExportedAssemblies.Intersect(dependencyInfo.ImportedAssemblies); + } + + public bool InitializeMod(ModRegistry modRegistry) + { + var modObject = Activator.CreateInstance(ModType); + if (modObject is null) return false; + ModInstance = modObject; + Initialized = true; + + var classMethods = ModType.GetMethods(); + + foreach (var classMethod in classMethods) + { + var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(ModLoader.RegisteredMethodAttributes.Keys.Contains); + foreach (var stringAttr in stringAttrs) + { + var attr = ModLoader.RegisteredMethodAttributes[stringAttr]; + + if (!attr.IsValidSignature(classMethod)) continue; + + modRegistry.AddModMethod(ModId, attr, modObject, classMethod); + } + } + + if (modRegistry.TryGetMod(ModId, out var modInfo) && modInfo.BeforeMainAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, []); + } + + return true; + } + + public List CheckForDependentMods(ModRegistry modRegistry) + { + List loadableMods = []; + if (modRegistry.WaitingModsDependencyGraph.TryGetValue(ModId, out var modDependents)) + { + foreach (var modDependent in modDependents) + { + var dependencyInfo = modDependent.NotLoadedModDependencies[ModId]; + modDependent.Dependencies.Add(this, [.. dependencyInfo.ImportedAssemblies]); + if (modDependent.NotLoadedModDependencies.Remove(ModId) && modDependent.NotLoadedModDependencies.Count == 0) + { + modRegistry.WaitingMods.Remove(modDependent); + loadableMods.Add(modDependent); + } + } + modRegistry.WaitingModsDependencyGraph.Remove(ModId); + } + return loadableMods; + } + } +} diff --git a/StarMap.Core/Patches/ModLibraryPatches.cs b/StarMap.Core/Patches/ModLibraryPatches.cs index cb87311..ab5fcea 100644 --- a/StarMap.Core/Patches/ModLibraryPatches.cs +++ b/StarMap.Core/Patches/ModLibraryPatches.cs @@ -1,5 +1,7 @@ using HarmonyLib; using KSA; +using StarMap.API; +using StarMap.Core.ModRepository; namespace StarMap.Core.Patches { @@ -10,7 +12,13 @@ internal class ModLibraryPatches [HarmonyPostfix] public static void AfterLoad() { - StarMapCore.Instance?.LoadedMods.OnAllModsLoaded(); + var modRegistry = StarMapCore.Instance?.Loader.ModRegistry; + if (modRegistry is not ModRegistry registry) return; + + foreach (var (_, @object, method) in registry.Get()) + { + method.Invoke(@object, []); + } } } } diff --git a/StarMap.Core/Patches/ModPatches.cs b/StarMap.Core/Patches/ModPatches.cs index 8d01a8e..8829746 100644 --- a/StarMap.Core/Patches/ModPatches.cs +++ b/StarMap.Core/Patches/ModPatches.cs @@ -12,7 +12,13 @@ internal static class ModPatches [HarmonyPrefix] public static void OnLoadMod(this Mod __instance) { - StarMapCore.Instance?.LoadedMods.ModPrepareSystems(__instance); + var modRegistry = StarMapCore.Instance?.Loader.ModRegistry; + if (modRegistry is not ModRegistry registry) return; + + if (registry.TryGetMod(__instance.Id, out var modInfo) && modInfo.PrepareSystemsAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, [__instance]); + } } } } diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index dd31775..495b4b7 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -13,7 +13,7 @@ internal static class ProgramPatcher [HarmonyPrefix] public static void BeforeOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { @@ -25,7 +25,7 @@ public static void BeforeOnDrawUi(double dt) [HarmonyPostfix] public static void AfterOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 6a7997d..828dbfd 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -12,26 +12,26 @@ internal class StarMapCore : IStarMapCore private readonly Harmony _harmony = new("StarMap.Core"); private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly ModLoader _loadedMods; - public ModLoader LoadedMods => _loadedMods; + private readonly ModLoader _loader; + public ModLoader Loader => _loader; public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) { Instance = this; _coreAssemblyLoadContext = coreAssemblyLoadContext; - _loadedMods = new(_coreAssemblyLoadContext); + _loader = new(_coreAssemblyLoadContext); } public void Init() { - _loadedMods.Init(); + _loader.Init(); _harmony.PatchAll(typeof(StarMapCore).Assembly); } public void DeInit() { _harmony.UnpatchAll(); - _loadedMods.Dispose(); + _loader.Dispose(); } } } From 6bb4491f7494b528705351bda433f7fc9d568f7a Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 19:50:23 +0100 Subject: [PATCH 06/14] Merge pull request #49 from StarMapLoader/dev Do not try to unload the assembly load contexts (#44) Add StarMap hook for after onFrame --- StarMap.API/BaseAttributes.cs | 1 - StarMap.API/README.md | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index d5ecb5a..1134976 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -81,7 +81,6 @@ public override bool IsValidSignature(MethodInfo method) /// /// 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: diff --git a/StarMap.API/README.md b/StarMap.API/README.md index d3604b5..94bb062 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -14,10 +14,10 @@ Every mod will at minimum contain a mod.toml (which is also the case for KSA mod While it is not stricly neccesary, it is adviced to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: ```toml -name = "StarMap.SimpleMod" +name = "MyAmazingMod" [StarMap] -EntryAssembly = "StarMap.SimpleMod" +EntryAssembly = "MyAmazingMod" ``` The name will be the modid of your mod, this should be the same name as the folder in which this lives in the Content folder. @@ -44,12 +44,12 @@ First of all, mods can configure what assemblies should be exposed to other mods In below example, only the StarMap.SimpleMod.Dependency(.dll) assembly will be accessable from other mods (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). ```toml -name = "StarMap.SimpleMod2" +name = "MyOtherAmazingMod" [StarMap] -EntryAssembly = "StarMap.SimpleMod2" +EntryAssembly = "MyOtherAmazingMod" ExportedAssemblies = [ - "StarMap.SimpleMod.Dependency" + "MyDependency" ] ``` @@ -58,16 +58,16 @@ ExportedAssemblies = [ Then, mods can define what mods they want to define on, they can do this by adding a new ModDependencies list entry in the mod.toml ```toml -name = "StarMap.SimpleMod" +name = "MyAmazingMod" [StarMap] -EntryAssembly = "StarMap.SimpleMod" +EntryAssembly = "MyAmazingMod" [[StarMap.ModDependencies]] -ModId = "StarMap.SimpleMod2" +ModId = "MyOtherAmazingMod" Optional = false ImportedAssemblies = [ - "StarMap.SimpleMod.Dependency" + "MyDependency" ] ``` From 60e0dc4430e57686962331c368164a7661b724b9 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Sat, 29 Nov 2025 12:41:01 +0100 Subject: [PATCH 07/14] Add dependencies to mods and use them for mod loading order --- StarMap.API/BaseAttributes.cs | 61 ++++ .../ModRepository/LoadedModRepository.cs | 156 ---------- .../ModRepository/ModAssemblyLoadContext.cs | 15 +- StarMap.Core/ModRepository/ModLoader.cs | 274 ++++++++++++++++++ StarMap.Core/ModRepository/ModRegistry.cs | 77 +++-- StarMap.Core/Patches/ModPatches.cs | 2 + StarMap.Core/Patches/ProgramPatcher.cs | 4 +- StarMap.Core/StarMapCore.cs | 4 +- 8 files changed, 407 insertions(+), 186 deletions(-) delete mode 100644 StarMap.Core/ModRepository/LoadedModRepository.cs create mode 100644 StarMap.Core/ModRepository/ModLoader.cs diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index d5ecb5a..b427812 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -12,6 +12,67 @@ public class StarMapModAttribute : Attribute { } + public class StarMapDependencyInfo + { + public required string ModId { get; init; } + public required bool Optional { get; init; } + } + + /// + /// Should mark a property that returns a list of mod IDs that this mod depends on. + /// Any mod in this list will be loaded before this mod is loaded, if a dependency is never loaded, this most will also not be loaded + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public static StarMapDependencyInfo[] PropertyName { get; } + /// + /// + /// Specifically: + /// + /// Return type must be . + /// Property must be a static property. + /// + /// + + [AttributeUsage(AttributeTargets.Property)] + public class StarMapDependenciesAttribute : Attribute + { + public bool IsValidSignature(PropertyInfo property) + { + return property.PropertyType == typeof(StarMapDependencyInfo[]); + } + } + + /// + /// Should mark a property that returns a list of assembly names other mods can access. + /// When other mods, that depend on this mod, try to load an assembly, they will first check this list and if it is in there, load it from this mods load context. + /// The assembly names should not contain any versions or .dll suffixes, just the raw assembly name. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public static string[] PropertyName { get; } + /// + /// + /// Specifically: + /// + /// Return type must be . + /// Property must be a static property. + /// + /// + + [AttributeUsage(AttributeTargets.Property)] + public class StarMapExportedAssemblyAttribute : Attribute + { + public bool IsValidSignature(PropertyInfo property) + { + return property.PropertyType == typeof(string[]); + } + } + [AttributeUsage(AttributeTargets.Method)] public abstract class StarMapMethodAttribute : Attribute { diff --git a/StarMap.Core/ModRepository/LoadedModRepository.cs b/StarMap.Core/ModRepository/LoadedModRepository.cs deleted file mode 100644 index b1177d1..0000000 --- a/StarMap.Core/ModRepository/LoadedModRepository.cs +++ /dev/null @@ -1,156 +0,0 @@ -using KSA; -using StarMap.API; -using System.Reflection; -using System.Runtime.Loader; - -namespace StarMap.Core.ModRepository -{ - internal class LoadedModRepository : IDisposable - { - private readonly AssemblyLoadContext _coreAssemblyLoadContext; - - private readonly Dictionary _registeredMethodAttributes = []; - private readonly Dictionary _attemptedMods = []; - private readonly Dictionary _modLoadContexts = []; - - 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; - - Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; - - _registeredMethodAttributes = coreAssembly - .GetTypes() - .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 Init() - { - PrepareMods(); - } - - private void PrepareMods() - { - var loadedManifest = ModLibrary.PrepareManifest(); - - if (!loadedManifest) return; - - var mods = ModLibrary.Manifest.Mods; - if (mods is null) return; - - string rootPath = "Content"; - string path = Path.Combine(new ReadOnlySpan(in rootPath)); - - foreach (var mod in mods) - { - var modPath = Path.Combine(path, mod.Id); - - if (!LoadMod(mod.Id, modPath)) - { - _attemptedMods[mod.Id] = false; - continue; - } - - if (_mods.GetBeforeMainAction(mod.Id) is (object @object, MethodInfo method)) - { - method.Invoke(@object, []); - } - _attemptedMods[mod.Id] = true; - } - } - - public void ModPrepareSystems(Mod mod) - { - if (!_attemptedMods.TryGetValue(mod.Id, out var succeeded)) - { - succeeded = LoadMod(mod.Id, mod.DirectoryPath); - } - - if (!succeeded) return; - - if (_mods.GetPrepareSystemsAction(mod.Id) is (object @object, MethodInfo method)) - { - method.Invoke(@object, [mod]); - } - } - - private bool LoadMod(string modId, string modDirectory) - { - var fullPath = Path.GetFullPath(modDirectory); - var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); - var assemblyExists = File.Exists(modAssemblyFile); - - if (!assemblyExists) return false; - - var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext); - var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId }); - - var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); - if (modClass is null) return false; - - var modObject = Activator.CreateInstance(modClass); - if (modObject is null) return false; - - _modLoadContexts.Add(modId, modLoadContext); - - var classMethods = modClass.GetMethods(); - var immediateLoadMethods = new List(); - - foreach (var classMethod in classMethods) - { - var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); - foreach (var stringAttr in stringAttrs) - { - var attr = _registeredMethodAttributes[stringAttr]; - - if (!attr.IsValidSignature(classMethod)) continue; - - if (attr.GetType() == typeof(StarMapImmediateLoadAttribute)) - { - immediateLoadMethods.Add(classMethod); - } - - _mods.Add(modId, attr, modObject, classMethod); - } - } - - Console.WriteLine($"StarMap - Loaded mod: {modId} from {modAssemblyFile}"); - return true; - } - - public void OnAllModsLoaded() - { - foreach (var (_, @object, method) in _mods.Get()) - { - method.Invoke(@object, []); - } - } - - public void Dispose() - { - foreach (var (_, @object, method) in _mods.Get()) - { - method.Invoke(@object, []); - } - - _mods.Dispose(); - } - } -} diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index d6dbd6f..78e6e69 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -9,6 +9,8 @@ internal class ModAssemblyLoadContext : AssemblyLoadContext private readonly AssemblyLoadContext _coreAssemblyLoadContext; private readonly AssemblyDependencyResolver _modDependencyResolver; + public ModInformation? ModInfo { get; set; } + public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadContext coreAssemblyContext) : base() { @@ -21,7 +23,7 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon protected override Assembly? Load(AssemblyName assemblyName) { - var existingInDefault = AssemblyLoadContext.Default.Assemblies + var existingInDefault = Default.Assemblies .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); if (existingInDefault != null) return existingInDefault; @@ -44,6 +46,17 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon } } + if (ModInfo is ModInformation modInfo && modInfo.DependencyContexts.Count > 0) + { + foreach (var context in modInfo.DependencyContexts) + { + var existsInDependencyContext = context.Assemblies + .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); + if (existsInDependencyContext != null) + return existsInDependencyContext; + } + } + var foundPath = _modDependencyResolver.ResolveAssemblyToPath(assemblyName); if (foundPath is null) return null; diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs new file mode 100644 index 0000000..c2862c4 --- /dev/null +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -0,0 +1,274 @@ +using KSA; +using StarMap.API; +using System.Reflection; +using System.Runtime.Loader; + +namespace StarMap.Core.ModRepository +{ + internal sealed class ModLoader : IDisposable + { + private readonly AssemblyLoadContext _coreAssemblyLoadContext; + + private readonly Dictionary _registeredMethodAttributes = []; + private readonly Dictionary> _modDependencyGraph = []; + private readonly HashSet _waitingMods = []; + + private readonly ModRegistry _modRegistry = new(); + public ModRegistry ModRegistry => _modRegistry; + + 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 ModLoader(AssemblyLoadContext coreAssemblyLoadContext) + { + _coreAssemblyLoadContext = coreAssemblyLoadContext; + + Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; + + _registeredMethodAttributes = coreAssembly + .GetTypes() + .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 Init() + { + PrepareMods(); + } + + private void PrepareMods() + { + var loadedManifest = ModLibrary.PrepareManifest(); + + if (!loadedManifest) return; + + var mods = ModLibrary.Manifest.Mods; + if (mods is null) return; + + string rootPath = "Content"; + string path = Path.Combine(new ReadOnlySpan(in rootPath)); + + foreach (var mod in mods) + { + var modPath = Path.Combine(path, mod.Id); + + if (!LoadMod(mod.Id, modPath)) + { + continue; + } + + if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.BeforeMainAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, []); + } + } + + var loadedMod = true; + + while (_modDependencyGraph.Count > 0 && loadedMod) + { + loadedMod = false; + foreach (var waitingMod in _waitingMods) + { + if (waitingMod.NotLoadedModDependencies.Count == 0 || waitingMod.NotLoadedModDependencies.Values.All(dependencyInfo => dependencyInfo.Optional)) + { + loadedMod = true; + _waitingMods.Remove(waitingMod); + + if (InitializeMod(waitingMod)) + { + Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + } + else + { + Console.WriteLine($"StarMap - Failed to load mod:{ waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + } + waitingMod.NotLoadedModDependencies.Clear(); + } + } + } + + if (_waitingMods.Count > 0) + { + foreach (var waitingMod in _waitingMods) + { + Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + } + } + } + + private bool LoadMod(string modId, string modDirectory) + { + var fullPath = Path.GetFullPath(modDirectory); + var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); + var assemblyExists = File.Exists(modAssemblyFile); + + if (!assemblyExists) return false; + + var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext); + var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId }); + + var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); + if (modClass is null) return false; + + var modInfo = new ModInformation() { + ModId = modId, + ModAssemblyLoadContext = modLoadContext, + ModType = modClass + }; + + modLoadContext.ModInfo = modInfo; + + _modRegistry.Add(modInfo); + + var dependencyPropertyField = modClass + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapDependenciesAttribute).Name)); + + if (dependencyPropertyField is PropertyInfo dependencyProperty) + { + object? value = dependencyProperty.GetValue(null); // null because static + if (value is StarMapDependencyInfo[] dependencies) + { + foreach (var dependency in dependencies) + { + if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) + { + modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); + if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) + { + dependents = []; + _modDependencyGraph[dependency.ModId] = dependents; + } + dependents.Add(modInfo); + } + else + { + modInfo.DependencyContexts.Add(modDependency.ModAssemblyLoadContext); + } + } + } + } + + if (modInfo.NotLoadedModDependencies.Count > 0) + { + Console.WriteLine($"StarMap - Delaying load of mod: {modInfo.ModId} due to missing dependencies: {string.Join(", ", modInfo.NotLoadedModDependencies.Keys)}"); + _waitingMods.Add(modInfo); + return false; + } + + if (!InitializeMod(modInfo)) + { + Console.WriteLine($"StarMap - Failed to initialize mod: {modInfo.ModId} from {modAssemblyFile}"); + return false; + } + + Console.WriteLine($"StarMap - Loaded mod: {modInfo.ModId} from {modAssemblyFile}"); + + if (_modDependencyGraph.TryGetValue(modInfo.ModId, out var modDependents)) + { + foreach(var modDependent in modDependents) + { + modDependent.DependencyContexts.Add(modInfo.ModAssemblyLoadContext); + if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) + { + _waitingMods.Remove(modDependent); + if (InitializeMod(modDependent)) + { + Console.WriteLine($"StarMap - Loaded mod: {modDependent.ModId} after loading {modInfo.ModId}"); + } + else + { + Console.WriteLine($"StarMap - Failed to load mod: {modDependent.ModId} after loading {modInfo.ModId}"); + } + } + } + _modDependencyGraph.Remove(modInfo.ModId); + } + + return true; + } + + private bool InitializeMod(ModInformation modInfo) + { + var modObject = Activator.CreateInstance(modInfo.ModType); + if (modObject is null) return false; + modInfo.ModInstance = modObject; + modInfo.Initialized = true; + + var classMethods = modInfo.ModType.GetMethods(); + var immediateLoadMethods = new List(); + + foreach (var classMethod in classMethods) + { + var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); + foreach (var stringAttr in stringAttrs) + { + var attr = _registeredMethodAttributes[stringAttr]; + + if (!attr.IsValidSignature(classMethod)) continue; + + if (attr.GetType() == typeof(StarMapImmediateLoadAttribute)) + { + immediateLoadMethods.Add(classMethod); + } + + _modRegistry.AddModMethod(modInfo.ModId, attr, modObject, classMethod); + } + } + + var exportedAssemblyAttribute = modInfo.ModType + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapExportedAssemblyAttribute).Name)); + + if (exportedAssemblyAttribute is PropertyInfo dependencyProperty) + { + object? value = dependencyProperty.GetValue(null); // null because static + if (value is string[] exportedAssemblies) + { + modInfo.ExportedAssemblies.AddRange(exportedAssemblies); + } + } + + return true; + } + + public void ModPrepareSystems(Mod mod) + { + if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.PrepareSystemsAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, [mod]); + } + } + + public void OnAllModsLoaded() + { + + + foreach (var (_, @object, method) in _modRegistry.Get()) + { + method.Invoke(@object, []); + } + } + + public void Dispose() + { + foreach (var (_, @object, method) in _modRegistry.Get()) + { + method.Invoke(@object, []); + } + + _modRegistry.Dispose(); + } + } +} diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 40e1b3b..18548e1 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -1,38 +1,75 @@ using KSA; using StarMap.API; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace StarMap.Core.ModRepository { + internal sealed class ModInformation + { + public required string ModId { get; init; } + public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } + public required Type ModType { get; init; } + public bool Initialized { get; set; } = false; + public object? ModInstance { get; set; } = null; + + public List ExportedAssemblies { get; set; } = []; + + public HashSet DependencyContexts = []; + public Dictionary NotLoadedModDependencies = []; + + public MethodInfo? BeforeMainAction { get; set; } = null; + public MethodInfo? PrepareSystemsAction { get; set; } = null; + } + internal sealed class ModRegistry : IDisposable { - private readonly Dictionary> _map = []; - private readonly Dictionary _beforeMainActions = []; - private readonly Dictionary _prepareSystemsActions = []; + private readonly Dictionary _mods = []; + private readonly Dictionary> _modMethods = []; + + public bool ModLoaded(string modId) => _mods.ContainsKey(modId); + + public bool TryGetMod(string modId, [NotNullWhen(true)] out ModInformation? modInfo) + { + return _mods.TryGetValue(modId, out modInfo); + } + + public void Add(ModInformation modInfo) + { + _mods.Add(modInfo.ModId, modInfo); + } + + public IEnumerable GetMods() + { + return _mods.Values; + } + - public void Add(string modId, StarMapMethodAttribute attribute, object @object, MethodInfo method) + public void AddModMethod(string modId, StarMapMethodAttribute methodAttribute, object @object, MethodInfo method) { - var attributeType = attribute.GetType(); + if (!_mods.TryGetValue(modId, out var modInfo)) return; + + var attributeType = methodAttribute.GetType(); - if (!_map.TryGetValue(attributeType, out var list)) + if (!_modMethods.TryGetValue(attributeType, out var list)) { list = []; - _map[attributeType] = list; + _modMethods[attributeType] = list; } - if (attribute.GetType() == typeof(StarMapBeforeMainAttribute)) - _beforeMainActions[modId] = (@object, method); + if (methodAttribute.GetType() == typeof(StarMapBeforeMainAttribute)) + modInfo.BeforeMainAction = method; - if (attribute.GetType() == typeof(StarMapImmediateLoadAttribute)) - _prepareSystemsActions[modId] = (@object, method); + if (methodAttribute.GetType() == typeof(StarMapImmediateLoadAttribute)) + modInfo.PrepareSystemsAction = method; - list.Add((attribute, @object, method)); + list.Add((methodAttribute, @object, method)); } public IReadOnlyList<(StarMapMethodAttribute attribute, object @object, MethodInfo method)> Get() where TAttribute : Attribute { - if (_map.TryGetValue(typeof(TAttribute), out var list)) + if (_modMethods.TryGetValue(typeof(TAttribute), out var list)) { return list.Cast<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>().ToList(); } @@ -42,24 +79,14 @@ public void Add(string modId, StarMapMethodAttribute attribute, object @object, public IReadOnlyList<(StarMapMethodAttribute attribute, object @object, MethodInfo method)> Get(Type iface) { - return _map.TryGetValue(iface, out var list) + return _modMethods.TryGetValue(iface, out var list) ? list : Array.Empty<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>(); } - public (object @object, MethodInfo method)? GetBeforeMainAction(string modId) - { - return _beforeMainActions.TryGetValue(modId, out var action) ? action : null; - } - - public (object @object, MethodInfo method)? GetPrepareSystemsAction(string modId) - { - return _prepareSystemsActions.TryGetValue(modId, out var action) ? action : null; - } - public void Dispose() { - _map.Clear(); + _modMethods.Clear(); } } } diff --git a/StarMap.Core/Patches/ModPatches.cs b/StarMap.Core/Patches/ModPatches.cs index 69ce5b0..8d01a8e 100644 --- a/StarMap.Core/Patches/ModPatches.cs +++ b/StarMap.Core/Patches/ModPatches.cs @@ -1,5 +1,7 @@ using HarmonyLib; using KSA; +using StarMap.Core.ModRepository; +using System.Reflection; namespace StarMap.Core.Patches { diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index 9bc2710..78b8184 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -14,7 +14,7 @@ internal static class ProgramPatcher [HarmonyPrefix] public static void BeforeOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { @@ -26,7 +26,7 @@ public static void BeforeOnDrawUi(double dt) [HarmonyPostfix] public static void AfterOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 4f54870..6a7997d 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -12,8 +12,8 @@ internal class StarMapCore : IStarMapCore private readonly Harmony _harmony = new("StarMap.Core"); private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly LoadedModRepository _loadedMods; - public LoadedModRepository LoadedMods => _loadedMods; + private readonly ModLoader _loadedMods; + public ModLoader LoadedMods => _loadedMods; public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) { From 7375c63693ab7ca8acb7683c232c197bc9d18bf9 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Sat, 13 Dec 2025 21:45:05 +0100 Subject: [PATCH 08/14] Allow mods to define what assemblies they want to export --- StarMap.API/README.md | 22 ++++++++++++++++++- .../ModRepository/ModAssemblyLoadContext.cs | 20 ++++++++++++----- StarMap.Core/ModRepository/ModLoader.cs | 9 ++++---- StarMap.Core/ModRepository/ModRegistry.cs | 4 ++-- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/StarMap.API/README.md b/StarMap.API/README.md index b414faa..b1f14d7 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -14,4 +14,24 @@ Any method within this class that has any of the attributes will used, so if two - StarMapUnload: Called when KSA is unloaded. - StarMapBeforeGui: Called just before KSA starts drawing its Ui. - StarMapAfterGui: Called after KSA has drawn its Ui. -- StarMapAfterOnFrame: Called after KSA calls Program.OnFrame + +## Dependencies + +Mods can define what mods they define on, as well as what assemblies they want to export themselves. +This only falls on assembly loading and load order within StarMap. +If a mod is set as a dependency, and it is present, it will be loaded before the mods that depends on it. +The dependent mod can then access any assembly that the dependency exposes, in addition to the main mod assembly, which is always exposed. + +This requires following attributes: + +- StarMapDependenciesAttribute: Describes what mods this mod dependends on. + + - This attribute should be placed on a static property that returns an array of StarMapDependencyInfo. + - The StarMapDependencyInfo provides info on the mod id of the mod that should be depended on, and if this dependency is optional. + - When a dependency is not optional, the mod will not be loaded when the dependency is not present. + - When a dependency is optional, the mod is loaded after all other mods are loaded, to ensure the dependency can be loaded before. + +- StarMapExportedAssemblyAttribute: Describes what assemblies are exposed by the mod. + - This attribute should be placed on a static property that returns an array of strings. + - The string should be the name of the assembly, with no ".dll" suffix. + - The main mod assembly (modid.dll) is always exposed, so this is only for other assemblies that other mods could depend on. diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index 78e6e69..6ec39b9 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -46,14 +46,22 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon } } - if (ModInfo is ModInformation modInfo && modInfo.DependencyContexts.Count > 0) + if (ModInfo is ModInformation modInfo && modInfo.Dependencies.Count > 0) { - foreach (var context in modInfo.DependencyContexts) + foreach (var dependency in modInfo.Dependencies) { - var existsInDependencyContext = context.Assemblies - .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); - if (existsInDependencyContext != null) - return existsInDependencyContext; + if (dependency.ModId == assemblyName.Name || dependency.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + { + try + { + var asm = dependency.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + if (asm != null) + return asm; + } + catch (FileNotFoundException) + { + } + } } } diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs index c2862c4..8b3ebc9 100644 --- a/StarMap.Core/ModRepository/ModLoader.cs +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -154,7 +154,7 @@ private bool LoadMod(string modId, string modDirectory) } else { - modInfo.DependencyContexts.Add(modDependency.ModAssemblyLoadContext); + modInfo.Dependencies.Add(modDependency); } } } @@ -179,7 +179,7 @@ private bool LoadMod(string modId, string modDirectory) { foreach(var modDependent in modDependents) { - modDependent.DependencyContexts.Add(modInfo.ModAssemblyLoadContext); + modDependent.Dependencies.Add(modInfo); if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) { _waitingMods.Remove(modDependent); @@ -236,7 +236,8 @@ private bool InitializeMod(ModInformation modInfo) object? value = dependencyProperty.GetValue(null); // null because static if (value is string[] exportedAssemblies) { - modInfo.ExportedAssemblies.AddRange(exportedAssemblies); + foreach (var assembly in exportedAssemblies) + modInfo.ExportedAssemblies.Add(assembly); } } @@ -253,8 +254,6 @@ public void ModPrepareSystems(Mod mod) public void OnAllModsLoaded() { - - foreach (var (_, @object, method) in _modRegistry.Get()) { method.Invoke(@object, []); diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 18548e1..e9f7cdd 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -13,9 +13,9 @@ internal sealed class ModInformation public bool Initialized { get; set; } = false; public object? ModInstance { get; set; } = null; - public List ExportedAssemblies { get; set; } = []; + public HashSet ExportedAssemblies { get; set; } = []; - public HashSet DependencyContexts = []; + public HashSet Dependencies = []; public Dictionary NotLoadedModDependencies = []; public MethodInfo? BeforeMainAction { get; set; } = null; From fb6680c44d4d2da7c3144e563dc2d9371cb2b880 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 16 Dec 2025 22:24:24 +0100 Subject: [PATCH 09/14] Alter dependency config to work using the mod.toml instead of static properties --- StarMap.API/BaseAttributes.cs | 61 ---------------- StarMap.API/README.md | 20 +----- StarMap.Core/Config/StarMapConfig.cs | 26 +++++++ .../ModRepository/ModAssemblyLoadContext.cs | 27 ++++++- StarMap.Core/ModRepository/ModLoader.cs | 70 ++++++++----------- StarMap.Core/ModRepository/ModRegistry.cs | 7 +- StarMap.Core/StarMap.Core.csproj | 3 + 7 files changed, 90 insertions(+), 124 deletions(-) create mode 100644 StarMap.Core/Config/StarMapConfig.cs diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index b427812..d5ecb5a 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -12,67 +12,6 @@ public class StarMapModAttribute : Attribute { } - public class StarMapDependencyInfo - { - public required string ModId { get; init; } - public required bool Optional { get; init; } - } - - /// - /// Should mark a property that returns a list of mod IDs that this mod depends on. - /// Any mod in this list will be loaded before this mod is loaded, if a dependency is never loaded, this most will also not be loaded - /// - /// - /// Methods using this attribute must match the following signature: - /// - /// - /// public static StarMapDependencyInfo[] PropertyName { get; } - /// - /// - /// Specifically: - /// - /// Return type must be . - /// Property must be a static property. - /// - /// - - [AttributeUsage(AttributeTargets.Property)] - public class StarMapDependenciesAttribute : Attribute - { - public bool IsValidSignature(PropertyInfo property) - { - return property.PropertyType == typeof(StarMapDependencyInfo[]); - } - } - - /// - /// Should mark a property that returns a list of assembly names other mods can access. - /// When other mods, that depend on this mod, try to load an assembly, they will first check this list and if it is in there, load it from this mods load context. - /// The assembly names should not contain any versions or .dll suffixes, just the raw assembly name. - /// - /// - /// Methods using this attribute must match the following signature: - /// - /// - /// public static string[] PropertyName { get; } - /// - /// - /// Specifically: - /// - /// Return type must be . - /// Property must be a static property. - /// - /// - - [AttributeUsage(AttributeTargets.Property)] - public class StarMapExportedAssemblyAttribute : Attribute - { - public bool IsValidSignature(PropertyInfo property) - { - return property.PropertyType == typeof(string[]); - } - } - [AttributeUsage(AttributeTargets.Method)] public abstract class StarMapMethodAttribute : Attribute { diff --git a/StarMap.API/README.md b/StarMap.API/README.md index b1f14d7..cca3f60 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -17,21 +17,5 @@ Any method within this class that has any of the attributes will used, so if two ## Dependencies -Mods can define what mods they define on, as well as what assemblies they want to export themselves. -This only falls on assembly loading and load order within StarMap. -If a mod is set as a dependency, and it is present, it will be loaded before the mods that depends on it. -The dependent mod can then access any assembly that the dependency exposes, in addition to the main mod assembly, which is always exposed. - -This requires following attributes: - -- StarMapDependenciesAttribute: Describes what mods this mod dependends on. - - - This attribute should be placed on a static property that returns an array of StarMapDependencyInfo. - - The StarMapDependencyInfo provides info on the mod id of the mod that should be depended on, and if this dependency is optional. - - When a dependency is not optional, the mod will not be loaded when the dependency is not present. - - When a dependency is optional, the mod is loaded after all other mods are loaded, to ensure the dependency can be loaded before. - -- StarMapExportedAssemblyAttribute: Describes what assemblies are exposed by the mod. - - This attribute should be placed on a static property that returns an array of strings. - - The string should be the name of the assembly, with no ".dll" suffix. - - The main mod assembly (modid.dll) is always exposed, so this is only for other assemblies that other mods could depend on. +Mods can define what mods they depend on, and what assemblies they want to import from that mod. +They can as well define what assemblies they want exported to other mods. diff --git a/StarMap.Core/Config/StarMapConfig.cs b/StarMap.Core/Config/StarMapConfig.cs new file mode 100644 index 0000000..57efc81 --- /dev/null +++ b/StarMap.Core/Config/StarMapConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace StarMap.Core.Config +{ + internal class RootConfig + { + public required StarMapConfig StarMap { get; set; } + } + + + internal class StarMapConfig + { + public required string EntryAssembly { get; set; } + public List ExportedAssemblies { get; set; } = []; + public List ModDependencies { get; set; } = []; + } + + internal class StarMapModDependency + { + public required string ModId { get; set; } + public bool Optional { get; set; } = false; + public List ImportedAssemblies { get; set; } = []; + } +} diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index 6ec39b9..b6c419f 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -48,13 +48,34 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon if (ModInfo is ModInformation modInfo && modInfo.Dependencies.Count > 0) { - foreach (var dependency in modInfo.Dependencies) + foreach (var (dependencyInfo, importedAssemblies) in modInfo.Dependencies) { - if (dependency.ModId == assemblyName.Name || dependency.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + bool ShouldTryLoad() + { + var hasExportedAssemblies = dependencyInfo.ExportedAssemblies.Count > 0; + var hasImportedAssemblies = importedAssemblies.Count > 0; + + if (!hasImportedAssemblies && !hasExportedAssemblies) { + if (dependencyInfo.Config.EntryAssembly == assemblyName.Name) + return true; + + return false; + } + + if (hasExportedAssemblies && !dependencyInfo.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + return false; + + if (hasImportedAssemblies && !importedAssemblies.Contains(assemblyName.Name ?? string.Empty)) + return false; + + return true; + } + + if (ShouldTryLoad()) { try { - var asm = dependency.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + var asm = dependencyInfo.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); if (asm != null) return asm; } diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs index 8b3ebc9..d4b4dd9 100644 --- a/StarMap.Core/ModRepository/ModLoader.cs +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -1,7 +1,9 @@ using KSA; using StarMap.API; +using StarMap.Core.Config; using System.Reflection; using System.Runtime.Loader; +using Tomlet; namespace StarMap.Core.ModRepository { @@ -61,8 +63,11 @@ private void PrepareMods() foreach (var mod in mods) { var modPath = Path.Combine(path, mod.Id); + var starMapConfig = TomletMain.To(File.ReadAllText(Path.Combine(modPath, "mod.toml"))); + if (starMapConfig.StarMap is null) + continue; - if (!LoadMod(mod.Id, modPath)) + if (!LoadMod(mod.Id, modPath, starMapConfig.StarMap)) { continue; } @@ -104,10 +109,11 @@ private void PrepareMods() { Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } + _waitingMods.Clear(); } } - private bool LoadMod(string modId, string modDirectory) + private bool LoadMod(string modId, string modDirectory, StarMapConfig config) { var fullPath = Path.GetFullPath(modDirectory); var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); @@ -124,39 +130,33 @@ private bool LoadMod(string modId, string modDirectory) var modInfo = new ModInformation() { ModId = modId, ModAssemblyLoadContext = modLoadContext, - ModType = modClass + ModType = modClass, + Config = config, }; - modLoadContext.ModInfo = modInfo; + foreach(var exportedAssembly in config.ExportedAssemblies) + { + modInfo.ExportedAssemblies.Add(exportedAssembly); + } + modLoadContext.ModInfo = modInfo; _modRegistry.Add(modInfo); - var dependencyPropertyField = modClass - .GetProperties(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapDependenciesAttribute).Name)); - - if (dependencyPropertyField is PropertyInfo dependencyProperty) + foreach (var dependency in config.ModDependencies) { - object? value = dependencyProperty.GetValue(null); // null because static - if (value is StarMapDependencyInfo[] dependencies) + if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) { - foreach (var dependency in dependencies) + modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); + if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) { - if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) - { - modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); - if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) - { - dependents = []; - _modDependencyGraph[dependency.ModId] = dependents; - } - dependents.Add(modInfo); - } - else - { - modInfo.Dependencies.Add(modDependency); - } + dependents = []; + _modDependencyGraph[dependency.ModId] = dependents; } + dependents.Add(modInfo); + } + else + { + modInfo.Dependencies.Add(modDependency, [.. dependency.ImportedAssemblies]); } } @@ -179,7 +179,8 @@ private bool LoadMod(string modId, string modDirectory) { foreach(var modDependent in modDependents) { - modDependent.Dependencies.Add(modInfo); + var dependencyInfo = modDependent.NotLoadedModDependencies[modInfo.ModId]; + modDependent.Dependencies.Add(modInfo, [.. dependencyInfo.ImportedAssemblies]); if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) { _waitingMods.Remove(modDependent); @@ -227,19 +228,8 @@ private bool InitializeMod(ModInformation modInfo) } } - var exportedAssemblyAttribute = modInfo.ModType - .GetProperties(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(prop => prop.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapExportedAssemblyAttribute).Name)); - - if (exportedAssemblyAttribute is PropertyInfo dependencyProperty) - { - object? value = dependencyProperty.GetValue(null); // null because static - if (value is string[] exportedAssemblies) - { - foreach (var assembly in exportedAssemblies) - modInfo.ExportedAssemblies.Add(assembly); - } - } + foreach (var assembly in modInfo.Config?.ExportedAssemblies ?? []) + modInfo.ExportedAssemblies.Add(assembly); return true; } diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index e9f7cdd..3a11a30 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -1,5 +1,6 @@ using KSA; using StarMap.API; +using StarMap.Core.Config; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -10,13 +11,15 @@ internal sealed class ModInformation public required string ModId { get; init; } public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } public required Type ModType { get; init; } + public required StarMapConfig Config { get; init; } + public bool Initialized { get; set; } = false; public object? ModInstance { get; set; } = null; public HashSet ExportedAssemblies { get; set; } = []; + public Dictionary> Dependencies { get; set; } = []; + public Dictionary NotLoadedModDependencies { get; set; } = []; - public HashSet Dependencies = []; - public Dictionary NotLoadedModDependencies = []; public MethodInfo? BeforeMainAction { get; set; } = null; public MethodInfo? PrepareSystemsAction { get; set; } = null; diff --git a/StarMap.Core/StarMap.Core.csproj b/StarMap.Core/StarMap.Core.csproj index cf2929a..2e14983 100644 --- a/StarMap.Core/StarMap.Core.csproj +++ b/StarMap.Core/StarMap.Core.csproj @@ -8,6 +8,9 @@ + + runtime + From 3c7717b1a8bc44c0116b25476fc1a6d543cd3857 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 18:20:22 +0100 Subject: [PATCH 10/14] Add StarMap section to mod.toml and improve API documentation --- README.md | 2 +- StarMap.API/README.md | 216 ++++++++++++++++-- StarMap.API/StarMap.API.csproj | 37 +-- .../ModRepository/ModAssemblyLoadContext.cs | 35 +-- StarMap.Core/ModRepository/ModLoader.cs | 199 ++++------------ StarMap.Core/ModRepository/ModRegistry.cs | 31 +-- StarMap.Core/ModRepository/RuntimeMod.cs | 176 ++++++++++++++ StarMap.Core/Patches/ModLibraryPatches.cs | 10 +- StarMap.Core/Patches/ModPatches.cs | 8 +- StarMap.Core/Patches/ProgramPatcher.cs | 4 +- StarMap.Core/StarMapCore.cs | 10 +- 11 files changed, 484 insertions(+), 244 deletions(-) create mode 100644 StarMap.Core/ModRepository/RuntimeMod.cs diff --git a/README.md b/README.md index ac40813..2f08e97 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It makes use of Assembly Load Contexts to ensure mod dependencies are managed se ## Mod location -Mods should be installed in the mods folder in the KSA installation, any KSA mod that has a dll with the same name as the mod that impelements the IStarMapMod interface will be loaded. +Mods should be installed in the contents folder in the KSA installation, StarMap makes use of the ## Mod creation diff --git a/StarMap.API/README.md b/StarMap.API/README.md index cca3f60..d3604b5 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -1,21 +1,209 @@ # 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 +## How to create mods -- 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. +### General architecture -## Dependencies +A StarMap mod is in essence an extension of KSA mods. +Every mod will at minimum contain a mod.toml (which is also the case for KSA mods) as well as an entry assembly. -Mods can define what mods they depend on, and what assemblies they want to import from that mod. -They can as well define what assemblies they want exported to other mods. +#### mod.toml + +While it is not stricly neccesary, it is adviced to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: + +```toml +name = "StarMap.SimpleMod" + +[StarMap] +EntryAssembly = "StarMap.SimpleMod" +``` + +The name will be the modid of your mod, this should be the same name as the folder in which this lives in the Content folder. +The "StarMap" section is optional, if it is provided, at mimimum, it should provide the name of the assembly which StarMap will initially load (without the dll extension). +If it is not provided, StarMap will search for a [modid].dll in the mod folder. + +#### Entry assembly + +The above mentioned entry assembly will be the first thing that StarMap will load. +Within this assembly, StarMap will search for a class that has the [StarMapMod](#starmapmod) attribute. It will use the first that it finds. +This class will be the core part of the mod, any entry from StarMap will be done through this class. +To add functionality to the mod, methods with attributes can be added, which attributes are available and what their functionality is, can be found in the [Attributes API reference](#attributes). +These methods will be called as instance methods on the mod class, throughout the runtime of KSA, StarMap will create once instance of this class and reuse it. +The implemenation should follow the signatures shown in the example code. + +### Dependencies + +In many cases, mods will have dependencies on other mods, or will want to overwrite functionality of other mods. +To achieve this, some extra configuration is required, this configuration is confined to the mod.toml within the StarMap section. + +#### Exported assemblies + +First of all, mods can configure what assemblies should be exposed to other mods, by default all assemblies that are provided with the mod can be accessed from other mods, this can be changed with the ExportedAssemblies. +In below example, only the StarMap.SimpleMod.Dependency(.dll) assembly will be accessable from other mods (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). + +```toml +name = "StarMap.SimpleMod2" + +[StarMap] +EntryAssembly = "StarMap.SimpleMod2" +ExportedAssemblies = [ + "StarMap.SimpleMod.Dependency" +] +``` + +#### Mod dependency + +Then, mods can define what mods they want to define on, they can do this by adding a new ModDependencies list entry in the mod.toml + +```toml +name = "StarMap.SimpleMod" + +[StarMap] +EntryAssembly = "StarMap.SimpleMod" + +[[StarMap.ModDependencies]] +ModId = "StarMap.SimpleMod2" +Optional = false +ImportedAssemblies = [ + "StarMap.SimpleMod.Dependency" +] +``` + +In above example, it is provided that StarMap.SimpleMod wants to depend on StarMap.SimpleMod2, this dependency is not optional and the mod wants to access the StarMap.SimpleMod.Dependency assembly. +Following fields can be used + +- The ModId should be the same as is provided as the name field in the mod.toml of the dependency mod. +- The optional field (default false) defines if this dependency is optional, more info in the [loading strategy](#dependency-loading-strategy) +- The ImportedAssemblies field contains a list of assemblies that this mod intends to use from the dependency (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). + +#### Imported and exported assemblies + +The goal of the imported and exported assembly fields is to compile a list of assemblies that will be provided to a mod from a dependency, below is the behaviour depending on the content of both fields: + +- If both fields are not filled in, the list will contain the entry assembly of the dependency. +- If only 1 of the lists is filled in, it will use this list to provide the assemblies. +- If both lists are defined, the intersect of the two will be used. + +## Mod loading strategy + +When StarMap is started, it will start with loading the manifest.toml (in the same way KSA does it, only sooner), it will then start loading mods from top to bottom. +It will first load the mod.toml, if the file does not exists, mod loading will not work, if there is no StarMap section, it will use a default configuration. +Using the config, if there are dependencies, it will first check if these dependencies are already loaded. If they are all loaded, mod loading continues, +otherwise, it stores what dependencies are still needed and continues to the next mod. +It will then search for the entry assembly, if it does not exists, loading will be stopped, otherwise, it will load the assembly. Then will search for a class with the StarMapMod attribute and create an instance. With the instance, it goes over the known attributes and stores a reference to the methods, allowing for quick quering, it stores the StarMapBeforeMain and StarMapImmediateLoad methods seperatly because they are mod specific. +Once the mod is fully loaded, it will call the StarMapBeforeMain method, if there is any. + +Now that the mod has been loaded, it checks the list of mods that are waiting for dependencies, and if there are any that are waiting for this mod. If so, it removes itself from the waiting dependencies and checks if the mod can now be loaded, if so, the mod is loaded and the StarMapBeforeMain of that mod is called. + +It does this for all the mods in the manifest. +Once it has tried loading all the mods, it gets the mods that are stil waiting and checks them again. +If for a waiting mod, all its dependencies are optional, it will now load this mod. The implementation of the mod should ensure it can handle the optional dependency can be absent. +It keeps looping over the list of waiting mods until it has gone through the list once without being able to load a new mod, this indicates there are no more mods that can load with the provided mods, and gives up on loading these mods. + +Now StarMap will start KSA, which in turn will call StarMapImmediateLoad for each mod, if implemented. + +## Examples + +Some examples can be found in the [example mods repository](https://github.com/StarMapLoader/StarMap-ExampleMods) + +## API reference + +### Attributes + +#### StarMapMod + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Class + +Marks the main class for a StarMap mod. +Only attributes on methods within classes marked with this attribute will be considered. + +```csharp +[StarMapMod] +public class ModClass +``` + +#### StarMapBeforeMain + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called before KSA is started. + +```csharp +[StarMapBeforeMain] +public void ModMethod() +``` + +#### StarMapImmediateLoad + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called immediately when the mod is loaded by KSA. +It is called before the `KSA.Mod.PrepareSystems` method for each mod + +```csharp +[StarMapBeforeMain] +public void ModMethod(KSA.Mod mod) +``` + +#### StarMapAllModsLoaded + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called when all mods are loaded. +It is called after the `KSA.ModLibrary.LoadAll` method. + +```csharp +[StarMapAllModsLoaded] +public void ModMethod() +``` + +#### StarMapUnload + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called when KSA is unloaded + +```csharp +[StarMapUnload] +public void ModMethod() +``` + +#### StarMapBeforeGui + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called before KSA starts creating its ImGui interface. +It is called just before the `KSA.Program.OnDrawUi` method. + +```csharp +[StarMapBeforeGui] +public void ModMethod(double dt) +``` + +#### StarMapAfterGui + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called when KSA has finished creating its ImGui interface. +It is called just after the `KSA.Program.OnDrawUi` method. + +```csharp +[StarMapAfterGui] +public void ModMethod(double dt) +``` diff --git a/StarMap.API/StarMap.API.csproj b/StarMap.API/StarMap.API.csproj index 209a1cd..ff44d0d 100644 --- a/StarMap.API/StarMap.API.csproj +++ b/StarMap.API/StarMap.API.csproj @@ -1,19 +1,26 @@  - - net10.0 - enable - enable - + + net10.0 + enable + enable + true + 1591 + README.md + - - - + + + - - - compile; build; analyzers - all - - - + + + + + + + compile; build; analyzers + all + + + \ No newline at end of file diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index b6c419f..ccfb44b 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -9,15 +9,15 @@ internal class ModAssemblyLoadContext : AssemblyLoadContext private readonly AssemblyLoadContext _coreAssemblyLoadContext; private readonly AssemblyDependencyResolver _modDependencyResolver; - public ModInformation? ModInfo { get; set; } + public RuntimeMod? RuntimeMod { get; set; } - public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadContext coreAssemblyContext) + public ModAssemblyLoadContext(string modId, string entryAssemblyLocation, AssemblyLoadContext coreAssemblyContext) : base() { _coreAssemblyLoadContext = coreAssemblyContext; _modDependencyResolver = new AssemblyDependencyResolver( - Path.GetFullPath(Path.Combine(modDirectory, modId + ".dll")) + Path.GetFullPath(entryAssemblyLocation) ); } @@ -46,36 +46,15 @@ public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadCon } } - if (ModInfo is ModInformation modInfo && modInfo.Dependencies.Count > 0) + if (RuntimeMod is RuntimeMod modInfo && modInfo.Dependencies.Count > 0) { - foreach (var (dependencyInfo, importedAssemblies) in modInfo.Dependencies) + foreach (var (dependency, importedAssemblies) in modInfo.Dependencies) { - bool ShouldTryLoad() - { - var hasExportedAssemblies = dependencyInfo.ExportedAssemblies.Count > 0; - var hasImportedAssemblies = importedAssemblies.Count > 0; - - if (!hasImportedAssemblies && !hasExportedAssemblies) { - if (dependencyInfo.Config.EntryAssembly == assemblyName.Name) - return true; - - return false; - } - - if (hasExportedAssemblies && !dependencyInfo.ExportedAssemblies.Contains(assemblyName.Name ?? string.Empty)) - return false; - - if (hasImportedAssemblies && !importedAssemblies.Contains(assemblyName.Name ?? string.Empty)) - return false; - - return true; - } - - if (ShouldTryLoad()) + if (importedAssemblies.Contains(assemblyName.Name ?? string.Empty)) { try { - var asm = dependencyInfo.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + var asm = dependency.ModAssemblyLoadContext.LoadFromAssemblyName(assemblyName); if (asm != null) return asm; } diff --git a/StarMap.Core/ModRepository/ModLoader.cs b/StarMap.Core/ModRepository/ModLoader.cs index d4b4dd9..c0a8754 100644 --- a/StarMap.Core/ModRepository/ModLoader.cs +++ b/StarMap.Core/ModRepository/ModLoader.cs @@ -11,9 +11,8 @@ internal sealed class ModLoader : IDisposable { private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly Dictionary _registeredMethodAttributes = []; - private readonly Dictionary> _modDependencyGraph = []; - private readonly HashSet _waitingMods = []; + public static Dictionary RegisteredMethodAttributes = []; + private readonly ModRegistry _modRegistry = new(); public ModRegistry ModRegistry => _modRegistry; @@ -30,7 +29,7 @@ public ModLoader(AssemblyLoadContext coreAssemblyLoadContext) Assembly coreAssembly = typeof(StarMapModAttribute).Assembly; - _registeredMethodAttributes = coreAssembly + RegisteredMethodAttributes = coreAssembly .GetTypes() .Where(t => typeof(StarMapMethodAttribute).IsAssignableFrom(t) && @@ -62,191 +61,85 @@ private void PrepareMods() foreach (var mod in mods) { - var modPath = Path.Combine(path, mod.Id); - var starMapConfig = TomletMain.To(File.ReadAllText(Path.Combine(modPath, "mod.toml"))); - if (starMapConfig.StarMap is null) - continue; - - if (!LoadMod(mod.Id, modPath, starMapConfig.StarMap)) + if (!mod.Enabled) { + Console.WriteLine($"StarMap - Nod loading mod: {mod.Id} because it is disable in manifest"); continue; } - if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.BeforeMainAction is MethodInfo action) - { - action.Invoke(modInfo.ModInstance, []); - } - } + var modPath = Path.Combine(path, mod.Id); - var loadedMod = true; + if (!RuntimeMod.TryCreateMod(mod, _coreAssemblyLoadContext, out var runtimeMod)) + continue; - while (_modDependencyGraph.Count > 0 && loadedMod) - { - loadedMod = false; - foreach (var waitingMod in _waitingMods) - { - if (waitingMod.NotLoadedModDependencies.Count == 0 || waitingMod.NotLoadedModDependencies.Values.All(dependencyInfo => dependencyInfo.Optional)) - { - loadedMod = true; - _waitingMods.Remove(waitingMod); + ModRegistry.Add(runtimeMod); - if (InitializeMod(waitingMod)) - { - Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); - } - else - { - Console.WriteLine($"StarMap - Failed to load mod:{ waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); - } - waitingMod.NotLoadedModDependencies.Clear(); - } + if (!runtimeMod.AllDependenciesLoaded(ModRegistry)) + { + Console.WriteLine($"StarMap - Delaying load of mod: {runtimeMod.ModId} due to missing dependencies: {string.Join(", ", runtimeMod.NotLoadedModDependencies.Keys)}"); + continue; } - } - if (_waitingMods.Count > 0) - { - foreach (var waitingMod in _waitingMods) + if (!runtimeMod.InitializeMod(ModRegistry)) { - Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); + Console.WriteLine($"StarMap - Failed to initialize mod: {runtimeMod.ModId} from manifest"); + continue; } - _waitingMods.Clear(); - } - } - - private bool LoadMod(string modId, string modDirectory, StarMapConfig config) - { - var fullPath = Path.GetFullPath(modDirectory); - var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll"); - var assemblyExists = File.Exists(modAssemblyFile); - - if (!assemblyExists) return false; - - var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext); - var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId }); - - var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); - if (modClass is null) return false; - - var modInfo = new ModInformation() { - ModId = modId, - ModAssemblyLoadContext = modLoadContext, - ModType = modClass, - Config = config, - }; - foreach(var exportedAssembly in config.ExportedAssemblies) - { - modInfo.ExportedAssemblies.Add(exportedAssembly); - } - - modLoadContext.ModInfo = modInfo; - _modRegistry.Add(modInfo); + Console.WriteLine($"StarMap - Loaded mod: {runtimeMod.ModId} from manifest"); - foreach (var dependency in config.ModDependencies) - { - if (!_modRegistry.TryGetMod(dependency.ModId, out var modDependency)) + var dependentMods = runtimeMod.CheckForDependentMods(ModRegistry); + + foreach (var dependentMod in dependentMods) { - modInfo.NotLoadedModDependencies.Add(dependency.ModId, dependency); - if (!_modDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) + if (dependentMod.InitializeMod(ModRegistry)) { - dependents = []; - _modDependencyGraph[dependency.ModId] = dependents; + Console.WriteLine($"StarMap - Loaded mod: {dependentMod.ModId} after loading {runtimeMod.ModId}"); + } + else + { + Console.WriteLine($"StarMap - Failed to load mod: {dependentMod.ModId} after loading {runtimeMod.ModId}"); } - dependents.Add(modInfo); - } - else - { - modInfo.Dependencies.Add(modDependency, [.. dependency.ImportedAssemblies]); } } - if (modInfo.NotLoadedModDependencies.Count > 0) - { - Console.WriteLine($"StarMap - Delaying load of mod: {modInfo.ModId} due to missing dependencies: {string.Join(", ", modInfo.NotLoadedModDependencies.Keys)}"); - _waitingMods.Add(modInfo); - return false; - } - - if (!InitializeMod(modInfo)) - { - Console.WriteLine($"StarMap - Failed to initialize mod: {modInfo.ModId} from {modAssemblyFile}"); - return false; - } + TryLoadWaitingMods(); + } - Console.WriteLine($"StarMap - Loaded mod: {modInfo.ModId} from {modAssemblyFile}"); + private void TryLoadWaitingMods() + { + var loadedMod = true; - if (_modDependencyGraph.TryGetValue(modInfo.ModId, out var modDependents)) + while (ModRegistry.WaitingModsDependencyGraph.Count > 0 && loadedMod) { - foreach(var modDependent in modDependents) + loadedMod = false; + foreach (var waitingMod in ModRegistry.WaitingMods) { - var dependencyInfo = modDependent.NotLoadedModDependencies[modInfo.ModId]; - modDependent.Dependencies.Add(modInfo, [.. dependencyInfo.ImportedAssemblies]); - if (modDependent.NotLoadedModDependencies.Remove(modInfo.ModId) && modDependent.NotLoadedModDependencies.Count == 0) + if (waitingMod.NotLoadedModDependencies.Count == 0 || waitingMod.NotLoadedModDependencies.Values.All(dependencyInfo => dependencyInfo.Optional)) { - _waitingMods.Remove(modDependent); - if (InitializeMod(modDependent)) + loadedMod = true; + ModRegistry.WaitingMods.Remove(waitingMod); + + if (waitingMod.InitializeMod(ModRegistry)) { - Console.WriteLine($"StarMap - Loaded mod: {modDependent.ModId} after loading {modInfo.ModId}"); + Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } else { - Console.WriteLine($"StarMap - Failed to load mod: {modDependent.ModId} after loading {modInfo.ModId}"); + Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } + waitingMod.NotLoadedModDependencies.Clear(); } } - _modDependencyGraph.Remove(modInfo.ModId); } - return true; - } - - private bool InitializeMod(ModInformation modInfo) - { - var modObject = Activator.CreateInstance(modInfo.ModType); - if (modObject is null) return false; - modInfo.ModInstance = modObject; - modInfo.Initialized = true; - - var classMethods = modInfo.ModType.GetMethods(); - var immediateLoadMethods = new List(); - - foreach (var classMethod in classMethods) + if (ModRegistry.WaitingMods.Count > 0) { - var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(_registeredMethodAttributes.Keys.Contains); - foreach (var stringAttr in stringAttrs) + foreach (var waitingMod in ModRegistry.WaitingMods) { - var attr = _registeredMethodAttributes[stringAttr]; - - if (!attr.IsValidSignature(classMethod)) continue; - - if (attr.GetType() == typeof(StarMapImmediateLoadAttribute)) - { - immediateLoadMethods.Add(classMethod); - } - - _modRegistry.AddModMethod(modInfo.ModId, attr, modObject, classMethod); + Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}"); } - } - - foreach (var assembly in modInfo.Config?.ExportedAssemblies ?? []) - modInfo.ExportedAssemblies.Add(assembly); - - return true; - } - - public void ModPrepareSystems(Mod mod) - { - if (_modRegistry.TryGetMod(mod.Id, out var modInfo) && modInfo.PrepareSystemsAction is MethodInfo action) - { - action.Invoke(modInfo.ModInstance, [mod]); - } - } - - public void OnAllModsLoaded() - { - foreach (var (_, @object, method) in _modRegistry.Get()) - { - method.Invoke(@object, []); + ModRegistry.WaitingMods.Clear(); } } diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index 3a11a30..04ae34f 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -6,48 +6,31 @@ namespace StarMap.Core.ModRepository { - internal sealed class ModInformation - { - public required string ModId { get; init; } - public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } - public required Type ModType { get; init; } - public required StarMapConfig Config { get; init; } - - public bool Initialized { get; set; } = false; - public object? ModInstance { get; set; } = null; - - public HashSet ExportedAssemblies { get; set; } = []; - public Dictionary> Dependencies { get; set; } = []; - public Dictionary NotLoadedModDependencies { get; set; } = []; - - - public MethodInfo? BeforeMainAction { get; set; } = null; - public MethodInfo? PrepareSystemsAction { get; set; } = null; - } - internal sealed class ModRegistry : IDisposable { - private readonly Dictionary _mods = []; + public Dictionary> WaitingModsDependencyGraph { get; } = []; + public HashSet WaitingMods { get; } = []; + + private readonly Dictionary _mods = []; private readonly Dictionary> _modMethods = []; public bool ModLoaded(string modId) => _mods.ContainsKey(modId); - public bool TryGetMod(string modId, [NotNullWhen(true)] out ModInformation? modInfo) + public bool TryGetMod(string modId, [NotNullWhen(true)] out RuntimeMod? modInfo) { return _mods.TryGetValue(modId, out modInfo); } - public void Add(ModInformation modInfo) + public void Add(RuntimeMod modInfo) { _mods.Add(modInfo.ModId, modInfo); } - public IEnumerable GetMods() + public IEnumerable GetMods() { return _mods.Values; } - public void AddModMethod(string modId, StarMapMethodAttribute methodAttribute, object @object, MethodInfo method) { if (!_mods.TryGetValue(modId, out var modInfo)) return; diff --git a/StarMap.Core/ModRepository/RuntimeMod.cs b/StarMap.Core/ModRepository/RuntimeMod.cs new file mode 100644 index 0000000..25bea2e --- /dev/null +++ b/StarMap.Core/ModRepository/RuntimeMod.cs @@ -0,0 +1,176 @@ +using KSA; +using StarMap.API; +using StarMap.Core.Config; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using Tomlet; + +namespace StarMap.Core.ModRepository +{ + internal class RuntimeMod + { + private static readonly string _rootContentPath = Path.Combine(["Content"]); + + public required string ModId { get; init; } + public required ModAssemblyLoadContext ModAssemblyLoadContext { get; init; } + public required Type ModType { get; init; } + public required StarMapConfig Config { get; init; } + + public bool Initialized { get; set; } = false; + public object? ModInstance { get; set; } = null; + + public HashSet ExportedAssemblies { get; set; } = []; + public Dictionary> Dependencies { get; set; } = []; + public Dictionary NotLoadedModDependencies { get; set; } = []; + + public MethodInfo? BeforeMainAction { get; set; } = null; + public MethodInfo? PrepareSystemsAction { get; set; } = null; + + + public static bool TryCreateMod(ModEntry manifestEntry, AssemblyLoadContext coreALC, [NotNullWhen(true)] out RuntimeMod? runtimeMod) + { + runtimeMod = null; + + var modPath = Path.Combine(_rootContentPath, manifestEntry.Id); + var modTomlPath = Path.Combine(modPath, "mod.toml"); + if (!File.Exists(modTomlPath)) return false; + var tomlConfig = TomletMain.To(File.ReadAllText(modTomlPath)); + if (tomlConfig?.StarMap is not StarMapConfig starMapConfig) + { + starMapConfig = new StarMapConfig + { + EntryAssembly = manifestEntry.Id, + }; + } + + var modAssemblyFile = Path.Combine(modPath, $"{starMapConfig.EntryAssembly}.dll"); + var assemblyExists = File.Exists(modAssemblyFile); + + if (!assemblyExists) return false; + + var modLoadContext = new ModAssemblyLoadContext(manifestEntry.Id, modAssemblyFile, coreALC); + var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = starMapConfig.EntryAssembly }); + + var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name)); + if (modClass is null) return false; + + runtimeMod = new RuntimeMod + { + ModId = manifestEntry.Id, + ModAssemblyLoadContext = modLoadContext, + ModType = modClass, + Config = starMapConfig, + }; + + modLoadContext.RuntimeMod = runtimeMod; + + return true; + } + + public bool AllDependenciesLoaded(ModRegistry modRegistry) + { + foreach (var dependency in Config.ModDependencies) + { + if (!modRegistry.TryGetMod(dependency.ModId, out var modDependency)) + { + NotLoadedModDependencies.Add(dependency.ModId, dependency); + + if (!modRegistry.WaitingModsDependencyGraph.TryGetValue(dependency.ModId, out var dependents)) + { + dependents = []; + modRegistry.WaitingModsDependencyGraph[dependency.ModId] = dependents; + } + dependents.Add(this); + } + else + { + Dependencies.Add(modDependency, [.. CalculateUseableAssemblies(modDependency, dependency)]); + } + } + + if (NotLoadedModDependencies.Count > 0) + { + + modRegistry.WaitingMods.Add(this); + return false; + } + + return true; + } + + private static IEnumerable CalculateUseableAssemblies(RuntimeMod dependency, StarMapModDependency dependencyInfo) + { + + var hasImportedAssemblies = dependencyInfo.ImportedAssemblies.Count > 0; + var hasExportedAssemblies = dependency.ExportedAssemblies.Count > 0; + + if (!hasImportedAssemblies && !hasExportedAssemblies) + { + return [dependency.Config.EntryAssembly]; + } + + if (hasImportedAssemblies && !hasExportedAssemblies) + return dependencyInfo.ImportedAssemblies; + + if (!hasImportedAssemblies && hasExportedAssemblies) + return dependency.ExportedAssemblies; + + return dependency.ExportedAssemblies.Intersect(dependencyInfo.ImportedAssemblies); + } + + public bool InitializeMod(ModRegistry modRegistry) + { + var modObject = Activator.CreateInstance(ModType); + if (modObject is null) return false; + ModInstance = modObject; + Initialized = true; + + var classMethods = ModType.GetMethods(); + + foreach (var classMethod in classMethods) + { + var stringAttrs = classMethod.GetCustomAttributes().Select((attr) => attr.GetType().Name).Where(ModLoader.RegisteredMethodAttributes.Keys.Contains); + foreach (var stringAttr in stringAttrs) + { + var attr = ModLoader.RegisteredMethodAttributes[stringAttr]; + + if (!attr.IsValidSignature(classMethod)) continue; + + modRegistry.AddModMethod(ModId, attr, modObject, classMethod); + } + } + + if (modRegistry.TryGetMod(ModId, out var modInfo) && modInfo.BeforeMainAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, []); + } + + return true; + } + + public List CheckForDependentMods(ModRegistry modRegistry) + { + List loadableMods = []; + if (modRegistry.WaitingModsDependencyGraph.TryGetValue(ModId, out var modDependents)) + { + foreach (var modDependent in modDependents) + { + var dependencyInfo = modDependent.NotLoadedModDependencies[ModId]; + modDependent.Dependencies.Add(this, [.. dependencyInfo.ImportedAssemblies]); + if (modDependent.NotLoadedModDependencies.Remove(ModId) && modDependent.NotLoadedModDependencies.Count == 0) + { + modRegistry.WaitingMods.Remove(modDependent); + loadableMods.Add(modDependent); + } + } + modRegistry.WaitingModsDependencyGraph.Remove(ModId); + } + return loadableMods; + } + } +} diff --git a/StarMap.Core/Patches/ModLibraryPatches.cs b/StarMap.Core/Patches/ModLibraryPatches.cs index cb87311..ab5fcea 100644 --- a/StarMap.Core/Patches/ModLibraryPatches.cs +++ b/StarMap.Core/Patches/ModLibraryPatches.cs @@ -1,5 +1,7 @@ using HarmonyLib; using KSA; +using StarMap.API; +using StarMap.Core.ModRepository; namespace StarMap.Core.Patches { @@ -10,7 +12,13 @@ internal class ModLibraryPatches [HarmonyPostfix] public static void AfterLoad() { - StarMapCore.Instance?.LoadedMods.OnAllModsLoaded(); + var modRegistry = StarMapCore.Instance?.Loader.ModRegistry; + if (modRegistry is not ModRegistry registry) return; + + foreach (var (_, @object, method) in registry.Get()) + { + method.Invoke(@object, []); + } } } } diff --git a/StarMap.Core/Patches/ModPatches.cs b/StarMap.Core/Patches/ModPatches.cs index 8d01a8e..8829746 100644 --- a/StarMap.Core/Patches/ModPatches.cs +++ b/StarMap.Core/Patches/ModPatches.cs @@ -12,7 +12,13 @@ internal static class ModPatches [HarmonyPrefix] public static void OnLoadMod(this Mod __instance) { - StarMapCore.Instance?.LoadedMods.ModPrepareSystems(__instance); + var modRegistry = StarMapCore.Instance?.Loader.ModRegistry; + if (modRegistry is not ModRegistry registry) return; + + if (registry.TryGetMod(__instance.Id, out var modInfo) && modInfo.PrepareSystemsAction is MethodInfo action) + { + action.Invoke(modInfo.ModInstance, [__instance]); + } } } } diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index 78b8184..ad20306 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -14,7 +14,7 @@ internal static class ProgramPatcher [HarmonyPrefix] public static void BeforeOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { @@ -26,7 +26,7 @@ public static void BeforeOnDrawUi(double dt) [HarmonyPostfix] public static void AfterOnDrawUi(double dt) { - var methods = StarMapCore.Instance?.LoadedMods.ModRegistry.Get() ?? []; + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 6a7997d..828dbfd 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -12,26 +12,26 @@ internal class StarMapCore : IStarMapCore private readonly Harmony _harmony = new("StarMap.Core"); private readonly AssemblyLoadContext _coreAssemblyLoadContext; - private readonly ModLoader _loadedMods; - public ModLoader LoadedMods => _loadedMods; + private readonly ModLoader _loader; + public ModLoader Loader => _loader; public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) { Instance = this; _coreAssemblyLoadContext = coreAssemblyLoadContext; - _loadedMods = new(_coreAssemblyLoadContext); + _loader = new(_coreAssemblyLoadContext); } public void Init() { - _loadedMods.Init(); + _loader.Init(); _harmony.PatchAll(typeof(StarMapCore).Assembly); } public void DeInit() { _harmony.UnpatchAll(); - _loadedMods.Dispose(); + _loader.Dispose(); } } } From f390078da17746033d320b7881b10e6c61c9fa4d Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 19:50:23 +0100 Subject: [PATCH 11/14] Merge pull request #49 from StarMapLoader/dev Do not try to unload the assembly load contexts (#44) Add StarMap hook for after onFrame --- StarMap.API/BaseAttributes.cs | 1 - StarMap.API/README.md | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index d5ecb5a..1134976 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -81,7 +81,6 @@ public override bool IsValidSignature(MethodInfo method) /// /// 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: diff --git a/StarMap.API/README.md b/StarMap.API/README.md index d3604b5..94bb062 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -14,10 +14,10 @@ Every mod will at minimum contain a mod.toml (which is also the case for KSA mod While it is not stricly neccesary, it is adviced to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: ```toml -name = "StarMap.SimpleMod" +name = "MyAmazingMod" [StarMap] -EntryAssembly = "StarMap.SimpleMod" +EntryAssembly = "MyAmazingMod" ``` The name will be the modid of your mod, this should be the same name as the folder in which this lives in the Content folder. @@ -44,12 +44,12 @@ First of all, mods can configure what assemblies should be exposed to other mods In below example, only the StarMap.SimpleMod.Dependency(.dll) assembly will be accessable from other mods (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). ```toml -name = "StarMap.SimpleMod2" +name = "MyOtherAmazingMod" [StarMap] -EntryAssembly = "StarMap.SimpleMod2" +EntryAssembly = "MyOtherAmazingMod" ExportedAssemblies = [ - "StarMap.SimpleMod.Dependency" + "MyDependency" ] ``` @@ -58,16 +58,16 @@ ExportedAssemblies = [ Then, mods can define what mods they want to define on, they can do this by adding a new ModDependencies list entry in the mod.toml ```toml -name = "StarMap.SimpleMod" +name = "MyAmazingMod" [StarMap] -EntryAssembly = "StarMap.SimpleMod" +EntryAssembly = "MyAmazingMod" [[StarMap.ModDependencies]] -ModId = "StarMap.SimpleMod2" +ModId = "MyOtherAmazingMod" Optional = false ImportedAssemblies = [ - "StarMap.SimpleMod.Dependency" + "MyDependency" ] ``` From dcd180557bfdc13b871a2a47db2d7e22ee14724f Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 20:02:43 +0100 Subject: [PATCH 12/14] Add OnFrame documentation to API --- StarMap.API/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/StarMap.API/README.md b/StarMap.API/README.md index 94bb062..34356e2 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -207,3 +207,16 @@ It is called just after the `KSA.Program.OnDrawUi` method. [StarMapAfterGui] public void ModMethod(double dt) ``` + +#### StarMapAfterOnFrame + +Namespace: `StarMap.API` +Assembly: `StarMap.API` +Target: Method + +Methods marked with this attribute will be called after `KSA.Program.OnFrame` is called. + +```csharp +[StarMapAfterOnFrame] +public void ModMethod(double currentPlayerTime, double dtPlayer); +``` From 448b5bcbb237d6e4989fc68430aa8793ac773632 Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 21:43:28 +0100 Subject: [PATCH 13/14] Update dummy version --- StarMap.Core/Patches/ProgramPatcher.cs | 2 +- StarMap.Core/StarMap.Core.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/StarMap.Core/Patches/ProgramPatcher.cs b/StarMap.Core/Patches/ProgramPatcher.cs index ad20306..5c819d7 100644 --- a/StarMap.Core/Patches/ProgramPatcher.cs +++ b/StarMap.Core/Patches/ProgramPatcher.cs @@ -38,7 +38,7 @@ public static void AfterOnDrawUi(double dt) [HarmonyPostfix] public static void AfterOnFrame(double currentPlayerTime, double dtPlayer) { - var methods = StarMapCore.Instance?.LoadedMods.Mods.Get() ?? []; + var methods = StarMapCore.Instance?.Loader.ModRegistry.Get() ?? []; foreach (var (_, @object, method) in methods) { diff --git a/StarMap.Core/StarMap.Core.csproj b/StarMap.Core/StarMap.Core.csproj index 2e14983..2d06ac0 100644 --- a/StarMap.Core/StarMap.Core.csproj +++ b/StarMap.Core/StarMap.Core.csproj @@ -23,7 +23,7 @@ - + runtime From ebee002f7d4305eba12bb3c771a3a5b3cdf4116a Mon Sep 17 00:00:00 2001 From: KlaasWhite Date: Tue, 23 Dec 2025 21:47:44 +0100 Subject: [PATCH 14/14] Fix spelling issues in README.md --- StarMap.API/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/StarMap.API/README.md b/StarMap.API/README.md index 34356e2..0a90ade 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -11,7 +11,7 @@ Every mod will at minimum contain a mod.toml (which is also the case for KSA mod #### mod.toml -While it is not stricly neccesary, it is adviced to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: +While it is not stricly neccesary, it is advised to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: ```toml name = "MyAmazingMod" @@ -55,7 +55,7 @@ ExportedAssemblies = [ #### Mod dependency -Then, mods can define what mods they want to define on, they can do this by adding a new ModDependencies list entry in the mod.toml +Then, mods can define what mods they want to depend on, they can do this by adding a new ModDependencies list entry in the mod.toml ```toml name = "MyAmazingMod" @@ -74,9 +74,9 @@ ImportedAssemblies = [ In above example, it is provided that StarMap.SimpleMod wants to depend on StarMap.SimpleMod2, this dependency is not optional and the mod wants to access the StarMap.SimpleMod.Dependency assembly. Following fields can be used -- The ModId should be the same as is provided as the name field in the mod.toml of the dependency mod. -- The optional field (default false) defines if this dependency is optional, more info in the [loading strategy](#dependency-loading-strategy) -- The ImportedAssemblies field contains a list of assemblies that this mod intends to use from the dependency (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). +- The `ModId` should be the same as is provided as the name field in the mod.toml of the dependency mod. +- The `Optional` field (default false) defines if this dependency is optional, more info in the [loading strategy](#dependency-loading-strategy) +- The `ImportedAssemblies` field contains a list of assemblies that this mod intends to use from the dependency (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). #### Imported and exported assemblies @@ -84,7 +84,7 @@ The goal of the imported and exported assembly fields is to compile a list of as - If both fields are not filled in, the list will contain the entry assembly of the dependency. - If only 1 of the lists is filled in, it will use this list to provide the assemblies. -- If both lists are defined, the intersect of the two will be used. +- If both lists are defined, the intersection of the two will be used. ## Mod loading strategy @@ -98,7 +98,7 @@ Once the mod is fully loaded, it will call the StarMapBeforeMain method, if ther Now that the mod has been loaded, it checks the list of mods that are waiting for dependencies, and if there are any that are waiting for this mod. If so, it removes itself from the waiting dependencies and checks if the mod can now be loaded, if so, the mod is loaded and the StarMapBeforeMain of that mod is called. It does this for all the mods in the manifest. -Once it has tried loading all the mods, it gets the mods that are stil waiting and checks them again. +Once it has tried loading all the mods, it gets the mods that are still waiting and checks them again. If for a waiting mod, all its dependencies are optional, it will now load this mod. The implementation of the mod should ensure it can handle the optional dependency can be absent. It keeps looping over the list of waiting mods until it has gone through the list once without being able to load a new mod, this indicates there are no more mods that can load with the provided mods, and gives up on loading these mods.