Skip to content

Commit 2892c8b

Browse files
Qubus0KANAjetztpirey0
authored
Feat zip peeking (#416)
* 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: 🚧 WIP: replace `super()` * feat: 🚧 WIP: replace `super()` * feat: 🚧 replace `super()` first working draft * 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 * feat: 🚧 read manifest from zip before loading the mod * feat: 🚧 read manifest from zip before loading the mod * feat: 🚧 read manifest from zip before loading the mod * feat: 🚧 read manifest from zip before loading the mod * fix: 🐛 fix typos * fix: 🐛 check overwrites in zip * fix: 🐛 type inference * feat: 🚧 hooks * docs: 📝 better doc comments for ml options * fix: 🐛 mac exe path * refactor: ♻️ move hook methods to new file * fix: 🐛 wrong hook location on mac * refactor: ♻️ move hook methods to new file * fix: 🐛 use callv for hooks * fix: 🐛 fix self reference error from 4.2 * fix: 🐛 getter for steam id * feat: ✨ improved restart notification * fix: merge duplication * Update addons/mod_loader/api/profile.gd Co-authored-by: KANAjetzt <[email protected]> * fix: rename zip_file_exists * docs: minimal hooks documentation * fix: typo * fix: review changes --------- Co-authored-by: Kai <[email protected]> Co-authored-by: Luca Martinelli <[email protected]> Co-authored-by: KANAjetzt <[email protected]>
1 parent 09c458d commit 2892c8b

20 files changed

+580
-351
lines changed

addons/mod_loader/_export_plugin/export_plugin.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
extends EditorExportPlugin
22

33
const ModHookPreprocessorScript := preload("res://addons/mod_loader/internal/mod_hook_preprocessor.gd")
4-
static var ModHookPreprocessor
4+
static var ModHookPreprocessor: ModLoaderHookPreprocessor
55

66

77
func _get_name() -> String:

addons/mod_loader/api/config.gd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ static func delete_config(config: ModConfig) -> bool:
125125

126126
# Remove the config from ModData
127127
ModLoaderStore.mod_data[config.mod_id].configs.erase(config.name)
128-
128+
129129
return true
130130

131131

@@ -213,7 +213,7 @@ static func _traverse_schema(schema_prop: Dictionary, prop_key_array: Array) ->
213213
schema_prop = schema_prop.properties
214214

215215
schema_prop = _traverse_schema(schema_prop, prop_key_array)
216-
216+
217217
return schema_prop
218218

219219

@@ -379,7 +379,7 @@ static func refresh_config_data(config: ModConfig) -> ModConfig:
379379
var new_config_data := _ModLoaderFile.get_json_as_dict(config.save_path)
380380
# Update the data property of the ModConfig object with the refreshed data
381381
config.data = new_config_data
382-
382+
383383
return config
384384

385385

addons/mod_loader/api/mod.gd

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,6 @@ 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-
4014
## Installs a script extension that extends a vanilla script.[br]
4115
## The [code]child_script_path[/code] should point to your mod's extender script.[br]
4216
## Example: [code]"MOD/extensions/singletons/utils.gd"[/code][br]
@@ -51,7 +25,6 @@ static func get_hook_hash(path:String, method:String, is_before:bool) -> int:
5125
##
5226
## [br][b]Returns:[/b] [code]void[/code][br]
5327
static func install_script_extension(child_script_path: String) -> void:
54-
5528
var mod_id: String = _ModLoaderPath.get_mod_dir(child_script_path)
5629
var mod_data: ModData = get_mod_data(mod_id)
5730
if not ModLoaderStore.saved_extension_paths.has(mod_data.manifest.get_mod_id()):
@@ -68,6 +41,12 @@ static func install_script_extension(child_script_path: String) -> void:
6841
_ModLoaderScriptExtension.apply_extension(child_script_path)
6942

7043

44+
## Adds a mod hook
45+
# TODO: detailed doc
46+
static func add_hook(mod_callable: Callable, script_path: String, method_name: String, is_before := false) -> void:
47+
_ModLoaderHooks.add_hook(mod_callable, script_path, method_name, is_before)
48+
49+
7150
## Registers an array of classes to the global scope since Godot only does that in the editor.[br]
7251
##
7352
## Format: [code]{ "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" }[/code][br]
@@ -84,8 +63,9 @@ static func register_global_classes_from_array(new_global_classes: Array) -> voi
8463
var _savecustom_error: int = ProjectSettings.save_custom(_ModLoaderPath.get_override_path())
8564

8665

87-
## Adds a translation file.[br]
88-
##[br]
66+
## Adds a translation file.
67+
## [br]
68+
## [br]
8969
## [i]Note: The translation file should have been created in Godot already,
9070
## such as when importing a CSV file. The translation file should be in the format [code]mytranslation.en.translation[/code].[/i][br]
9171
##
@@ -106,19 +86,20 @@ static func add_translation(resource_path: String) -> void:
10686
ModLoaderLog.fatal("Failed to load translation at path: %s" % [resource_path], LOG_NAME)
10787

10888

89+
10990
## [i]Note: This function requires Godot 4.3 or higher.[/i][br]
110-
##[br]
91+
## [br]
11192
## Refreshes a specific scene by marking it for refresh.[br]
112-
##[br]
93+
## [br]
11394
## This function is useful if a script extension is not automatically applied.
11495
## This situation can occur when a script is attached to a preloaded scene.
11596
## If you encounter issues where your script extension is not working as expected,
11697
## try to identify the scene to which it is attached and use this method to refresh it.
11798
## This will reload already loaded scenes and apply the script extension.
118-
##[br]
99+
## [br]
119100
## [br][b]Parameters:[/b][br]
120101
## - [code]scene_path[/code] (String): The path to the scene file to be refreshed.
121-
##[br]
102+
## [br]
122103
## [br][b]Returns:[/b] [code]void[/code][br]
123104
static func refresh_scene(scene_path: String) -> void:
124105
if scene_path in ModLoaderStore.scenes_to_refresh:
@@ -196,12 +177,12 @@ static func is_mod_loaded(mod_id: String) -> bool:
196177
return true
197178

198179

199-
# Returns true if the mod with the given mod_id was successfully loaded and is currently active.
200-
#
201-
# Parameters:
202-
# - mod_id (String): The ID of the mod.
203-
#
204-
# Returns:
205-
# - bool: true if the mod is loaded and active, false otherwise.
180+
## Returns true if the mod with the given mod_id was successfully loaded and is currently active.
181+
## [br]
182+
## Parameters:
183+
## - mod_id (String): The ID of the mod.
184+
## [br]
185+
## Returns:
186+
## - bool: true if the mod is loaded and active, false otherwise.
206187
static func is_mod_active(mod_id: String) -> bool:
207188
return is_mod_loaded(mod_id) and ModLoaderStore.mod_data[mod_id].is_active

addons/mod_loader/api/profile.gd

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ static func enable_mod(mod_id: String, user_profile:= ModLoaderStore.current_use
2525
return _set_mod_state(mod_id, user_profile.name, true)
2626

2727

28+
## Forces a mod to enable, ensuring it loads at the next game start, regardless of load warnings.[br]
29+
## [br]
30+
## [b]Parameters:[/b][br]
31+
## - [code]mod_id[/code] ([String]): The ID of the mod to enable.[br]
32+
## - [code]user_profile[/code] ([ModUserProfile]): (Optional) The user profile for which the mod will be enabled. Defaults to the current user profile.[br]
33+
## [br]
34+
## [b]Returns:[/b] [bool]: True if the mod is successfully set to enable, False otherwise.
35+
static func force_enable_mod(mod_id: String, user_profile:= ModLoaderStore.current_user_profile) -> bool:
36+
return _set_mod_state(mod_id, user_profile.name, true, true)
37+
38+
2839
## Disables a mod - it will not be loaded on the next game start[br]
2940
## [br]
3041
## [b]Parameters:[/b][br]
@@ -208,7 +219,7 @@ static func _update_disabled_mods() -> void:
208219
for mod_id in current_user_profile.mod_list:
209220
var mod_list_entry: Dictionary = current_user_profile.mod_list[mod_id]
210221
if ModLoaderStore.mod_data.has(mod_id):
211-
ModLoaderStore.mod_data[mod_id].is_active = mod_list_entry.is_active
222+
ModLoaderStore.mod_data[mod_id].set_mod_state(mod_list_entry.is_active, true)
212223

213224
ModLoaderLog.debug(
214225
"Updated the active state of all mods, based on the current user profile \"%s\""
@@ -329,30 +340,25 @@ static func _generate_mod_list_entry(mod_id: String, is_active: bool) -> Diction
329340

330341

331342
# Handles the activation or deactivation of a mod in a user profile.
332-
static func _set_mod_state(mod_id: String, profile_name: String, activate: bool) -> bool:
343+
static func _set_mod_state(mod_id: String, profile_name: String, should_activate: bool, force := false) -> bool:
333344
# Verify whether the mod_id is present in the profile's mod_list.
334345
if not _is_mod_id_in_mod_list(mod_id, profile_name):
335346
return false
336347

337-
# Check if it is a locked mod
338-
if ModLoaderStore.mod_data.has(mod_id) and ModLoaderStore.mod_data[mod_id].is_locked:
339-
ModLoaderLog.error(
340-
"Unable to disable mod \"%s\" as it is marked as locked. Locked mods: %s"
341-
% [mod_id, ModLoaderStore.ml_options.locked_mods],
342-
LOG_NAME)
348+
# Handle mod state
349+
# Set state in the ModData
350+
var was_toggled: bool = ModLoaderStore.mod_data[mod_id].set_mod_state(should_activate, force)
351+
if not was_toggled:
343352
return false
344353

345-
# Handle mod state
346354
# Set state for user profile
347-
ModLoaderStore.user_profiles[profile_name].mod_list[mod_id].is_active = activate
348-
# Set state in the ModData
349-
ModLoaderStore.mod_data[mod_id].is_active = activate
355+
ModLoaderStore.user_profiles[profile_name].mod_list[mod_id].is_active = should_activate
350356

351357
# Save profiles to the user profiles JSON file
352358
var is_save_success := _save()
353359

354360
if is_save_success:
355-
ModLoaderLog.debug("Mod activation state changed: mod_id=%s activate=%s profile_name=%s" % [mod_id, activate, profile_name], LOG_NAME)
361+
ModLoaderLog.debug("Mod activation state changed: mod_id=%s should_activate=%s profile_name=%s" % [mod_id, should_activate, profile_name], LOG_NAME)
356362

357363
return is_save_success
358364

addons/mod_loader/internal/cli.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ static func _get_fixed_cmdline_args() -> PackedStringArray:
4949
static func fix_godot_cmdline_args_string_space_splitting(args: PackedStringArray) -> PackedStringArray:
5050
if not OS.has_feature("editor"): # only happens in editor builds
5151
return args
52-
if OS.has_feature("Windows"): # windows is unaffected
52+
if OS.has_feature("windows"): # windows is unaffected
5353
return args
5454

5555
var fixed_args := PackedStringArray([])

0 commit comments

Comments
 (0)