mod_hook_preprocessor.gd 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. @tool
  2. class_name _ModLoaderModHookPreProcessor
  3. extends RefCounted
  4. # This class is used to process the source code from a script at a given path.
  5. # Currently all of the included functions are internal and should only be used by the mod loader itself.
  6. const LOG_NAME := "ModLoader:ModHookPreProcessor"
  7. const REQUIRE_EXPLICIT_ADDITION := false
  8. const METHOD_PREFIX := "vanilla_"
  9. const HASH_COLLISION_ERROR := \
  10. "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."
  11. const MOD_LOADER_HOOKS_START_STRING := \
  12. "\n# ModLoader Hooks - The following code has been automatically added by the Godot Mod Loader."
  13. ## \\bfunc\\b\\s+ -> Match the word 'func' and one or more whitespace characters
  14. ## \\b%s\\b -> the function name
  15. ## (?:.*\\n*)*?\\s*\\( -> Match any character between zero and unlimited times, but be lazy
  16. ## and only do this until a '(' is found.
  17. const REGEX_MATCH_FUNC_WITH_WHITESPACE := "\\bfunc\\b\\s+\\b%s\\b(?:.*\\n*)*?\\s*\\("
  18. ## finds function names used as setters and getters (excluding inline definitions)
  19. ## group 2 and 4 contain the setter/getter names
  20. var regex_getter_setter := RegEx.create_from_string("(.*?[sg]et\\s*=\\s*)(\\w+)(\\g<1>)?(\\g<2>)?")
  21. ## finds every instance where super() is called
  22. ## returns only the super word, excluding the (, as match to make substitution easier
  23. var regex_super_call := RegEx.create_from_string("\\bsuper(?=\\s*\\()")
  24. ## Matches the indented function body.
  25. ## Needs to start from the : of a function declaration to work (.search() offset param)
  26. ## The body of a function is every line that is empty or starts with an indent or comment
  27. var regex_func_body := RegEx.create_from_string("(?smn)\\N*(\\n^(([\\t #]+\\N*)|$))*")
  28. ## Just await between word boundaries
  29. var regex_keyword_await := RegEx.create_from_string("\\bawait\\b")
  30. ## Just void between word boundaries
  31. var regex_keyword_void := RegEx.create_from_string("\\bvoid\\b")
  32. var hashmap := {}
  33. var script_paths_hooked := {}
  34. func process_begin() -> void:
  35. hashmap.clear()
  36. ## Calls [method process_script] with additional logging
  37. func process_script_verbose(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
  38. var start_time := Time.get_ticks_msec()
  39. ModLoaderLog.debug("Start processing script at path: %s" % path, LOG_NAME)
  40. var processed := process_script(path, enable_hook_check, method_mask)
  41. ModLoaderLog.debug("Finished processing script at path: %s in %s ms" % [path, Time.get_ticks_msec() - start_time], LOG_NAME)
  42. return processed
  43. ## [param path]: File path to the script to be processed.[br]
  44. ## [param enable_hook_check]: Adds a check that _ModLoaderHooks.any_mod_hooked is [code]true[/code] to the processed method, reducing hash checks.[br]
  45. ## [param method_mask]: If provided, only methods in this [Array] will be processed.[br]
  46. func process_script(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
  47. var current_script := load(path) as GDScript
  48. var source_code := current_script.source_code
  49. var source_code_additions := ""
  50. # We need to stop all vanilla methods from forming inheritance chains,
  51. # since the generated methods will fulfill inheritance requirements
  52. var class_prefix := str(hash(path))
  53. var method_store: Array[String] = []
  54. var getters_setters := collect_getters_and_setters(source_code)
  55. var moddable_methods := current_script.get_script_method_list().filter(
  56. is_func_moddable.bind(source_code, getters_setters)
  57. )
  58. var methods_hooked := {}
  59. for method in moddable_methods:
  60. if method.name in method_store:
  61. continue
  62. var full_prefix := "%s%s_" % [METHOD_PREFIX, class_prefix]
  63. # Check if the method name starts with the prefix added by `edit_vanilla_method()`.
  64. # This indicates that the method was previously processed, possibly by the export plugin.
  65. # If so, store the method name (excluding the prefix) in `methods_hooked`.
  66. if method.name.begins_with(full_prefix):
  67. var method_name_vanilla: String = method.name.trim_prefix(full_prefix)
  68. methods_hooked[method_name_vanilla] = true
  69. continue
  70. # This ensures we avoid creating a hook for the 'imposter' method, which
  71. # is generated by `build_mod_hook_string()` and has the vanilla method name.
  72. if methods_hooked.has(method.name):
  73. continue
  74. # If a mask is provided, only methods with their name in the mask will be converted.
  75. # Can't be filtered before the loop since it removes prefixed methods required by the previous check.
  76. if not method_mask.is_empty():
  77. if not method.name in method_mask:
  78. continue
  79. var type_string := get_return_type_string(method.return)
  80. var is_static := true if method.flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false
  81. var func_def: RegExMatch = match_func_with_whitespace(method.name, source_code)
  82. if not func_def: # Could not regex match a function with that name
  83. continue # Means invalid Script, should never happen
  84. # Processing does not cover methods in subclasses yet.
  85. # If a function with the same name was found in a subclass,
  86. # try again until we find the top level one
  87. var max_loop := 1000
  88. while not is_top_level_func(source_code, func_def.get_start(), is_static): # indent before "func"
  89. func_def = match_func_with_whitespace(method.name, source_code, func_def.get_end())
  90. if not func_def or max_loop <= 0: # Couldn't match any func like before
  91. break # Means invalid Script, unless it's a child script.
  92. # In such cases, the method name might be listed in the script_method_list
  93. # but absent in the actual source_code.
  94. max_loop -= 1
  95. if not func_def: # If no valid function definition is found after processing.
  96. continue # Skip to the next iteration.
  97. # Shift the func_def_end index back by one to start on the opening parentheses.
  98. # Because the match_func_with_whitespace().get_end() is the index after the opening parentheses.
  99. var closing_paren_index := get_closing_paren_index(func_def.get_end() - 1, source_code)
  100. var func_body_start_index := get_func_body_start_index(closing_paren_index, source_code)
  101. if func_body_start_index == -1: # The function is malformed, opening ( was not closed by )
  102. continue # Means invalid Script, should never happen
  103. var func_body := match_method_body(method.name, func_body_start_index, source_code)
  104. if not func_body: # No indented lines found
  105. continue # Means invalid Script, should never happen
  106. var is_async := is_func_async(func_body.get_string())
  107. var can_return := can_return(source_code, method.name, closing_paren_index, func_body_start_index)
  108. var method_arg_string_with_defaults_and_types := get_function_parameters(method.name, source_code, is_static)
  109. var method_arg_string_names_only := get_function_arg_name_string(method.args)
  110. var hook_id := _ModLoaderHooks.get_hook_hash(path, method.name)
  111. var hook_id_data := [path, method.name, true]
  112. if hashmap.has(hook_id):
  113. push_error(HASH_COLLISION_ERROR%[hashmap[hook_id], hook_id_data])
  114. hashmap[hook_id] = hook_id_data
  115. var mod_loader_hook_string := build_mod_hook_string(
  116. method.name,
  117. method_arg_string_names_only,
  118. method_arg_string_with_defaults_and_types,
  119. type_string,
  120. can_return,
  121. is_static,
  122. is_async,
  123. hook_id,
  124. full_prefix,
  125. enable_hook_check
  126. )
  127. # Store the method name
  128. # Not sure if there is a way to get only the local methods in a script,
  129. # get_script_method_list() returns a full list,
  130. # including the methods from the scripts it extends,
  131. # which leads to multiple entries in the list if they are overridden by the child script.
  132. method_store.push_back(method.name)
  133. source_code = edit_vanilla_method(
  134. method.name,
  135. source_code,
  136. func_def,
  137. func_body,
  138. full_prefix
  139. )
  140. source_code_additions += "\n%s" % mod_loader_hook_string
  141. script_paths_hooked[path] = true
  142. # If we have some additions to the code, append them at the end
  143. if source_code_additions != "":
  144. source_code = "%s\n%s\n%s" % [source_code, MOD_LOADER_HOOKS_START_STRING, source_code_additions]
  145. return source_code
  146. static func is_func_moddable(method: Dictionary, source_code: String, getters_setters := {}) -> bool:
  147. if getters_setters.has(method.name):
  148. return false
  149. var method_first_line_start := _ModLoaderModHookPreProcessor.get_index_at_method_start(method.name, source_code)
  150. if method_first_line_start == -1:
  151. return false
  152. if not _ModLoaderModHookPreProcessor.is_func_marked_moddable(method_first_line_start, source_code):
  153. return false
  154. return true
  155. func is_func_async(func_body_text: String) -> bool:
  156. if not func_body_text.contains("await"):
  157. return false
  158. var lines := func_body_text.split("\n")
  159. var in_multiline_string := false
  160. var current_multiline_delimiter := ""
  161. for _line in lines:
  162. var line: String = _line
  163. var char_index := 0
  164. while char_index < line.length():
  165. if in_multiline_string:
  166. # Check if we are exiting the multiline string
  167. if line.substr(char_index).begins_with(current_multiline_delimiter):
  168. in_multiline_string = false
  169. char_index += 3
  170. else:
  171. char_index += 1
  172. continue
  173. # Comments: Skip the rest of the line
  174. if line.substr(char_index).begins_with("#"):
  175. break
  176. # Check for multiline string start
  177. if line.substr(char_index).begins_with('"""') or line.substr(char_index).begins_with("'''"):
  178. in_multiline_string = true
  179. current_multiline_delimiter = line.substr(char_index, 3)
  180. char_index += 3
  181. continue
  182. # Check for single-quoted strings
  183. if line[char_index] == '"' or line[char_index] == "'":
  184. var delimiter = line[char_index]
  185. char_index += 1
  186. while char_index < line.length() and line[char_index] != delimiter:
  187. # Skip escaped quotes
  188. if line[char_index] == "\\":
  189. char_index += 1
  190. char_index += 1
  191. char_index += 1 # Skip the closing quote
  192. continue
  193. # Check for the "await" keyword
  194. if not line.substr(char_index).begins_with("await"):
  195. char_index += 1
  196. continue
  197. # Ensure "await" is a standalone word
  198. var start := char_index -1 if char_index > 0 else 0
  199. if regex_keyword_await.search(line.substr(start)):
  200. return true # Just return here, we don't need every occurence
  201. # i += 5 # Normal parser: Skip the keyword
  202. else:
  203. char_index += 1
  204. return false
  205. static func get_function_arg_name_string(args: Array) -> String:
  206. var arg_string := ""
  207. for x in args.size():
  208. if x == args.size() -1:
  209. arg_string += args[x].name
  210. else:
  211. arg_string += "%s, " % args[x].name
  212. return arg_string
  213. static func get_function_parameters(method_name: String, text: String, is_static: bool, offset := 0) -> String:
  214. var result := match_func_with_whitespace(method_name, text, offset)
  215. if result == null:
  216. return ""
  217. # Find the index of the opening parenthesis
  218. var opening_paren_index := result.get_end() - 1
  219. if opening_paren_index == -1:
  220. return ""
  221. if not is_top_level_func(text, result.get_start(), is_static):
  222. return get_function_parameters(method_name, text, is_static, result.get_end())
  223. # Shift the func_def_end index back by one to start on the opening parentheses.
  224. # Because the match_func_with_whitespace().get_end() is the index after the opening parentheses.
  225. var closing_paren_index := get_closing_paren_index(opening_paren_index - 1, text)
  226. if closing_paren_index == -1:
  227. return ""
  228. # Extract the substring between the parentheses
  229. var param_string := text.substr(opening_paren_index + 1, closing_paren_index - opening_paren_index - 1)
  230. # Clean whitespace characters (spaces, newlines, tabs)
  231. param_string = param_string.strip_edges()\
  232. .replace(" ", "")\
  233. .replace("\t", "")\
  234. .replace(",", ", ")\
  235. .replace(":", ": ")
  236. return param_string
  237. static func get_closing_paren_index(opening_paren_index: int, text: String) -> int:
  238. # Use a stack counter to match parentheses
  239. var stack := 0
  240. var closing_paren_index := opening_paren_index
  241. while closing_paren_index < text.length():
  242. var char := text[closing_paren_index]
  243. if char == '(':
  244. stack += 1
  245. elif char == ')':
  246. stack -= 1
  247. if stack == 0:
  248. break
  249. closing_paren_index += 1
  250. # If the stack is not empty, that means there's no matching closing parenthesis
  251. if stack != 0:
  252. return -1
  253. return closing_paren_index
  254. func edit_vanilla_method(
  255. method_name: String,
  256. text: String,
  257. func_def: RegExMatch,
  258. func_body: RegExMatch,
  259. prefix := METHOD_PREFIX,
  260. ) -> String:
  261. text = fix_method_super(method_name, func_body, text)
  262. text = text.erase(func_def.get_start(), func_def.get_end() - func_def.get_start())
  263. text = text.insert(func_def.get_start(), "func %s%s(" % [prefix, method_name])
  264. return text
  265. func fix_method_super(method_name: String, func_body: RegExMatch, text: String) -> String:
  266. if _ModLoaderGodot.is_version_below(_ModLoaderGodot.ENGINE_VERSION_HEX_4_2_2):
  267. return fix_method_super_before_4_2_2(method_name, func_body, text)
  268. return regex_super_call.sub(
  269. text, "super.%s" % method_name,
  270. true, func_body.get_start(), func_body.get_end()
  271. )
  272. # https://github.com/godotengine/godot/pull/86052
  273. # Quote:
  274. # When the end argument of RegEx.sub was used,
  275. # it would truncate the Subject String before even doing the substitution.
  276. func fix_method_super_before_4_2_2(method_name: String, func_body: RegExMatch, text: String) -> String:
  277. var text_after_func_body_end := text.substr(func_body.get_end())
  278. text = regex_super_call.sub(
  279. text, "super.%s" % method_name,
  280. true, func_body.get_start(), func_body.get_end()
  281. )
  282. text = text + text_after_func_body_end
  283. return text
  284. static func get_func_body_start_index(closing_paren_index: int, source_code: String) -> int:
  285. if closing_paren_index == -1:
  286. return -1
  287. return source_code.find(":", closing_paren_index) + 1
  288. func match_method_body(method_name: String, func_body_start_index: int, text: String) -> RegExMatch:
  289. return regex_func_body.search(text, func_body_start_index)
  290. static func match_func_with_whitespace(method_name: String, text: String, offset := 0) -> RegExMatch:
  291. # Dynamically create the new regex for that specific name
  292. var func_with_whitespace := RegEx.create_from_string(REGEX_MATCH_FUNC_WITH_WHITESPACE % method_name)
  293. return func_with_whitespace.search(text, offset)
  294. static func build_mod_hook_string(
  295. method_name: String,
  296. method_arg_string_names_only: String,
  297. method_arg_string_with_defaults_and_types: String,
  298. method_type: String,
  299. can_return: bool,
  300. is_static: bool,
  301. is_async: bool,
  302. hook_id: int,
  303. method_prefix := METHOD_PREFIX,
  304. enable_hook_check := false,
  305. ) -> String:
  306. var type_string := " -> %s" % method_type if not method_type.is_empty() else ""
  307. var return_string := "return " if can_return else ""
  308. var static_string := "static " if is_static else ""
  309. var await_string := "await " if is_async else ""
  310. var async_string := "_async" if is_async else ""
  311. var hook_check := "if _ModLoaderHooks.any_mod_hooked:\n\t\t" if enable_hook_check else ""
  312. var hook_check_else := get_hook_check_else_string(
  313. return_string, await_string, method_prefix, method_name, method_arg_string_names_only
  314. ) if enable_hook_check else ""
  315. return """
  316. {STATIC}func {METHOD_NAME}({METHOD_PARAMS}){RETURN_TYPE_STRING}:
  317. {HOOK_CHECK}{RETURN}{AWAIT}_ModLoaderHooks.call_hooks{ASYNC}({METHOD_PREFIX}{METHOD_NAME}, [{METHOD_ARGS}], {HOOK_ID}){HOOK_CHECK_ELSE}
  318. """.format({
  319. "METHOD_PREFIX": method_prefix,
  320. "METHOD_NAME": method_name,
  321. "METHOD_PARAMS": method_arg_string_with_defaults_and_types,
  322. "RETURN_TYPE_STRING": type_string,
  323. "METHOD_ARGS": method_arg_string_names_only,
  324. "STATIC": static_string,
  325. "RETURN": return_string,
  326. "AWAIT": await_string,
  327. "ASYNC": async_string,
  328. "HOOK_ID": hook_id,
  329. "HOOK_CHECK": hook_check,
  330. "HOOK_CHECK_ELSE": hook_check_else
  331. })
  332. static func get_previous_line_to(text: String, index: int) -> String:
  333. if index <= 0 or index >= text.length():
  334. return ""
  335. var start_index := index - 1
  336. # Find the end of the previous line
  337. while start_index > 0 and text[start_index] != "\n":
  338. start_index -= 1
  339. if start_index == 0:
  340. return ""
  341. start_index -= 1
  342. # Find the start of the previous line
  343. var end_index := start_index
  344. while start_index > 0 and text[start_index - 1] != "\n":
  345. start_index -= 1
  346. return text.substr(start_index, end_index - start_index + 1)
  347. static func is_func_marked_moddable(method_start_idx, text) -> bool:
  348. var prevline := get_previous_line_to(text, method_start_idx)
  349. if prevline.contains("@not-moddable"):
  350. return false
  351. if not REQUIRE_EXPLICIT_ADDITION:
  352. return true
  353. return prevline.contains("@moddable")
  354. static func get_index_at_method_start(method_name: String, text: String) -> int:
  355. var result := match_func_with_whitespace(method_name, text)
  356. if result:
  357. return text.find("\n", result.get_end())
  358. else:
  359. return -1
  360. static func is_top_level_func(text: String, result_start_index: int, is_static := false) -> bool:
  361. if is_static:
  362. result_start_index = text.rfind("static", result_start_index)
  363. var line_start_index := text.rfind("\n", result_start_index) + 1
  364. var pre_func_length := result_start_index - line_start_index
  365. if pre_func_length > 0:
  366. return false
  367. return true
  368. # Make sure to only pass one line
  369. static func is_comment(text: String, start_index: int) -> bool:
  370. # Check for # before the start_index
  371. if text.rfind("#", start_index) == -1:
  372. return false
  373. return true
  374. # Get the left side substring of a line from a given start index
  375. static func get_line_left(text: String, start: int) -> String:
  376. var line_start_index := text.rfind("\n", start) + 1
  377. return text.substr(line_start_index, start - line_start_index)
  378. # Check if a static void type is declared
  379. func is_void(source_code: String, func_def_closing_paren_index: int, func_body_start_index: int) -> bool:
  380. var func_def_end_index := func_body_start_index - 1 # func_body_start_index - 1 should be `:` position.
  381. var type_zone := source_code.substr(func_def_closing_paren_index, func_def_end_index - func_def_closing_paren_index)
  382. for void_match in regex_keyword_void.search_all(type_zone):
  383. if is_comment(
  384. get_line_left(type_zone, void_match.get_start()),
  385. void_match.get_start()
  386. ):
  387. continue
  388. return true
  389. return false
  390. func can_return(source_code: String, method_name: String, func_def_closing_paren_index: int, func_body_start_index: int) -> bool:
  391. if method_name == "_init":
  392. return false
  393. if is_void(source_code, func_def_closing_paren_index, func_body_start_index):
  394. return false
  395. return true
  396. static func get_return_type_string(return_data: Dictionary) -> String:
  397. if return_data.type == 0:
  398. return ""
  399. var type_base: String
  400. if return_data.has("class_name") and not str(return_data.class_name).is_empty():
  401. type_base = str(return_data.class_name)
  402. else:
  403. type_base = get_type_name(return_data.type)
  404. var type_hint: String = "" if return_data.hint_string.is_empty() else ("[%s]" % return_data.hint_string)
  405. return "%s%s" % [type_base, type_hint]
  406. func collect_getters_and_setters(text: String) -> Dictionary:
  407. var result := {}
  408. # a valid match has 2 or 4 groups, split into the method names and the rest of the line
  409. # (var example: set = )(example_setter)(, get = )(example_getter)
  410. # if things between the names are empty or commented, exclude them
  411. for mat in regex_getter_setter.search_all(text):
  412. if mat.get_string(1).is_empty() or mat.get_string(1).contains("#"):
  413. continue
  414. result[mat.get_string(2)] = true
  415. if mat.get_string(3).is_empty() or mat.get_string(3).contains("#"):
  416. continue
  417. result[mat.get_string(4)] = true
  418. return result
  419. static func get_hook_check_else_string(
  420. return_string: String,
  421. await_string: String,
  422. method_prefix: String,
  423. method_name: String,
  424. method_arg_string_names_only: String
  425. ) -> String:
  426. return "\n\telse:\n\t\t{RETURN}{AWAIT}{METHOD_PREFIX}{METHOD_NAME}({METHOD_ARGS})".format(
  427. {
  428. "RETURN": return_string,
  429. "AWAIT": await_string,
  430. "METHOD_PREFIX": method_prefix,
  431. "METHOD_NAME": method_name,
  432. "METHOD_ARGS": method_arg_string_names_only
  433. }
  434. )
  435. # This function was taken from
  436. # https://github.com/godotengine/godot/blob/7e67b496ff7e35f66b88adcbdd5b252d01739cbb/modules/gdscript/tests/scripts/utils.notest.gd#L69
  437. # It is used instead of type_string because type_string does not exist in Godot 4.1
  438. static func get_type_name(type: Variant.Type) -> String:
  439. match type:
  440. TYPE_NIL:
  441. return "Nil" # `Nil` in core, `null` in GDScript.
  442. TYPE_BOOL:
  443. return "bool"
  444. TYPE_INT:
  445. return "int"
  446. TYPE_FLOAT:
  447. return "float"
  448. TYPE_STRING:
  449. return "String"
  450. TYPE_VECTOR2:
  451. return "Vector2"
  452. TYPE_VECTOR2I:
  453. return "Vector2i"
  454. TYPE_RECT2:
  455. return "Rect2"
  456. TYPE_RECT2I:
  457. return "Rect2i"
  458. TYPE_VECTOR3:
  459. return "Vector3"
  460. TYPE_VECTOR3I:
  461. return "Vector3i"
  462. TYPE_TRANSFORM2D:
  463. return "Transform2D"
  464. TYPE_VECTOR4:
  465. return "Vector4"
  466. TYPE_VECTOR4I:
  467. return "Vector4i"
  468. TYPE_PLANE:
  469. return "Plane"
  470. TYPE_QUATERNION:
  471. return "Quaternion"
  472. TYPE_AABB:
  473. return "AABB"
  474. TYPE_BASIS:
  475. return "Basis"
  476. TYPE_TRANSFORM3D:
  477. return "Transform3D"
  478. TYPE_PROJECTION:
  479. return "Projection"
  480. TYPE_COLOR:
  481. return "Color"
  482. TYPE_STRING_NAME:
  483. return "StringName"
  484. TYPE_NODE_PATH:
  485. return "NodePath"
  486. TYPE_RID:
  487. return "RID"
  488. TYPE_OBJECT:
  489. return "Object"
  490. TYPE_CALLABLE:
  491. return "Callable"
  492. TYPE_SIGNAL:
  493. return "Signal"
  494. TYPE_DICTIONARY:
  495. return "Dictionary"
  496. TYPE_ARRAY:
  497. return "Array"
  498. TYPE_PACKED_BYTE_ARRAY:
  499. return "PackedByteArray"
  500. TYPE_PACKED_INT32_ARRAY:
  501. return "PackedInt32Array"
  502. TYPE_PACKED_INT64_ARRAY:
  503. return "PackedInt64Array"
  504. TYPE_PACKED_FLOAT32_ARRAY:
  505. return "PackedFloat32Array"
  506. TYPE_PACKED_FLOAT64_ARRAY:
  507. return "PackedFloat64Array"
  508. TYPE_PACKED_STRING_ARRAY:
  509. return "PackedStringArray"
  510. TYPE_PACKED_VECTOR2_ARRAY:
  511. return "PackedVector2Array"
  512. TYPE_PACKED_VECTOR3_ARRAY:
  513. return "PackedVector3Array"
  514. TYPE_PACKED_COLOR_ARRAY:
  515. return "PackedColorArray"
  516. 38: # TYPE_PACKED_VECTOR4_ARRAY
  517. return "PackedVector4Array"
  518. push_error("Argument `type` is invalid. Use `TYPE_*` constants.")
  519. return "<unknown type %s>" % type