Skip to content

Commit 9c9a5fd

Browse files
authored
refactor: ♻️ mod loading refactor (#485)
* refactor: ♻️ should probably start committing * refactor: ♻️ load mod zips * refactor: ♻️ move DOCS const to Store * refactor: ♻️ draw the rest of the owl * refactor: 🔥 remove old mod_loader.gd * fix: 🐛 only log pck warning if zip is loaded * refactor: ♻️ leaner mod order logging * refactor: ♻️ always have `mod_data` available * refactor: ♻️ prevent error if mods dir doesn't exists * style: 🎨 formating * style: ✏️ looong * test: 🧪 fix for non static manifest * fix: 🐛 Add back cache saving on `_exit_tree()` * fix: 🐛 Add back restart scene on new hooks created * fix: 🐛 bring back `_ready()` * refactor: ♻️ skip config loading if mod is not loadable * fix: ✏️ don't log success if mod is not loaded * refactor: ♻️ move `get_zip_paths_in()` to `_ModLoaderPath` and use it for `get_mod_paths_from_all_sources()`, also removed the logging from `get_zip_paths_in()` and moved it to `ModLoader` * docs: ✏️ better error message * feat: ✨ added `_ModLoaderFile.get_mod_dir_name_in_zip()` and used it in `ModData` * refactor: ♻️ remove unused `else` * refactor: ♻️ removed class from func call * refactor: ♻️ use `is_zip()` * style: ✏️ changed `mod_i` to `mod_index` * refactor: 📝 mention the dev tool in `any_mod_hooked` info log * style: ✏️ fix typo
1 parent 7b36e32 commit 9c9a5fd

File tree

12 files changed

+417
-525
lines changed

12 files changed

+417
-525
lines changed

addons/mod_loader/api/config.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ static func get_current_config_name(mod_id: String) -> String:
349349
# Check if user profile has been loaded
350350
if not ModLoaderStore.current_user_profile or not ModLoaderStore.user_profiles.has(ModLoaderStore.current_user_profile.name):
351351
# Warn and return an empty string if the user profile has not been loaded
352-
ModLoaderLog.warning("Can't get current mod config for \"%s\", because no current user profile is present." % mod_id, LOG_NAME)
352+
ModLoaderLog.warning("Can't get current mod config name for \"%s\", because no current user profile is present." % mod_id, LOG_NAME)
353353
return ""
354354

355355
# Retrieve the current user profile from ModLoaderStore
@@ -359,7 +359,7 @@ static func get_current_config_name(mod_id: String) -> String:
359359
# Check if the mod exists in the user profile's mod list and if it has a current config
360360
if not current_user_profile.mod_list.has(mod_id) or not current_user_profile.mod_list[mod_id].has("current_config"):
361361
# Log an error and return an empty string if the mod has no config file
362-
ModLoaderLog.error("Mod \"%s\" has no config file." % mod_id, LOG_NAME)
362+
ModLoaderLog.error("Can't get current mod config name for \"%s\" because no config file exists." % mod_id, LOG_NAME)
363363
return ""
364364

365365
# Return the name of the current configuration for the mod

addons/mod_loader/internal/file.gd

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,25 +107,6 @@ static func get_json_as_dict_from_zip(zip_path: String, file_path: String, is_fu
107107
return _get_json_string_as_dict(content)
108108

109109

110-
# Finds the global paths to all zips in provided directory
111-
static func get_zip_paths_in(folder_path: String) -> Array[String]:
112-
var zip_paths: Array[String] = []
113-
114-
var files := Array(DirAccess.get_files_at(folder_path))\
115-
.filter(
116-
func(file_name: String):
117-
return file_name.get_extension() == "zip"
118-
).map(
119-
func(file_name: String):
120-
return ProjectSettings.globalize_path(folder_path.path_join(file_name))
121-
)
122-
ModLoaderLog.debug("Found %s mod ZIPs: %s" % [files.size(), str(files)], LOG_NAME)
123-
124-
# only .assign()ing to a typed array lets us return Array[String] instead of just Array
125-
zip_paths.assign(files)
126-
return zip_paths
127-
128-
129110
# Save Data
130111
# =============================================================================
131112

@@ -194,7 +175,7 @@ static func remove_file(file_path: String) -> bool:
194175

195176
static func file_exists(path: String, zip_path: String = "") -> bool:
196177
if not zip_path.is_empty():
197-
return file_exists_in_zip(path, zip_path)
178+
return file_exists_in_zip(zip_path, path)
198179

199180
var exists := FileAccess.file_exists(path)
200181

@@ -209,13 +190,28 @@ static func dir_exists(path: String) -> bool:
209190
return DirAccess.dir_exists_absolute(path)
210191

211192

212-
static func file_exists_in_zip(path: String, zip_path: String = "") -> bool:
193+
static func file_exists_in_zip(zip_path: String, path: String) -> bool:
213194
var reader := zip_reader_open(zip_path)
214195
if not reader:
215196
return false
216197
return reader.file_exists(path.trim_prefix("res://"))
217198

218199

200+
static func get_mod_dir_name_in_zip(zip_path: String) -> String:
201+
var reader := _ModLoaderFile.zip_reader_open(zip_path)
202+
if not reader:
203+
return ""
204+
205+
var file_paths := reader.get_files()
206+
207+
for file_path in file_paths:
208+
# We asume tat the mod_main.gd is at the root of the mod dir
209+
if file_path.ends_with("mod_main.gd") and file_path.split("/").size() == 3:
210+
return file_path.split("/")[-2]
211+
212+
return ""
213+
214+
219215
static func zip_reader_open(zip_path) -> ZIPReader:
220216
var reader := ZIPReader.new()
221217
var err := reader.open(zip_path)
@@ -225,10 +221,14 @@ static func zip_reader_open(zip_path) -> ZIPReader:
225221
return reader
226222

227223

228-
# Internal util functions
229-
# =============================================================================
230-
# These are duplicates of the functions in mod_loader_utils.gd to prevent
231-
# a cyclic reference error.
224+
static func load_manifest_file(path: String) -> Dictionary:
225+
ModLoaderLog.debug("Loading mod_manifest from -> %s" % path, LOG_NAME)
226+
227+
if _ModLoaderPath.is_zip(path):
228+
return get_json_as_dict_from_zip(path, ModData.MANIFEST)
229+
230+
return get_json_as_dict(path.path_join(ModData.MANIFEST))
231+
232232

233233
# This is a dummy func. It is exclusively used to show notes in the code that
234234
# stay visible after decompiling a PCK, as is primarily intended to assist new

addons/mod_loader/internal/godot.gd

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,31 @@ const LOG_NAME := "ModLoader:Godot"
99
const AUTOLOAD_CONFIG_HELP_MSG := "To configure your autoloads, go to Project > Project Settings > Autoload."
1010

1111

12+
# Check autoload positions:
13+
# Ensure 1st autoload is `ModLoaderStore`, and 2nd is `ModLoader`.
14+
static func check_autoload_positions() -> void:
15+
var override_cfg_path := _ModLoaderPath.get_override_path()
16+
var is_override_cfg_setup := _ModLoaderFile.file_exists(override_cfg_path)
17+
# If the override file exists we assume the ModLoader was setup with the --setup-create-override-cfg cli arg
18+
# In that case the ModLoader will be the last entry in the autoload array
19+
if is_override_cfg_setup:
20+
ModLoaderLog.info("override.cfg setup detected, ModLoader will be the last autoload loaded.", LOG_NAME)
21+
return
22+
23+
# If there are Autoloads that need to be before the ModLoader
24+
# "allow_modloader_autoloads_anywhere" in the ModLoader Options can be enabled.
25+
# With that only the correct order of, ModLoaderStore first and ModLoader second, is checked.
26+
if ModLoaderStore.ml_options.allow_modloader_autoloads_anywhere:
27+
is_autoload_before("ModLoaderStore", "ModLoader", true)
28+
else:
29+
var _pos_ml_store := check_autoload_position("ModLoaderStore", 0, true)
30+
var _pos_ml_core := check_autoload_position("ModLoader", 1, true)
31+
32+
1233
# Check if autoload_name_before is before autoload_name_after
1334
# Returns a bool if the position does not match.
1435
# Optionally triggers a fatal error
15-
static func check_autoload_order(autoload_name_before: String, autoload_name_after: String, trigger_error := false) -> bool:
36+
static func is_autoload_before(autoload_name_before: String, autoload_name_after: String, trigger_error := false) -> bool:
1637
var autoload_name_before_index := get_autoload_index(autoload_name_before)
1738
var autoload_name_after_index := get_autoload_index(autoload_name_after)
1839

addons/mod_loader/internal/hooks.gd

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@ static func get_hook_hash(path: String, method: String) -> int:
4545
return hash(path + method)
4646

4747

48-
48+
static func on_new_hooks_created() -> void:
49+
if ModLoaderStore.ml_options.disable_restart:
50+
ModLoaderLog.debug("Mod Loader handled restart is disabled.", LOG_NAME)
51+
return
52+
ModLoaderLog.debug("Instancing restart notification scene from path: %s" % [ModLoaderStore.ml_options.restart_notification_scene_path], LOG_NAME)
53+
var restart_notification_scene = load(ModLoaderStore.ml_options.restart_notification_scene_path).instantiate()
54+
ModLoader.add_child(restart_notification_scene)

addons/mod_loader/internal/mod_loader_utils.gd

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,8 @@ static func get_dict_from_dict(dict: Dictionary, key: String) -> Dictionary:
4747

4848
## Works like [method Dictionary.has_all],
4949
## but allows for more specific errors if a field is missing
50-
static func dict_has_fields(dict: Dictionary, required_fields: Array) -> bool:
51-
var missing_fields := required_fields.duplicate()
52-
53-
for key in dict.keys():
54-
if(required_fields.has(key)):
55-
missing_fields.erase(key)
50+
static func dict_has_fields(dict: Dictionary, required_fields: Array[String]) -> bool:
51+
var missing_fields := get_missing_dict_fields(dict, required_fields)
5652

5753
if missing_fields.size() > 0:
5854
ModLoaderLog.fatal("Dictionary is missing required fields: %s" % str(missing_fields), LOG_NAME)
@@ -61,6 +57,16 @@ static func dict_has_fields(dict: Dictionary, required_fields: Array) -> bool:
6157
return true
6258

6359

60+
static func get_missing_dict_fields(dict: Dictionary, required_fields: Array[String]) -> Array[String]:
61+
var missing_fields := required_fields.duplicate()
62+
63+
for key in dict.keys():
64+
if(required_fields.has(key)):
65+
missing_fields.erase(key)
66+
67+
return missing_fields
68+
69+
6470
## Register an array of classes to the global scope, since Godot only does that in the editor.
6571
static func register_global_classes_from_array(new_global_classes: Array) -> void:
6672
var registered_classes: Array = ProjectSettings.get_setting("_global_script_classes")

addons/mod_loader/internal/path.gd

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ static func get_steam_workshop_id(zip_path: String) -> String:
6868
return zip_path.get_base_dir().split("/")[-1]
6969

7070

71-
# Get a flat array of all files in the target directory. This was needed in the
72-
# original version of this script, before becoming deprecated. It may still be
73-
# used if DEBUG_ENABLE_STORING_FILEPATHS is true.
71+
# Get a flat array of all files in the target directory.
7472
# Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
7573
static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_regex := false) -> PackedStringArray:
7674
var data: PackedStringArray = []
@@ -167,12 +165,54 @@ static func get_dir_paths_in_dir(src_dir_path: String) -> Array:
167165
# Get the path to the mods folder, with any applicable overrides applied
168166
static func get_path_to_mods() -> String:
169167
var mods_folder_path := get_local_folder_dir("mods")
168+
170169
if ModLoaderStore:
171170
if ModLoaderStore.ml_options.override_path_to_mods:
172171
mods_folder_path = ModLoaderStore.ml_options.override_path_to_mods
173172
return mods_folder_path
174173

175174

175+
# Finds the global paths to all zips in provided directory
176+
static func get_zip_paths_in(folder_path: String) -> Array[String]:
177+
var zip_paths: Array[String] = []
178+
179+
var files := Array(DirAccess.get_files_at(folder_path))\
180+
.filter(
181+
func(file_name: String):
182+
return is_zip(file_name)
183+
).map(
184+
func(file_name: String):
185+
return ProjectSettings.globalize_path(folder_path.path_join(file_name))
186+
)
187+
188+
# only .assign()ing to a typed array lets us return Array[String] instead of just Array
189+
zip_paths.assign(files)
190+
return zip_paths
191+
192+
193+
static func get_mod_paths_from_all_sources() -> Array[String]:
194+
var mod_paths: Array[String] = []
195+
196+
var mod_dirs := get_dir_paths_in_dir(get_unpacked_mods_dir_path())
197+
mod_paths.append_array(mod_dirs)
198+
199+
if ModLoaderStore.ml_options.load_from_local:
200+
var mods_dir := get_path_to_mods()
201+
if not DirAccess.dir_exists_absolute(mods_dir):
202+
ModLoaderLog.info("The directory for mods at path \"%s\" does not exist." % mods_dir, LOG_NAME)
203+
else:
204+
mod_paths.append_array(get_zip_paths_in(mods_dir))
205+
206+
if ModLoaderStore.ml_options.load_from_steam_workshop:
207+
mod_paths.append_array(_ModLoaderSteam.find_steam_workshop_zips())
208+
209+
return mod_paths
210+
211+
212+
static func get_path_to_mod_manifest(mod_id: String) -> String:
213+
return get_path_to_mods().path_join(mod_id).path_join("manifest.json")
214+
215+
176216
static func get_unpacked_mods_dir_path() -> String:
177217
return ModLoaderStore.UNPACKED_DIR
178218

@@ -234,6 +274,11 @@ static func get_mod_dir(path: String) -> String:
234274
return found_string
235275

236276

277+
# Checks if the path ends with .zip
278+
static func is_zip(path: String) -> bool:
279+
return path.get_extension() == "zip"
280+
281+
237282
static func handle_mod_config_path_deprecation() -> void:
238283
ModLoaderDeprecated.deprecated_message("The mod config path has been moved to \"%s\".
239284
The Mod Loader will attempt to rename the config directory." % MOD_CONFIG_DIR_PATH, "7.0.0")

addons/mod_loader/internal/third_party/steam.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam"
66
# Methods related to Steam and the Steam Workshop
77

88

9-
# Load mod ZIPs from Steam workshop folders.
9+
# Get mod zip paths from steam workshop folders.
1010
# folder structure of a workshop item
1111
# <workshop folder>/<steam app id>/<workshop item id>/<mod>.zip
1212
static func find_steam_workshop_zips() -> Array[String]:
@@ -40,7 +40,7 @@ static func find_steam_workshop_zips() -> Array[String]:
4040
continue
4141

4242
# Loop 2: ZIPs inside the workshop folders
43-
zip_paths.append_array(_ModLoaderFile.get_zip_paths_in(ProjectSettings.globalize_path(item_path)))
43+
zip_paths.append_array(_ModLoaderPath.get_zip_paths_in(ProjectSettings.globalize_path(item_path)))
4444

4545
workshop_dir.list_dir_end()
4646

0 commit comments

Comments
 (0)