-
Notifications
You must be signed in to change notification settings - Fork 37
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