Skip to content

Commit ee7312a

Browse files
authored
refactor: ♻️ reworked mod uninstall detection (#298)
* feat: ✨ added ` get_dir_paths_in_dir()` * feat: ✨ added `zip_name` and `zip_path` to `ModData` * refactor: ♻️ 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
1 parent 21dea2a commit ee7312a

File tree

7 files changed

+124
-40
lines changed

7 files changed

+124
-40
lines changed

addons/mod_loader/api/profile.gd

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,28 @@ static func _update_mod_list(mod_list: Dictionary, mod_data := ModLoaderStore.mo
270270
# If the current config doesn't exist, reset it to the default configuration
271271
mod_list_entry.current_config = ModLoaderConfig.DEFAULT_CONFIG_NAME
272272

273+
# If the mod is not loaded
274+
if not mod_data.has(mod_id):
275+
if (
276+
# Check if the entry has a zip_path key
277+
mod_list_entry.has("zip_path") and
278+
# Check if the entry has a zip_path
279+
not mod_list_entry.zip_path.empty() and
280+
# Check if the zip file for the mod exists
281+
not _ModLoaderFile.file_exists(mod_list_entry.zip_path)
282+
):
283+
# If the mod directory doesn't exist,
284+
# the mod is no longer installed and can be removed from the mod list
285+
ModLoaderLog.debug(
286+
"Mod \"%s\" has been deleted from all user profiles as the corresponding zip file no longer exists at path \"%s\"."
287+
% [mod_id, mod_list_entry.zip_path],
288+
LOG_NAME,
289+
true
290+
)
291+
292+
updated_mod_list.erase(mod_id)
293+
continue
294+
273295
updated_mod_list[mod_id] = mod_list_entry
274296

275297
return updated_mod_list
@@ -295,7 +317,13 @@ static func _generate_mod_list() -> Dictionary:
295317
static func _generate_mod_list_entry(mod_id: String, is_active: bool) -> Dictionary:
296318
var mod_list_entry := {}
297319

320+
# Set the mods active state
298321
mod_list_entry.is_active = is_active
322+
323+
# Set the mods zip path if available
324+
if ModLoaderStore.mod_data.has(mod_id):
325+
mod_list_entry.zip_path = ModLoaderStore.mod_data[mod_id].zip_path
326+
299327
# Set the current_config if the mod has a config schema and is active
300328
if is_active and not ModLoaderConfig.get_config_schema(mod_id).empty():
301329
var current_config: ModConfig = ModLoaderStore.mod_data[mod_id].current_config

addons/mod_loader/internal/file.gd

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,20 @@ static func _get_json_string_as_dict(string: String) -> Dictionary:
4444

4545

4646
# Load the mod ZIP from the provided directory
47-
static func load_zips_in_folder(folder_path: String) -> int:
48-
var temp_zipped_mods_count := 0
47+
static func load_zips_in_folder(folder_path: String) -> Dictionary:
48+
var zip_data := {}
4949

5050
var mod_dir := Directory.new()
5151
var mod_dir_open_error := mod_dir.open(folder_path)
5252
if not mod_dir_open_error == OK:
5353
ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME)
54-
return -1
54+
return {}
5555
var mod_dir_listdir_error := mod_dir.list_dir_begin()
5656
if not mod_dir_listdir_error == OK:
5757
ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME)
58-
return -1
58+
return {}
59+
60+
5961

6062
# Get all zip folders inside the game mod folder
6163
while true:
@@ -76,9 +78,25 @@ static func load_zips_in_folder(folder_path: String) -> int:
7678
# Go to the next file
7779
continue
7880

79-
var mod_folder_path := folder_path.plus_file(mod_zip_file_name)
80-
var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path)
81-
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false)
81+
var mod_zip_path := folder_path.plus_file(mod_zip_file_name)
82+
var mod_zip_global_path := ProjectSettings.globalize_path(mod_zip_path)
83+
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_zip_global_path, false)
84+
85+
# Get the current directories inside UNPACKED_DIR
86+
# This array is used to determine which directory is new
87+
var current_mod_dirs := _ModLoaderPath.get_dir_paths_in_dir(_ModLoaderPath.get_unpacked_mods_dir_path())
88+
# Create a backup to reference when the next mod is loaded
89+
var current_mod_dirs_backup := current_mod_dirs.duplicate()
90+
91+
# Remove all directory paths that existed before, leaving only the one added last
92+
for previous_mod_dir in ModLoaderStore.previous_mod_dirs:
93+
current_mod_dirs.erase(previous_mod_dir)
94+
95+
# The key is the mod_id of the latest loaded mod, and the value is the path to the zip file
96+
zip_data[current_mod_dirs[0].get_slice("/", 3)] = mod_zip_global_path
97+
98+
# Update previous_mod_dirs in ModLoaderStore to use for the next mod
99+
ModLoaderStore.previous_mod_dirs = current_mod_dirs_backup
82100

