Skip to content

Commit

Permalink
Merge pull request #124 from GDami/script-extensions-with-inheritance
Browse files Browse the repository at this point in the history
Inheritance checks for script extensions
  • Loading branch information
KANAjetzt authored Feb 24, 2023
2 parents b372e77 + 3b5d46d commit f92b68d
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 17 deletions.
162 changes: 145 additions & 17 deletions addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions addons/mod_loader/script_extension_data.gd
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit f92b68d

Please sign in to comment.