mirror of
				https://github.com/JHDev2006/Super-Mario-Bros.-Remastered-Public.git
				synced 2025-11-04 08:35:41 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			650 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			GDScript
		
	
	
	
	
	
			
		
		
	
	
			650 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			GDScript
		
	
	
	
	
	
@tool
 | 
						|
class_name _ModLoaderModHookPreProcessor
 | 
						|
extends RefCounted
 | 
						|
 | 
						|
 | 
						|
# This class is used to process the source code from a script at a given path.
 | 
						|
# Currently all of the included functions are internal and should only be used by the mod loader itself.
 | 
						|
 | 
						|
const LOG_NAME := "ModLoader:ModHookPreProcessor"
 | 
						|
 | 
						|
const REQUIRE_EXPLICIT_ADDITION := false
 | 
						|
const METHOD_PREFIX := "vanilla_"
 | 
						|
const HASH_COLLISION_ERROR := \
 | 
						|
	"MODDING HOOKS ERROR: Hash collision between %s and %s. The collision can be resolved by renaming one of the methods or changing their script's path."
 | 
						|
const MOD_LOADER_HOOKS_START_STRING := \
 | 
						|
	"\n# ModLoader Hooks - The following code has been automatically added by the Godot Mod Loader."
 | 
						|
 | 
						|
## \\bfunc\\b\\s+		->	Match the word 'func' and one or more whitespace characters
 | 
						|
## \\b%s\\b 			->	the function name
 | 
						|
## (?:.*\\n*)*?\\s*\\( 	->	Match any character between zero and unlimited times, but be lazy
 | 
						|
## 							and only do this until a '(' is found.
 | 
						|
const REGEX_MATCH_FUNC_WITH_WHITESPACE := "\\bfunc\\b\\s+\\b%s\\b(?:.*\\n*)*?\\s*\\("
 | 
						|
 | 
						|
## finds function names used as setters and getters (excluding inline definitions)
 | 
						|
## group 2 and 4 contain the setter/getter names
 | 
						|
var regex_getter_setter := RegEx.create_from_string("(.*?[sg]et\\s*=\\s*)(\\w+)(\\g<1>)?(\\g<2>)?")
 | 
						|
 | 
						|
## finds every instance where super() is called
 | 
						|
## returns only the super word, excluding the (, as match to make substitution easier
 | 
						|
var regex_super_call := RegEx.create_from_string("\\bsuper(?=\\s*\\()")
 | 
						|
 | 
						|
## Matches the indented function body.
 | 
						|
## Needs to start from the : of a function declaration to work (.search() offset param)
 | 
						|
## The body of a function is every line that is empty or starts with an indent or comment
 | 
						|
var regex_func_body := RegEx.create_from_string("(?smn)\\N*(\\n^(([\\t #]+\\N*)|$))*")
 | 
						|
 | 
						|
## Just await between word boundaries
 | 
						|
var regex_keyword_await := RegEx.create_from_string("\\bawait\\b")
 | 
						|
 | 
						|
## Just void between word boundaries
 | 
						|
var regex_keyword_void := RegEx.create_from_string("\\bvoid\\b")
 | 
						|
 | 
						|
var hashmap := {}
 | 
						|
var script_paths_hooked := {}
 | 
						|
 | 
						|
 | 
						|
func process_begin() -> void:
 | 
						|
	hashmap.clear()
 | 
						|
 | 
						|
## 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, method_mask)
 | 
						|
	ModLoaderLog.debug("Finished processing script at path: %s in %s ms" % [path, Time.get_ticks_msec() - start_time], LOG_NAME)
 | 
						|
	return processed
 | 
						|
 | 
						|
 | 
						|
## [param path]: File path to the script to be processed.[br]
 | 
						|
