Skip to content

Commit 76c7d9e

Browse files
committed
Add StarMapBeforeMainAttribute attribute and functionality + FIx issue with ALCs unloading after initialization
1 parent c1b95a5 commit 76c7d9e

File tree

11 files changed

+155
-44
lines changed

11 files changed

+155
-44
lines changed

.github/workflows/pr-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030

3131
- name: Restore dependencies
3232
run: |
33-
dotnet nuget add source --username "${{ github.actor }}" --password "${{ secrets.GITHUB_TOKEN }}" --store-password-in-clear-text --name github "${{ env.NUGET_SOURCE }}"
33+
dotnet nuget add source --username "${{ secrets.ORG_PACKAGE_USERNAME }}" --password "${{ secrets.ORG_PACKAGE_TOKEN }}" --store-password-in-clear-text --name github "${{ env.NUGET_SOURCE }}"
3434
dotnet restore ${{ env.PROJECT }}
3535
3636
# - name: Run tests

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ jobs:
7777
- name: Setup Github NuGet
7878
run: |
7979
dotnet nuget add source \
80-
--username ${{ github.actor }} \
81-
--password ${{ secrets.GITHUB_TOKEN }} \
80+
--username ${{ secrets.ORG_PACKAGE_USERNAME }} \
81+
--password ${{ secrets.ORG_PACKAGE_TOKEN }} \
8282
--store-password-in-clear-text \
8383
--name github "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
8484

StarMap.API/BaseAttributes.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,33 @@ public abstract class StarMapMethodAttribute : Attribute
1919
}
2020

2121
/// <summary>
22-
/// Methods marked with this attribute will be called immediately when the mod is loaded.
22+
/// Methods marked with this attribute will be called before KSA is started.
23+
/// </summary>
24+
/// <remarks>
25+
/// Methods using this attribute must match the following signature:
26+
///
27+
/// <code>
28+
/// public void MethodName();
29+
/// </code>
30+
///
31+
/// Specifically:
32+
/// <list type="bullet">
33+
/// <item><description>No parameters are allowed.</description></item>
34+
/// <item><description>Return type must be <see cref="void"/>.</description></item>
35+
/// <item><description>Method must be an instance method (non-static).</description></item>
36+
/// </list>
37+
/// </remarks>
38+
public class StarMapBeforeMainAttribute : StarMapMethodAttribute
39+
{
40+
public override bool IsValidSignature(MethodInfo method)
41+
{
42+
return method.ReturnType == typeof(void) &&
43+
method.GetParameters().Length == 0;
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Methods marked with this attribute will be called immediately when the mod is loaded by KSA.
2349
/// </summary>
2450
/// <remarks>
2551
/// Methods using this attribute must match the following signature:

StarMap.API/StarMap.API.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</ItemGroup>
1212

1313
<ItemGroup Condition="'$(Configuration)' != 'Debug'">
14-
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.6">
14+
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.9">
1515
<IncludeAssets>compile; build; analyzers</IncludeAssets>
1616
<PrivateAssets>all</PrivateAssets>
1717
</PackageReference>

StarMap.Core/ModRepository/LoadedModRepository.cs

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ namespace StarMap.Core.ModRepository
88
internal class LoadedModRepository : IDisposable
99
{
1010
private readonly AssemblyLoadContext _coreAssemblyLoadContext;
11+
1112
private readonly Dictionary<string, StarMapMethodAttribute> _registeredMethodAttributes = [];
13+
private readonly Dictionary<string, bool> _attemptedMods = [];
14+
private readonly Dictionary<string, ModAssemblyLoadContext> _modLoadContexts = [];
1215

1316
private readonly ModRegistry _mods = new();
1417
public ModRegistry Mods => _mods;
@@ -38,23 +41,72 @@ public LoadedModRepository(AssemblyLoadContext coreAssemblyLoadContext)
3841
.ToDictionary();
3942
}
4043

41-
public void LoadMod(Mod mod)
44+
public void Init()
45+
{
46+
PrepareMods();
47+
}
48+
49+
private void PrepareMods()
50+
{
51+
ModLibrary.PrepareManifest();
52+
53+
var mods = ModLibrary.Manifest.Mods;
54+
if (mods is null) return;
55+
56+
string rootPath = "Content";
57+
string path = Path.Combine(new ReadOnlySpan<string>(in rootPath));
58+
59+
foreach (var mod in mods)
60+
{
61+
var modPath = Path.Combine(path, mod.Id);
62+
63+
if (!LoadMod(mod.Id, modPath))
64+
{
65+
_attemptedMods[mod.Id] = false;
66+
continue;
67+
}
68+
69+
if (_mods.GetBeforeMainAction(mod.Id) is (object @object, MethodInfo method))
70+
{
71+
method.Invoke(@object, []);
72+
}
73+
_attemptedMods[mod.Id] = true;
74+
}
75+
}
76+
77+
public void ModPrepareSystems(Mod mod)
4278
{
43-
var fullPath = Path.GetFullPath(mod.DirectoryPath);
44-
var filePath = Path.Combine(fullPath, $"{mod.Name}.dll");
45-
var folderExists = Directory.Exists(fullPath);
46-
var fileExists = File.Exists(filePath);
79+
if (!_attemptedMods.TryGetValue(mod.Id, out var succeeded))
80+
{
81+
succeeded = LoadMod(mod.Id, mod.DirectoryPath);
82+
}
4783

48-
if (!folderExists || !fileExists) return;
84+
if (!succeeded) return;
85+
86+
if (_mods.GetPrepareSystemsAction(mod.Id) is (object @object, MethodInfo method))
87+
{
88+
method.Invoke(@object, [mod]);
89+
}
90+
}
91+
92+
private bool LoadMod(string modId, string modDirectory)
93+
{
94+
var fullPath = Path.GetFullPath(modDirectory);
95+
var modAssemblyFile = Path.Combine(fullPath, $"{modId}.dll");
96+
var assemblyExists = File.Exists(modAssemblyFile);
4997

50-
var modLoadContext = new ModAssemblyLoadContext(mod, _coreAssemblyLoadContext);
51-
var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = mod.Name });
98+
if (!assemblyExists) return false;
5299

53-
var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.IsDefined(typeof(StarMapModAttribute), inherit: false));
54-
if (modClass is null) return;
100+
var modLoadContext = new ModAssemblyLoadContext(modId, modDirectory, _coreAssemblyLoadContext);
101+
var modAssembly = modLoadContext.LoadFromAssemblyName(new AssemblyName() { Name = modId });
102+
103+
var modClass = modAssembly.GetTypes().FirstOrDefault(type => type.GetCustomAttributes().Any(attr => attr.GetType().Name == typeof(StarMapModAttribute).Name));
104+
if (modClass is null) return false;
55105

