diff --git a/addons/mod_loader/api/mod.gd b/addons/mod_loader/api/mod.gd index 4860bdbe..d880a29e 100644 --- a/addons/mod_loader/api/mod.gd +++ b/addons/mod_loader/api/mod.gd @@ -29,14 +29,10 @@ static func install_script_extension(child_script_path: String) -> void: ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()] = [] ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()].append(child_script_path) - # If this is called during initialization, add it with the other - # extensions to be installed taking inheritance chain into account - if ModLoaderStore.is_initializing: - ModLoaderStore.script_extensions.push_back(child_script_path) - - # If not, apply the extension directly - else: - _ModLoaderScriptExtension.apply_extension(child_script_path) + if not ModLoaderStore.is_initializing: + ModLoaderLog.error("Cannot install extension after init", LOG_NAME) + return + ModLoaderStore.script_extensions.push_back(child_script_path) # Register an array of classes to the global scope since Godot only does that in the editor. diff --git a/addons/mod_loader/api/mod_manager.gd b/addons/mod_loader/api/mod_manager.gd index e8c0da3c..2c570b9d 100644 --- a/addons/mod_loader/api/mod_manager.gd +++ b/addons/mod_loader/api/mod_manager.gd @@ -5,73 +5,3 @@ extends RefCounted const LOG_NAME := "ModLoader:Manager" - - -# Uninstall a script extension. -# -# Parameters: -# - extension_script_path (String): The path to the extension script to be uninstalled. -# -# Returns: void -static func uninstall_script_extension(extension_script_path: String) -> void: - # Currently this is the only thing we do, but it is better to expose - # this function like this for further changes - _ModLoaderScriptExtension.remove_specific_extension_from_script(extension_script_path) - - -# Reload all mods. -# -# *Note: This function should be called only when actually necessary -# as it can break the game and require a restart for mods -# that do not fully use the systems put in place by the mod loader, -# so anything that just uses add_node, move_node ecc... -# To not have your mod break on reload please use provided functions -# like ModLoader::save_scene, ModLoader::append_node_in_scene and -# all the functions that will be added in the next versions -# Used to reload already present mods and load new ones* -# -# Returns: void -static func reload_mods() -> void: - - # Currently this is the only thing we do, but it is better to expose - # this function like this for further changes - ModLoader._reload_mods() - - -# Disable all mods. -# -# *Note: This function should be called only when actually necessary -# as it can break the game and require a restart for mods -# that do not fully use the systems put in place by the mod loader, -# so anything that just uses add_node, move_node ecc... -# To not have your mod break on disable please use provided functions -# and implement a _disable function in your mod_main.gd that will -# handle removing all the changes that were not done through the Mod Loader* -# -# Returns: void -static func disable_mods() -> void: - - # Currently this is the only thing we do, but it is better to expose - # this function like this for further changes - ModLoader._disable_mods() - - -# Disable a mod. -# -# *Note: This function should be called only when actually necessary -# as it can break the game and require a restart for mods -# that do not fully use the systems put in place by the mod loader, -# so anything that just uses add_node, move_node ecc... -# To not have your mod break on disable please use provided functions -# and implement a _disable function in your mod_main.gd that will -# handle removing all the changes that were not done through the Mod Loader* -# -# Parameters: -# - mod_data (ModData): The ModData object representing the mod to be disabled. -# -# Returns: void -static func disable_mod(mod_data: ModData) -> void: - - # Currently this is the only thing we do, but it is better to expose - # this function like this for further changes - ModLoader._disable_mod(mod_data) diff --git a/addons/mod_loader/internal/script_extension.gd b/addons/mod_loader/internal/script_extension.gd index cd3f3a62..f14dd81d 100644 --- a/addons/mod_loader/internal/script_extension.gd +++ b/addons/mod_loader/internal/script_extension.gd @@ -10,22 +10,78 @@ const LOG_NAME := "ModLoader:ScriptExtension" # Sort script extensions by inheritance and apply them in order static func handle_script_extensions() -> void: var extension_paths := [] - for extension_path in ModLoaderStore.script_extensions: - if FileAccess.file_exists(extension_path): - extension_paths.push_back(extension_path) + for path in ModLoaderStore.script_extensions: + if FileAccess.file_exists(path): + extension_paths.push_back(path) else: - ModLoaderLog.error( - "The child script path '%s' does not exist" % [extension_path], LOG_NAME - ) - - # Sort by inheritance - extension_paths.sort_custom(Callable(InheritanceSorting, "_check_inheritances")) - - # Load and install all extensions - for extension in extension_paths: - var script: Script = apply_extension(extension) - _reload_vanilla_child_classes_for(script) - + ModLoaderLog.error("Extension script path '%s' does not exist" % [path], LOG_NAME) + + # sort by inheritance + extension_paths.sort_custom(InheritanceSorting.new()._check_inheritances) + + # used to replace the extends clause in the script + var extendsRegex := RegEx.new() + extendsRegex.compile("(?m)^extends \\\"(.*)\\\"") + + var chains = {} + + # prepare extensions chains: apply extends overrides, copy orig, ... + for path in extension_paths: + var script: Script = ResourceLoader.load(path) + var extending_script: Script = script.get_base_script() + if not extending_script: + ModLoaderLog.error("Extension script '%s' does not inherit from any other script" % [path], LOG_NAME) + continue + var extending_path: String = extending_script.resource_path + if extending_path.is_empty(): + ModLoaderLog.error("extending_path in '%s' is empty ??" % [path], LOG_NAME) + continue + + if not chains.has(extending_path): + var orig_path := "res://mod_loader_temp/" + extending_path.trim_prefix("res://") + # copy original script to the new path + _write_file(orig_path, extending_script.source_code) + chains[extending_path] = [ResourceLoader.load(orig_path)] + + var prev_path: String = chains[extending_path].front().resource_path + ModLoaderLog.info("Patching extends: %s -> %s" % [path, prev_path], LOG_NAME) + var patched_path := "res://mod_loader_temp/" + script.resource_path.trim_prefix("res://") + _write_file(patched_path, extendsRegex.sub(script.source_code, "extends \"" + prev_path + "\"")) + chains[extending_path].push_front(ResourceLoader.load(patched_path)) + + # apply final source code override and reload everything + for extending_path in chains: + var chain: Array = chains[extending_path] + + # overwrite real original script's code with the last extension, + # the hierarchy would eventually reach our copied original script + ModLoaderLog.info("Overwriting: %s -> %s" % [chain.front().resource_path, extending_path], LOG_NAME) + var extending_script: Script = ResourceLoader.load(extending_path) + extending_script.source_code = chain.front().source_code + + # reload order goes from orig to last applied extension + ModLoaderLog.info("Reloading chain: %s" % [extending_path], LOG_NAME) + for i in range(chain.size() - 1, 0, -1): + var script: Script = chain[i] + ModLoaderLog.info(" Reloading: %s" % [script.resource_path], LOG_NAME) + script.reload() + ModLoaderLog.info(" Reloading: %s" % [extending_path], LOG_NAME) + extending_script.reload() + + # clear temp directory + for i in range(chain.size() - 1, -1, -1): + var p: String = chain[i].resource_path.trim_prefix("res://") + while true: + var err := DirAccess.remove_absolute(p) + if err != Error.OK: + break + p = p.get_base_dir() + if not p.begins_with("mod_loader_temp"): + break + +static func _write_file(path: String, contents: String) -> void: + DirAccess.make_dir_recursive_absolute(path.get_base_dir().trim_prefix("res://")) + FileAccess.open(path, FileAccess.ModeFlags.WRITE).store_string(contents) # Sorts script paths by their ancestors. Scripts are organized by their common # ancestors then sorted such that scripts extending script A will be before @@ -70,172 +126,3 @@ class InheritanceSorting: stack_cache[extension_path] = stack return stack - - -static func apply_extension(extension_path: String) -> Script: - # Check path to file exists - if not FileAccess.file_exists(extension_path): - ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) - return null - - var child_script: Script = ResourceLoader.load(extension_path) - # Adding metadata that contains the extension script path - # We cannot get that path in any other way - # Passing the child_script as is would return the base script path - # Passing the .duplicate() would return a '' path - child_script.set_meta("extension_script_path", extension_path) - - # Force Godot to compile the script now. - # We need to do this here to ensure that the inheritance chain is - # properly set up, and multiple mods can chain-extend the same - # class multiple times. - # This is also needed to make Godot instantiate the extended class - # when creating singletons. - # The actual instance is thrown away. - child_script.new() - - var parent_script: Script = child_script.get_base_script() - var parent_script_path: String = parent_script.resource_path - - # We want to save scripts for resetting later - # All the scripts are saved in order already - if not ModLoaderStore.saved_scripts.has(parent_script_path): - ModLoaderStore.saved_scripts[parent_script_path] = [] - # The first entry in the saved script array that has the path - # used as a key will be the duplicate of the not modified script - ModLoaderStore.saved_scripts[parent_script_path].append(parent_script.duplicate()) - - ModLoaderStore.saved_scripts[parent_script_path].append(child_script) - - ModLoaderLog.info( - "Installing script extension: %s <- %s" % [parent_script_path, extension_path], LOG_NAME - ) - child_script.take_over_path(parent_script_path) - - return child_script - - -# Reload all children classes of the vanilla class we just extended -# Calling reload() the children of an extended class seems to allow them to be extended -# e.g if B is a child class of A, reloading B after apply an extender of A allows extenders of B to properly extend B, taking A's extender(s) into account -static func _reload_vanilla_child_classes_for(script: Script) -> void: - if script == null: - return - var current_child_classes := [] - var actual_path: String = script.get_base_script().resource_path - var classes: Array = ProjectSettings.get_global_class_list() - - for _class in classes: - if _class.path == actual_path: - current_child_classes.push_back(_class) - break - - for _class in current_child_classes: - for child_class in classes: - if child_class.base == _class.get_class(): - load(child_class.path).reload() - - -# Used to remove a specific extension -static func remove_specific_extension_from_script(extension_path: String) -> void: - # Check path to file exists - if not _ModLoaderFile.file_exists(extension_path): - ModLoaderLog.error( - 'The extension script path "%s" does not exist' % [extension_path], LOG_NAME - ) - return - - var extension_script: Script = ResourceLoader.load(extension_path) - var parent_script: Script = extension_script.get_base_script() - var parent_script_path: String = parent_script.resource_path - - # Check if the script to reset has been extended - if not ModLoaderStore.saved_scripts.has(parent_script_path): - ModLoaderLog.error( - 'The extension parent script path "%s" has not been extended' % [parent_script_path], - LOG_NAME - ) - return - - # Check if the script to reset has anything actually saved - # If we ever encounter this it means something went very wrong in extending - if not ModLoaderStore.saved_scripts[parent_script_path].size() > 0: - ModLoaderLog.error( - ( - 'The extension script path "%s" does not have the base script saved, this should never happen, if you encounter this please create an issue in the github repository' - % [parent_script_path] - ), - LOG_NAME - ) - return - - var parent_script_extensions: Array = ModLoaderStore.saved_scripts[parent_script_path].duplicate() - parent_script_extensions.remove_at(0) - - # Searching for the extension that we want to remove - var found_script_extension: Script = null - for script_extension in parent_script_extensions: - if script_extension.get_meta("extension_script_path") == extension_path: - found_script_extension = script_extension - break - - if found_script_extension == null: - ModLoaderLog.error( - ( - 'The extension script path "%s" has not been found in the saved extension of the base script' - % [parent_script_path] - ), - LOG_NAME - ) - return - parent_script_extensions.erase(found_script_extension) - - # Preparing the script to have all other extensions reapllied - _remove_all_extensions_from_script(parent_script_path) - - # Reapplying all the extensions without the removed one - for script_extension in parent_script_extensions: - apply_extension(script_extension.get_meta("extension_script_path")) - - -# Used to fully reset the provided script to a state prior of any extension -static func _remove_all_extensions_from_script(parent_script_path: String) -> void: - # Check path to file exists - if not _ModLoaderFile.file_exists(parent_script_path): - ModLoaderLog.error( - 'The parent script path "%s" does not exist' % [parent_script_path], LOG_NAME - ) - return - - # Check if the script to reset has been extended - if not ModLoaderStore.saved_scripts.has(parent_script_path): - ModLoaderLog.error( - 'The parent script path "%s" has not been extended' % [parent_script_path], LOG_NAME - ) - return - - # Check if the script to reset has anything actually saved - # If we ever encounter this it means something went very wrong in extending - if not ModLoaderStore.saved_scripts[parent_script_path].size() > 0: - ModLoaderLog.error( - ( - 'The parent script path "%s" does not have the base script saved, \nthis should never happen, if you encounter this please create an issue in the github repository' - % [parent_script_path] - ), - LOG_NAME - ) - return - - var parent_script: Script = ModLoaderStore.saved_scripts[parent_script_path][0] - parent_script.take_over_path(parent_script_path) - - # Remove the script after it has been reset so we do not do it again - ModLoaderStore.saved_scripts.erase(parent_script_path) - - -# Used to remove all extensions that are of a specific mod -static func remove_all_extensions_of_mod(mod: ModData) -> void: - var _to_remove_extension_paths: Array = ModLoaderStore.saved_extension_paths[mod.manifest.get_mod_id()] - for extension_path in _to_remove_extension_paths: - remove_specific_extension_from_script(extension_path) - ModLoaderStore.saved_extension_paths.erase(mod.manifest.get_mod_id()) diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index e12b7117..b597e264 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -182,27 +182,6 @@ func _load_mods() -> void: ModLoaderStore.is_initializing = false -# Internal call to reload mods -func _reload_mods() -> void: - _reset_mods() - _load_mods() - - -# Internal call that handles the resetting of all mod related data -func _reset_mods() -> void: - _disable_mods() - ModLoaderStore.mod_data.clear() - ModLoaderStore.mod_load_order.clear() - ModLoaderStore.mod_missing_dependencies.clear() - ModLoaderStore.script_extensions.clear() - - -# Internal call that handles the disabling of all mods -func _disable_mods() -> void: - for mod in ModLoaderStore.mod_data: - _disable_mod(ModLoaderStore.mod_data[mod]) - - # Check autoload positions: # Ensure 1st autoload is `ModLoaderStore`, and 2nd is `ModLoader`. func _check_autoload_positions() -> void: @@ -349,37 +328,10 @@ func _init_mod(mod: ModData) -> void: mod_main_instance = mod_main_script.new() mod_main_instance.name = mod.manifest.get_mod_id() - ModLoaderStore.saved_mod_mains[mod_main_path] = mod_main_instance - ModLoaderLog.debug("Adding child -> %s" % mod_main_instance, LOG_NAME) add_child(mod_main_instance, true) -# Call the disable method in every mod if present. -# This way developers can implement their own disable handling logic, -# that is needed if there are actions that are not done through the Mod Loader. -func _disable_mod(mod: ModData) -> void: - if mod == null: - ModLoaderLog.error("The provided ModData does not exist", LOG_NAME) - return - var mod_main_path := mod.get_required_mod_file_path(ModData.required_mod_files.MOD_MAIN) - - if not ModLoaderStore.saved_mod_mains.has(mod_main_path): - ModLoaderLog.error("The provided Mod %s has no saved mod main" % mod.manifest.get_mod_id(), LOG_NAME) - return - - var mod_main_instance: Node = ModLoaderStore.saved_mod_mains[mod_main_path] - if mod_main_instance.has_method("_disable"): - mod_main_instance._disable() - else: - ModLoaderLog.warning("The provided Mod %s does not have a \"_disable\" method" % mod.manifest.get_mod_id(), LOG_NAME) - - ModLoaderStore.saved_mod_mains.erase(mod_main_path) - _ModLoaderScriptExtension.remove_all_extensions_of_mod(mod) - - remove_child(mod_main_instance) - - # Deprecated # ============================================================================= diff --git a/addons/mod_loader/mod_loader_store.gd b/addons/mod_loader/mod_loader_store.gd index 54775ec2..9461b2c3 100644 --- a/addons/mod_loader/mod_loader_store.gd +++ b/addons/mod_loader/mod_loader_store.gd @@ -56,12 +56,6 @@ var has_shown_editor_zips_warning := false # Things to keep to ensure they are not garbage collected (used by `save_scene`) var saved_objects := [] -# Stores all the taken over scripts for restoration -var saved_scripts := {} - -# Stores main scripts for mod disabling -var saved_mod_mains := {} - # Stores script extension paths with the key being the namespace of a mod var saved_extension_paths := {}