Skip to content

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

Merged
merged 28 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6df0b4d
feat: :construction: first draft of the export plugin
KANAjetzt Aug 22, 2024
36ff6be
feat: :construction: wip callable_stack
KANAjetzt Aug 26, 2024
abf1d76
refactor: :construction: rename vanilla functions
KANAjetzt Aug 27, 2024
f2dffe2
feat: :construction: added return with types to hook
KANAjetzt Aug 28, 2024
d01e030
refactor: :construction: reworked callable_sack funcs
KANAjetzt Aug 28, 2024
b553487
refactor: :construction: added return usage check
KANAjetzt Aug 28, 2024
ba89eeb
refactor: :construction: remove and add back class_name
KANAjetzt Aug 29, 2024
e1d4533
refactor: :construction: No new script instance - load() the script 😶‍🌫️
KANAjetzt Aug 30, 2024
3ea4c26
refactor: :construction: added `get_return_type_string`
KANAjetzt Sep 2, 2024
a9c80fb
refactor: :construction: added `is_top_level_func`
KANAjetzt Sep 2, 2024
43adc11
fix: :construction: corrected return type in generated methods
pirey0 Sep 3, 2024
34020fd
feat: :construction: only add modloader text to modified scripts
pirey0 Sep 3, 2024
e49652c
feat: :construction: added optional @moddable requirement
pirey0 Sep 3, 2024
84e901c
feat: :construction: added `is_setter`
KANAjetzt Sep 4, 2024
5f3a5e4
feat: :construction: improved runtime performance by pre-hashing dict…
pirey0 Sep 4, 2024
fecbc52
feat: :construction: fixed issues with inheritance by adding class hash
pirey0 Sep 4, 2024
04dbf78
cleaner getters and setters
Qubus0 Sep 4, 2024
d42c7f3
feat: :construction: added @not-moddable option to exclude methods
pirey0 Sep 4, 2024
f1ea296
refactor: :recycle: removed no longer used functions
KANAjetzt Sep 5, 2024
cae54ae
refactor: 🚧 general code cleanup
Qubus0 Sep 5, 2024
e912619
:construction: refactor: move methods
Qubus0 Sep 5, 2024
35bab06
:construction: refactor: extract methods
Qubus0 Sep 5, 2024
479a53a
:construction: feat: mod hook packing prototype
Qubus0 Sep 5, 2024
5f3c581
fix: :construction: fix path to `mod_hook_preprocessor.gd`
KANAjetzt Sep 6, 2024
088d379
:construction: fix: typos
Qubus0 Sep 6, 2024
8343f52
feat: :construction: replace super calls
Qubus0 Sep 9, 2024
7c78a92
refactor: :recycle: remove static from regex vars
KANAjetzt Sep 17, 2024
addb0a5
feat: :construction: hook creation and loading (#3)
KANAjetzt Oct 2, 2024
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
28 changes: 28 additions & 0 deletions addons/mod_loader/_export_plugin/export_plugin.gd
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
)
7 changes: 7 additions & 0 deletions addons/mod_loader/_export_plugin/plugin.cfg
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"
Copy link
Contributor

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

version="0.1"
script="plugin.gd"
14 changes: 14 additions & 0 deletions addons/mod_loader/_export_plugin/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)
26 changes: 26 additions & 0 deletions addons/mod_loader/api/mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: We are using a Dictionary as a Hashset here.
Even though we do not intend to use the dictionary value, it might make sense to store a "truthy" value instead of null so that both has and get can be used in if checks. (I would just use True)
Currently hooked_script_paths.get(script_path, false) would always be falsey, which is not the expected interpretation



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]
Expand Down
2 changes: 1 addition & 1 deletion addons/mod_loader/internal/cache.gd
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ static func add_data(key: String, data: Dictionary) -> Dictionary:
# Get data from a specific key
static func get_data(key: String) -> Dictionary:
if not ModLoaderStore.cache.has(key):
ModLoaderLog.error("key: \"%s\" not found in \"ModLoaderStore.cache\"" % key, LOG_NAME)
ModLoaderLog.info("key: \"%s\" not found in \"ModLoaderStore.cache\"" % key, LOG_NAME)
return {}

return ModLoaderStore.cache[key]
Expand Down
1 change: 0 additions & 1 deletion addons/mod_loader/internal/file.gd
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,3 @@ static func dir_exists(path: String) -> bool:
# modders in understanding and troubleshooting issues.
static func _code_note(_msg:String):
pass

65 changes: 65 additions & 0 deletions addons/mod_loader/internal/mod_hook_packer.gd
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()
Loading