Skip to content

Commit

Permalink
Merge pull request #4 from Carnagion/development
Browse files Browse the repository at this point in the history
Merge v2.0.0 into stable
  • Loading branch information
Carnagion committed Aug 12, 2022
2 parents fd59f53 + cf265df commit bcd6493
Show file tree
Hide file tree
Showing 28 changed files with 1,026 additions and 87 deletions.
19 changes: 2 additions & 17 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[*]
charset = utf-8
end_of_line = crlf
trim_trailing_whitespace = true
trim_trailing_whitespace = false
insert_final_newline = false
indent_style = space
indent_size = 4
Expand Down Expand Up @@ -48,12 +48,6 @@ dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d
dotnet_naming_rule.unity_serialized_field_rule.severity = warning
dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style
dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols
dotnet_naming_rule.unity_serialized_field_rule_1.import_to_resharper = True
dotnet_naming_rule.unity_serialized_field_rule_1.resharper_description = Unity serialized field
dotnet_naming_rule.unity_serialized_field_rule_1.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
dotnet_naming_rule.unity_serialized_field_rule_1.severity = warning
dotnet_naming_rule.unity_serialized_field_rule_1.style = lower_camel_case_style
dotnet_naming_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
Expand All @@ -79,10 +73,6 @@ dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities
dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds =
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities = *
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds =
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance
dotnet_separate_import_directive_groups = true
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
Expand Down Expand Up @@ -121,17 +111,12 @@ resharper_csharp_int_align_fix_in_adjacent = false
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 0
resharper_csharp_naming_rule.constants = AaBb
resharper_csharp_naming_rule.private_constants = aaBb
resharper_csharp_naming_rule.private_static_fields = aaBb
resharper_csharp_naming_rule.private_static_readonly = aaBb
resharper_csharp_naming_rule.static_readonly = AaBb
resharper_csharp_stick_comment = false
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_csharp_wrap_before_binary_opsign = true
resharper_csharp_wrap_extends_list_style = chop_if_long
resharper_csharp_wrap_lines = false
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_enforce_line_ending_style = true
resharper_indent_nested_fixed_stmt = true
resharper_indent_nested_foreach_stmt = true
resharper_indent_nested_for_stmt = true
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Carnagion
Copyright (c) 2022 Indraneel Mahendrakumar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
56 changes: 45 additions & 11 deletions Mod.cs → Modding/Mod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using JetBrains.Annotations;

using Godot.Modding.Patching;
using Godot.Serialization;

namespace Godot.Modding
Expand All @@ -24,9 +25,10 @@ public sealed record Mod
public Mod(Metadata metadata)
{
this.Meta = metadata;
this.Assemblies = this.LoadAssemblies();
this.Data = this.LoadData();
this.LoadResources();
this.Data = this.LoadData();
this.Patches = this.LoadPatches();
this.Assemblies = this.LoadAssemblies();
}

/// <summary>
Expand All @@ -46,9 +48,17 @@ public IEnumerable<Assembly> Assemblies
}

/// <summary>
/// The XML data of the <see cref="Mod"/>, combined into a single <see cref="XmlNode"/> as its children.
/// The XML data of the <see cref="Mod"/> if any, combined into a single <see cref="XmlDocument"/>.
/// </summary>
public XmlDocument? Data
{
get;
}

/// <summary>
/// The patches applied by the <see cref="Mod"/>.
/// </summary>
public XmlNode? Data
public IEnumerable<IPatch> Patches
{
get;
}
Expand All @@ -62,7 +72,7 @@ private IEnumerable<Assembly> LoadAssemblies()
: Enumerable.Empty<Assembly>();
}

private XmlNode? LoadData()
private XmlDocument? LoadData()
{
IEnumerable<XmlDocument> documents = this.LoadDocuments().ToArray();
if (!documents.Any())
Expand All @@ -71,11 +81,16 @@ private IEnumerable<Assembly> LoadAssemblies()
}

XmlDocument data = new();
data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), data.DocumentElement);

XmlElement root = data.CreateElement("Data");
data.AppendChild(root);
data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), root);

