Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

.godot
# IDEs
.idea
.vscode

# mac thing
**/.DS_Store
.DS_Store
4 changes: 3 additions & 1 deletion addons/mod_loader/internal/hooks.gd
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ static func add_hook(mod_callable: Callable, script_path: String, method_name: S
)

if not ModLoaderStore.hooked_script_paths.has(script_path):
ModLoaderStore.hooked_script_paths[script_path] = true
ModLoaderStore.hooked_script_paths[script_path] = [method_name]
elif not ModLoaderStore.hooked_script_paths[script_path].has(method_name):
ModLoaderStore.hooked_script_paths[script_path].append(method_name)


static func call_hooks(vanilla_method: Callable, args: Array, hook_hash: int) -> Variant:
Expand Down
25 changes: 14 additions & 11 deletions addons/mod_loader/internal/mod_hook_packer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,27 @@ static func start() -> void:
_ModLoaderCache.remove_data("hooks")
error = zip_writer.open(mod_hook_pack_path)
else:
# If there is a pack already append to it
# If there is a pack already, append to it
error = zip_writer.open(mod_hook_pack_path, ZIPPacker.APPEND_ADDINZIP)
if not error == OK:
ModLoaderLog.error("Error(%s) writing to zip file at path: %s" % [error, mod_hook_pack_path], LOG_NAME)
ModLoaderLog.error("Error (%s) writing to hooks zip, consider deleting this file: %s" % [error, mod_hook_pack_path], LOG_NAME)
return

ModLoaderLog.debug("Scripts requiring hooks: %s" % [ModLoaderStore.hooked_script_paths], LOG_NAME)

var cache := _ModLoaderCache.get_data("hooks")
var script_paths_with_hook: Array = [] if cache.is_empty() else cache.script_paths
var new_hooks_created := false
var cached_script_paths: Dictionary = {} if cache.is_empty() or not cache.has("hooked_script_paths") else cache.hooked_script_paths
if cached_script_paths == ModLoaderStore.hooked_script_paths:
ModLoaderLog.info("Scripts are already processed according to cache, skipping process.", LOG_NAME)
zip_writer.close()
return

var new_hooks_created := false
# Get all scripts that need processing
ModLoaderLog.debug("Scripts requiring hooks: %s" % [ModLoaderStore.hooked_script_paths.keys()], LOG_NAME)
for path in ModLoaderStore.hooked_script_paths.keys():
if path in script_paths_with_hook:
continue

var processed_source_code := hook_pre_processor.process_script_verbose(path)
var method_mask: Array[String] = []
method_mask.assign(ModLoaderStore.hooked_script_paths[path])
var processed_source_code := hook_pre_processor.process_script_verbose(path, false, method_mask)

# Skip writing to the zip if no new hooks were created for this script
if not hook_pre_processor.script_paths_hooked.has(path):
Expand All @@ -61,10 +65,9 @@ static func start() -> void:

ModLoaderLog.debug("Hooks created for script: %s" % path, LOG_NAME)
new_hooks_created = true
script_paths_with_hook.push_back(path)

if new_hooks_created:
_ModLoaderCache.update_data("hooks", {"script_paths": script_paths_with_hook})
_ModLoaderCache.update_data("hooks", {"hooked_script_paths": ModLoaderStore.hooked_script_paths})
_ModLoaderCache.save_to_file()
ModLoader.new_hooks_created.emit()

Expand Down
41 changes: 21 additions & 20 deletions addons/mod_loader/internal/mod_hook_preprocessor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,17 @@ func process_begin() -> void:
hashmap.clear()


