From 3ae72b73f204d8d414f333a9f9a7263318876ca1 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 12:25:26 +0200 Subject: [PATCH 01/21] Add mod and mod metadata --- Modding/Mod.cs | 224 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 Modding/Mod.cs diff --git a/Modding/Mod.cs b/Modding/Mod.cs new file mode 100644 index 0000000..c31c53a --- /dev/null +++ b/Modding/Mod.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; + +using Godot.Serialization; +using Godot.Utility.Extensions; + +using JetBrains.Annotations; + +namespace Godot.Modding +{ + public sealed record Mod + { + public Mod(Metadata metadata) + { + this.Meta = metadata; + this.Assemblies = this.LoadAssemblies(); + this.Data = this.LoadData(); + this.LoadResources(); + } + + public Metadata Meta + { + get; + } + + public IEnumerable Assemblies + { + get; + } + + public XmlNode? Data + { + get; + } + + private IEnumerable LoadAssemblies() + { + string assembliesPath = $"{this.Meta.Directory}/Assemblies"; + + using Directory directory = new(); + return directory.DirExists(assembliesPath) + ? from assemblyPath in directory.GetFiles(assembliesPath, true) + where assemblyPath.EndsWith(".dll") + select Assembly.LoadFile(assemblyPath) + : Enumerable.Empty(); + } + + private XmlNode? LoadData() + { + IEnumerable documents = this.LoadDocuments().ToArray(); + if (!documents.Any()) + { + return null; + } + + XmlDocument data = new(); + data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), data.DocumentElement); + (from document in documents + from node in document.Cast() + where node.NodeType is not XmlNodeType.XmlDeclaration + select node).ForEach(node => data.AppendChild(node)); + return data; + } + + private IEnumerable LoadDocuments() + { + string dataPath = $"{this.Meta.Directory}/Data"; + + using Directory directory = new(); + if (!directory.DirExists(dataPath)) + { + yield break; + } + + foreach (string xmlPath in directory.GetFiles(dataPath, true)) + { + XmlDocument document = new(); + document.Load(xmlPath); + yield return document; + } + } + + private void LoadResources() + { + string resourcesPath = $"{this.Meta.Directory}/Resources"; + + using Directory directory = new(); + if (!directory.DirExists(resourcesPath)) + { + return; + } + + foreach (string resourcePath in from path in directory.GetFiles(resourcesPath, true) + where path.EndsWith(".pck") + select path) + { + if (!ProjectSettings.LoadResourcePack(resourcePath)) + { + throw new ModLoadException(this.Meta.Directory, $"Error loading resource pack at {resourcePath}"); + } + } + } + + public sealed record Metadata + { + [UsedImplicitly] + private Metadata() + { + } + + [Serialize] + public string Directory + { + get; + [UsedImplicitly] + private set; + } = null!; + + [Serialize] + public string Id + { + get; + [UsedImplicitly] + private set; + } = null!; + + [Serialize] + public string Name + { + get; + [UsedImplicitly] + private set; + } = null!; + + [Serialize] + public string Author + { + get; + [UsedImplicitly] + private set; + } = null!; + + public IEnumerable Dependencies + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + public IEnumerable Before + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + public IEnumerable After + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + public IEnumerable Incompatible + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + public static Metadata Load(string directoryPath) + { + try + { + string metadataFilePath = $"{directoryPath}/Mod.xml"; + + using File file = new(); + if (!file.FileExists(metadataFilePath)) + { + throw new ModLoadException(directoryPath, new FileNotFoundException($"Mod metadata file {metadataFilePath} does not exist")); + } + + XmlDocument document = new(); + document.Load(metadataFilePath); + if (document.DocumentElement?.Name is not "Mod") + { + throw new ModLoadException(directoryPath, "Root XML node \"Mod\" for serializing mod metadata does not exist"); + } + + XmlNode directoryNode = document.CreateNode(XmlNodeType.Element, "Directory", null); + directoryNode.InnerText = directoryPath; + document.DocumentElement.AppendChild(directoryNode); + + Metadata metadata = new Serializer().Deserialize(document.DocumentElement)!; + return metadata.IsValid() ? metadata : throw new ModLoadException(directoryPath, "Invalid metadata"); + } + catch (Exception exception) when (exception is not ModLoadException) + { + throw new ModLoadException(directoryPath, exception); + } + } + + private bool IsValid() + { + // Check that the incompatible, load before, and load after lists don't have anything in common or contain the mod's own ID + bool invalidLoadOrder = this.Id.Yield() + .Concat(this.Incompatible) + .Concat(this.Before) + .Concat(this.After) + .Indistinct() + .Any(); + // Check that the dependency and incompatible lists don't have anything in common + bool invalidDependencies = this.Dependencies + .Intersect(this.Incompatible) + .Any(); + return !(invalidLoadOrder || invalidDependencies); + } + } + } +} \ No newline at end of file From d0f3d74d2dfe9a5ee6307fd6754134b7911f53da Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 12:27:26 +0200 Subject: [PATCH 02/21] Add exception class for when mod loading fails --- Modding/ModLoadException.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Modding/ModLoadException.cs diff --git a/Modding/ModLoadException.cs b/Modding/ModLoadException.cs new file mode 100644 index 0000000..f4455ad --- /dev/null +++ b/Modding/ModLoadException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Godot.Modding +{ + public class ModLoadException : Exception + { + public ModLoadException(string directoryPath, string message) : base($"Could not load mod at {directoryPath}: {message}") + { + } + + public ModLoadException(string directoryPath, Exception cause) : base($"Could not load mod at {directoryPath}.{System.Environment.NewLine}{cause}") + { + } + } +} \ No newline at end of file From 9d78719ba7166605a56504b8b348e1851030a1db Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 12:28:01 +0200 Subject: [PATCH 03/21] Add startup attribute for methods in mod assemblies --- Modding/ModStartupAttribute.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Modding/ModStartupAttribute.cs diff --git a/Modding/ModStartupAttribute.cs b/Modding/ModStartupAttribute.cs new file mode 100644 index 0000000..809a059 --- /dev/null +++ b/Modding/ModStartupAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace Godot.Modding +{ + [AttributeUsage(AttributeTargets.Method)] + public class ModStartupAttribute : Attribute + { + public ModStartupAttribute() : this(null) + { + } + + public ModStartupAttribute(params object[]? parameters) + { + this.Parameters = parameters; + } + + public object[]? Parameters + { + get; + } + } +} \ No newline at end of file From b497c0e87a0feeb0099ae5ce774416d48dcb7594 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 12:29:19 +0200 Subject: [PATCH 04/21] Add mod loader class and utility methods --- Modding/ModLoader.cs | 123 +++++++++++++++++++++ Utility/Extensions/DirectoryExtensions.cs | 66 +++++++++++ Utility/Extensions/EnumerableExtensions.cs | 54 +++++++++ 3 files changed, 243 insertions(+) create mode 100644 Modding/ModLoader.cs create mode 100644 Utility/Extensions/DirectoryExtensions.cs create mode 100644 Utility/Extensions/EnumerableExtensions.cs diff --git a/Modding/ModLoader.cs b/Modding/ModLoader.cs new file mode 100644 index 0000000..b6abce8 --- /dev/null +++ b/Modding/ModLoader.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using Godot.Utility.Extensions; + +namespace Godot.Modding +{ + public static class ModLoader + { + private static readonly Dictionary loadedMods = new(); + + public static IReadOnlyDictionary LoadedMods + { + get + { + return ModLoader.loadedMods; + } + } + + public static Mod LoadMod(string modDirectoryPath) + { + Mod mod = new(Mod.Metadata.Load(modDirectoryPath)); + ModLoader.loadedMods.Add(mod.Meta.Id, mod); + ModLoader.StartupMods(mod.Yield()); + return mod; + } + + public static IEnumerable LoadMods(IEnumerable modDirectoryPaths) + { + List mods = (from metadata in ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths))) + select new Mod(metadata)).ToList(); + mods.ForEach(mod => ModLoader.loadedMods.Add(mod.Meta.Id, mod)); + ModLoader.StartupMods(mods); + return mods; + } + + private static void StartupMods(IEnumerable mods) + { + // Invoke all static methods annotated with [Startup] along with the supplied parameters (if any) + foreach ((MethodInfo method, ModStartupAttribute attribute) in from mod in mods + from assembly in mod.Assemblies + from type in assembly.GetTypes() + from method in type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public) + let attribute = method.GetCustomAttribute() + where attribute is not null + select (method, attribute)) + { + method.Invoke(null, attribute.Parameters); + } + } + + private static Dictionary LoadModMetadata(IEnumerable modDirectories) + { + Dictionary loadedMetadata = new(); + foreach (string modDirectory in modDirectories) + { + Mod.Metadata metadata = Mod.Metadata.Load(modDirectory); + + // Fail if the metadata is incompatible with any of the loaded metadata (and vice-versa), or if the ID already exists + IEnumerable incompatibleMetadata = (from id in metadata.Incompatible + select loadedMetadata.GetValueOrDefault(id)) + .NotNull() + .Concat(from loaded in loadedMetadata.Values + where loaded.Incompatible.Contains(metadata.Id) + select loaded); + if (incompatibleMetadata.Any()) + { + Log.Error(new ModLoadException(metadata.Directory, "Incompatible with other loaded mods")); + } + else if (!loadedMetadata.TryAdd(metadata.Id, metadata)) + { + Log.Error(new ModLoadException(metadata.Directory, "Duplicate ID")); + } + } + return loadedMetadata; + } + + private static Dictionary FilterModMetadata(Dictionary loadedMetadata) + { + // If the dependencies of any metadata have not been loaded, remove that metadata and try again + foreach (Mod.Metadata metadata in from metadata in loadedMetadata.Values + where (from dependency in metadata.Dependencies + select loadedMetadata.TryGetValue(dependency, out _)).Any(dependency => !dependency) + select metadata) + { + Log.Error(new ModLoadException(metadata.Directory, "Not all dependencies are loaded")); + loadedMetadata.Remove(metadata.Id); + return ModLoader.FilterModMetadata(loadedMetadata); + } + return loadedMetadata; + } + + private static IEnumerable SortModMetadata(Dictionary filteredMetadata) + { + // Create a graph of each metadata ID and the IDs of those that need to be loaded after it + Dictionary> dependencyGraph = new(); + foreach (Mod.Metadata metadata in filteredMetadata.Values) + { + dependencyGraph.TryAdd(metadata.Id, new()); + metadata.After.ForEach(after => dependencyGraph[metadata.Id].Add(after)); + foreach (string before in metadata.Before) + { + dependencyGraph.TryAdd(before, new()); + dependencyGraph[before].Add(metadata.Id); + } + } + + // Topologically sort the dependency graph, removing cyclic dependencies if any + IEnumerable? sortedMetadataIds = dependencyGraph.Keys + .TopologicalSort(id => dependencyGraph.GetValueOrDefault(id) ?? Enumerable.Empty(), cyclic => + { + Log.Error(new ModLoadException(filteredMetadata[cyclic].Directory, "Cyclic dependencies with other mod(s)")); + filteredMetadata.Remove(cyclic); + }); + + // If there is no valid topological sorting (cyclic dependencies detected), remove the cyclic metadata and try again + return sortedMetadataIds? + .Select(filteredMetadata.GetValueOrDefault) + .NotNull() ?? ModLoader.SortModMetadata(ModLoader.FilterModMetadata(filteredMetadata)); + } + } +} \ No newline at end of file diff --git a/Utility/Extensions/DirectoryExtensions.cs b/Utility/Extensions/DirectoryExtensions.cs new file mode 100644 index 0000000..b7e7837 --- /dev/null +++ b/Utility/Extensions/DirectoryExtensions.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Godot.Utility.Extensions +{ + public static class DirectoryExtensions + { + public static IEnumerable GetFiles(this Directory directory, bool recursive = false) + { + return recursive + ? directory.GetDirectories(true) + .SelectMany(path => + { + Directory recursiveDirectory = new(); + recursiveDirectory.Open(path); + return recursiveDirectory.GetElementsNonRecursive(true); + }) + .Concat(directory.GetElementsNonRecursive(true)) + : directory.GetElementsNonRecursive(true); + } + + public static IEnumerable GetFiles(this Directory directory, string directoryPath, bool recursive = false) + { + directory.Open(directoryPath); + return directory.GetFiles(recursive); + } + + public static IEnumerable GetDirectories(this Directory directory, bool recursive = false) + { + return recursive + ? directory.GetElementsNonRecursive(false) + .SelectMany(path => + { + Directory recursiveDirectory = new(); + recursiveDirectory.Open(path); + return recursiveDirectory.GetDirectories(true).Prepend(path); + }) + : directory.GetElementsNonRecursive(false); + } + + public static IEnumerable GetDirectories(this Directory directory, string directoryPath, bool recursive = false) + { + directory.Open(directoryPath); + return directory.GetDirectories(recursive); + } + + private static IEnumerable GetElementsNonRecursive(this Directory directory, bool trueIfFiles) + { + directory.ListDirBegin(true); + while (true) + { + string next = directory.GetNext(); + if (next is "") + { + yield break; + } + if (directory.CurrentIsDir() == trueIfFiles) + { + continue; + } + string current = directory.GetCurrentDir(); + yield return current.EndsWith('/') ? $"{current}{next}" : $"{current}/{next}"; + } + } + } +} \ No newline at end of file diff --git a/Utility/Extensions/EnumerableExtensions.cs b/Utility/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..6194c06 --- /dev/null +++ b/Utility/Extensions/EnumerableExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Godot.Utility.Extensions +{ + public static class EnumerableExtensions + { + public static IEnumerable? TopologicalSort(this IEnumerable source, Func> dependencies, Action? cyclic = null) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + if (dependencies is null) + { + throw new ArgumentNullException(nameof(dependencies)); + } + + List sorted = new(); + Dictionary states = new(); + + bool allValid = source + .Select(VisitDependencies) + .All(boolean => boolean); + return allValid ? sorted : null; + + bool VisitDependencies(T t) + { + states.TryAdd(t, false); + switch (states[t]) + { + case true: + return true; + case false: + states[t] = null; + bool dependenciesValid = dependencies.Invoke(t) + .Select(VisitDependencies) + .All(boolean => boolean); + if (!dependenciesValid) + { + return false; + } + states[t] = true; + sorted.Add(t); + return true; + case null: + cyclic?.Invoke(t); + return false; + } + } + } + } +} \ No newline at end of file From b0b789cd51897b5a3b37816cad00499797b39f93 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 12:50:57 +0200 Subject: [PATCH 05/21] Add documentation comments --- Modding/Mod.cs | 49 ++++++++++++++++++++++++++++++++++ Modding/ModLoadException.cs | 13 +++++++++ Modding/ModLoader.cs | 18 +++++++++++++ Modding/ModStartupAttribute.cs | 13 +++++++++ 4 files changed, 93 insertions(+) diff --git a/Modding/Mod.cs b/Modding/Mod.cs index c31c53a..a4343b0 100644 --- a/Modding/Mod.cs +++ b/Modding/Mod.cs @@ -12,8 +12,15 @@ namespace Godot.Modding { + /// + /// Represents a modular component loaded at runtime, with its own assemblies, resource packs, and data. + /// public sealed record Mod { + /// + /// Initializes a new using . + /// + /// The to use. Assemblies, resource packs, and data are all loaded according to the directory specified in the metadata. public Mod(Metadata metadata) { this.Meta = metadata; @@ -22,16 +29,25 @@ public Mod(Metadata metadata) this.LoadResources(); } + /// + /// The metadata of the , such as its ID, name, load order, etc. + /// public Metadata Meta { get; } + /// + /// The assemblies of the . + /// public IEnumerable Assemblies { get; } + /// + /// The XML data of the , combined into a single as its children. + /// public XmlNode? Data { get; @@ -105,6 +121,9 @@ where path.EndsWith(".pck") } } + /// + /// Represents the metadata of a , such as its unique ID, name, author, load order, etc. + /// public sealed record Metadata { [UsedImplicitly] @@ -112,6 +131,9 @@ private Metadata() { } + /// + /// The directory where the was loaded from. + /// [Serialize] public string Directory { @@ -120,6 +142,9 @@ public string Directory private set; } = null!; + /// + /// The unique ID of the . + /// [Serialize] public string Id { @@ -128,6 +153,9 @@ public string Id private set; } = null!; + /// + /// The name of the . + /// [Serialize] public string Name { @@ -136,6 +164,9 @@ public string Name private set; } = null!; + /// + /// The individual or group that created the . + /// [Serialize] public string Author { @@ -144,6 +175,9 @@ public string Author private set; } = null!; + /// + /// The unique IDs of all other s that the depends on. + /// public IEnumerable Dependencies { get; @@ -151,6 +185,9 @@ public IEnumerable Dependencies private set; } = Enumerable.Empty(); + /// + /// The unique IDs of all other s that should be loaded before the . + /// public IEnumerable Before { get; @@ -158,6 +195,9 @@ public IEnumerable Before private set; } = Enumerable.Empty(); + /// + /// The unique IDs of all other s that should be loaded after the . + /// public IEnumerable After { get; @@ -165,6 +205,9 @@ public IEnumerable After private set; } = Enumerable.Empty(); + /// + /// The unique IDs of all other s that are incompatible with the . + /// public IEnumerable Incompatible { get; @@ -172,6 +215,12 @@ public IEnumerable Incompatible private set; } = Enumerable.Empty(); + /// + /// Loads a from . + /// + /// The directory path. It must contain a "Mod.xml" file inside it with valid metadata. + /// A loaded from . + /// Thrown if the metadata file does not exist, or the metadata is invalid, or if there is another unexpected issue while trying to load the metadata. public static Metadata Load(string directoryPath) { try diff --git a/Modding/ModLoadException.cs b/Modding/ModLoadException.cs index f4455ad..40dcbb7 100644 --- a/Modding/ModLoadException.cs +++ b/Modding/ModLoadException.cs @@ -2,12 +2,25 @@ namespace Godot.Modding { + /// + /// The exception thrown when an error occurs while loading a . + /// public class ModLoadException : Exception { + /// + /// Initializes a new with the specified arguments. + /// + /// The directory path from where an attempt was made to load the . + /// A brief description of the issue. public ModLoadException(string directoryPath, string message) : base($"Could not load mod at {directoryPath}: {message}") { } + /// + /// Initializes a new with the specified arguments. + /// + /// The directory path from where an attempt was made to load the . + /// The that caused the loading to fail. public ModLoadException(string directoryPath, Exception cause) : base($"Could not load mod at {directoryPath}.{System.Environment.NewLine}{cause}") { } diff --git a/Modding/ModLoader.cs b/Modding/ModLoader.cs index b6abce8..1da6744 100644 --- a/Modding/ModLoader.cs +++ b/Modding/ModLoader.cs @@ -6,10 +6,16 @@ namespace Godot.Modding { + /// + /// Provides methods and properties for loading s at runtime, obtaining all loaded s, and finding a loaded by its ID. + /// public static class ModLoader { private static readonly Dictionary loadedMods = new(); + /// + /// All the s that have been loaded at runtime. + /// public static IReadOnlyDictionary LoadedMods { get @@ -18,6 +24,12 @@ public static IReadOnlyDictionary LoadedMods } } + /// + /// Loads a from and runs all methods marked with in its assemblies (if any). + /// + /// The directory path containing the 's metadata, assemblies, data, and resource packs. + /// The loaded from . + /// This method only loads a individually, and does not check whether it has been loaded with all dependencies and in the correct load order. To load multiple s in a safe and orderly manner, should be used. public static Mod LoadMod(string modDirectoryPath) { Mod mod = new(Mod.Metadata.Load(modDirectoryPath)); @@ -26,6 +38,12 @@ public static Mod LoadMod(string modDirectoryPath) return mod; } + /// + /// Loads s from and runs all methods marked with in their assemblies (if any). + /// + /// The directory paths to load the s from, containing each 's metadata, assemblies, data, and resource packs. + /// An of the loaded s in the correct load order. s that could not be loaded due to issues will not be contained in the sequence. + /// This method loads multiple s after sorting them according to the load order specified in their metadata. To load a individually without regard to its dependencies and load order, should be used. public static IEnumerable LoadMods(IEnumerable modDirectoryPaths) { List mods = (from metadata in ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths))) diff --git a/Modding/ModStartupAttribute.cs b/Modding/ModStartupAttribute.cs index 809a059..b8c1d93 100644 --- a/Modding/ModStartupAttribute.cs +++ b/Modding/ModStartupAttribute.cs @@ -2,18 +2,31 @@ namespace Godot.Modding { + /// + /// Indicates that the marked method is to be invoked after the loading of the assemblies in which it is contained. + /// [AttributeUsage(AttributeTargets.Method)] public class ModStartupAttribute : Attribute { + /// + /// Initializes a new . + /// public ModStartupAttribute() : this(null) { } + /// + /// Initializes a new with the specified arguments. + /// + /// The parameters to supply to the marked method when invoking it. public ModStartupAttribute(params object[]? parameters) { this.Parameters = parameters; } + /// + /// The parameters that are supplied to the marked method when invoking it. + /// public object[]? Parameters { get; From 9d4e3851513515baf83c712c0c097deee922db02 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 13:00:08 +0200 Subject: [PATCH 06/21] Refactor mod loading to use C# filesystem API instead of Godot's --- Modding/Mod.cs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Modding/Mod.cs b/Modding/Mod.cs index a4343b0..e499a1a 100644 --- a/Modding/Mod.cs +++ b/Modding/Mod.cs @@ -55,12 +55,10 @@ public XmlNode? Data private IEnumerable LoadAssemblies() { - string assembliesPath = $"{this.Meta.Directory}/Assemblies"; + string assembliesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Assemblies"; - using Directory directory = new(); - return directory.DirExists(assembliesPath) - ? from assemblyPath in directory.GetFiles(assembliesPath, true) - where assemblyPath.EndsWith(".dll") + return System.IO.Directory.Exists(assembliesPath) + ? from assemblyPath in System.IO.Directory.GetFiles(assembliesPath, "*.dll", SearchOption.AllDirectories) select Assembly.LoadFile(assemblyPath) : Enumerable.Empty(); } @@ -84,15 +82,14 @@ where node.NodeType is not XmlNodeType.XmlDeclaration private IEnumerable LoadDocuments() { - string dataPath = $"{this.Meta.Directory}/Data"; + string dataPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Data"; - using Directory directory = new(); - if (!directory.DirExists(dataPath)) + if (!System.IO.Directory.Exists(dataPath)) { yield break; } - foreach (string xmlPath in directory.GetFiles(dataPath, true)) + foreach (string xmlPath in System.IO.Directory.GetFiles(dataPath, "*.xml", SearchOption.AllDirectories)) { XmlDocument document = new(); document.Load(xmlPath); @@ -102,17 +99,14 @@ private IEnumerable LoadDocuments() private void LoadResources() { - string resourcesPath = $"{this.Meta.Directory}/Resources"; + string resourcesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Resources"; - using Directory directory = new(); - if (!directory.DirExists(resourcesPath)) + if (!System.IO.Directory.Exists(resourcesPath)) { return; } - foreach (string resourcePath in from path in directory.GetFiles(resourcesPath, true) - where path.EndsWith(".pck") - select path) + foreach (string resourcePath in System.IO.Directory.GetFiles(resourcesPath, "*.pck", SearchOption.AllDirectories)) { if (!ProjectSettings.LoadResourcePack(resourcePath)) { @@ -225,10 +219,9 @@ public static Metadata Load(string directoryPath) { try { - string metadataFilePath = $"{directoryPath}/Mod.xml"; + string metadataFilePath = $"{directoryPath}{System.IO.Path.DirectorySeparatorChar}Mod.xml"; - using File file = new(); - if (!file.FileExists(metadataFilePath)) + if (!System.IO.File.Exists(metadataFilePath)) { throw new ModLoadException(directoryPath, new FileNotFoundException($"Mod metadata file {metadataFilePath} does not exist")); } From 576bba4109542775220f51f6ee6de7dc4e67a832 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 14:22:06 +0200 Subject: [PATCH 07/21] Add documentation comments --- Utility/Extensions/EnumerableExtensions.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Utility/Extensions/EnumerableExtensions.cs b/Utility/Extensions/EnumerableExtensions.cs index 6194c06..1ae441c 100644 --- a/Utility/Extensions/EnumerableExtensions.cs +++ b/Utility/Extensions/EnumerableExtensions.cs @@ -4,8 +4,20 @@ namespace Godot.Utility.Extensions { + /// + /// Contains extension methods for . + /// public static class EnumerableExtensions { + /// + /// Topologically sorts the given sequence of elements. + /// + /// The to sort. + /// A that returns an of dependencies for each element in . + /// An optional that is invoked if a cyclic dependency is found while sorting. + /// The of elements in . + /// An of elements from sorted topologically, or if no valid topological sorting exists. + /// Thrown if either or is . public static IEnumerable? TopologicalSort(this IEnumerable source, Func> dependencies, Action? cyclic = null) { if (source is null) From 0c0dbc6be53b63a883ebf6898d680a26e3267005 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 16:00:18 +0200 Subject: [PATCH 08/21] Remove unused classes and namespaces --- Modding/Mod.cs | 1 - Utility/Extensions/DirectoryExtensions.cs | 66 ----------------------- 2 files changed, 67 deletions(-) delete mode 100644 Utility/Extensions/DirectoryExtensions.cs diff --git a/Modding/Mod.cs b/Modding/Mod.cs index e499a1a..b941c47 100644 --- a/Modding/Mod.cs +++ b/Modding/Mod.cs @@ -6,7 +6,6 @@ using System.Xml; using Godot.Serialization; -using Godot.Utility.Extensions; using JetBrains.Annotations; diff --git a/Utility/Extensions/DirectoryExtensions.cs b/Utility/Extensions/DirectoryExtensions.cs deleted file mode 100644 index b7e7837..0000000 --- a/Utility/Extensions/DirectoryExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Godot.Utility.Extensions -{ - public static class DirectoryExtensions - { - public static IEnumerable GetFiles(this Directory directory, bool recursive = false) - { - return recursive - ? directory.GetDirectories(true) - .SelectMany(path => - { - Directory recursiveDirectory = new(); - recursiveDirectory.Open(path); - return recursiveDirectory.GetElementsNonRecursive(true); - }) - .Concat(directory.GetElementsNonRecursive(true)) - : directory.GetElementsNonRecursive(true); - } - - public static IEnumerable GetFiles(this Directory directory, string directoryPath, bool recursive = false) - { - directory.Open(directoryPath); - return directory.GetFiles(recursive); - } - - public static IEnumerable GetDirectories(this Directory directory, bool recursive = false) - { - return recursive - ? directory.GetElementsNonRecursive(false) - .SelectMany(path => - { - Directory recursiveDirectory = new(); - recursiveDirectory.Open(path); - return recursiveDirectory.GetDirectories(true).Prepend(path); - }) - : directory.GetElementsNonRecursive(false); - } - - public static IEnumerable GetDirectories(this Directory directory, string directoryPath, bool recursive = false) - { - directory.Open(directoryPath); - return directory.GetDirectories(recursive); - } - - private static IEnumerable GetElementsNonRecursive(this Directory directory, bool trueIfFiles) - { - directory.ListDirBegin(true); - while (true) - { - string next = directory.GetNext(); - if (next is "") - { - yield break; - } - if (directory.CurrentIsDir() == trueIfFiles) - { - continue; - } - string current = directory.GetCurrentDir(); - yield return current.EndsWith('/') ? $"{current}{next}" : $"{current}/{next}"; - } - } - } -} \ No newline at end of file From e87ec5ad2a482423efa0e0d3a8ca9a5d3c678737 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 16:05:31 +0200 Subject: [PATCH 09/21] Add csproj file --- Modot.csproj | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Modot.csproj diff --git a/Modot.csproj b/Modot.csproj new file mode 100644 index 0000000..9e72023 --- /dev/null +++ b/Modot.csproj @@ -0,0 +1,29 @@ + + + default + enable + netstandard2.1 + Godot + + true + + + C:\Users\Indraneel\Projects\Modot\.mono\temp\bin\Debug\Modot.xml + + + C:\Users\Indraneel\Projects\Modot\.mono\temp\bin\ExportDebug\Modot.xml + + + C:\Users\Indraneel\Projects\Modot\.mono\temp\bin\ExportRelease\Modot.xml + + + + + + + + + + + + \ No newline at end of file From d3f301b917e786a02087cf8fefc38a2ec6fcc1ce Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 16:06:05 +0200 Subject: [PATCH 10/21] Refactor folder structure --- Modding/Mod.cs => Mod.cs | 0 Modding/ModLoadException.cs => ModLoadException.cs | 0 Modding/ModLoader.cs => ModLoader.cs | 0 Modding/ModStartupAttribute.cs => ModStartupAttribute.cs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Modding/Mod.cs => Mod.cs (100%) rename Modding/ModLoadException.cs => ModLoadException.cs (100%) rename Modding/ModLoader.cs => ModLoader.cs (100%) rename Modding/ModStartupAttribute.cs => ModStartupAttribute.cs (100%) diff --git a/Modding/Mod.cs b/Mod.cs similarity index 100% rename from Modding/Mod.cs rename to Mod.cs diff --git a/Modding/ModLoadException.cs b/ModLoadException.cs similarity index 100% rename from Modding/ModLoadException.cs rename to ModLoadException.cs diff --git a/Modding/ModLoader.cs b/ModLoader.cs similarity index 100% rename from Modding/ModLoader.cs rename to ModLoader.cs diff --git a/Modding/ModStartupAttribute.cs b/ModStartupAttribute.cs similarity index 100% rename from Modding/ModStartupAttribute.cs rename to ModStartupAttribute.cs From 5963e47212b5009e53869dabfb136bad2bce4202 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 16:06:40 +0200 Subject: [PATCH 11/21] Refactor namespaces to match folder structure --- ModLoader.cs | 2 +- Modot.csproj | 2 +- Utility/Extensions/EnumerableExtensions.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ModLoader.cs b/ModLoader.cs index 1da6744..2443874 100644 --- a/ModLoader.cs +++ b/ModLoader.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Reflection; -using Godot.Utility.Extensions; +using Godot.Modding.Utility.Extensions; namespace Godot.Modding { diff --git a/Modot.csproj b/Modot.csproj index 9e72023..db22c6f 100644 --- a/Modot.csproj +++ b/Modot.csproj @@ -3,7 +3,7 @@ default enable netstandard2.1 - Godot + Godot.Modding true diff --git a/Utility/Extensions/EnumerableExtensions.cs b/Utility/Extensions/EnumerableExtensions.cs index 1ae441c..f7f137c 100644 --- a/Utility/Extensions/EnumerableExtensions.cs +++ b/Utility/Extensions/EnumerableExtensions.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Godot.Utility.Extensions +namespace Godot.Modding.Utility.Extensions { /// /// Contains extension methods for . From 4f4edfed409cf363d7240f6d4839298fb4b6aee8 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 19:19:00 +0200 Subject: [PATCH 12/21] Refactor try/catch statement --- Mod.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Mod.cs b/Mod.cs index b941c47..97c4ba5 100644 --- a/Mod.cs +++ b/Mod.cs @@ -216,15 +216,15 @@ public IEnumerable Incompatible /// Thrown if the metadata file does not exist, or the metadata is invalid, or if there is another unexpected issue while trying to load the metadata. public static Metadata Load(string directoryPath) { + string metadataFilePath = $"{directoryPath}{System.IO.Path.DirectorySeparatorChar}Mod.xml"; + + if (!System.IO.File.Exists(metadataFilePath)) + { + throw new ModLoadException(directoryPath, new FileNotFoundException($"Mod metadata file {metadataFilePath} does not exist")); + } + try { - string metadataFilePath = $"{directoryPath}{System.IO.Path.DirectorySeparatorChar}Mod.xml"; - - if (!System.IO.File.Exists(metadataFilePath)) - { - throw new ModLoadException(directoryPath, new FileNotFoundException($"Mod metadata file {metadataFilePath} does not exist")); - } - XmlDocument document = new(); document.Load(metadataFilePath); if (document.DocumentElement?.Name is not "Mod") From 6932597e089453a34d257133cd70a598b873c2ad Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 15 Jun 2022 20:32:02 +0200 Subject: [PATCH 13/21] Add public API attributes --- Mod.cs | 2 ++ ModLoader.cs | 3 +++ ModStartupAttribute.cs | 3 +++ 3 files changed, 8 insertions(+) diff --git a/Mod.cs b/Mod.cs index 97c4ba5..f82b62c 100644 --- a/Mod.cs +++ b/Mod.cs @@ -14,6 +14,7 @@ namespace Godot.Modding /// /// Represents a modular component loaded at runtime, with its own assemblies, resource packs, and data. /// + [PublicAPI] public sealed record Mod { /// @@ -117,6 +118,7 @@ private void LoadResources() /// /// Represents the metadata of a , such as its unique ID, name, author, load order, etc. /// + [PublicAPI] public sealed record Metadata { [UsedImplicitly] diff --git a/ModLoader.cs b/ModLoader.cs index 2443874..f48930f 100644 --- a/ModLoader.cs +++ b/ModLoader.cs @@ -4,11 +4,14 @@ using Godot.Modding.Utility.Extensions; +using JetBrains.Annotations; + namespace Godot.Modding { /// /// Provides methods and properties for loading s at runtime, obtaining all loaded s, and finding a loaded by its ID. /// + [PublicAPI] public static class ModLoader { private static readonly Dictionary loadedMods = new(); diff --git a/ModStartupAttribute.cs b/ModStartupAttribute.cs index b8c1d93..402b127 100644 --- a/ModStartupAttribute.cs +++ b/ModStartupAttribute.cs @@ -1,10 +1,13 @@ using System; +using JetBrains.Annotations; + namespace Godot.Modding { /// /// Indicates that the marked method is to be invoked after the loading of the assemblies in which it is contained. /// + [PublicAPI] [AttributeUsage(AttributeTargets.Method)] public class ModStartupAttribute : Attribute { From 68162a4b4eeec39c46978acb657eb7f528c3a53a Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 17 Jun 2022 14:39:15 +0200 Subject: [PATCH 14/21] Tweak namespace arrangement --- Mod.cs | 4 ++-- ModLoader.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mod.cs b/Mod.cs index f82b62c..0bbc95d 100644 --- a/Mod.cs +++ b/Mod.cs @@ -5,10 +5,10 @@ using System.Reflection; using System.Xml; -using Godot.Serialization; - using JetBrains.Annotations; +using Godot.Serialization; + namespace Godot.Modding { /// diff --git a/ModLoader.cs b/ModLoader.cs index f48930f..8edfaf8 100644 --- a/ModLoader.cs +++ b/ModLoader.cs @@ -2,10 +2,10 @@ using System.Linq; using System.Reflection; -using Godot.Modding.Utility.Extensions; - using JetBrains.Annotations; +using Godot.Modding.Utility.Extensions; + namespace Godot.Modding { /// From 3358d3dd9e168a82c5ea42653e37613cb4472156 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 17 Jun 2022 14:39:30 +0200 Subject: [PATCH 15/21] Add extension methods for Godot.Directory --- Utility/Extensions/DirectoryExtensions.cs | 141 ++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 Utility/Extensions/DirectoryExtensions.cs diff --git a/Utility/Extensions/DirectoryExtensions.cs b/Utility/Extensions/DirectoryExtensions.cs new file mode 100644 index 0000000..90c9ccf --- /dev/null +++ b/Utility/Extensions/DirectoryExtensions.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +using JetBrains.Annotations; + +namespace Godot.Modding.Utility.Extensions +{ + /// + /// Contains extension methods for . + /// + [PublicAPI] + public static class DirectoryExtensions + { + /// + /// Copies all files from the directory at to the directory at . + /// + /// The to use when copying files. + /// The source directory path. It can be an absolute path, or relative to . + /// The destination directory path. It can be an absolute path, or relative to . + /// Whether the contents should be copied recursively (i.e. copy files inside subdirectories and so on) or not. + public static void CopyContents(this Directory directory, string from, string to, bool recursive = false) + { + directory.Open(from); + + // Create destination directory if it doesn't already exist + directory.MakeDirRecursive(to); + + // Regex is used to replace only the first instance of the destination directory in file and subdirectory paths (string.Replace() replaces all instances) + Regex fromReplacement = new(Regex.Escape(from)); + + // Copy all files inside the source directory non-recursively + foreach (string fromFile in directory.GetFiles()) + { + string toFile = fromReplacement.Replace(fromFile, to, 1); + directory.Copy(fromFile, toFile); + } + + if (!recursive) + { + return; + } + + // Copy all files recursively + foreach (string fromSubDirectory in directory.GetDirectories(true)) + { + string toSubDirectory = fromReplacement.Replace(fromSubDirectory, to, 1); + directory.MakeDirRecursive(toSubDirectory); + + using Directory innerDirectory = new(); + innerDirectory.Open(fromSubDirectory); + foreach (string fromFile in innerDirectory.GetFiles()) + { + string toFile = fromReplacement.Replace(fromFile, to, 1); + directory.Copy(fromFile, toFile); + } + } + } + + /// + /// Returns the complete file paths of all files inside . + /// + /// The to search in. + /// Whether the search should be conducted recursively (return paths of files inside 's subdirectories and so on) or not. + /// An array of the paths of all files inside . + [MustUseReturnValue] + public static string[] GetFiles(this Directory directory, bool recursive = false) + { + return recursive + ? directory.GetDirectories(true) + .SelectMany(path => + { + using Directory recursiveDirectory = new(); + recursiveDirectory.Open(path); + return recursiveDirectory.GetElementsNonRecursive(true); + }) + .Concat(directory.GetElementsNonRecursive(true)) + .ToArray() + : directory.GetElementsNonRecursive(true) + .ToArray(); + } + + /// + /// Returns the complete file paths of all files inside whose extensions match any of . + /// + /// The to search in. + /// Whether the search should be conducted recursively (return paths of files inside 's subdirectories and so on) or not. + /// The file extensions to search for. If none are provided, all file paths are returned. + /// An array of the paths of all files inside whose extensions match any of . + [MustUseReturnValue] + public static string[] GetFiles(this Directory directory, bool recursive = false, params string[] fileExtensions) + { + return fileExtensions.Any() + ? Array.FindAll(directory.GetFiles(recursive), file => fileExtensions.Any(file.EndsWith)) + : directory.GetFiles(recursive); + } + + /// + /// Returns the complete directory paths of all directories inside . + /// + /// The to search in. + /// Whether the search should be conducted recursively (return paths of directories inside 's subdirectories and so on) or not. + /// An array of the paths of all files inside . + [MustUseReturnValue] + public static string[] GetDirectories(this Directory directory, bool recursive = false) + { + return recursive + ? directory.GetElementsNonRecursive(false) + .SelectMany(path => + { + using Directory recursiveDirectory = new(); + recursiveDirectory.Open(path); + return recursiveDirectory.GetDirectories(true).Prepend(path); + }) + .ToArray() + : directory.GetElementsNonRecursive(false) + .ToArray(); + } + + [MustUseReturnValue] + private static IEnumerable GetElementsNonRecursive(this Directory directory, bool trueIfFiles) + { + directory.ListDirBegin(true); + while (true) + { + string next = directory.GetNext(); + if (next is "") + { + yield break; + } + if (directory.CurrentIsDir() == trueIfFiles) + { + continue; + } + string current = directory.GetCurrentDir(); + yield return current.EndsWith("/") ? $"{current}{next}" : $"{current}/{next}"; + } + } + } +} \ No newline at end of file From d13408620238384e987773004bbfcdbd1de0541d Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 18 Jun 2022 14:13:42 +0200 Subject: [PATCH 16/21] Add attributes for better intellisense --- ModLoader.cs | 3 +++ Utility/Extensions/EnumerableExtensions.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ModLoader.cs b/ModLoader.cs index 8edfaf8..56ddf80 100644 --- a/ModLoader.cs +++ b/ModLoader.cs @@ -71,6 +71,7 @@ where attribute is not null } } + [MustUseReturnValue] private static Dictionary LoadModMetadata(IEnumerable modDirectories) { Dictionary loadedMetadata = new(); @@ -97,6 +98,7 @@ where loaded.Incompatible.Contains(metadata.Id) return loadedMetadata; } + [MustUseReturnValue] private static Dictionary FilterModMetadata(Dictionary loadedMetadata) { // If the dependencies of any metadata have not been loaded, remove that metadata and try again @@ -112,6 +114,7 @@ select loadedMetadata.TryGetValue(dependency, out _)).Any(dependency => !depende return loadedMetadata; } + [MustUseReturnValue] private static IEnumerable SortModMetadata(Dictionary filteredMetadata) { // Create a graph of each metadata ID and the IDs of those that need to be loaded after it diff --git a/Utility/Extensions/EnumerableExtensions.cs b/Utility/Extensions/EnumerableExtensions.cs index f7f137c..fb9d23b 100644 --- a/Utility/Extensions/EnumerableExtensions.cs +++ b/Utility/Extensions/EnumerableExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; + namespace Godot.Modding.Utility.Extensions { /// @@ -18,6 +20,7 @@ public static class EnumerableExtensions /// The of elements in . /// An of elements from sorted topologically, or if no valid topological sorting exists. /// Thrown if either or is . + [MustUseReturnValue] public static IEnumerable? TopologicalSort(this IEnumerable source, Func> dependencies, Action? cyclic = null) { if (source is null) From d0906badb4d400229e3009cb9321deaba98667de Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 18 Jun 2022 17:08:36 +0200 Subject: [PATCH 17/21] Add README --- Modot.csproj | 1 + README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 README.md diff --git a/Modot.csproj b/Modot.csproj index db22c6f..6330f72 100644 --- a/Modot.csproj +++ b/Modot.csproj @@ -25,5 +25,6 @@ + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a81442 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Modot + +**Modot** is a mod loader for applications made using Godot, inspired heavily by [RimWorld](https://rimworldgame.com)'s mod loading process. + +# Features + +- Load mods with C# assemblies, XML data, and resource packs at runtime +- Sort mods using load orders defined partially by each mod to prevent conflicts +- Optionally execute code from mod assemblies upon loading + +A more detailed explanation of all features along with instructions on usage is available on the [wiki](https://github.com/Carnagion/Modot/wiki). + +# Installation + +**Modot** is available as a [NuGet package](https://www.nuget.org/packages/Modot). +Simply include the following lines in a Godot project's `.csproj` file (either by editing the file manually or letting an IDE install the package): +```xml + + + + ``` + +Due to [a bug](https://github.com/godotengine/godot/issues/42271) in Godot, the following lines will also need to be included in the `.csproj` file to properly compile along with NuGet packages: +```xml + + true + +``` + +# Security + +**Modot** includes the ability to execute code from C# assemblies (`.dll` files) at runtime. +While this feature is immensely useful and opens up a plethora of possibilities for modding, it also comes with the risk of executing potentially malicious code. + +This is unfortunately an issue that has no easy solution, as it is fairly difficult to accurately detect whether an assembly contains harmful code. + +As such, it is important to note that **Modot does not bear the responsibility of checking for potentially malicious code in a mod's assembly**. + +However, it does provide the option to ignore a mod's assemblies, preventing any code from being executed. +Along with the ability to load mods individually, this can be used to ensure that only trusted mods can execute their code. + +Another way to prevent executing malicious code is by restricting the source of mods to websites that thoroughly scan and verify uploaded user content, such as [Steam](https://store.steampowered.com). As mentioned earlier though, **it is not Modot's responsibility to implement such checks**. \ No newline at end of file From 77c8faa3cc0ed80b920d58410ce07e85f206d757 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 18 Jun 2022 17:14:49 +0200 Subject: [PATCH 18/21] Update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0a81442..d61da73 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,14 @@ **Modot** is a mod loader for applications made using Godot, inspired heavily by [RimWorld](https://rimworldgame.com)'s mod loading process. +Its API is aimed at allowing creators to easily modularise their Godot applications, create and deploy patches and DLCs, and let users expand the functionality of their applications. + # Features - Load mods with C# assemblies, XML data, and resource packs at runtime - Sort mods using load orders defined partially by each mod to prevent conflicts - Optionally execute code from mod assemblies upon loading +- Load mods individually, bypassing load order restrictions A more detailed explanation of all features along with instructions on usage is available on the [wiki](https://github.com/Carnagion/Modot/wiki). From 53fb85651d4260dc2d90065af647a15429593549 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 18 Jun 2022 17:25:28 +0200 Subject: [PATCH 19/21] Update README --- Modot.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modot.csproj b/Modot.csproj index 6330f72..00fb8b7 100644 --- a/Modot.csproj +++ b/Modot.csproj @@ -6,6 +6,12 @@ Godot.Modding true + true + 1.0.0 + Modot + Carnagion + A mod loader and API for applications made using Godot, with the ability to load C# assemblies, XML data, and resource packs at runtime. + https://github.com/Carnagion/Modot C:\Users\Indraneel\Projects\Modot\.mono\temp\bin\Debug\Modot.xml From 42e0b977ca9258bff0e55a97fc892bd8b744cee0 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 18 Jun 2022 17:26:32 +0200 Subject: [PATCH 20/21] Add license --- LICENSE | 21 +++++++++++++++++++++ Modot.csproj | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..98fd7d6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Carnagion + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Modot.csproj b/Modot.csproj index 00fb8b7..1805fd9 100644 --- a/Modot.csproj +++ b/Modot.csproj @@ -12,6 +12,7 @@ Carnagion A mod loader and API for applications made using Godot, with the ability to load C# assemblies, XML data, and resource packs at runtime. https://github.com/Carnagion/Modot + LICENSE C:\Users\Indraneel\Projects\Modot\.mono\temp\bin\Debug\Modot.xml @@ -31,6 +32,10 @@ + + + + \ No newline at end of file From ab8efba4446143d01491b2edf8006c7b41d2c220 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 18 Jun 2022 17:33:56 +0200 Subject: [PATCH 21/21] Make executing mod assembly code optional --- ModLoader.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ModLoader.cs b/ModLoader.cs index 56ddf80..2800ca1 100644 --- a/ModLoader.cs +++ b/ModLoader.cs @@ -28,31 +28,39 @@ public static IReadOnlyDictionary LoadedMods } /// - /// Loads a from and runs all methods marked with in its assemblies (if any). + /// Loads a from and runs all methods marked with in its assemblies if specified. /// /// The directory path containing the 's metadata, assemblies, data, and resource packs. + /// Whether any code in any assemblies of the loaded gets executed. /// The loaded from . /// This method only loads a individually, and does not check whether it has been loaded with all dependencies and in the correct load order. To load multiple s in a safe and orderly manner, should be used. - public static Mod LoadMod(string modDirectoryPath) + public static Mod LoadMod(string modDirectoryPath, bool executeAssemblies = true) { Mod mod = new(Mod.Metadata.Load(modDirectoryPath)); ModLoader.loadedMods.Add(mod.Meta.Id, mod); - ModLoader.StartupMods(mod.Yield()); + if (executeAssemblies) + { + ModLoader.StartupMods(mod.Yield()); + } return mod; } /// - /// Loads s from and runs all methods marked with in their assemblies (if any). + /// Loads s from and runs all methods marked with in their assemblies if specified. /// /// The directory paths to load the s from, containing each 's metadata, assemblies, data, and resource packs. + /// Whether any code in any assemblies of the loaded s gets executed. /// An of the loaded s in the correct load order. s that could not be loaded due to issues will not be contained in the sequence. /// This method loads multiple s after sorting them according to the load order specified in their metadata. To load a individually without regard to its dependencies and load order, should be used. - public static IEnumerable LoadMods(IEnumerable modDirectoryPaths) + public static IEnumerable LoadMods(IEnumerable modDirectoryPaths, bool executeAssemblies = true) { List mods = (from metadata in ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths))) select new Mod(metadata)).ToList(); mods.ForEach(mod => ModLoader.loadedMods.Add(mod.Meta.Id, mod)); - ModLoader.StartupMods(mods); + if (executeAssemblies) + { + ModLoader.StartupMods(mods); + } return mods; }