Skip to content

Commit

Permalink
Added mod manager
Browse files Browse the repository at this point in the history
  • Loading branch information
nexus4880 committed Nov 27, 2024
1 parent 7002468 commit 529f806
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Fuyu.Backend/Fuyu.Backend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<ProjectReference Include="../Fuyu.Backend.Arena/Fuyu.Backend.Arena.csproj" />
<ProjectReference Include="../Fuyu.Backend.Core/Fuyu.Backend.Core.csproj" />
<ProjectReference Include="../Fuyu.Backend.EFT/Fuyu.Backend.EFT.csproj" />
<ProjectReference Include="../Fuyu.DependencyInjection/Fuyu.DependencyInjection.csproj" />
<ProjectReference Include="../Fuyu.Modding/Fuyu.Modding.csproj" />
</ItemGroup>

<Target Name="PostClean" AfterTargets="Clean">
Expand Down
13 changes: 10 additions & 3 deletions Fuyu.Backend/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
using Fuyu.Backend.EFT;
using Fuyu.Backend.EFT.Servers;
using Fuyu.Backend.BSG.DTO.Services;
using Fuyu.DependencyInjection;
using Fuyu.Modding;
using System.Threading.Tasks;

namespace Fuyu.Backend
{
public class Program
{
static void Main()
static async Task Main()
{
var container = new DependencyContainer();

CoreDatabase.Load();
EftDatabase.Load();
TraderDatabase.Load();
Expand All @@ -24,7 +29,9 @@ static void Main()
eftMainServer.RegisterServices();
eftMainServer.Start();

Terminal.WaitForInput();
}
await ModManager.Instance.Load(container);
Terminal.WaitForInput();
await ModManager.Instance.UnloadAll();
}
}
}
9 changes: 9 additions & 0 deletions Fuyu.DependencyInjection/DependencyContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ public T Resolve<T>(string id) where T : class
return (T)Resolve(id, typeof(T));
}

/// <summary>
/// Used to resolve an object of the desired type and id
/// </summary>
public T Resolve<TBase, T>(string id) where T : class
where TBase : class
{
return (T)Resolve(id, typeof(TBase));
}

#endregion

#region ResolveAll
Expand Down
12 changes: 12 additions & 0 deletions Fuyu.Modding/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Fuyu.DependencyInjection;

namespace Fuyu.Modding
{
public static class Extensions
{
public static T ResolveMod<T>(this DependencyContainer container, string id) where T: Mod
{
return container.Resolve<Mod, T>(id);
}
}
}
9 changes: 9 additions & 0 deletions Fuyu.Modding/Fuyu.Modding.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Fuyu.Common\Fuyu.Common.csproj" />
<ProjectReference Include="..\Fuyu.DependencyInjection\Fuyu.DependencyInjection.csproj" />
</ItemGroup>
</Project>
40 changes: 40 additions & 0 deletions Fuyu.Modding/Mod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Threading.Tasks;
using Fuyu.DependencyInjection;

namespace Fuyu.Modding
{
public abstract class Mod
{
internal bool IsLoaded { get; set; }

/// <summary>
/// Your plugin will be automatically registered into this dependency container with this ID.
/// </summary>
public abstract string Id { get; }

/// <summary>
/// This is the name that will be used in logs.
/// </summary>
public abstract string Name { get; }

// TODO: Load dependencies first
public virtual string[] Dependencies { get; }

/// <summary>
/// Gets called after the server has set up everything.
/// </summary>
/// <param name="container"></param>
public virtual Task OnLoad(DependencyContainer container)
{
return Task.CompletedTask;
}

/// <summary>
/// Gets called whenever the server shuts down
/// </summary>
public virtual Task OnShutdown()
{
return Task.CompletedTask;
}
}
}
208 changes: 208 additions & 0 deletions Fuyu.Modding/ModManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Fuyu.Common.IO;
using Fuyu.DependencyInjection;

namespace Fuyu.Modding
{
// This should live during the entire lifetime of the application
public class ModManager
{
private static ModManager _instance;

private readonly List<Mod> _sortedMods = new List<Mod>();

private static readonly object _lock = new object();

public static ModManager Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ModManager();
}

return _instance;
}
}
}

public ModManager()
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}

private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
foreach (var mod in _sortedMods)
{
var modAssembly = mod.GetType().Assembly;
if (modAssembly.FullName == args.Name)
{
return modAssembly;
}
}

return null;
}