56106
var modObject = Activator.CreateInstance(modClass);
57-
if (modObject is null) return;
107+
if (modObject is null) return false;
108+
109+
_modLoadContexts.Add(modId, modLoadContext);
58110

59111
var classMethods = modClass.GetMethods();
60112
var immediateLoadMethods = new List<MethodInfo>();
@@ -73,16 +125,12 @@ public void LoadMod(Mod mod)
73125
immediateLoadMethods.Add(classMethod);
74126
}
75127

76-
_mods.Add(attr, modObject, classMethod);
128+
_mods.Add(modId, attr, modObject, classMethod);
77129
}
78130
}
79131

80-
foreach (var method in immediateLoadMethods)
81-
{
82-
method.Invoke(modObject, [mod]);
83-
}
84-
85-
Console.WriteLine($"StarMap - Loaded mod: {mod.Name}");
132+
Console.WriteLine($"StarMap - Loaded mod: {modId} from {modAssemblyFile}");
133+
return true;
86134
}
87135

88136
public void OnAllModsLoaded()
@@ -100,6 +148,11 @@ public void Dispose()
100148
method.Invoke(@object, []);
101149
}
102150

151+
foreach (var modLoadContext in _modLoadContexts.Values)
152+
{
153+
modLoadContext.Unload();
154+
}
155+
103156
_mods.Dispose();
104157
}
105158
}

StarMap.Core/ModRepository/ModAssemblyLoadContext.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@
22
using System.Reflection;
33
using System.Runtime.Loader;
44

5-
namespace StarMap.Core
5+
namespace StarMap.Core.ModRepository
66
{
77
internal class ModAssemblyLoadContext : AssemblyLoadContext
88
{
99
private readonly AssemblyLoadContext _coreAssemblyLoadContext;
1010
private readonly AssemblyDependencyResolver _modDependencyResolver;
1111

12-
public ModAssemblyLoadContext(Mod mod, AssemblyLoadContext coreAssemblyContext)
12+
public ModAssemblyLoadContext(string modId, string modDirectory, AssemblyLoadContext coreAssemblyContext)
1313
: base(isCollectible: true)
1414
{
1515
_coreAssemblyLoadContext = coreAssemblyContext;
1616

1717
_modDependencyResolver = new AssemblyDependencyResolver(
18-
Path.GetFullPath(Path.Combine(mod.DirectoryPath, mod.Name + ".dll"))
18+
Path.GetFullPath(Path.Combine(modDirectory, modId + ".dll"))
1919
);
2020
}
2121

2222
protected override Assembly? Load(AssemblyName assemblyName)
2323
{
24-
var existingInDefault = Default.Assemblies
24+
var existingInDefault = AssemblyLoadContext.Default.Assemblies
2525
.FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName.Name, StringComparison.OrdinalIgnoreCase));
2626
if (existingInDefault != null)
2727
return existingInDefault;
@@ -31,12 +31,24 @@ public ModAssemblyLoadContext(Mod mod, AssemblyLoadContext coreAssemblyContext)
3131
if (existingInGameContext != null)
3232
return existingInGameContext;
3333

34+
if (_coreAssemblyLoadContext != null)
35+
{
36+
try
37+
{
38+
var asm = _coreAssemblyLoadContext.LoadFromAssemblyName(assemblyName);
39+
if (asm != null)
40+
return asm;
41+
}
42+
catch (FileNotFoundException)
43+
{
44+
}
45+
}
46+
3447
var foundPath = _modDependencyResolver.ResolveAssemblyToPath(assemblyName);
3548
if (foundPath is null)
3649
return null;
3750

38-
var path = Path.GetFullPath(foundPath);
39-
return path != null ? LoadFromAssemblyPath(path) : null;
51+
return LoadFromAssemblyPath(Path.GetFullPath(foundPath));
4052
}
4153
}
4254
}

StarMap.Core/ModRepository/ModRegistry.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1-
using StarMap.API;
1+
using KSA;
2+
using StarMap.API;
23
using System.Reflection;
34

45
namespace StarMap.Core.ModRepository
56
{
67
internal sealed class ModRegistry : IDisposable
78
{
8-
private readonly Dictionary<Type, List<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>> _map = new();
9+
private readonly Dictionary<Type, List<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>> _map = [];
10+
private readonly Dictionary<string, (object @object, MethodInfo method)> _beforeMainActions = [];
11+
private readonly Dictionary<string, (object @object, MethodInfo method)> _prepareSystemsActions = [];
912

10-
public void Add(StarMapMethodAttribute attribute, object @object, MethodInfo method)
13+
public void Add(string modId, StarMapMethodAttribute attribute, object @object, MethodInfo method)
1114
{
1215
var attributeType = attribute.GetType();
1316

14-
// --- add instance ---
1517
if (!_map.TryGetValue(attributeType, out var list))
1618
{
1719
list = [];
1820
_map[attributeType] = list;
1921
}
2022

23+
if (attribute.GetType() == typeof(StarMapBeforeMainAttribute))
24+
_beforeMainActions[modId] = (@object, method);
25+
26+
if (attribute.GetType() == typeof(StarMapImmediateLoadAttribute))
27+
_prepareSystemsActions[modId] = (@object, method);
28+
2129
list.Add((attribute, @object, method));
2230
}
2331

@@ -39,6 +47,16 @@ public void Add(StarMapMethodAttribute attribute, object @object, MethodInfo met
3947
: Array.Empty<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>();
4048
}
4149

50+
public (object @object, MethodInfo method)? GetBeforeMainAction(string modId)
51+
{
52+
return _beforeMainActions.TryGetValue(modId, out var action) ? action : null;
53+
}
54+
55+
public (object @object, MethodInfo method)? GetPrepareSystemsAction(string modId)
56+
{
57+
return _prepareSystemsActions.TryGetValue(modId, out var action) ? action : null;
58+
}
59+
4260
public void Dispose()
4361
{
4462
_map.Clear();

StarMap.Core/Patches/ModPatches.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class ModPatches
1010
[HarmonyPrefix]
1111
public static void OnLoadMod(this Mod __instance)
1212
{
13-
StarMapCore.Instance?.LoadedMods.LoadMod(__instance);
13+
StarMapCore.Instance?.LoadedMods.ModPrepareSystems(__instance);
1414
}
1515
}
1616
}

StarMap.Core/StarMap.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</ItemGroup>
2121

2222
<ItemGroup Condition="'$(Configuration)' != 'Debug'">
23-
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.6">
23+
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.9">
2424
<ExcludeAssets>runtime</ExcludeAssets>
2525
</PackageReference>
2626
</ItemGroup>

StarMap.Core/StarMapCore.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext)
2424

2525
public void Init()
2626
{
27+
_loadedMods.Init();
2728
_harmony.PatchAll(typeof(StarMapCore).Assembly);
2829
}
2930

0 commit comments

Comments
 (0)