- 
                Notifications
    You must be signed in to change notification settings 
- Fork 44
feat: ✨ Mod Hooks #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: ✨ Mod Hooks #408
Changes from all commits
6df0b4d
              36ff6be
              abf1d76
              f2dffe2
              d01e030
              b553487
              ba89eeb
              e1d4533
              3ea4c26
              a9c80fb
              43adc11
              34020fd
              e49652c
              84e901c
              5f3a5e4
              fecbc52
              04dbf78
              d42c7f3
              f1ea296
              cae54ae
              e912619
              35bab06
              479a53a
              5f3c581
              088d379
              8343f52
              7c78a92
              addb0a5
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| extends EditorExportPlugin | ||
|  | ||
| const ModHookPreprocessorScript := preload("res://addons/mod_loader/internal/mod_hook_preprocessor.gd") | ||
| static var ModHookPreprocessor | ||
|  | ||
|  | ||
| func _get_name() -> String: | ||
| return "Godot Mod Loader Export Plugin" | ||
|  | ||
|  | ||
| func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void: | ||
| ModHookPreprocessor = ModHookPreprocessorScript.new() | ||
| ModHookPreprocessor.process_begin() | ||
|  | ||
|  | ||
| func _export_file(path: String, type: String, features: PackedStringArray) -> void: | ||
| if path.begins_with("res://addons") or path.begins_with("res://mods-unpacked"): | ||
| return | ||
|  | ||
| if type != "GDScript": | ||
| return | ||
|  | ||
| skip() | ||
| add_file( | ||
| path, | ||
| ModHookPreprocessor.process_script(path).to_utf8_buffer(), | ||
| false | ||
| ) | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| [plugin] | ||
|  | ||
| name="A Mod Loader Export" | ||
| description="Export plugin to generate the necessary callable stack." | ||
| author="KANA" | ||
| version="0.1" | ||
| script="plugin.gd" | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| @tool | ||
| extends EditorPlugin | ||
|  | ||
|  | ||
| var _export_plugin: EditorExportPlugin | ||
|  | ||
|  | ||
| func _enter_tree(): | ||
| _export_plugin = preload("res://addons/mod_loader/_export_plugin/export_plugin.gd").new() | ||
| add_export_plugin(_export_plugin) | ||
|  | ||
|  | ||
| func _exit_tree() -> void: | ||
| remove_export_plugin(_export_plugin) | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -11,6 +11,32 @@ extends Object | |
| const LOG_NAME := "ModLoader:Mod" | ||
|  | ||
|  | ||
| static func set_modding_hooks(new_callable_stack: Dictionary) -> void: | ||
| ModLoaderStore.modding_hooks = new_callable_stack | ||
|  | ||
|  | ||
| static func add_hook(mod_callable: Callable, script_path: String, method_name: String, is_before := false) -> void: | ||
| ModLoaderStore.any_mod_hooked = true | ||
| var hash = get_hook_hash(script_path,method_name,is_before) | ||
| if not ModLoaderStore.modding_hooks.has(hash): | ||
| ModLoaderStore.modding_hooks[hash] = [] | ||
| ModLoaderStore.modding_hooks[hash].push_back(mod_callable) | ||
| ModLoaderLog.debug("Added hook script: \"%s\" method: \"%s\" is_before: \"%s\"" % [script_path, method_name, is_before], LOG_NAME) | ||
| if not ModLoaderStore.hooked_script_paths.has(script_path): | ||
| ModLoaderStore.hooked_script_paths[script_path] = null | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: We are using a Dictionary as a Hashset here. | ||
|  | ||
|  | ||
| static func call_hooks(self_object: Object, args: Array, hook_hash:int) -> void: | ||
| var hooks = ModLoaderStore.modding_hooks.get(hook_hash, null) | ||
| if hooks: | ||
| for mod_func in hooks: | ||
| mod_func.call(self_object, args) | ||
|  | ||
|  | ||
| static func get_hook_hash(path:String, method:String, is_before:bool) -> int: | ||
| return hash(path + method + ("before" if is_before else "after")) | ||
|  | ||
|  | ||
| ## Installs a script extension that extends a vanilla script.[br] | ||
| ## The [code]child_script_path[/code] should point to your mod's extender script.[br] | ||
| ## Example: [code]"MOD/extensions/singletons/utils.gd"[/code][br] | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| class_name _ModLoaderModHookPacker | ||
| extends RefCounted | ||
|  | ||
|  | ||
| # This class is used to generate mod hooks on demand and pack them into a zip file. | ||
| # Currently all of the included functions are internal and should only be used by the mod loader itself. | ||
|  | ||
| const LOG_NAME := "ModLoader:ModHookPacker" | ||
|  | ||
|  | ||
| static func start() -> void: | ||
| var hook_pre_processor = _ModLoaderModHookPreProcessor.new() | ||
| hook_pre_processor.process_begin() | ||
|  | ||
| var mod_hook_pack_path := _ModLoaderPath.get_path_to_hook_pack() | ||
|  | ||
| # Create mod hook pack path if necessary | ||
| if not DirAccess.dir_exists_absolute(mod_hook_pack_path.get_base_dir()): | ||
| var error := DirAccess.make_dir_recursive_absolute(mod_hook_pack_path.get_base_dir()) | ||
| if not error == OK: | ||
| ModLoaderLog.error("Error creating the mod hook directory at %s" % mod_hook_pack_path, LOG_NAME) | ||
| return | ||
| ModLoaderLog.debug("Created dir at: %s" % mod_hook_pack_path, LOG_NAME) | ||
|  | ||
| # Create mod hook zip | ||
| var zip_writer := ZIPPacker.new() | ||
| var error: Error | ||
|  | ||
| if not FileAccess.file_exists(mod_hook_pack_path): | ||
| # Clear cache if the hook pack does not exist | ||
| _ModLoaderCache.remove_data("hooks") | ||
| error = zip_writer.open(mod_hook_pack_path) | ||
| else: | ||
| # 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) | ||
| return | ||
|  | ||
| 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 | ||
|  | ||
| # 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(path) | ||
|  | ||
| zip_writer.start_file(path.trim_prefix("res://")) | ||
| zip_writer.write_file(processed_source_code.to_utf8_buffer()) | ||
| zip_writer.close_file() | ||
|  | ||
| 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.save_to_file() | ||
| ModLoader.new_hooks_created.emit() | ||
|  | ||
| zip_writer.close() | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might make sense to have https://github.com/GodotModding as author instead of just KANA