Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
094a83a
refactor: :recycle: should probably start committing
KANAjetzt Dec 7, 2024
564ee14
refactor: :recycle: load mod zips
KANAjetzt Dec 8, 2024
d40b0c6
refactor: :recycle: move DOCS const to Store
KANAjetzt Dec 8, 2024
765d5d2
refactor: :recycle: draw the rest of the owl
KANAjetzt Dec 8, 2024
c6d4f2d
refactor: :fire: remove old mod_loader.gd
KANAjetzt Dec 8, 2024
35920b8
fix: :bug: only log pck warning if zip is loaded
KANAjetzt Dec 9, 2024
d1c05b7
refactor: :recycle: leaner mod order logging
KANAjetzt Dec 9, 2024
80e408c
refactor: :recycle: always have `mod_data` available
KANAjetzt Dec 10, 2024
86def66
refactor: :recycle: prevent error if mods dir doesn't exists
KANAjetzt Dec 10, 2024
3f3e973
style: :art: formating
KANAjetzt Dec 10, 2024
17065e6
style: :pencil2: looong
KANAjetzt Dec 10, 2024
179f554
test: :test_tube: fix for non static manifest
KANAjetzt Dec 11, 2024
d9c0b37
fix: :bug: Add back cache saving on `_exit_tree()`
KANAjetzt Dec 11, 2024
59b7bc5
fix: :bug: Add back restart scene on new hooks created
KANAjetzt Dec 11, 2024
f9a90fe
fix: :bug: bring back `_ready()`
KANAjetzt Dec 11, 2024
d262254
refactor: :recycle: skip config loading if mod is not loadable
KANAjetzt Dec 11, 2024
c0f1d47
fix: :pencil2: don't log success if mod is not loaded
KANAjetzt Dec 11, 2024
ecebb28
refactor: :recycle: move `get_zip_paths_in()` to `_ModLoaderPath`
KANAjetzt Dec 11, 2024
e1e21a3
docs: :pencil2: better error message
KANAjetzt Dec 11, 2024
47b1409
feat: :sparkles: added `_ModLoaderFile.get_mod_dir_name_in_zip()`
KANAjetzt Dec 11, 2024
423d0af
refactor: :recycle: remove unused `else`
KANAjetzt Jan 7, 2025
84a5f43
refactor: :recycle: removed class from func call
KANAjetzt Jan 7, 2025
e7b8927
refactor: :recycle: use `is_zip()`
KANAjetzt Jan 7, 2025
6a35588
style: :pencil2: changed `mod_i` to `mod_index`
KANAjetzt Jan 7, 2025
73102a9
refactor: :memo: mention the dev tool in `any_mod_hooked` info log
KANAjetzt Jan 7, 2025
ad0eadb
style: :pencil2: fix typo
KANAjetzt Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions addons/mod_loader/api/config.gd
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ static func get_current_config_name(mod_id: String) -> String:
# Check if user profile has been loaded
if not ModLoaderStore.current_user_profile or not ModLoaderStore.user_profiles.has(ModLoaderStore.current_user_profile.name):
# Warn and return an empty string if the user profile has not been loaded
ModLoaderLog.warning("Can't get current mod config for \"%s\", because no current user profile is present." % mod_id, LOG_NAME)
ModLoaderLog.warning("Can't get current mod config name for \"%s\", because no current user profile is present." % mod_id, LOG_NAME)
return ""

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

