1
1
extends EditorExportPlugin
2
2
3
- const REQUIRE_EXPLICIT_ADDITION := false
4
- const METHOD_PREFIX := "vanilla_"
5
- const HASH_COLLISION_ERROR := "MODDING EXPORT ERROR: Hash collision between %s and %s . The collision can be resolved by renaming one of the methods or changing their script's path."
6
-
7
- static var regex_getter_setter : RegEx
8
-
9
- var hashmap := {}
3
+ const ModHookPreprocessorScript := preload ("res://addons/mod_loader/_export_plugin/mod_hook_preprocessor.gd" )
4
+ static var ModHookPreprocessor
10
5
11
6
12
7
func _get_name () -> String :
13
8
return "Godot Mod Loader Export Plugin"
14
9
15
10
16
11
func _export_begin (features : PackedStringArray , is_debug : bool , path : String , flags : int ) -> void :
17
- process_begin ()
12
+ ModHookPreprocessor = ModHookPreprocessorScript .new ()
13
+ ModHookPreprocessor .process_begin ()
18
14
19
15
20
16
func _export_file (path : String , type : String , features : PackedStringArray ) -> void :
@@ -25,287 +21,8 @@ func _export_file(path: String, type: String, features: PackedStringArray) -> vo
25
21
return
26
22
27
23
skip ()
28
- add_file (path , process_script (path , type , features ).to_utf8_buffer (), false )
29
-
30
-
31
- func process_begin () -> void :
32
- hashmap .clear ()
33
- regex_getter_setter = RegEx .new ()
34
- regex_getter_setter .compile ("(.*?[sg]et\\ s*=\\ s*)(\\ w+)(\\ g<1>)?(\\ g<2>)?" )
35
-
36
-
37
- func process_script (path : String , type : String , features : PackedStringArray ) -> String :
38
- var current_script := load (path ) as GDScript
39
- var source_code := current_script .source_code
40
- var source_code_additions := ""
41
-
42
- # We need to stop all vanilla methods from forming inheritance chains,
43
- # since the generated methods will fulfill inheritance requirements
44
- var class_prefix := str (hash (path ))
45
- var method_store : Array [String ] = []
46
- var mod_loader_hooks_start_string := \
47
- "\n # ModLoader Hooks - The following code has been automatically added by the Godot Mod Loader export plugin.\n "
48
-
49
- var getters_setters := collect_getters_and_setters (source_code )
50
-
51
- for method in current_script .get_script_method_list ():
52
- var method_first_line_start := get_index_at_method_start (method .name , source_code )
53
- if method_first_line_start == - 1 or method .name in method_store :
54
- continue
55
-
56
- if getters_setters .has (method .name ):
57
- continue
58
-
59
- if not is_func_moddable (method_first_line_start , source_code ):
60
- continue
61
-
62
- var type_string := get_return_type_string (method .return )
63
- var is_static := true if method .flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false
64
- var method_arg_string_with_defaults_and_types := get_function_parameters (method .name , source_code , is_static )
65
- var method_arg_string_names_only := get_function_arg_name_string (method .args )
66
-
67
- var hash_before := ModLoaderMod .get_hook_hash (path , method .name , true )
68
- var hash_after := ModLoaderMod .get_hook_hash (path , method .name , false )
69
- var hash_before_data := [path , method .name ,true ]
70
- var hash_after_data := [path , method .name ,false ]
71
- if hashmap .has (hash_before ):
72
- push_error (HASH_COLLISION_ERROR % [hashmap [hash_before ], hash_before_data ])
73
- if hashmap .has (hash_after ):
74
- push_error (HASH_COLLISION_ERROR % [hashmap [hash_after ], hash_after_data ])
75
- hashmap [hash_before ] = hash_before_data
76
- hashmap [hash_after ] = hash_after_data
77
-
78
- var mod_loader_hook_string := get_mod_loader_hook (
79
- method .name ,
80
- method_arg_string_names_only ,
81
- method_arg_string_with_defaults_and_types ,
82
- type_string ,
83
- method .return .usage ,
84
- is_static ,
85
- path ,
86
- hash_before ,
87
- hash_after ,
88
- METHOD_PREFIX + class_prefix ,
89
- )
90
-
91
- # Store the method name
92
- # Not sure if there is a way to get only the local methods in a script,
93
- # get_script_method_list() returns a full list,
94
- # including the methods from the scripts it extends,
95
- # which leads to multiple entries in the list if they are overridden by the child script.
96
- method_store .push_back (method .name )
97
- source_code = prefix_method_name (method .name , is_static , source_code , METHOD_PREFIX + class_prefix )
98
- source_code_additions += "\n %s " % mod_loader_hook_string
99
-
100
- # if we have some additions to the code, append them at the end
101
- if source_code_additions != "" :
102
- source_code = "%s \n %s \n %s " % [source_code ,mod_loader_hooks_start_string , source_code_additions ]
103
-
104
- return source_code
105
-
106
-
107
-
108
- static func get_function_arg_name_string (args : Array ) -> String :
109
- var arg_string := ""
110
- for x in args .size ():
111
- if x == args .size () - 1 :
112
- arg_string += args [x ].name
113
- else :
114
- arg_string += "%s , " % args [x ].name
115
-
116
- return arg_string
117
-
118
-
119
- static func get_function_parameters (method_name : String , text : String , is_static : bool , offset := 0 ) -> String :
120
- var result := match_func_with_whitespace (method_name , text , offset )
121
- if result == null :
122
- return ""
123
-
124
- # Find the index of the opening parenthesis
125
- var opening_paren_index := result .get_end () - 1
126
- if opening_paren_index == - 1 :
127
- return ""
128
-
129
- if not is_top_level_func (text , result .get_start (), is_static ):
130
- return get_function_parameters (method_name , text , is_static , result .get_end ())
131
-
132
- # Use a stack to match parentheses
133
- var stack := []
134
- var closing_paren_index := opening_paren_index
135
- while closing_paren_index < text .length ():
136
- var char := text [closing_paren_index ]
137
- if char == '(' :
138
- stack .push_back ('(' )
139
- elif char == ')' :
140
- stack .pop_back ()
141
- if stack .size () == 0 :
142
- break
143
- closing_paren_index += 1
144
-
145
- # If the stack is not empty, that means there's no matching closing parenthesis
146
- if stack .size () != 0 :
147
- return ""
148
-
149
- # Extract the substring between the parentheses
150
- var param_string := text .substr (opening_paren_index + 1 , closing_paren_index - opening_paren_index - 1 )
151
-
152
- # Clean whitespace characters (spaces, newlines, tabs)
153
- param_string = param_string .strip_edges ()\
154
- .replace (" " , "" )\
155
- .replace ("\n " , "" )\
156
- .replace ("\t " , "" )\
157
- .replace ("," , ", " )\
158
- .replace (":" , ": " )
159
-
160
- return param_string
161
-
162
-
163
- static func prefix_method_name (method_name : String , is_static : bool , text : String , prefix := METHOD_PREFIX , offset := 0 ) -> String :
164
- var result := match_func_with_whitespace (method_name , text , offset )
165
-
166
- if not result :
167
- return text
168
-
169
- if not is_top_level_func (text , result .get_start (), is_static ):
170
- return prefix_method_name (method_name , is_static , text , prefix , result .get_end ())
171
-
172
- text = text .erase (result .get_start (), result .get_end () - result .get_start ())
173
- text = text .insert (result .get_start (), "func %s _%s (" % [prefix , method_name ])
174
-
175
- return text
176
-
177
-
178
- static func match_func_with_whitespace (method_name : String , text : String , offset := 0 ) -> RegExMatch :
179
- var func_with_whitespace := RegEx .new ()
180
- func_with_whitespace .compile ("func\\ s+%s \\ s*\\ (" % method_name )
181
-
182
- # Search for the function definition
183
- return func_with_whitespace .search (text , offset )
184
-
185
-
186
- static func get_mod_loader_hook (
187
- method_name : String ,
188
- method_arg_string_names_only : String ,
189
- method_arg_string_with_defaults_and_types : String ,
190
- method_type : String ,
191
- return_prop_usage : int ,
192
- is_static : bool ,
193
- script_path : String ,
194
- hash_before :int ,
195
- hash_after :int ,
196
- method_prefix := METHOD_PREFIX ) -> String :
197
- var type_string := " -> %s " % method_type if not method_type .is_empty () else ""
198
- var static_string := "static " if is_static else ""
199
- # Cannot use "self" inside a static function.
200
- var self_string := "null" if is_static else "self"
201
- var return_var := "var %s = " % "return_var" if not method_type .is_empty () or return_prop_usage == 131072 else ""
202
- var method_return := "return %s " % "return_var" if not method_type .is_empty () or return_prop_usage == 131072 else ""
203
-
204
- return """
205
- {%STATIC%}func {%METHOD_NAME%}({%METHOD_PARAMS%}){%RETURN_TYPE_STRING%}:
206
- if ModLoaderStore.any_mod_hooked:
207
- ModLoaderMod.call_hooks({%SELF%}, [{%METHOD_ARGS%}], {%HOOK_ID_BEFORE%})
208
- {%METHOD_RETURN_VAR%}{%METHOD_PREFIX%}_{%METHOD_NAME%}({%METHOD_ARGS%})
209
- if ModLoaderStore.any_mod_hooked:
210
- ModLoaderMod.call_hooks({%SELF%}, [{%METHOD_ARGS%}], {%HOOK_ID_AFTER%})
211
- {%METHOD_RETURN%}
212
- """ .format ({
213
- "%METHOD_PREFIX%" : method_prefix ,
214
- "%METHOD_NAME%" : method_name ,
215
- "%METHOD_PARAMS%" : method_arg_string_with_defaults_and_types ,
216
- "%RETURN_TYPE_STRING%" : type_string ,
217
- "%METHOD_ARGS%" : method_arg_string_names_only ,
218
- "%SCRIPT_PATH%" : script_path ,
219
- "%METHOD_RETURN_VAR%" : return_var ,
220
- "%METHOD_RETURN%" : method_return ,
221
- "%STATIC%" : static_string ,
222
- "%SELF%" : self_string ,
223
- "%HOOK_ID_BEFORE%" : hash_before ,
224
- "%HOOK_ID_AFTER%" : hash_after ,
225
- })
226
-
227
-
228
- static func get_previous_line_to (text : String , index : int ) -> String :
229
- if index <= 0 or index >= text .length ():
230
- return ""
231
-
232
- var start_index := index - 1
233
- # Find the end of the previous line
234
- while start_index > 0 and text [start_index ] != "\n " :
235
- start_index -= 1
236
-
237
- if start_index == 0 :
238
- return ""
239
-
240
- start_index -= 1
241
-
242
- # Find the start of the previous line
243
- var end_index := start_index
244
- while start_index > 0 and text [start_index - 1 ] != "\n " :
245
- start_index -= 1
246
-
247
- return text .substr (start_index , end_index - start_index + 1 )
248
-
249
-
250
- static func is_func_moddable (method_start_idx , text ) -> bool :
251
- var prevline := get_previous_line_to (text , method_start_idx )
252
-
253
- if prevline .contains ("@not-moddable" ):
254
- return false
255
- if not REQUIRE_EXPLICIT_ADDITION :
256
- return true
257
-
258
- return prevline .contains ("@moddable" )
259
-
260
-
261
- static func get_index_at_method_start (method_name : String , text : String ) -> int :
262
- var result := match_func_with_whitespace (method_name , text )
263
-
264
- if result :
265
- return text .find ("\n " , result .get_end ())
266
- else :
267
- return - 1
268
-
269
-
270
- static func is_top_level_func (text : String , result_start_index : int , is_static := false ) -> bool :
271
- if is_static :
272
- result_start_index = text .rfind ("static" , result_start_index )
273
-
274
- var line_start_index := text .rfind ("\n " , result_start_index ) + 1
275
- var pre_func_length := result_start_index - line_start_index
276
-
277
- if pre_func_length > 0 :
278
- return false
279
-
280
- return true
281
-
282
-
283
- static func get_return_type_string (return_data : Dictionary ) -> String :
284
- if return_data .type == 0 :
285
- return ""
286
- var type_base : String
287
- if return_data .has ("class_name" ) and not str (return_data .class_name ).is_empty ():
288
- type_base = str (return_data .class_name )
289
- else :
290
- type_base = type_string (return_data .type )
291
-
292
- var type_hint := "" if return_data .hint_string .is_empty () else ("[%s ]" % return_data .hint_string )
293
-
294
- return "%s%s " % [type_base , type_hint ]
295
-
296
-
297
- static func collect_getters_and_setters (text : String ) -> Dictionary :
298
- var result := {}
299
- # a valid match has 2 or 4 groups, split into the method names and the rest of the line
300
- # (var example: set = )(example_setter)(, get = )(example_getter)
301
- # if things between the names are empty or commented, exclude them
302
- for mat in regex_getter_setter .search_all (text ):
303
- if mat .get_string (1 ).is_empty () or mat .get_string (1 ).contains ("#" ):
304
- continue
305
- result [mat .get_string (2 )] = null
306
-
307
- if mat .get_string (3 ).is_empty () or mat .get_string (3 ).contains ("#" ):
308
- continue
309
- result [mat .get_string (4 )] = null
310
-
311
- return result
24
+ add_file (
25
+ path ,
26
+ ModHookPreprocessor .process_script (path ).to_utf8_buffer (),
27
+ false
28
+ )
0 commit comments