-
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
Conversation
5abbd80
to
bad077b
Compare
727e388
to
976c923
Compare
The plan is to automate the creation of hooks for each function in all GDScripts.
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`.
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.
That seems to work for filtering out methods that have a return.
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.
Also added a check for static functions and currently working on recognizing inner classes.
check the `type_hint` and add it if it's present.
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.
Also removed extra comma in args list
until now all scripts received the "Mod Loader Hooks" header at the bottom even when no changes where made.
used to ignore setter funcs
Also refactored some of the function/field names to improved the modding experience
added getter method, made both static and moved getter/setter check to is_moddable check
improved performance by exiting early when nothing is hooked
and a first style and type pass
* feat: 🚧 hook creation and loading * feat: ✨ hook creation and loading
ef7e0b7
to
addb0a5
Compare
|
||
return """ | ||
{%STATIC%}func {%METHOD_NAME%}({%METHOD_PARAMS%}){%RETURN_TYPE_STRING%}: | ||
if ModLoaderStore.get("any_mod_hooked") and ModLoaderStore.any_mod_hooked: |
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.
I think the ModLoaderStore.get("any_mod_hooked")
kills all the performance-benefits of checking ModLoaderStore.any_mod_hooked.
This was originally a way to reduce the modding overhead when running a modless version of a game.
2 topics:
- can we somehow skip the
ModLoaderStore.get("any_mod_hooked")
call? - can we skip the check altogether when dynamically generating the scripts, since there we only create the methods when hooks actually exist.
(Same below on l.271)
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.
Only reviewed the code itself, did not run any tests.
Only change we should really look into is the any_mods_hooked
one, remaining feedback is just for code quality.
|
||
func _exit_tree() -> void: | ||
# Save the cache stored in ModLoaderStore to the cache file. | ||
_ModLoaderCache.save_to_file() | ||
|
||
|
||
func are_mods_disabled() -> bool: | ||
return false |
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.
Don't see any reference to this call, looks like some mid-development method that was never used/implemented?
|
||
name="A Mod Loader Export" | ||
description="Export plugin to generate the necessary callable stack." | ||
author="KANA" |
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
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 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
var getters_setters := collect_getters_and_setters(source_code, regex_getter_setter) | ||
|
||
var moddable_methods := current_script.get_script_method_list().filter( | ||
func is_func_moddable(method: Dictionary): |
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.
why are we inlining this function? For me it just worsens readability.
|
||
static func get_closing_paren_index(opening_paren_index: int, text: String) -> int: | ||
# Use a stack to match parentheses | ||
var stack := [] |
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.
No need to use a stack here, could just be another accumulator, just doing +1 on "(" and -1 on ")"
The requested changes will be addressed in subsequent PRs. |
Modding Hooks
New Class
_ModLoaderModHookPreProcessor
This class is used to process the source code from a script at a given path.
vanilla_2924080498__ready()
super()
withsuper.vanilla_method_name()
ModLoaderMod.add_hook()
How to use:
_ModLoaderModHookPreProcessor
process_script(path)
to start processing the source code of a scripthook_pre_processor.process_begin()
can be called to clear the hashmap storing the hook data.Result
Before processing
After processing
New Class
_ModLoaderModHookPacker
The
_ModLoaderModHookPacker
is used for the dynamic creation of the “mod hook pack”. The mod hook pack is a zip archive that includes all transformed script that are required for the currently installed mods.Hook generation is started in ModLoader
_ready()
, we have to do it here because we run into issues with variables declared with autoload props as defaults. These autoloads are not available if we run it in_init()
.godot-mod-loader/addons/mod_loader/mod_loader.gd
Lines 88 to 92 in ef7e0b7
Changes to ModLoaderStore
const MOD_HOOK_PACK_NAME := "mod-hooks.zip"
- the default name for the mod hook pack.any_mod_hooked
- to exit early of no mod hooks are usedmodding_hooks
- stores the main hook datahooked_script_paths
- Only stores the hooked script_paths. Used to generate the hooks in_ModLoaderModHookPacker
.New Export Plugin
The Export plugin can be used to do a full conversion of a project on export.
This will process each script and add hooks to it.
Note: This only affects the exported project, the code in the editor will stay as is.
New API Method in
ModLoaderMod
add_hook(mod_callable: Callable, script_path: String, method_name: String, is_before := false) -> void
Installs a hook into a vanilla function.
Parameters:
mod_callable
(Callable): The Callable that is called at the beginning or the end of the vanilla function.script_path
(String): The path to the hooked script.method_name
(String): The name of the hooked function.is_before
(bool): By defaultfalse
, if true the hook is placed before the function code.New Mod Loader Options
Added new options to configure the hook system
override_path_to_hook_pack
Can be used to override the default hook pack path, the hook pack is located inside the game's install directory by default.
To override the path specify a new absolute path.
override_hook_pack_name
Can be used to override the default hook pack name, by default it is
ModLoaderStore.MOD_HOOK_PACK_NAME
restart_notification_scene_path
Can be used to specify your own scene that is displayed if a game restart is required.
For example if new mod hooks where generated.
disable_restart
Can be used to disable the mod loader's restart logic. Use the
ModLoader.new_hooks_created
signal to implement your own restart logic.Restart notification
Added a new restart notification.
The
new_hooks_created
signal is emitted if new hooks are created meaning a restart is pending.With the default setting this will instance the new restart_notification scene that will show a countdown and automatically restarts the game to apply the new mod hooks.
This behavior can be modified with the options described above.
Miner Changes
_ModLoaderCache
get_data(key: String)
if the cache doesn't have the desired key, fromerror
toinfo
._ModLoaderPath
get_path_to_hook_pack()
to get the path to the created hook pack with all possible overrides applied.TODO
-> Update Mod Hooks plugin.cfg #421
ModLoaderMod.add_hook()
call_hooks()
,get_hook_hash()
and the main functionality ofadd_hook()
to a new Class_ModLoaderModHooks
.-> Feat zip peeking #416
_ModLoaderModHooks.add_hook()
fromModLoaderMod.add_hook()
-> Feat zip peeking #416
ModLoaderStore.hooked_script_paths
key value to trueinstead
ofnull
-> refactor: use truthy values in dictionaries used as hashsets #422
is_func_moddable()
-> refactor: don't inline is_func_moddable #423
get_closing_paren_index()
-> fix: Use counter instead of stack in get_closing_paren_index() #420
ModLoaderStore.get("any_mod_hooked")
check, it invalidates the performance benefit ofModLoaderStore.any_mod_hooked
.are_mods_disabled()
inModLoader
-> Feat zip peeking #416
ModLoaderMod.add_hook()
Description Todos
ModLoaderMod.add_hook()
get_path_to_hook_pack()
Documentation Todos
ModLoaderMod.add_hook()
override_path_to_hook_pack
override_hook_pack_name
restart_notification_scene_path
disable_restart