## [param enable_hook_check]: Adds a check that _ModLoaderHooks.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,
 | 
						|
	# since the generated methods will fulfill inheritance requirements
 | 
						|
	var class_prefix := str(hash(path))
 | 
						|
	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 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(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
 | 
						|
 | 
						|
		var func_def: RegExMatch = match_func_with_whitespace(method.name, source_code)
 | 
						|
		if not func_def: # Could not regex match a function with that name
 | 
						|
			continue # Means invalid Script, should never happen
 | 
						|
 | 
						|
		# Processing does not cover methods in subclasses yet.
 | 
						|
		# If a function with the same name was found in a subclass,
 | 
						|
		# try again until we find the top level one
 | 
						|
		var max_loop := 1000
 | 
						|
		while not is_top_level_func(source_code, func_def.get_start(), is_static): # indent before "func"
 | 
						|
			func_def = match_func_with_whitespace(method.name, source_code, func_def.get_end())
 | 
						|
			if not func_def or max_loop <= 0: # Couldn't match any func like before
 | 
						|
				break 	# Means invalid Script, unless it's a child script.
 | 
						|
								# In such cases, the method name might be listed in the script_method_list
 | 
						|
								# but absent in the actual source_code.
 | 
						|
			max_loop -= 1
 | 
						|
 | 
						|
		if not func_def: # If no valid function definition is found after processing.
 | 
						|
			continue # Skip to the next iteration.
 | 
						|
 | 
						|
		# Shift the func_def_end index back by one to start on the opening parentheses.
 | 
						|
		# Because the match_func_with_whitespace().get_end() is the index after the opening parentheses.
 | 
						|
		var closing_paren_index := get_closing_paren_index(func_def.get_end() - 1, source_code)
 | 
						|
 | 
						|
		var func_body_start_index := get_func_body_start_index(closing_paren_index, source_code)
 | 
						|
		if func_body_start_index == -1: # The function is malformed, opening ( was not closed by )
 | 
						|
			continue # Means invalid Script, should never happen
 | 
						|
 | 
						|
		var func_body := match_method_body(method.name, func_body_start_index, source_code)
 | 
						|
		if not func_body: # No indented lines found
 | 
						|
			continue # Means invalid Script, should never happen
 | 
						|
 | 
						|
		var is_async := is_func_async(func_body.get_string())
 | 
						|
		var can_return := can_return(source_code, method.name, closing_paren_index, func_body_start_index)
 | 
						|
		var method_arg_string_with_defaults_and_types := get_function_parameters(method.name, source_code, is_static)
 | 
						|
		var method_arg_string_names_only := get_function_arg_name_string(method.args)
 | 
						|
 | 
						|
		var hook_id := _ModLoaderHooks.get_hook_hash(path, method.name)
 | 
						|
		var hook_id_data := [path, method.name, true]
 | 
						|
		if hashmap.has(hook_id):
 | 
						|
			push_error(HASH_COLLISION_ERROR%[hashmap[hook_id], hook_id_data])
 | 
						|
		hashmap[hook_id] = hook_id_data
 | 
						|
 | 
						|
		var mod_loader_hook_string := build_mod_hook_string(
 | 
						|
			method.name,
 | 
						|
			method_arg_string_names_only,
 | 
						|
			method_arg_string_with_defaults_and_types,
 | 
						|
			type_string,
 | 
						|
			can_return,
 | 
						|
			is_static,
 | 
						|
			is_async,
 | 
						|
			hook_id,
 | 
						|
			full_prefix,
 | 
						|
			enable_hook_check
 | 
						|
		)
 | 
						|
 | 
						|
		# Store the method name
 | 
						|
		# Not sure if there is a way to get only the local methods in a script,
 | 
						|
		# get_script_method_list() returns a full list,
 | 
						|
		# including the methods from the scripts it extends,
 | 
						|
		# which leads to multiple entries in the list if they are overridden by the child script.
 | 
						|
		method_store.push_back(method.name)
 | 
						|
		source_code = edit_vanilla_method(
 | 
						|
			method.name,
 | 
						|
			source_code,
 | 
						|
			func_def,
 | 
						|
			func_body,
 | 
						|
			full_prefix
 | 
						|
		)
 | 
						|
		source_code_additions += "\n%s" % mod_loader_hook_string
 | 
						|
 | 
						|
		script_paths_hooked[path] = true
 | 
						|
 | 
						|
	# If we have some additions to the code, append them at the end
 | 
						|
	if source_code_additions != "":
 | 
						|
		source_code = "%s\n%s\n%s" % [source_code, MOD_LOADER_HOOKS_START_STRING, source_code_additions]
 | 
						|
 | 
						|
	return source_code
 | 
						|
 | 
						|
 | 
						|
static func is_func_moddable(method: Dictionary, source_code: String, getters_setters := {}) -> bool:
 | 
						|
	if getters_setters.has(method.name):
 | 
						|
		return false
 | 
						|
 | 
						|
	var method_first_line_start := _ModLoaderModHookPreProcessor.get_index_at_method_start(method.name, source_code)
 | 
						|
	if method_first_line_start == -1:
 | 
						|
		return false
 | 
						|
 | 
						|
	if not _ModLoaderModHookPreProcessor.is_func_marked_moddable(method_first_line_start, source_code):
 | 
						|
		return false
 | 
						|
 | 
						|
	return true
 | 
						|
 | 
						|
 | 
						|
func is_func_async(func_body_text: String) -> bool:
 | 
						|
	if not func_body_text.contains("await"):
 | 
						|
		return false
 | 
						|
 | 
						|
	var lines := func_body_text.split("\n")
 | 
						|
	var in_multiline_string := false
 | 
						|
	var current_multiline_delimiter := ""
 | 
						|
 | 
						|
	for _line in lines:
 | 
						|
		var line: String = _line
 | 
						|
		var char_index := 0
 | 
						|
		while char_index < line.length():
 | 
						|
			if in_multiline_string:
 | 
						|
				# Check if we are exiting the multiline string
 | 
						|
				if line.substr(char_index).begins_with(current_multiline_delimiter):
 | 
						|
					in_multiline_string = false
 | 
						|
					char_index += 3
 | 
						|
				else:
 | 
						|
					char_index += 1
 | 
						|
				continue
 | 
						|
 | 
						|
			# Comments: Skip the rest of the line
 | 
						|
			if line.substr(char_index).begins_with("#"):
 | 
						|
				break
 | 
						|
 | 
						|
			# Check for multiline string start
 | 
						|
			if line.substr(char_index).begins_with('"""') or line.substr(char_index).begins_with("'''"):
 | 
						|
				in_multiline_string = true
 | 
						|
				current_multiline_delimiter = line.substr(char_index, 3)
 | 
						|
				char_index += 3
 | 
						|
				continue
 | 
						|
 | 
						|
			# Check for single-quoted strings
 | 
						|
			if line[char_index] == '"' or line[char_index] == "'":
 | 
						|
				var delimiter = line[char_index]
 | 
						|
				char_index += 1
 | 
						|
				while char_index < line.length() and line[char_index] != delimiter:
 | 
						|
					# Skip escaped quotes
 | 
						|
					if line[char_index] == "\\":
 | 
						|
						char_index += 1
 | 
						|
					char_index += 1
 | 
						|
				char_index += 1  # Skip the closing quote
 | 
						|
				continue
 | 
						|
 | 
						|
			# Check for the "await" keyword
 | 
						|
			if not line.substr(char_index).begins_with("await"):
 | 
						|
				char_index += 1
 | 
						|
				continue
 | 
						|
 | 
						|
			# Ensure "await" is a standalone word
 | 
						|
			var start := char_index -1 if char_index > 0 else 0
 | 
						|
			if regex_keyword_await.search(line.substr(start)):
 | 
						|
				return true # Just return here, we don't need every occurence
 | 
						|
				# i += 5  # Normal parser: Skip the keyword
 | 
						|
			else:
 | 
						|
				char_index += 1
 | 
						|
 | 
						|
	return false
 | 
						|
 | 
						|
 | 
						|
static func get_function_arg_name_string(args: Array) -> String:
 | 
						|
	var arg_string := ""
 | 
						|
	for x in args.size():
 | 
						|
		if x == args.size() -1:
 | 
						|
			arg_string += args[x].name
 | 
						|
		else:
 | 
						|
			arg_string += "%s, " % args[x].name
 | 
						|
 | 
						|
	return arg_string
 | 
						|
 | 
						|
 | 
						|
static func get_function_parameters(method_name: String, text: String, is_static: bool, offset := 0) -> String:
 | 
						|
	var result := match_func_with_whitespace(method_name, text, offset)
 | 
						|
	if result == null:
 | 
						|
		return ""
 | 
						|
 | 
						|
	# Find the index of the opening parenthesis
 | 
						|
	var opening_paren_index := result.get_end() - 1
 | 
						|
	if opening_paren_index == -1:
 | 
						|
		return ""
 | 
						|
 | 
						|
	if not is_top_level_func(text, result.get_start(), is_static):
 | 
						|
		return get_function_parameters(method_name, text, is_static, result.get_end())
 | 
						|
 | 
						|
	# Shift the func_def_end index back by one to start on the opening parentheses.
 | 
						|
	# Because the match_func_with_whitespace().get_end() is the index after the opening parentheses.
 | 
						|
	var closing_paren_index := get_closing_paren_index(opening_paren_index - 1, text)
 | 
						|
	if closing_paren_index == -1:
 | 
						|
		return ""
 | 
						|
 | 
						|
	# Extract the substring between the parentheses
 | 
						|
	var param_string := text.substr(opening_paren_index + 1, closing_paren_index - opening_paren_index - 1)
 | 
						|
 | 
						|
	# Clean whitespace characters (spaces, newlines, tabs)
 | 
						|
	param_string = param_string.strip_edges()\
 | 
						|
		.replace(" ", "")\
 | 
						|
		.replace("\t", "")\
 | 
						|
		.replace(",", ", ")\
 | 
						|
		.replace(":", ": ")
 | 
						|
 | 
						|
	return param_string
 | 
						|
 | 
						|
 | 
						|
static func get_closing_paren_index(opening_paren_index: int, text: String) -> int:
 | 
						|
	# Use a stack counter to match parentheses
 | 
						|
	var stack := 0
 | 
						|
	var closing_paren_index := opening_paren_index
 | 
						|
	while closing_paren_index < text.length():
 | 
						|
		var char := text[closing_paren_index]
 | 
						|
		if char == '(':
 | 
						|
			stack += 1
 | 
						|
		elif char == ')':
 | 
						|
			stack -= 1
 | 
						|
			if stack == 0:
 | 
						|
				break
 | 
						|
		closing_paren_index += 1
 | 
						|
 | 
						|
	# If the stack is not empty, that means there's no matching closing parenthesis
 | 
						|
	if stack != 0:
 | 
						|
		return -1
 | 
						|
 | 
						|
	return closing_paren_index
 | 
						|
 | 
						|
 | 
						|
func edit_vanilla_method(
 | 
						|
	method_name: String,
 | 
						|
	text: String,
 | 
						|
	func_def: RegExMatch,
 | 
						|
	func_body: RegExMatch,
 | 
						|
	prefix := METHOD_PREFIX,
 | 
						|
) -> 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])
 | 
						|
 | 
						|
	return text
 | 
						|
 | 
						|
 | 
						|
