diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index 266ea9b5..02b68dea 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -77,6 +77,16 @@ var mod_missing_dependencies := {} # Things to keep to ensure they are not garbage collected (used by `save_scene`) var _saved_objects := [] +# Store all extenders paths +var script_extensions := [] + +# Store vanilla classes for script extension sorting +var loaded_vanilla_parents_cache := {} + +# Set to false after _init() +# Helps to decide whether a script extension should go through the _handle_script_extensions process +var is_initializing := true + # Main # ============================================================================= @@ -164,6 +174,12 @@ func _init() -> void: ModLoaderUtils.log_debug_json_print("mod data", mod_data, LOG_NAME) ModLoaderUtils.log_success("DONE: Completely finished loading mods", LOG_NAME) + + _handle_script_extensions() + + ModLoaderUtils.log_success("DONE: Installed all script extensions", LOG_NAME) + + is_initializing = false # Ensure ModLoader is the first autoload @@ -463,25 +479,112 @@ func _init_mod(mod: ModData) -> void: add_child(mod_main_instance, true) -# Helpers -# ============================================================================= +# Couple the extension paths with the parent paths and the extension's mod id +# in a ScriptExtensionData resource +func _handle_script_extensions()->void: + var script_extension_data_array := [] + for extension_path in script_extensions: + + if not File.new().file_exists(extension_path): + ModLoaderUtils.log_error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) + continue + + var child_script = ResourceLoader.load(extension_path) + + var mod_id:String = extension_path.trim_prefix(UNPACKED_DIR).get_slice("/", 0) + + var parent_script:Script = child_script.get_base_script() + var parent_script_path:String = parent_script.resource_path + + if not loaded_vanilla_parents_cache.keys().has(parent_script_path): + loaded_vanilla_parents_cache[parent_script_path] = parent_script + + script_extension_data_array.push_back( + ScriptExtensionData.new(extension_path, parent_script_path, mod_id) + ) + + # Sort the extensions based on dependencies + script_extension_data_array = _sort_extensions_from_load_order(script_extension_data_array) + + # Inheritance is more important so this called last + script_extension_data_array.sort_custom(self, "check_inheritances") + + # This saved some bugs in the past. + loaded_vanilla_parents_cache.clear() + + # Load and install all extensions + for extension in script_extension_data_array: + var script:Script = _apply_extension(extension.extension_path) + _reload_vanilla_child_classes_for(script) + + +# Sort an array of ScriptExtensionData following the load order +func _sort_extensions_from_load_order(extensions:Array)->Array: + var extensions_sorted := [] + + for _mod_data in mod_load_order: + for script in extensions: + if script.mod_id == _mod_data.dir_name: + extensions_sorted.push_front(script) + + return extensions_sorted + + +# Inheritance sorting +# Go up extension_a's inheritance tree to find if any parent shares the same vanilla path as extension_b +func _check_inheritances(extension_a:ScriptExtensionData, extension_b:ScriptExtensionData)->bool: + var a_child_script:Script + + if loaded_vanilla_parents_cache.keys().has(extension_a.parent_script_path): + a_child_script = ResourceLoader.load(extension_a.parent_script_path) + else: + a_child_script = ResourceLoader.load(extension_a.parent_script_path) + loaded_vanilla_parents_cache[extension_a.parent_script_path] = a_child_script + + var a_parent_script:Script = a_child_script.get_base_script() + + if a_parent_script == null: + return true + + var a_parent_script_path = a_parent_script.resource_path + if a_parent_script_path == extension_b.parent_script_path: + return false + + else: + return _check_inheritances(ScriptExtensionData.new(extension_a.extension_path, a_parent_script_path, extension_a.mod_id), extension_b) -# Helper functions to build mods -# Add a script that extends a vanilla script. `child_script_path` should point -# to your mod's extender script, eg "MOD/extensions/singletons/utils.gd". -# Inside that extender script, it should include "extends {target}", where -# {target} is the vanilla path, eg: `extends "res://singletons/utils.gd"`. -# Note that your extender script doesn't have to follow the same directory path -# as the vanilla file, but it's good practice to do so. -func install_script_extension(child_script_path: String) -> void: - # Check path to file exists - if not File.new().file_exists(child_script_path): - ModLoaderUtils.log_error("The child script path '%s' does not exist" % [child_script_path], LOG_NAME) +# 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 +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_setting("_global_script_classes") + + 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.class: + load(child_class.path).reload() - var child_script := ResourceLoader.load(child_script_path) +func _apply_extension(extension_path)->Script: + # Check path to file exists + if not File.new().file_exists(extension_path): + ModLoaderUtils.log_error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) + return null + + var child_script:Script = ResourceLoader.load(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 @@ -491,10 +594,35 @@ func install_script_extension(child_script_path: String) -> void: # The actual instance is thrown away. child_script.new() - var parent_script = child_script.get_base_script() - var parent_script_path: String = parent_script.resource_path - ModLoaderUtils.log_info("Installing script extension: %s <- %s" % [parent_script_path, child_script_path], LOG_NAME) + var parent_script:Script = child_script.get_base_script() + var parent_script_path:String = parent_script.resource_path + ModLoaderUtils.log_info("Installing script extension: %s <- %s" % [parent_script_path, extension_path], LOG_NAME) child_script.take_over_path(parent_script_path) + + return child_script + + +# Helpers +# ============================================================================= + +# Helper functions to build mods + +# Add a script that extends a vanilla script. `child_script_path` should point +# to your mod's extender script, eg "MOD/extensions/singletons/utils.gd". +# Inside that extender script, it should include "extends {target}", where +# {target} is the vanilla path, eg: `extends "res://singletons/utils.gd"`. +# Note that your extender script doesn't have to follow the same directory path +# as the vanilla file, but it's good practice to do so. +func install_script_extension(child_script_path:String): + + # If this is called during initialization, add it with the other + # extensions to be installed taking inheritance chain into account + if is_initializing: + script_extensions.push_back(child_script_path) + + # If not, apply the extension directly + else: + _apply_extension(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/script_extension_data.gd b/addons/mod_loader/script_extension_data.gd new file mode 100644 index 00000000..b6f8c8e4 --- /dev/null +++ b/addons/mod_loader/script_extension_data.gd @@ -0,0 +1,20 @@ +class_name ScriptExtensionData +extends Resource + +# Stores all Data defining a script extension + +# Full path to the extension file +var extension_path:String + +# Full path to the vanilla script extended +var parent_script_path:String + +# Mod requesting the extension +var mod_id:String + + +func _init(p_extension_path:String, p_parent_script_path:String, p_mod_id:String): + extension_path = p_extension_path + parent_script_path = p_parent_script_path + mod_id = p_mod_id +