setup_utils.gd 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. class_name ModLoaderSetupUtils
  2. # Slimed down version of ModLoaderUtils for the ModLoader Self Setup
  3. const LOG_NAME := "ModLoader:SetupUtils"
  4. static var ModLoaderSetupLog: Object = load("res://addons/mod_loader/setup/setup_log.gd")
  5. # Get the path to a local folder. Primarily used to get the (packed) mods
  6. # folder, ie "res://mods" or the OS's equivalent, as well as the configs path
  7. static func get_local_folder_dir(subfolder: String = "") -> String:
  8. var game_install_directory := OS.get_executable_path().get_base_dir()
  9. if OS.get_name() == "macOS":
  10. game_install_directory = game_install_directory.get_base_dir().get_base_dir()
  11. # Fix for running the game through the Godot editor (as the EXE path would be
  12. # the editor's own EXE, which won't have any mod ZIPs)
  13. # if OS.is_debug_build():
  14. if OS.has_feature("editor"):
  15. game_install_directory = "res://"
  16. return game_install_directory.path_join(subfolder)
  17. # Provide a path, get the file name at the end of the path
  18. static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String:
  19. var file_name := path.get_file()
  20. if make_lower_case:
  21. file_name = file_name.to_lower()
  22. if remove_extension:
  23. file_name = file_name.trim_suffix("." + file_name.get_extension())
  24. return file_name
  25. # Get an array of all autoloads -> ["autoload/AutoloadName", ...]
  26. static func get_autoload_array() -> Array:
  27. var autoloads := []
  28. # Get all autoload settings
  29. for prop in ProjectSettings.get_property_list():
  30. var name: String = prop.name
  31. if name.begins_with("autoload/"):
  32. autoloads.append(name.trim_prefix("autoload/"))
  33. return autoloads
  34. # Get the index of a specific autoload
  35. static func get_autoload_index(autoload_name: String) -> int:
  36. var autoloads := get_autoload_array()
  37. var autoload_index := autoloads.find(autoload_name)
  38. return autoload_index
  39. # Get the path where override.cfg will be stored.
  40. # Not the same as the local folder dir (for mac)
  41. static func get_override_path() -> String:
  42. var base_path := ""
  43. if OS.has_feature("editor"):
  44. base_path = ProjectSettings.globalize_path("res://")
  45. else:
  46. # this is technically different to res:// in macos, but we want the
  47. # executable dir anyway, so it is exactly what we need
  48. base_path = OS.get_executable_path().get_base_dir()
  49. return base_path.path_join("override.cfg")
  50. # Register an array of classes to the global scope, since Godot only does that in the editor.
  51. static func register_global_classes_from_array(new_global_classes: Array) -> void:
  52. var registered_classes: Array = ProjectSettings.get_setting("_global_script_classes")
  53. var registered_class_icons: Dictionary = ProjectSettings.get_setting("_global_script_class_icons")
  54. for new_class in new_global_classes:
  55. if not _is_valid_global_class_dict(new_class):
  56. continue
  57. for old_class in registered_classes:
  58. if old_class.class == new_class.class:
  59. if OS.has_feature("editor"):
  60. ModLoaderSetupLog.info('Class "%s" to be registered as global was already registered by the editor. Skipping.' % new_class.class, LOG_NAME)
  61. else:
  62. ModLoaderSetupLog.info('Class "%s" to be registered as global already exists. Skipping.' % new_class.class, LOG_NAME)
  63. continue
  64. registered_classes.append(new_class)
  65. registered_class_icons[new_class.class] = "" # empty icon, does not matter
  66. ProjectSettings.set_setting("_global_script_classes", registered_classes)
  67. ProjectSettings.set_setting("_global_script_class_icons", registered_class_icons)
  68. # Checks if all required fields are in the given [Dictionary]
  69. # Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" }
  70. static func _is_valid_global_class_dict(global_class_dict: Dictionary) -> bool:
  71. var required_fields := ["base", "class", "language", "path"]
  72. if not global_class_dict.has_all(required_fields):
  73. ModLoaderSetupLog.fatal("Global class to be registered is missing one of %s" % required_fields, LOG_NAME)
  74. return false
  75. if not FileAccess.file_exists(global_class_dict.path):
  76. ModLoaderSetupLog.fatal('Class "%s" to be registered as global could not be found at given path "%s"' %
  77. [global_class_dict.class, global_class_dict.path], LOG_NAME)
  78. return false
  79. return true
  80. # Check if the provided command line argument was present when launching the game
  81. static func is_running_with_command_line_arg(argument: String) -> bool:
  82. for arg in OS.get_cmdline_args():
  83. if argument == arg.split("=")[0]:
  84. return true
  85. return false
  86. # Get the command line argument value if present when launching the game
  87. static func get_cmd_line_arg_value(argument: String) -> String:
  88. var args := _get_fixed_cmdline_args()
  89. for arg_index in args.size():
  90. var arg := args[arg_index] as String
  91. var key := arg.split("=")[0]
  92. if key == argument:
  93. # format: `--arg=value` or `--arg="value"`
  94. if "=" in arg:
  95. var value := arg.trim_prefix(argument + "=")
  96. value = value.trim_prefix('"').trim_suffix('"')
  97. value = value.trim_prefix("'").trim_suffix("'")
  98. return value
  99. # format: `--arg value` or `--arg "value"`
  100. elif arg_index +1 < args.size() and not args[arg_index +1].begins_with("--"):
  101. return args[arg_index + 1]
  102. return ""
  103. static func _get_fixed_cmdline_args() -> PackedStringArray:
  104. return fix_godot_cmdline_args_string_space_splitting(OS.get_cmdline_args())
  105. # Reverses a bug in Godot, which splits input strings at spaces even if they are quoted
  106. # e.g. `--arg="some value" --arg-two 'more value'` becomes `[ --arg="some, value", --arg-two, 'more, value' ]`
  107. static func fix_godot_cmdline_args_string_space_splitting(args: PackedStringArray) -> PackedStringArray:
  108. if not OS.has_feature("editor"): # only happens in editor builds
  109. return args
  110. if OS.has_feature("windows"): # windows is unaffected
  111. return args
  112. var fixed_args := PackedStringArray([])
  113. var fixed_arg := ""
  114. # if we encounter an argument that contains `=` followed by a quote,
  115. # or an argument that starts with a quote, take all following args and
  116. # concatenate them into one, until we find the closing quote
  117. for arg in args:
  118. var arg_string := arg as String
  119. if '="' in arg_string or '="' in fixed_arg or \
  120. arg_string.begins_with('"') or fixed_arg.begins_with('"'):
  121. if not fixed_arg == "":
  122. fixed_arg += " "
  123. fixed_arg += arg_string
  124. if arg_string.ends_with('"'):
  125. fixed_args.append(fixed_arg.trim_prefix(" "))
  126. fixed_arg = ""
  127. continue
  128. # same thing for single quotes
  129. elif "='" in arg_string or "='" in fixed_arg \
  130. or arg_string.begins_with("'") or fixed_arg.begins_with("'"):
  131. if not fixed_arg == "":
  132. fixed_arg += " "
  133. fixed_arg += arg_string
  134. if arg_string.ends_with("'"):
  135. fixed_args.append(fixed_arg.trim_prefix(" "))
  136. fixed_arg = ""
  137. continue
  138. else:
  139. fixed_args.append(arg_string)
  140. return fixed_args
  141. # Slightly modified version of:
  142. # https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
  143. # Removed .import from the extension filter.
  144. # p_match is a string that filters the list of files.
  145. # If p_match_is_regex is false, p_match is directly string-searched against the FILENAME.
  146. # If it is true, a regex object compiles p_match and runs it against the FILEPATH.
  147. static func get_flat_view_dict(
  148. p_dir := "res://",
  149. p_match := "",
  150. p_match_file_extensions: Array[StringName] = [],
  151. p_match_is_regex := false,
  152. include_empty_dirs := false,
  153. ignored_dirs: Array[StringName] = []
  154. ) -> PackedStringArray:
  155. var data: PackedStringArray = []
  156. var regex: RegEx
  157. if p_match_is_regex:
  158. regex = RegEx.new()
  159. var _compile_error: int = regex.compile(p_match)
  160. if not regex.is_valid():
  161. return data
  162. var dirs := [p_dir]
  163. var first := true
  164. while not dirs.is_empty():
  165. var dir_name : String = dirs.back()
  166. var dir := DirAccess.open(dir_name)
  167. dirs.pop_back()
  168. if dir_name.lstrip("res://").get_slice("/", 0) in ignored_dirs:
  169. continue
  170. if dir:
  171. var _dirlist_error: int = dir.list_dir_begin()
  172. var file_name := dir.get_next()
  173. if include_empty_dirs and not dir_name == p_dir:
  174. data.append(dir_name)
  175. while file_name != "":
  176. if not dir_name == "res://":
  177. first = false
  178. # ignore hidden, temporary, or system content
  179. if not file_name.begins_with(".") and not file_name.get_extension() == "tmp":
  180. # If a directory, then add to list of directories to visit
  181. if dir.current_is_dir():
  182. dirs.push_back(dir.get_current_dir() + "/" + file_name)
  183. # If a file, check if we already have a record for the same name
  184. else:
  185. var path := dir.get_current_dir() + ("/" if not first else "") + file_name
  186. # grab all
  187. if not p_match and not p_match_file_extensions:
  188. data.append(path)
  189. # grab matching strings
  190. elif not p_match_is_regex and p_match and file_name.contains(p_match):
  191. data.append(path)
  192. # garb matching file extension
  193. elif p_match_file_extensions and file_name.get_extension() in p_match_file_extensions:
  194. data.append(path)
  195. # grab matching regex
  196. elif p_match_is_regex:
  197. var regex_match := regex.search(path)
  198. if regex_match != null:
  199. data.append(path)
  200. # Move on to the next file in this directory
  201. file_name = dir.get_next()
  202. # We've exhausted all files in this directory. Close the iterator.
  203. dir.list_dir_end()
  204. return data
  205. static func copy_file(from: String, to: String) -> void:
  206. ModLoaderSetupLog.debug("Copy file from: \"%s\" to: \"%s\"" % [from, to], LOG_NAME)
  207. var global_to_path := ProjectSettings.globalize_path(to.get_base_dir())
  208. if not DirAccess.dir_exists_absolute(global_to_path):
  209. ModLoaderSetupLog.debug("Creating dir \"%s\"" % global_to_path, LOG_NAME)
  210. DirAccess.make_dir_recursive_absolute(global_to_path)
  211. var file_from := FileAccess.open(from, FileAccess.READ)
  212. var file_from_error := file_from.get_error()
  213. if not file_from_error == OK:
  214. ModLoaderSetupLog.error("Error accessing file \"%s\": %s" % [from, error_string(file_from_error)], LOG_NAME)
  215. return
  216. var file_from_content := file_from.get_buffer(file_from.get_length())
  217. var file_to := FileAccess.open(to, FileAccess.WRITE)
  218. var file_to_error := file_to.get_error()
  219. if not file_to_error == OK:
  220. ModLoaderSetupLog.error("Error writing file \"%s\": %s" % [to, error_string(file_to_error)], LOG_NAME)
  221. return
  222. file_to.store_buffer(file_from_content)