Files
Super-Mario-Bros.-Remastere…/addons/mod_tool/global/utils.gd
BarrierFalki 78f68b3be1 Allow Mod Loader to automatically add hooks on export (#405)
* Update Mod Loader and Tool addons

* Mod tool checks if script exists before reloading

* Change script export mode to Text for hook export

* Change return type of load_image_from_path to Texture2D
2025-09-26 19:30:40 +01:00

244 lines
7.4 KiB
GDScript

@tool
extends Node
class_name ModToolUtils
# Utility functions used across the ModTool.
# ! Not used currently. This can overwrite existing text very easily if the wrong script is shown in the text editor.
static func reload_script(script: Script, mod_tool_store: ModToolStore) -> void:
var pending_reloads := mod_tool_store.pending_reloads
if script and script.resource_path in pending_reloads:
var source_code_from_disc := FileAccess.open(script.resource_path, FileAccess.READ).get_as_text()
var script_editor := EditorInterface.get_script_editor()
var text_edit: CodeEdit = script_editor.get_current_editor().get_base_editor()
var column := text_edit.get_caret_column()
var row := text_edit.get_caret_line()
var scroll_position_h := text_edit.get_h_scroll_bar().value
var scroll_position_v := text_edit.get_v_scroll_bar().value
text_edit.text = source_code_from_disc
text_edit.set_caret_column(column)
text_edit.set_caret_line(row)
text_edit.scroll_horizontal = scroll_position_h
text_edit.scroll_vertical = scroll_position_v
text_edit.tag_saved_version()
pending_reloads.erase(script.resource_path)
# Takes a file path and an array of file extensions [.txt, .tscn, ..]
static func is_file_extension(path: String, excluded_extensions: PackedStringArray) -> bool:
var is_extension := false
for extension in excluded_extensions:
var file_name := path.get_file()
if(extension in file_name):
is_extension = true
break
else:
is_extension = false
return is_extension
# Returns the content of the file from the given path as a string.
static func file_get_as_text(path: String) -> String:
var file_access := FileAccess.open(path, FileAccess.READ)
var content := file_access.get_as_text()
file_access.close()
return content
# Copies a file from a given src to the specified dst path.
# src = path/to/file.extension
# dst = other/path/to/file.extension
static func file_copy(src: String, dst: String) -> void:
var dst_dir := dst.get_base_dir()
if not DirAccess.dir_exists_absolute(dst_dir):
DirAccess.make_dir_recursive_absolute(dst_dir)
DirAccess.copy_absolute(src, dst)
# Log error messages
static func output_error(message) -> void:
printerr("ModTool Error: " + str(message))
static func output_info(message) -> void:
print("ModTool: " + str(message))
static func save_to_manifest_json(manifest_data: ModManifest, path_manifest: String) -> bool:
var is_success := _ModLoaderFile._save_string_to_file(
manifest_data.to_json(),
path_manifest
)
if is_success:
output_info("Successfully saved manifest.json file!")
return is_success
static func make_dir_recursive(dst_dir: String) -> bool:
var error := DirAccess.make_dir_recursive_absolute(dst_dir)
if not error == OK:
output_error("Failed creating directory at %s with error \"%s\"" % [dst_dir, error_string(error)])
return false
return true
# Takes a directory path to get removed.
# https://www.davidepesce.com/2019/11/04/essential-guide-to-godot-filesystem-api/
static func remove_recursive(path: String) -> void:
var directory := DirAccess.open(path)
if not directory:
print("Error removing " + path)
return
# List directory content
directory.list_dir_begin()
var file_name := directory.get_next()
while file_name != "":
if directory.current_is_dir():
remove_recursive(path + "/" + file_name)
else:
directory.remove(file_name)
file_name = directory.get_next()
# Remove current path
directory.remove(path)
static func check_for_hooked_script(script_paths: Array[String], mod_tool_store: ModToolStore) -> int:
var count := 0
for script_path in script_paths:
if mod_tool_store.hooked_scripts.has(script_path):
count += 1
return count
static func quote_string(string: String) -> String:
var settings: EditorSettings = EditorInterface.get_editor_settings()
if settings.get_setting("text_editor/completion/use_single_quotes"):
return "'%s'" % string
return "\"%s\"" % string
static func script_has_method(script_path: String, method: String) -> bool:
var script: Script = load(script_path)
for script_method in script.get_script_method_list():
if script_method.name == method:
return true
if method in script.source_code:
return true
return false
static func get_index_at_method_end(method_name: String, text: String) -> int:
var starting_index := text.rfind(method_name)
# Find the end of the method
var next_method_line_index := text.find("func ", starting_index)
var method_end := -1
if next_method_line_index == -1:
# Backtrack empty lines from the end of the file
method_end = text.length() -1
else:
# Get the line before the next function line
method_end = text.rfind("\n", next_method_line_index)
# Backtrack to the last non-empty line
var last_non_empty_line_index := method_end
while last_non_empty_line_index > starting_index:
last_non_empty_line_index -= 1
# Remove spaces, tabs and newlines (whitespace) to check if the line really is empty
if text[last_non_empty_line_index].rstrip("\t\n "):
break # encountered a filled line
return last_non_empty_line_index +1
# Slightly modified version of:
# https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
# Removed .import from the extension filter.
# p_match is a string that filters the list of files.
# If p_match_is_regex is false, p_match is directly string-searched against the FILENAME.
# If it is true, a regex object compiles p_match and runs it against the FILEPATH.
static func get_flat_view_dict(
p_dir := "res://",
p_match := "",
p_match_file_extensions: Array[StringName] = [],
p_match_is_regex := false,
include_empty_dirs := false,
ignored_dirs: Array[StringName] = []
) -> PackedStringArray:
var data: PackedStringArray = []
var regex: RegEx
if p_match_is_regex:
regex = RegEx.new()
var _compile_error: int = regex.compile(p_match)
if not regex.is_valid():
return data
var dirs := [p_dir]
var first := true
while not dirs.is_empty():
var dir_name : String = dirs.back()
var dir := DirAccess.open(dir_name)
dirs.pop_back()
if dir_name.lstrip("res://").get_slice("/", 0) in ignored_dirs:
continue
if dir:
var _dirlist_error: int = dir.list_dir_begin()
var file_name := dir.get_next()
if include_empty_dirs and not dir_name == p_dir:
data.append(dir_name)
while file_name != "":
if not dir_name == "res://":
first = false
# ignore hidden, temporary, or system content
if not file_name.begins_with(".") and not file_name.get_extension() == "tmp":
# If a directory, then add to list of directories to visit
if dir.current_is_dir():
dirs.push_back(dir.get_current_dir() + "/" + file_name)
# If a file, check if we already have a record for the same name
else:
var path := dir.get_current_dir() + ("/" if not first else "") + file_name
# grab all
if not p_match and not p_match_file_extensions:
data.append(path)
# grab matching strings
elif not p_match_is_regex and p_match and file_name.contains(p_match):
data.append(path)
# garb matching file extension
elif p_match_file_extensions and file_name.get_extension() in p_match_file_extensions:
data.append(path)
# grab matching regex
elif p_match_is_regex:
var regex_match := regex.search(path)
if regex_match != null:
data.append(path)
# Move on to the next file in this directory
file_name = dir.get_next()
# We've exhausted all files in this directory. Close the iterator.
dir.list_dir_end()
return data