Skip to content

Commit 7fdb894

Browse files
authored
feat: ✨ detect and handle hooks for async methods (#475)
* feat: ✨ support async hooks for methods using await * privatization * feat: ✨ detect and handle hooks for async methods * feat: ✨ detect and handle hooks for async methods * feat: ✨ detect and handle hooks for async methods * feat: ✨ detect and handle hooks for async methods * fix infinite loop * no prints
1 parent 1746bc9 commit 7fdb894

File tree

1 file changed

+121
-50
lines changed

1 file changed

+121
-50
lines changed

addons/mod_loader/internal/mod_hook_preprocessor.gd

Lines changed: 121 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ var regex_getter_setter := RegEx.create_from_string("(.*?[sg]et\\s*=\\s*)(\\w+)(
2323
## returns only the super word, excluding the (, as match to make substitution easier
2424
var regex_super_call := RegEx.create_from_string("\\bsuper(?=\\s*\\()")
2525

26-
## matches the indented function body
27-
## needs to start from the : of a function definition to work (offset)
28-
## the body of a function is every line that is empty or starts with an indent or comment
26+
## Matches the indented function body.
27+
## Needs to start from the : of a function declaration to work (.search() offset param)
28+
## The body of a function is every line that is empty or starts with an indent or comment
2929
var regex_func_body := RegEx.create_from_string("(?smn)\\N*(\\n^(([\\t #]+\\N*)|$))*")
3030

31+
## Just await between word boundaries
32+
var regex_keyword_await := RegEx.create_from_string("\\bawait\\b")
33+
3134

3235
var hashmap := {}
3336

@@ -56,7 +59,7 @@ func process_script(path: String, enable_hook_check := false) -> String:
5659
var class_prefix := str(hash(path))
5760
var method_store: Array[String] = []
5861

59-
var getters_setters := collect_getters_and_setters(source_code, regex_getter_setter)
62+
var getters_setters := collect_getters_and_setters(source_code)
6063

6164
var moddable_methods := current_script.get_script_method_list().filter(
6265
is_func_moddable.bind(source_code, getters_setters)
@@ -68,6 +71,30 @@ func process_script(path: String, enable_hook_check := false) -> String:
6871

6972
var type_string := get_return_type_string(method.return)
7073
var is_static := true if method.flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false
74+
75+
var func_def: RegExMatch = match_func_with_whitespace(method.name, source_code)
76+
if not func_def: # Could not regex match a function with that name
77+
continue # Means invalid Script, should never happen
78+
79+
# Processing does not cover methods in subclasses yet.
80+
# If a function with the same name was found in a subclass,
81+
# try again until we find the top level one
82+
var max_loop := 1000
83+
while not is_top_level_func(source_code, func_def.get_start(), is_static): # indent before "func"
84+
func_def = match_func_with_whitespace(method.name, source_code, func_def.get_end())
85+
if not func_def or max_loop <= 0: # Couldn't match any func like before
86+
break # Means invalid Script, should never happen
87+
max_loop -= 1
88+
89+
var func_body_start_index := get_func_body_start_index(func_def.get_end(), source_code)
90+
if func_body_start_index == -1: # The function is malformed, opening ( was not closed by )
91+
continue # Means invalid Script, should never happen
92+
93+
var func_body := match_method_body(method.name, func_body_start_index, source_code)
94+
if not func_body: # No indented lines found
95+
continue # Means invalid Script, should never happen
96+
97+
var is_async := is_func_async(func_body.get_string())
7198
var method_arg_string_with_defaults_and_types := get_function_parameters(method.name, source_code, is_static)
7299
var method_arg_string_names_only := get_function_arg_name_string(method.args)
73100

@@ -77,13 +104,14 @@ func process_script(path: String, enable_hook_check := false) -> String:
77104
push_error(HASH_COLLISION_ERROR%[hashmap[hook_id], hook_id_data])
78105
hashmap[hook_id] = hook_id_data
79106

80-
var mod_loader_hook_string := get_mod_loader_hook(
107+
var mod_loader_hook_string := build_mod_hook_string(
81108
method.name,
82109
method_arg_string_names_only,
83110
method_arg_string_with_defaults_and_types,
84111
type_string,
85112
method.return.usage,
86113
is_static,
114+
is_async,
87115
hook_id,
88116
METHOD_PREFIX + class_prefix,
89117
enable_hook_check
@@ -97,15 +125,14 @@ func process_script(path: String, enable_hook_check := false) -> String:
97125
method_store.push_back(method.name)
98126
source_code = edit_vanilla_method(
99127
method.name,
100-
is_static,
101128
source_code,
102-
regex_func_body,
103-
regex_super_call,
129+
func_def,
130+
func_body,
104131
METHOD_PREFIX + class_prefix
105132
)
106133
source_code_additions += "\n%s" % mod_loader_hook_string
107134

108-
#if we have some additions to the code, append them at the end
135+
# If we have some additions to the code, append them at the end
109136
if source_code_additions != "":
110137
source_code = "%s\n%s\n%s" % [source_code, MOD_LOADER_HOOKS_START_STRING, source_code_additions]
111138

@@ -126,6 +153,65 @@ static func is_func_moddable(method: Dictionary, source_code: String, getters_se
126153
return true
127154

128155

156+
func is_func_async(func_body_text: String) -> bool:
157+
if not func_body_text.contains("await"):
158+
return false
159+
160+
var lines := func_body_text.split("\n")
161+
var in_multiline_string := false
162+
var current_multiline_delimiter := ""
163+
164+
for line: String in lines:
165+
var char_index := 0
166+
while char_index < line.length():
167+
if in_multiline_string:
168+
# Check if we are exiting the multiline string
169+
if line.substr(char_index).begins_with(current_multiline_delimiter):
170+
in_multiline_string = false
171+
char_index += 3
172+
else:
173+
char_index += 1
174+
continue
175+
176+
# Comments: Skip the rest of the line
177+
if line.substr(char_index).begins_with("#"):
178+
break
179+
180+
# Check for multiline string start
181+
if line.substr(char_index).begins_with('"""') or line.substr(char_index).begins_with("'''"):
182+
in_multiline_string = true
183+
current_multiline_delimiter = line.substr(char_index, 3)
184+
char_index += 3
185+
continue
186+
187+
# Check for single-quoted strings
188+
if line[char_index] == '"' or line[char_index] == "'":
189+
var delimiter = line[char_index]
190+
char_index += 1
191+
while char_index < line.length() and line[char_index] != delimiter:
192+
# Skip escaped quotes
193+
if line[char_index] == "\\":
194+
char_index += 1
195+
char_index += 1
196+
char_index += 1 # Skip the closing quote
197+
continue
198+
199+
# Check for the "await" keyword
200+
if not line.substr(char_index).begins_with("await"):
201+
char_index += 1
202+
continue
203+
204+
# Ensure "await" is a standalone word
205+
var start := char_index -1 if char_index > 0 else 0
206+
if regex_keyword_await.search(line.substr(start)):
207+
return true # Just return here, we don't need every occurence
208+
# i += 5 # Normal parser: Skip the keyword
209+
else:
210+
char_index += 1
211+
212+
return false
213+
214+
129215
static func get_function_arg_name_string(args: Array) -> String:
130216
var arg_string := ""
131217
for x in args.size():
@@ -191,86 +277,69 @@ static func get_closing_paren_index(opening_paren_index: int, text: String) -> i
191277
return closing_paren_index
192278

193279

194-
static func edit_vanilla_method(
280+
func edit_vanilla_method(
195281
method_name: String,
196-
is_static: bool,
197282
text: String,
198-
regex_func_body: RegEx,
199-
regex_super_call: RegEx,
283+
func_def: RegExMatch,
284+
func_body: RegExMatch,
200285
prefix := METHOD_PREFIX,
201-
offset := 0
202286
) -> String:
203-
var func_def := match_func_with_whitespace(method_name, text, offset)
204-
205-
if not func_def:
206-
return text
207-
208-
if not is_top_level_func(text, func_def.get_start(), is_static):
209-
return edit_vanilla_method(
210-
method_name,
211-
is_static,
212-
text,
213-
regex_func_body,
214-
regex_super_call,
215-
prefix,
216-
func_def.get_end()
217-
)
218-
219-
text = fix_method_super(method_name, func_def.get_end(), text, regex_func_body, regex_super_call)
287+
text = fix_method_super(method_name, func_body, text)
220288
text = text.erase(func_def.get_start(), func_def.get_end() - func_def.get_start())
221289
text = text.insert(func_def.get_start(), "func %s_%s(" % [prefix, method_name])
222290

223291
return text
224292

225293

226-
static func fix_method_super(method_name: String, func_def_end: int, text: String, regex_func_body: RegEx, regex_super_call: RegEx, offset := 0) -> String:
294+
func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
295+
return regex_super_call.sub(
296+
text, "super.%s" % method_name,
297+
true, func_body.get_start(), func_body.get_end()
298+
)
299+
300+
301+
static func get_func_body_start_index(func_def_end: int, source_code: String) -> int:
227302
# Shift the func_def_end index back by one to start on the opening parentheses.
228303
# Because the match_func_with_whitespace().get_end() is the index after the opening parentheses.
229-
var closing_paren_index := get_closing_paren_index(func_def_end - 1, text)
304+
var closing_paren_index := get_closing_paren_index(func_def_end - 1, source_code)
230305
if closing_paren_index == -1:
231-
return text
232-
var func_body_start_index := text.find(":", closing_paren_index) +1
233-
234-
var func_body := regex_func_body.search(text, func_body_start_index)
235-
if not func_body:
236-
return text
237-
var func_body_end_index := func_body.get_end()
306+
return -1
307+
return source_code.find(":", closing_paren_index) +1
238308

239-
text = regex_super_call.sub(
240-
text, "super.%s" % method_name,
241-
true, func_body_start_index, func_body_end_index
242-
)
243309

244-
return text
310+
func match_method_body(method_name: String, func_body_start_index: int, text: String) -> RegExMatch:
311+
return regex_func_body.search(text, func_body_start_index)
245312

246313

247314
static func match_func_with_whitespace(method_name: String, text: String, offset := 0) -> RegExMatch:
315+
# Dynamically create the new regex for that specific name
248316
var func_with_whitespace := RegEx.create_from_string("func\\s+%s\\s*\\(" % method_name)
249-
250-
# Search for the function definition
251317
return func_with_whitespace.search(text, offset)
252318

253319

254-
static func get_mod_loader_hook(
320+
static func build_mod_hook_string(
255321
method_name: String,
256322
method_arg_string_names_only: String,
257323
method_arg_string_with_defaults_and_types: String,
258324
method_type: String,
259325
return_prop_usage: int,
260326
is_static: bool,
327+
is_async: bool,
261328
hook_id: int,
262329
method_prefix := METHOD_PREFIX,
263330
enable_hook_check := false,
264331
) -> String:
265332
var type_string := " -> %s" % method_type if not method_type.is_empty() else ""
266333
var static_string := "static " if is_static else ""
334+
var await_string := "await " if is_async else ""
335+
var async_string := "_async" if is_async else ""
267336
var return_var := "var %s = " % "return_var" if not method_type.is_empty() or return_prop_usage == 131072 else ""
268337
var method_return := "return " if not method_type.is_empty() or return_prop_usage == 131072 else ""
269338
var hook_check := 'if ModLoaderStore.get("any_mod_hooked") and ModLoaderStore.any_mod_hooked:\n\t\t' if enable_hook_check else ""
270339

271340
return """
272341
{STATIC}func {METHOD_NAME}({METHOD_PARAMS}){RETURN_TYPE_STRING}:
273-
{HOOK_CHECK}{METHOD_RETURN}_ModLoaderHooks.call_hooks({METHOD_PREFIX}_{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID})
342+
{HOOK_CHECK}{METHOD_RETURN}{AWAIT}_ModLoaderHooks.call_hooks{ASYNC}({METHOD_PREFIX}_{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID})
274343
""".format({
275344
"METHOD_PREFIX": method_prefix,
276345
"METHOD_NAME": method_name,
@@ -280,6 +349,8 @@ static func get_mod_loader_hook(
280349
"METHOD_RETURN_VAR": return_var,
281350
"METHOD_RETURN": method_return,
282351
"STATIC": static_string,
352+
"AWAIT": await_string,
353+
"ASYNC": async_string,
283354
"HOOK_ID": hook_id,
284355
"HOOK_CHECK": hook_check,
285356
})
@@ -354,7 +425,7 @@ static func get_return_type_string(return_data: Dictionary) -> String:
354425
return "%s%s" % [type_base, type_hint]
355426

356427

357-
static func collect_getters_and_setters(text: String, regex_getter_setter: RegEx) -> Dictionary:
428+
func collect_getters_and_setters(text: String) -> Dictionary:
358429
var result := {}
359430
# a valid match has 2 or 4 groups, split into the method names and the rest of the line
360431
# (var example: set = )(example_setter)(, get = )(example_getter)

0 commit comments

Comments
 (0)