diff --git a/FEZ.HAT.mm.csproj b/FEZ.HAT.mm.csproj
index 61842d3..3c80fbc 100644
--- a/FEZ.HAT.mm.csproj
+++ b/FEZ.HAT.mm.csproj
@@ -10,7 +10,7 @@
HatModLoader
enable
- enable
+ disable
true
diff --git a/Installers/ModMenuInstaller.cs b/Installers/ModMenuInstaller.cs
index f754c9d..abf3a61 100644
--- a/Installers/ModMenuInstaller.cs
+++ b/Installers/ModMenuInstaller.cs
@@ -105,10 +105,10 @@ private static void CreateAndAddModLevel(object MenuBase)
else
{
AddInactiveStringItem(null, null);
- AddInactiveStringItem(null, () => Hat.Instance.Mods[modMenuCurrentIndex].Info.Name);
- AddInactiveStringItem(null, () => Hat.Instance.Mods[modMenuCurrentIndex].Info.Description);
- AddInactiveStringItem(null, () => $"made by {Hat.Instance.Mods[modMenuCurrentIndex].Info.Author}");
- AddInactiveStringItem(null, () => $"version {Hat.Instance.Mods[modMenuCurrentIndex].Info.Version}");
+ AddInactiveStringItem(null, () => Hat.Instance.Mods[modMenuCurrentIndex].Metadata.Name);
+ AddInactiveStringItem(null, () => Hat.Instance.Mods[modMenuCurrentIndex].Metadata.Description);
+ AddInactiveStringItem(null, () => $"made by {Hat.Instance.Mods[modMenuCurrentIndex].Metadata.Author}");
+ AddInactiveStringItem(null, () => $"version {Hat.Instance.Mods[modMenuCurrentIndex].Metadata.Version}");
}
// add created menu level to the main menu
diff --git a/Patches/Fez.cs b/Patches/Fez.cs
index bce376d..2a050a8 100644
--- a/Patches/Fez.cs
+++ b/Patches/Fez.cs
@@ -31,7 +31,7 @@ public void ctor()
protected override void Initialize()
{
HatML = new Hat(this);
- HatML.InitalizeAssemblies();
+ HatML.InitializeAssemblies();
//HatML.InitializeAssets(musicPass: false);
orig_Initialize();
DrawingTools.Init();
@@ -44,7 +44,7 @@ internal static void LoadComponents(Fez game)
bool doLoad = !ServiceHelper.FirstLoadDone;
orig_LoadComponents(game);
if (doLoad) {
- HatML.InitalizeComponents();
+ HatML.InitializeComponents();
}
}
diff --git a/Patches/FezLogo.cs b/Patches/FezLogo.cs
index 604e9de..9faacda 100644
--- a/Patches/FezLogo.cs
+++ b/Patches/FezLogo.cs
@@ -146,7 +146,7 @@ public override void Draw(GameTime gameTime)
Viewport viewport = DrawingTools.GetViewport();
int modCount = Hat.Instance.Mods.Count;
- string hatText = $"HAT Mod Loader, version {Hat.Version}, {modCount} mod{(modCount != 1 ? "s" : "")} installed";
+ string hatText = $"HAT Mod Loader, version {Hat.VersionString}, {modCount} mod{(modCount != 1 ? "s" : "")} installed";
if (modCount == 69) hatText += "... nice";
Color textColor = Color.Lerp(Color.White, Color.Black, alpha);
@@ -154,7 +154,7 @@ public override void Draw(GameTime gameTime)
float lineHeight = DrawingTools.DefaultFont.LineSpacing * DrawingTools.DefaultFontSize;
- int invalidModCount = Hat.Instance.InvalidMods.Count;
+ int invalidModCount = Hat.Instance.InvalidModsCount;
string invalidModsText = $"Could not load {invalidModCount} mod{(invalidModCount != 1 ? "s" : "")}. Check logs for more details.";
DrawingTools.BeginBatch();
diff --git a/Patches/Program.cs b/Patches/Program.cs
index 5f0af19..be8ca80 100644
--- a/Patches/Program.cs
+++ b/Patches/Program.cs
@@ -1,7 +1,6 @@
using Common;
using HatModLoader.Source;
using System.Globalization;
-using System.Runtime.InteropServices;
namespace FezGame
{
@@ -11,8 +10,8 @@ internal static class patch_Program
private static void Main(string[] args)
{
- // Ensuring that dependency resolver is registered as soon as it's possible.
- DependencyResolver.Register();
+ // Ensuring that required dependencies can be resolved before anything else.
+ Hat.RegisterRequiredDependencyResolvers();
// Ensure uniform culture
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
diff --git a/Source/AssemblyResolving/AssemblyResolveCompability.cs b/Source/AssemblyResolving/AssemblyResolveCompability.cs
new file mode 100644
index 0000000..7917483
--- /dev/null
+++ b/Source/AssemblyResolving/AssemblyResolveCompability.cs
@@ -0,0 +1,56 @@
+using System.Reflection;
+
+namespace HatModLoader.Source.AssemblyResolving
+{
+ internal static class AssemblyResolveCompability
+ {
+ public static bool MatchesRequest(this AssemblyName assemblyName, ResolveEventArgs args, bool allowRollForward)
+ {
+ var requestedName = new AssemblyName(args.Name);
+
+ return assemblyName.Name == requestedName.Name &&
+ assemblyName.CultureName == requestedName.CultureName &&
+ ComparePublicKeyTokens(assemblyName.GetPublicKeyToken(), requestedName.GetPublicKeyToken()) &&
+ CompareVersions(assemblyName.Version, requestedName.Version, allowRollForward);
+ }
+
+ private static bool ComparePublicKeyTokens(byte[] tokenA, byte[] tokenB)
+ {
+ // Avoiding usage of stuff like SequenceEqual to prevent accidental dependency request at this stage.
+
+ if (tokenA == null && tokenB == null)
+ {
+ return true;
+ }
+
+ if (tokenA == null || tokenB == null || tokenA.Length != tokenB.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < tokenA.Length; i++)
+ {
+ if (tokenA[i] != tokenB[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool CompareVersions(Version checkedVersion, Version requiredVersion, bool allowRollForward)
+ {
+ if (allowRollForward)
+ {
+ return checkedVersion >= requiredVersion;
+ }
+
+ return checkedVersion.Major == requiredVersion.Major &&
+ checkedVersion.Minor == requiredVersion.Minor &&
+ checkedVersion.Build == requiredVersion.Build &&
+ checkedVersion.Revision >= requiredVersion.Revision;
+ }
+ }
+}
+
diff --git a/Source/AssemblyResolving/AssemblyResolverRegistry.cs b/Source/AssemblyResolving/AssemblyResolverRegistry.cs
new file mode 100644
index 0000000..f629379
--- /dev/null
+++ b/Source/AssemblyResolving/AssemblyResolverRegistry.cs
@@ -0,0 +1,30 @@
+namespace HatModLoader.Source.AssemblyResolving
+{
+ internal static class AssemblyResolverRegistry
+ {
+ private static readonly HashSet RegisteredResolvers = new();
+
+ public static void Register(IAssemblyResolver resolver)
+ {
+ if (RegisteredResolvers.Contains(resolver))
+ {
+ return;
+ }
+
+ RegisteredResolvers.Add(resolver);
+ AppDomain.CurrentDomain.AssemblyResolve += resolver.ProvideAssembly;
+ }
+
+ public static void Unregister(IAssemblyResolver resolver)
+ {
+ if (!RegisteredResolvers.Contains(resolver))
+ {
+ return;
+ }
+
+ RegisteredResolvers.Remove(resolver);
+ AppDomain.CurrentDomain.AssemblyResolve -= resolver.ProvideAssembly;
+ }
+ }
+}
+
diff --git a/Source/AssemblyResolving/HatSubdirectoryAssemblyResolver.cs b/Source/AssemblyResolving/HatSubdirectoryAssemblyResolver.cs
new file mode 100644
index 0000000..fa97161
--- /dev/null
+++ b/Source/AssemblyResolving/HatSubdirectoryAssemblyResolver.cs
@@ -0,0 +1,69 @@
+using System.Reflection;
+
+namespace HatModLoader.Source.AssemblyResolving
+{
+ internal class HatSubdirectoryAssemblyResolver : IAssemblyResolver
+ {
+ private static readonly string DependencyDirectory = "HATDependencies";
+
+ private readonly string _subdirectoryName;
+
+ public HatSubdirectoryAssemblyResolver(string subdirectoryName)
+ {
+ this._subdirectoryName = subdirectoryName;
+ }
+
+
+ public Assembly ProvideAssembly(object sender, ResolveEventArgs args)
+ {
+ foreach (var file in EnumerateAssemblyFilesInSubdirectory())
+ {
+ if (!TryGetAssemblyName(file, out var assemblyName))
+ {
+ continue;
+ }
+
+ if (assemblyName.MatchesRequest(args, true))
+ {
+ return Assembly.LoadFrom(file);
+ }
+ }
+
+ return null;
+ }
+
+ private IEnumerable EnumerateAssemblyFilesInSubdirectory()
+ {
+ var path = Path.Combine(DependencyDirectory, _subdirectoryName);
+
+ if (!Directory.Exists(path))
+ {
+ yield break;
+ }
+
+ foreach (var file in Directory.EnumerateFiles(path, "*.dll", SearchOption.TopDirectoryOnly))
+ {
+ yield return file;
+ }
+
+ foreach (var file in Directory.EnumerateFiles(path, "*.exe", SearchOption.TopDirectoryOnly))
+ {
+ yield return file;
+ }
+ }
+
+ private bool TryGetAssemblyName(string filePath, out AssemblyName assemblyName)
+ {
+ assemblyName = null;
+ try
+ {
+ assemblyName = AssemblyName.GetAssemblyName(filePath);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/Source/AssemblyResolving/IAssemblyResolver.cs b/Source/AssemblyResolving/IAssemblyResolver.cs
new file mode 100644
index 0000000..09df89f
--- /dev/null
+++ b/Source/AssemblyResolving/IAssemblyResolver.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+
+namespace HatModLoader.Source.AssemblyResolving
+{
+ internal interface IAssemblyResolver
+ {
+ public Assembly ProvideAssembly(object sender, ResolveEventArgs args);
+ }
+}
+
diff --git a/Source/AssemblyResolving/ModInternalAssemblyResolver.cs b/Source/AssemblyResolving/ModInternalAssemblyResolver.cs
new file mode 100644
index 0000000..f365de9
--- /dev/null
+++ b/Source/AssemblyResolving/ModInternalAssemblyResolver.cs
@@ -0,0 +1,68 @@
+using System.Reflection;
+using HatModLoader.Source.ModDefinition;
+using Mono.Cecil;
+
+namespace HatModLoader.Source.AssemblyResolving
+{
+ internal class ModInternalAssemblyResolver : IAssemblyResolver
+ {
+ private readonly ModIdentity _mod;
+
+ private readonly Dictionary _cachedAssemblyPaths = new();
+
+ public ModInternalAssemblyResolver(ModIdentity mod)
+ {
+ _mod = mod;
+ CacheAssemblyPaths();
+ }
+
+ public Assembly ProvideAssembly(object sender, ResolveEventArgs args)
+ {
+ if (_mod.CodeMod != null && _mod.CodeMod.Assembly.GetName().MatchesRequest(args, false))
+ {
+ return _mod.CodeMod.Assembly;
+ }
+
+ foreach(var assemblyName in _cachedAssemblyPaths.Keys)
+ {
+ if (assemblyName.MatchesRequest(args, false))
+ {
+ using var assemblyData = _mod.FileProxy.OpenFile(_cachedAssemblyPaths[assemblyName]);
+ var assemblyBytes = new byte[assemblyData.Length];
+ assemblyData.Read(assemblyBytes, 0, assemblyBytes.Length);
+ return Assembly.Load(assemblyBytes);
+ }
+ }
+ return null;
+ }
+
+ private void CacheAssemblyPaths()
+ {
+ foreach (var filePath in EnumerateAssemblyFilesInMod())
+ {
+ using var assemblyFile = _mod.FileProxy.OpenFile(filePath);
+ using var assemblyDef = AssemblyDefinition.ReadAssembly(assemblyFile, new ReaderParameters { ReadSymbols = false });
+ var fullName = new AssemblyName(assemblyDef.Name.ToString());
+
+ if (!_cachedAssemblyPaths.ContainsKey(fullName))
+ {
+ _cachedAssemblyPaths[fullName] = filePath;
+ }
+ }
+ }
+
+ private IEnumerable EnumerateAssemblyFilesInMod()
+ {
+ foreach (var file in _mod.FileProxy.EnumerateFiles(""))
+ {
+ if (
+ file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
+ file.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
+ ) {
+ yield return file;
+ }
+ }
+ }
+ }
+}
+
diff --git a/Source/DependencyResolver.cs b/Source/DependencyResolver.cs
deleted file mode 100644
index ab12fb1..0000000
--- a/Source/DependencyResolver.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using Common;
-using System.Reflection;
-
-namespace HatModLoader.Source
-{
- internal static class DependencyResolver
- {
- private static readonly string DependencyDirectory = "HATDependencies";
-
- private static readonly Dictionary DependencyMap = new();
- private static readonly Dictionary DependencyCache = new();
-
- public static void Register()
- {
- AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembliesEventHandler;
- }
-
- private static Assembly ResolveAssembliesEventHandler(object sender, ResolveEventArgs args)
- {
- FillInDependencyMap(args);
-
- Assembly assembly;
- if (DependencyCache.TryGetValue(IsolateName(args.Name), out assembly)) return assembly;
- if (TryResolveAssemblyFor("MonoMod", args, out assembly)) return assembly;
- if (TryResolveAssemblyFor("FEZRepacker.Core", args, out assembly)) return assembly;
- if (TryResolveModdedDependency(args, out assembly)) return assembly;
-
- Logger.Log("HAT", "Could not resolve assembly: \"" + args.Name + "\", required by \"" + args.RequestingAssembly?.FullName ?? "(none)" + "\"");
-
- return default!;
- }
-
- private static void FillInDependencyMap(ResolveEventArgs args)
- {
- if (args.RequestingAssembly == null) return;
-
- var assemblyName = IsolateName(args.Name);
- var requestingAssemblyName = IsolateName(args.RequestingAssembly?.FullName ?? "");
-
- if (DependencyMap.ContainsKey(requestingAssemblyName))
- {
- DependencyMap[assemblyName] = DependencyMap[requestingAssemblyName];
- }
- else
- {
- DependencyMap[assemblyName] = args.RequestingAssembly!;
- }
- }
-
- private static bool TryResolveAssemblyFor(string assemblyName, ResolveEventArgs args, out Assembly assembly)
- {
- if (!ShouldResolveNamedFor(assemblyName, args))
- {
- assembly = default!;
- return false;
- }
-
- var requiredAssemblyName = args.Name.Split(',')[0];
- var dependencyPath = Path.Combine(DependencyDirectory, assemblyName);
-
- foreach (var file in Directory.EnumerateFiles(dependencyPath))
- {
- var fileName = Path.GetFileNameWithoutExtension(file);
-
- if (requiredAssemblyName == fileName)
- {
- assembly = Assembly.Load(File.ReadAllBytes(file));
- DependencyCache[requiredAssemblyName] = assembly;
- return true;
- }
- }
- assembly = default!;
- return false;
- }
-
- private static bool ShouldResolveNamedFor(string assemblyName, ResolveEventArgs args)
- {
- var requiredAssemblyName = IsolateName(args.Name);
- var requestingAssemblyName = IsolateName(args.RequestingAssembly?.FullName ?? "");
- var mainRequestingAssemblyName = IsolateName(GetMainRequiringAssembly(args)?.FullName ?? "");
-
- bool requiredAssemblyValid = requiredAssemblyName.Contains(assemblyName);
- bool requestingAssemblyValid = requestingAssemblyName.Contains(assemblyName);
- bool mainRequestingAssemblyValid = mainRequestingAssemblyName.Contains(assemblyName);
-
- return requiredAssemblyValid || requestingAssemblyValid || mainRequestingAssemblyValid;
- }
-
- private static bool TryResolveModdedDependency(ResolveEventArgs args, out Assembly assembly)
- {
- assembly = default!;
-
- if (Hat.Instance == null) return false;
-
- var requestingMainAssembly = GetMainRequiringAssembly(args);
- if (requestingMainAssembly == null) return false;
-
- var matchingAssembliesInMods = Hat.Instance.Mods
- .Where(mod => mod.Assembly == requestingMainAssembly);
- if (!matchingAssembliesInMods.Any()) return false;
-
- var requiredAssemblyName = IsolateName(args.Name);
- var requiredAssemblyPath = requiredAssemblyName + ".dll";
- var fileProxy = matchingAssembliesInMods.First().FileProxy;
- if (!fileProxy.FileExists(requiredAssemblyPath)) return false;
-
- using var assemblyData = fileProxy.OpenFile(requiredAssemblyPath);
- var assemblyBytes = new byte[assemblyData.Length];
- assemblyData.Read(assemblyBytes, 0, assemblyBytes.Length);
- assembly = Assembly.Load(assemblyBytes);
- DependencyCache[requiredAssemblyName] = assembly;
- return true;
- }
-
- private static Assembly GetMainRequiringAssembly(ResolveEventArgs args)
- {
- var requestingMainAssembly = args.RequestingAssembly;
-
- if(requestingMainAssembly == null)
- {
- return default!;
- }
-
- var requestingAssemblyName = IsolateName(requestingMainAssembly.FullName);
-
- if (DependencyMap.ContainsKey(requestingAssemblyName))
- {
- requestingMainAssembly = DependencyMap[requestingAssemblyName];
- }
-
- return requestingMainAssembly;
- }
-
- private static string IsolateName(string fullAssemblyQualifier)
- {
- return fullAssemblyQualifier.Split(',')[0];
- }
- }
-}
diff --git a/Source/Hat.cs b/Source/Hat.cs
index 5ee4a99..05ac080 100644
--- a/Source/Hat.cs
+++ b/Source/Hat.cs
@@ -1,5 +1,6 @@
using Common;
using FezGame;
+using HatModLoader.Source.AssemblyResolving;
using HatModLoader.Source.Assets;
using HatModLoader.Source.FileProxies;
using HatModLoader.Source.ModDefinition;
@@ -8,338 +9,203 @@ namespace HatModLoader.Source
{
public class Hat
{
- private List ignoredModNames = new();
- private List priorityModNamesList = new();
+ public static readonly Version Version = new("1.2.1");
- public static Hat Instance;
+ private static readonly string ModsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Mods");
- public Fez Game;
- public List Mods;
- public List InvalidMods;
+ private static readonly IList IgnoredModNames = InitializeIgnoredModsList();
- public static string Version
+ private static readonly IList PriorityModNames = InitializePriorityList();
+
+ public List Mods { get; private set; }
+
+ public int InvalidModsCount { get; private set; }
+
+ public static string VersionString
{
get
{
- const string version = "1.2.1";
#if DEBUG
- return $"{version}-dev";
+ return $"{Version}-dev";
#else
- return $"{version}";
+ return Version.ToString();
#endif
}
}
+ public static Hat Instance { get; private set; }
+
+ private readonly Fez _fezGame;
public Hat(Fez fez)
{
Instance = this;
- Game = fez;
-
- Mods = new List();
- InvalidMods = new List();
-
- Logger.Log("HAT", $"HAT Mod Loader {Version}");
- PrepareMods();
+ _fezGame = fez;
+ Initialize();
}
-
- private void PrepareMods()
+ private void Initialize()
{
- LoadMods();
-
- if(Mods.Count == 0)
+ Logger.Log("HAT", $"HAT Mod Loader {VersionString}");
+
+ if (CheckModsFolder())
{
- Logger.Log("HAT", $"No mods have been found in the directory.");
- return;
+ if (GetModProxies(out var proxies))
+ {
+ if (GetModList(proxies, out var mods))
+ {
+ ResolveDependencies(mods);
+ LoadMods();
+ return; // HAT initialized
+ }
+ }
}
-
- InitializeIgnoredModsList();
- InitializePriorityList();
-
- RemoveBlacklistedMods();
- SortModsByPriority();
- RemoveDuplicates();
- InitializeDependencies();
- FilterOutInvalidMods();
- SortModsBasedOnDependencies();
-
- LogLoadedMods();
+
+ Logger.Log("HAT", LogSeverity.Warning, "Skip the initialization process...");
}
- private void EnsureModDirectory()
+ private static bool CheckModsFolder()
{
- if (!Directory.Exists(Mod.GetModsDirectory()))
+ if (!Directory.Exists(ModsDirectory))
{
- Logger.Log("HAT", LogSeverity.Warning, "Main mods directory not found. Creating and skipping mod loading process...");
- Directory.CreateDirectory(Mod.GetModsDirectory());
- return;
+ Logger.Log("HAT", LogSeverity.Warning,"'Mods' directory not found. Creating it...");
+ Directory.CreateDirectory(ModsDirectory);
+ return false;
}
+
+ return true;
}
- private void LoadMods()
+ private static bool GetModProxies(out IEnumerable proxies)
{
- Mods.Clear();
-
- EnsureModDirectory();
-
- var modProxies = EnumerateFileProxiesInModsDirectory();
-
- foreach (var proxy in modProxies)
- {
- bool loadingState = Mod.TryLoad(this, proxy, out Mod mod);
- if (loadingState)
+ proxies = new IEnumerable[]
{
- Mods.Add(mod);
+ DirectoryFileProxy.EnumerateInDirectory(ModsDirectory),
+ ZipFileProxy.EnumerateInDirectory(ModsDirectory),
}
- LogModLoadingState(mod, loadingState);
- }
- }
+ .SelectMany(x => x);
- private static IEnumerable EnumerateFileProxiesInModsDirectory()
- {
- var modsDir = Mod.GetModsDirectory();
-
- return new IEnumerable[]
+ if (!proxies.Any())
{
- DirectoryFileProxy.EnumerateInDirectory(modsDir),
- ZipFileProxy.EnumerateInDirectory(modsDir),
+ Logger.Log("HAT", LogSeverity.Warning, "There are no mods inside 'Mods' directory.");
+ return false;
}
- .SelectMany(x => x);
+
+ return true;
}
- private void LogModLoadingState(Mod mod, bool loadState)
+ private static bool GetModList(in IEnumerable proxies, out IList mods)
{
- if (loadState)
- {
- var libraryInfo = "no library";
- if (mod.IsCodeMod)
- {
- libraryInfo = $"library \"{mod.Info.LibraryName}\"";
- }
- var assetsText = $"{mod.Assets.Count} asset{(mod.Assets.Count != 1 ? "s" : "")}";
- Logger.Log("HAT", $"Loaded mod \"{mod.Info.Name}\" ver. {mod.Info.Version} by {mod.Info.Author} ({assetsText} and {libraryInfo})");
- }
- else
+ mods = new List();
+ foreach (var proxy in proxies.Where(fp => !IgnoredModNames.Contains(fp.ContainerName)))
{
- if (mod.Info.Name == null)
+ if (Metadata.TryLoad(proxy, out var metadata))
{
- Logger.Log("HAT", LogSeverity.Warning, $"Mod \"{mod.FileProxy.ContainerName}\" does not have a valid metadata file.");
- }
- else if (mod.Info.LibraryName != null && mod.Info.LibraryName.Length > 0 && !mod.IsCodeMod)
- {
- var info = $"Mod \"{mod.Info.Name}\" has library name defined (\"{mod.Info.LibraryName}\"), but no such library was found.";
- Logger.Log("HAT", LogSeverity.Warning, info);
- }
- else if (!mod.IsCodeMod && !mod.IsAssetMod)
- {
- Logger.Log("HAT", LogSeverity.Warning, $"Mod \"{mod.Info.Name}\" is empty and will not be added.");
+ mods.Add(new ModIdentity(proxy, metadata));
}
}
- }
-
- private void InitializeIgnoredModsList()
- {
- var ignoredModsNamesFilePath = Path.Combine(Mod.GetModsDirectory(), "ignorelist.txt");
- var defaultContent =
- "# List of directories and zip archives to ignore when loading mods, one per line.\n" +
- "# Lines starting with # will be ignored.\n\n" +
- "ExampleDirectoryModName\n" +
- "ExampleZipPackageName.zip\n";
- ignoredModNames = ModsTextListLoader.LoadOrCreateDefault(ignoredModsNamesFilePath, defaultContent);
- }
-
- private void InitializePriorityList()
- {
- var priorityListFilePath = Path.Combine(Mod.GetModsDirectory(), "prioritylist.txt");
- var defaultContent =
- "# List of directories and zip archives to prioritize during mod loading.\n" +
- "# If present on this list, the mod will be loaded before other mods not listed here or listed below it,\n" +
- "# including newer versions of the same mod. However, it does not override dependency ordering.\n" +
- "# Lines starting with # will be ignored.\n\n" +
- "ExampleDirectoryModName\n" +
- "ExampleZipPackageName.zip\n";
- priorityModNamesList = ModsTextListLoader.LoadOrCreateDefault(priorityListFilePath, defaultContent);
- }
-
- private void RemoveBlacklistedMods()
- {
- Mods = Mods.Where(mod => !ignoredModNames.Contains(mod.FileProxy.ContainerName)).ToList();
- }
-
- private int GetPriorityIndexOfMod(Mod mod)
- {
- var index = priorityModNamesList.IndexOf(mod.FileProxy.ContainerName);
- if (index == -1) index = int.MaxValue;
-
- return index;
- }
- private void SortModsByPriority()
- {
- Mods.Sort((mod1, mod2) =>
- {
- var priorityIndex1 = GetPriorityIndexOfMod(mod1);
- var priorityIndex2 = GetPriorityIndexOfMod(mod2);
- return priorityIndex1.CompareTo(priorityIndex2);
- });
- }
-
- private int CompareDuplicateMods(Mod mod1, Mod mod2)
- {
- var priorityIndex1 = GetPriorityIndexOfMod(mod1);
- var priorityIndex2 = GetPriorityIndexOfMod(mod2);
- var priorityComparison = priorityIndex1.CompareTo(priorityIndex2);
- if(priorityComparison != 0)
+ if (mods.Count < 1)
{
- return priorityComparison;
- }
- else
- {
- // Newest (largest) versions should be first, hence the negative sign.
- return -mod1.CompareVersionsWith(mod2);
+ Logger.Log("HAT", LogSeverity.Warning, "There are no mods to load. Perhaps they are all in 'ignorelist.txt'.");
+ return false;
}
+
+ return true;
}
- private void RemoveDuplicates()
+ private void ResolveDependencies(IList mods)
{
- var uniqueNames = Mods.Select(mod => mod.Info.Name).Distinct().ToList();
- foreach (var modName in uniqueNames)
+ var resolverResult = ModDependencyResolver.Resolve(mods, PriorityModNames);
+ Mods = resolverResult.LoadOrder;
+ InvalidModsCount = resolverResult.Invalid.Count;
+
+ foreach (var node in resolverResult.Invalid)
{
- var sameNamedMods = Mods.Where(mod => mod.Info.Name == modName).ToList();
- if (sameNamedMods.Count() > 1)
- {
- sameNamedMods.Sort(CompareDuplicateMods);
- var newestMod = sameNamedMods.First();
- Logger.Log("HAT", LogSeverity.Warning, $"Multiple instances of mod {modName} detected! Leaving version {newestMod.Info.Version}");
-
- foreach (var mod in sameNamedMods)
- {
- if (mod == newestMod) continue;
- Mods.Remove(mod);
- }
- }
+ Logger.Log("HAT", $"Mod '{node.Mod.Metadata.Name}' is invalid: {node.Details}");
}
- }
-
- private void InitializeDependencies()
- {
+
+ Logger.Log("HAT", "The loading order of mods:");
foreach (var mod in Mods)
{
- mod.InitializeDependencies();
+ Logger.Log("HAT", $" {mod.Metadata.Name} by {mod.Metadata.Author} version {mod.Metadata.Version}");
}
-
- FinalizeDependencies();
}
- private void FinalizeDependencies()
+ private void LoadMods()
{
- for(int i=0;i<=Mods.Count; i++)
+ var assetModCount = 0;
+ var codeModsCount = 0;
+
+ foreach (var mod in Mods)
{
- if(i == Mods.Count)
+ if (AssetMod.TryLoad(mod.FileProxy, mod.Metadata, out var assetMod))
{
- // there's no possible way to have more dependency nesting levels than the mod count. Escape!
- throw new ApplicationException("Stuck in a mod dependency finalization loop!");
+ mod.AssetMod = assetMod;
+ assetModCount += 1;
}
- bool noInvalidMods = true;
- foreach (var mod in Mods)
- {
- if (mod.TryFinalizeDependencies()) continue;
-
- noInvalidMods = false;
- }
- if (noInvalidMods)
+ if (CodeMod.TryLoad(mod.FileProxy, mod.Metadata, out var codeMod))
{
- break;
+ mod.CodeMod = codeMod;
+ codeModsCount += 1;
}
}
- }
-
- private void FilterOutInvalidMods()
- {
- InvalidMods = Mods.Where(mod => !mod.AreDependenciesValid()).ToList();
- foreach (var invalidMod in InvalidMods)
- {
- LogIssuesWithInvalidMod(invalidMod);
- Mods.Remove(invalidMod);
- }
- }
-
- private void LogIssuesWithInvalidMod(Mod invalidMod)
- {
- var delegateIssues = invalidMod.Dependencies
- .Where(dep => dep.Status != ModDependencyStatus.Valid)
- .Select(dependency => $"{dependency.Info.Name} ({dependency.GetStatusString()})")
- .ToList();
-
- string error = $"Dependency issues in mod {invalidMod.Info.Name} found: {string.Join(", ", delegateIssues)}";
-
- Logger.Log("HAT", LogSeverity.Warning, error);
- }
-
- private void SortModsBasedOnDependencies()
- {
- Mods.Sort((a, b) =>
- {
- if (a.Dependencies.Where(d => d.Instance == b).Any()) return 1;
- if (b.Dependencies.Where(d => d.Instance == a).Any()) return -1;
- return 0;
- });
- }
-
- private void LogLoadedMods()
- {
- int codeModsCount = Mods.Count(mod => mod.IsCodeMod);
- int assetModsCount = Mods.Count(mod => mod.IsAssetMod);
-
+
var modsText = $"{Mods.Count} mod{(Mods.Count != 1 ? "s" : "")}";
var codeModsText = $"{codeModsCount} code mod{(codeModsCount != 1 ? "s" : "")}";
- var assetModsText = $"{assetModsCount} asset mod{(assetModsCount != 1 ? "s" : "")}";
-
+ var assetModsText = $"{assetModCount} asset mod{(assetModCount != 1 ? "s" : "")}";
Logger.Log("HAT", $"Successfully loaded {modsText} ({codeModsText} and {assetModsText})");
-
- Logger.Log("HAT", $"Mods in their order of appearance:");
-
- foreach (var mod in Mods)
- {
- Logger.Log("HAT", $" {mod.Info.Name} by {mod.Info.Author} version {mod.Info.Version}");
- }
}
-
- internal void InitalizeAssemblies()
+
+ public void InitializeAssemblies()
{
foreach (var mod in Mods)
{
- mod.InitializeAssembly();
- }
- foreach (var mod in Mods)
- {
- mod.InitializeComponents();
+ mod.Initialize(_fezGame);
}
- Logger.Log("HAT", "Assembly initialization completed!");
}
- internal List GetFullAssetList()
+ public void InitializeComponents()
{
- var list = new List();
-
foreach (var mod in Mods)
{
- list.AddRange(mod.Assets);
+ mod.InjectComponents();
}
+ }
+
+ public List GetFullAssetList()
+ {
+ return Mods.SelectMany(x => x.GetAssets()).ToList();
+ }
- return list;
+ public static void RegisterRequiredDependencyResolvers()
+ {
+ AssemblyResolverRegistry.Register(new HatSubdirectoryAssemblyResolver("MonoMod"));
+ AssemblyResolverRegistry.Register(new HatSubdirectoryAssemblyResolver("FEZRepacker.Core"));
}
- internal void InitalizeComponents()
+ private static IList InitializeIgnoredModsList()
{
- foreach(var mod in Mods)
- {
- mod.InjectComponents();
- }
- Logger.Log("HAT", "Component initialization completed!");
+ var ignoredModsNamesFilePath = Path.Combine(ModsDirectory, "ignorelist.txt");
+ const string defaultContent =
+ "# List of directories and zip archives to ignore when loading mods, one per line.\n" +
+ "# Lines starting with # will be ignored.\n\n" +
+ "ExampleDirectoryModName\n" +
+ "ExampleZipPackageName.zip\n";
+ return ModsTextListLoader.LoadOrCreateDefault(ignoredModsNamesFilePath, defaultContent);
}
+ private static IList InitializePriorityList()
+ {
+ var priorityListFilePath = Path.Combine(ModsDirectory, "prioritylist.txt");
+ const string defaultContent = "# List of directories and zip archives to prioritize during mod loading.\n" +
+ "# If present on this list, the mod will be loaded before other mods not listed here or listed below it,\n" +
+ "# including newer versions of the same mod. However, it does not override dependency ordering.\n" +
+ "# Lines starting with # will be ignored.\n\n" +
+ "ExampleDirectoryModName\n" +
+ "ExampleZipPackageName.zip\n";
+ return ModsTextListLoader.LoadOrCreateDefault(priorityListFilePath, defaultContent);
+ }
}
-}
+}
\ No newline at end of file
diff --git a/Source/ModDefinition/AssetMod.cs b/Source/ModDefinition/AssetMod.cs
new file mode 100644
index 0000000..0549afd
--- /dev/null
+++ b/Source/ModDefinition/AssetMod.cs
@@ -0,0 +1,46 @@
+using HatModLoader.Source.Assets;
+using HatModLoader.Source.FileProxies;
+
+namespace HatModLoader.Source.ModDefinition
+{
+ public class AssetMod
+ {
+ private const string AssetDirectoryName = "Assets";
+
+ private const string AssetPakName = AssetDirectoryName + ".pak";
+
+ public List Assets { get; }
+
+ private AssetMod(List assets)
+ {
+ Assets = assets;
+ }
+
+ public static bool TryLoad(IFileProxy proxy, Metadata metadata, out AssetMod assetMod)
+ {
+ var files = new Dictionary();
+ foreach (var filePath in proxy.EnumerateFiles(AssetDirectoryName))
+ {
+ var relativePath = filePath.Substring(AssetDirectoryName.Length + 1).Replace("/", "\\").ToLower();
+ var fileStream = proxy.OpenFile(filePath);
+ files.Add(relativePath, fileStream);
+ }
+
+ var assets = AssetLoaderHelper.GetListFromFileDictionary(files);
+ if (proxy.FileExists(AssetPakName))
+ {
+ using var pakPackage = proxy.OpenFile(AssetPakName);
+ assets.AddRange(AssetLoaderHelper.LoadPakPackage(pakPackage));
+ }
+
+ if (assets.Count < 1)
+ {
+ assetMod = null;
+ return false;
+ }
+
+ assetMod = new AssetMod(assets);
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ModDefinition/CodeMod.cs b/Source/ModDefinition/CodeMod.cs
new file mode 100644
index 0000000..60ee85c
--- /dev/null
+++ b/Source/ModDefinition/CodeMod.cs
@@ -0,0 +1,72 @@
+using System.Reflection;
+using FezEngine.Tools;
+using HatModLoader.Source.AssemblyResolving;
+using HatModLoader.Source.FileProxies;
+using Microsoft.Xna.Framework;
+
+namespace HatModLoader.Source.ModDefinition
+{
+ public class CodeMod
+ {
+ public byte[] RawAssembly { get; }
+
+ public Assembly Assembly { get; private set; }
+
+ public List Components { get; private set; }
+
+ private CodeMod(byte[] rawAssembly)
+ {
+ RawAssembly = rawAssembly;
+ }
+
+ public void Initialize(Game game)
+ {
+ if (RawAssembly == null || RawAssembly.Length < 1)
+ {
+ throw new ArgumentNullException(nameof(RawAssembly), "There's not raw assembly data.");
+ }
+
+ if (Assembly != null)
+ {
+ throw new InvalidOperationException("Assembly is already loaded.");
+ }
+
+ Assembly = Assembly.Load(RawAssembly);
+ Components = [];
+
+ foreach (var type in Assembly.GetExportedTypes())
+ {
+ if (typeof(GameComponent).IsAssignableFrom(type) && type.IsPublic && !type.IsAbstract)
+ {
+ // NOTE: The constructor accepting the type (Game) is defined in GameComponent
+ var gameComponent = (GameComponent)Activator.CreateInstance(type, [game]);
+ Components.Add(gameComponent);
+ }
+ }
+ }
+
+ public static bool TryLoad(IFileProxy proxy, Metadata metadata, out CodeMod codeMod)
+ {
+ if (string.IsNullOrEmpty(metadata.LibraryName) ||
+ !metadata.LibraryName.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase) ||
+ !proxy.FileExists(metadata.LibraryName))
+ {
+ codeMod = null;
+ return false;
+ }
+
+ using var assemblyStream = proxy.OpenFile(metadata.LibraryName);
+ var rawAssembly = new byte[assemblyStream.Length];
+ var count = assemblyStream.Read(rawAssembly, 0, rawAssembly.Length);
+
+ if (rawAssembly.Length != count)
+ {
+ codeMod = null;
+ return false;
+ }
+
+ codeMod = new CodeMod(rawAssembly);
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ModDefinition/Metadata.cs b/Source/ModDefinition/Metadata.cs
new file mode 100644
index 0000000..9295b33
--- /dev/null
+++ b/Source/ModDefinition/Metadata.cs
@@ -0,0 +1,89 @@
+using System.Xml.Serialization;
+using Common;
+using HatModLoader.Source.FileProxies;
+
+namespace HatModLoader.Source.ModDefinition
+{
+ [Serializable]
+ public struct Metadata
+ {
+ private const string ModMetadataFile = "Metadata.xml";
+
+ public string Name { get; set; }
+
+ public string Description { get; set; }
+
+ public string Author { get; set; }
+
+ [XmlIgnore] public Version Version { get; set; }
+
+ [XmlElement("Version")]
+ public string VersionString
+ {
+ get => Version.ToString();
+ set
+ {
+ if (!string.IsNullOrEmpty(value) && Version.TryParse(value, out var version))
+ {
+ Version = version;
+ }
+ }
+ }
+
+ public string LibraryName { get; set; }
+
+ public DependencyInfo[] Dependencies { get; set; }
+
+ public static bool TryLoad(IFileProxy proxy, out Metadata metadata)
+ {
+ if (!proxy.FileExists(ModMetadataFile))
+ {
+ metadata = default;
+ return false;
+ }
+
+ try
+ {
+ using var stream = proxy.OpenFile(ModMetadataFile);
+ using var reader = new StreamReader(stream);
+
+ var serializer = new XmlSerializer(typeof(Metadata));
+ metadata = (Metadata)serializer.Deserialize(reader);
+ if (string.IsNullOrEmpty(metadata.Name) || metadata.Version == null)
+ {
+ metadata = default;
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Log("HAT", LogSeverity.Warning, $"Failed to load mod metadata: {ex.Message}");
+ metadata = default;
+ return false;
+ }
+ }
+
+ [Serializable]
+ public struct DependencyInfo
+ {
+ [XmlAttribute] public string Name { get; set; }
+
+ [XmlIgnore] public Version MinimumVersion { get; set; }
+
+ [XmlAttribute("MinimumVersion")]
+ public string MinimumVersionString
+ {
+ get => MinimumVersion.ToString();
+ set
+ {
+ if (!string.IsNullOrEmpty(value) && Version.TryParse(value, out var version))
+ {
+ MinimumVersion = version;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ModDefinition/Mod.cs b/Source/ModDefinition/Mod.cs
deleted file mode 100644
index f862970..0000000
--- a/Source/ModDefinition/Mod.cs
+++ /dev/null
@@ -1,204 +0,0 @@
-using Common;
-using FezEngine.Tools;
-using HatModLoader.Source.Assets;
-using HatModLoader.Source.FileProxies;
-using Microsoft.Xna.Framework;
-using System.Reflection;
-
-namespace HatModLoader.Source.ModDefinition
-{
- public class Mod : IDisposable
- {
- public static readonly string ModsDirectoryName = "Mods";
-
- public static readonly string AssetsDirectoryName = "Assets";
- public static readonly string ModMetadataFileName = "Metadata.xml";
-
- public Hat ModLoader;
-
- public byte[] RawAssembly { get; private set; }
- public Assembly Assembly { get; private set; }
- public ModMetadata Info { get; private set; }
- public IFileProxy FileProxy { get; private set; }
- public List Dependencies { get; private set; }
- public List Assets { get; private set; }
- public List Components { get; private set; }
-
- public bool IsAssetMod => Assets.Count > 0;
- public bool IsCodeMod => RawAssembly != null;
-
- public Mod(Hat modLoader, IFileProxy fileProxy)
- {
- ModLoader = modLoader;
-
- RawAssembly = null;
- Assembly = null;
- Assets = new List();
- Components = new List();
- Dependencies = new List();
- FileProxy = fileProxy;
- }
-
- public void InitializeComponents()
- {
- if (RawAssembly == null || Assembly == null) return;
-
- foreach (Type type in Assembly.GetExportedTypes())
- {
- if (!typeof(GameComponent).IsAssignableFrom(type) || !type.IsPublic || type.IsAbstract) continue;
- //Note: The constructor accepting the type (Game) is defined in GameComponent
- var gameComponent = (GameComponent)Activator.CreateInstance(type, new object[] { ModLoader.Game });
- Components.Add(gameComponent);
- }
-
- if (Components.Count > 0)
- {
- var countText = $"{Components.Count} component{(Components.Count != 1 ? "s" : "")}";
- Logger.Log("HAT", $"Initialized {countText} in mod \"{Info.Name}\"");
- }
- }
-
- public void InjectComponents()
- {
- foreach (var component in Components)
- {
- ServiceHelper.AddComponent(component);
- }
- }
-
- public void InitializeAssembly()
- {
- if (RawAssembly == null) return;
- Assembly = Assembly.Load(RawAssembly);
- }
-
- public void Dispose()
- {
- // TODO: dispose assets
-
- foreach (var component in Components)
- {
- ServiceHelper.RemoveComponent(component);
- }
- }
-
- public int CompareVersionsWith(Mod mod)
- {
- return ModMetadata.CompareVersions(Info.Version, mod.Info.Version);
- }
-
- public void InitializeDependencies()
- {
- if (Info.Dependencies == null || Info.Dependencies.Count() == 0) return;
- if (Dependencies.Count() == Info.Dependencies.Length) return;
-
- Dependencies.Clear();
- foreach (var dependencyInfo in Info.Dependencies)
- {
- var matchingMod = ModLoader.Mods.FirstOrDefault(mod => mod.Info.Name == dependencyInfo.Name);
- var dependency = new ModDependency(dependencyInfo, matchingMod);
- Dependencies.Add(dependency);
- }
- }
-
- public bool TryFinalizeDependencies()
- {
- foreach (var dependency in Dependencies)
- {
- if (dependency.TryFinalize()) continue;
- else return false;
- }
- return true;
- }
-
- public bool AreDependenciesFinalized()
- {
- return Dependencies.All(dependency => dependency.IsFinalized);
- }
-
- public bool AreDependenciesValid()
- {
- if (Info.Dependencies == null) return true; // if mod has no dependencies, they are "valid"
- if (Info.Dependencies.Count() != Dependencies.Count()) return false;
-
- return Dependencies.All(dependency => dependency.Status == ModDependencyStatus.Valid);
- }
-
- public static bool TryLoad(Hat modLoader, IFileProxy fileProxy, out Mod mod)
- {
- mod = new Mod(modLoader, fileProxy);
-
- if (!mod.TryLoadMetadata()) return false;
-
- mod.TryLoadAssets();
- mod.TryLoadAssembly();
-
- return mod.IsAssetMod || mod.IsCodeMod;
- }
-
- public static string GetModsDirectory()
- {
- return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ModsDirectoryName);
- }
-
- private bool TryLoadMetadata()
- {
- if (!FileProxy.FileExists(ModMetadataFileName))
- {
- return false;
- }
-
- using var metadataStream = FileProxy.OpenFile(ModMetadataFileName);
- if (!ModMetadata.TryLoadFrom(metadataStream, out var metadata))
- {
- return false;
- }
-
- Info = metadata;
-
- return true;
- }
-
- private bool TryLoadAssets()
- {
- var files = new Dictionary();
-
- foreach (var filePath in FileProxy.EnumerateFiles(AssetsDirectoryName))
- {
- var relativePath = filePath.Substring(AssetsDirectoryName.Length + 1).Replace("/", "\\").ToLower();
- var fileStream = FileProxy.OpenFile(filePath);
- files.Add(relativePath, fileStream);
- }
-
- Assets = AssetLoaderHelper.GetListFromFileDictionary(files);
-
- var pakPackagePath = AssetsDirectoryName + ".pak";
- if (FileProxy.FileExists(pakPackagePath))
- {
- using var pakPackage = FileProxy.OpenFile(pakPackagePath);
- Assets.AddRange(AssetLoaderHelper.LoadPakPackage(pakPackage));
- }
-
- return Assets.Count > 0;
- }
-
- private bool TryLoadAssembly()
- {
- if(!IsLibraryNameValid()) return false;
-
- if (!FileProxy.FileExists(Info.LibraryName)) return false;
-
- using var assemblyStream = FileProxy.OpenFile(Info.LibraryName);
- RawAssembly = new byte[assemblyStream.Length];
- assemblyStream.Read(RawAssembly, 0, RawAssembly.Length);
-
- return true;
- }
-
- private bool IsLibraryNameValid()
- {
- var libraryName = Info.LibraryName;
- return libraryName != null && libraryName.Length > 0 && libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase);
- }
- }
-}
diff --git a/Source/ModDefinition/ModDependency.cs b/Source/ModDefinition/ModDependency.cs
deleted file mode 100644
index 9e36e47..0000000
--- a/Source/ModDefinition/ModDependency.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using System.Linq;
-
-namespace HatModLoader.Source.ModDefinition
-{
- [Serializable]
- public struct ModDependency
- {
- public ModDependencyInfo Info;
- public Mod Instance;
- public ModDependencyStatus Status;
- public bool IsModLoaderDependency => Info.Name == "HAT";
- public bool IsFinalized => Status != ModDependencyStatus.None;
- public string DetectedVersion => IsModLoaderDependency ? Hat.Version : Instance != null ? Instance.Info.Version : null;
-
-
- public ModDependency(ModDependencyInfo info, Mod instance)
- {
- Info = info;
- Instance = instance;
- Status = ModDependencyStatus.None;
-
- Initialize();
- }
- public void Initialize()
- {
- if (IsModLoaderDependency || Instance != null)
- {
- if (ModMetadata.CompareVersions(DetectedVersion, Info.MinimumVersion) < 0)
- {
- Status = ModDependencyStatus.InvalidVersion;
- }
- else
- {
- Status = ModDependencyStatus.Valid;
- }
- }
-
- if (!IsModLoaderDependency)
- {
- if (Instance == null)
- {
- Status = ModDependencyStatus.InvalidNotFound;
- }
- else if (Instance.AreDependenciesValid())
- {
- Status = ModDependencyStatus.Valid;
- }
-
- if (IsRecursive())
- {
- Status = ModDependencyStatus.InvalidRecursive;
- }
- }
- }
-
- public bool TryFinalize()
- {
- if (IsModLoaderDependency) return true;
-
- if (!Instance.AreDependenciesFinalized()) return false;
-
- Status =
- Instance.AreDependenciesValid()
- ? ModDependencyStatus.Valid
- : ModDependencyStatus.InvalidDependencyTree;
-
- return true;
- }
-
- public bool IsRecursive()
- {
- var currentModQueue = new List() { Instance };
-
- var iterationsCount = Instance.ModLoader.Mods.Count();
-
- while (currentModQueue.Count > 0)
- {
- var newDependencyMods = currentModQueue.SelectMany(mod => mod.Dependencies).Select(dep => dep.Instance).ToList();
- if (newDependencyMods.Contains(Instance))
- {
- return true;
- }
-
- currentModQueue = newDependencyMods;
-
- iterationsCount--;
-
- if (iterationsCount <= 0)
- {
- break;
- }
- }
-
- return false;
- }
-
- public string GetStatusString()
- {
- return Status switch
- {
- ModDependencyStatus.Valid => $"valid",
- ModDependencyStatus.InvalidVersion => $"needs version >={Info.MinimumVersion}, found {DetectedVersion}",
- ModDependencyStatus.InvalidNotFound => $"not found",
- ModDependencyStatus.InvalidRecursive => $"recursive dependency - consider merging mods or separating it into modules",
- ModDependencyStatus.InvalidDependencyTree => $"couldn't load its own dependencies",
- _ => "unknown"
- };
- }
-
- }
-}
diff --git a/Source/ModDefinition/ModDependencyGraph.cs b/Source/ModDefinition/ModDependencyGraph.cs
new file mode 100644
index 0000000..4ace89c
--- /dev/null
+++ b/Source/ModDefinition/ModDependencyGraph.cs
@@ -0,0 +1,60 @@
+namespace HatModLoader.Source.ModDefinition
+{
+ public class ModDependencyGraph
+ {
+ private readonly Dictionary _nodes = new(StringComparer.OrdinalIgnoreCase);
+
+ public void AddNode(ModIdentity mod)
+ {
+ var name = mod.Metadata.Name;
+ if (_nodes.TryGetValue(name, out var existing))
+ {
+ if (mod.Metadata.Version > existing.Mod.Metadata.Version)
+ {
+ _nodes[name] = new Node(mod);
+ }
+ }
+ else
+ {
+ _nodes[name] = new Node(mod);
+ }
+ }
+
+ public bool TryGetNode(string name, out Node node)
+ {
+ return _nodes.TryGetValue(name, out node);
+ }
+
+ public IEnumerable Nodes => _nodes.Values;
+
+ public class Node
+ {
+ public ModIdentity Mod { get; }
+
+ public List Dependencies { get; } = [];
+
+ public ModDependencyStatus Status { get; private set; } = ModDependencyStatus.Valid;
+
+ public string Details { get; private set; }
+
+ public Node(ModIdentity mod)
+ {
+ Mod = mod;
+ }
+
+ public void MarkInvalid(ModDependencyStatus status, string details)
+ {
+ if (Status == ModDependencyStatus.Valid && status != ModDependencyStatus.Valid)
+ {
+ Status = status;
+ Details = details;
+ }
+ }
+
+ public override string ToString()
+ {
+ return Mod.Metadata.Name;
+ }
+ }
+ }
+}
diff --git a/Source/ModDefinition/ModDependencyInfo.cs b/Source/ModDefinition/ModDependencyInfo.cs
deleted file mode 100644
index 66d966c..0000000
--- a/Source/ModDefinition/ModDependencyInfo.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Xml.Serialization;
-
-namespace HatModLoader.Source.ModDefinition
-{
- [Serializable]
- public struct ModDependencyInfo
- {
- [XmlAttribute] public string Name;
- [XmlAttribute] public string MinimumVersion;
- }
-}
diff --git a/Source/ModDefinition/ModDependencyResolver.cs b/Source/ModDefinition/ModDependencyResolver.cs
new file mode 100644
index 0000000..436b94b
--- /dev/null
+++ b/Source/ModDefinition/ModDependencyResolver.cs
@@ -0,0 +1,256 @@
+using HatModLoader.Source.FileProxies;
+
+namespace HatModLoader.Source.ModDefinition
+{
+ public static class ModDependencyResolver
+ {
+ private const string HatDependencyName = "HAT";
+
+ public static ResolverResult Resolve(IList mods, IList priorityList)
+ {
+ var graph = new ModDependencyGraph();
+ var rejected = new List();
+
+ // Validate HAT dependency and build graph nodes
+ foreach (var mod in mods)
+ {
+ var status = ValidateHatDependency(mod, out var details);
+ if (status != ModDependencyStatus.Valid)
+ {
+ var node = new ModDependencyGraph.Node(mod);
+ node.MarkInvalid(status, details);
+ rejected.Add(node);
+ }
+ else
+ {
+ graph.AddNode(mod);
+ }
+ }
+
+ // Build edges and validate dependencies
+ foreach (var node in graph.Nodes)
+ {
+ BuildEdges(graph, node);
+ }
+
+ // Topological sort with cycle detection
+ var loadOrder = TopologicalSort(graph, priorityList);
+
+ // Collect invalid nodes
+ var invalid = rejected
+ .Concat(graph.Nodes.Where(n => n.Status != ModDependencyStatus.Valid))
+ .ToList();
+
+ return new ResolverResult(loadOrder, invalid);
+ }
+
+ private static ModDependencyStatus ValidateHatDependency(ModIdentity mod, out string details)
+ {
+ var deps = mod.Metadata.Dependencies;
+ if (deps == null)
+ {
+ details = "No dependencies declared, HAT dependency required";
+ return ModDependencyStatus.InvalidNotFound;
+ }
+
+ var hatDependency = deps.FirstOrDefault(d => IsHatDependency(d.Name));
+ if (string.IsNullOrEmpty(hatDependency.Name))
+ {
+ details = "HAT dependency not declared";
+ return ModDependencyStatus.InvalidNotFound;
+ }
+
+ if (hatDependency.MinimumVersion != null && Hat.Version < hatDependency.MinimumVersion)
+ {
+ details = $"Requires HAT >={hatDependency.MinimumVersion}, found {Hat.Version}";
+ return ModDependencyStatus.InvalidVersion;
+ }
+
+ details = string.Empty;
+ return ModDependencyStatus.Valid;
+ }
+
+ private static void BuildEdges(ModDependencyGraph graph, ModDependencyGraph.Node node)
+ {
+ var deps = node.Mod.Metadata.Dependencies;
+ if (deps == null)
+ {
+ return;
+ }
+
+ foreach (var dep in deps.Where(d => !IsHatDependency(d.Name)))
+ {
+ if (!graph.TryGetNode(dep.Name, out var depNode))
+ {
+ node.MarkInvalid(ModDependencyStatus.InvalidNotFound, $"Missing dependency '{dep.Name}'");
+ return;
+ }
+
+ if (dep.MinimumVersion != null && depNode.Mod.Metadata.Version < dep.MinimumVersion)
+ {
+ node.MarkInvalid(ModDependencyStatus.InvalidVersion,
+ $"'{dep.Name}' requires >={dep.MinimumVersion}, found {depNode.Mod.Metadata.Version}");
+ return;
+ }
+
+ node.Dependencies.Add(depNode);
+ }
+ }
+
+ private static List TopologicalSort(ModDependencyGraph graph, IList priorityList)
+ {
+ var result = new List();
+ var visited = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var path = new List();
+ var pathSet = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var stack = new Stack