documents
.SelectMany(document => document.Cast<XmlNode>())
.Where(node => node.NodeType is not XmlNodeType.XmlDeclaration)
.ForEach(node => data.AppendChild(node));
.Where(node => node is not XmlDeclaration)
.ForEach(node => root.AppendChild(data.ImportNode(node, true)));

return data;
}

Expand Down Expand Up @@ -105,11 +120,30 @@ private void LoadResources()
return;
}

foreach (string resourcePath in System.IO.Directory.GetFiles(resourcesPath, "*.pck", SearchOption.AllDirectories))
string? invalidResourcePath = System.IO.Directory.GetFiles(resourcesPath, "*.pck", SearchOption.AllDirectories).FirstOrDefault(resourcePath => !ProjectSettings.LoadResourcePack(resourcePath));
if (invalidResourcePath is not null)
{
throw new ModLoadException(this.Meta.Directory, $"Error loading resource pack at {invalidResourcePath}");
}
}

private IEnumerable<IPatch> LoadPatches()
{
string patchesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Patches";

if (!System.IO.Directory.Exists(patchesPath))
{
yield break;
}

Serializer serializer = new();
XmlDocument document = new();
foreach (string patchPath in System.IO.Directory.GetFiles(patchesPath, "*.xml", SearchOption.AllDirectories))
{
if (!ProjectSettings.LoadResourcePack(resourcePath))
document.Load(patchPath);
if (document.DocumentElement is not null)
{
throw new ModLoadException(this.Meta.Directory, $"Error loading resource pack at {resourcePath}");
yield return serializer.Deserialize(document.DocumentElement) as IPatch ?? throw new ModLoadException(this.Meta.Directory, $"Invalid patch at {patchPath}");
}
}
}
Expand Down
File renamed without changes.
64 changes: 53 additions & 11 deletions ModLoader.cs → Modding/ModLoader.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml;

using JetBrains.Annotations;

using Godot.Modding.Utility.Extensions;
using Godot.Utility;
using Godot.Utility.Extensions;

namespace Godot.Modding
{
Expand All @@ -14,7 +16,7 @@ namespace Godot.Modding
[PublicAPI]
public static class ModLoader
{
private static readonly Dictionary<string, Mod> loadedMods = new();
private static readonly OrderedDictionary<string, Mod> loadedMods = new();

/// <summary>
/// All the <see cref="Mod"/>s that have been loaded at runtime.
Expand All @@ -28,43 +30,83 @@ public static IReadOnlyDictionary<string, Mod> LoadedMods
}

/// <summary>
/// Loads a <see cref="Mod"/> from <paramref name="modDirectoryPath"/> and runs all methods marked with <see cref="ModStartupAttribute"/> in its assemblies if specified.
/// Loads a <see cref="Mod"/> from <paramref name="modDirectoryPath"/>, applies its patches if any, and runs all methods marked with <see cref="ModStartupAttribute"/> in its assemblies if specified.
/// </summary>
/// <param name="modDirectoryPath">The directory path containing the <see cref="Mod"/>'s metadata, assemblies, data, and resource packs.</param>
/// <param name="executeAssemblies">Whether any code in any assemblies of the loaded <see cref="Mod"/> gets executed.</param>
/// <returns>The <see cref="Mod"/> loaded from <paramref name="modDirectoryPath"/>.</returns>
/// <remarks>This method only loads a <see cref="Mod"/> individually, and does not check whether it has been loaded with all dependencies and in the correct load order. To load multiple <see cref="Mod"/>s in a safe and orderly manner, <see cref="LoadMods"/> should be used.</remarks>
public static Mod LoadMod(string modDirectoryPath, bool executeAssemblies = true)
{
// Load mod
Mod mod = new(Mod.Metadata.Load(modDirectoryPath));
ModLoader.loadedMods.Add(mod.Meta.Id, mod);

// Cache XML data of loaded mods for repeat enumeration later
XmlElement[] data = ModLoader.LoadedMods.Values
.Select(loadedMod => loadedMod.Data?.DocumentElement)
.Append(mod.Data?.DocumentElement)
.NotNull()
.ToArray();

// Apply mod patches
mod.Patches.ForEach(patch => data.ForEach(patch.Apply));

// Execute mod assemblies
if (executeAssemblies)
{
ModLoader.StartupMod(mod);
}

// Register mod as fully loaded
ModLoader.loadedMods.Add(mod.Meta.Id, mod);

return mod;
}

/// <summary>
/// Loads <see cref="Mod"/>s from <paramref name="modDirectoryPaths"/> and runs all methods marked with <see cref="ModStartupAttribute"/> in their assemblies if specified.
/// Loads <see cref="Mod"/>s from <paramref name="modDirectoryPaths"/>, applies their patches if any, runs all methods marked with <see cref="ModStartupAttribute"/> in their assemblies if specified.
/// </summary>
/// <param name="modDirectoryPaths">The directory paths to load the <see cref="Mod"/>s from, containing each <see cref="Mod"/>'s metadata, assemblies, data, and resource packs.</param>
/// <param name="executeAssemblies">Whether any code in any assemblies of the loaded <see cref="Mod"/>s gets executed.</param>
/// <returns>An <see cref="IEnumerable{T}"/> of the loaded <see cref="Mod"/>s in the correct load order. <see cref="Mod"/>s that could not be loaded due to issues will not be contained in the sequence.</returns>
/// <remarks>This method loads multiple <see cref="Mod"/>s after sorting them according to the load order specified in their metadata. To load a <see cref="Mod"/> individually without regard to its dependencies and load order, <see cref="LoadMod"/> should be used.</remarks>
public static IEnumerable<Mod> LoadMods(IEnumerable<string> modDirectoryPaths, bool executeAssemblies = true)
{
List<Mod> mods = ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths)))
.Select(metadata => new Mod(metadata))
// Cache XML data of loaded mods for repeat enumeration later
List<XmlElement> data = ModLoader.LoadedMods.Values
.Select(mod => mod.Data?.DocumentElement)
.NotNull()
.ToList();
mods.ForEach(mod => ModLoader.loadedMods.Add(mod.Meta.Id, mod));
if (executeAssemblies)

