path.gd 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. class_name _ModLoaderPath
  2. extends RefCounted
  3. # This Class provides util functions for working with paths.
  4. # Currently all of the included functions are internal and should only be used by the mod loader itself.
  5. const LOG_NAME := "ModLoader:Path"
  6. const MOD_CONFIG_DIR_PATH := "user://mod_configs"
  7. const MOD_CONFIG_DIR_PATH_OLD := "user://configs"
  8. # Get the path to a local folder. Primarily used to get the (packed) mods
  9. # folder, ie "res://mods" or the OS's equivalent, as well as the configs path
  10. static func get_local_folder_dir(subfolder: String = "") -> String:
  11. return get_game_install_dir().path_join(subfolder)
  12. static func get_game_install_dir() -> String:
  13. var game_install_directory := OS.get_executable_path().get_base_dir()
  14. if OS.get_name() == "macOS":
  15. game_install_directory = game_install_directory.get_base_dir().get_base_dir()
  16. if game_install_directory.ends_with(".app"):
  17. game_install_directory = game_install_directory.get_base_dir()
  18. # Fix for running the game through the Godot editor (as the EXE path would be
  19. # the editor's own EXE, which won't have any mod ZIPs)
  20. # if OS.is_debug_build():
  21. if OS.has_feature("editor"):
  22. game_install_directory = "res://"
  23. return game_install_directory
  24. # Get the path where override.cfg will be stored.
  25. # Not the same as the local folder dir (for mac)
  26. static func get_override_path() -> String:
  27. var base_path := ""
  28. if OS.has_feature("editor"):
  29. base_path = ProjectSettings.globalize_path("res://")
  30. else:
  31. # this is technically different to res:// in macos, but we want the
  32. # executable dir anyway, so it is exactly what we need
  33. base_path = OS.get_executable_path().get_base_dir()
  34. return base_path.path_join("override.cfg")
  35. # Provide a path, get the file name at the end of the path
  36. static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String:
  37. var file_name := path.get_file()
  38. if make_lower_case:
  39. file_name = file_name.to_lower()
  40. if remove_extension:
  41. file_name = file_name.trim_suffix("." + file_name.get_extension())
  42. return file_name
  43. # Provide a zip_path to a workshop mod, returns the steam_workshop_id
  44. static func get_steam_workshop_id(zip_path: String) -> String:
  45. if not zip_path.contains("/Steam/steamapps/workshop/content"):
  46. return ""
  47. return zip_path.get_base_dir().split("/")[-1]
  48. # Get a flat array of all files in the target directory.
  49. # Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
  50. static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_regex := false) -> PackedStringArray:
  51. var data: PackedStringArray = []
  52. var regex: RegEx
  53. if p_match_is_regex:
  54. regex = RegEx.new()
  55. var _compile_error: int = regex.compile(p_match)
  56. if not regex.is_valid():
  57. return data
  58. var dirs := [p_dir]
  59. var first := true
  60. while not dirs.is_empty():
  61. var dir_name: String = dirs.back()
  62. dirs.pop_back()
  63. var dir := DirAccess.open(dir_name)
  64. if not dir == null:
  65. var _dirlist_error: int = dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
  66. var file_name := dir.get_next()
  67. while file_name != "":
  68. if not dir_name == "res://":
  69. first = false
  70. # ignore hidden, temporary, or system content
  71. if not file_name.begins_with(".") and not file_name.get_extension() in ["tmp", "import"]:
  72. # If a directory, then add to list of directories to visit
  73. if dir.current_is_dir():
  74. dirs.push_back(dir.get_current_dir().path_join(file_name))
  75. # If a file, check if we already have a record for the same name
  76. else:
  77. var path := dir.get_current_dir() + ("/" if not first else "") + file_name
  78. # grab all
  79. if not p_match:
  80. data.append(path)
  81. # grab matching strings
  82. elif not p_match_is_regex and file_name.find(p_match, 0) != -1:
  83. data.append(path)
  84. # grab matching regex
  85. else:
  86. var regex_match := regex.search(path)
  87. if regex_match != null:
  88. data.append(path)
  89. # Move on to the next file in this directory
  90. file_name = dir.get_next()
  91. # We've exhausted all files in this directory. Close the iterator.
  92. dir.list_dir_end()
  93. return data
  94. # Returns an array of file paths inside the src dir
  95. static func get_file_paths_in_dir(src_dir_path: String) -> Array:
  96. var file_paths := []
  97. var dir := DirAccess.open(src_dir_path)
  98. if dir == null:
  99. ModLoaderLog.error("Encountered an error (%s) when attempting to open a directory, with the path: %s" % [error_string(DirAccess.get_open_error()), src_dir_path], LOG_NAME)
  100. return file_paths
  101. dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
  102. var file_name := dir.get_next()
  103. while (file_name != ""):
  104. if not dir.current_is_dir():
  105. file_paths.push_back(src_dir_path.path_join(file_name))
  106. file_name = dir.get_next()
  107. return file_paths
  108. # Returns an array of directory paths inside the src dir
  109. static func get_dir_paths_in_dir(src_dir_path: String) -> Array:
  110. var dir_paths := []
  111. var dir := DirAccess.open(src_dir_path)
  112. if dir == null:
  113. ModLoaderLog.error("Encountered an error (%s) when attempting to open a directory, with the path: %s" % [error_string(DirAccess.get_open_error()), src_dir_path], LOG_NAME)
  114. return dir_paths
  115. dir.list_dir_begin()
  116. var file_name := dir.get_next()
  117. while (file_name != ""):
  118. if file_name == "." or file_name == "..":
  119. file_name = dir.get_next()
  120. continue
  121. if dir.current_is_dir():
  122. dir_paths.push_back(src_dir_path.path_join(file_name))
  123. file_name = dir.get_next()
  124. return dir_paths
  125. # Get the path to the mods folder, with any applicable overrides applied
  126. static func get_path_to_mods() -> String:
  127. var mods_folder_path := get_local_folder_dir("mods")
  128. if ModLoaderStore:
  129. if ModLoaderStore.ml_options.override_path_to_mods:
  130. mods_folder_path = ModLoaderStore.ml_options.override_path_to_mods
  131. return mods_folder_path
  132. # Finds the global paths to all zips in provided directory
  133. static func get_zip_paths_in(folder_path: String) -> Array[String]:
  134. var zip_paths: Array[String] = []
  135. var files := Array(DirAccess.get_files_at(folder_path))\
  136. .filter(
  137. func(file_name: String):
  138. return is_zip(file_name)
  139. ).map(
  140. func(file_name: String):
  141. return ProjectSettings.globalize_path(folder_path.path_join(file_name))
  142. )
  143. # only .assign()ing to a typed array lets us return Array[String] instead of just Array
  144. zip_paths.assign(files)
  145. return zip_paths
  146. static func get_mod_paths_from_all_sources() -> Array[String]:
  147. var mod_paths: Array[String] = []
  148. var mod_dirs := get_dir_paths_in_dir(get_unpacked_mods_dir_path())
  149. if ModLoaderStore.has_feature.editor or ModLoaderStore.ml_options.load_from_unpacked:
  150. mod_paths.append_array(mod_dirs)
  151. else:
  152. ModLoaderLog.info("Loading mods from \"res://mods-unpacked\" is disabled.", LOG_NAME)
  153. if ModLoaderStore.ml_options.load_from_local:
  154. var mods_dir := get_path_to_mods()
  155. if not DirAccess.dir_exists_absolute(mods_dir):
  156. ModLoaderLog.info("The directory for mods at path \"%s\" does not exist." % mods_dir, LOG_NAME)
  157. else:
  158. mod_paths.append_array(get_zip_paths_in(mods_dir))
  159. if ModLoaderStore.ml_options.load_from_steam_workshop:
  160. mod_paths.append_array(_ModLoaderSteam.find_steam_workshop_zips())
  161. return mod_paths
  162. static func get_path_to_mod_manifest(mod_id: String) -> String:
  163. return get_path_to_mods().path_join(mod_id).path_join("manifest.json")
  164. static func get_unpacked_mods_dir_path() -> String:
  165. return ModLoaderStore.UNPACKED_DIR
  166. # Get the path to the configs folder, with any applicable overrides applied
  167. static func get_path_to_configs() -> String:
  168. if _ModLoaderFile.dir_exists(MOD_CONFIG_DIR_PATH_OLD):
  169. handle_mod_config_path_deprecation()
  170. var configs_path := MOD_CONFIG_DIR_PATH
  171. if ModLoaderStore:
  172. if ModLoaderStore.ml_options.override_path_to_configs:
  173. configs_path = ModLoaderStore.ml_options.override_path_to_configs
  174. return configs_path
  175. # Get the path to a mods config folder
  176. static func get_path_to_mod_configs_dir(mod_id: String) -> String:
  177. return get_path_to_configs().path_join(mod_id)
  178. # Get the path to a mods config file
  179. static func get_path_to_mod_config_file(mod_id: String, config_name: String) -> String:
  180. var mod_config_dir := get_path_to_mod_configs_dir(mod_id)
  181. return mod_config_dir.path_join(config_name + ".json")
  182. # Get the path to the zip file that contains the vanilla scripts with
  183. # added mod hooks, considering all overrides
  184. static func get_path_to_hook_pack() -> String:
  185. var path := get_game_install_dir()
  186. if not ModLoaderStore.ml_options.override_path_to_hook_pack.is_empty():
  187. path = ModLoaderStore.ml_options.override_path_to_hook_pack
  188. var name := ModLoaderStore.MOD_HOOK_PACK_NAME
  189. if not ModLoaderStore.ml_options.override_hook_pack_name.is_empty():
  190. name = ModLoaderStore.ml_options.override_hook_pack_name
  191. return path.path_join(name)
  192. # Returns the mod directory name ("some-mod") from a given path (e.g. "res://mods-unpacked/some-mod/extensions/extension.gd")
  193. static func get_mod_dir(path: String) -> String:
  194. var initial := ModLoaderStore.UNPACKED_DIR
  195. var ending := "/"
  196. var start_index: int = path.find(initial)
  197. if start_index == -1:
  198. ModLoaderLog.error("Initial string not found.", LOG_NAME)
  199. return ""
  200. start_index += initial.length()
  201. var end_index: int = path.find(ending, start_index)
  202. if end_index == -1:
  203. ModLoaderLog.error("Ending string not found.", LOG_NAME)
  204. return ""
  205. var found_string: String = path.substr(start_index, end_index - start_index)
  206. return found_string
  207. # Checks if the path ends with .zip
  208. static func is_zip(path: String) -> bool:
  209. return path.get_extension() == "zip"
  210. static func handle_mod_config_path_deprecation() -> void:
  211. ModLoaderDeprecated.deprecated_message("The mod config path has been moved to \"%s\".
  212. The Mod Loader will attempt to rename the config directory." % MOD_CONFIG_DIR_PATH, "7.0.0")
  213. var error := DirAccess.rename_absolute(MOD_CONFIG_DIR_PATH_OLD, MOD_CONFIG_DIR_PATH)
  214. if not error == OK:
  215. ModLoaderLog.error("Failed to rename the config directory with error \"%s\"." % [error_string(error)], LOG_NAME)
  216. else:
  217. ModLoaderLog.success("Successfully renamed config directory to \"%s\"." % MOD_CONFIG_DIR_PATH, LOG_NAME)