func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
 | 
						|
	if _ModLoaderGodot.is_version_below(_ModLoaderGodot.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()
 | 
						|
	)
 | 
						|
 | 
						|
 | 
						|
# https://github.com/godotengine/godot/pull/86052
 | 
						|
# Quote:
 | 
						|
# 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
 | 
						|
 | 
						|
 | 
						|
static func get_func_body_start_index(closing_paren_index: int, source_code: String) -> int:
 | 
						|
	if closing_paren_index == -1:
 | 
						|
		return -1
 | 
						|
	return source_code.find(":", closing_paren_index) + 1
 | 
						|
 | 
						|
 | 
						|
func match_method_body(method_name: String, func_body_start_index: int, text: String) -> RegExMatch:
 | 
						|
	return regex_func_body.search(text, func_body_start_index)
 | 
						|
 | 
						|
 | 
						|
static func match_func_with_whitespace(method_name: String, text: String, offset := 0) -> RegExMatch:
 | 
						|
	# Dynamically create the new regex for that specific name
 | 
						|
	var func_with_whitespace := RegEx.create_from_string(REGEX_MATCH_FUNC_WITH_WHITESPACE % method_name)
 | 
						|
	return func_with_whitespace.search(text, offset)
 | 
						|
 | 
						|
 | 
						|