83101
# Notifies developer of an issue with Godot, where using `load_resource_pack`
84102
# 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:
94112
"Please unpack your mod ZIPs instead, and add them to ", _ModLoaderPath.get_unpacked_mods_dir_path()), LOG_NAME)
95113
ModLoaderStore.has_shown_editor_zips_warning = true
96114

97-
ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME)
115+
ModLoaderLog.debug("Found mod ZIP: %s" % mod_zip_global_path, LOG_NAME)
98116

99117
# If there was an error loading the mod zip file
100118
if not is_mod_loaded_successfully:
@@ -104,11 +122,10 @@ static func load_zips_in_folder(folder_path: String) -> int:
104122

105123
# Mod successfully loaded!
106124
ModLoaderLog.success("%s loaded." % mod_zip_file_name, LOG_NAME)
107-
temp_zipped_mods_count += 1
108125

109126
mod_dir.list_dir_end()
110127

111-
return temp_zipped_mods_count
128+
return zip_data
112129

113130

114131
# Save Data

addons/mod_loader/internal/path.gd

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ static func get_file_paths_in_dir(src_dir_path: String) -> Array:
126126
return file_paths
127127

128128

129+
# Returns an array of directory paths inside the src dir
130+
static func get_dir_paths_in_dir(src_dir_path: String) -> Array:
131+
var dir_paths := []
132+
133+
var directory := Directory.new()
134+
var error := directory.open(src_dir_path)
135+
136+
if not error == OK:
137+
return dir_paths
138+
ModLoaderLog.error("Error opening directory", LOG_NAME)
139+
140+
directory.list_dir_begin()
141+
var file_name := directory.get_next()
142+
while (file_name != ""):
143+
if file_name == "." or file_name == "..":
144+
file_name = directory.get_next()
145+
continue
146+
if directory.current_is_dir():
147+
dir_paths.push_back(src_dir_path.plus_file(file_name))
148+
file_name = directory.get_next()
149+
150+
return dir_paths
151+
152+
129153
# Get the path to the mods folder, with any applicable overrides applied
130154
static func get_path_to_mods() -> String:
131155
var mods_folder_path := get_local_folder_dir("mods")

addons/mod_loader/internal/third_party/steam.gd

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam"
99
# Load mod ZIPs from Steam workshop folders. Uses 2 loops: One for each
1010
# workshop item's folder, with another inside that which loops over the ZIPs
1111
# inside each workshop item's folder
12-
static func load_steam_workshop_zips() -> int:
13-
var temp_zipped_mods_count := 0
12+
static func load_steam_workshop_zips() -> Dictionary:
13+
var zip_data := {}
1414
var workshop_folder_path := _get_path_to_workshop()
1515