List<Mod> mods = new();
foreach (Mod.Metadata metadata in ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths))))
{
mods.ForEach(ModLoader.StartupMod);
// Load mod
Mod mod = new(metadata);
mods.Add(mod);

// Apply mod patches
XmlElement? root = mod.Data?.DocumentElement;
if (root is not null)
{
data.Add(root);
}
mod.Patches.ForEach(patch => data.ForEach(patch.Apply));
}
foreach (Mod mod in mods)
{
// Execute mod assemblies
if (executeAssemblies)
{
ModLoader.StartupMod(mod);
}

// Register mod as fully loaded
ModLoader.loadedMods.Add(mod.Meta.Id, mod);
}
return mods;
}

private static void StartupMod(Mod mod)
{
// Invoke all static methods annotated with [Startup] along with the supplied parameters (if any)
Expand Down
File renamed without changes.
52 changes: 52 additions & 0 deletions Modding/Patching/AttributeRemovePatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Xml;

using JetBrains.Annotations;

using Godot.Serialization;

namespace Godot.Modding.Patching
{
/// <summary>
/// An <see cref="IPatch"/> that removes an attribute from an <see cref="XmlElement"/>.
/// </summary>
[PublicAPI]
public class AttributeRemovePatch : IPatch
{
/// <summary>
/// Initialises a new <see cref="AttributeRemovePatch"/> with the specified parameters.
/// </summary>
/// <param name="attribute">The name of the attribute to remove.</param>
public AttributeRemovePatch(string attribute)
{
this.Attribute = attribute;
}

[UsedImplicitly]
private AttributeRemovePatch()
{
}

/// <summary>
/// The name of the attribute to remove.
/// </summary>
[Serialize]
public string Attribute
{
get;
[UsedImplicitly]
private set;
} = null!;

/// <summary>
/// Removes <see cref="Attribute"/> from <paramref name="data"/> if <paramref name="data"/> is an <see cref="XmlElement"/>.
/// </summary>
/// <param name="data">The <see cref="XmlNode"/> to apply the patch on.</param>
public void Apply(XmlNode data)
{
if (data is XmlElement element)
{
element.RemoveAttribute(this.Attribute);
}
}
}
}
Loading

0 comments on commit bcd6493

Please sign in to comment.