Skip to content

Commit 09c458d

Browse files
KANAjetztpirey0Qubus0
authored
feat: ✨ Mod Hooks (#408)
* feat: 🚧 first draft of the export plugin The plan is to automate the creation of hooks for each function in all GDScripts. * feat: 🚧 wip callable_stack * refactor: 🚧 rename vanilla functions Vanilla functions are renamed, and at the bottom of the script, mod loader hooks are generated. These hooks replace the vanilla functions and handle calls to the `callable_stack`. * feat: 🚧 added return with types to hook Currently, I'm stuck on this approach because the method list's return property does not contain any information if there is no return type defined for the method. * refactor: 🚧 reworked callable_sack funcs * refactor: 🚧 added return usage check That seems to work for filtering out methods that have a return. * refactor: 🚧 remove and add back class_name Without removing the `class_name` before initializing the script on export, I get this parse error: `hides a global script class`. With these code changes, I get `Value of type [script_path] cannot be assigned to a variable of type [class_name]` if there is a self-assignment somewhere in the script. * refactor: 🚧 No new script instance - load() the script 😶‍🌫️ Also added a check for static functions and currently working on recognizing inner classes. * refactor: 🚧 added `get_return_type_string` check the `type_hint` and add it if it's present. * refactor: 🚧 added `is_top_level_func` To check if the function has no space before `func` or `static`, used to ignore functions in inner classes. Later, we might want to figure out a way to include these. * fix: 🚧 corrected return type in generated methods Also removed extra comma in args list * feat: 🚧 only add modloader text to modified scripts until now all scripts received the "Mod Loader Hooks" header at the bottom even when no changes where made. * feat: 🚧 added optional @moddable requirement * feat: 🚧 added `is_setter` used to ignore setter funcs * feat: 🚧 improved runtime performance by pre-hashing dict-lookup Also refactored some of the function/field names to improved the modding experience * feat: 🚧 fixed issues with inheritance by adding class hash added getter method, made both static and moved getter/setter check to is_moddable check * cleaner getters and setters * feat: 🚧 added @not-moddable option to exclude methods improved performance by exiting early when nothing is hooked * refactor: ♻️ removed no longer used functions and a first style and type pass * refactor: 🚧 general code cleanup * 🚧 refactor: move methods * 🚧 refactor: extract methods * 🚧 feat: mod hook packing prototype * fix: 🚧 fix path to `mod_hook_preprocessor.gd` * 🚧 fix: typos * feat: 🚧 replace super calls * refactor: ♻️ remove static from regex vars * feat: 🚧 hook creation and loading (#3) * feat: 🚧 hook creation and loading * feat: ✨ hook creation and loading --------- Co-authored-by: Luca Martinelli <[email protected]> Co-authored-by: Qubus0 <[email protected]>
1 parent 6feaf59 commit 09c458d

File tree

14 files changed

+647
-7
lines changed

14 files changed

+647
-7
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
extends EditorExportPlugin
2+
3+
const ModHookPreprocessorScript := preload("res://addons/mod_loader/internal/mod_hook_preprocessor.gd")
4+
static var ModHookPreprocessor
5+
6+
7+
func _get_name() -> String:
8+
return "Godot Mod Loader Export Plugin"
9+
10+
11+
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
12+
ModHookPreprocessor = ModHookPreprocessorScript.new()
13+
ModHookPreprocessor.process_begin()
14+
15+
16+
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
17+
if path.begins_with("res://addons") or path.begins_with("res://mods-unpacked"):
18+
return
19+
20+
if type != "GDScript":
21+
return
22+
23+
skip()
24+
add_file(
25+
path,
26+
ModHookPreprocessor.process_script(path).to_utf8_buffer(),
27+
false
28+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[plugin]
2+
3+
name="A Mod Loader Export"
4+
description="Export plugin to generate the necessary callable stack."
5+
author="KANA"
6+
version="0.1"
7+
script="plugin.gd"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@tool
2+
extends EditorPlugin
3+
4+
5+
var _export_plugin: EditorExportPlugin
6+
7+
8+
func _enter_tree():
9+
_export_plugin = preload("res://addons/mod_loader/_export_plugin/export_plugin.gd").new()
10+
add_export_plugin(_export_plugin)
11+
12+
13+
func _exit_tree() -> void:
14+
remove_export_plugin(_export_plugin)

addons/mod_loader/api/mod.gd

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@ extends Object
1111
const LOG_NAME := "ModLoader:Mod"
1212

1313

14+
static func set_modding_hooks(new_callable_stack: Dictionary) -> void:
15+
ModLoaderStore.modding_hooks = new_callable_stack
16+
17+
18+
static func add_hook(mod_callable: Callable, script_path: String, method_name: String, is_before := false) -> void:
19+
ModLoaderStore.any_mod_hooked = true
20+
var hash = get_hook_hash(script_path,method_name,is_before)
21+
if not ModLoaderStore.modding_hooks.has(hash):
22+
ModLoaderStore.modding_hooks[hash] = []
23+
ModLoaderStore.modding_hooks[hash].push_back(mod_callable)
24+
ModLoaderLog.debug("Added hook script: \"%s\" method: \"%s\" is_before: \"%s\"" % [script_path, method_name, is_before], LOG_NAME)
25+
if not ModLoaderStore.hooked_script_paths.has(script_path):
26+
ModLoaderStore.hooked_script_paths[script_path] = null
27+
28+
29+
static func call_hooks(self_object: Object, args: Array, hook_hash:int) -> void:
30+
var hooks = ModLoaderStore.modding_hooks.get(hook_hash, null)
31+
if hooks:
32+
for mod_func in hooks:
33+
mod_func.call(self_object, args)
34+
35+
36+
static func get_hook_hash(path:String, method:String, is_before:bool) -> int:
37+
return hash(path + method + ("before" if is_before else "after"))
38+
39+
1440
## Installs a script extension that extends a vanilla script.[br]
1541
## The [code]child_script_path[/code] should point to your mod's extender script.[br]
1642
## Example: [code]"MOD/extensions/singletons/utils.gd"[/code][br]

addons/mod_loader/internal/cache.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ static func add_data(key: String, data: Dictionary) -> Dictionary:
3131
# Get data from a specific key
3232
static func get_data(key: String) -> Dictionary:
3333
if not ModLoaderStore.cache.has(key):
34-
ModLoaderLog.error("key: \"%s\" not found in \"ModLoaderStore.cache\"" % key, LOG_NAME)
34+
ModLoaderLog.info("key: \"%s\" not found in \"ModLoaderStore.cache\"" % key, LOG_NAME)
3535
return {}
3636

3737
return ModLoaderStore.cache[key]

addons/mod_loader/internal/file.gd

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,3 @@ static func dir_exists(path: String) -> bool:
227227
# modders in understanding and troubleshooting issues.
228228
static func _code_note(_msg:String):
229229
pass
230-
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class_name _ModLoaderModHookPacker
2+
extends RefCounted
3+
4+
5+
# This class is used to generate mod hooks on demand and pack them into a zip file.
6+
# Currently all of the included functions are internal and should only be used by the mod loader itself.
7+
8+
const LOG_NAME := "ModLoader:ModHookPacker"
9+
10+
11+
static func start() -> void:
12+
var hook_pre_processor = _ModLoaderModHookPreProcessor.new()
13+
hook_pre_processor.process_begin()
14+
15+
var mod_hook_pack_path := _ModLoaderPath.get_path_to_hook_pack()
16+
17+
# Create mod hook pack path if necessary
18+
if not DirAccess.dir_exists_absolute(mod_hook_pack_path.get_base_dir()):
19+
var error := DirAccess.make_dir_recursive_absolute(mod_hook_pack_path.get_base_dir())
20+
if not error == OK:
21+
ModLoaderLog.error("Error creating the mod hook directory at %s" % mod_hook_pack_path, LOG_NAME)
22+
return
23+
ModLoaderLog.debug("Created dir at: %s" % mod_hook_pack_path, LOG_NAME)
24+
25+
# Create mod hook zip
26+
var zip_writer := ZIPPacker.new()
27+
var error: Error
28+
29+
if not FileAccess.file_exists(mod_hook_pack_path):
30+
# Clear cache if the hook pack does not exist
31+
_ModLoaderCache.remove_data("hooks")
32+
error = zip_writer.open(mod_hook_pack_path)
33+
else:
34+
# If there is a pack already append to it
35+
error = zip_writer.open(mod_hook_pack_path, ZIPPacker.APPEND_ADDINZIP)
36+
if not error == OK:
37+
ModLoaderLog.error("Error(%s) writing to zip file at path: %s" % [error, mod_hook_pack_path], LOG_NAME)
38+
return
39+
40+
var cache := _ModLoaderCache.get_data("hooks")
41+
var script_paths_with_hook: Array = [] if cache.is_empty() else cache.script_paths
42+
var new_hooks_created := false
43+
44+
# Get all scripts that need processing
45+
ModLoaderLog.debug("Scripts requiring hooks: %s" % [ModLoaderStore.hooked_script_paths.keys()], LOG_NAME)
46+
for path in ModLoaderStore.hooked_script_paths.keys():
47+
if path in script_paths_with_hook:
48+
continue
49+
50+
var processed_source_code := hook_pre_processor.process_script(path)
51+
52+
zip_writer.start_file(path.trim_prefix("res://"))
53+
zip_writer.write_file(processed_source_code.to_utf8_buffer())
54+
zip_writer.close_file()
55+
56+
ModLoaderLog.debug("Hooks created for script: %s" % path, LOG_NAME)
57+
new_hooks_created = true
58+
script_paths_with_hook.push_back(path)
59+
60+
if new_hooks_created:
61+
_ModLoaderCache.update_data("hooks", {"script_paths": script_paths_with_hook})
62+
_ModLoaderCache.save_to_file()
63+
ModLoader.new_hooks_created.emit()
64+
65+
zip_writer.close()

0 commit comments

Comments
 (0)