# Return the name of the current configuration for the mod
Expand Down
50 changes: 25 additions & 25 deletions addons/mod_loader/internal/file.gd
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,6 @@ static func get_json_as_dict_from_zip(zip_path: String, file_path: String, is_fu
return _get_json_string_as_dict(content)


# Finds the global paths to all zips in provided directory
static func get_zip_paths_in(folder_path: String) -> Array[String]:
var zip_paths: Array[String] = []

var files := Array(DirAccess.get_files_at(folder_path))\
.filter(
func(file_name: String):
return file_name.get_extension() == "zip"
).map(
func(file_name: String):
return ProjectSettings.globalize_path(folder_path.path_join(file_name))
)
ModLoaderLog.debug("Found %s mod ZIPs: %s" % [files.size(), str(files)], LOG_NAME)

# only .assign()ing to a typed array lets us return Array[String] instead of just Array
zip_paths.assign(files)
return zip_paths


# Save Data
# =============================================================================

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

static func file_exists(path: String, zip_path: String = "") -> bool:
if not zip_path.is_empty():
return file_exists_in_zip(path, zip_path)
return file_exists_in_zip(zip_path, path)

var exists := FileAccess.file_exists(path)

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


static func file_exists_in_zip(path: String, zip_path: String = "") -> bool:
static func file_exists_in_zip(zip_path: String, path: String) -> bool:
var reader := zip_reader_open(zip_path)
if not reader:
return false
return reader.file_exists(path.trim_prefix("res://"))


static func get_mod_dir_name_in_zip(zip_path: String) -> String:
var reader := _ModLoaderFile.zip_reader_open(zip_path)
if not reader:
return ""

var file_paths := reader.get_files()

for file_path in file_paths:
# We asume tat the mod_main.gd is at the root of the mod dir
if file_path.ends_with("mod_main.gd") and file_path.split("/").size() == 3:
return file_path.split("/")[-2]

return ""


static func zip_reader_open(zip_path) -> ZIPReader:
var reader := ZIPReader.new()
var err := reader.open(zip_path)
Expand All @@ -225,10 +221,14 @@ static func zip_reader_open(zip_path) -> ZIPReader:
return reader


# Internal util functions
# =============================================================================
# These are duplicates of the functions in mod_loader_utils.gd to prevent
# a cyclic reference error.
static func load_manifest_file(path: String) -> Dictionary:
ModLoaderLog.debug("Loading mod_manifest from -> %s" % path, LOG_NAME)

if _ModLoaderPath.is_zip(path):
return get_json_as_dict_from_zip(path, ModData.MANIFEST)

return get_json_as_dict(path.path_join(ModData.MANIFEST))


# This is a dummy func. It is exclusively used to show notes in the code that
# stay visible after decompiling a PCK, as is primarily intended to assist new
Expand Down
23 changes: 22 additions & 1 deletion addons/mod_loader/internal/godot.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,31 @@ const LOG_NAME := "ModLoader:Godot"
const AUTOLOAD_CONFIG_HELP_MSG := "To configure your autoloads, go to Project > Project Settings > Autoload."


# Check autoload positions:
# Ensure 1st autoload is `ModLoaderStore`, and 2nd is `ModLoader`.
static func check_autoload_positions() -> void:
var override_cfg_path := _ModLoaderPath.get_override_path()
var is_override_cfg_setup := _ModLoaderFile.file_exists(override_cfg_path)
# If the override file exists we assume the ModLoader was setup with the --setup-create-override-cfg cli arg
# In that case the ModLoader will be the last entry in the autoload array
if is_override_cfg_setup:
ModLoaderLog.info("override.cfg setup detected, ModLoader will be the last autoload loaded.", LOG_NAME)
return

# If there are Autoloads that need to be before the ModLoader
# "allow_modloader_autoloads_anywhere" in the ModLoader Options can be enabled.
# With that only the correct order of, ModLoaderStore first and ModLoader second, is checked.
if ModLoaderStore.ml_options.allow_modloader_autoloads_anywhere:
is_autoload_before("ModLoaderStore", "ModLoader", true)
else:
var _pos_ml_store := check_autoload_position("ModLoaderStore", 0, true)
var _pos_ml_core := check_autoload_position("ModLoader", 1, true)


# Check if autoload_name_before is before autoload_name_after
# Returns a bool if the position does not match.
# Optionally triggers a fatal error
static func check_autoload_order(autoload_name_before: String, autoload_name_after: String, trigger_error := false) -> bool:
static func is_autoload_before(autoload_name_before: String, autoload_name_after: String, trigger_error := false) -> bool:
var autoload_name_before_index := get_autoload_index(autoload_name_before)
var autoload_name_after_index := get_autoload_index(autoload_name_after)

Expand Down
8 changes: 7 additions & 1 deletion addons/mod_loader/internal/hooks.gd
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ static func get_hook_hash(path: String, method: String) -> int:
return hash(path + method)



static func on_new_hooks_created() -> void:
if ModLoaderStore.ml_options.disable_restart:
ModLoaderLog.debug("Mod Loader handled restart is disabled.", LOG_NAME)
return
ModLoaderLog.debug("Instancing restart notification scene from path: %s" % [ModLoaderStore.ml_options.restart_notification_scene_path], LOG_NAME)
var restart_notification_scene = load(ModLoaderStore.ml_options.restart_notification_scene_path).instantiate()
ModLoader.add_child(restart_notification_scene)
18 changes: 12 additions & 6 deletions addons/mod_loader/internal/mod_loader_utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,8 @@ static func get_dict_from_dict(dict: Dictionary, key: String) -> Dictionary:

## Works like [method Dictionary.has_all],
## but allows for more specific errors if a field is missing
static func dict_has_fields(dict: Dictionary, required_fields: Array) -> bool:
var missing_fields := required_fields.duplicate()

for key in dict.keys():
if(required_fields.has(key)):
missing_fields.erase(key)
static func dict_has_fields(dict: Dictionary, required_fields: Array[String]) -> bool:
var missing_fields := get_missing_dict_fields(dict, required_fields)

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


static func get_missing_dict_fields(dict: Dictionary, required_fields: Array[String]) -> Array[String]:
var missing_fields := required_fields.duplicate()

for key in dict.keys():
if(required_fields.has(key)):
missing_fields.erase(key)

return missing_fields


## Register an array of classes to the global scope, since Godot only does that in the editor.
static func register_global_classes_from_array(new_global_classes: Array) -> void:
var registered_classes: Array = ProjectSettings.get_setting("_global_script_classes")
Expand Down
51 changes: 48 additions & 3 deletions addons/mod_loader/internal/path.gd
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ static func get_steam_workshop_id(zip_path: String) -> String:
return zip_path.get_base_dir().split("/")[-1]


# Get a flat array of all files in the target directory. This was needed in the
# original version of this script, before becoming deprecated. It may still be
# used if DEBUG_ENABLE_STORING_FILEPATHS is true.
# Get a flat array of all files in the target directory.
# Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_regex := false) -> PackedStringArray:
var data: PackedStringArray = []
Expand Down Expand Up @@ -167,12 +165,54 @@ static func get_dir_paths_in_dir(src_dir_path: String) -> Array:
# 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")

if ModLoaderStore:
if ModLoaderStore.ml_options.override_path_to_mods:
mods_folder_path = ModLoaderStore.ml_options.override_path_to_mods
return mods_folder_path


# Finds the global paths to all zips in provided directory
static func get_zip_paths_in(folder_path: String) -> Array[String]:
var zip_paths: Array[String] = []

var files := Array(DirAccess.get_files_at(folder_path))\
.filter(
func(file_name: String):
return is_zip(file_name)
).map(
func(file_name: String):
return ProjectSettings.globalize_path(folder_path.path_join(file_name))
)

# only .assign()ing to a typed array lets us return Array[String] instead of just Array
zip_paths.assign(files)
return zip_paths


static func get_mod_paths_from_all_sources() -> Array[String]:
var mod_paths: Array[String] = []

var mod_dirs := get_dir_paths_in_dir(get_unpacked_mods_dir_path())
mod_paths.append_array(mod_dirs)

if ModLoaderStore.ml_options.load_from_local:
var mods_dir := get_path_to_mods()
if not DirAccess.dir_exists_absolute(mods_dir):
ModLoaderLog.info("The directory for mods at path \"%s\" does not exist." % mods_dir, LOG_NAME)
else:
mod_paths.append_array(get_zip_paths_in(mods_dir))

if ModLoaderStore.ml_options.load_from_steam_workshop:
mod_paths.append_array(_ModLoaderSteam.find_steam_workshop_zips())

return mod_paths


static func get_path_to_mod_manifest(mod_id: String) -> String:
return get_path_to_mods().path_join(mod_id).path_join("manifest.json")


static func get_unpacked_mods_dir_path() -> String:
return ModLoaderStore.UNPACKED_DIR

Expand Down Expand Up @@ -234,6 +274,11 @@ static func get_mod_dir(path: String) -> String:
return found_string


# Checks if the path ends with .zip
static func is_zip(path: String) -> bool:
return path.get_extension() == "zip"


static func handle_mod_config_path_deprecation() -> void:
ModLoaderDeprecated.deprecated_message("The mod config path has been moved to \"%s\".
The Mod Loader will attempt to rename the config directory." % MOD_CONFIG_DIR_PATH, "7.0.0")
Expand Down
4 changes: 2 additions & 2 deletions addons/mod_loader/internal/third_party/steam.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam"
# Methods related to Steam and the Steam Workshop


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

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

workshop_dir.list_dir_end()

Expand Down
Loading
Loading