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]);
}