diff --git a/addons/mod_loader/internal/file.gd b/addons/mod_loader/internal/file.gd index 9d95d091..2e956f06 100644 --- a/addons/mod_loader/internal/file.gd +++ b/addons/mod_loader/internal/file.gd @@ -43,6 +43,74 @@ static func _get_json_string_as_dict(string: String) -> Dictionary: return parsed.result +# Load the mod ZIP from the provided directory +static func load_zips_in_folder(folder_path: String) -> int: + var temp_zipped_mods_count := 0 + + var mod_dir := Directory.new() + var mod_dir_open_error := mod_dir.open(folder_path) + if not mod_dir_open_error == OK: + ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME) + return -1 + var mod_dir_listdir_error := mod_dir.list_dir_begin() + if not mod_dir_listdir_error == OK: + ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME) + return -1 + + # Get all zip folders inside the game mod folder + while true: + # Get the next file in the directory + var mod_zip_file_name := mod_dir.get_next() + + # If there is no more file + if mod_zip_file_name == "": + # Stop loading mod zip files + break + + # Ignore files that aren't ZIP or PCK + if not mod_zip_file_name.get_extension() == "zip" and not mod_zip_file_name.get_extension() == "pck": + continue + + # If the current file is a directory + if mod_dir.current_is_dir(): + # Go to the next file + continue + + var mod_folder_path := folder_path.plus_file(mod_zip_file_name) + var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path) + var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false) + + # Notifies developer of an issue with Godot, where using `load_resource_pack` + # in the editor WIPES the entire virtual res:// directory the first time you + # use it. This means that unpacked mods are no longer accessible, because they + # no longer exist in the file system. So this warning basically says + # "don't use ZIPs with unpacked mods!" + # https://github.com/godotengine/godot/issues/19815 + # https://github.com/godotengine/godot/issues/16798 + if OS.has_feature("editor") and not ModLoaderStore.has_shown_editor_zips_warning: + ModLoaderLog.warning(str( + "Loading any resource packs (.zip/.pck) with `load_resource_pack` will WIPE the entire virtual res:// directory. ", + "If you have any unpacked mods in ", _ModLoaderPath.get_unpacked_mods_dir_path(), ", they will not be loaded. ", + "Please unpack your mod ZIPs instead, and add them to ", _ModLoaderPath.get_unpacked_mods_dir_path()), LOG_NAME) + ModLoaderStore.has_shown_editor_zips_warning = true + + ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME) + + # If there was an error loading the mod zip file + if not is_mod_loaded_successfully: + # Log the error and continue with the next file + ModLoaderLog.error("%s failed to load." % mod_zip_file_name, LOG_NAME) + continue + + # Mod successfully loaded! + ModLoaderLog.success("%s loaded." % mod_zip_file_name, LOG_NAME) + temp_zipped_mods_count += 1 + + mod_dir.list_dir_end() + + return temp_zipped_mods_count + + # Save Data # ============================================================================= diff --git a/addons/mod_loader/internal/path.gd b/addons/mod_loader/internal/path.gd index face3b43..d5fbe194 100644 --- a/addons/mod_loader/internal/path.gd +++ b/addons/mod_loader/internal/path.gd @@ -153,7 +153,8 @@ static func get_path_to_configs() -> String: static func get_path_to_mod_configs_dir(mod_id: String) -> String: var mod_config_dir := get_path_to_configs().plus_file(mod_id) - if not _ModLoaderFile.dir_exists(mod_config_dir): + # Can't use _ModLoaderFile because of cyclic dependency. + if not Directory.new().dir_exists(mod_config_dir): return "" return mod_config_dir @@ -169,7 +170,8 @@ static func get_path_to_mod_config_file(mod_id: String, config_name: String) -> var mod_config_file_dir := mod_config_dir.plus_file( config_name + ".json") - if not _ModLoaderFile.file_exists(mod_config_file_dir): + # Can't use _ModLoaderFile because of cyclic dependency. + if not Directory.new().file_exists(mod_config_file_dir): return "" return mod_config_file_dir diff --git a/addons/mod_loader/internal/third_party/steam.gd b/addons/mod_loader/internal/third_party/steam.gd index 594e16f9..49fdf14b 100644 --- a/addons/mod_loader/internal/third_party/steam.gd +++ b/addons/mod_loader/internal/third_party/steam.gd @@ -6,6 +6,49 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam" # Methods related to Steam and the Steam Workshop +# Load mod ZIPs from Steam workshop folders. Uses 2 loops: One for each +# workshop item's folder, with another inside that which loops over the ZIPs +# inside each workshop item's folder +static func load_steam_workshop_zips() -> int: + var temp_zipped_mods_count := 0 + var workshop_folder_path := _get_path_to_workshop() + + ModLoaderLog.info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME) + + var workshop_dir := Directory.new() + var workshop_dir_open_error := workshop_dir.open(workshop_folder_path) + if not workshop_dir_open_error == OK: + ModLoaderLog.error("Can't open workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_open_error], LOG_NAME) + return -1 + var workshop_dir_listdir_error := workshop_dir.list_dir_begin() + if not workshop_dir_listdir_error == OK: + ModLoaderLog.error("Can't read workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_listdir_error], LOG_NAME) + return -1 + + # Loop 1: Workshop folders + while true: + # Get the next workshop item folder + var item_dir := workshop_dir.get_next() + var item_path := workshop_dir.get_current_dir() + "/" + item_dir + + ModLoaderLog.info("Checking workshop item path: \"%s\"" % item_path, LOG_NAME) + + # Stop loading mods when there's no more folders + if item_dir == '': + break + + # Only check directories + if not workshop_dir.current_is_dir(): + continue + + # Loop 2: ZIPs inside the workshop folders + temp_zipped_mods_count += _ModLoaderFile.load_zips_in_folder(ProjectSettings.globalize_path(item_path)) + + workshop_dir.list_dir_end() + + return temp_zipped_mods_count + + # Get the path to the Steam workshop folder. Only works for Steam games, as it # traverses directories relative to where a Steam game and its workshop content # would be installed. Based on code by Blobfish (developer of Brotato). @@ -15,7 +58,7 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam" # Eg. Brotato: # GAME = Steam/steamapps/common/Brotato # WORKSHOP = Steam/steamapps/workshop/content/1942280 -static func get_path_to_workshop() -> String: +static func _get_path_to_workshop() -> String: if ModLoaderStore.ml_options.override_path_to_workshop: return ModLoaderStore.ml_options.override_path_to_workshop diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index 4e5dfac3..d03e24a8 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -209,125 +209,14 @@ func _load_mod_zips() -> int: var mods_folder_path := _ModLoaderPath.get_path_to_mods() # If we're not using Steam workshop, just loop over the mod ZIPs. - zipped_mods_count += _load_zips_in_folder(mods_folder_path) + zipped_mods_count += _ModLoaderFile.load_zips_in_folder(mods_folder_path) else: # If we're using Steam workshop, loop over the workshop item directories - zipped_mods_count += _load_steam_workshop_zips() + zipped_mods_count += _ModLoaderSteam.load_steam_workshop_zips() return zipped_mods_count -# Load the mod ZIP from the provided directory -func _load_zips_in_folder(folder_path: String) -> int: - var temp_zipped_mods_count := 0 - - var mod_dir := Directory.new() - var mod_dir_open_error := mod_dir.open(folder_path) - if not mod_dir_open_error == OK: - ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME) - return -1 - var mod_dir_listdir_error := mod_dir.list_dir_begin() - if not mod_dir_listdir_error == OK: - ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME) - return -1 - - # Get all zip folders inside the game mod folder - while true: - # Get the next file in the directory - var mod_zip_file_name := mod_dir.get_next() - - # If there is no more file - if mod_zip_file_name == "": - # Stop loading mod zip files - break - - # Ignore files that aren't ZIP or PCK - if not mod_zip_file_name.get_extension() == "zip" and not mod_zip_file_name.get_extension() == "pck": - continue - - # If the current file is a directory - if mod_dir.current_is_dir(): - # Go to the next file - continue - - var mod_folder_path := folder_path.plus_file(mod_zip_file_name) - var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path) - var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false) - - # Notifies developer of an issue with Godot, where using `load_resource_pack` - # in the editor WIPES the entire virtual res:// directory the first time you - # use it. This means that unpacked mods are no longer accessible, because they - # no longer exist in the file system. So this warning basically says - # "don't use ZIPs with unpacked mods!" - # https://github.com/godotengine/godot/issues/19815 - # https://github.com/godotengine/godot/issues/16798 - if OS.has_feature("editor") and not ModLoaderStore.has_shown_editor_zips_warning: - ModLoaderLog.warning(str( - "Loading any resource packs (.zip/.pck) with `load_resource_pack` will WIPE the entire virtual res:// directory. ", - "If you have any unpacked mods in ", _ModLoaderPath.get_unpacked_mods_dir_path(), ", they will not be loaded. ", - "Please unpack your mod ZIPs instead, and add them to ", _ModLoaderPath.get_unpacked_mods_dir_path()), LOG_NAME) - ModLoaderStore.has_shown_editor_zips_warning = true - - ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME) - - # If there was an error loading the mod zip file - if not is_mod_loaded_successfully: - # Log the error and continue with the next file - ModLoaderLog.error("%s failed to load." % mod_zip_file_name, LOG_NAME) - continue - - # Mod successfully loaded! - ModLoaderLog.success("%s loaded." % mod_zip_file_name, LOG_NAME) - temp_zipped_mods_count += 1 - - mod_dir.list_dir_end() - - return temp_zipped_mods_count - - -# Load mod ZIPs from Steam workshop folders. Uses 2 loops: One for each -# workshop item's folder, with another inside that which loops over the ZIPs -# inside each workshop item's folder -func _load_steam_workshop_zips() -> int: - var temp_zipped_mods_count := 0 - var workshop_folder_path := _ModLoaderSteam.get_path_to_workshop() - - ModLoaderLog.info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME) - - var workshop_dir := Directory.new() - var workshop_dir_open_error := workshop_dir.open(workshop_folder_path) - if not workshop_dir_open_error == OK: - ModLoaderLog.error("Can't open workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_open_error], LOG_NAME) - return -1 - var workshop_dir_listdir_error := workshop_dir.list_dir_begin() - if not workshop_dir_listdir_error == OK: - ModLoaderLog.error("Can't read workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_listdir_error], LOG_NAME) - return -1 - - # Loop 1: Workshop folders - while true: - # Get the next workshop item folder - var item_dir := workshop_dir.get_next() - var item_path := workshop_dir.get_current_dir() + "/" + item_dir - - ModLoaderLog.info("Checking workshop item path: \"%s\"" % item_path, LOG_NAME) - - # Stop loading mods when there's no more folders - if item_dir == '': - break - - # Only check directories - if not workshop_dir.current_is_dir(): - continue - - # Loop 2: ZIPs inside the workshop folders - temp_zipped_mods_count += _load_zips_in_folder(ProjectSettings.globalize_path(item_path)) - - workshop_dir.list_dir_end() - - return temp_zipped_mods_count - - # Loop over UNPACKED_DIR and triggers `_init_mod_data` for each mod directory, # which adds their data to mod_data. func _setup_mods() -> int: