diff --git a/.gitignore b/.gitignore
index d87d1ac..43db2b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,10 @@
bin/
obj/
*.sln
+*.Dotsettings.user
+
+*.godot
.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 89%
rename from Modding/Patching/NodeReplacePatch.cs
rename to C#/Modding/Patching/NodeReplacePatch.cs
index fa9b390..b0aab2b 100644
--- a/Modding/Patching/NodeReplacePatch.cs
+++ b/C#/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);
}
}
}
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 80%
rename from Modot.csproj
rename to C#/Modot.csproj
index 0125df0..3b47681 100644
--- a/Modot.csproj
+++ b/C#/Modot.csproj
@@ -8,12 +8,12 @@
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.
https://github.com/Carnagion/Modot
- LICENSE
+ ../LICENSE
@@ -21,11 +21,11 @@
-
-
-
+
+
+
-
+
\ 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
diff --git a/GDScript/modding/mod.gd b/GDScript/modding/mod.gd
new file mode 100644
index 0000000..224ee3a
--- /dev/null
+++ b/GDScript/modding/mod.gd
@@ -0,0 +1,210 @@
+## Represents a modular component loaded at runtime, with its own scripts, resource packs, and data.
+class_name Mod
+extends RefCounted
+
+## Initializes a new [Mod] using the [code]metadata[/code].
+func _init(metadata):
+ self._meta = metadata
+ self._load_resources()
+ self._load_data()
+ self._load_scripts()
+
+var _meta
+
+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
+
+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)
+
+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"]):
+ 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
+
+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(directory_path):
+ return []
+ directory.open(directory_path)
+ var file = File.new()
+ 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]
+
+## Represents the metadata of a [Mod], such as its unique ID, name, author, load order, etc.
+class Metadata extends RefCounted:
+
+ var _directory
+
+ var _id
+
+ var _name
+
+ var _author
+
+ var _dependencies
+
+ var _before
+
+ var _after
+
+ 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
+
+ static func _load(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
+ 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
+ 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
+
+ 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]
diff --git a/GDScript/modding/mod_loader.gd b/GDScript/modding/mod_loader.gd
new file mode 100644
index 0000000..209c976
--- /dev/null
+++ b/GDScript/modding/mod_loader.gd
@@ -0,0 +1,85 @@
+## 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:
+ return null
+ var mod = Mod.new(metadata)
+ self._loaded_mods[mod.meta.id] = mod
+ if execute_scripts:
+ 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))):
+ 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()
+ if not (script is RefCounted):
+ script.free()
+
+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)
diff --git a/GDScript/utility/errors.gd b/GDScript/utility/errors.gd
new file mode 100644
index 0000000..1b504d1
--- /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..f34acb8
--- /dev/null
+++ b/GDScript/utility/extensions/array_extensions.gd
@@ -0,0 +1,26 @@
+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 = {}
+ 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..91b4a6c
--- /dev/null
+++ b/GDScript/utility/extensions/directory_extensions.gd
@@ -0,0 +1,55 @@
+## 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 = []
+ 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
+
+## 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():
+ 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
+
+## 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)))
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
diff --git a/README.md b/README.md
index 73e2e7a..ca7b696 100644
--- a/README.md
+++ b/README.md
@@ -6,41 +6,54 @@ 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 can be found 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
-
-```
+- **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
+
+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