From a6b5339a8709169e90a7a67c92365c62e6fec44d Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 19 Aug 2022 14:27:46 +0200 Subject: [PATCH 01/32] Import node when using replacement patch --- Modding/Patching/NodeReplacePatch.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modding/Patching/NodeReplacePatch.cs b/Modding/Patching/NodeReplacePatch.cs index fa9b390..b0aab2b 100644 --- a/Modding/Patching/NodeReplacePatch.cs +++ b/Modding/Patching/NodeReplacePatch.cs @@ -45,14 +45,15 @@ public void Apply(XmlNode data) { XmlNode? previous = data.PreviousSibling; XmlNode parent = data.ParentNode!; + XmlNode replacement = data.OwnerDocument!.ImportNode(this.Replacement, true); parent.RemoveChild(data); if (previous is null) { - parent.PrependChild(this.Replacement); + parent.PrependChild(replacement); } else { - parent.InsertAfter(this.Replacement, previous); + parent.InsertAfter(replacement, previous); } } } From 67d2a2c918114d6656bf4ee55e08d7686b39d2c8 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 21 Sep 2022 12:47:35 +0100 Subject: [PATCH 02/32] Refactor C# project into separate directory --- .gitignore | 6 +++--- .editorconfig => C#/.editorconfig | 0 {Modding => C#/Modding}/Mod.cs | 0 {Modding => C#/Modding}/ModLoadException.cs | 0 {Modding => C#/Modding}/ModLoader.cs | 0 {Modding => C#/Modding}/ModStartupAttribute.cs | 0 .../Modding}/Patching/AttributeRemovePatch.cs | 0 {Modding => C#/Modding}/Patching/AttributeSetPatch.cs | 0 {Modding => C#/Modding}/Patching/ConditionalPatch.cs | 0 .../Modding}/Patching/Conditions/AndCondition.cs | 0 .../Modding}/Patching/Conditions/ICondition.cs | 0 .../Modding}/Patching/Conditions/ModLoadedCondition.cs | 0 .../Patching/Conditions/NodeExistsCondition.cs | 0 .../Modding}/Patching/Conditions/NotCondition.cs | 0 .../Modding}/Patching/Conditions/OrCondition.cs | 0 {Modding => C#/Modding}/Patching/IPatch.cs | 0 {Modding => C#/Modding}/Patching/LogPatch.cs | 0 {Modding => C#/Modding}/Patching/MultiPatch.cs | 0 {Modding => C#/Modding}/Patching/NodeAddPatch.cs | 0 {Modding => C#/Modding}/Patching/NodeRemovePatch.cs | 0 {Modding => C#/Modding}/Patching/NodeReplacePatch.cs | 0 {Modding => C#/Modding}/Patching/TargetedPatch.cs | 0 Modot.csproj => C#/Modot.csproj | 10 +++++----- C#/Mods/Test 2/Patches/Patch.xml | 10 ++++++++++ {Utility => C#/Utility}/ErrorException.cs | 0 .../Utility}/Extensions/DirectoryExtensions.cs | 0 .../Utility}/Extensions/EnumerableExtensions.cs | 0 {Utility => C#/Utility}/Extensions/ErrorExtensions.cs | 0 28 files changed, 18 insertions(+), 8 deletions(-) rename .editorconfig => C#/.editorconfig (100%) rename {Modding => C#/Modding}/Mod.cs (100%) rename {Modding => C#/Modding}/ModLoadException.cs (100%) rename {Modding => C#/Modding}/ModLoader.cs (100%) rename {Modding => C#/Modding}/ModStartupAttribute.cs (100%) rename {Modding => C#/Modding}/Patching/AttributeRemovePatch.cs (100%) rename {Modding => C#/Modding}/Patching/AttributeSetPatch.cs (100%) rename {Modding => C#/Modding}/Patching/ConditionalPatch.cs (100%) rename {Modding => C#/Modding}/Patching/Conditions/AndCondition.cs (100%) rename {Modding => C#/Modding}/Patching/Conditions/ICondition.cs (100%) rename {Modding => C#/Modding}/Patching/Conditions/ModLoadedCondition.cs (100%) rename {Modding => C#/Modding}/Patching/Conditions/NodeExistsCondition.cs (100%) rename {Modding => C#/Modding}/Patching/Conditions/NotCondition.cs (100%) rename {Modding => C#/Modding}/Patching/Conditions/OrCondition.cs (100%) rename {Modding => C#/Modding}/Patching/IPatch.cs (100%) rename {Modding => C#/Modding}/Patching/LogPatch.cs (100%) rename {Modding => C#/Modding}/Patching/MultiPatch.cs (100%) rename {Modding => C#/Modding}/Patching/NodeAddPatch.cs (100%) rename {Modding => C#/Modding}/Patching/NodeRemovePatch.cs (100%) rename {Modding => C#/Modding}/Patching/NodeReplacePatch.cs (100%) rename {Modding => C#/Modding}/Patching/TargetedPatch.cs (100%) rename Modot.csproj => C#/Modot.csproj (83%) create mode 100644 C#/Mods/Test 2/Patches/Patch.xml rename {Utility => C#/Utility}/ErrorException.cs (100%) rename {Utility => C#/Utility}/Extensions/DirectoryExtensions.cs (100%) rename {Utility => C#/Utility}/Extensions/EnumerableExtensions.cs (100%) rename {Utility => C#/Utility}/Extensions/ErrorExtensions.cs (100%) diff --git a/.gitignore b/.gitignore index d87d1ac..8fb2434 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ bin/ obj/ *.sln -.mono/ -.import/ -.idea/ \ No newline at end of file +**/.mono/ +**/.import/ +**/.idea/ \ No newline at end of file diff --git a/.editorconfig b/C#/.editorconfig similarity index 100% rename from .editorconfig rename to C#/.editorconfig diff --git a/Modding/Mod.cs b/C#/Modding/Mod.cs similarity index 100% rename from Modding/Mod.cs rename to C#/Modding/Mod.cs diff --git a/Modding/ModLoadException.cs b/C#/Modding/ModLoadException.cs similarity index 100% rename from Modding/ModLoadException.cs rename to C#/Modding/ModLoadException.cs diff --git a/Modding/ModLoader.cs b/C#/Modding/ModLoader.cs similarity index 100% rename from Modding/ModLoader.cs rename to C#/Modding/ModLoader.cs diff --git a/Modding/ModStartupAttribute.cs b/C#/Modding/ModStartupAttribute.cs similarity index 100% rename from Modding/ModStartupAttribute.cs rename to C#/Modding/ModStartupAttribute.cs diff --git a/Modding/Patching/AttributeRemovePatch.cs b/C#/Modding/Patching/AttributeRemovePatch.cs similarity index 100% rename from Modding/Patching/AttributeRemovePatch.cs rename to C#/Modding/Patching/AttributeRemovePatch.cs diff --git a/Modding/Patching/AttributeSetPatch.cs b/C#/Modding/Patching/AttributeSetPatch.cs similarity index 100% rename from Modding/Patching/AttributeSetPatch.cs rename to C#/Modding/Patching/AttributeSetPatch.cs diff --git a/Modding/Patching/ConditionalPatch.cs b/C#/Modding/Patching/ConditionalPatch.cs similarity index 100% rename from Modding/Patching/ConditionalPatch.cs rename to C#/Modding/Patching/ConditionalPatch.cs diff --git a/Modding/Patching/Conditions/AndCondition.cs b/C#/Modding/Patching/Conditions/AndCondition.cs similarity index 100% rename from Modding/Patching/Conditions/AndCondition.cs rename to C#/Modding/Patching/Conditions/AndCondition.cs diff --git a/Modding/Patching/Conditions/ICondition.cs b/C#/Modding/Patching/Conditions/ICondition.cs similarity index 100% rename from Modding/Patching/Conditions/ICondition.cs rename to C#/Modding/Patching/Conditions/ICondition.cs diff --git a/Modding/Patching/Conditions/ModLoadedCondition.cs b/C#/Modding/Patching/Conditions/ModLoadedCondition.cs similarity index 100% rename from Modding/Patching/Conditions/ModLoadedCondition.cs rename to C#/Modding/Patching/Conditions/ModLoadedCondition.cs diff --git a/Modding/Patching/Conditions/NodeExistsCondition.cs b/C#/Modding/Patching/Conditions/NodeExistsCondition.cs similarity index 100% rename from Modding/Patching/Conditions/NodeExistsCondition.cs rename to C#/Modding/Patching/Conditions/NodeExistsCondition.cs diff --git a/Modding/Patching/Conditions/NotCondition.cs b/C#/Modding/Patching/Conditions/NotCondition.cs similarity index 100% rename from Modding/Patching/Conditions/NotCondition.cs rename to C#/Modding/Patching/Conditions/NotCondition.cs diff --git a/Modding/Patching/Conditions/OrCondition.cs b/C#/Modding/Patching/Conditions/OrCondition.cs similarity index 100% rename from Modding/Patching/Conditions/OrCondition.cs rename to C#/Modding/Patching/Conditions/OrCondition.cs diff --git a/Modding/Patching/IPatch.cs b/C#/Modding/Patching/IPatch.cs similarity index 100% rename from Modding/Patching/IPatch.cs rename to C#/Modding/Patching/IPatch.cs diff --git a/Modding/Patching/LogPatch.cs b/C#/Modding/Patching/LogPatch.cs similarity index 100% rename from Modding/Patching/LogPatch.cs rename to C#/Modding/Patching/LogPatch.cs diff --git a/Modding/Patching/MultiPatch.cs b/C#/Modding/Patching/MultiPatch.cs similarity index 100% rename from Modding/Patching/MultiPatch.cs rename to C#/Modding/Patching/MultiPatch.cs diff --git a/Modding/Patching/NodeAddPatch.cs b/C#/Modding/Patching/NodeAddPatch.cs similarity index 100% rename from Modding/Patching/NodeAddPatch.cs rename to C#/Modding/Patching/NodeAddPatch.cs diff --git a/Modding/Patching/NodeRemovePatch.cs b/C#/Modding/Patching/NodeRemovePatch.cs similarity index 100% rename from Modding/Patching/NodeRemovePatch.cs rename to C#/Modding/Patching/NodeRemovePatch.cs diff --git a/Modding/Patching/NodeReplacePatch.cs b/C#/Modding/Patching/NodeReplacePatch.cs similarity index 100% rename from Modding/Patching/NodeReplacePatch.cs rename to C#/Modding/Patching/NodeReplacePatch.cs diff --git a/Modding/Patching/TargetedPatch.cs b/C#/Modding/Patching/TargetedPatch.cs similarity index 100% rename from Modding/Patching/TargetedPatch.cs rename to C#/Modding/Patching/TargetedPatch.cs diff --git a/Modot.csproj b/C#/Modot.csproj similarity index 83% rename from Modot.csproj rename to C#/Modot.csproj index 0125df0..510dc9b 100644 --- a/Modot.csproj +++ b/C#/Modot.csproj @@ -13,7 +13,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 + ../LICENSE @@ -21,11 +21,11 @@ - - - + + + - + \ No newline at end of file diff --git a/C#/Mods/Test 2/Patches/Patch.xml b/C#/Mods/Test 2/Patches/Patch.xml new file mode 100644 index 0000000..b22135a --- /dev/null +++ b/C#/Mods/Test 2/Patches/Patch.xml @@ -0,0 +1,10 @@ + + + /Data/Test[@Add="True"] + + + This has been added + s + + + \ No newline at end of file diff --git a/Utility/ErrorException.cs b/C#/Utility/ErrorException.cs similarity index 100% rename from Utility/ErrorException.cs rename to C#/Utility/ErrorException.cs diff --git a/Utility/Extensions/DirectoryExtensions.cs b/C#/Utility/Extensions/DirectoryExtensions.cs similarity index 100% rename from Utility/Extensions/DirectoryExtensions.cs rename to C#/Utility/Extensions/DirectoryExtensions.cs diff --git a/Utility/Extensions/EnumerableExtensions.cs b/C#/Utility/Extensions/EnumerableExtensions.cs similarity index 100% rename from Utility/Extensions/EnumerableExtensions.cs rename to C#/Utility/Extensions/EnumerableExtensions.cs diff --git a/Utility/Extensions/ErrorExtensions.cs b/C#/Utility/Extensions/ErrorExtensions.cs similarity index 100% rename from Utility/Extensions/ErrorExtensions.cs rename to C#/Utility/Extensions/ErrorExtensions.cs From 73b88045b61504cfb19ec0619abf6e3314ca9bc8 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 21 Sep 2022 12:50:04 +0100 Subject: [PATCH 03/32] Remove test mod files --- C#/Mods/Test 2/Patches/Patch.xml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 C#/Mods/Test 2/Patches/Patch.xml diff --git a/C#/Mods/Test 2/Patches/Patch.xml b/C#/Mods/Test 2/Patches/Patch.xml deleted file mode 100644 index b22135a..0000000 --- a/C#/Mods/Test 2/Patches/Patch.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - /Data/Test[@Add="True"] - - - This has been added - s - - - \ No newline at end of file From b97c29e74f99a2315b68820f70111b770113f4b8 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 21 Sep 2022 13:42:58 +0100 Subject: [PATCH 04/32] Track files --- .editorconfig | 196 +++++++++++ LICENSE | 21 ++ Modding/Mod.cs | 303 ++++++++++++++++++ Modding/ModLoadException.cs | 28 ++ Modding/ModLoader.cs | 182 +++++++++++ Modding/ModStartupAttribute.cs | 38 +++ Modding/Patching/AttributeRemovePatch.cs | 52 +++ Modding/Patching/AttributeSetPatch.cs | 65 ++++ Modding/Patching/ConditionalPatch.cs | 75 +++++ Modding/Patching/Conditions/AndCondition.cs | 52 +++ Modding/Patching/Conditions/ICondition.cs | 17 + .../Patching/Conditions/ModLoadedCondition.cs | 50 +++ .../Conditions/NodeExistsCondition.cs | 50 +++ Modding/Patching/Conditions/NotCondition.cs | 50 +++ Modding/Patching/Conditions/OrCondition.cs | 51 +++ Modding/Patching/IPatch.cs | 16 + Modding/Patching/LogPatch.cs | 50 +++ Modding/Patching/MultiPatch.cs | 51 +++ Modding/Patching/NodeAddPatch.cs | 73 +++++ Modding/Patching/NodeRemovePatch.cs | 22 ++ Modding/Patching/NodeReplacePatch.cs | 60 ++++ Modding/Patching/TargetedPatch.cs | 65 ++++ Modot.csproj | 31 ++ README.md | 46 +++ Utility/ErrorException.cs | 30 ++ Utility/Extensions/DirectoryExtensions.cs | 157 +++++++++ Utility/Extensions/EnumerableExtensions.cs | 70 ++++ Utility/Extensions/ErrorExtensions.cs | 39 +++ 28 files changed, 1940 insertions(+) create mode 100644 .editorconfig create mode 100644 LICENSE create mode 100644 Modding/Mod.cs create mode 100644 Modding/ModLoadException.cs create mode 100644 Modding/ModLoader.cs create mode 100644 Modding/ModStartupAttribute.cs create mode 100644 Modding/Patching/AttributeRemovePatch.cs create mode 100644 Modding/Patching/AttributeSetPatch.cs create mode 100644 Modding/Patching/ConditionalPatch.cs create mode 100644 Modding/Patching/Conditions/AndCondition.cs create mode 100644 Modding/Patching/Conditions/ICondition.cs create mode 100644 Modding/Patching/Conditions/ModLoadedCondition.cs create mode 100644 Modding/Patching/Conditions/NodeExistsCondition.cs create mode 100644 Modding/Patching/Conditions/NotCondition.cs create mode 100644 Modding/Patching/Conditions/OrCondition.cs create mode 100644 Modding/Patching/IPatch.cs create mode 100644 Modding/Patching/LogPatch.cs create mode 100644 Modding/Patching/MultiPatch.cs create mode 100644 Modding/Patching/NodeAddPatch.cs create mode 100644 Modding/Patching/NodeRemovePatch.cs create mode 100644 Modding/Patching/NodeReplacePatch.cs create mode 100644 Modding/Patching/TargetedPatch.cs create mode 100644 Modot.csproj create mode 100644 README.md create mode 100644 Utility/ErrorException.cs create mode 100644 Utility/Extensions/DirectoryExtensions.cs create mode 100644 Utility/Extensions/EnumerableExtensions.cs create mode 100644 Utility/Extensions/ErrorExtensions.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f879977 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,196 @@ + +[*] +charset = utf-8 +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_preferred_modifier_order = public, protected, internal, private, static, extern, sealed, abstract, virtual, override, new, readonly, async, unsafe, volatile:warning +csharp_prefer_braces = true:warning +csharp_preserve_single_line_blocks = false +csharp_style_var_elsewhere = false:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = false:warning +dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +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_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 +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly +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_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 +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = false:warning +dotnet_style_qualification_for_event = true:warning +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_property = true:warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# ReSharper properties +resharper_accessor_owner_body = accessors_with_block_body +resharper_align_linq_query = true +resharper_align_multiline_argument = true +resharper_align_multiline_binary_expressions_chain = false +resharper_align_multiline_extends_list = true +resharper_align_multiline_for_stmt = true +resharper_align_multiline_parameter = true +resharper_align_multiple_declaration = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_align_tuple_components = true +resharper_autodetect_indent_settings = true +resharper_blank_lines_after_block_statements = 0 +resharper_blank_lines_around_single_line_auto_property = 1 +resharper_blank_lines_around_single_line_local_method = 1 +resharper_blank_lines_around_single_line_property = 1 +resharper_braces_redundant = true +resharper_csharp_align_first_arg_by_paren = true +resharper_csharp_allow_far_alignment = true +resharper_csharp_blank_lines_around_single_line_field = 1 +resharper_csharp_blank_lines_around_single_line_invocable = 1 +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_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_indent_nested_fixed_stmt = true +resharper_indent_nested_foreach_stmt = true +resharper_indent_nested_for_stmt = true +resharper_indent_nested_lock_stmt = true +resharper_indent_nested_usings_stmt = true +resharper_indent_nested_while_stmt = true +resharper_indent_preprocessor_region = no_indent +resharper_keep_existing_arrangement = false +resharper_max_array_initializer_elements_on_line = 1 +resharper_max_attribute_length_for_same_line = 0 +resharper_max_enum_members_on_line = 1 +resharper_max_initializer_elements_on_line = 1 +resharper_nested_ternary_style = expanded +resharper_object_creation_when_type_not_evident = target_typed +resharper_outdent_statement_labels = true +resharper_parentheses_non_obvious_operations = none, null_coalescing, conditional, conditional_or, conditional_and, bitwise, bitwise_inclusive_or, range, bitwise_exclusive_or, equality, relational, shift, arithmetic, additive, multiplicative, bitwise_and +resharper_parentheses_same_type_operations = true +resharper_place_attribute_on_same_line = false +resharper_place_expr_accessor_on_single_line = true +resharper_place_expr_method_on_single_line = true +resharper_place_expr_property_on_single_line = true +resharper_place_linq_into_on_new_line = false +resharper_place_simple_initializer_on_single_line = false +resharper_place_simple_property_pattern_on_single_line = false +resharper_show_autodetect_configure_formatting_tip = false +resharper_static_members_qualify_members = none, field, property, event, method +resharper_static_members_qualify_with = current_type +resharper_trailing_comma_in_multiline_lists = true +resharper_trailing_comma_in_singleline_lists = true +resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_always +resharper_wrap_chained_binary_expressions = chop_if_long +resharper_wrap_chained_method_calls = chop_if_long +resharper_wrap_linq_expressions = chop_always +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_wrap_property_pattern = chop_always +resharper_wrap_text = false +resharper_xmldoc_allow_far_alignment = true +resharper_xmldoc_attribute_indent = align_by_first_attribute +resharper_xmldoc_keep_user_linebreaks = false +resharper_xmldoc_pi_attribute_style = on_single_line +resharper_xmldoc_space_before_self_closing = false +resharper_xmldoc_wrap_tags_and_pi = false + +# ReSharper inspection severities +resharper_arguments_style_anonymous_function_highlighting = warning +resharper_arguments_style_literal_highlighting = warning +resharper_arguments_style_named_expression_highlighting = warning +resharper_arguments_style_other_highlighting = warning +resharper_arguments_style_string_literal_highlighting = warning +resharper_arrange_accessor_owner_body_highlighting = warning +resharper_arrange_attributes_highlighting = warning +resharper_arrange_constructor_or_destructor_body_highlighting = warning +resharper_arrange_default_value_when_type_evident_highlighting = warning +resharper_arrange_default_value_when_type_not_evident_highlighting = warning +resharper_arrange_local_function_body_highlighting = warning +resharper_arrange_method_or_operator_body_highlighting = warning +resharper_arrange_missing_parentheses_highlighting = warning +resharper_arrange_object_creation_when_type_evident_highlighting = warning +resharper_arrange_object_creation_when_type_not_evident_highlighting = warning +resharper_arrange_redundant_parentheses_highlighting = warning +resharper_arrange_static_member_qualifier_highlighting = warning +resharper_arrange_trailing_comma_in_multiline_lists_highlighting = warning +resharper_arrange_trailing_comma_in_singleline_lists_highlighting = warning +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting = warning +resharper_member_can_be_made_static_local_highlighting = none +resharper_member_can_be_private_global_highlighting = none +resharper_parameter_hides_member_highlighting = none +resharper_suggest_discard_declaration_var_style_highlighting = warning +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..daadb22 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +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/Modding/Mod.cs b/Modding/Mod.cs new file mode 100644 index 0000000..2595910 --- /dev/null +++ b/Modding/Mod.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Modding.Patching; +using Godot.Serialization; + +namespace Godot.Modding +{ + /// + /// Represents a modular component loaded at runtime, with its own assemblies, resource packs, and data. + /// + [PublicAPI] + 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; + this.LoadResources(); + this.Data = this.LoadData(); + this.Patches = this.LoadPatches(); + this.Assemblies = this.LoadAssemblies(); + } + + /// + /// 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 if any, combined into a single . + /// + public XmlDocument? Data + { + get; + } + + /// + /// The patches applied by the . + /// + public IEnumerable Patches + { + get; + } + + private IEnumerable LoadAssemblies() + { + string assembliesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Assemblies"; + + return System.IO.Directory.Exists(assembliesPath) + ? System.IO.Directory.GetFiles(assembliesPath, "*dll", SearchOption.AllDirectories).Select(Assembly.LoadFile) + : Enumerable.Empty(); + } + + private XmlDocument? LoadData() + { + IEnumerable documents = this.LoadDocuments().ToArray(); + if (!documents.Any()) + { + return null; + } + + XmlDocument data = new(); + + XmlElement root = data.CreateElement("Data"); + data.AppendChild(root); + data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), root); + + documents + .SelectMany(document => document.Cast()) + .Where(node => node is not XmlDeclaration) + .ForEach(node => root.AppendChild(data.ImportNode(node, true))); + + return data; + } + + private IEnumerable LoadDocuments() + { + string dataPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Data"; + + if (!System.IO.Directory.Exists(dataPath)) + { + yield break; + } + + foreach (string xmlPath in System.IO.Directory.GetFiles(dataPath, "*.xml", SearchOption.AllDirectories)) + { + XmlDocument document = new(); + document.Load(xmlPath); + yield return document; + } + } + + private void LoadResources() + { + string resourcesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Resources"; + + if (!System.IO.Directory.Exists(resourcesPath)) + { + return; + } + + 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 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)) + { + document.Load(patchPath); + if (document.DocumentElement is not null) + { + yield return serializer.Deserialize(document.DocumentElement) as IPatch ?? throw new ModLoadException(this.Meta.Directory, $"Invalid patch at {patchPath}"); + } + } + } + + /// + /// Represents the metadata of a , such as its unique ID, name, author, load order, etc. + /// + [PublicAPI] + public sealed record Metadata + { + [UsedImplicitly] + private Metadata() + { + } + + /// + /// The directory where the was loaded from. + /// + [Serialize] + public string Directory + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The unique ID of the . + /// + [Serialize] + public string Id + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The name of the . + /// + [Serialize] + public string Name + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The individual or group that created the . + /// + [Serialize] + public string Author + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The unique IDs of all other s that the depends on. + /// + public IEnumerable Dependencies + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + /// + /// The unique IDs of all other s that should be loaded before the . + /// + public IEnumerable Before + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + /// + /// The unique IDs of all other s that should be loaded after the . + /// + public IEnumerable After + { + get; + [UsedImplicitly] + private set; + } = Enumerable.Empty(); + + /// + /// The unique IDs of all other s that are incompatible with the . + /// + public IEnumerable Incompatible + { + get; + [UsedImplicitly] + 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) + { + 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 + { + 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); + + return new Serializer().Deserialize(document.DocumentElement)!; + } + catch (Exception exception) when (exception is not ModLoadException) + { + throw new ModLoadException(directoryPath, exception); + } + } + + [AfterDeserialization] + private void 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.Incompatible + .Prepend(this.Id) + .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(); + if (invalidLoadOrder || invalidDependencies) + { + throw new ModLoadException(this.Directory, "Invalid metadata"); + } + } + } + } +} \ No newline at end of file diff --git a/Modding/ModLoadException.cs b/Modding/ModLoadException.cs new file mode 100644 index 0000000..40dcbb7 --- /dev/null +++ b/Modding/ModLoadException.cs @@ -0,0 +1,28 @@ +using System; + +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}") + { + } + } +} \ No newline at end of file diff --git a/Modding/ModLoader.cs b/Modding/ModLoader.cs new file mode 100644 index 0000000..9396a5d --- /dev/null +++ b/Modding/ModLoader.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Utility; +using Godot.Utility.Extensions; + +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 OrderedDictionary loadedMods = new(); + + /// + /// All the s that have been loaded at runtime. + /// + public static IReadOnlyDictionary LoadedMods + { + get + { + return ModLoader.loadedMods; + } + } + + /// + /// Loads a from , applies its patches if any, 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, 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); + } + + return mod; + } + + /// + /// Loads s from , applies their patches if any, 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, bool executeAssemblies = true) + { + // Cache XML data of loaded mods for repeat enumeration later + List data = ModLoader.LoadedMods.Values + .Select(mod => mod.Data?.DocumentElement) + .NotNull() + .ToList(); + + List mods = new(); + foreach (Mod.Metadata metadata in ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths)))) + { + // Load mod + Mod mod = new(metadata); + mods.Add(mod); + ModLoader.loadedMods.Add(mod.Meta.Id, 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)); + } + // Execute mod assemblies + if (executeAssemblies) + { + mods.ForEach(ModLoader.StartupMod); + } + return mods; + } + + private static void StartupMod(Mod mod) + { + // Invoke all static methods annotated with [Startup] along with the supplied parameters (if any) + mod.Assemblies + .SelectMany(assembly => assembly.GetTypes()) + .SelectMany(type => type.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + .Select(method => (method, method.GetCustomAttribute())) + .Where(pair => pair.Item2 is not null) + .ForEach(pair => pair.Item1.Invoke(null, pair.Item2.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 = metadata.Incompatible + .Select(id => loadedMetadata.GetValueOrDefault(id)) + .NotNull() + .Concat(loadedMetadata.Values.Where(loaded => loaded.Incompatible.Contains(metadata.Id))); + 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 + IEnumerable invalidMetadata = loadedMetadata.Values + .Where(metadata => metadata.Dependencies + .Select(dependency => loadedMetadata.TryGetValue(dependency, out _)) + .Any(dependency => !dependency)); + foreach (Mod.Metadata metadata in invalidMetadata) + { + 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/Modding/ModStartupAttribute.cs b/Modding/ModStartupAttribute.cs new file mode 100644 index 0000000..402b127 --- /dev/null +++ b/Modding/ModStartupAttribute.cs @@ -0,0 +1,38 @@ +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 + { + /// + /// 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; + } + } +} \ No newline at end of file diff --git a/Modding/Patching/AttributeRemovePatch.cs b/Modding/Patching/AttributeRemovePatch.cs new file mode 100644 index 0000000..3e3f7c0 --- /dev/null +++ b/Modding/Patching/AttributeRemovePatch.cs @@ -0,0 +1,52 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that removes an attribute from an . + /// + [PublicAPI] + public class AttributeRemovePatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The name of the attribute to remove. + public AttributeRemovePatch(string attribute) + { + this.Attribute = attribute; + } + + [UsedImplicitly] + private AttributeRemovePatch() + { + } + + /// + /// The name of the attribute to remove. + /// + [Serialize] + public string Attribute + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Removes from if is an . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + if (data is XmlElement element) + { + element.RemoveAttribute(this.Attribute); + } + } + } +} \ No newline at end of file diff --git a/Modding/Patching/AttributeSetPatch.cs b/Modding/Patching/AttributeSetPatch.cs new file mode 100644 index 0000000..44784cc --- /dev/null +++ b/Modding/Patching/AttributeSetPatch.cs @@ -0,0 +1,65 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that sets the value of an attribute in an . + /// + [PublicAPI] + public class AttributeSetPatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The name of the attribute to add/set. + /// The value of the attribute to add/set. + public AttributeSetPatch(string attribute, string value) + { + this.Attribute = attribute; + this.Value = value; + } + + [UsedImplicitly] + private AttributeSetPatch() + { + } + + /// + /// The name of the attribute to add/set. + /// + [Serialize] + public string Attribute + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The value of the attribute to add/set. + /// + [Serialize] + public string Value + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Sets to on if is an . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + if (data is XmlElement element) + { + element.SetAttribute(this.Attribute, this.Value); + } + } + } +} \ No newline at end of file diff --git a/Modding/Patching/ConditionalPatch.cs b/Modding/Patching/ConditionalPatch.cs new file mode 100644 index 0000000..60157ff --- /dev/null +++ b/Modding/Patching/ConditionalPatch.cs @@ -0,0 +1,75 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Modding.Patching.Conditions; +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that can apply either a "success" or a "failure" patch to an depending on a condition. + /// + [PublicAPI] + public class ConditionalPatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The condition to check. + /// The patch to apply if succeeds. + /// The patch to apply if fails. + public ConditionalPatch(ICondition condition, IPatch? success, IPatch? failure) + { + this.Condition = condition; + this.Success = success; + this.Failure = failure; + } + + [UsedImplicitly] + private ConditionalPatch() + { + } + + /// + /// The condition to check when applying the . + /// + [Serialize] + public ICondition Condition + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The applied if succeeds. + /// + public IPatch? Success + { + get; + [UsedImplicitly] + private set; + } + + /// + /// The applied if fails. + /// + public IPatch? Failure + { + get; + [UsedImplicitly] + private set; + } + + /// + /// Applies either or to depending on . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + IPatch? patch = this.Condition.Check(data) ? this.Success : this.Failure; + patch?.Apply(data); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/Conditions/AndCondition.cs b/Modding/Patching/Conditions/AndCondition.cs new file mode 100644 index 0000000..a99c9b7 --- /dev/null +++ b/Modding/Patching/Conditions/AndCondition.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching.Conditions +{ + /// + /// An that succeeds if all of a specified sequence of conditions succeed. + /// + [PublicAPI] + public class AndCondition : ICondition + { + /// + /// Initialises a new with the specified parameters. + /// + /// The conditions to check. + public AndCondition(IEnumerable conditions) + { + this.Conditions = conditions; + } + + [UsedImplicitly] + private AndCondition() + { + } + + /// + /// The conditions to check. + /// + [Serialize] + public IEnumerable Conditions + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Succeeds if all conditions in succeed. + /// + /// The to apply the patch on. + /// if all conditions in succeed, else . + public bool Check(XmlNode data) + { + return this.Conditions.All(condition => condition.Check(data)); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/Conditions/ICondition.cs b/Modding/Patching/Conditions/ICondition.cs new file mode 100644 index 0000000..e1aa214 --- /dev/null +++ b/Modding/Patching/Conditions/ICondition.cs @@ -0,0 +1,17 @@ +using System.Xml; + +namespace Godot.Modding.Patching.Conditions +{ + /// + /// Represents a condition that can be tested on an . + /// + public interface ICondition + { + /// + /// Checks if a 's XML data satisfies the . + /// + /// The to apply the patch on. + /// if the condition succeeds, else . + public bool Check(XmlNode data); + } +} \ No newline at end of file diff --git a/Modding/Patching/Conditions/ModLoadedCondition.cs b/Modding/Patching/Conditions/ModLoadedCondition.cs new file mode 100644 index 0000000..f29c7b7 --- /dev/null +++ b/Modding/Patching/Conditions/ModLoadedCondition.cs @@ -0,0 +1,50 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching.Conditions +{ + /// + /// An that checks if a particular has been loaded. + /// + [PublicAPI] + public class ModLoadedCondition : ICondition + { + /// + /// Initialises a new with the specified parameters. + /// + /// The ID of the to check for. + public ModLoadedCondition(string modId) + { + this.ModId = modId; + } + + [UsedImplicitly] + private ModLoadedCondition() + { + } + + /// + /// The ID of the to check for. + /// + [Serialize] + public string ModId + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Checks if any loaded 's ID equals . + /// + /// The to apply the patch on. This is not used by the . + /// if the given by is loaded, else . + public bool Check(XmlNode data) + { + return ModLoader.LoadedMods.TryGetValue(this.ModId, out _); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/Conditions/NodeExistsCondition.cs b/Modding/Patching/Conditions/NodeExistsCondition.cs new file mode 100644 index 0000000..6e5b483 --- /dev/null +++ b/Modding/Patching/Conditions/NodeExistsCondition.cs @@ -0,0 +1,50 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching.Conditions +{ + /// + /// An that checks if a descendant of an matching an XPath string exists. + /// + [PublicAPI] + public class NodeExistsCondition : ICondition + { + /// + /// Initialises a new with the specified parameters. + /// + /// The XPath string to check. + public NodeExistsCondition(string xPath) + { + this.XPath = xPath; + } + + [UsedImplicitly] + private NodeExistsCondition() + { + } + + /// + /// The XPath string to use when checking if an exists. + /// + [Serialize] + public string XPath + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Checks if any s match the XPath given by . + /// + /// The to apply the patch on. + /// if selects at least one , else . + public bool Check(XmlNode data) + { + return data.SelectSingleNode(this.XPath) is not null; + } + } +} \ No newline at end of file diff --git a/Modding/Patching/Conditions/NotCondition.cs b/Modding/Patching/Conditions/NotCondition.cs new file mode 100644 index 0000000..9c34630 --- /dev/null +++ b/Modding/Patching/Conditions/NotCondition.cs @@ -0,0 +1,50 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching.Conditions +{ + /// + /// An that succeeds if a specified condition fails. + /// + [PublicAPI] + public class NotCondition : ICondition + { + /// + /// Initialises a new with the specified parameters. + /// + /// The to check. + public NotCondition(ICondition condition) + { + this.Condition = condition; + } + + [UsedImplicitly] + private NotCondition() + { + } + + /// + /// The condition to check. + /// + [Serialize] + public ICondition Condition + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Succeeds if fails. + /// + /// The to apply the patch on. + /// if fails, else . + public bool Check(XmlNode data) + { + return !this.Condition.Check(data); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/Conditions/OrCondition.cs b/Modding/Patching/Conditions/OrCondition.cs new file mode 100644 index 0000000..ac1ed10 --- /dev/null +++ b/Modding/Patching/Conditions/OrCondition.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching.Conditions +{ + /// + /// An that succeeds if at least one of a specified sequence of conditions succeed. + /// + [PublicAPI] + public class OrCondition : ICondition + { + /// + /// Initialises a new with the specified parameters. + /// + /// The conditions to check. + public OrCondition(IEnumerable conditions) + { + this.Conditions = conditions; + } + + [UsedImplicitly] + private OrCondition() + { + } + + /// + /// The conditions to check. + /// + public IEnumerable Conditions + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Succeeds if at least one condition in succeeds. + /// + /// The to apply the patch on. + /// if at least one condition in succeeds, else . + public bool Check(XmlNode data) + { + return this.Conditions.Any(condition => condition.Check(data)); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/IPatch.cs b/Modding/Patching/IPatch.cs new file mode 100644 index 0000000..794087e --- /dev/null +++ b/Modding/Patching/IPatch.cs @@ -0,0 +1,16 @@ +using System.Xml; + +namespace Godot.Modding.Patching +{ + /// + /// Represents a modification that can be applied to the XML data of a . + /// + public interface IPatch + { + /// + /// Applies the patch to . + /// + /// The to apply the patch on. + public void Apply(XmlNode data); + } +} \ No newline at end of file diff --git a/Modding/Patching/LogPatch.cs b/Modding/Patching/LogPatch.cs new file mode 100644 index 0000000..c8b8fe6 --- /dev/null +++ b/Modding/Patching/LogPatch.cs @@ -0,0 +1,50 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that logs the state of any s before and after applying a separate patch to them. + /// + [PublicAPI] + public class LogPatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The patch to apply before and after logging the . + public LogPatch(IPatch patch) + { + this.Patch = patch; + } + + private LogPatch() + { + } + + /// + /// The patch to apply before and after logging the . + /// + [Serialize] + public IPatch Patch + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Logs the XML string representation of before and after applying to it. + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + Log.Write($"Before: {data.OuterXml}"); + this.Patch.Apply(data); + Log.Write($"After: {data.OuterXml}"); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/MultiPatch.cs b/Modding/Patching/MultiPatch.cs new file mode 100644 index 0000000..911613c --- /dev/null +++ b/Modding/Patching/MultiPatch.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that applies multiple patches in sequence onto the same . + /// + [PublicAPI] + public class MultiPatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The patches to apply in sequence. + public MultiPatch(IEnumerable patches) + { + this.Patches = patches; + } + + [UsedImplicitly] + private MultiPatch() + { + } + + /// + /// The patches to apply in sequence. + /// + [Serialize] + public IEnumerable Patches + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Applies all patches in to . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + this.Patches.ForEach(patch => patch.Apply(data)); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/NodeAddPatch.cs b/Modding/Patching/NodeAddPatch.cs new file mode 100644 index 0000000..9734233 --- /dev/null +++ b/Modding/Patching/NodeAddPatch.cs @@ -0,0 +1,73 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that adds an as a child to another . + /// + [PublicAPI] + public class NodeAddPatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The to add as a child. + /// The index to insert at, or -1 if it should simply be appended to the end. + public NodeAddPatch(XmlNode value, int index = -1) + { + this.Value = value; + this.Index = index; + } + + [UsedImplicitly] + private NodeAddPatch() + { + } + + /// + /// The to add as a child. + /// + [Serialize] + public XmlNode Value + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The index to insert at. + /// + public int Index + { + get; + [UsedImplicitly] + private set; + } = -1; + + /// + /// Adds as a child to at the index specified by . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + XmlNode value = data.OwnerDocument!.ImportNode(this.Value, true); + switch (this.Index) + { + case < 0: + data.AppendChild(value); + break; + case 0: + data.PrependChild(value); + break; + default: + data.InsertBefore(value, data.ChildNodes[this.Index]); + break; + } + } + } +} \ No newline at end of file diff --git a/Modding/Patching/NodeRemovePatch.cs b/Modding/Patching/NodeRemovePatch.cs new file mode 100644 index 0000000..00c95ab --- /dev/null +++ b/Modding/Patching/NodeRemovePatch.cs @@ -0,0 +1,22 @@ +using System.Xml; + +using JetBrains.Annotations; + +namespace Godot.Modding.Patching +{ + /// + /// An that removes the it is applied on. + /// + [PublicAPI] + public class NodeRemovePatch : IPatch + { + /// + /// Removes from its parent . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + data.ParentNode!.RemoveChild(data); + } + } +} \ No newline at end of file diff --git a/Modding/Patching/NodeReplacePatch.cs b/Modding/Patching/NodeReplacePatch.cs new file mode 100644 index 0000000..b0aab2b --- /dev/null +++ b/Modding/Patching/NodeReplacePatch.cs @@ -0,0 +1,60 @@ +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that replaces an with another one. + /// + [PublicAPI] + public class NodeReplacePatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// The to add in place of the removed . + public NodeReplacePatch(XmlNode replacement) + { + this.Replacement = replacement; + } + + [UsedImplicitly] + private NodeReplacePatch() + { + } + + /// + /// The to add in place of the removed . + /// + [Serialize] + public XmlNode Replacement + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Removes and replaces it with . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + XmlNode? previous = data.PreviousSibling; + XmlNode parent = data.ParentNode!; + XmlNode replacement = data.OwnerDocument!.ImportNode(this.Replacement, true); + parent.RemoveChild(data); + if (previous is null) + { + parent.PrependChild(replacement); + } + else + { + parent.InsertAfter(replacement, previous); + } + } + } +} \ No newline at end of file diff --git a/Modding/Patching/TargetedPatch.cs b/Modding/Patching/TargetedPatch.cs new file mode 100644 index 0000000..8cc0ea4 --- /dev/null +++ b/Modding/Patching/TargetedPatch.cs @@ -0,0 +1,65 @@ +using System.Linq; +using System.Xml; + +using JetBrains.Annotations; + +using Godot.Serialization; + +namespace Godot.Modding.Patching +{ + /// + /// An that selects descendants of an according to an XPath string and applies a separate patch on them. + /// + [PublicAPI] + public class TargetedPatch : IPatch + { + /// + /// Initialises a new with the specified parameters. + /// + /// An XPath string that specifies descendant s to apply on. + /// The patch to apply on all s selected by . + public TargetedPatch(string targets, IPatch patch) + { + this.Targets = targets; + this.Patch = patch; + } + + [UsedImplicitly] + private TargetedPatch() + { + } + + /// + /// The targets to apply the on, in the form of an XPath. + /// + [Serialize] + public string Targets + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// The patch to apply on s that match . + /// + [Serialize] + public IPatch Patch + { + get; + [UsedImplicitly] + private set; + } = null!; + + /// + /// Applies to all s under that match . + /// + /// The to apply the patch on. + public void Apply(XmlNode data) + { + data.SelectNodes(this.Targets)? + .Cast() + .ForEach(this.Patch.Apply); + } + } +} \ No newline at end of file diff --git a/Modot.csproj b/Modot.csproj new file mode 100644 index 0000000..0125df0 --- /dev/null +++ b/Modot.csproj @@ -0,0 +1,31 @@ + + + default + enable + netstandard2.1 + Godot + true + + true + true + 2.0.2 + 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 + LICENSE + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..73e2e7a --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Modot + +**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 +- Patch XML data of other loaded mods without executing code +- 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). + +# 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. As mentioned earlier though, **it is not Modot's responsibility to implement such checks**. \ No newline at end of file diff --git a/Utility/ErrorException.cs b/Utility/ErrorException.cs new file mode 100644 index 0000000..46743e1 --- /dev/null +++ b/Utility/ErrorException.cs @@ -0,0 +1,30 @@ +using System; + +using JetBrains.Annotations; + +namespace Godot.Utility +{ + /// + /// A wrapper around to make it throwable. + /// + [PublicAPI] + public class ErrorException : Exception + { + /// + /// Initialises a new with the specified . + /// + /// The Godot . + public ErrorException(Error error) : base(error.ToString()) + { + this.Error = error; + } + + /// + /// The Godot . + /// + public Error Error + { + get; + } + } +} \ No newline at end of file diff --git a/Utility/Extensions/DirectoryExtensions.cs b/Utility/Extensions/DirectoryExtensions.cs new file mode 100644 index 0000000..bb77db6 --- /dev/null +++ b/Utility/Extensions/DirectoryExtensions.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +using JetBrains.Annotations; + +namespace Godot.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. + /// An array of the paths of all files that were copied from to . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string[] CopyContents(this Directory directory, string from, string to, bool recursive = false) + { + return directory.CopyContentsLazy(from, to, recursive).ToArray(); + } + + /// + /// 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).Throw(); + 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).Throw(); + return recursiveDirectory + .GetDirectories(true) + .Prepend(path); + }) + .ToArray() + : directory + .GetElementsNonRecursive(false) + .ToArray(); + } + + private static IEnumerable GetElementsNonRecursive(this Directory directory, bool trueIfFiles) + { + directory.ListDirBegin(true).Throw(); + while (true) + { + string next = directory.GetNext(); + if (next is "") + { + yield break; + } + // Continue if the current element is a file or directory depending on which one is being queried + if (directory.CurrentIsDir() == trueIfFiles) + { + continue; + } + string current = directory.GetCurrentDir(); + yield return current.EndsWith("/") ? $"{current}{next}" : $"{current}/{next}"; + } + } + + private static IEnumerable CopyContentsLazy(this Directory directory, string from, string to, bool recursive = false) + { + directory.Open(from).Throw(); + + // Create destination directory if it doesn't already exist + directory.MakeDirRecursive(to).Throw(); + + // Replace only the first instance of the destination directory in file and subdirectory paths using regex (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.GetElementsNonRecursive(true)) + { + string toFile = fromReplacement.Replace(fromFile, to, 1); + directory.Copy(fromFile, toFile).Throw(); + yield return toFile; + } + + if (!recursive) + { + yield break; + } + + // Copy all files recursively + foreach (string fromSubDirectory in directory.GetDirectories(true)) + { + string toSubDirectory = fromReplacement.Replace(fromSubDirectory, to, 1); + directory.MakeDirRecursive(toSubDirectory).Throw(); + + using Directory innerDirectory = new(); + innerDirectory.Open(fromSubDirectory).Throw(); + foreach (string fromFile in innerDirectory.GetElementsNonRecursive(true)) + { + string toFile = fromReplacement.Replace(fromFile, to, 1); + directory.Copy(fromFile, toFile).Throw(); + yield return toFile; + } + } + } + } +} \ No newline at end of file diff --git a/Utility/Extensions/EnumerableExtensions.cs b/Utility/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..58fd6b4 --- /dev/null +++ b/Utility/Extensions/EnumerableExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using JetBrains.Annotations; + +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 . + [MustUseReturnValue] + 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 diff --git a/Utility/Extensions/ErrorExtensions.cs b/Utility/Extensions/ErrorExtensions.cs new file mode 100644 index 0000000..43446f7 --- /dev/null +++ b/Utility/Extensions/ErrorExtensions.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; + +using JetBrains.Annotations; + +namespace Godot.Utility.Extensions +{ + /// + /// Contains extension methods for . + /// + [PublicAPI] + public static class ErrorExtensions + { + /// + /// Checks if indicates success. + /// + /// The to check. + /// if is , else . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Success(this Error error) + { + return error is Error.Ok; + } + + /// + /// Throws an exception if indicates failure. + /// + /// The to check. + /// Thrown if is not . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Throw(this Error error) + { + if (error is not Error.Ok) + { + throw new ErrorException(error); + } + } + } +} \ No newline at end of file From e8c64db420c54c10d4b18595f06e40cebeda9c36 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 21 Sep 2022 13:55:28 +0100 Subject: [PATCH 05/32] Create GDScript branch --- .editorconfig | 196 ----------- .gitignore | 8 +- Modding/Mod.cs | 303 ------------------ Modding/ModLoadException.cs | 28 -- Modding/ModLoader.cs | 182 ----------- Modding/ModStartupAttribute.cs | 38 --- Modding/Patching/AttributeRemovePatch.cs | 52 --- Modding/Patching/AttributeSetPatch.cs | 65 ---- Modding/Patching/ConditionalPatch.cs | 75 ----- Modding/Patching/Conditions/AndCondition.cs | 52 --- Modding/Patching/Conditions/ICondition.cs | 17 - .../Patching/Conditions/ModLoadedCondition.cs | 50 --- .../Conditions/NodeExistsCondition.cs | 50 --- Modding/Patching/Conditions/NotCondition.cs | 50 --- Modding/Patching/Conditions/OrCondition.cs | 51 --- Modding/Patching/IPatch.cs | 16 - Modding/Patching/LogPatch.cs | 50 --- Modding/Patching/MultiPatch.cs | 51 --- Modding/Patching/NodeAddPatch.cs | 73 ----- Modding/Patching/NodeRemovePatch.cs | 22 -- Modding/Patching/NodeReplacePatch.cs | 59 ---- Modding/Patching/TargetedPatch.cs | 65 ---- Modot.csproj | 31 -- Utility/ErrorException.cs | 30 -- Utility/Extensions/DirectoryExtensions.cs | 157 --------- Utility/Extensions/EnumerableExtensions.cs | 70 ---- Utility/Extensions/ErrorExtensions.cs | 39 --- 27 files changed, 4 insertions(+), 1876 deletions(-) delete mode 100644 .editorconfig delete mode 100644 Modding/Mod.cs delete mode 100644 Modding/ModLoadException.cs delete mode 100644 Modding/ModLoader.cs delete mode 100644 Modding/ModStartupAttribute.cs delete mode 100644 Modding/Patching/AttributeRemovePatch.cs delete mode 100644 Modding/Patching/AttributeSetPatch.cs delete mode 100644 Modding/Patching/ConditionalPatch.cs delete mode 100644 Modding/Patching/Conditions/AndCondition.cs delete mode 100644 Modding/Patching/Conditions/ICondition.cs delete mode 100644 Modding/Patching/Conditions/ModLoadedCondition.cs delete mode 100644 Modding/Patching/Conditions/NodeExistsCondition.cs delete mode 100644 Modding/Patching/Conditions/NotCondition.cs delete mode 100644 Modding/Patching/Conditions/OrCondition.cs delete mode 100644 Modding/Patching/IPatch.cs delete mode 100644 Modding/Patching/LogPatch.cs delete mode 100644 Modding/Patching/MultiPatch.cs delete mode 100644 Modding/Patching/NodeAddPatch.cs delete mode 100644 Modding/Patching/NodeRemovePatch.cs delete mode 100644 Modding/Patching/NodeReplacePatch.cs delete mode 100644 Modding/Patching/TargetedPatch.cs delete mode 100644 Modot.csproj delete mode 100644 Utility/ErrorException.cs delete mode 100644 Utility/Extensions/DirectoryExtensions.cs delete mode 100644 Utility/Extensions/EnumerableExtensions.cs delete mode 100644 Utility/Extensions/ErrorExtensions.cs diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index f879977..0000000 --- a/.editorconfig +++ /dev/null @@ -1,196 +0,0 @@ - -[*] -charset = utf-8 -end_of_line = crlf -trim_trailing_whitespace = false -insert_final_newline = false -indent_style = space -indent_size = 4 - -# Microsoft .NET properties -csharp_preferred_modifier_order = public, protected, internal, private, static, extern, sealed, abstract, virtual, override, new, readonly, async, unsafe, volatile:warning -csharp_prefer_braces = true:warning -csharp_preserve_single_line_blocks = false -csharp_style_var_elsewhere = false:warning -csharp_style_var_for_built_in_types = false:warning -csharp_style_var_when_type_is_apparent = false:warning -dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined -dotnet_naming_rule.constants_rule.severity = warning -dotnet_naming_rule.constants_rule.style = upper_camel_case_style -dotnet_naming_rule.constants_rule.symbols = constants_symbols -dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined -dotnet_naming_rule.private_constants_rule.severity = warning -dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style -dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols -dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined -dotnet_naming_rule.private_instance_fields_rule.severity = warning -dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style -dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols -dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined -dotnet_naming_rule.private_static_fields_rule.severity = warning -dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style -dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols -dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined -dotnet_naming_rule.private_static_readonly_rule.severity = warning -dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style -dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols -dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined -dotnet_naming_rule.public_fields_rule.severity = warning -dotnet_naming_rule.public_fields_rule.style = lower_camel_case_style -dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols -dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined -dotnet_naming_rule.static_readonly_rule.severity = warning -dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style -dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols -dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True -dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field -dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef -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_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 -dotnet_naming_symbols.constants_symbols.applicable_kinds = field -dotnet_naming_symbols.constants_symbols.required_modifiers = const -dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field -dotnet_naming_symbols.private_constants_symbols.required_modifiers = const -dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field -dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field -dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static -dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field -dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly -dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field -dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field -dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly -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_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 -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none -dotnet_style_predefined_type_for_locals_parameters_members = true:warning -dotnet_style_predefined_type_for_member_access = false:warning -dotnet_style_qualification_for_event = true:warning -dotnet_style_qualification_for_field = true:warning -dotnet_style_qualification_for_method = true:warning -dotnet_style_qualification_for_property = true:warning -dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning - -# ReSharper properties -resharper_accessor_owner_body = accessors_with_block_body -resharper_align_linq_query = true -resharper_align_multiline_argument = true -resharper_align_multiline_binary_expressions_chain = false -resharper_align_multiline_extends_list = true -resharper_align_multiline_for_stmt = true -resharper_align_multiline_parameter = true -resharper_align_multiple_declaration = true -resharper_align_multline_type_parameter_constrains = true -resharper_align_multline_type_parameter_list = true -resharper_align_tuple_components = true -resharper_autodetect_indent_settings = true -resharper_blank_lines_after_block_statements = 0 -resharper_blank_lines_around_single_line_auto_property = 1 -resharper_blank_lines_around_single_line_local_method = 1 -resharper_blank_lines_around_single_line_property = 1 -resharper_braces_redundant = true -resharper_csharp_align_first_arg_by_paren = true -resharper_csharp_allow_far_alignment = true -resharper_csharp_blank_lines_around_single_line_field = 1 -resharper_csharp_blank_lines_around_single_line_invocable = 1 -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_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_indent_nested_fixed_stmt = true -resharper_indent_nested_foreach_stmt = true -resharper_indent_nested_for_stmt = true -resharper_indent_nested_lock_stmt = true -resharper_indent_nested_usings_stmt = true -resharper_indent_nested_while_stmt = true -resharper_indent_preprocessor_region = no_indent -resharper_keep_existing_arrangement = false -resharper_max_array_initializer_elements_on_line = 1 -resharper_max_attribute_length_for_same_line = 0 -resharper_max_enum_members_on_line = 1 -resharper_max_initializer_elements_on_line = 1 -resharper_nested_ternary_style = expanded -resharper_object_creation_when_type_not_evident = target_typed -resharper_outdent_statement_labels = true -resharper_parentheses_non_obvious_operations = none, null_coalescing, conditional, conditional_or, conditional_and, bitwise, bitwise_inclusive_or, range, bitwise_exclusive_or, equality, relational, shift, arithmetic, additive, multiplicative, bitwise_and -resharper_parentheses_same_type_operations = true -resharper_place_attribute_on_same_line = false -resharper_place_expr_accessor_on_single_line = true -resharper_place_expr_method_on_single_line = true -resharper_place_expr_property_on_single_line = true -resharper_place_linq_into_on_new_line = false -resharper_place_simple_initializer_on_single_line = false -resharper_place_simple_property_pattern_on_single_line = false -resharper_show_autodetect_configure_formatting_tip = false -resharper_static_members_qualify_members = none, field, property, event, method -resharper_static_members_qualify_with = current_type -resharper_trailing_comma_in_multiline_lists = true -resharper_trailing_comma_in_singleline_lists = true -resharper_use_indent_from_vs = false -resharper_wrap_array_initializer_style = chop_always -resharper_wrap_chained_binary_expressions = chop_if_long -resharper_wrap_chained_method_calls = chop_if_long -resharper_wrap_linq_expressions = chop_always -resharper_wrap_object_and_collection_initializer_style = chop_always -resharper_wrap_property_pattern = chop_always -resharper_wrap_text = false -resharper_xmldoc_allow_far_alignment = true -resharper_xmldoc_attribute_indent = align_by_first_attribute -resharper_xmldoc_keep_user_linebreaks = false -resharper_xmldoc_pi_attribute_style = on_single_line -resharper_xmldoc_space_before_self_closing = false -resharper_xmldoc_wrap_tags_and_pi = false - -# ReSharper inspection severities -resharper_arguments_style_anonymous_function_highlighting = warning -resharper_arguments_style_literal_highlighting = warning -resharper_arguments_style_named_expression_highlighting = warning -resharper_arguments_style_other_highlighting = warning -resharper_arguments_style_string_literal_highlighting = warning -resharper_arrange_accessor_owner_body_highlighting = warning -resharper_arrange_attributes_highlighting = warning -resharper_arrange_constructor_or_destructor_body_highlighting = warning -resharper_arrange_default_value_when_type_evident_highlighting = warning -resharper_arrange_default_value_when_type_not_evident_highlighting = warning -resharper_arrange_local_function_body_highlighting = warning -resharper_arrange_method_or_operator_body_highlighting = warning -resharper_arrange_missing_parentheses_highlighting = warning -resharper_arrange_object_creation_when_type_evident_highlighting = warning -resharper_arrange_object_creation_when_type_not_evident_highlighting = warning -resharper_arrange_redundant_parentheses_highlighting = warning -resharper_arrange_static_member_qualifier_highlighting = warning -resharper_arrange_trailing_comma_in_multiline_lists_highlighting = warning -resharper_arrange_trailing_comma_in_singleline_lists_highlighting = warning -resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting = warning -resharper_member_can_be_made_static_local_highlighting = none -resharper_member_can_be_private_global_highlighting = none -resharper_parameter_hides_member_highlighting = none -resharper_suggest_discard_declaration_var_style_highlighting = warning -resharper_web_config_module_not_resolved_highlighting = warning -resharper_web_config_type_not_resolved_highlighting = warning -resharper_web_config_wrong_module_highlighting = warning - -[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] -indent_style = space -indent_size = 4 -tab_width = 4 diff --git a/.gitignore b/.gitignore index d87d1ac..ca2abb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ bin/ obj/ -*.sln -.mono/ -.import/ -.idea/ \ No newline at end of file +**/*.sln +**/.mono/ +**/.import/ +**/.idea/ \ No newline at end of file diff --git a/Modding/Mod.cs b/Modding/Mod.cs deleted file mode 100644 index 2595910..0000000 --- a/Modding/Mod.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Modding.Patching; -using Godot.Serialization; - -namespace Godot.Modding -{ - /// - /// Represents a modular component loaded at runtime, with its own assemblies, resource packs, and data. - /// - [PublicAPI] - 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; - this.LoadResources(); - this.Data = this.LoadData(); - this.Patches = this.LoadPatches(); - this.Assemblies = this.LoadAssemblies(); - } - - /// - /// 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 if any, combined into a single . - /// - public XmlDocument? Data - { - get; - } - - /// - /// The patches applied by the . - /// - public IEnumerable Patches - { - get; - } - - private IEnumerable LoadAssemblies() - { - string assembliesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Assemblies"; - - return System.IO.Directory.Exists(assembliesPath) - ? System.IO.Directory.GetFiles(assembliesPath, "*dll", SearchOption.AllDirectories).Select(Assembly.LoadFile) - : Enumerable.Empty(); - } - - private XmlDocument? LoadData() - { - IEnumerable documents = this.LoadDocuments().ToArray(); - if (!documents.Any()) - { - return null; - } - - XmlDocument data = new(); - - XmlElement root = data.CreateElement("Data"); - data.AppendChild(root); - data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), root); - - documents - .SelectMany(document => document.Cast()) - .Where(node => node is not XmlDeclaration) - .ForEach(node => root.AppendChild(data.ImportNode(node, true))); - - return data; - } - - private IEnumerable LoadDocuments() - { - string dataPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Data"; - - if (!System.IO.Directory.Exists(dataPath)) - { - yield break; - } - - foreach (string xmlPath in System.IO.Directory.GetFiles(dataPath, "*.xml", SearchOption.AllDirectories)) - { - XmlDocument document = new(); - document.Load(xmlPath); - yield return document; - } - } - - private void LoadResources() - { - string resourcesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Resources"; - - if (!System.IO.Directory.Exists(resourcesPath)) - { - return; - } - - 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 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)) - { - document.Load(patchPath); - if (document.DocumentElement is not null) - { - yield return serializer.Deserialize(document.DocumentElement) as IPatch ?? throw new ModLoadException(this.Meta.Directory, $"Invalid patch at {patchPath}"); - } - } - } - - /// - /// Represents the metadata of a , such as its unique ID, name, author, load order, etc. - /// - [PublicAPI] - public sealed record Metadata - { - [UsedImplicitly] - private Metadata() - { - } - - /// - /// The directory where the was loaded from. - /// - [Serialize] - public string Directory - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The unique ID of the . - /// - [Serialize] - public string Id - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The name of the . - /// - [Serialize] - public string Name - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The individual or group that created the . - /// - [Serialize] - public string Author - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The unique IDs of all other s that the depends on. - /// - public IEnumerable Dependencies - { - get; - [UsedImplicitly] - private set; - } = Enumerable.Empty(); - - /// - /// The unique IDs of all other s that should be loaded before the . - /// - public IEnumerable Before - { - get; - [UsedImplicitly] - private set; - } = Enumerable.Empty(); - - /// - /// The unique IDs of all other s that should be loaded after the . - /// - public IEnumerable After - { - get; - [UsedImplicitly] - private set; - } = Enumerable.Empty(); - - /// - /// The unique IDs of all other s that are incompatible with the . - /// - public IEnumerable Incompatible - { - get; - [UsedImplicitly] - 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) - { - 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 - { - 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); - - return new Serializer().Deserialize(document.DocumentElement)!; - } - catch (Exception exception) when (exception is not ModLoadException) - { - throw new ModLoadException(directoryPath, exception); - } - } - - [AfterDeserialization] - private void 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.Incompatible - .Prepend(this.Id) - .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(); - if (invalidLoadOrder || invalidDependencies) - { - throw new ModLoadException(this.Directory, "Invalid metadata"); - } - } - } - } -} \ No newline at end of file diff --git a/Modding/ModLoadException.cs b/Modding/ModLoadException.cs deleted file mode 100644 index 40dcbb7..0000000 --- a/Modding/ModLoadException.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -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}") - { - } - } -} \ No newline at end of file diff --git a/Modding/ModLoader.cs b/Modding/ModLoader.cs deleted file mode 100644 index 9396a5d..0000000 --- a/Modding/ModLoader.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Utility; -using Godot.Utility.Extensions; - -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 OrderedDictionary loadedMods = new(); - - /// - /// All the s that have been loaded at runtime. - /// - public static IReadOnlyDictionary LoadedMods - { - get - { - return ModLoader.loadedMods; - } - } - - /// - /// Loads a from , applies its patches if any, 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, 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); - } - - return mod; - } - - /// - /// Loads s from , applies their patches if any, 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, bool executeAssemblies = true) - { - // Cache XML data of loaded mods for repeat enumeration later - List data = ModLoader.LoadedMods.Values - .Select(mod => mod.Data?.DocumentElement) - .NotNull() - .ToList(); - - List mods = new(); - foreach (Mod.Metadata metadata in ModLoader.SortModMetadata(ModLoader.FilterModMetadata(ModLoader.LoadModMetadata(modDirectoryPaths)))) - { - // Load mod - Mod mod = new(metadata); - mods.Add(mod); - ModLoader.loadedMods.Add(mod.Meta.Id, 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)); - } - // Execute mod assemblies - if (executeAssemblies) - { - mods.ForEach(ModLoader.StartupMod); - } - return mods; - } - - private static void StartupMod(Mod mod) - { - // Invoke all static methods annotated with [Startup] along with the supplied parameters (if any) - mod.Assemblies - .SelectMany(assembly => assembly.GetTypes()) - .SelectMany(type => type.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) - .Select(method => (method, method.GetCustomAttribute())) - .Where(pair => pair.Item2 is not null) - .ForEach(pair => pair.Item1.Invoke(null, pair.Item2.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 = metadata.Incompatible - .Select(id => loadedMetadata.GetValueOrDefault(id)) - .NotNull() - .Concat(loadedMetadata.Values.Where(loaded => loaded.Incompatible.Contains(metadata.Id))); - 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 - IEnumerable invalidMetadata = loadedMetadata.Values - .Where(metadata => metadata.Dependencies - .Select(dependency => loadedMetadata.TryGetValue(dependency, out _)) - .Any(dependency => !dependency)); - foreach (Mod.Metadata metadata in invalidMetadata) - { - 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/Modding/ModStartupAttribute.cs b/Modding/ModStartupAttribute.cs deleted file mode 100644 index 402b127..0000000 --- a/Modding/ModStartupAttribute.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 - { - /// - /// 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; - } - } -} \ No newline at end of file diff --git a/Modding/Patching/AttributeRemovePatch.cs b/Modding/Patching/AttributeRemovePatch.cs deleted file mode 100644 index 3e3f7c0..0000000 --- a/Modding/Patching/AttributeRemovePatch.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that removes an attribute from an . - /// - [PublicAPI] - public class AttributeRemovePatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The name of the attribute to remove. - public AttributeRemovePatch(string attribute) - { - this.Attribute = attribute; - } - - [UsedImplicitly] - private AttributeRemovePatch() - { - } - - /// - /// The name of the attribute to remove. - /// - [Serialize] - public string Attribute - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Removes from if is an . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - if (data is XmlElement element) - { - element.RemoveAttribute(this.Attribute); - } - } - } -} \ No newline at end of file diff --git a/Modding/Patching/AttributeSetPatch.cs b/Modding/Patching/AttributeSetPatch.cs deleted file mode 100644 index 44784cc..0000000 --- a/Modding/Patching/AttributeSetPatch.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that sets the value of an attribute in an . - /// - [PublicAPI] - public class AttributeSetPatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The name of the attribute to add/set. - /// The value of the attribute to add/set. - public AttributeSetPatch(string attribute, string value) - { - this.Attribute = attribute; - this.Value = value; - } - - [UsedImplicitly] - private AttributeSetPatch() - { - } - - /// - /// The name of the attribute to add/set. - /// - [Serialize] - public string Attribute - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The value of the attribute to add/set. - /// - [Serialize] - public string Value - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Sets to on if is an . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - if (data is XmlElement element) - { - element.SetAttribute(this.Attribute, this.Value); - } - } - } -} \ No newline at end of file diff --git a/Modding/Patching/ConditionalPatch.cs b/Modding/Patching/ConditionalPatch.cs deleted file mode 100644 index 60157ff..0000000 --- a/Modding/Patching/ConditionalPatch.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Modding.Patching.Conditions; -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that can apply either a "success" or a "failure" patch to an depending on a condition. - /// - [PublicAPI] - public class ConditionalPatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The condition to check. - /// The patch to apply if succeeds. - /// The patch to apply if fails. - public ConditionalPatch(ICondition condition, IPatch? success, IPatch? failure) - { - this.Condition = condition; - this.Success = success; - this.Failure = failure; - } - - [UsedImplicitly] - private ConditionalPatch() - { - } - - /// - /// The condition to check when applying the . - /// - [Serialize] - public ICondition Condition - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The applied if succeeds. - /// - public IPatch? Success - { - get; - [UsedImplicitly] - private set; - } - - /// - /// The applied if fails. - /// - public IPatch? Failure - { - get; - [UsedImplicitly] - private set; - } - - /// - /// Applies either or to depending on . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - IPatch? patch = this.Condition.Check(data) ? this.Success : this.Failure; - patch?.Apply(data); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/Conditions/AndCondition.cs b/Modding/Patching/Conditions/AndCondition.cs deleted file mode 100644 index a99c9b7..0000000 --- a/Modding/Patching/Conditions/AndCondition.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching.Conditions -{ - /// - /// An that succeeds if all of a specified sequence of conditions succeed. - /// - [PublicAPI] - public class AndCondition : ICondition - { - /// - /// Initialises a new with the specified parameters. - /// - /// The conditions to check. - public AndCondition(IEnumerable conditions) - { - this.Conditions = conditions; - } - - [UsedImplicitly] - private AndCondition() - { - } - - /// - /// The conditions to check. - /// - [Serialize] - public IEnumerable Conditions - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Succeeds if all conditions in succeed. - /// - /// The to apply the patch on. - /// if all conditions in succeed, else . - public bool Check(XmlNode data) - { - return this.Conditions.All(condition => condition.Check(data)); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/Conditions/ICondition.cs b/Modding/Patching/Conditions/ICondition.cs deleted file mode 100644 index e1aa214..0000000 --- a/Modding/Patching/Conditions/ICondition.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Xml; - -namespace Godot.Modding.Patching.Conditions -{ - /// - /// Represents a condition that can be tested on an . - /// - public interface ICondition - { - /// - /// Checks if a 's XML data satisfies the . - /// - /// The to apply the patch on. - /// if the condition succeeds, else . - public bool Check(XmlNode data); - } -} \ No newline at end of file diff --git a/Modding/Patching/Conditions/ModLoadedCondition.cs b/Modding/Patching/Conditions/ModLoadedCondition.cs deleted file mode 100644 index f29c7b7..0000000 --- a/Modding/Patching/Conditions/ModLoadedCondition.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching.Conditions -{ - /// - /// An that checks if a particular has been loaded. - /// - [PublicAPI] - public class ModLoadedCondition : ICondition - { - /// - /// Initialises a new with the specified parameters. - /// - /// The ID of the to check for. - public ModLoadedCondition(string modId) - { - this.ModId = modId; - } - - [UsedImplicitly] - private ModLoadedCondition() - { - } - - /// - /// The ID of the to check for. - /// - [Serialize] - public string ModId - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Checks if any loaded 's ID equals . - /// - /// The to apply the patch on. This is not used by the . - /// if the given by is loaded, else . - public bool Check(XmlNode data) - { - return ModLoader.LoadedMods.TryGetValue(this.ModId, out _); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/Conditions/NodeExistsCondition.cs b/Modding/Patching/Conditions/NodeExistsCondition.cs deleted file mode 100644 index 6e5b483..0000000 --- a/Modding/Patching/Conditions/NodeExistsCondition.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching.Conditions -{ - /// - /// An that checks if a descendant of an matching an XPath string exists. - /// - [PublicAPI] - public class NodeExistsCondition : ICondition - { - /// - /// Initialises a new with the specified parameters. - /// - /// The XPath string to check. - public NodeExistsCondition(string xPath) - { - this.XPath = xPath; - } - - [UsedImplicitly] - private NodeExistsCondition() - { - } - - /// - /// The XPath string to use when checking if an exists. - /// - [Serialize] - public string XPath - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Checks if any s match the XPath given by . - /// - /// The to apply the patch on. - /// if selects at least one , else . - public bool Check(XmlNode data) - { - return data.SelectSingleNode(this.XPath) is not null; - } - } -} \ No newline at end of file diff --git a/Modding/Patching/Conditions/NotCondition.cs b/Modding/Patching/Conditions/NotCondition.cs deleted file mode 100644 index 9c34630..0000000 --- a/Modding/Patching/Conditions/NotCondition.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching.Conditions -{ - /// - /// An that succeeds if a specified condition fails. - /// - [PublicAPI] - public class NotCondition : ICondition - { - /// - /// Initialises a new with the specified parameters. - /// - /// The to check. - public NotCondition(ICondition condition) - { - this.Condition = condition; - } - - [UsedImplicitly] - private NotCondition() - { - } - - /// - /// The condition to check. - /// - [Serialize] - public ICondition Condition - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Succeeds if fails. - /// - /// The to apply the patch on. - /// if fails, else . - public bool Check(XmlNode data) - { - return !this.Condition.Check(data); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/Conditions/OrCondition.cs b/Modding/Patching/Conditions/OrCondition.cs deleted file mode 100644 index ac1ed10..0000000 --- a/Modding/Patching/Conditions/OrCondition.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching.Conditions -{ - /// - /// An that succeeds if at least one of a specified sequence of conditions succeed. - /// - [PublicAPI] - public class OrCondition : ICondition - { - /// - /// Initialises a new with the specified parameters. - /// - /// The conditions to check. - public OrCondition(IEnumerable conditions) - { - this.Conditions = conditions; - } - - [UsedImplicitly] - private OrCondition() - { - } - - /// - /// The conditions to check. - /// - public IEnumerable Conditions - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Succeeds if at least one condition in succeeds. - /// - /// The to apply the patch on. - /// if at least one condition in succeeds, else . - public bool Check(XmlNode data) - { - return this.Conditions.Any(condition => condition.Check(data)); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/IPatch.cs b/Modding/Patching/IPatch.cs deleted file mode 100644 index 794087e..0000000 --- a/Modding/Patching/IPatch.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Xml; - -namespace Godot.Modding.Patching -{ - /// - /// Represents a modification that can be applied to the XML data of a . - /// - public interface IPatch - { - /// - /// Applies the patch to . - /// - /// The to apply the patch on. - public void Apply(XmlNode data); - } -} \ No newline at end of file diff --git a/Modding/Patching/LogPatch.cs b/Modding/Patching/LogPatch.cs deleted file mode 100644 index c8b8fe6..0000000 --- a/Modding/Patching/LogPatch.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that logs the state of any s before and after applying a separate patch to them. - /// - [PublicAPI] - public class LogPatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The patch to apply before and after logging the . - public LogPatch(IPatch patch) - { - this.Patch = patch; - } - - private LogPatch() - { - } - - /// - /// The patch to apply before and after logging the . - /// - [Serialize] - public IPatch Patch - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Logs the XML string representation of before and after applying to it. - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - Log.Write($"Before: {data.OuterXml}"); - this.Patch.Apply(data); - Log.Write($"After: {data.OuterXml}"); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/MultiPatch.cs b/Modding/Patching/MultiPatch.cs deleted file mode 100644 index 911613c..0000000 --- a/Modding/Patching/MultiPatch.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that applies multiple patches in sequence onto the same . - /// - [PublicAPI] - public class MultiPatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The patches to apply in sequence. - public MultiPatch(IEnumerable patches) - { - this.Patches = patches; - } - - [UsedImplicitly] - private MultiPatch() - { - } - - /// - /// The patches to apply in sequence. - /// - [Serialize] - public IEnumerable Patches - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Applies all patches in to . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - this.Patches.ForEach(patch => patch.Apply(data)); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/NodeAddPatch.cs b/Modding/Patching/NodeAddPatch.cs deleted file mode 100644 index 9734233..0000000 --- a/Modding/Patching/NodeAddPatch.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that adds an as a child to another . - /// - [PublicAPI] - public class NodeAddPatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The to add as a child. - /// The index to insert at, or -1 if it should simply be appended to the end. - public NodeAddPatch(XmlNode value, int index = -1) - { - this.Value = value; - this.Index = index; - } - - [UsedImplicitly] - private NodeAddPatch() - { - } - - /// - /// The to add as a child. - /// - [Serialize] - public XmlNode Value - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The index to insert at. - /// - public int Index - { - get; - [UsedImplicitly] - private set; - } = -1; - - /// - /// Adds as a child to at the index specified by . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - XmlNode value = data.OwnerDocument!.ImportNode(this.Value, true); - switch (this.Index) - { - case < 0: - data.AppendChild(value); - break; - case 0: - data.PrependChild(value); - break; - default: - data.InsertBefore(value, data.ChildNodes[this.Index]); - break; - } - } - } -} \ No newline at end of file diff --git a/Modding/Patching/NodeRemovePatch.cs b/Modding/Patching/NodeRemovePatch.cs deleted file mode 100644 index 00c95ab..0000000 --- a/Modding/Patching/NodeRemovePatch.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -namespace Godot.Modding.Patching -{ - /// - /// An that removes the it is applied on. - /// - [PublicAPI] - public class NodeRemovePatch : IPatch - { - /// - /// Removes from its parent . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - data.ParentNode!.RemoveChild(data); - } - } -} \ No newline at end of file diff --git a/Modding/Patching/NodeReplacePatch.cs b/Modding/Patching/NodeReplacePatch.cs deleted file mode 100644 index fa9b390..0000000 --- a/Modding/Patching/NodeReplacePatch.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that replaces an with another one. - /// - [PublicAPI] - public class NodeReplacePatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// The to add in place of the removed . - public NodeReplacePatch(XmlNode replacement) - { - this.Replacement = replacement; - } - - [UsedImplicitly] - private NodeReplacePatch() - { - } - - /// - /// The to add in place of the removed . - /// - [Serialize] - public XmlNode Replacement - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Removes and replaces it with . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - XmlNode? previous = data.PreviousSibling; - XmlNode parent = data.ParentNode!; - parent.RemoveChild(data); - if (previous is null) - { - parent.PrependChild(this.Replacement); - } - else - { - parent.InsertAfter(this.Replacement, previous); - } - } - } -} \ No newline at end of file diff --git a/Modding/Patching/TargetedPatch.cs b/Modding/Patching/TargetedPatch.cs deleted file mode 100644 index 8cc0ea4..0000000 --- a/Modding/Patching/TargetedPatch.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Linq; -using System.Xml; - -using JetBrains.Annotations; - -using Godot.Serialization; - -namespace Godot.Modding.Patching -{ - /// - /// An that selects descendants of an according to an XPath string and applies a separate patch on them. - /// - [PublicAPI] - public class TargetedPatch : IPatch - { - /// - /// Initialises a new with the specified parameters. - /// - /// An XPath string that specifies descendant s to apply on. - /// The patch to apply on all s selected by . - public TargetedPatch(string targets, IPatch patch) - { - this.Targets = targets; - this.Patch = patch; - } - - [UsedImplicitly] - private TargetedPatch() - { - } - - /// - /// The targets to apply the on, in the form of an XPath. - /// - [Serialize] - public string Targets - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// The patch to apply on s that match . - /// - [Serialize] - public IPatch Patch - { - get; - [UsedImplicitly] - private set; - } = null!; - - /// - /// Applies to all s under that match . - /// - /// The to apply the patch on. - public void Apply(XmlNode data) - { - data.SelectNodes(this.Targets)? - .Cast() - .ForEach(this.Patch.Apply); - } - } -} \ No newline at end of file diff --git a/Modot.csproj b/Modot.csproj deleted file mode 100644 index 0125df0..0000000 --- a/Modot.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - default - enable - netstandard2.1 - Godot - true - - true - true - 2.0.2 - 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 - LICENSE - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Utility/ErrorException.cs b/Utility/ErrorException.cs deleted file mode 100644 index 46743e1..0000000 --- a/Utility/ErrorException.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -using JetBrains.Annotations; - -namespace Godot.Utility -{ - /// - /// A wrapper around to make it throwable. - /// - [PublicAPI] - public class ErrorException : Exception - { - /// - /// Initialises a new with the specified . - /// - /// The Godot . - public ErrorException(Error error) : base(error.ToString()) - { - this.Error = error; - } - - /// - /// The Godot . - /// - public Error Error - { - get; - } - } -} \ No newline at end of file diff --git a/Utility/Extensions/DirectoryExtensions.cs b/Utility/Extensions/DirectoryExtensions.cs deleted file mode 100644 index bb77db6..0000000 --- a/Utility/Extensions/DirectoryExtensions.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; - -using JetBrains.Annotations; - -namespace Godot.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. - /// An array of the paths of all files that were copied from to . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string[] CopyContents(this Directory directory, string from, string to, bool recursive = false) - { - return directory.CopyContentsLazy(from, to, recursive).ToArray(); - } - - /// - /// 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).Throw(); - 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).Throw(); - return recursiveDirectory - .GetDirectories(true) - .Prepend(path); - }) - .ToArray() - : directory - .GetElementsNonRecursive(false) - .ToArray(); - } - - private static IEnumerable GetElementsNonRecursive(this Directory directory, bool trueIfFiles) - { - directory.ListDirBegin(true).Throw(); - while (true) - { - string next = directory.GetNext(); - if (next is "") - { - yield break; - } - // Continue if the current element is a file or directory depending on which one is being queried - if (directory.CurrentIsDir() == trueIfFiles) - { - continue; - } - string current = directory.GetCurrentDir(); - yield return current.EndsWith("/") ? $"{current}{next}" : $"{current}/{next}"; - } - } - - private static IEnumerable CopyContentsLazy(this Directory directory, string from, string to, bool recursive = false) - { - directory.Open(from).Throw(); - - // Create destination directory if it doesn't already exist - directory.MakeDirRecursive(to).Throw(); - - // Replace only the first instance of the destination directory in file and subdirectory paths using regex (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.GetElementsNonRecursive(true)) - { - string toFile = fromReplacement.Replace(fromFile, to, 1); - directory.Copy(fromFile, toFile).Throw(); - yield return toFile; - } - - if (!recursive) - { - yield break; - } - - // Copy all files recursively - foreach (string fromSubDirectory in directory.GetDirectories(true)) - { - string toSubDirectory = fromReplacement.Replace(fromSubDirectory, to, 1); - directory.MakeDirRecursive(toSubDirectory).Throw(); - - using Directory innerDirectory = new(); - innerDirectory.Open(fromSubDirectory).Throw(); - foreach (string fromFile in innerDirectory.GetElementsNonRecursive(true)) - { - string toFile = fromReplacement.Replace(fromFile, to, 1); - directory.Copy(fromFile, toFile).Throw(); - yield return toFile; - } - } - } - } -} \ No newline at end of file diff --git a/Utility/Extensions/EnumerableExtensions.cs b/Utility/Extensions/EnumerableExtensions.cs deleted file mode 100644 index 58fd6b4..0000000 --- a/Utility/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using JetBrains.Annotations; - -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 . - [MustUseReturnValue] - 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 diff --git a/Utility/Extensions/ErrorExtensions.cs b/Utility/Extensions/ErrorExtensions.cs deleted file mode 100644 index 43446f7..0000000 --- a/Utility/Extensions/ErrorExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Runtime.CompilerServices; - -using JetBrains.Annotations; - -namespace Godot.Utility.Extensions -{ - /// - /// Contains extension methods for . - /// - [PublicAPI] - public static class ErrorExtensions - { - /// - /// Checks if indicates success. - /// - /// The to check. - /// if is , else . - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Success(this Error error) - { - return error is Error.Ok; - } - - /// - /// Throws an exception if indicates failure. - /// - /// The to check. - /// Thrown if is not . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Throw(this Error error) - { - if (error is not Error.Ok) - { - throw new ErrorException(error); - } - } - } -} \ No newline at end of file From 07c4f73603567103129a801655fc0f25726352ed Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 21 Sep 2022 14:00:56 +0100 Subject: [PATCH 06/32] Update gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 8fb2434..c86a7d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ bin/ obj/ *.sln +*.Dotsettings.user + +*.godot **/.mono/ **/.import/ + **/.idea/ \ No newline at end of file From 298967badb296b57c472acca44a3032137fa76de Mon Sep 17 00:00:00 2001 From: Carnagion Date: Thu, 22 Sep 2022 19:12:15 +0100 Subject: [PATCH 07/32] Add mod and mod loader classes --- GDScript/modding/mod.gd | 162 +++++++++++++++++++++++++++++++++ GDScript/modding/mod_loader.gd | 62 +++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 GDScript/modding/mod.gd create mode 100644 GDScript/modding/mod_loader.gd diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd new file mode 100644 index 0000000..97f3644 --- /dev/null +++ b/GDScript/modding/mod.gd @@ -0,0 +1,162 @@ +class_name Mod +extends RefCounted + +func _init(metadata): + self._meta = metadata + self._load_resources() + self._load_data() + +var _meta + +var _data = {} + +var meta: + get: + return self._meta + +var data: + get: + return self._data + +func _load_resources(): + var resources_path = self.meta.directory.path_join("resources") + var directory = Directory.new() + directory.open(resources_path) + for unloaded_resource in DirectoryExtensions.get_files_recursive_ending(directory, ["pck"]).filter(func(resource_path): return not ProjectSettings.load_resource_pack(resource_path)): + Errors.mod_load_error(self.meta.directory, "Could not load resource pack at %s" % unloaded_resource) + +func _load_data(): + var data_path = self.meta.directory.path_join("data") + var directory = Directory.new() + directory.open(data_path) + var file = File.new() + for json_path in DirectoryExtensions.get_files_recursive_ending(directory, ["json"]): + file.open(json_path, File.READ) + var json = JSON.parse_string(file.get_as_text()) + file.close() + if json == null: + Errors.mod_load_error(self.meta.directory, "Could not parse JSON at %s" % json_path) + continue + self._data[json_path] = json + +class Metadata extends RefCounted: + + var _directory + + var _id + + var _name + + var _author + + var _dependencies + + var _before + + var _after + + var _incompatible + + var directory: + get: + return self._directory + + var id: + get: + return self._id + + var name: + get: + return self._name + + var author: + get: + return self._author + + var dependencies: + get: + return self._dependencies + + var before: + get: + return self._before + + var after: + get: + return self._after + + var incompatible: + get: + return self._incompatible + + static func _load(directory_path): + var meta = Mod.Metadata.new() + meta._directory = directory_path + # Locate metadata file + var metadata_file_path = directory_path.path_join("mod.json") + var file = File.new() + if not file.file_exists(metadata_file_path): + Errors.mod_load_error(directory_path, "Mod metadata file does not exist") + return null + # Retrieve metadata file contents + file.open(metadata_file_path, File.READ) + var json = JSON.parse_string(file.get_as_text()) + file.close() + if not json is Dictionary: + Errors.mod_load_error(directory_path, "Mod metadata is invalid") + return null + return meta if meta._try_deserialize(json) and meta._is_valid() else null + + + func _try_deserialize(json): + # Retrieve compulsory metadata + var id = json.get("id") + var name = json.get("name") + var author = json.get("author") + if not (id is String and name is String and author is String): + Errors.mod_load_error(self.directory, "Mod metadata contains invalid ID, name, or author") + return false + self._id = id + self._name = name + self._author = author + # Retrieve optional metadata + var dependencies = json.get("dependencies", []) + if dependencies is Array: + self._dependencies = dependencies + else: + Errors.mod_load_error(self.directory, "Mod metadata contains invalid dependencies") + return false + var before = json.get("before", []) + if before is Array: + self._before = before + else: + Errors.mod_load_error(self.directory, "Mod metadata contains invalid load before list") + return false + var after = json.get("after", []) + if after is Array: + self._after = after + else: + Errors.mod_load_error(self.directory, "Mod metadata contains invalid load after list") + return false + var incompatible = json.get("incompatible", []) + if incompatible is Array: + self._incompatible = incompatible + else: + Errors.mod_load_error(self.directory, "Mod metadata contains invalid incompatibilities") + return false + return true + + func _is_valid(): + # Check that the incompatible, load before, and load after lists don't have anything common or contain the mod's own ID + var duplicates = {} + var valid_load_order = ([self.id] + self.before + self.after + self.incompatible).filter(func(id): + if id in duplicates: + return true + duplicates[id] = true + return false).is_empty() + # Check that the dependency and incompatible lists don't have anything in common + var valid_dependencies = self.dependencies.filter(func(id): return id in incompatible).is_empty() + if valid_load_order and valid_dependencies: + return true + Errors.mod_load_error(self.directory, "Mod metadata contains invalid load order or invalid dependencies") + return false diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd new file mode 100644 index 0000000..2116ea0 --- /dev/null +++ b/GDScript/modding/mod_loader.gd @@ -0,0 +1,62 @@ +class_name ModLoader +extends Node + +var _loaded_mods = {} + +var loaded_mods: + get: + return self._loaded_mods + +func load_mod(mod_directory_path): + var metadata = Mod.Metadata._load(mod_directory_path) + if not metadata: + return null + var mod = Mod.new(metadata) + self._loaded_mods[mod.meta.id] = mod + return mod + +func _load_mod_metadata(mod_directory_paths): + var loaded_metadata = {} + for metadata in mod_directory_paths.map(func(mod_directory_path): return Mod.Metadata._load(mod_directory_path)).filter(func(metadata): return metadata != null): + # Fail if the metadata is incompatible with any of the loaded metadata (and vice-versa), or if the ID already exists + var incompatible_metadata = metadata.incompatible.map(func(id): return loaded_metadata[id].filter(func(loaded): return loaded != null)) + loaded_metadata.values().filter(func(loaded): return metadata in loaded.incompatible) + if not incompatible_metadata.is_empty(): + Errors.mod_load_error(metadata.directory, "Mod is incompatible with other loaded mods") + continue + elif metadata.id in loaded_metadata: + Errors.mod_load_error(metadata.directory, "Mod has duplicate ID") + continue + loaded_metadata[metadata.id] = metadata + return loaded_metadata + +func _filter_mod_metadata(loaded_metadata): + # If the dependencies of any metadata have not been loaded, remove that metadata and try again + var invalid_metadata = loaded_metadata.values().filter(func(metadata): return metadata.dependencies.any(func(dependency): return not dependency in loaded_metadata)) + for metadata in invalid_metadata: + Errors.mod_load_error(metadata.directory, "Not all dependencies are loaded") + loaded_metadata.erase(metadata.id) + return self._filter_mod_metadata(loaded_metadata) + return loaded_metadata + +func _sort_mod_metadata(filtered_metadata): + if filtered_metadata.is_empty(): + return [] + # Create a graph of each metadata ID and the IDs of those that need to be loaded after it + var dependency_graph = {} + for metadata in filtered_metadata.values(): + if not metadata.id in dependency_graph: + dependency_graph[metadata.id] = [] + for after in metadata.after: + dependency_graph[metadata.id].append(after) + for before in metadata.before: + if not before in dependency_graph: + dependency_graph[before] = [] + dependency_graph[before].append(metadata.id) + # Topologically sort the dependency graph, removing cyclic dependencies if any + var sorted_metadata = ArrayExtensions.topological_sort(dependency_graph.keys(), func(id): return dependency_graph.get(id, []), func(cyclic): + Errors.mod_load_error(filtered_metadata[cyclic].directory, "Mod has cyclic dependencies with other mods") + filtered_metadata.erase(cyclic)) + # If there is no valid topological sorting (cyclic dependencies detected), remove the cyclic metadata and try again + if sorted_metadata.is_empty(): + return self._sort_mod_metadata(self._filter_mod_metadata(filtered_metadata)) + return sorted_metadata.map(func(id): return filtered_metadata.get(id)).filter(func(metadata): return metadata != null) From 3673b9209356d84e6bb24e2ea1f59e215006ac49 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Thu, 22 Sep 2022 19:12:29 +0100 Subject: [PATCH 08/32] Add utility classes --- GDScript/utility/errors.gd | 4 +++ .../utility/extensions/array_extensions.gd | 25 +++++++++++++++++++ .../extensions/directory_extensions.gd | 24 ++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 GDScript/utility/errors.gd create mode 100644 GDScript/utility/extensions/array_extensions.gd create mode 100644 GDScript/utility/extensions/directory_extensions.gd diff --git a/GDScript/utility/errors.gd b/GDScript/utility/errors.gd new file mode 100644 index 0000000..968bcc3 --- /dev/null +++ b/GDScript/utility/errors.gd @@ -0,0 +1,4 @@ +class_name Errors + +static func mod_load_error(directory_path, message): + push_error("Error loading mod at %s: %s" % [directory_path, message]) diff --git a/GDScript/utility/extensions/array_extensions.gd b/GDScript/utility/extensions/array_extensions.gd new file mode 100644 index 0000000..7d90263 --- /dev/null +++ b/GDScript/utility/extensions/array_extensions.gd @@ -0,0 +1,25 @@ +class_name ArrayExtensions + +static func topological_sort(array, dependencies, cyclic): + var sorted = [] + var states = {} + var all_valid = array.all(func(element): return ArrayExtensions._visit_dependencies(element, dependencies, cyclic, sorted, states)) + return sorted if all_valid else [] + +static func _visit_dependencies(element, dependencies, cyclic, sorted, states): + if not element in states: + states[element] = false + match states[element]: + true: + return true + false: + states[element] = null + var dependencies_valid = dependencies.call(element).all(func(dependency): return ArrayExtensions._visit_dependencies(dependency, dependencies, cyclic, sorted, states)) + if not dependencies_valid: + return false + states[element] = true + sorted.append(element) + return true + null: + cyclic.call(element) + return false diff --git a/GDScript/utility/extensions/directory_extensions.gd b/GDScript/utility/extensions/directory_extensions.gd new file mode 100644 index 0000000..79c7cfd --- /dev/null +++ b/GDScript/utility/extensions/directory_extensions.gd @@ -0,0 +1,24 @@ +class_name DirectoryExtensions + +static func get_directories_recursive(directory): + var directories = [] + for subdir_path in directory.get_directories(): + var path_full = directory.get_current_dir().path_join(subdir_path) + directories.append(path_full) + var subdir = Directory.new() + subdir.open(path_full) + directories.append_array(DirectoryExtensions.get_directories_recursive(subdir)) + return directories + +static func get_files_recursive(directory): + var files = [] + for file_path in directory.get_files(): + files.append(directory.get_current_dir().path_join(file_path)) + for subdir_path in DirectoryExtensions.get_directories_recursive(directory): + var subdir = Directory.new() + subdir.open(directory.get_current_dir().path_join(subdir_path)) + files.append_array(subdir.get_files()) + return files + +static func get_files_recursive_ending(directory, extensions): + return DirectoryExtensions.get_files_recursive(directory).filter(func(file_path): return extensions.any(func(extension): return file_path.ends_with(".%s" % extension))) From 6e208ecbdef781a17cb75e3acd4e0eef63867032 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 23 Sep 2022 11:25:52 +0100 Subject: [PATCH 09/32] Update gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ca2abb3..785cf2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ bin/ obj/ -**/*.sln +*.sln +*.Dotsettings.user + +**/*.godot **/.mono/ **/.import/ + **/.idea/ \ No newline at end of file From c18dcb52ad7188816d51e9912f8eac5cf9c1a22d Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 23 Sep 2022 11:26:25 +0100 Subject: [PATCH 10/32] Update gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c86a7d7..785cf2f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ obj/ *.sln *.Dotsettings.user -*.godot +**/*.godot **/.mono/ **/.import/ From 1e3ad02f09c29aec632833e03bb09c6bb39d60c0 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 23 Sep 2022 15:00:08 +0100 Subject: [PATCH 11/32] Improve error handling when loading mod --- GDScript/modding/mod.gd | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd index 97f3644..bcfdbf2 100644 --- a/GDScript/modding/mod.gd +++ b/GDScript/modding/mod.gd @@ -21,6 +21,8 @@ var data: func _load_resources(): var resources_path = self.meta.directory.path_join("resources") var directory = Directory.new() + if not directory.dir_exists(resources_path): + return directory.open(resources_path) for unloaded_resource in DirectoryExtensions.get_files_recursive_ending(directory, ["pck"]).filter(func(resource_path): return not ProjectSettings.load_resource_pack(resource_path)): Errors.mod_load_error(self.meta.directory, "Could not load resource pack at %s" % unloaded_resource) @@ -28,6 +30,8 @@ func _load_resources(): func _load_data(): var data_path = self.meta.directory.path_join("data") var directory = Directory.new() + if not directory.dir_exists(data_path): + return directory.open(data_path) var file = File.new() for json_path in DirectoryExtensions.get_files_recursive_ending(directory, ["json"]): @@ -90,8 +94,6 @@ class Metadata extends RefCounted: return self._incompatible static func _load(directory_path): - var meta = Mod.Metadata.new() - meta._directory = directory_path # Locate metadata file var metadata_file_path = directory_path.path_join("mod.json") var file = File.new() @@ -105,6 +107,8 @@ class Metadata extends RefCounted: if not json is Dictionary: Errors.mod_load_error(directory_path, "Mod metadata is invalid") return null + var meta = Mod.Metadata.new() + meta._directory = directory_path return meta if meta._try_deserialize(json) and meta._is_valid() else null From 06d1bcad0361d90720be6e4a4c3850c9aab16b4d Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 23 Sep 2022 15:00:44 +0100 Subject: [PATCH 12/32] Add GDScript function to load multiple mods --- GDScript/modding/mod_loader.gd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd index 2116ea0..ca47b1b 100644 --- a/GDScript/modding/mod_loader.gd +++ b/GDScript/modding/mod_loader.gd @@ -15,6 +15,12 @@ func load_mod(mod_directory_path): self._loaded_mods[mod.meta.id] = mod return mod +func load_mods(mod_directory_paths): + var mods = self._sort_mod_metadata(self._filter_mod_metadata(self._load_mod_metadata(mod_directory_paths))).map(func(metadata): return Mod.new(metadata)) + for mod in mods: + self._loaded_mods[mod.meta.id] = mod + return mods + func _load_mod_metadata(mod_directory_paths): var loaded_metadata = {} for metadata in mod_directory_paths.map(func(mod_directory_path): return Mod.Metadata._load(mod_directory_path)).filter(func(metadata): return metadata != null): From 85d3400e28ab5444fb4d70fdc69d633ff6399daf Mon Sep 17 00:00:00 2001 From: Carnagion Date: Fri, 23 Sep 2022 22:08:27 +0100 Subject: [PATCH 13/32] Add support for executing mod scripts on loading --- GDScript/modding/mod.gd | 22 ++++++++++++++++++++++ GDScript/modding/mod_loader.gd | 22 +++++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd index bcfdbf2..b6e7bdf 100644 --- a/GDScript/modding/mod.gd +++ b/GDScript/modding/mod.gd @@ -5,11 +5,14 @@ func _init(metadata): self._meta = metadata self._load_resources() self._load_data() + self._load_scripts() var _meta var _data = {} +var _scripts = [] + var meta: get: return self._meta @@ -18,6 +21,10 @@ var data: get: return self._data +var scripts: + get: + return self._scripts + func _load_resources(): var resources_path = self.meta.directory.path_join("resources") var directory = Directory.new() @@ -43,6 +50,21 @@ func _load_data(): continue self._data[json_path] = json +func _load_scripts(): + var scripts_path = self.meta.directory.path_join("scripts") + var directory = Directory.new() + if not directory.dir_exists(scripts_path): + return + directory.open(scripts_path) + var file = File.new() + for script_path in DirectoryExtensions.get_files_recursive_ending(directory, ["gd"]): + file.open(script_path, File.READ) + var code = file.get_as_text() + file.close() + var script = GDScript.new() + script.source_code = code + self._scripts.append(script) + class Metadata extends RefCounted: var _directory diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd index ca47b1b..df66bdb 100644 --- a/GDScript/modding/mod_loader.gd +++ b/GDScript/modding/mod_loader.gd @@ -7,20 +7,32 @@ var loaded_mods: get: return self._loaded_mods -func load_mod(mod_directory_path): +func load_mod(mod_directory_path, execute_scripts = true): var metadata = Mod.Metadata._load(mod_directory_path) if not metadata: return null var mod = Mod.new(metadata) self._loaded_mods[mod.meta.id] = mod + if execute_scripts: + self._startup_mod(mod) return mod -func load_mods(mod_directory_paths): - var mods = self._sort_mod_metadata(self._filter_mod_metadata(self._load_mod_metadata(mod_directory_paths))).map(func(metadata): return Mod.new(metadata)) - for mod in mods: - self._loaded_mods[mod.meta.id] = mod +func load_mods(mod_directory_paths, execute_scripts = true): + var mods = [] + for metadata in self._sort_mod_metadata(self._filter_mod_metadata(self._load_mod_metadata(mod_directory_paths))): + var mod = Mod.new(metadata) + mods.append(mod) + self._loaded_mods[metadata.id] = mod + if execute_scripts: + for mod in mods: + self._startup_mod(mod) return mods +func _startup_mod(mod): + for script in mod.scripts: + script.reload() + script.new() + func _load_mod_metadata(mod_directory_paths): var loaded_metadata = {} for metadata in mod_directory_paths.map(func(mod_directory_path): return Mod.Metadata._load(mod_directory_path)).filter(func(metadata): return metadata != null): From 7df7c7c004a49607d02779b497a4d4add928fb93 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sun, 25 Sep 2022 13:15:57 +0100 Subject: [PATCH 14/32] Fix bracket --- GDScript/modding/mod_loader.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd index df66bdb..5f2fc14 100644 --- a/GDScript/modding/mod_loader.gd +++ b/GDScript/modding/mod_loader.gd @@ -37,7 +37,7 @@ func _load_mod_metadata(mod_directory_paths): var loaded_metadata = {} for metadata in mod_directory_paths.map(func(mod_directory_path): return Mod.Metadata._load(mod_directory_path)).filter(func(metadata): return metadata != null): # Fail if the metadata is incompatible with any of the loaded metadata (and vice-versa), or if the ID already exists - var incompatible_metadata = metadata.incompatible.map(func(id): return loaded_metadata[id].filter(func(loaded): return loaded != null)) + loaded_metadata.values().filter(func(loaded): return metadata in loaded.incompatible) + var incompatible_metadata = metadata.incompatible.map(func(id): return loaded_metadata[id]).filter(func(loaded): return loaded != null) + loaded_metadata.values().filter(func(loaded): return metadata in loaded.incompatible) if not incompatible_metadata.is_empty(): Errors.mod_load_error(metadata.directory, "Mod is incompatible with other loaded mods") continue From 7e29fbb609e58d2f8795757b13c9d027b1ddbcdf Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sun, 25 Sep 2022 14:29:10 +0100 Subject: [PATCH 15/32] Improve string representation for mods --- GDScript/modding/mod.gd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd index b6e7bdf..f4678f1 100644 --- a/GDScript/modding/mod.gd +++ b/GDScript/modding/mod.gd @@ -65,6 +65,9 @@ func _load_scripts(): script.source_code = code self._scripts.append(script) +func _to_string(): + return "{ meta: %s, data: %s, scripts: %s }" % [self.meta, self.data, self.scripts] + class Metadata extends RefCounted: var _directory @@ -186,3 +189,6 @@ class Metadata extends RefCounted: return true Errors.mod_load_error(self.directory, "Mod metadata contains invalid load order or invalid dependencies") return false + + func _to_string(): + return "{ directory: %s, id: %s, name: %s, author: %s, dependencies: %s, before: %s, after: %s, incompatible: %s }" % [self.directory, self.id, self.name, self.author, self.dependencies, self.before, self.after, self.incompatible] From 8011e1fa3b9e46da17e934b0ad0eed49e006967e Mon Sep 17 00:00:00 2001 From: Carnagion Date: Thu, 29 Sep 2022 14:18:11 +0100 Subject: [PATCH 16/32] Refactor code and remove global ModLoader name --- GDScript/modding/mod.gd | 23 +++++++++++++---------- GDScript/modding/mod_loader.gd | 1 - 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd index f4678f1..3b39e9d 100644 --- a/GDScript/modding/mod.gd +++ b/GDScript/modding/mod.gd @@ -52,18 +52,21 @@ func _load_data(): func _load_scripts(): var scripts_path = self.meta.directory.path_join("scripts") + self._scripts.append_array(self._load_code(scripts_path)) + +func _load_code(directory_path): var directory = Directory.new() - if not directory.dir_exists(scripts_path): - return - directory.open(scripts_path) + if not directory.dir_exists(directory_path): + return [] + directory.open(directory_path) var file = File.new() - for script_path in DirectoryExtensions.get_files_recursive_ending(directory, ["gd"]): - file.open(script_path, File.READ) - var code = file.get_as_text() - file.close() - var script = GDScript.new() - script.source_code = code - self._scripts.append(script) + return DirectoryExtensions.get_files_recursive_ending(directory, ["gd"]).map(func(file_path): + file.open(file_path, File.READ) + var code = file.get_as_text() + file.close() + var script = GDScript.new() + script.source_code = code + return script) func _to_string(): return "{ meta: %s, data: %s, scripts: %s }" % [self.meta, self.data, self.scripts] diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd index 5f2fc14..7489140 100644 --- a/GDScript/modding/mod_loader.gd +++ b/GDScript/modding/mod_loader.gd @@ -1,4 +1,3 @@ -class_name ModLoader extends Node var _loaded_mods = {} From e0e51ab80032069decd35087d99eaab7d8f6e061 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 11:00:40 +0100 Subject: [PATCH 17/32] Add documentation comments --- GDScript/modding/mod.gd | 34 +++++++++++++------ GDScript/modding/mod_loader.gd | 12 ++++--- GDScript/utility/errors.gd | 2 +- .../utility/extensions/array_extensions.gd | 1 + .../extensions/directory_extensions.gd | 4 +++ 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd index 3b39e9d..dce4c7b 100644 --- a/GDScript/modding/mod.gd +++ b/GDScript/modding/mod.gd @@ -1,3 +1,4 @@ +## Represents a modular component loaded at runtime, with its own scripts, resource packs, and data. class_name Mod extends RefCounted @@ -13,14 +14,17 @@ var _data = {} var _scripts = [] +## The metadata of the [Mod], such as its ID, name, load order, etc. var meta: get: return self._meta +## The JSON data of the [Mod], combined into a single JSON dictionary with the file names as keys and their parsed contents as values. var data: get: return self._data +## The scripts of the [Mod]. var scripts: get: return self._scripts @@ -32,7 +36,7 @@ func _load_resources(): return directory.open(resources_path) for unloaded_resource in DirectoryExtensions.get_files_recursive_ending(directory, ["pck"]).filter(func(resource_path): return not ProjectSettings.load_resource_pack(resource_path)): - Errors.mod_load_error(self.meta.directory, "Could not load resource pack at %s" % unloaded_resource) + Errors._mod_load_error(self.meta.directory, "Could not load resource pack at %s" % unloaded_resource) func _load_data(): var data_path = self.meta.directory.path_join("data") @@ -46,7 +50,7 @@ func _load_data(): var json = JSON.parse_string(file.get_as_text()) file.close() if json == null: - Errors.mod_load_error(self.meta.directory, "Could not parse JSON at %s" % json_path) + Errors._mod_load_error(self.meta.directory, "Could not parse JSON at %s" % json_path) continue self._data[json_path] = json @@ -71,6 +75,7 @@ func _load_code(directory_path): func _to_string(): return "{ meta: %s, data: %s, scripts: %s }" % [self.meta, self.data, self.scripts] +## Represents the metadata of a [Mod], such as its unique ID, name, author, load order, etc. class Metadata extends RefCounted: var _directory @@ -89,34 +94,42 @@ class Metadata extends RefCounted: var _incompatible + ## The directory where the [Metadata] was loaded from. var directory: get: return self._directory + ## The unique ID of the [Mod]. var id: get: return self._id + ## THe name of the [Mod]. var name: get: return self._name + ## The individual or group that created the [Mod]. var author: get: return self._author + ## The unique IDs of all other [Mod]s that the [Mod] depends on. var dependencies: get: return self._dependencies + ## The unique IDs of all other [Mod]s that should be loaded before the [Mod]. var before: get: return self._before + ## The unique IDs of all other [Mod]s that should be loaded after the [Mod]. var after: get: return self._after + ## The unique IDs of all other [Mod]s that are incompatible with the [Mod]. var incompatible: get: return self._incompatible @@ -126,19 +139,18 @@ class Metadata extends RefCounted: var metadata_file_path = directory_path.path_join("mod.json") var file = File.new() if not file.file_exists(metadata_file_path): - Errors.mod_load_error(directory_path, "Mod metadata file does not exist") + Errors._mod_load_error(directory_path, "Mod metadata file does not exist") return null # Retrieve metadata file contents file.open(metadata_file_path, File.READ) var json = JSON.parse_string(file.get_as_text()) file.close() if not json is Dictionary: - Errors.mod_load_error(directory_path, "Mod metadata is invalid") + Errors._mod_load_error(directory_path, "Mod metadata is invalid") return null var meta = Mod.Metadata.new() meta._directory = directory_path return meta if meta._try_deserialize(json) and meta._is_valid() else null - func _try_deserialize(json): # Retrieve compulsory metadata @@ -146,7 +158,7 @@ class Metadata extends RefCounted: var name = json.get("name") var author = json.get("author") if not (id is String and name is String and author is String): - Errors.mod_load_error(self.directory, "Mod metadata contains invalid ID, name, or author") + Errors._mod_load_error(self.directory, "Mod metadata contains invalid ID, name, or author") return false self._id = id self._name = name @@ -156,25 +168,25 @@ class Metadata extends RefCounted: if dependencies is Array: self._dependencies = dependencies else: - Errors.mod_load_error(self.directory, "Mod metadata contains invalid dependencies") + Errors._mod_load_error(self.directory, "Mod metadata contains invalid dependencies") return false var before = json.get("before", []) if before is Array: self._before = before else: - Errors.mod_load_error(self.directory, "Mod metadata contains invalid load before list") + Errors._mod_load_error(self.directory, "Mod metadata contains invalid load before list") return false var after = json.get("after", []) if after is Array: self._after = after else: - Errors.mod_load_error(self.directory, "Mod metadata contains invalid load after list") + Errors._mod_load_error(self.directory, "Mod metadata contains invalid load after list") return false var incompatible = json.get("incompatible", []) if incompatible is Array: self._incompatible = incompatible else: - Errors.mod_load_error(self.directory, "Mod metadata contains invalid incompatibilities") + Errors._mod_load_error(self.directory, "Mod metadata contains invalid incompatibilities") return false return true @@ -190,7 +202,7 @@ class Metadata extends RefCounted: var valid_dependencies = self.dependencies.filter(func(id): return id in incompatible).is_empty() if valid_load_order and valid_dependencies: return true - Errors.mod_load_error(self.directory, "Mod metadata contains invalid load order or invalid dependencies") + Errors._mod_load_error(self.directory, "Mod metadata contains invalid load order or invalid dependencies") return false func _to_string(): diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd index 7489140..3b1b44d 100644 --- a/GDScript/modding/mod_loader.gd +++ b/GDScript/modding/mod_loader.gd @@ -1,11 +1,14 @@ +## Provides methods and properties for loading [Mod]s at runtime, obtaining all loaded [Mod]s, and finding a loaded [Mod] by its ID. extends Node var _loaded_mods = {} +## All the [Mod]s that have been loaded at runtime. var loaded_mods: get: return self._loaded_mods +## Loads a [Mod] from [code]mod_directory_path[/code] and runs all [code]_init()[/code] functions in its scripts if [code]execute_scripts[/code] is true. func load_mod(mod_directory_path, execute_scripts = true): var metadata = Mod.Metadata._load(mod_directory_path) if not metadata: @@ -16,6 +19,7 @@ func load_mod(mod_directory_path, execute_scripts = true): self._startup_mod(mod) return mod +## Loads [Mod]s from [code]mod_directory_paths[/code] and runs all [code]_init()[/code] functions in their scripts if [code]execute_scripts[/code] is true. func load_mods(mod_directory_paths, execute_scripts = true): var mods = [] for metadata in self._sort_mod_metadata(self._filter_mod_metadata(self._load_mod_metadata(mod_directory_paths))): @@ -38,10 +42,10 @@ func _load_mod_metadata(mod_directory_paths): # Fail if the metadata is incompatible with any of the loaded metadata (and vice-versa), or if the ID already exists var incompatible_metadata = metadata.incompatible.map(func(id): return loaded_metadata[id]).filter(func(loaded): return loaded != null) + loaded_metadata.values().filter(func(loaded): return metadata in loaded.incompatible) if not incompatible_metadata.is_empty(): - Errors.mod_load_error(metadata.directory, "Mod is incompatible with other loaded mods") + Errors._mod_load_error(metadata.directory, "Mod is incompatible with other loaded mods") continue elif metadata.id in loaded_metadata: - Errors.mod_load_error(metadata.directory, "Mod has duplicate ID") + Errors._mod_load_error(metadata.directory, "Mod has duplicate ID") continue loaded_metadata[metadata.id] = metadata return loaded_metadata @@ -50,7 +54,7 @@ func _filter_mod_metadata(loaded_metadata): # If the dependencies of any metadata have not been loaded, remove that metadata and try again var invalid_metadata = loaded_metadata.values().filter(func(metadata): return metadata.dependencies.any(func(dependency): return not dependency in loaded_metadata)) for metadata in invalid_metadata: - Errors.mod_load_error(metadata.directory, "Not all dependencies are loaded") + Errors._mod_load_error(metadata.directory, "Not all dependencies are loaded") loaded_metadata.erase(metadata.id) return self._filter_mod_metadata(loaded_metadata) return loaded_metadata @@ -71,7 +75,7 @@ func _sort_mod_metadata(filtered_metadata): dependency_graph[before].append(metadata.id) # Topologically sort the dependency graph, removing cyclic dependencies if any var sorted_metadata = ArrayExtensions.topological_sort(dependency_graph.keys(), func(id): return dependency_graph.get(id, []), func(cyclic): - Errors.mod_load_error(filtered_metadata[cyclic].directory, "Mod has cyclic dependencies with other mods") + Errors._mod_load_error(filtered_metadata[cyclic].directory, "Mod has cyclic dependencies with other mods") filtered_metadata.erase(cyclic)) # If there is no valid topological sorting (cyclic dependencies detected), remove the cyclic metadata and try again if sorted_metadata.is_empty(): diff --git a/GDScript/utility/errors.gd b/GDScript/utility/errors.gd index 968bcc3..1b504d1 100644 --- a/GDScript/utility/errors.gd +++ b/GDScript/utility/errors.gd @@ -1,4 +1,4 @@ class_name Errors -static func mod_load_error(directory_path, message): +static func _mod_load_error(directory_path, message): push_error("Error loading mod at %s: %s" % [directory_path, message]) diff --git a/GDScript/utility/extensions/array_extensions.gd b/GDScript/utility/extensions/array_extensions.gd index 7d90263..f34acb8 100644 --- a/GDScript/utility/extensions/array_extensions.gd +++ b/GDScript/utility/extensions/array_extensions.gd @@ -1,5 +1,6 @@ class_name ArrayExtensions +## Topologically sorts [code]array[/code] using [code]dependencies[/code] for each element's dependencies, and invoking [code]cyclic[/code] if a cyclic dependency is found. static func topological_sort(array, dependencies, cyclic): var sorted = [] var states = {} diff --git a/GDScript/utility/extensions/directory_extensions.gd b/GDScript/utility/extensions/directory_extensions.gd index 79c7cfd..cb94d92 100644 --- a/GDScript/utility/extensions/directory_extensions.gd +++ b/GDScript/utility/extensions/directory_extensions.gd @@ -1,5 +1,7 @@ +## Contains utility methods for [Directory]. class_name DirectoryExtensions +## Returns the complete paths of all subdirectories inside [code]directory[/code], searching recursively. static func get_directories_recursive(directory): var directories = [] for subdir_path in directory.get_directories(): @@ -10,6 +12,7 @@ static func get_directories_recursive(directory): directories.append_array(DirectoryExtensions.get_directories_recursive(subdir)) return directories +## Returns the complete paths of all files inside [code]directory[/code], searching recursively. static func get_files_recursive(directory): var files = [] for file_path in directory.get_files(): @@ -20,5 +23,6 @@ static func get_files_recursive(directory): files.append_array(subdir.get_files()) return files +## Returns the complete file paths of all files inside [code]directory[/code] whose extensions match any of [code]extensions[/code], searching recursively. static func get_files_recursive_ending(directory, extensions): return DirectoryExtensions.get_files_recursive(directory).filter(func(file_path): return extensions.any(func(extension): return file_path.ends_with(".%s" % extension))) From 619f0200f6ae95f655f0e25fb98e279231240497 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 11:16:15 +0100 Subject: [PATCH 18/32] Update gitignore --- .gitignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 785cf2f..43db2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ obj/ *.sln *.Dotsettings.user -**/*.godot -**/.mono/ -**/.import/ +*.godot +.mono/ +.import/ -**/.idea/ \ No newline at end of file +.idea/ \ No newline at end of file From c33eef3d0d1334a93bd4e6376c51ece203cf4fae Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 11:16:44 +0100 Subject: [PATCH 19/32] Update gitignore --- .gitignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 785cf2f..43db2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ obj/ *.sln *.Dotsettings.user -**/*.godot -**/.mono/ -**/.import/ +*.godot +.mono/ +.import/ -**/.idea/ \ No newline at end of file +.idea/ \ No newline at end of file From e9bd8a3c2d4bdb6957bce089f1373beebb0b1e5e Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 12:19:05 +0100 Subject: [PATCH 20/32] Update csproj --- C#/Modot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/C#/Modot.csproj b/C#/Modot.csproj index 510dc9b..3b47681 100644 --- a/C#/Modot.csproj +++ b/C#/Modot.csproj @@ -8,7 +8,7 @@ true true - 2.0.2 + 2.0.3 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. From 54379776bbd626a9a154f894edc240334cfa85a7 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 12:19:10 +0100 Subject: [PATCH 21/32] Update README --- README.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 73e2e7a..426d673 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,18 @@ Its API is aimed at allowing creators to easily modularise their Godot applicati # Features -- Load mods with C# assemblies, XML data, and resource packs at runtime +- Load mods with resource packs, XML or JSON data, and C# assemblies or GDScript scripts at runtime - Sort mods using load orders defined partially by each mod to prevent conflicts -- Patch XML data of other loaded mods without executing code -- Optionally execute code from mod assemblies upon loading +- Patch XML data of other loaded mods without executing mod code +- Optionally execute mod code 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). +A more detailed explanation of all features, instructions on usage, and **Modot**'s C# and GDScript API differences are available on the [wiki](https://github.com/Carnagion/Modot/wiki). # Installation +## C# + **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 @@ -31,16 +33,27 @@ Due to [a bug](https://github.com/godotengine/godot/issues/42271) in Godot, the ``` +## GDScript + +Currently, GDScript lacks a proper package management system, making it difficult to distribute **Modot** via a package manager. + +As such, the easiest way to install **Modot** for GDScript is to simply copy all files and directories under the `GDScript` directory and add them to the desired Godot project. + +# Tutorial + +A complete walkthrough on making a simple game using **Modot** is available on [GitHub](https://github.com/Carnagion/Pong). + # Security -**Modot** includes the ability to execute code from C# assemblies (`.dll` files) at runtime. +**Modot** includes the ability to execute code from C# assemblies (`.dll` files) and GDScript scripts (`.gd` 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. +This is unfortunately an issue that has no easy solution, as it is fairly difficult to accurately detect whether an assembly or script 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**. +As such, it is important to note that **Modot does not bear the responsibility of checking for potentially malicious code in a mod**. -However, it does provide the option to ignore a mod's assemblies, preventing any code from being executed. +However, it does provide the option to ignore a mod's assemblies or scripts, 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. As mentioned earlier though, **it is not Modot's responsibility to implement such checks**. \ No newline at end of file +Another way to prevent executing malicious code is by restricting the source of mods to websites that thoroughly scan and verify uploaded user content. +As mentioned earlier though, **it is not Modot's responsibility to implement such checks**. \ No newline at end of file From 2b680a4000efcd932e491319e56b1c18f4a0889c Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 12:19:49 +0100 Subject: [PATCH 22/32] Update csproj and README --- C#/Modot.csproj | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/C#/Modot.csproj b/C#/Modot.csproj index 3b47681..1472a54 100644 --- a/C#/Modot.csproj +++ b/C#/Modot.csproj @@ -8,7 +8,7 @@ true true - 2.0.3 + 3.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. diff --git a/README.md b/README.md index 426d673..c63bede 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A more detailed explanation of all features, instructions on usage, and **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 - + ``` From dd99dbf0a6ee32e01d8300a559ba5d7855b156cb Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 13:04:20 +0100 Subject: [PATCH 23/32] Update README --- README.md | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c63bede..4acc2fb 100644 --- a/README.md +++ b/README.md @@ -16,28 +16,24 @@ A more detailed explanation of all features, instructions on usage, and **Modot* # Installation -## C# - -**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 - -``` - -## GDScript - -Currently, GDScript lacks a proper package management system, making it difficult to distribute **Modot** via a package manager. - -As such, the easiest way to install **Modot** for GDScript is to simply copy all files and directories under the `GDScript` directory and add them to the desired Godot project. +- ## C# +**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 + + ``` + +- ## GDScript + Currently, GDScript lacks a proper package management system, making it difficult to distribute **Modot** via a package manager. + As such, the easiest way to install **Modot** for GDScript is to simply copy all files and directories under the `GDScript` directory and add them to the desired Godot project. # Tutorial From fd720f1782761a064ba388a5e25dd19a383f3e17 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 13:04:59 +0100 Subject: [PATCH 24/32] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4acc2fb..a0c56e0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A more detailed explanation of all features, instructions on usage, and **Modot* # Installation -- ## C# +- **C#** **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 @@ -31,7 +31,7 @@ A more detailed explanation of all features, instructions on usage, and **Modot* ``` -- ## GDScript +- **GDScript** Currently, GDScript lacks a proper package management system, making it difficult to distribute **Modot** via a package manager. As such, the easiest way to install **Modot** for GDScript is to simply copy all files and directories under the `GDScript` directory and add them to the desired Godot project. From bcc17de03b68757a9931ac576cddde735bd3c3e0 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 13:06:06 +0100 Subject: [PATCH 25/32] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0c56e0..4acc2fb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A more detailed explanation of all features, instructions on usage, and **Modot* # Installation -- **C#** +- ## C# **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 @@ -31,7 +31,7 @@ A more detailed explanation of all features, instructions on usage, and **Modot* ``` -- **GDScript** +- ## GDScript Currently, GDScript lacks a proper package management system, making it difficult to distribute **Modot** via a package manager. As such, the easiest way to install **Modot** for GDScript is to simply copy all files and directories under the `GDScript` directory and add them to the desired Godot project. From 1f07533cdc9272a89356f3bbf8261ce3a7def04e Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 1 Oct 2022 13:06:39 +0100 Subject: [PATCH 26/32] Tweak alignment in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4acc2fb..5d48419 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A more detailed explanation of all features, instructions on usage, and **Modot* # Installation - ## C# -**Modot** is available as a [NuGet package](https://www.nuget.org/packages/Modot). + **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 From 0e7d81e70f15c7f0c183e8fbfdacef44d3f46471 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Wed, 12 Oct 2022 11:36:25 +0100 Subject: [PATCH 27/32] Tweak README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d48419..8a60eff 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Its API is aimed at allowing creators to easily modularise their Godot applicati - Optionally execute mod code upon loading - Load mods individually, bypassing load order restrictions -A more detailed explanation of all features, instructions on usage, and **Modot**'s C# and GDScript API differences are available on the [wiki](https://github.com/Carnagion/Modot/wiki). +A more detailed explanation of all features, instructions on usage, and **Modot**'s C# and GDScript API differences can be found on the [wiki](https://github.com/Carnagion/Modot/wiki). # Installation From ffd3008229bc125a91d76d7c7f1b1dc0306961f8 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 5 Nov 2022 09:56:58 +0000 Subject: [PATCH 28/32] Add documentation comment --- GDScript/modding/mod.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd index dce4c7b..224ee3a 100644 --- a/GDScript/modding/mod.gd +++ b/GDScript/modding/mod.gd @@ -2,6 +2,7 @@ class_name Mod extends RefCounted +## Initializes a new [Mod] using the [code]metadata[/code]. func _init(metadata): self._meta = metadata self._load_resources() From a8c12ab64514de5618f0354cb3589d7f14932b6b Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 5 Nov 2022 10:13:22 +0000 Subject: [PATCH 29/32] Free mod script after running code --- GDScript/modding/mod_loader.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd index 3b1b44d..209c976 100644 --- a/GDScript/modding/mod_loader.gd +++ b/GDScript/modding/mod_loader.gd @@ -35,6 +35,8 @@ func _startup_mod(mod): for script in mod.scripts: script.reload() script.new() + if not (script is RefCounted): + script.free() func _load_mod_metadata(mod_directory_paths): var loaded_metadata = {} From 5c1ddb4f2ffef0f51fdb9a1345420925f0b0ce8e Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 5 Nov 2022 10:33:25 +0000 Subject: [PATCH 30/32] Update README --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8a60eff..ca7b696 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,14 @@ A more detailed explanation of all features, instructions on usage, and **Modot* # Installation -- ## C# - **Modot** is available as a [NuGet package](https://www.nuget.org/packages/Modot). +- **C#** + + **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: @@ -31,8 +33,10 @@ A more detailed explanation of all features, instructions on usage, and **Modot* ``` -- ## GDScript +- **GDScript** + Currently, GDScript lacks a proper package management system, making it difficult to distribute **Modot** via a package manager. + As such, the easiest way to install **Modot** for GDScript is to simply copy all files and directories under the `GDScript` directory and add them to the desired Godot project. # Tutorial From 4186375414f1eeb2a2d1a8d3658dc820fe642418 Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sat, 5 Nov 2022 10:36:59 +0000 Subject: [PATCH 31/32] Update csproj --- C#/Modot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/C#/Modot.csproj b/C#/Modot.csproj index 1472a54..3b47681 100644 --- a/C#/Modot.csproj +++ b/C#/Modot.csproj @@ -8,7 +8,7 @@ true true - 3.0.0 + 2.0.3 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. From b0b3b01833454c4394f73032c65593a94a1032be Mon Sep 17 00:00:00 2001 From: Carnagion Date: Sun, 6 Nov 2022 14:47:26 +0000 Subject: [PATCH 32/32] Add method for copying directory contents --- .../extensions/directory_extensions.gd | 27 +++++++++++++++++++ .../utility/extensions/string_extensions.gd | 11 ++++++++ 2 files changed, 38 insertions(+) create mode 100644 GDScript/utility/extensions/string_extensions.gd diff --git a/GDScript/utility/extensions/directory_extensions.gd b/GDScript/utility/extensions/directory_extensions.gd index cb94d92..91b4a6c 100644 --- a/GDScript/utility/extensions/directory_extensions.gd +++ b/GDScript/utility/extensions/directory_extensions.gd @@ -1,6 +1,33 @@ ## Contains utility methods for [Directory]. class_name DirectoryExtensions +## Copies all files from the directory at [code]from[/code] to the directory at [code]to[/code], recursively if specified. +static func copy_contents(directory, from, to, recursive = false): + directory.open(from) + # Create destination directory if it doesn't already exist + directory.make_dir_recursive(to) + var copied = [] + # Copy all files inside the source directory non-recursively + for from_file in directory.get_files(): + from_file = from.path_join(from_file) + var to_file = StringExtensions.replace_once(from_file, from, to) + directory.copy(from_file, to_file) + copied.append(to_file) + if not recursive: + return copied + # Copy all files recursively + for from_sub_directory in DirectoryExtensions.get_directories_recursive(directory): + var to_sub_directory = StringExtensions.replace_once(from_sub_directory, from, to) + directory.make_dir_recursive(to_sub_directory) + var inner_directory = Directory.new() + inner_directory.open(from_sub_directory) + for from_file in inner_directory.get_files(): + from_file = from_sub_directory.path_join(from_file) + var to_file = StringExtensions.replace_once(from_file, from, to) + directory.copy(from_file, to_file) + copied.append(to_file) + return copied + ## Returns the complete paths of all subdirectories inside [code]directory[/code], searching recursively. static func get_directories_recursive(directory): var directories = [] diff --git a/GDScript/utility/extensions/string_extensions.gd b/GDScript/utility/extensions/string_extensions.gd new file mode 100644 index 0000000..fd7af0e --- /dev/null +++ b/GDScript/utility/extensions/string_extensions.gd @@ -0,0 +1,11 @@ +class_name StringExtensions + +static func replace_once(string, substring, replacement): + var index = string.findn(substring) + match index: + -1: + return string + _: + var before = string.substr(0, index) + var after = string.substr(index + substring.length()) + return before + replacement + after