diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 57945be..4166c17 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -30,7 +30,7 @@ jobs: - name: Restore dependencies run: | - dotnet nuget add source --username "${{ github.actor }}" --password "${{ secrets.GITHUB_TOKEN }}" --store-password-in-clear-text --name github "${{ env.NUGET_SOURCE }}" + dotnet nuget add source --username "${{ secrets.ORG_PACKAGE_USERNAME }}" --password "${{ secrets.ORG_PACKAGE_TOKEN }}" --store-password-in-clear-text --name github "${{ env.NUGET_SOURCE }}" dotnet restore ${{ env.PROJECT }} # - name: Run tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00dd80d..1787fb4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,8 +77,8 @@ jobs: - name: Setup Github NuGet run: | dotnet nuget add source \ - --username ${{ github.actor }} \ - --password ${{ secrets.GITHUB_TOKEN }} \ + --username ${{ secrets.ORG_PACKAGE_USERNAME }} \ + --password ${{ secrets.ORG_PACKAGE_TOKEN }} \ --store-password-in-clear-text \ --name github "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" diff --git a/StarMap.API/BaseAttributes.cs b/StarMap.API/BaseAttributes.cs index f5df406..d5ecb5a 100644 --- a/StarMap.API/BaseAttributes.cs +++ b/StarMap.API/BaseAttributes.cs @@ -19,7 +19,33 @@ public abstract class StarMapMethodAttribute : Attribute } /// - /// Methods marked with this attribute will be called immediately when the mod is loaded. + /// Methods marked with this attribute will be called before KSA is started. + /// + /// + /// Methods using this attribute must match the following signature: + /// + /// + /// public void MethodName(); + /// + /// + /// Specifically: + /// + /// No parameters are allowed. + /// Return type must be . + /// Method must be an instance method (non-static). + /// + /// + public class StarMapBeforeMainAttribute : StarMapMethodAttribute + { + public override bool IsValidSignature(MethodInfo method) + { + return method.ReturnType == typeof(void) && + method.GetParameters().Length == 0; + } + } + + /// + /// Methods marked with this attribute will be called immediately when the mod is loaded by KSA. /// /// /// Methods using this attribute must match the following signature: diff --git a/StarMap.API/StarMap.API.csproj b/StarMap.API/StarMap.API.csproj index 0529043..ba6aec8 100644 --- a/StarMap.API/StarMap.API.csproj +++ b/StarMap.API/StarMap.API.csproj @@ -11,7 +11,7 @@ - + compile; build; analyzers all diff --git a/StarMap.Core/ModRepository/LoadedModRepository.cs b/StarMap.Core/ModRepository/LoadedModRepository.cs index c7c8aa5..1f316e4 100644 --- a/StarMap.Core/ModRepository/LoadedModRepository.cs +++ b/StarMap.Core/ModRepository/LoadedModRepository.cs @@ -8,7 +8,10 @@ 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; @@ -38,23 +41,72 @@ public LoadedModRepository(AssemblyLoadContext coreAssemblyLoadContext) .ToDictionary(); } - public void LoadMod(Mod mod) + public void Init() + { + PrepareMods(); + } + + private void PrepareMods() + { + ModLibrary.PrepareManifest(); + + 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) { - var fullPath = Path.GetFullPath(mod.DirectoryPath); - var filePath = Path.Combine(fullPath, $"{mod.Name}.dll"); - var folderExists = Directory.Exists(fullPath); - var fileExists = File.Exists(filePath); + if (!_attemptedMods.TryGetValue(mod.Id, out var succeeded)) + { + succeeded = LoadMod(mod.Id, mod.DirectoryPath); + } - if (!folderExists || !fileExists) return; + 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); - var modLoadContext = new ModAssemblyLoadContext(mod, _coreAssemblyLoadContext); - var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = mod.Name }); + if (!assemblyExists) return false; - var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.IsDefined(typeof(StarMapModAttribute), inherit: false)); - if (modClass is null) return; + 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; + if (modObject is null) return false; + + _modLoadContexts.Add(modId, modLoadContext); var classMethods = modClass.GetMethods(); var immediateLoadMethods = new List(); @@ -73,16 +125,12 @@ public void LoadMod(Mod mod) immediateLoadMethods.Add(classMethod); } - _mods.Add(attr, modObject, classMethod); + _mods.Add(modId, attr, modObject, classMethod); } } - foreach (var method in immediateLoadMethods) - { - method.Invoke(modObject, [mod]); - } - - Console.WriteLine($"StarMap - Loaded mod: {mod.Name}"); + Console.WriteLine($"StarMap - Loaded mod: {modId} from {modAssemblyFile}"); + return true; } public void OnAllModsLoaded() @@ -100,6 +148,11 @@ public void Dispose() method.Invoke(@object, []); } + foreach (var modLoadContext in _modLoadContexts.Values) + { + modLoadContext.Unload(); + } + _mods.Dispose(); } } diff --git a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs index 9217a8b..92b7b9f 100644 --- a/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs +++ b/StarMap.Core/ModRepository/ModAssemblyLoadContext.cs @@ -2,26 +2,26 @@ using System.Reflection; using System.Runtime.Loader; -namespace StarMap.Core +namespace StarMap.Core.ModRepository { internal class ModAssemblyLoadContext : AssemblyLoadContext { private readonly AssemblyLoadContext _coreAssemblyLoadContext; private readonly AssemblyDependencyResolver _modDependencyResolver; - public ModAssemblyLoadContext(Mod mod, AssemblyLoadContext coreAssemblyContext) + public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadContext coreAssemblyContext) : base(isCollectible: true) { _coreAssemblyLoadContext = coreAssemblyContext; _modDependencyResolver = new AssemblyDependencyResolver( - Path.GetFullPath(Path.Combine(mod.DirectoryPath, mod.Name + ".dll")) + Path.GetFullPath(Path.Combine(modDirectory, modId + ".dll")) ); } protected override Assembly? Load(AssemblyName assemblyName) { - var existingInDefault = Default.Assemblies + var existingInDefault = AssemblyLoadContext.Default.Assemblies .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase)); if (existingInDefault != null) return existingInDefault; @@ -31,12 +31,24 @@ public ModAssemblyLoadContext(Mod mod, AssemblyLoadContext coreAssemblyContext) if (existingInGameContext != null) return existingInGameContext; + if (_coreAssemblyLoadContext != null) + { + try + { + var asm = _coreAssemblyLoadContext.LoadFromAssemblyName(assemblyName); + if (asm != null) + return asm; + } + catch (FileNotFoundException) + { + } + } + var foundPath = _modDependencyResolver.ResolveAssemblyToPath(assemblyName); if (foundPath is null) return null; - var path = Path.GetFullPath(foundPath); - return path != null ? LoadFromAssemblyPath(path) : null; + return LoadFromAssemblyPath(Path.GetFullPath(foundPath)); } } } diff --git a/StarMap.Core/ModRepository/ModRegistry.cs b/StarMap.Core/ModRepository/ModRegistry.cs index ffd9c0b..40e1b3b 100644 --- a/StarMap.Core/ModRepository/ModRegistry.cs +++ b/StarMap.Core/ModRepository/ModRegistry.cs @@ -1,23 +1,31 @@ -using StarMap.API; +using KSA; +using StarMap.API; using System.Reflection; namespace StarMap.Core.ModRepository { internal sealed class ModRegistry : IDisposable { - private readonly Dictionary> _map = new(); + private readonly Dictionary> _map = []; + private readonly Dictionary _beforeMainActions = []; + private readonly Dictionary _prepareSystemsActions = []; - public void Add(StarMapMethodAttribute attribute, object @object, MethodInfo method) + public void Add(string modId, StarMapMethodAttribute attribute, object @object, MethodInfo method) { var attributeType = attribute.GetType(); - // --- add instance --- if (!_map.TryGetValue(attributeType, out var list)) { list = []; _map[attributeType] = list; } + if (attribute.GetType() == typeof(StarMapBeforeMainAttribute)) + _beforeMainActions[modId] = (@object, method); + + if (attribute.GetType() == typeof(StarMapImmediateLoadAttribute)) + _prepareSystemsActions[modId] = (@object, method); + list.Add((attribute, @object, method)); } @@ -39,6 +47,16 @@ public void Add(StarMapMethodAttribute attribute, object @object, MethodInfo met : 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(); diff --git a/StarMap.Core/Patches/ModPatches.cs b/StarMap.Core/Patches/ModPatches.cs index 7934da2..69ce5b0 100644 --- a/StarMap.Core/Patches/ModPatches.cs +++ b/StarMap.Core/Patches/ModPatches.cs @@ -10,7 +10,7 @@ internal static class ModPatches [HarmonyPrefix] public static void OnLoadMod(this Mod __instance) { - StarMapCore.Instance?.LoadedMods.LoadMod(__instance); + StarMapCore.Instance?.LoadedMods.ModPrepareSystems(__instance); } } } diff --git a/StarMap.Core/StarMap.Core.csproj b/StarMap.Core/StarMap.Core.csproj index facc085..e70bf3f 100644 --- a/StarMap.Core/StarMap.Core.csproj +++ b/StarMap.Core/StarMap.Core.csproj @@ -20,7 +20,7 @@ - + runtime diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs index 0690665..4f54870 100644 --- a/StarMap.Core/StarMapCore.cs +++ b/StarMap.Core/StarMapCore.cs @@ -24,6 +24,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext) public void Init() { + _loadedMods.Init(); _harmony.PatchAll(typeof(StarMapCore).Assembly); } diff --git a/StarMap/GameSurveyer.cs b/StarMap/GameSurveyer.cs index 08e531a..a706090 100644 --- a/StarMap/GameSurveyer.cs +++ b/StarMap/GameSurveyer.cs @@ -32,6 +32,15 @@ public bool TryLoadCoreAndGame() _game = _gameAssemblyContext.LoadFromAssemblyPath(_gameLocation); + var gameDirectory = Path.GetDirectoryName(_gameLocation); + if (string.IsNullOrWhiteSpace(gameDirectory) || !Directory.Exists(gameDirectory)) + { + Console.WriteLine("StarMap - Game directory not found"); + return false; + } + + Directory.SetCurrentDirectory(gameDirectory); + _core = core; core.Init(); return true; @@ -42,15 +51,7 @@ public void RunGame() Debug.Assert(_game is not null, "Load needs to be called before running game"); string[] args = []; - - var gameDirectory = Path.GetDirectoryName(_gameLocation); - if (string.IsNullOrWhiteSpace(gameDirectory) || !Directory.Exists(gameDirectory)) - { - Console.WriteLine("StarMap - Game directory not found"); - return; - } - - Directory.SetCurrentDirectory(gameDirectory); + _game.EntryPoint!.Invoke(null, [args]); }