func process_script_verbose(path: String, enable_hook_check := false) -> String:
func process_script_verbose(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
var start_time := Time.get_ticks_msec()
ModLoaderLog.debug("Start processing script at path: %s" % path, LOG_NAME)
var processed := process_script(path, enable_hook_check)
var processed := process_script(path, enable_hook_check, method_mask)
ModLoaderLog.debug("Finished processing script at path: %s in %s ms" % [path, Time.get_ticks_msec() - start_time], LOG_NAME)
return processed


func process_script(path: String, enable_hook_check := false) -> String:
func process_script(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
var current_script := load(path) as GDScript

var source_code := current_script.source_code

var source_code_additions := ""

# We need to stop all vanilla methods from forming inheritance chains,
Expand All @@ -72,32 +70,35 @@ func process_script(path: String, enable_hook_check := false) -> String:
var method_store: Array[String] = []

var getters_setters := collect_getters_and_setters(source_code)

var moddable_methods := current_script.get_script_method_list().filter(
is_func_moddable.bind(source_code, getters_setters)
)

var methods_hooked := {}

for method in moddable_methods:
if method.name in method_store:
continue

var prefix := "%s%s_" % [METHOD_PREFIX, class_prefix]
var full_prefix := "%s%s_" % [METHOD_PREFIX, class_prefix]

# Check if the method name starts with the prefix added by `edit_vanilla_method()`.
# This indicates that the method was previously processed, possibly by the export plugin.
# If so, store the method name (excluding the prefix) in `methods_hooked`.
if method.name.begins_with(prefix):
var method_name_vanilla: String = method.name.trim_prefix(prefix)
if method.name.begins_with(full_prefix):
var method_name_vanilla: String = method.name.trim_prefix(full_prefix)
methods_hooked[method_name_vanilla] = true
continue

# This ensures we avoid creating a hook for the 'imposter' method, which
# is generated by `build_mod_hook_string()` and has the vanilla method name.
if methods_hooked.has(method.name):
continue

# If a mask is provided, only methods with their name in the mask will be converted.
# Can't be pre-filtered since it removes prefixed methods required by the previous check.
if not method_mask.is_empty():
if not method.name in method_mask:
continue

var type_string := get_return_type_string(method.return)
var is_static := true if method.flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false

Expand Down Expand Up @@ -152,7 +153,7 @@ func process_script(path: String, enable_hook_check := false) -> String:
is_static,
is_async,
hook_id,
METHOD_PREFIX + class_prefix,
full_prefix,
enable_hook_check
)

Expand All @@ -167,7 +168,7 @@ func process_script(path: String, enable_hook_check := false) -> String:
source_code,
func_def,
func_body,
METHOD_PREFIX + class_prefix
full_prefix
)
source_code_additions += "\n%s" % mod_loader_hook_string

Expand Down Expand Up @@ -327,15 +328,15 @@ func edit_vanilla_method(
) -> String:
text = fix_method_super(method_name, func_body, text)
text = text.erase(func_def.get_start(), func_def.get_end() - func_def.get_start())
text = text.insert(func_def.get_start(), "func %s_%s(" % [prefix, method_name])
text = text.insert(func_def.get_start(), "func %s%s(" % [prefix, method_name])

return text


func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
if engine_version_hex < ENGINE_VERSION_HEX_4_2_2:
return fix_method_super_before_4_2_2(method_name, func_body, text)

return regex_super_call.sub(
text, "super.%s" % method_name,
true, func_body.get_start(), func_body.get_end()
Expand All @@ -344,18 +345,18 @@ func fix_method_super(method_name: String, func_body: RegExMatch, text: String)

# https://github.com/godotengine/godot/pull/86052
# Quote:
# When the end argument of RegEx.sub was used,
# When the end argument of RegEx.sub was used,
# it would truncate the Subject String before even doing the substitution.
func fix_method_super_before_4_2_2(method_name: String, func_body: RegExMatch, text: String) -> String:
var text_after_func_body_end := text.substr(func_body.get_end())

text = regex_super_call.sub(
text, "super.%s" % method_name,
true, func_body.get_start(), func_body.get_end()
)

text = text + text_after_func_body_end

return text


Expand Down
5 changes: 4 additions & 1 deletion addons/mod_loader/mod_loader_store.gd
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ const MOD_LOADER_DEV_TOOL_URL := "https://github.com/GodotModding/godot-mod-tool

var any_mod_hooked := false

# Stores arrays of hook callables that will be applied to a function,
# associated by a hash of the function name and script path
# Example:
# var modding_hooks := {
# 1917482423: [Callable, Callable],
# 3108290668: [Callable],
# }
var modding_hooks := {}

# Stores script paths and method names to be processed for hooks
# Example:
# var hooked_script_paths := {
# "res://game/game.gd": true,
# "res://game/game.gd": ["_ready", "do_something"],
# }
var hooked_script_paths := {}

Expand Down
Loading