diff --git a/MonkeyLoader/Meta/IMod.cs b/MonkeyLoader/Meta/IMod.cs index 34b0bde..8a92526 100644 --- a/MonkeyLoader/Meta/IMod.cs +++ b/MonkeyLoader/Meta/IMod.cs @@ -11,7 +11,7 @@ namespace MonkeyLoader.Meta /// /// Contains all metadata and references for a mod. /// - public interface IMod : IConfigOwner, ILoadedNuGetPackage, IShutdown, IComparable + public interface IMod : IConfigOwner, ILoadedNuGetPackage, IComparable { /// /// Gets the names of the authors of this mod. @@ -65,10 +65,6 @@ public interface IMod : IConfigOwner, ILoadedNuGetPackage, IShutdown, IComparabl /// public abstract Uri? IconUrl { get; } - /// - /// Gets the unique identifier of this mod. - /// - //public string Id { get; } /// /// Gets whether this mod is a game pack. /// @@ -150,7 +146,7 @@ public interface IMod : IConfigOwner, ILoadedNuGetPackage, IShutdown, IComparabl public bool HasTag(string tag); } - internal interface IModInternal : IMod + internal interface IModInternal : IMod, IShutdown { public bool LoadEarlyMonkeys(); diff --git a/MonkeyLoader/Meta/IShutdown.cs b/MonkeyLoader/Meta/IShutdown.cs index 9782716..3761d84 100644 --- a/MonkeyLoader/Meta/IShutdown.cs +++ b/MonkeyLoader/Meta/IShutdown.cs @@ -6,6 +6,28 @@ namespace MonkeyLoader.Meta { + /// + /// Contains extension methods for collections of instances. + /// + public static class ShutdownEnumerableExtensions + { + /// + /// Calls the method on all elements of the collection, + /// aggregating their success state as an 'all'. + /// + /// The instances to process. + /// true if all instances successfully shut down, false otherwise. + public static bool ShutdownAll(this IEnumerable shutdowns) + { + var success = true; + + foreach (var shutdown in shutdowns) + success &= shutdown.Shutdown(); + + return success; + } + } + /// /// Interface for everything that can be shut down. /// diff --git a/MonkeyLoader/ModEnumerableExtensions.cs b/MonkeyLoader/ModEnumerableExtensions.cs index cb3e97c..6ac86e0 100644 --- a/MonkeyLoader/ModEnumerableExtensions.cs +++ b/MonkeyLoader/ModEnumerableExtensions.cs @@ -29,6 +29,22 @@ public static IEarlyMonkey[] GetEarlyMonkeys(this IEnumerable mods) public static IEarlyMonkey[] GetEarlyMonkeysAscending(this IEnumerable mods) => mods.GetSortedEarlyMonkeys(Monkey.AscendingComparer); + /// + /// Gets the s of the given in topological order. + /// + /// The mod to get the s of. + /// The s of the given in topological order. + public static IEarlyMonkey[] GetEarlyMonkeysAscending(this IMod mod) + => mod.GetSortedEarlyMonkeys(Monkey.AscendingComparer); + + /// + /// Gets the s of the given in reverse-topological order. + /// + /// The mod to get the s of. + /// The s of the given in reverse-topological order. + public static IEarlyMonkey[] GetEarlyMonkeysDescending(this IMod mod) + => mod.GetSortedEarlyMonkeys(Monkey.DescendingComparer); + /// /// Gets the s of all given in reverse-topological order. /// @@ -54,12 +70,42 @@ public static IMonkey[] GetMonkeysAscending(this IEnumerable mods) => mods.GetSortedMonkeys(Monkey.AscendingComparer); /// - /// Gets the s of all given in reverse-topological order. + /// Gets the s of the given in topological order. /// - /// The mods to get the s of. - /// All s of the given in reverse-topological order. - public static IMonkey[] GetMonkeysDescending(this IEnumerable mods) - => mods.GetSortedMonkeys(Monkey.DescendingComparer); + /// The mod to get the s of. + /// The s of the given in topological order. + public static IMonkey[] GetMonkeysAscending(this IMod mod) + => mod.GetSortedMonkeys(Monkey.AscendingComparer); + + /// + /// Gets the s of all given in reverse-topological order. + /// + /// The mods to get the s of. + /// All s of the given in reverse-topological order. + public static IMonkey[] GetMonkeysDescending(this IEnumerable mod) + => mod.GetSortedMonkeys(Monkey.DescendingComparer); + + /// + /// Gets the s of the given in reverse-topological order. + /// + /// The mod to get the s of. + /// The s of the given in reverse-topological order. + public static IMonkey[] GetMonkeysDescending(this IMod mod) + => mod.GetSortedMonkeys(Monkey.DescendingComparer); + + /// + /// Gets the s of the given in the order defined by the given . + /// + /// The mod to get the s of. + /// The defining the order. + /// The s of the given in the defined order. + public static IEarlyMonkey[] GetSortedEarlyMonkeys(this IMod mod, Comparison comparison) + { + var earlyMonkeys = mod.EarlyMonkeys.ToArray(); + Array.Sort(earlyMonkeys, comparison); + + return earlyMonkeys; + } /// /// Gets the s of all given in the order defined by the given . @@ -84,6 +130,29 @@ public static IEarlyMonkey[] GetSortedEarlyMonkeys(this IEnumerable mods, public static IEarlyMonkey[] GetSortedEarlyMonkeys(this IEnumerable mods, IComparer comparer) => mods.GetSortedEarlyMonkeys(comparer.Compare); + /// + /// Gets the s of the given in the order defined by the given . + /// + /// The mod to get the s of. + /// The defining the order. + /// The s of the given in the defined order. + public static IEarlyMonkey[] GetSortedEarlyMonkeys(this IMod mod, IComparer comparer) + => mod.GetSortedEarlyMonkeys(comparer.Compare); + + /// + /// Gets the s of the given in the order defined by the given . + /// + /// The mod to get the s of. + /// The defining the order. + /// The s of the given in the defined order. + public static IMonkey[] GetSortedMonkeys(this IMod mod, Comparison comparison) + { + var monkeys = mod.Monkeys.ToArray(); + Array.Sort(monkeys, comparison); + + return monkeys; + } + /// /// Gets the s of all given in the order defined by the given . /// @@ -93,6 +162,15 @@ public static IEarlyMonkey[] GetSortedEarlyMonkeys(this IEnumerable mods, public static IMonkey[] GetSortedMonkeys(this IEnumerable mods, IComparer comparer) => mods.GetSortedMonkeys(comparer.Compare); + /// + /// Gets the s of the given in the order defined by the given . + /// + /// The mod to get the s of. + /// The defining the order. + /// All s of the given in the defined order. + public static IMonkey[] GetSortedMonkeys(this IMod mod, IComparer comparer) + => mod.GetSortedMonkeys(comparer.Compare); + /// /// Gets the s of all given in the order defined by the given . /// diff --git a/MonkeyLoader/MonkeyLoader.cs b/MonkeyLoader/MonkeyLoader.cs index f84af22..e8d458f 100644 --- a/MonkeyLoader/MonkeyLoader.cs +++ b/MonkeyLoader/MonkeyLoader.cs @@ -24,7 +24,7 @@ namespace MonkeyLoader /// public sealed class MonkeyLoader : IConfigOwner, IShutdown { - private readonly SortedSet _allMods = new(Mod.AscendingComparer); + private readonly SortedDictionary _allMods = new(Mod.AscendingComparer); private LoggingHandler _loggingHandler = MissingLoggingHandler.Instance; /// @@ -45,7 +45,7 @@ public sealed class MonkeyLoader : IConfigOwner, IShutdown /// /// Gets all loaded game pack s in topological order. /// - public IEnumerable GamePacks => _allMods.Where(mod => mod.IsGamePack); + public IEnumerable GamePacks => _allMods.Keys.Where(mod => mod.IsGamePack); /// /// Gets this loader's id. @@ -99,7 +99,7 @@ public LoggingHandler LoggingHandler /// /// Gets all loaded s in topological order. /// - public IEnumerable Mods => _allMods.AsSafeEnumerable(); + public IEnumerable Mods => _allMods.Keys.AsSafeEnumerable(); /// /// Gets the NuGet manager used by this loader. @@ -109,7 +109,7 @@ public LoggingHandler LoggingHandler /// /// Gets all loaded regular s in topological order. /// - public IEnumerable RegularMods => _allMods.Where(mod => !mod.IsGamePack); + public IEnumerable RegularMods => _allMods.Keys.Where(mod => !mod.IsGamePack); /// /// Gets whether this loaders's Shutdown() failed when it was called. @@ -146,7 +146,11 @@ public MonkeyLoader(string configPath = "MonkeyLoader/MonkeyLoader.json", Loggin foreach (var modLocation in Locations.Mods) { modLocation.LoadMod += (mL, path) => TryLoadAndRunMod(path, out _); - modLocation.UnloadMod += (mL, path) => ShutdownMod(path); + modLocation.UnloadMod += (mL, path) => + { + if (TryFindModByLocation(path, out var mod)) + ShutdownMod(mod); + }; } // TODO: do this properly - scan all loaded assemblies? @@ -215,7 +219,7 @@ public void AddMod(IMod mod) { Logger.Debug(() => $"Adding {(mod.IsGamePack ? "game pack" : "regular")} mod: {mod.Title}"); - _allMods.Add((IModInternal)mod); + _allMods.Add(mod, (IModInternal)mod); NuGet.Add(mod); } @@ -329,7 +333,7 @@ public IEnumerable LoadAllMods() public void LoadEarlyMonkeys(IEnumerable mods) { Logger.Trace(() => "Loading early monkeys in this order:"); - Logger.Trace(mods.Cast().Select(mod => new Func(() => mod.Title))); + Logger.Trace(mods.Cast().Select(mod => mod.Title)); foreach (var mod in mods) mod.TryResolveDependencies(); @@ -447,7 +451,7 @@ public NuGetPackageMod LoadMod(string path, bool isGamePack = false) public void LoadMonkeys(IEnumerable mods) { Logger.Trace(() => "Loading monkeys in this order:"); - Logger.Trace(mods.Cast().Select(mod => new Func(() => mod.Title))); + Logger.Trace(mods.Cast().Select(mod => mod.Title)); // TODO: For a FullLoad this shouldn't make a difference since LoadEarlyMonkeys does the same. // However users of the library may add more mods inbetween those phases or even later afterwards. @@ -482,7 +486,7 @@ public void RunEarlyMonkeys(IEnumerable mods) Logger.Info(() => $"Running {earlyMonkeys.Length} early monkeys!"); Logger.Trace(() => "Running early monkeys in this order:"); - Logger.Trace(earlyMonkeys.Select(eM => new Func(() => $"{eM.Mod.Title}/{eM.Name}"))); + Logger.Trace(earlyMonkeys); var sw = Stopwatch.StartNew(); @@ -552,7 +556,7 @@ public void RunMonkeys(IEnumerable mods) Logger.Info(() => $"Running {monkeys.Length} monkeys!"); Logger.Trace(() => "Running monkeys in this order:"); - Logger.Trace(monkeys.Select(m => new Func(() => $"{m.Mod.Title}/{m.Name}"))); + Logger.Trace(monkeys); var sw = Stopwatch.StartNew(); @@ -570,17 +574,21 @@ public void RunMonkeys(IEnumerable mods) public bool Shutdown() { if (ShutdownRan) + { Logger.Warn(() => "This loader's Shutdown() method has already been called!"); - //throw new InvalidOperationException("A loader's Shutdown() method must only be called once!"); + return !ShutdownFailed; + } ShutdownRan = true; - var sw = Stopwatch.StartNew(); - Logger.Info(() => $"Triggering shutdown routine! Saving the loader's config."); + Logger.Warn(() => $"The loader's shutdown routine was triggered! Triggering shutdown for all {_allMods.Count} mods!"); + + ShutdownFailed |= !ShutdownMods(_allMods.Values.ToArray()); + + Logger.Info(() => $"Saving the loader's config!"); try { - Logger.Debug(() => $"Triggering save for the mod loader's config to shut down!"); Config.Save(); } catch (Exception ex) @@ -589,53 +597,77 @@ public bool Shutdown() Logger.Error(() => ex.Format("The mod loader's config threw an exception while saving during shutdown!")); } - Logger.Info(() => $"Triggering shutdown for all {_allMods.Count} mods!"); - - // TODO: Shutdown monkeys separately in reverse launch order first - foreach (var mod in _allMods.Reverse()) - ShutdownFailed |= !mod.Shutdown(); - Logger.Info(() => $"Processed shutdown in {sw.ElapsedMilliseconds}ms!"); return !ShutdownFailed; } - public bool ShutdownMod(string path) + /// + /// Calls the given 's Shutdown method + /// and removes it from this loader's Mods. + /// + /// The mod to shut down. + /// Whether it ran successfully. + public bool ShutdownMod(IMod mod) => ShutdownMod((IModInternal)mod); + + /// + /// Calls the given s' Shutdown methods + /// and removes them from this loader's Mods in reverse topological order. + /// + /// + /// Use if you want to check if any failed. + /// + /// The mods to shut down. + /// When contains invalid items. + public bool ShutdownMods(params IMod[] mods) => ShutdownMods((IEnumerable)mods); + + /// + /// Calls the given s' Shutdown methods + /// and removes them from this loader's Mods in reverse topological order. + /// + /// The mods to shut down. + public bool ShutdownMods(IEnumerable mods) + { + var internalMods = mods.TrySelect(_allMods.TryGetValue).ToArray(); + + if (mods.Count() != internalMods.Length) + throw new InvalidOperationException($"Tried to shut down mods that aren't weren't part of this loader or already removed!"); + + return ShutdownMods(internalMods); + } + + /// + /// Searches all of this loader's loaded Mods to find a single one with the given location. + /// + /// + /// If zero or multiple matching mods are found, will be null. + /// + /// + /// The mod that was found. + /// Whether a single matching mod was found. + public bool TryFindModByLocation(string location, [NotNullWhen(true)] out IMod? mod) { - if (string.IsNullOrWhiteSpace(path)) + mod = null; + + if (string.IsNullOrWhiteSpace(location)) { - Logger.Warn(() => $"Attempted to shutdown mod using invalid path!"); + Logger.Warn(() => $"Attempted to get a mod using an invalid location!"); return false; } - var mods = _allMods.Where(mod => mod.Location?.Equals(path, StringComparison.Ordinal) ?? false).ToArray(); + var mods = _allMods.Values.Where(mod => mod.Location?.Equals(location, StringComparison.Ordinal) ?? false).ToArray(); if (mods.Length == 0) - return true; + return false; if (mods.Length > 1) { - Logger.Error(() => $"Attempted to shutdown multiple mods using path: {path}"); + Logger.Error(() => $"Attempted to get multiple mods using path: {location}"); return false; } - return ShutdownMod(mods[0]); - } - - public bool ShutdownMod(IMod mod) - { - Logger.Debug(() => $"Shutting down mod {mod.Title}"); - - var success = mod.Shutdown(); - _allMods.Remove((IModInternal)mod); - - return success; - } - - public void ShutdownMods(params IMod[] mods) => ShutdownMods((IEnumerable)mods); - - public void ShutdownMods(IEnumerable mods) - { + mod = mods[0]; + return true; } /// @@ -732,6 +764,64 @@ internal void OnAnyConfigChanged(IConfigKeyChangedEventArgs configChangedEvent) } } + private bool ShutdownMod(IModInternal internalMod) + { + var earlyMonkeys = internalMod.GetEarlyMonkeysDescending(); + var monkeys = internalMod.GetMonkeysDescending(); + + Logger.Info(() => $"Shutting down mod {internalMod.Title} with {earlyMonkeys.Length} early and {monkeys.Length} regular monkeys."); + + var success = true; + + ShutdownMonkeys(earlyMonkeys, monkeys); + + success &= internalMod.Shutdown(); + + _allMods.Remove(internalMod); + + return success; + } + + private bool ShutdownMods(IModInternal[] internalMods) + { + var success = true; + Array.Sort(internalMods, Mod.DescendingComparer); + + var earlyMonkeys = internalMods.GetEarlyMonkeysDescending(); + var monkeys = internalMods.GetMonkeysDescending(); + + Logger.Info(() => $"Shutting down {internalMods.Length} mods with {earlyMonkeys.Length} early and {monkeys.Length} regular monkeys."); + + ShutdownMonkeys(earlyMonkeys, monkeys); + + Logger.Trace(() => "Shutting down mods in this order:"); + Logger.Trace(internalMods); + + success &= internalMods.ShutdownAll(); + + foreach (var mod in internalMods) + _allMods.Remove(mod); + + return success; + } + + private bool ShutdownMonkeys(IEarlyMonkey[] earlyMonkeys, IMonkey[] monkeys) + { + var success = true; + + Logger.Trace(() => "Shutting down monkeys in this order:"); + Logger.Trace(monkeys); + + success &= monkeys.ShutdownAll(); + + Logger.Trace(() => "Shutting down early monkeys in this order:"); + Logger.Trace(earlyMonkeys); + + success &= earlyMonkeys.ShutdownAll(); + + return success; + } + private bool TryLoadGamePack(string path, [NotNullWhen(true)] out NuGetPackageMod? gamePack) => TryLoadMod(path, out gamePack, true); diff --git a/MonkeyLoader/Patching/Monkey.cs b/MonkeyLoader/Patching/Monkey.cs index cd5fc3e..306806c 100644 --- a/MonkeyLoader/Patching/Monkey.cs +++ b/MonkeyLoader/Patching/Monkey.cs @@ -86,8 +86,8 @@ protected Monkey() public override sealed bool Run() { ThrowIfRan(); - Ran = true; + Ran = true; Debug(() => "Running OnLoaded!"); try diff --git a/MonkeyLoader/Patching/MonkeyBase.cs b/MonkeyLoader/Patching/MonkeyBase.cs index f148bff..7bf33d4 100644 --- a/MonkeyLoader/Patching/MonkeyBase.cs +++ b/MonkeyLoader/Patching/MonkeyBase.cs @@ -4,6 +4,7 @@ using MonkeyLoader.Meta; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -14,6 +15,11 @@ namespace MonkeyLoader.Patching /// public abstract partial class MonkeyBase : IMonkey { + /// + /// The monkey's runtime type. + /// + protected readonly Type type; + private static readonly Type _monkeyType = typeof(MonkeyBase); private readonly Lazy _featurePatches; private Mod _mod = null!; @@ -42,18 +48,23 @@ public abstract partial class MonkeyBase : IMonkey public Mod Mod { get => _mod; + + [MemberNotNull(nameof(Mod), nameof(_mod))] internal set { - if (value == _mod) - return; + if (value is null) + throw new ArgumentNullException(nameof(value)); _mod = value; Logger = new MonkeyLogger(_mod.Logger, Name); } } + /// + /// By Default: The monkey's type's Name. + /// /// - public abstract string Name { get; } + public virtual string Name => type.Name; /// /// Gets whether this monkey's Run() method has been called. @@ -72,7 +83,7 @@ internal set internal MonkeyBase() { - var type = GetType(); + type = GetType(); AssemblyName = new(type.Assembly.GetName().Name); _featurePatches = new Lazy(() => @@ -105,6 +116,7 @@ public bool Shutdown() throw new InvalidOperationException("A monkey's Shutdown() method must only be called once!"); ShutdownRan = true; + Logger.Debug(() => "Running OnShutdown!"); try { @@ -123,6 +135,12 @@ public bool Shutdown() return !ShutdownFailed; } + /// + /// + /// Format: {Mod.Title}/{Name} + /// + public override string ToString() => $"{Mod.Title}/{Name}"; + internal static MonkeyBase GetInstance(Type type) { // Could do more specific inheriting from Monkey<> check @@ -210,6 +228,12 @@ internal MonkeyBase() : base() /// The producers to log as individual lines if possible. public static void Debug(IEnumerable> messageProducers) => Logger.Debug(messageProducers); + /// + /// Logs events considered to be useful during debugging when more granular information is needed. + /// + /// The messages to log as individual lines if possible. + public static void Debug(IEnumerable messages) => Logger.Debug(messages); + /// /// Logs that one or more functionalities are not working, preventing some from working correctly. /// @@ -228,6 +252,12 @@ internal MonkeyBase() : base() /// The producers to log as individual lines if possible. public static void Error(IEnumerable> messageProducers) => Logger.Error(messageProducers); + /// + /// Logs that one or more functionalities are not working, preventing some from working correctly. + /// + /// The messages to log as individual lines if possible. + public static void Error(IEnumerable messages) => Logger.Error(messages); + /// /// Logs that one or more key functionalities, or the whole system isn't working. /// @@ -246,6 +276,12 @@ internal MonkeyBase() : base() /// The producers to log as individual lines if possible. public static void Fatal(IEnumerable> messageProducers) => Logger.Fatal(messageProducers); + /// + /// Logs that one or more key functionalities, or the whole system isn't working. + /// + /// The messages to log as individual lines if possible. + public static void Fatal(IEnumerable messages) => Logger.Fatal(messages); + /// /// Logs that something happened, which is purely informative and can be ignored during normal use. /// @@ -264,6 +300,12 @@ internal MonkeyBase() : base() /// The producers to log as individual lines if possible. public static void Info(IEnumerable> messageProducers) => Logger.Info(messageProducers); + /// + /// Logs that something happened, which is purely informative and can be ignored during normal use. + /// + /// The messages to log as individual lines if possible. + public static void Info(IEnumerable messages) => Logger.Info(messages); + /// /// Logs step by step execution of code that can be ignored during standard operation, /// but may be useful during extended debugging sessions. @@ -285,6 +327,13 @@ internal MonkeyBase() : base() /// The producers to log as individual lines if possible. public static void Trace(IEnumerable> messageProducers) => Logger.Trace(messageProducers); + /// + /// Logs step by step execution of code that can be ignored during standard operation, + /// but may be useful during extended debugging sessions. + /// + /// The messages to log as individual lines if possible. + public static void Trace(IEnumerable messages) => Logger.Trace(messages); + /// /// Logs that unexpected behavior happened, but work is continuing and the key functionalities are operating as expected. /// @@ -302,5 +351,11 @@ internal MonkeyBase() : base() /// /// The producers to log as individual lines if possible. public static void Warn(IEnumerable> messageProducers) => Logger.Warn(messageProducers); + + /// + /// Logs that unexpected behavior happened, but work is continuing and the key functionalities are operating as expected. + /// + /// The messages to log as individual lines if possible. + public static void Warn(IEnumerable messages) => Logger.Warn(messages); } } \ No newline at end of file