Skip to content

Commit 5c61730

Browse files
Qubus0KANAjetzt
andauthored
feat: per method hooks (#534)
* feat: 426 make hooks apply per method instead of file * move when masking is done * fix: 🔥 remove `_` * fix prefix --------- Co-authored-by: Kai <kai@kana.jetzt>
1 parent 22e9169 commit 5c61730

File tree

5 files changed

+50
-38
lines changed

5 files changed

+50
-38
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11

2+
.godot
23
# IDEs
34
.idea
45
.vscode
56

67
# mac thing
7-
**/.DS_Store
8+
.DS_Store

addons/mod_loader/internal/hooks.gd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ static func add_hook(mod_callable: Callable, script_path: String, method_name: S
2020
)
2121

2222
if not ModLoaderStore.hooked_script_paths.has(script_path):
23-
ModLoaderStore.hooked_script_paths[script_path] = true
23+
ModLoaderStore.hooked_script_paths[script_path] = [method_name]
24+
elif not ModLoaderStore.hooked_script_paths[script_path].has(method_name):
25+
ModLoaderStore.hooked_script_paths[script_path].append(method_name)
2426

2527

2628
static func call_hooks(vanilla_method: Callable, args: Array, hook_hash: int) -> Variant:

addons/mod_loader/internal/mod_hook_packer.gd

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,27 @@ static func start() -> void:
3232
_ModLoaderCache.remove_data("hooks")
3333
error = zip_writer.open(mod_hook_pack_path)
3434
else:
35-
# If there is a pack already append to it
35+
# If there is a pack already, append to it
3636
error = zip_writer.open(mod_hook_pack_path, ZIPPacker.APPEND_ADDINZIP)
3737
if not error == OK:
38-
ModLoaderLog.error("Error(%s) writing to zip file at path: %s" % [error, mod_hook_pack_path], LOG_NAME)
38+
ModLoaderLog.error("Error (%s) writing to hooks zip, consider deleting this file: %s" % [error, mod_hook_pack_path], LOG_NAME)
3939
return
4040

41+
ModLoaderLog.debug("Scripts requiring hooks: %s" % [ModLoaderStore.hooked_script_paths], LOG_NAME)
42+
4143
var cache := _ModLoaderCache.get_data("hooks")
42-
var script_paths_with_hook: Array = [] if cache.is_empty() else cache.script_paths
43-
var new_hooks_created := false
44+
var cached_script_paths: Dictionary = {} if cache.is_empty() or not cache.has("hooked_script_paths") else cache.hooked_script_paths
45+
if cached_script_paths == ModLoaderStore.hooked_script_paths:
46+
ModLoaderLog.info("Scripts are already processed according to cache, skipping process.", LOG_NAME)
47+
zip_writer.close()
48+
return
4449

50+
var new_hooks_created := false
4551
# Get all scripts that need processing
46-
ModLoaderLog.debug("Scripts requiring hooks: %s" % [ModLoaderStore.hooked_script_paths.keys()], LOG_NAME)
4752
for path in ModLoaderStore.hooked_script_paths.keys():
48-
if path in script_paths_with_hook:
49-
continue
50-
51-
var processed_source_code := hook_pre_processor.process_script_verbose(path)
53+
var method_mask: Array[String] = []
54+
method_mask.assign(ModLoaderStore.hooked_script_paths[path])
55+
var processed_source_code := hook_pre_processor.process_script_verbose(path, false, method_mask)
5256

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

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

6669
if new_hooks_created:
67-
_ModLoaderCache.update_data("hooks", {"script_paths": script_paths_with_hook})
70+
_ModLoaderCache.update_data("hooks", {"hooked_script_paths": ModLoaderStore.hooked_script_paths})
6871
_ModLoaderCache.save_to_file()
6972
ModLoader.new_hooks_created.emit()
7073

addons/mod_loader/internal/mod_hook_preprocessor.gd

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,21 @@ var script_paths_hooked := {}
5050
func process_begin() -> void:
5151
hashmap.clear()
5252

53-
54-
func process_script_verbose(path: String, enable_hook_check := false) -> String:
53+
## Calls [method process_script] with additional logging
54+
func process_script_verbose(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
5555
var start_time := Time.get_ticks_msec()
5656
ModLoaderLog.debug("Start processing script at path: %s" % path, LOG_NAME)
57-
var processed := process_script(path, enable_hook_check)
57+
var processed := process_script(path, enable_hook_check, method_mask)
5858
ModLoaderLog.debug("Finished processing script at path: %s in %s ms" % [path, Time.get_ticks_msec() - start_time], LOG_NAME)
5959
return processed
6060

6161

62-
func process_script(path: String, enable_hook_check := false) -> String:
62+
## [param path]: File path to the script to be processed.[br]
63+
## [param enable_hook_check]: Adds a check that ModLoaderStore.any_mod_hooked is [code]true[/code] to the processed method, reducing hash checks.[br]
64+
## [param method_mask]: If provided, only methods in this [Array] will be processed.[br]
65+
func process_script(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
6366
var current_script := load(path) as GDScript
64-
6567
var source_code := current_script.source_code
66-
6768
var source_code_additions := ""
6869

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

7475
var getters_setters := collect_getters_and_setters(source_code)
75-
7676
var moddable_methods := current_script.get_script_method_list().filter(
7777
is_func_moddable.bind(source_code, getters_setters)
7878
)
7979

8080
var methods_hooked := {}
81-
8281
for method in moddable_methods:
8382
if method.name in method_store:
8483
continue
8584

86-
var prefix := "%s%s_" % [METHOD_PREFIX, class_prefix]
85+
var full_prefix := "%s%s_" % [METHOD_PREFIX, class_prefix]
8786

8887
# Check if the method name starts with the prefix added by `edit_vanilla_method()`.
8988
# This indicates that the method was previously processed, possibly by the export plugin.
9089
# If so, store the method name (excluding the prefix) in `methods_hooked`.
91-
if method.name.begins_with(prefix):
92-
var method_name_vanilla: String = method.name.trim_prefix(prefix)
90+
if method.name.begins_with(full_prefix):
91+
var method_name_vanilla: String = method.name.trim_prefix(full_prefix)
9392
methods_hooked[method_name_vanilla] = true
9493
continue
95-
9694
# This ensures we avoid creating a hook for the 'imposter' method, which
9795
# is generated by `build_mod_hook_string()` and has the vanilla method name.
9896
if methods_hooked.has(method.name):
9997
continue
10098

99+
# If a mask is provided, only methods with their name in the mask will be converted.
100+
# Can't be filtered before the loop since it removes prefixed methods required by the previous check.
101+
if not method_mask.is_empty():
102+
if not method.name in method_mask:
103+
continue
104+
101105
var type_string := get_return_type_string(method.return)
102106
var is_static := true if method.flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false
103107

@@ -152,7 +156,7 @@ func process_script(path: String, enable_hook_check := false) -> String:
152156
is_static,
153157
is_async,
154158
hook_id,
155-
METHOD_PREFIX + class_prefix,
159+
full_prefix,
156160
enable_hook_check
157161
)
158162

@@ -167,7 +171,7 @@ func process_script(path: String, enable_hook_check := false) -> String:
167171
source_code,
168172
func_def,
169173
func_body,
170-
METHOD_PREFIX + class_prefix
174+
full_prefix
171175
)
172176
source_code_additions += "\n%s" % mod_loader_hook_string
173177

@@ -327,15 +331,15 @@ func edit_vanilla_method(
327331
) -> String:
328332
text = fix_method_super(method_name, func_body, text)
329333
text = text.erase(func_def.get_start(), func_def.get_end() - func_def.get_start())
330-
text = text.insert(func_def.get_start(), "func %s_%s(" % [prefix, method_name])
334+
text = text.insert(func_def.get_start(), "func %s%s(" % [prefix, method_name])
331335

332336
return text
333337

334338

335-
func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
339+
func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
336340
if engine_version_hex < ENGINE_VERSION_HEX_4_2_2:
337341
return fix_method_super_before_4_2_2(method_name, func_body, text)
338-
342+
339343
return regex_super_call.sub(
340344
text, "super.%s" % method_name,
341345
true, func_body.get_start(), func_body.get_end()
@@ -344,18 +348,18 @@ func fix_method_super(method_name: String, func_body: RegExMatch, text: String)
344348

345349
# https://github.com/godotengine/godot/pull/86052
346350
# Quote:
347-
# When the end argument of RegEx.sub was used,
351+
# When the end argument of RegEx.sub was used,
348352
# it would truncate the Subject String before even doing the substitution.
349353
func fix_method_super_before_4_2_2(method_name: String, func_body: RegExMatch, text: String) -> String:
350354
var text_after_func_body_end := text.substr(func_body.get_end())
351-
355+
352356
text = regex_super_call.sub(
353357
text, "super.%s" % method_name,
354358
true, func_body.get_start(), func_body.get_end()
355359
)
356-
360+
357361
text = text + text_after_func_body_end
358-
362+
359363
return text
360364

361365

@@ -397,10 +401,9 @@ static func build_mod_hook_string(
397401
return_string, await_string, method_prefix, method_name, method_arg_string_names_only
398402
) if enable_hook_check else ""
399403

400-
401404
return """
402405
{STATIC}func {METHOD_NAME}({METHOD_PARAMS}){RETURN_TYPE_STRING}:
403-
{HOOK_CHECK}{RETURN}{AWAIT}_ModLoaderHooks.call_hooks{ASYNC}({METHOD_PREFIX}_{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID}){HOOK_CHECK_ELSE}
406+
{HOOK_CHECK}{RETURN}{AWAIT}_ModLoaderHooks.call_hooks{ASYNC}({METHOD_PREFIX}{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID}){HOOK_CHECK_ELSE}
404407
""".format({
405408
"METHOD_PREFIX": method_prefix,
406409
"METHOD_NAME": method_name,
@@ -551,7 +554,7 @@ static func get_hook_check_else_string(
551554
method_name: String,
552555
method_arg_string_names_only: String
553556
) -> String:
554-
return "\n\telse:\n\t\t{RETURN}{AWAIT}{METHOD_PREFIX}_{METHOD_NAME}({METHOD_ARGS})".format(
557+
return "\n\telse:\n\t\t{RETURN}{AWAIT}{METHOD_PREFIX}{METHOD_NAME}({METHOD_ARGS})".format(
555558
{
556559
"RETURN": return_string,
557560
"AWAIT": await_string,

addons/mod_loader/mod_loader_store.gd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,19 @@ const MOD_LOADER_DEV_TOOL_URL := "https://github.com/GodotModding/godot-mod-tool
3434

3535
var any_mod_hooked := false
3636

37+
# Stores arrays of hook callables that will be applied to a function,
38+
# associated by a hash of the function name and script path
3739
# Example:
3840
# var modding_hooks := {
3941
# 1917482423: [Callable, Callable],
4042
# 3108290668: [Callable],
4143
# }
4244
var modding_hooks := {}
4345

46+
# Stores script paths and method names to be processed for hooks
4447
# Example:
4548
# var hooked_script_paths := {
46-
# "res://game/game.gd": true,
49+
# "res://game/game.gd": ["_ready", "do_something"],
4750
# }
4851
var hooked_script_paths := {}
4952

0 commit comments

Comments
 (0)