public Task Load(DependencyContainer container)
{
var modsDirectory = Path.Combine(Environment.CurrentDirectory, "Fuyu", "Mods");
if (!Directory.Exists(modsDirectory))
{
Directory.CreateDirectory(modsDirectory);
return Task.CompletedTask;
}

var dllFiles = Directory.GetFiles(modsDirectory, "*.dll");
if (dllFiles.Length == 0)
{
return Task.CompletedTask;
}

return Load_Internal(dllFiles, container);
}

private async Task Load_Internal(string[] filePaths, DependencyContainer container)
{
List<Mod> mods = new List<Mod>();
foreach (var filePath in filePaths)
{
// Load the dll
var bytes = File.ReadAllBytes(filePath);
var assembly = AppDomain.CurrentDomain.Load(bytes);

// Get types where T implements IMod
var modTypes = assembly.GetExportedTypes()
.Where(t => typeof(Mod).IsAssignableFrom(t));

// Technically you could export multiple mods per file
foreach (var modType in modTypes)
{
var mod = (Mod)Activator.CreateInstance(modType);

Terminal.WriteLine($"Adding mod {mod.Name}");

mods.Add(mod);
}
}

if (mods.Count == 0)
{
return;
}

CheckDependencies(mods);
SortMods(mods);

Terminal.WriteLine($"Current order: {string.Join(", ", _sortedMods.Select(p => p.Name))}");

foreach (var mod in _sortedMods)
{
// All mods will be registered to the container
container.RegisterSingleton(mod.Id, mod);
}

foreach (var mod in _sortedMods)
{
Terminal.WriteLine($"Loading mod {mod.Name}");
await mod.OnLoad(container);
mod.IsLoaded = true;
}
}

private void SortMods(List<Mod> mods)
{
foreach (var mod in mods)
{
AddModToSorted(mods, mod, null);
}
}

private void AddModToSorted(List<Mod> mods, Mod mod, Mod dependent)
{
if (_sortedMods.Contains(mod))
{
return;
}

if (mod.Dependencies != null)
{
foreach (var dependency in mod.Dependencies)
{
if (dependent != null && dependent.Id == dependency)
{
throw new Exception($"Cyclic dependency {mod.Id} <-> {dependency}");
}

AddModToSorted(mods, mods.Find(m => m.Id == dependency), mod);
}
}

_sortedMods.Add(mod);
}

private void CheckDependencies(List<Mod> mods)
{
var runAgain = false;

for (var i = 0; i < mods.Count; i++)
{
var mod = mods[i];
var hasAllDependencies = true;

if (mod.Dependencies != null)
{
foreach (var dependency in mod.Dependencies)
{
if (mods.FindIndex(m => m.Id == dependency) == -1)
{
hasAllDependencies = false;
Terminal.WriteLine($"{mod.Id} is missing dependency {dependency} and will not be loaded");
}
}
}

if (!hasAllDependencies)
{
mods.RemoveAt(i);
i--;
runAgain = true;
}
}

if (runAgain)
{
CheckDependencies(mods);
}
}

private async Task Unload(Mod mod)
{
if (mod.IsLoaded)
{
Terminal.WriteLine($"Unloading mod {mod.Name}");
await mod.OnShutdown();
mod.IsLoaded = false;
}

_sortedMods.Remove(mod);
}

public async Task UnloadAll()
{
while (_sortedMods.Count > 0)
{
await Unload(_sortedMods[_sortedMods.Count - 1]);
}
}
}
}
9 changes: 9 additions & 0 deletions Fuyu.sln
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fuyu.Tests.Backend.Core", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fuyu.DependencyInjection", "Fuyu.DependencyInjection\Fuyu.DependencyInjection.csproj", "{8B94FFE8-658E-4261-8B96-7FBD0C08374B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fuyu.Modding", "Fuyu.Modding\Fuyu.Modding.csproj", "{1687E80C-E174-4844-9939-7AAF6E1139F0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -117,8 +119,15 @@ Global
{8B94FFE8-658E-4261-8B96-7FBD0C08374B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B94FFE8-658E-4261-8B96-7FBD0C08374B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B94FFE8-658E-4261-8B96-7FBD0C08374B}.Release|Any CPU.Build.0 = Release|Any CPU
{1687E80C-E174-4844-9939-7AAF6E1139F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1687E80C-E174-4844-9939-7AAF6E1139F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1687E80C-E174-4844-9939-7AAF6E1139F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1687E80C-E174-4844-9939-7AAF6E1139F0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {67ADC3F3-9ADA-4F75-AEDC-DB5A17E9E5AF}
EndGlobalSection
EndGlobal

0 comments on commit 529f806

Please sign in to comment.