1616
ModLoaderLog.info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME)
@@ -19,11 +19,11 @@ static func load_steam_workshop_zips() -> int:
1919
var workshop_dir_open_error := workshop_dir.open(workshop_folder_path)
2020
if not workshop_dir_open_error == OK:
2121
ModLoaderLog.error("Can't open workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_open_error], LOG_NAME)
22-
return -1
22+
return {}
2323
var workshop_dir_listdir_error := workshop_dir.list_dir_begin()
2424
if not workshop_dir_listdir_error == OK:
2525
ModLoaderLog.error("Can't read workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_listdir_error], LOG_NAME)
26-
return -1
26+
return {}
2727

2828
# Loop 1: Workshop folders
2929
while true:
@@ -42,11 +42,11 @@ static func load_steam_workshop_zips() -> int:
4242
continue
4343

4444
# Loop 2: ZIPs inside the workshop folders
45-
temp_zipped_mods_count += _ModLoaderFile.load_zips_in_folder(ProjectSettings.globalize_path(item_path))
45+
zip_data.merge(_ModLoaderFile.load_zips_in_folder(ProjectSettings.globalize_path(item_path)))
4646

4747
workshop_dir.list_dir_end()
4848

49-
return temp_zipped_mods_count
49+
return zip_data
5050

5151

5252
# Get the path to the Steam workshop folder. Only works for Steam games, as it

addons/mod_loader/mod_loader.gd

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,20 @@ func _exit_tree() -> void:
8585
func _load_mods() -> void:
8686
# Loop over "res://mods" and add any mod zips to the unpacked virtual
8787
# directory (UNPACKED_DIR)
88-
var unzipped_mods := _load_mod_zips()
89-
if unzipped_mods > 0:
90-
ModLoaderLog.success("DONE: Loaded %s mod files into the virtual filesystem" % unzipped_mods, LOG_NAME)
91-
else:
88+
var zip_data := _load_mod_zips()
89+
90+
if zip_data.empty():
9291
ModLoaderLog.info("No zipped mods found", LOG_NAME)
92+
else:
93+
ModLoaderLog.success("DONE: Loaded %s mod files into the virtual filesystem" % zip_data.size(), LOG_NAME)
94+
95+
# Initializes the mod_data dictionary if zipped mods are loaded.
96+
# If mods are unpacked in the "mods-unpacked" directory,
97+
# mod_data is initialized in the _setup_mods() function.
98+
for mod_id in zip_data.keys():
99+
var zip_path: String = zip_data[mod_id]
100+
_init_mod_data(mod_id, zip_path)
101+
93102

94103
# Loop over UNPACKED_DIR. This triggers _init_mod_data for each mod
95104
# directory, which adds their data to mod_data.
@@ -219,19 +228,21 @@ func _check_autoload_positions() -> void:
219228

220229
# Loop over "res://mods" and add any mod zips to the unpacked virtual directory
221230
# (UNPACKED_DIR)
222-
func _load_mod_zips() -> int:
223-
var zipped_mods_count := 0
231+
func _load_mod_zips() -> Dictionary:
232+
var zip_data := {}
224233

225234
if not ModLoaderStore.ml_options.steam_workshop_enabled:
226235
var mods_folder_path := _ModLoaderPath.get_path_to_mods()
227236

228237
# If we're not using Steam workshop, just loop over the mod ZIPs.
229-
zipped_mods_count += _ModLoaderFile.load_zips_in_folder(mods_folder_path)
238+
var loaded_zip_data := _ModLoaderFile.load_zips_in_folder(mods_folder_path)
239+
zip_data.merge(loaded_zip_data)
230240
else:
231241
# If we're using Steam workshop, loop over the workshop item directories
232-
zipped_mods_count += _ModLoaderSteam.load_steam_workshop_zips()
242+
var loaded_workshop_zip_data := _ModLoaderSteam.load_steam_workshop_zips()
243+
zip_data.merge(loaded_workshop_zip_data)
233244

234-
return zipped_mods_count
245+
return zip_data
235246

236247

237248
# Loop over UNPACKED_DIR and triggers `_init_mod_data` for each mod directory,
@@ -282,19 +293,20 @@ func _setup_mods() -> int:
282293
# Add a mod's data to mod_data.
283294
# The mod_folder_path is just the folder name that was added to UNPACKED_DIR,
284295
# which depends on the name used in a given mod ZIP (eg "mods-unpacked/Folder-Name")
285-
func _init_mod_data(mod_folder_path: String) -> void:
286-
# The file name should be a valid mod id
287-
var dir_name := _ModLoaderPath.get_file_name_from_path(mod_folder_path, false, true)
288-
289-
# Path to the mod in UNPACKED_DIR (eg "res://mods-unpacked/My-Mod")
290-
var local_mod_path := _ModLoaderPath.get_unpacked_mods_dir_path().plus_file(dir_name)
291-
292-
var mod := ModData.new(local_mod_path)
293-
mod.dir_name = dir_name
296+
func _init_mod_data(mod_id: String, zip_path := "") -> void:
297+
# Path to the mod in UNPACKED_DIR (eg "res://mods-unpacked/My-Mod")
298+
var local_mod_path := _ModLoaderPath.get_unpacked_mods_dir_path().plus_file(mod_id)
299+
300+
var mod := ModData.new()
301+
if not zip_path.empty():
302+
mod.zip_name = _ModLoaderPath.get_file_name_from_path(zip_path)
303+
mod.zip_path = zip_path
304+
mod.dir_path = local_mod_path
305+
mod.dir_name = mod_id
294306
var mod_overwrites_path := mod.get_optional_mod_file_path(ModData.optional_mod_files.OVERWRITES)
295307
mod.is_overwrite = _ModLoaderFile.file_exists(mod_overwrites_path)
296-
mod.is_locked = true if dir_name in ModLoaderStore.ml_options.locked_mods else false
297-
ModLoaderStore.mod_data[dir_name] = mod
308+
mod.is_locked = true if mod_id in ModLoaderStore.ml_options.locked_mods else false
309+
ModLoaderStore.mod_data[mod_id] = mod
298310

299311
# Get the mod file paths
300312
# Note: This was needed in the original version of this script, but it's

addons/mod_loader/mod_loader_store.gd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ var mod_missing_dependencies := {}
4444
# Helps to decide whether a script extension should go through the _ModLoaderScriptExtension.handle_script_extensions() process
4545
var is_initializing := true
4646

47+
# Used when loading mod zips to determine which mod zip corresponds to which mod directory in the UNPACKED_DIR.
48+
var previous_mod_dirs := []
49+
4750
# Store all extenders paths
4851
var script_extensions := []
4952

addons/mod_loader/resources/mod_data.gd

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ enum optional_mod_files {
2323
OVERWRITES
2424
}
2525

26+
# Name of the Mod's zip file
27+
var zip_name := ""
28+
# Path to the Mod's zip file
29+
var zip_path := ""
2630
# Directory of the mod. Has to be identical to [method ModManifest.get_mod_id]
2731
var dir_name := ""
2832
# Path to the Mod's Directory
@@ -47,10 +51,6 @@ var current_config: ModConfig setget _set_current_config
4751
var file_paths: PoolStringArray = []
4852

4953

50-
func _init(_dir_path: String) -> void:
51-
dir_path = _dir_path
52-
53-
5454
# Load meta data from a mod's manifest.json file
5555
func load_manifest() -> void:
5656
if not _has_required_files():

0 commit comments

Comments
 (0)