From ee7312a297236ef2c795859eb59f799795da954e Mon Sep 17 00:00:00 2001 From: KANAjetzt <41547570+KANAjetzt@users.noreply.github.com> Date: Tue, 27 Jun 2023 11:00:30 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20reworked=20mod?= =?UTF-8?q?=20uninstall=20detection=20(#298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: :sparkles: added ` get_dir_paths_in_dir()` * feat: :sparkles: added `zip_name` and `zip_path` to `ModData` * refactor: :recycle: added `zip_path` to user profiles This allows to verify if the mod is still installed by confirming the existence of the zip file. However, this check is only performed when the mod is not loaded and a path to the zip file exists. This ensures that mods are not deleted from the profile when running in the editor. It's important to note that this new check may cause mods to appear in user profiles even if they are currently not loaded. To determine if a mod is actually loaded, you should also check `ModLoaderStore.mod_data` or use `ModLoaderMod.is_mod_loaded()`. closes #288 --- addons/mod_loader/api/profile.gd | 28 ++++++++++ addons/mod_loader/internal/file.gd | 37 +++++++++---- addons/mod_loader/internal/path.gd | 24 +++++++++ .../mod_loader/internal/third_party/steam.gd | 12 ++--- addons/mod_loader/mod_loader.gd | 52 ++++++++++++------- addons/mod_loader/mod_loader_store.gd | 3 ++ addons/mod_loader/resources/mod_data.gd | 8 +-- 7 files changed, 124 insertions(+), 40 deletions(-) diff --git a/addons/mod_loader/api/profile.gd b/addons/mod_loader/api/profile.gd index 96c114e1..79fa993e 100644 --- a/addons/mod_loader/api/profile.gd +++ b/addons/mod_loader/api/profile.gd @@ -270,6 +270,28 @@ static func _update_mod_list(mod_list: Dictionary, mod_data := ModLoaderStore.mo # If the current config doesn't exist, reset it to the default configuration mod_list_entry.current_config = ModLoaderConfig.DEFAULT_CONFIG_NAME + # If the mod is not loaded + if not mod_data.has(mod_id): + if ( + # Check if the entry has a zip_path key + mod_list_entry.has("zip_path") and + # Check if the entry has a zip_path + not mod_list_entry.zip_path.empty() and + # Check if the zip file for the mod exists + not _ModLoaderFile.file_exists(mod_list_entry.zip_path) + ): + # If the mod directory doesn't exist, + # the mod is no longer installed and can be removed from the mod list + ModLoaderLog.debug( + "Mod \"%s\" has been deleted from all user profiles as the corresponding zip file no longer exists at path \"%s\"." + % [mod_id, mod_list_entry.zip_path], + LOG_NAME, + true + ) + + updated_mod_list.erase(mod_id) + continue + updated_mod_list[mod_id] = mod_list_entry return updated_mod_list @@ -295,7 +317,13 @@ static func _generate_mod_list() -> Dictionary: static func _generate_mod_list_entry(mod_id: String, is_active: bool) -> Dictionary: var mod_list_entry := {} + # Set the mods active state mod_list_entry.is_active = is_active + + # Set the mods zip path if available + if ModLoaderStore.mod_data.has(mod_id): + mod_list_entry.zip_path = ModLoaderStore.mod_data[mod_id].zip_path + # Set the current_config if the mod has a config schema and is active if is_active and not ModLoaderConfig.get_config_schema(mod_id).empty(): var current_config: ModConfig = ModLoaderStore.mod_data[mod_id].current_config diff --git a/addons/mod_loader/internal/file.gd b/addons/mod_loader/internal/file.gd index 2e956f06..a7287392 100644 --- a/addons/mod_loader/internal/file.gd +++ b/addons/mod_loader/internal/file.gd @@ -44,18 +44,20 @@ static func _get_json_string_as_dict(string: String) -> Dictionary: # Load the mod ZIP from the provided directory -static func load_zips_in_folder(folder_path: String) -> int: - var temp_zipped_mods_count := 0 +static func load_zips_in_folder(folder_path: String) -> Dictionary: + var zip_data := {} 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 + return {} 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 + return {} + + # Get all zip folders inside the game mod folder while true: @@ -76,9 +78,25 @@ static func load_zips_in_folder(folder_path: String) -> int: # 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) + var mod_zip_path := folder_path.plus_file(mod_zip_file_name) + var mod_zip_global_path := ProjectSettings.globalize_path(mod_zip_path) + var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_zip_global_path, false) + + # Get the current directories inside UNPACKED_DIR + # This array is used to determine which directory is new + var current_mod_dirs := _ModLoaderPath.get_dir_paths_in_dir(_ModLoaderPath.get_unpacked_mods_dir_path()) + # Create a backup to reference when the next mod is loaded + var current_mod_dirs_backup := current_mod_dirs.duplicate() + + # Remove all directory paths that existed before, leaving only the one added last + for previous_mod_dir in ModLoaderStore.previous_mod_dirs: + current_mod_dirs.erase(previous_mod_dir) + + # The key is the mod_id of the latest loaded mod, and the value is the path to the zip file + zip_data[current_mod_dirs[0].get_slice("/", 3)] = mod_zip_global_path + + # Update previous_mod_dirs in ModLoaderStore to use for the next mod + ModLoaderStore.previous_mod_dirs = current_mod_dirs_backup # 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 @@ -94,7 +112,7 @@ static func load_zips_in_folder(folder_path: String) -> int: "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) + ModLoaderLog.debug("Found mod ZIP: %s" % mod_zip_global_path, LOG_NAME) # If there was an error loading the mod zip file if not is_mod_loaded_successfully: @@ -104,11 +122,10 @@ static func load_zips_in_folder(folder_path: String) -> int: # 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 + return zip_data # Save Data diff --git a/addons/mod_loader/internal/path.gd b/addons/mod_loader/internal/path.gd index db542e41..6bccfa5b 100644 --- a/addons/mod_loader/internal/path.gd +++ b/addons/mod_loader/internal/path.gd @@ -126,6 +126,30 @@ static func get_file_paths_in_dir(src_dir_path: String) -> Array: return file_paths +# Returns an array of directory paths inside the src dir +static func get_dir_paths_in_dir(src_dir_path: String) -> Array: + var dir_paths := [] + + var directory := Directory.new() + var error := directory.open(src_dir_path) + + if not error == OK: + return dir_paths + ModLoaderLog.error("Error opening directory", LOG_NAME) + + directory.list_dir_begin() + var file_name := directory.get_next() + while (file_name != ""): + if file_name == "." or file_name == "..": + file_name = directory.get_next() + continue + if directory.current_is_dir(): + dir_paths.push_back(src_dir_path.plus_file(file_name)) + file_name = directory.get_next() + + return dir_paths + + # Get the path to the mods folder, with any applicable overrides applied static func get_path_to_mods() -> String: var mods_folder_path := get_local_folder_dir("mods") diff --git a/addons/mod_loader/internal/third_party/steam.gd b/addons/mod_loader/internal/third_party/steam.gd index b05fc8fd..e4371896 100644 --- a/addons/mod_loader/internal/third_party/steam.gd +++ b/addons/mod_loader/internal/third_party/steam.gd @@ -9,8 +9,8 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam" # 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 +static func load_steam_workshop_zips() -> Dictionary: + var zip_data := {} var workshop_folder_path := _get_path_to_workshop() ModLoaderLog.info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME) @@ -19,11 +19,11 @@ static func load_steam_workshop_zips() -> int: 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 + return {} 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 + return {} # Loop 1: Workshop folders while true: @@ -42,11 +42,11 @@ static func load_steam_workshop_zips() -> int: continue # Loop 2: ZIPs inside the workshop folders - temp_zipped_mods_count += _ModLoaderFile.load_zips_in_folder(ProjectSettings.globalize_path(item_path)) + zip_data.merge(_ModLoaderFile.load_zips_in_folder(ProjectSettings.globalize_path(item_path))) workshop_dir.list_dir_end() - return temp_zipped_mods_count + return zip_data # Get the path to the Steam workshop folder. Only works for Steam games, as it diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index e2a3040b..7a1b15b9 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -85,11 +85,20 @@ func _exit_tree() -> void: func _load_mods() -> void: # Loop over "res://mods" and add any mod zips to the unpacked virtual # directory (UNPACKED_DIR) - var unzipped_mods := _load_mod_zips() - if unzipped_mods > 0: - ModLoaderLog.success("DONE: Loaded %s mod files into the virtual filesystem" % unzipped_mods, LOG_NAME) - else: + var zip_data := _load_mod_zips() + + if zip_data.empty(): ModLoaderLog.info("No zipped mods found", LOG_NAME) + else: + ModLoaderLog.success("DONE: Loaded %s mod files into the virtual filesystem" % zip_data.size(), LOG_NAME) + + # Initializes the mod_data dictionary if zipped mods are loaded. + # If mods are unpacked in the "mods-unpacked" directory, + # mod_data is initialized in the _setup_mods() function. + for mod_id in zip_data.keys(): + var zip_path: String = zip_data[mod_id] + _init_mod_data(mod_id, zip_path) + # Loop over UNPACKED_DIR. This triggers _init_mod_data for each mod # directory, which adds their data to mod_data. @@ -219,19 +228,21 @@ func _check_autoload_positions() -> void: # Loop over "res://mods" and add any mod zips to the unpacked virtual directory # (UNPACKED_DIR) -func _load_mod_zips() -> int: - var zipped_mods_count := 0 +func _load_mod_zips() -> Dictionary: + var zip_data := {} if not ModLoaderStore.ml_options.steam_workshop_enabled: 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 += _ModLoaderFile.load_zips_in_folder(mods_folder_path) + var loaded_zip_data := _ModLoaderFile.load_zips_in_folder(mods_folder_path) + zip_data.merge(loaded_zip_data) else: # If we're using Steam workshop, loop over the workshop item directories - zipped_mods_count += _ModLoaderSteam.load_steam_workshop_zips() + var loaded_workshop_zip_data := _ModLoaderSteam.load_steam_workshop_zips() + zip_data.merge(loaded_workshop_zip_data) - return zipped_mods_count + return zip_data # Loop over UNPACKED_DIR and triggers `_init_mod_data` for each mod directory, @@ -282,19 +293,20 @@ func _setup_mods() -> int: # Add a mod's data to mod_data. # The mod_folder_path is just the folder name that was added to UNPACKED_DIR, # which depends on the name used in a given mod ZIP (eg "mods-unpacked/Folder-Name") -func _init_mod_data(mod_folder_path: String) -> void: - # The file name should be a valid mod id - var dir_name := _ModLoaderPath.get_file_name_from_path(mod_folder_path, false, true) - - # Path to the mod in UNPACKED_DIR (eg "res://mods-unpacked/My-Mod") - var local_mod_path := _ModLoaderPath.get_unpacked_mods_dir_path().plus_file(dir_name) - - var mod := ModData.new(local_mod_path) - mod.dir_name = dir_name +func _init_mod_data(mod_id: String, zip_path := "") -> void: + # Path to the mod in UNPACKED_DIR (eg "res://mods-unpacked/My-Mod") + var local_mod_path := _ModLoaderPath.get_unpacked_mods_dir_path().plus_file(mod_id) + + var mod := ModData.new() + if not zip_path.empty(): + mod.zip_name = _ModLoaderPath.get_file_name_from_path(zip_path) + mod.zip_path = zip_path + mod.dir_path = local_mod_path + mod.dir_name = mod_id var mod_overwrites_path := mod.get_optional_mod_file_path(ModData.optional_mod_files.OVERWRITES) mod.is_overwrite = _ModLoaderFile.file_exists(mod_overwrites_path) - mod.is_locked = true if dir_name in ModLoaderStore.ml_options.locked_mods else false - ModLoaderStore.mod_data[dir_name] = mod + mod.is_locked = true if mod_id in ModLoaderStore.ml_options.locked_mods else false + ModLoaderStore.mod_data[mod_id] = mod # Get the mod file paths # Note: This was needed in the original version of this script, but it's diff --git a/addons/mod_loader/mod_loader_store.gd b/addons/mod_loader/mod_loader_store.gd index 4c9c24f6..4c89d790 100644 --- a/addons/mod_loader/mod_loader_store.gd +++ b/addons/mod_loader/mod_loader_store.gd @@ -44,6 +44,9 @@ var mod_missing_dependencies := {} # Helps to decide whether a script extension should go through the _ModLoaderScriptExtension.handle_script_extensions() process var is_initializing := true +# Used when loading mod zips to determine which mod zip corresponds to which mod directory in the UNPACKED_DIR. +var previous_mod_dirs := [] + # Store all extenders paths var script_extensions := [] diff --git a/addons/mod_loader/resources/mod_data.gd b/addons/mod_loader/resources/mod_data.gd index a2a2dbe9..c7640e09 100644 --- a/addons/mod_loader/resources/mod_data.gd +++ b/addons/mod_loader/resources/mod_data.gd @@ -23,6 +23,10 @@ enum optional_mod_files { OVERWRITES } +# Name of the Mod's zip file +var zip_name := "" +# Path to the Mod's zip file +var zip_path := "" # Directory of the mod. Has to be identical to [method ModManifest.get_mod_id] var dir_name := "" # Path to the Mod's Directory @@ -47,10 +51,6 @@ var current_config: ModConfig setget _set_current_config var file_paths: PoolStringArray = [] -func _init(_dir_path: String) -> void: - dir_path = _dir_path - - # Load meta data from a mod's manifest.json file func load_manifest() -> void: if not _has_required_files():