Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
28 changes: 27 additions & 1 deletion StarMap.API/BaseAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,33 @@ public abstract class StarMapMethodAttribute : Attribute
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Methods using this attribute must match the following signature:
///
/// <code>
/// public void MethodName();
/// </code>
///
/// Specifically:
/// <list type="bullet">
/// <item><description>No parameters are allowed.</description></item>
/// <item><description>Return type must be <see cref="void"/>.</description></item>
/// <item><description>Method must be an instance method (non-static).</description></item>
/// </list>
/// </remarks>
public class StarMapBeforeMainAttribute : StarMapMethodAttribute
{
public override bool IsValidSignature(MethodInfo method)
{
return method.ReturnType == typeof(void) &&
method.GetParameters().Length == 0;
}
}

/// <summary>
/// Methods marked with this attribute will be called immediately when the mod is loaded by KSA.
/// </summary>
/// <remarks>
/// Methods using this attribute must match the following signature:
Expand Down
2 changes: 1 addition & 1 deletion StarMap.API/StarMap.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' != 'Debug'">
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.6">
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.9">
<IncludeAssets>compile; build; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
89 changes: 71 additions & 18 deletions StarMap.Core/ModRepository/LoadedModRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ namespace StarMap.Core.ModRepository
internal class LoadedModRepository : IDisposable
{
private readonly AssemblyLoadContext _coreAssemblyLoadContext;

private readonly Dictionary<string, StarMapMethodAttribute> _registeredMethodAttributes = [];
private readonly Dictionary<string, bool> _attemptedMods = [];
private readonly Dictionary<string, ModAssemblyLoadContext> _modLoadContexts = [];

private readonly ModRegistry _mods = new();
public ModRegistry Mods => _mods;
Expand Down Expand Up @@ -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<string>(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<MethodInfo>();
Expand All @@ -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()
Expand All @@ -100,6 +148,11 @@ public void Dispose()
method.Invoke(@object, []);
}

foreach (var modLoadContext in _modLoadContexts.Values)
{
modLoadContext.Unload();
}

_mods.Dispose();
}
}
Expand Down
24 changes: 18 additions & 6 deletions StarMap.Core/ModRepository/ModAssemblyLoadContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
}
}
}
26 changes: 22 additions & 4 deletions StarMap.Core/ModRepository/ModRegistry.cs
Original file line number Diff line number Diff line change
@@ -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<Type, List<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>> _map = new();
private readonly Dictionary<Type, List<(StarMapMethodAttribute attribute, object @object, MethodInfo method)>> _map = [];
private readonly Dictionary<string, (object @object, MethodInfo method)> _beforeMainActions = [];
private readonly Dictionary<string, (object @object, MethodInfo method)> _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));
}

Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion StarMap.Core/Patches/ModPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion StarMap.Core/StarMap.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' != 'Debug'">
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.6">
<PackageReference Include="StarMap.KSA.Dummy" Version="1.0.9">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions StarMap.Core/StarMapCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext)

public void Init()
{
_loadedMods.Init();
_harmony.PatchAll(typeof(StarMapCore).Assembly);
}

Expand Down
19 changes: 10 additions & 9 deletions StarMap/GameSurveyer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]);
}

Expand Down
Loading