@@ -3,16 +3,22 @@ extends Object
33##
44## This Class provides helper functions to build mods.
55##
6- ## @tutorial(Script Extensions): https://github.com/GodotModding/godot-mod-loader/wiki/Script-Extensions
7- ## @tutorial(Mod Structure): https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Structure
8- ## @tutorial(Mod Files): https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files
6+ ## @tutorial(Script Extensions): https://wiki.godotmodding.com/#/guides/modding/script_extensions
7+ ## @tutorial(Script Hooks): https://wiki.godotmodding.com/#/guides/modding/script_hooks
8+ ## @tutorial(Mod Structure): https://wiki.godotmodding.com/#/guides/modding/mod_structure
9+ ## @tutorial(Mod Files): https://wiki.godotmodding.com/#/guides/modding/mod_files
910
1011
1112const LOG_NAME := "ModLoader:Mod"
1213
1314
1415## Installs a script extension that extends a vanilla script.[br]
15- ## The [code]child_script_path[/code] should point to your mod's extender script.[br]
16+ ## This is the preferred way of modifying a vanilla [Script][br]
17+ ## Since Godot 4, extensions can cause issues with scripts that use [code]class_name[/code]
18+ ## and should be avoided if present.[br]
19+ ## See [method add_hook] for those cases.[br]
20+ ## [br]
21+ ## The [param child_script_path] should point to your mod's extender script.[br]
1622## Example: [code]"MOD/extensions/singletons/utils.gd"[/code][br]
1723## Inside the extender script, include [code]extends {target}[/code] where [code]{target}[/code] is the vanilla path.[br]
1824## Example: [code]extends "res://singletons/utils.gd"[/code].[br]
@@ -21,7 +27,7 @@ const LOG_NAME := "ModLoader:Mod"
2127## but it's good practice to do so.[br]
2228##
2329## [br][b]Parameters:[/b][br]
24- ## - [code] child_script_path[/code ] (String): The path to the mod's extender script.[br]
30+ ## - [param child_script_path] ([ String] ): The path to the mod's extender script.[br]
2531##
2632## [br][b]Returns:[/b] [code]void[/code][br]
2733static func install_script_extension (child_script_path : String ) -> void :
@@ -41,10 +47,146 @@ static func install_script_extension(child_script_path: String) -> void:
4147 _ModLoaderScriptExtension .apply_extension (child_script_path )
4248
4349
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 )
50+ ## Adds all methods from a file as hooks. [br]
51+ ## The file needs to extend [Object] [br]
52+ ## The methods in the file need to have the exact same name as the vanilla method
53+ ## they intend to hook, all mismatches will be ignored. [br]
54+ ## See: [method add_hook]
55+ ##
56+ ## [codeblock]
57+ ## ModLoaderMod.install_script_hooks(
58+ ## "res://tools/utilities.gd",
59+ ## extensions_dir_path.path_join("tools/utilities-hook.gd")
60+ ## )
61+ ## [/codeblock]
62+ static func install_script_hooks (vanilla_script_path : String , hook_script_path : String ) -> void :
63+ var hook_script := load (hook_script_path ) as GDScript
64+ var hook_script_instance := hook_script .new ()
65+
66+ # Every script that inherits RefCounted will be cleaned up by the engine as
67+ # soon as there are no more references to it. If the reference is gone
68+ # the method can't be called and everything returns null.
69+ # Only Object won't be removed, so we can use it here.
70+ if hook_script_instance is RefCounted :
71+ ModLoaderLog .fatal (
72+ "Scripts holding mod hooks should always extend Object (%s )"
73+ % hook_script_path , LOG_NAME
74+ )
75+
76+ var vanilla_script := load (vanilla_script_path ) as GDScript
77+ var vanilla_methods := vanilla_script .get_script_method_list ().map (
78+ func (method : Dictionary ) -> String :
79+ return method .name
80+ )
81+
82+ var methods := hook_script .get_script_method_list ()
83+ for hook in methods :
84+ if hook .name in vanilla_methods :
85+ ModLoaderMod .add_hook (Callable (hook_script_instance , hook .name ), vanilla_script_path , hook .name )
86+ continue
87+
88+ ModLoaderLog .debug (
89+ 'Skipped adding hook "%s " (not found in vanilla script %s )'
90+ % [hook .name , vanilla_script_path ], LOG_NAME
91+ )
92+
93+ if not OS .has_feature ("editor" ):
94+ continue
95+
96+ vanilla_methods .sort_custom ((
97+ func (a_name : String , b_name : String , target_name : String ) -> bool :
98+ return a_name .similarity (target_name ) > b_name .similarity (target_name )
99+ ).bind (hook .name ))
100+
101+ var closest_vanilla : String = vanilla_methods .front ()
102+ if closest_vanilla .similarity (hook .name ) > 0.8 :
103+ ModLoaderLog .debug (
104+ 'Did you mean "%s " instead of "%s "?'
105+ % [closest_vanilla , hook .name ], LOG_NAME
106+ )
107+
108+
109+ ## Adds a hook, a custom mod function, to a vanilla method.[br]
110+ ## Opposed to script extensions, hooks can be applied to scripts that use
111+ ## [code]class_name[/code] without issues.[br]
112+ ## If possible, prefer [method install_script_extension].[br]
113+ ##
114+ ## [br][b]Parameters:[/b][br]
115+ ## - [param mod_callable] ([Callable]): The function that will executed when
116+ ## the vanilla method is executed. When writing a mod callable, make sure
117+ ## that it [i]always[/i] receives a [ModLoaderHookChain] object as first argument,
118+ ## which is used to continue down the hook chain (see: [method ModLoaderHookChain.execute_next])
119+ ## and allows manipulating parameters before and return values after the
120+ ## vanilla method is called. [br]
121+ ## - [param script_path] ([String]): Path to the vanilla script that holds the method.[br]
122+ ## - [param method_name] ([String]): The method the hook will be applied to.[br]
123+ ##
124+ ## [br][b]Returns:[/b] [code]void[/code][br][br]
125+ ##
126+ ## [b]Examples:[/b]
127+ ##
128+ ## [br]
129+ ## Given the following vanilla script [code]main.gd[/code]
130+ ## [codeblock]
131+ ## class_name MainGame
132+ ## extends Node2D
133+ ##
134+ ## var version := "vanilla 1.0.0"
135+ ##
136+ ##
137+ ## func _ready():
138+ ## $CanvasLayer/Control/Label.text = "Version: %s" % version
139+ ## print(Utilities.format_date(15, 11, 2024))
140+ ## [/codeblock]
141+ ##
142+ ## It can be hooked in [code]mod_main.gd[/code] like this
143+ ## [codeblock]
144+ ## func _init() -> void:
145+ ## ModLoaderMod.add_hook(change_version, "res://main.gd", "_ready")
146+ ## ModLoaderMod.add_hook(time_travel, "res://tools/utilities.gd", "format_date")
147+ ## # Multiple hooks can be added to a single method.
148+ ## ModLoaderMod.add_hook(add_season, "res://tools/utilities.gd", "format_date")
149+ ##
150+ ##
151+ ## # The script we are hooking is attached to a node, which we can get from reference_object
152+ ## # then we can change any variables it has
153+ ## func change_version(chain: ModLoaderHookChain) -> void:
154+ ## # Using a typecast here (with "as") can help with autocomplete and avoiding errors
155+ ## var main_node := chain.reference_object as MainGame
156+ ## main_node.version = "Modloader Hooked!"
157+ ## # _ready, which we are hooking, does not have any arguments
158+ ## chain.execute_next()
159+ ##
160+ ##
161+ ## # Parameters can be manipulated easily by changing what is passed into .execute_next()
162+ ## # The vanilla method (Utilities.format_date) takes 3 arguments, our hook method takes
163+ ## # the ModLoaderHookChain followed by the same 3
164+ ## func time_travel(chain: ModLoaderHookChain, day: int, month: int, year: int) -> String:
165+ ## print("time travel!")
166+ ## year -= 100
167+ ## # Just the vanilla arguments are passed along in the same order, wrapped into an Array
168+ ## var val = chain.execute_next([day, month, year])
169+ ## return val
170+ ##
171+ ##
172+ ## # The return value can be manipulated by calling the next hook (or vanilla) first
173+ ## # then changing it and returning the new value.
174+ ## func add_season(chain: ModLoaderHookChain, day: int, month: int, year: int) -> String:
175+ ## var output = chain.execute_next([day, month, year])
176+ ## match month:
177+ ## 12, 1, 2:
178+ ## output += ", Winter"
179+ ## 3, 4, 5:
180+ ## output += ", Spring"
181+ ## 6, 7, 8:
182+ ## output += ", Summer"
183+ ## 9, 10, 11:
184+ ## output += ", Autumn"
185+ ## return output
186+ ## [/codeblock]
187+ ##
188+ static func add_hook (mod_callable : Callable , script_path : String , method_name : String ) -> void :
189+ _ModLoaderHooks .add_hook (mod_callable , script_path , method_name )
48190
49191
50192## Registers an array of classes to the global scope since Godot only does that in the editor.[br]
@@ -55,7 +197,7 @@ static func add_hook(mod_callable: Callable, script_path: String, method_name: S
55197## (but you should only include classes belonging to your mod)[br]
56198##
57199## [br][b]Parameters:[/b][br]
58- ## - [code] new_global_classes[/code ] (Array): An array of class definitions to be registered.[br]
200+ ## - [param new_global_classes] ([ Array] ): An array of class definitions to be registered.[br]
59201##
60202## [br][b]Returns:[/b] [code]void[/code][br]
61203static func register_global_classes_from_array (new_global_classes : Array ) -> void :
@@ -70,7 +212,7 @@ static func register_global_classes_from_array(new_global_classes: Array) -> voi
70212## such as when importing a CSV file. The translation file should be in the format [code]mytranslation.en.translation[/code].[/i][br]
71213##
72214## [br][b]Parameters:[/b][br]
73- ## - [code] resource_path[/code ] (String): The path to the translation resource file.[br]
215+ ## - [param resource_path] ([ String] ): The path to the translation resource file.[br]
74216##
75217## [br][b]Returns:[/b] [code]void[/code][br]
76218static func add_translation (resource_path : String ) -> void :
@@ -84,7 +226,7 @@ static func add_translation(resource_path: String) -> void:
84226 ModLoaderLog .info ("Added Translation from Resource -> %s " % resource_path , LOG_NAME )
85227 else :
86228 ModLoaderLog .fatal ("Failed to load translation at path: %s " % [resource_path ], LOG_NAME )
87-
229+
88230
89231
90232## [i]Note: This function requires Godot 4.3 or higher.[/i][br]
@@ -98,7 +240,7 @@ static func add_translation(resource_path: String) -> void:
98240## This will reload already loaded scenes and apply the script extension.
99241## [br]
100242## [br][b]Parameters:[/b][br]
101- ## - [code] scene_path[/code ] (String): The path to the scene file to be refreshed.
243+ ## - [param scene_path] ([ String] ): The path to the scene file to be refreshed.
102244## [br]
103245## [br][b]Returns:[/b] [code]void[/code][br]
104246static func refresh_scene (scene_path : String ) -> void :
@@ -113,8 +255,8 @@ static func refresh_scene(scene_path: String) -> void:
113255## The callable receives an instance of the "vanilla_scene" as the first parameter.[br]
114256##
115257## [br][b]Parameters:[/b][br]
116- ## - [code] scene_vanilla_path[/code ] (String): The path to the vanilla scene file.[br]
117- ## - [code] edit_callable[/code ] (Callable): The callable function to modify the scene.[br]
258+ ## - [param scene_vanilla_path] ([ String] ): The path to the vanilla scene file.[br]
259+ ## - [param edit_callable] ([ Callable] ): The callable function to modify the scene.[br]
118260##
119261## [br][b]Returns:[/b] [code]void[/code][br]
120262static func extend_scene (scene_vanilla_path : String , edit_callable : Callable ) -> void :
@@ -127,7 +269,7 @@ static func extend_scene(scene_vanilla_path: String, edit_callable: Callable) ->
127269## Gets the [ModData] from the provided namespace.[br]
128270##
129271## [br][b]Parameters:[/b][br]
130- ## - [code] mod_id[/code ] (String): The ID of the mod.[br]
272+ ## - [param mod_id] ([ String] ): The ID of the mod.[br]
131273##
132274## [br][b]Returns:[/b][br]
133275## - [ModData]: The [ModData] associated with the provided [code]mod_id[/code], or null if the [code]mod_id[/code] is invalid.[br]
@@ -158,7 +300,7 @@ static func get_unpacked_dir() -> String:
158300## Returns true if the mod with the given [code]mod_id[/code] was successfully loaded.[br]
159301##
160302## [br][b]Parameters:[/b][br]
161- ## - [code] mod_id[/code ] (String): The ID of the mod.[br]
303+ ## - [param mod_id] ([ String] ): The ID of the mod.[br]
162304##
163305## [br][b]Returns:[/b][br]
164306## - [bool]: true if the mod is loaded, false otherwise.[br]
@@ -180,9 +322,9 @@ static func is_mod_loaded(mod_id: String) -> bool:
180322## Returns true if the mod with the given mod_id was successfully loaded and is currently active.
181323## [br]
182324## Parameters:
183- ## - mod_id ( String): The ID of the mod.
325+ ## - [param mod_id] ([ String] ): The ID of the mod.
184326## [br]
185327## Returns:
186- ## - bool: true if the mod is loaded and active, false otherwise.
328+ ## - [ bool] : true if the mod is loaded and active, false otherwise.
187329static func is_mod_active (mod_id : String ) -> bool :
188330 return is_mod_loaded (mod_id ) and ModLoaderStore .mod_data [mod_id ].is_active
0 commit comments