static func build_mod_hook_string(
 | 
						|
	method_name: String,
 | 
						|
	method_arg_string_names_only: String,
 | 
						|
	method_arg_string_with_defaults_and_types: String,
 | 
						|
	method_type: String,
 | 
						|
	can_return: bool,
 | 
						|
	is_static: bool,
 | 
						|
	is_async: bool,
 | 
						|
	hook_id: int,
 | 
						|
	method_prefix := METHOD_PREFIX,
 | 
						|
	enable_hook_check := false,
 | 
						|
) -> String:
 | 
						|
	var type_string := " -> %s" % method_type if not method_type.is_empty() else ""
 | 
						|
	var return_string := "return " if can_return else ""
 | 
						|
	var static_string := "static " if is_static else ""
 | 
						|
	var await_string := "await " if is_async else ""
 | 
						|
	var async_string := "_async" if is_async else ""
 | 
						|
	var hook_check := "if _ModLoaderHooks.any_mod_hooked:\n\t\t" if enable_hook_check else ""
 | 
						|
	var hook_check_else := get_hook_check_else_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}
 | 
						|
""".format({
 | 
						|
		"METHOD_PREFIX": method_prefix,
 | 
						|
		"METHOD_NAME": method_name,
 | 
						|
		"METHOD_PARAMS": method_arg_string_with_defaults_and_types,
 | 
						|
		"RETURN_TYPE_STRING": type_string,
 | 
						|
		"METHOD_ARGS": method_arg_string_names_only,
 | 
						|
		"STATIC": static_string,
 | 
						|
		"RETURN": return_string,
 | 
						|
		"AWAIT": await_string,
 | 
						|
		"ASYNC": async_string,
 | 
						|
		"HOOK_ID": hook_id,
 | 
						|
		"HOOK_CHECK": hook_check,
 | 
						|
		"HOOK_CHECK_ELSE": hook_check_else
 | 
						|
	})
 | 
						|
 | 
						|
 | 
						|
static func get_previous_line_to(text: String, index: int) -> String:
 | 
						|
	if index <= 0 or index >= text.length():
 | 
						|
		return ""
 | 
						|
 | 
						|
	var start_index := index - 1
 | 
						|
	# Find the end of the previous line
 | 
						|
	while start_index > 0 and text[start_index] != "\n":
 | 
						|
		start_index -= 1
 | 
						|
 | 
						|
	if start_index == 0:
 | 
						|
		return ""
 | 
						|
 | 
						|
	start_index -= 1
 | 
						|
 | 
						|
	# Find the start of the previous line
 | 
						|
	var end_index := start_index
 | 
						|
	while start_index > 0 and text[start_index - 1] != "\n":
 | 
						|
		start_index -= 1
 | 
						|
 | 
						|
	return text.substr(start_index, end_index - start_index + 1)
 | 
						|
 | 
						|
 | 
						|
static func is_func_marked_moddable(method_start_idx, text) -> bool:
 | 
						|
	var prevline := get_previous_line_to(text, method_start_idx)
 | 
						|
 | 
						|
	if prevline.contains("@not-moddable"):
 | 
						|
		return false
 | 
						|
	if not REQUIRE_EXPLICIT_ADDITION:
 | 
						|
		return true
 | 
						|
 | 
						|
	return prevline.contains("@moddable")
 | 
						|
 | 
						|
 | 
						|
static func get_index_at_method_start(method_name: String, text: String) -> int:
 | 
						|
	var result := match_func_with_whitespace(method_name, text)
 | 
						|
 | 
						|
	if result:
 | 
						|
		return text.find("\n", result.get_end())
 | 
						|
	else:
 | 
						|
		return -1
 | 
						|
 | 
						|
 | 
						|
static func is_top_level_func(text: String, result_start_index: int, is_static := false) -> bool:
 | 
						|
	if is_static:
 | 
						|
		result_start_index = text.rfind("static", result_start_index)
 | 
						|
 | 
						|
	var line_start_index := text.rfind("\n", result_start_index) + 1
 | 
						|
	var pre_func_length := result_start_index - line_start_index
 | 
						|
 | 
						|
	if pre_func_length > 0:
 | 
						|
		return false
 | 
						|
 | 
						|
	return true
 | 
						|
 | 
						|
 | 
						|
# Make sure to only pass one line
 | 
						|
static func is_comment(text: String, start_index: int) -> bool:
 | 
						|
	# Check for # before the start_index
 | 
						|
	if text.rfind("#", start_index) == -1:
 | 
						|
		return false
 | 
						|
 | 
						|
	return true
 | 
						|
 | 
						|
 | 
						|
# Get the left side substring of a line from a given start index
 | 
						|
static func get_line_left(text: String, start: int) -> String:
 | 
						|
	var line_start_index := text.rfind("\n", start) + 1
 | 
						|
	return text.substr(line_start_index, start - line_start_index)
 | 
						|
 | 
						|
 | 
						|
# Check if a static void type is declared
 | 
						|
func is_void(source_code: String, func_def_closing_paren_index: int, func_body_start_index: int) -> bool:
 | 
						|
	var func_def_end_index := func_body_start_index - 1 # func_body_start_index - 1 should be `:` position.
 | 
						|
	var type_zone := source_code.substr(func_def_closing_paren_index, func_def_end_index - func_def_closing_paren_index)
 | 
						|
 | 
						|
	for void_match in regex_keyword_void.search_all(type_zone):
 | 
						|
		if is_comment(
 | 
						|
			get_line_left(type_zone, void_match.get_start()),
 | 
						|
			void_match.get_start()
 | 
						|
		):
 | 
						|
			continue
 | 
						|
 | 
						|
		return true
 | 
						|
 | 
						|
	return false
 | 
						|
 | 
						|
 | 
						|
func can_return(source_code: String, method_name: String, func_def_closing_paren_index: int, func_body_start_index: int) -> bool:
 | 
						|
	if method_name == "_init":
 | 
						|
		return false
 | 
						|
	if is_void(source_code, func_def_closing_paren_index, func_body_start_index):
 | 
						|
		return false
 | 
						|
 | 
						|
	return true
 | 
						|
 | 
						|
 | 
						|
static func get_return_type_string(return_data: Dictionary) -> String:
 | 
						|
	if return_data.type == 0:
 | 
						|
		return ""
 | 
						|
	var type_base: String
 | 
						|
	if return_data.has("class_name") and not str(return_data.class_name).is_empty():
 | 
						|
		type_base = str(return_data.class_name)
 | 
						|
	else:
 | 
						|
		type_base = get_type_name(return_data.type)
 | 
						|
 | 
						|
	var type_hint: String = "" if return_data.hint_string.is_empty() else ("[%s]" % return_data.hint_string)
 | 
						|
 | 
						|
	return "%s%s" % [type_base, type_hint]
 | 
						|
 | 
						|
 | 
						|
func collect_getters_and_setters(text: String) -> Dictionary:
 | 
						|
	var result := {}
 | 
						|
	# a valid match has 2 or 4 groups, split into the method names and the rest of the line
 | 
						|
	# (var example: set = )(example_setter)(, get = )(example_getter)
 | 
						|
	# if things between the names are empty or commented, exclude them
 | 
						|
	for mat in regex_getter_setter.search_all(text):
 | 
						|
		if mat.get_string(1).is_empty() or mat.get_string(1).contains("#"):
 | 
						|
			continue
 | 
						|
		result[mat.get_string(2)] = true
 | 
						|
 | 
						|
		if mat.get_string(3).is_empty() or mat.get_string(3).contains("#"):
 | 
						|
			continue
 | 
						|
		result[mat.get_string(4)] = true
 | 
						|
 | 
						|
	return result
 | 
						|
 | 
						|
 | 
						|
static func get_hook_check_else_string(
 | 
						|
	return_string: String,
 | 
						|
	await_string: String,
 | 
						|
	method_prefix: 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": return_string,
 | 
						|
				"AWAIT": await_string,
 | 
						|
				"METHOD_PREFIX": method_prefix,
 | 
						|
				"METHOD_NAME": method_name,
 | 
						|
				"METHOD_ARGS": method_arg_string_names_only
 | 
						|
			}
 | 
						|
		)
 | 
						|
 | 
						|
 | 
						|
# This function was taken from
 | 
						|
# https://github.com/godotengine/godot/blob/7e67b496ff7e35f66b88adcbdd5b252d01739cbb/modules/gdscript/tests/scripts/utils.notest.gd#L69
 | 
						|
# It is used instead of type_string because type_string does not exist in Godot 4.1
 | 
						|
static func get_type_name(type: Variant.Type) -> String:
 | 
						|
	match type:
 | 
						|
		TYPE_NIL:
 | 
						|
			return "Nil" # `Nil` in core, `null` in GDScript.
 | 
						|
		TYPE_BOOL:
 | 
						|
			return "bool"
 | 
						|
		TYPE_INT:
 | 
						|
			return "int"
 | 
						|
		TYPE_FLOAT:
 | 
						|
			return "float"
 | 
						|
		TYPE_STRING:
 | 
						|
			return "String"
 | 
						|
		TYPE_VECTOR2:
 | 
						|
			return "Vector2"
 | 
						|
		TYPE_VECTOR2I:
 | 
						|
			return "Vector2i"
 | 
						|
		TYPE_RECT2:
 | 
						|
			return "Rect2"
 | 
						|
		TYPE_RECT2I:
 | 
						|
			return "Rect2i"
 | 
						|
		TYPE_VECTOR3:
 | 
						|
			return "Vector3"
 | 
						|
		TYPE_VECTOR3I:
 | 
						|
			return "Vector3i"
 | 
						|
		TYPE_TRANSFORM2D:
 | 
						|
			return "Transform2D"
 | 
						|
		TYPE_VECTOR4:
 | 
						|
			return "Vector4"
 | 
						|
		TYPE_VECTOR4I:
 | 
						|
			return "Vector4i"
 | 
						|
		TYPE_PLANE:
 | 
						|
			return "Plane"
 | 
						|
		TYPE_QUATERNION:
 | 
						|
			return "Quaternion"
 | 
						|
		TYPE_AABB:
 | 
						|
			return "AABB"
 | 
						|
		TYPE_BASIS:
 | 
						|
			return "Basis"
 | 
						|
		TYPE_TRANSFORM3D:
 | 
						|
			return "Transform3D"
 | 
						|
		TYPE_PROJECTION:
 | 
						|
			return "Projection"
 | 
						|
		TYPE_COLOR:
 | 
						|
			return "Color"
 | 
						|
		TYPE_STRING_NAME:
 | 
						|
			return "StringName"
 | 
						|
		TYPE_NODE_PATH:
 | 
						|
			return "NodePath"
 | 
						|
		TYPE_RID:
 | 
						|
			return "RID"
 | 
						|
		TYPE_OBJECT:
 | 
						|
			return "Object"
 | 
						|
		TYPE_CALLABLE:
 | 
						|
			return "Callable"
 | 
						|
		TYPE_SIGNAL:
 | 
						|
			return "Signal"
 | 
						|
		TYPE_DICTIONARY:
 | 
						|
			return "Dictionary"
 | 
						|
		TYPE_ARRAY:
 | 
						|
			return "Array"
 | 
						|
		TYPE_PACKED_BYTE_ARRAY:
 | 
						|
			return "PackedByteArray"
 | 
						|
		TYPE_PACKED_INT32_ARRAY:
 | 
						|
			return "PackedInt32Array"
 | 
						|
		TYPE_PACKED_INT64_ARRAY:
 | 
						|
			return "PackedInt64Array"
 | 
						|
		TYPE_PACKED_FLOAT32_ARRAY:
 | 
						|
			return "PackedFloat32Array"
 | 
						|
		TYPE_PACKED_FLOAT64_ARRAY:
 | 
						|
			return "PackedFloat64Array"
 | 
						|
		TYPE_PACKED_STRING_ARRAY:
 | 
						|
			return "PackedStringArray"
 | 
						|
		TYPE_PACKED_VECTOR2_ARRAY:
 | 
						|
			return "PackedVector2Array"
 | 
						|
		TYPE_PACKED_VECTOR3_ARRAY:
 | 
						|
			return "PackedVector3Array"
 | 
						|
		TYPE_PACKED_COLOR_ARRAY:
 | 
						|
			return "PackedColorArray"
 | 
						|
		38: # TYPE_PACKED_VECTOR4_ARRAY
 | 
						|
			return "PackedVector4Array"
 | 
						|
	push_error("Argument `type` is invalid. Use `TYPE_*` constants.")
 | 
						|
	return "<unknown type %s>" % type
 |