diff --git a/.gitignore b/.gitignore index 1fdd44b2..501101e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +.godot # IDEs .idea .vscode # mac thing -**/.DS_Store \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/addons/mod_loader/internal/hooks.gd b/addons/mod_loader/internal/hooks.gd index 512b5d6e..575a0f01 100644 --- a/addons/mod_loader/internal/hooks.gd +++ b/addons/mod_loader/internal/hooks.gd @@ -20,7 +20,9 @@ static func add_hook(mod_callable: Callable, script_path: String, method_name: S ) if not ModLoaderStore.hooked_script_paths.has(script_path): - ModLoaderStore.hooked_script_paths[script_path] = true + ModLoaderStore.hooked_script_paths[script_path] = [method_name] + elif not ModLoaderStore.hooked_script_paths[script_path].has(method_name): + ModLoaderStore.hooked_script_paths[script_path].append(method_name) static func call_hooks(vanilla_method: Callable, args: Array, hook_hash: int) -> Variant: diff --git a/addons/mod_loader/internal/mod_hook_packer.gd b/addons/mod_loader/internal/mod_hook_packer.gd index e13de9ae..809c1885 100644 --- a/addons/mod_loader/internal/mod_hook_packer.gd +++ b/addons/mod_loader/internal/mod_hook_packer.gd @@ -32,23 +32,27 @@ static func start() -> void: _ModLoaderCache.remove_data("hooks") error = zip_writer.open(mod_hook_pack_path) else: - # If there is a pack already append to it + # 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) + ModLoaderLog.error("Error (%s) writing to hooks zip, consider deleting this file: %s" % [error, mod_hook_pack_path], LOG_NAME) return + ModLoaderLog.debug("Scripts requiring hooks: %s" % [ModLoaderStore.hooked_script_paths], LOG_NAME) + 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 + var cached_script_paths: Dictionary = {} if cache.is_empty() or not cache.has("hooked_script_paths") else cache.hooked_script_paths + if cached_script_paths == ModLoaderStore.hooked_script_paths: + ModLoaderLog.info("Scripts are already processed according to cache, skipping process.", LOG_NAME) + zip_writer.close() + return + 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_verbose(path) + var method_mask: Array[String] = [] + method_mask.assign(ModLoaderStore.hooked_script_paths[path]) + var processed_source_code := hook_pre_processor.process_script_verbose(path, false, method_mask) # Skip writing to the zip if no new hooks were created for this script if not hook_pre_processor.script_paths_hooked.has(path): @@ -61,10 +65,9 @@ static func start() -> void: 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.update_data("hooks", {"hooked_script_paths": ModLoaderStore.hooked_script_paths}) _ModLoaderCache.save_to_file() ModLoader.new_hooks_created.emit() diff --git a/addons/mod_loader/internal/mod_hook_preprocessor.gd b/addons/mod_loader/internal/mod_hook_preprocessor.gd index 1389d674..24e071e2 100644 --- a/addons/mod_loader/internal/mod_hook_preprocessor.gd +++ b/addons/mod_loader/internal/mod_hook_preprocessor.gd @@ -50,20 +50,21 @@ var script_paths_hooked := {} func process_begin() -> void: hashmap.clear() - -func process_script_verbose(path: String, enable_hook_check := false) -> String: +## Calls [method process_script] with additional logging +func process_script_verbose(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String: var start_time := Time.get_ticks_msec() ModLoaderLog.debug("Start processing script at path: %s" % path, LOG_NAME) - var processed := process_script(path, enable_hook_check) + var processed := process_script(path, enable_hook_check, method_mask) ModLoaderLog.debug("Finished processing script at path: %s in %s ms" % [path, Time.get_ticks_msec() - start_time], LOG_NAME) return processed -func process_script(path: String, enable_hook_check := false) -> String: +## [param path]: File path to the script to be processed.[br] +## [param enable_hook_check]: Adds a check that ModLoaderStore.any_mod_hooked is [code]true[/code] to the processed method, reducing hash checks.[br] +## [param method_mask]: If provided, only methods in this [Array] will be processed.[br] +func process_script(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String: var current_script := load(path) as GDScript - var source_code := current_script.source_code - var source_code_additions := "" # We need to stop all vanilla methods from forming inheritance chains, @@ -72,32 +73,35 @@ func process_script(path: String, enable_hook_check := false) -> String: var method_store: Array[String] = [] var getters_setters := collect_getters_and_setters(source_code) - var moddable_methods := current_script.get_script_method_list().filter( is_func_moddable.bind(source_code, getters_setters) ) var methods_hooked := {} - for method in moddable_methods: if method.name in method_store: continue - var prefix := "%s%s_" % [METHOD_PREFIX, class_prefix] + var full_prefix := "%s%s_" % [METHOD_PREFIX, class_prefix] # Check if the method name starts with the prefix added by `edit_vanilla_method()`. # This indicates that the method was previously processed, possibly by the export plugin. # If so, store the method name (excluding the prefix) in `methods_hooked`. - if method.name.begins_with(prefix): - var method_name_vanilla: String = method.name.trim_prefix(prefix) + if method.name.begins_with(full_prefix): + var method_name_vanilla: String = method.name.trim_prefix(full_prefix) methods_hooked[method_name_vanilla] = true continue - # This ensures we avoid creating a hook for the 'imposter' method, which # is generated by `build_mod_hook_string()` and has the vanilla method name. if methods_hooked.has(method.name): continue + # If a mask is provided, only methods with their name in the mask will be converted. + # Can't be filtered before the loop since it removes prefixed methods required by the previous check. + if not method_mask.is_empty(): + if not method.name in method_mask: + continue + var type_string := get_return_type_string(method.return) var is_static := true if method.flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false @@ -152,7 +156,7 @@ func process_script(path: String, enable_hook_check := false) -> String: is_static, is_async, hook_id, - METHOD_PREFIX + class_prefix, + full_prefix, enable_hook_check ) @@ -167,7 +171,7 @@ func process_script(path: String, enable_hook_check := false) -> String: source_code, func_def, func_body, - METHOD_PREFIX + class_prefix + full_prefix ) source_code_additions += "\n%s" % mod_loader_hook_string @@ -327,15 +331,15 @@ func edit_vanilla_method( ) -> String: text = fix_method_super(method_name, func_body, text) text = text.erase(func_def.get_start(), func_def.get_end() - func_def.get_start()) - text = text.insert(func_def.get_start(), "func %s_%s(" % [prefix, method_name]) + text = text.insert(func_def.get_start(), "func %s%s(" % [prefix, method_name]) return text -func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String: +func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String: if engine_version_hex < ENGINE_VERSION_HEX_4_2_2: return fix_method_super_before_4_2_2(method_name, func_body, text) - + return regex_super_call.sub( text, "super.%s" % method_name, true, func_body.get_start(), func_body.get_end() @@ -344,18 +348,18 @@ func fix_method_super(method_name: String, func_body: RegExMatch, text: String) # https://github.com/godotengine/godot/pull/86052 # Quote: -# When the end argument of RegEx.sub was used, +# When the end argument of RegEx.sub was used, # it would truncate the Subject String before even doing the substitution. func fix_method_super_before_4_2_2(method_name: String, func_body: RegExMatch, text: String) -> String: var text_after_func_body_end := text.substr(func_body.get_end()) - + text = regex_super_call.sub( text, "super.%s" % method_name, true, func_body.get_start(), func_body.get_end() ) - + text = text + text_after_func_body_end - + return text @@ -397,10 +401,9 @@ static func build_mod_hook_string( return_string, await_string, method_prefix, method_name, method_arg_string_names_only ) if enable_hook_check else "" - return """ {STATIC}func {METHOD_NAME}({METHOD_PARAMS}){RETURN_TYPE_STRING}: - {HOOK_CHECK}{RETURN}{AWAIT}_ModLoaderHooks.call_hooks{ASYNC}({METHOD_PREFIX}_{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID}){HOOK_CHECK_ELSE} + {HOOK_CHECK}{RETURN}{AWAIT}_ModLoaderHooks.call_hooks{ASYNC}({METHOD_PREFIX}{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID}){HOOK_CHECK_ELSE} """.format({ "METHOD_PREFIX": method_prefix, "METHOD_NAME": method_name, @@ -551,7 +554,7 @@ static func get_hook_check_else_string( method_name: String, method_arg_string_names_only: String ) -> String: - return "\n\telse:\n\t\t{RETURN}{AWAIT}{METHOD_PREFIX}_{METHOD_NAME}({METHOD_ARGS})".format( + return "\n\telse:\n\t\t{RETURN}{AWAIT}{METHOD_PREFIX}{METHOD_NAME}({METHOD_ARGS})".format( { "RETURN": return_string, "AWAIT": await_string, diff --git a/addons/mod_loader/mod_loader_store.gd b/addons/mod_loader/mod_loader_store.gd index 86c1aa02..f9fc89fe 100644 --- a/addons/mod_loader/mod_loader_store.gd +++ b/addons/mod_loader/mod_loader_store.gd @@ -34,6 +34,8 @@ const MOD_LOADER_DEV_TOOL_URL := "https://github.com/GodotModding/godot-mod-tool var any_mod_hooked := false +# Stores arrays of hook callables that will be applied to a function, +# associated by a hash of the function name and script path # Example: # var modding_hooks := { # 1917482423: [Callable, Callable], @@ -41,9 +43,10 @@ var any_mod_hooked := false # } var modding_hooks := {} +# Stores script paths and method names to be processed for hooks # Example: # var hooked_script_paths := { -# "res://game/game.gd": true, +# "res://game/game.gd": ["_ready", "do_something"], # } var hooked_script_paths := {}