diff --git a/addons/mod_loader/api/config.gd b/addons/mod_loader/api/config.gd index 8d43e950..4f1380a4 100644 --- a/addons/mod_loader/api/config.gd +++ b/addons/mod_loader/api/config.gd @@ -117,10 +117,10 @@ static func save_mod_config_dictionary(mod_id: String, data: Dictionary, update_ data_new = data_original.duplicate(true) data_new.merge(data, true) - var configs_path := ModLoaderUtils.get_path_to_configs() + var configs_path := _ModLoaderPath.get_path_to_configs() var json_path := configs_path.plus_file(mod_id + ".json") - return ModLoaderUtils.save_dictionary_to_json_file(data_new, json_path) + return _ModLoaderFile.save_dictionary_to_json_file(data_new, json_path) # Saves a single settings to a mod's custom config file. diff --git a/addons/mod_loader/api/mod.gd b/addons/mod_loader/api/mod.gd index 423e7b78..361b5dfb 100644 --- a/addons/mod_loader/api/mod.gd +++ b/addons/mod_loader/api/mod.gd @@ -52,14 +52,14 @@ func reload_mods() -> void: # (but you should only include classes belonging to your mod) static func register_global_classes_from_array(new_global_classes: Array) -> void: ModLoaderUtils.register_global_classes_from_array(new_global_classes) - var _savecustom_error: int = ProjectSettings.save_custom(ModLoaderUtils.get_override_path()) + var _savecustom_error: int = ProjectSettings.save_custom(_ModLoaderPath.get_override_path()) # Add a translation file, eg "mytranslation.en.translation". The translation # file should have been created in Godot already: When you import a CSV, such # a file will be created for you. static func add_translation_from_resource(resource_path: String) -> void: - if not File.new().file_exists(resource_path): + if not _ModLoaderFile.file_exists(resource_path): ModLoaderLog.fatal("Tried to load a translation resource from a file that doesn't exist. The invalid path was: %s" % [resource_path], LOG_NAME) return diff --git a/addons/mod_loader/api/third_party/steam.gd b/addons/mod_loader/api/third_party/steam.gd index fb9a8e7a..4a2d144d 100644 --- a/addons/mod_loader/api/third_party/steam.gd +++ b/addons/mod_loader/api/third_party/steam.gd @@ -19,7 +19,7 @@ static func get_path_to_workshop() -> String: if ModLoaderStore.ml_options.override_path_to_workshop: return ModLoaderStore.ml_options.override_path_to_workshop - var game_install_directory := ModLoaderUtils.get_local_folder_dir() + var game_install_directory := _ModLoaderPath.get_local_folder_dir() var path := "" # Traverse up to the steamapps directory (ie. `cd ..\..\` on Windows) @@ -40,7 +40,7 @@ static func get_path_to_workshop() -> String: # Utility (GWU), which was developed by Brotato developer Blobfish: # https://github.com/thomasgvd/godot-workshop-utility static func get_steam_app_id() -> String: - var game_install_directory := ModLoaderUtils.get_local_folder_dir() + var game_install_directory := _ModLoaderPath.get_local_folder_dir() var steam_app_id := "" var file := File.new() diff --git a/addons/mod_loader/classes/mod_data.gd b/addons/mod_loader/classes/mod_data.gd index 0fcdf966..640e2bad 100644 --- a/addons/mod_loader/classes/mod_data.gd +++ b/addons/mod_loader/classes/mod_data.gd @@ -55,7 +55,7 @@ func load_manifest() -> void: # Load meta data file var manifest_path := get_required_mod_file_path(required_mod_files.MANIFEST) - var manifest_dict := ModLoaderUtils.get_json_as_dict(manifest_path) + var manifest_dict := _ModLoaderFile.get_json_as_dict(manifest_path) if USE_EXTENDED_DEBUGLOG: ModLoaderLog.debug_json_print("%s loaded manifest data -> " % dir_name, manifest_dict, LOG_NAME) @@ -82,12 +82,10 @@ func is_mod_dir_name_same_as_id(mod_manifest: ModManifest) -> bool: # Confirms that all files from [member required_mod_files] exist func has_required_files() -> bool: - var file_check := File.new() - for required_file in required_mod_files: var file_path := get_required_mod_file_path(required_mod_files[required_file]) - if !file_check.file_exists(file_path): + if !_ModLoaderFile.file_exists(file_path): ModLoaderLog.fatal("ERROR - %s is missing a required file: %s" % [dir_name, file_path], LOG_NAME) is_loadable = false return is_loadable diff --git a/addons/mod_loader/internal/cli.gd b/addons/mod_loader/internal/cli.gd new file mode 100644 index 00000000..91fdc4ea --- /dev/null +++ b/addons/mod_loader/internal/cli.gd @@ -0,0 +1,85 @@ +class_name _ModLoaderCLI +extends Reference + + +# This Class provides util functions for working with cli arguments. +# Currently all of the included functions are internal and should only be used by the mod loader itself. + +const LOG_NAME := "ModLoader:CLI" + + +# Check if the provided command line argument was present when launching the game +static func is_running_with_command_line_arg(argument: String) -> bool: + for arg in OS.get_cmdline_args(): + if argument == arg.split("=")[0]: + return true + + return false + + +# Get the command line argument value if present when launching the game +static func get_cmd_line_arg_value(argument: String) -> String: + var args := get_fixed_cmdline_args() + + for arg_index in args.size(): + var arg := args[arg_index] as String + + var key := arg.split("=")[0] + if key == argument: + # format: `--arg=value` or `--arg="value"` + if "=" in arg: + var value := arg.trim_prefix(argument + "=") + value = value.trim_prefix('"').trim_suffix('"') + value = value.trim_prefix("'").trim_suffix("'") + return value + + # format: `--arg value` or `--arg "value"` + elif arg_index +1 < args.size() and not args[arg_index +1].begins_with("--"): + return args[arg_index + 1] + + return "" + + +static func get_fixed_cmdline_args() -> PoolStringArray: + return fix_godot_cmdline_args_string_space_splitting(OS.get_cmdline_args()) + + +# Reverses a bug in Godot, which splits input strings at spaces even if they are quoted +# e.g. `--arg="some value" --arg-two 'more value'` becomes `[ --arg="some, value", --arg-two, 'more, value' ]` +static func fix_godot_cmdline_args_string_space_splitting(args: PoolStringArray) -> PoolStringArray: + if not OS.has_feature("editor"): # only happens in editor builds + return args + if OS.has_feature("Windows"): # windows is unaffected + return args + + var fixed_args := PoolStringArray([]) + var fixed_arg := "" + # if we encounter an argument that contains `=` followed by a quote, + # or an argument that starts with a quote, take all following args and + # concatenate them into one, until we find the closing quote + for arg in args: + var arg_string := arg as String + if '="' in arg_string or '="' in fixed_arg or \ + arg_string.begins_with('"') or fixed_arg.begins_with('"'): + if not fixed_arg == "": + fixed_arg += " " + fixed_arg += arg_string + if arg_string.ends_with('"'): + fixed_args.append(fixed_arg.trim_prefix(" ")) + fixed_arg = "" + continue + # same thing for single quotes + elif "='" in arg_string or "='" in fixed_arg \ + or arg_string.begins_with("'") or fixed_arg.begins_with("'"): + if not fixed_arg == "": + fixed_arg += " " + fixed_arg += arg_string + if arg_string.ends_with("'"): + fixed_args.append(fixed_arg.trim_prefix(" ")) + fixed_arg = "" + continue + + else: + fixed_args.append(arg_string) + + return fixed_args diff --git a/addons/mod_loader/internal/file.gd b/addons/mod_loader/internal/file.gd new file mode 100644 index 00000000..5ec0dc5f --- /dev/null +++ b/addons/mod_loader/internal/file.gd @@ -0,0 +1,109 @@ +class_name _ModLoaderFile +extends Reference + + +# This Class provides util functions for working with files. +# Currently all of the included functions are internal and should only be used by the mod loader itself. + +const LOG_NAME := "ModLoader:File" + + +# Get Data +# ============================================================================= + +# Parses JSON from a given file path and returns a [Dictionary]. +# Returns an empty [Dictionary] if no file exists (check with size() < 1) +static func get_json_as_dict(path: String) -> Dictionary: + var file := File.new() + + if !file.file_exists(path): + file.close() + return {} + + var error = file.open(path, File.READ) + if not error == OK: + ModLoaderLog.error("Error opening file. Code: %s" % error, LOG_NAME) + + var content := file.get_as_text() + return get_json_string_as_dict(content) + + +# Parses JSON from a given [String] and returns a [Dictionary]. +# Returns an empty [Dictionary] on error (check with size() < 1) +static func get_json_string_as_dict(string: String) -> Dictionary: + if string == "": + return {} + var parsed := JSON.parse(string) + if parsed.error: + ModLoaderLog.error("Error parsing JSON", LOG_NAME) + return {} + if not parsed.result is Dictionary: + ModLoaderLog.error("JSON is not a dictionary", LOG_NAME) + return {} + return parsed.result + + +# Save Data +# ============================================================================= + +# Saves a dictionary to a file, as a JSON string +static func save_string_to_file(save_string: String, filepath: String) -> bool: + # Create directory if it doesn't exist yet + var file_directory := filepath.get_base_dir() + var dir := Directory.new() + + _code_note(str( + "View error codes here:", + "https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-error" + )) + + if not dir.dir_exists(file_directory): + var makedir_error = dir.make_dir_recursive(file_directory) + if not makedir_error == OK: + ModLoaderLog.fatal("Encountered an error (%s) when attempting to create a directory, with the path: %s" % [makedir_error, file_directory], LOG_NAME) + return false + + var file = File.new() + + # Save data to the file + var fileopen_error = file.open(filepath, File.WRITE) + + if not fileopen_error == OK: + ModLoaderLog.fatal("Encountered an error (%s) when attempting to write to a file, with the path: %s" % [fileopen_error, filepath], LOG_NAME) + return false + + file.store_string(save_string) + file.close() + + return true + + +# Saves a dictionary to a file, as a JSON string +static func save_dictionary_to_json_file(data: Dictionary, filepath: String) -> bool: + var json_string = JSON.print(data, "\t") + return save_string_to_file(json_string, filepath) + + +# Checks +# ============================================================================= + +static func file_exists(path: String) -> bool: + var file = File.new() + return file.file_exists(path) + + +static func dir_exists(path: String) -> bool: + var dir = Directory.new() + return dir.dir_exists(path) + + +# Internal util functions +# ============================================================================= +# This are duplicates of the functions in mod_loader_utils.gd to prevent +# a cyclic reference error. + +# 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 +# modders in understanding and troubleshooting issues. +static func _code_note(_msg:String): + pass diff --git a/addons/mod_loader/api/godot.gd b/addons/mod_loader/internal/godot.gd similarity index 60% rename from addons/mod_loader/api/godot.gd rename to addons/mod_loader/internal/godot.gd index 5bdb829a..b55b5cd7 100644 --- a/addons/mod_loader/api/godot.gd +++ b/addons/mod_loader/internal/godot.gd @@ -1,7 +1,9 @@ -class_name ModLoaderGodot +class_name _ModLoaderGodot extends Object -# API methods for interacting with Godot + +# This Class provides methods for interacting with Godot. +# Currently all of the included methods are internal and should only be used by the mod loader itself. const LOG_NAME := "ModLoader:Godot" @@ -10,7 +12,7 @@ const LOG_NAME := "ModLoader:Godot" # Returns a bool if the position does not match. # Optionally triggers a fatal error static func check_autoload_position(autoload_name: String, position_index: int, trigger_error: bool = false) -> bool: - var autoload_array := ModLoaderUtils.get_autoload_array() + var autoload_array := _get_autoload_array() var autoload_index := autoload_array.find(autoload_name) var position_matches := autoload_index == position_index @@ -24,3 +26,16 @@ static func check_autoload_position(autoload_name: String, position_index: int, ModLoaderLog.fatal(error_msg + help_msg, LOG_NAME) return position_matches + + +# Get an array of all autoloads -> ["autoload/AutoloadName", ...] +static func _get_autoload_array() -> Array: + var autoloads := [] + + # Get all autoload settings + for prop in ProjectSettings.get_property_list(): + var name: String = prop.name + if name.begins_with("autoload/"): + autoloads.append(name.trim_prefix("autoload/")) + + return autoloads diff --git a/addons/mod_loader/internal/mod_loader_utils.gd b/addons/mod_loader/internal/mod_loader_utils.gd new file mode 100644 index 00000000..fc61fb6c --- /dev/null +++ b/addons/mod_loader/internal/mod_loader_utils.gd @@ -0,0 +1,142 @@ +class_name ModLoaderUtils +extends Node + + +const LOG_NAME := "ModLoader:ModLoaderUtils" + + +# 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 +# modders in understanding and troubleshooting issues +static func code_note(_msg:String): + pass + + +# Returns an empty String if the key does not exist or is not type of String +static func get_string_from_dict(dict: Dictionary, key: String) -> String: + if not dict.has(key): + return "" + + if not dict[key] is String: + return "" + + return dict[key] + + +# Returns an empty Array if the key does not exist or is not type of Array +static func get_array_from_dict(dict: Dictionary, key: String) -> Array: + if not dict.has(key): + return [] + + if not dict[key] is Array: + return [] + + return dict[key] + + +# 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 + + for key in dict.keys(): + if(required_fields.has(key)): + missing_fields.erase(key) + + if missing_fields.size() > 0: + ModLoaderLog.fatal("Mod manifest is missing required fields: %s" % missing_fields, LOG_NAME) + return false + + return true + + +# 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") + var registered_class_icons: Dictionary = ProjectSettings.get_setting("_global_script_class_icons") + + for new_class in new_global_classes: + if not is_valid_global_class_dict(new_class): + continue + for old_class in registered_classes: + if old_class.class == new_class.class: + if OS.has_feature("editor"): + ModLoaderLog.info('Class "%s" to be registered as global was already registered by the editor. Skipping.' % new_class.class, LOG_NAME) + else: + ModLoaderLog.info('Class "%s" to be registered as global already exists. Skipping.' % new_class.class, LOG_NAME) + continue + + registered_classes.append(new_class) + registered_class_icons[new_class.class] = "" # empty icon, does not matter + + ProjectSettings.set_setting("_global_script_classes", registered_classes) + ProjectSettings.set_setting("_global_script_class_icons", registered_class_icons) + + +# Checks if all required fields are in the given [Dictionary] +# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" } +static func is_valid_global_class_dict(global_class_dict: Dictionary) -> bool: + var required_fields := ["base", "class", "language", "path"] + if not global_class_dict.has_all(required_fields): + ModLoaderLog.fatal("Global class to be registered is missing one of %s" % required_fields, LOG_NAME) + return false + + if not _ModLoaderFile.file_exists(global_class_dict.path): + ModLoaderLog.fatal('Class "%s" to be registered as global could not be found at given path "%s"' % + [global_class_dict.class, global_class_dict.path], LOG_NAME) + return false + + return true + + +# Deprecated +# ============================================================================= + +# Logs the error in red and a stack trace. Prefixed FATAL-ERROR +# Stops the execution in editor +# Always logged +static func log_fatal(message: String, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_fatal", "ModLoaderLog.fatal", "6.0.0") + ModLoaderLog.fatal(message, mod_name) + + +# Logs the message and pushed an error. Prefixed ERROR +# Always logged +static func log_error(message: String, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_error", "ModLoaderLog.error", "6.0.0") + ModLoaderLog.error(message, mod_name) + + +# Logs the message and pushes a warning. Prefixed WARNING +# Logged with verbosity level at or above warning (-v) +static func log_warning(message: String, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_warning", "ModLoaderLog.warning", "6.0.0") + ModLoaderLog.warning(message, mod_name) + + +# Logs the message. Prefixed INFO +# Logged with verbosity level at or above info (-vv) +static func log_info(message: String, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_info", "ModLoaderLog.info", "6.0.0") + ModLoaderLog.info(message, mod_name) + + +# Logs the message. Prefixed SUCCESS +# Logged with verbosity level at or above info (-vv) +static func log_success(message: String, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_success", "ModLoaderLog.success", "6.0.0") + ModLoaderLog.success(message, mod_name) + + +# Logs the message. Prefixed DEBUG +# Logged with verbosity level at or above debug (-vvv) +static func log_debug(message: String, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_debug", "ModLoaderLog.debug", "6.0.0") + ModLoaderLog.debug(message, mod_name) + + +# Logs the message formatted with [method JSON.print]. Prefixed DEBUG +# Logged with verbosity level at or above debug (-vvv) +static func log_debug_json_print(message: String, json_printable, mod_name: String) -> void: + ModLoaderDeprecated.deprecated_changed("ModLoader.log_debug_json_print", "ModLoaderLog.debug_json_print", "6.0.0") + ModLoaderLog.debug_json_print(message, json_printable, mod_name) diff --git a/addons/mod_loader/internal/path.gd b/addons/mod_loader/internal/path.gd new file mode 100644 index 00000000..87a2168a --- /dev/null +++ b/addons/mod_loader/internal/path.gd @@ -0,0 +1,124 @@ +class_name _ModLoaderPath +extends Reference + + +# This Class provides util functions for working with paths. +# Currently all of the included functions are internal and should only be used by the mod loader itself. + +const LOG_NAME := "ModLoader:Path" +const MOD_CONFIG_DIR_PATH := "user://configs" + + +# Get the path to a local folder. Primarily used to get the (packed) mods +# folder, ie "res://mods" or the OS's equivalent, as well as the configs path +static func get_local_folder_dir(subfolder: String = "") -> String: + var game_install_directory := OS.get_executable_path().get_base_dir() + + if OS.get_name() == "OSX": + game_install_directory = game_install_directory.get_base_dir().get_base_dir() + + # Fix for running the game through the Godot editor (as the EXE path would be + # the editor's own EXE, which won't have any mod ZIPs) + # if OS.is_debug_build(): + if OS.has_feature("editor"): + game_install_directory = "res://" + + return game_install_directory.plus_file(subfolder) + + +# Get the path where override.cfg will be stored. +# Not the same as the local folder dir (for mac) +static func get_override_path() -> String: + var base_path := "" + if OS.has_feature("editor"): + base_path = ProjectSettings.globalize_path("res://") + else: + # this is technically different to res:// in macos, but we want the + # executable dir anyway, so it is exactly what we need + base_path = OS.get_executable_path().get_base_dir() + + return base_path.plus_file("override.cfg") + + +# Provide a path, get the file name at the end of the path +static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String: + var file_name := path.get_file() + + if make_lower_case: + file_name = file_name.to_lower() + + if remove_extension: + file_name = file_name.trim_suffix("." + file_name.get_extension()) + + return file_name + + +# 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. +# Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e +static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_regex := false) -> PoolStringArray: + var data: PoolStringArray = [] + var regex: RegEx + if p_match_is_regex: + regex = RegEx.new() + var _compile_error: int = regex.compile(p_match) + if not regex.is_valid(): + return data + + var dirs := [p_dir] + var first := true + while not dirs.empty(): + var dir := Directory.new() + var dir_name: String = dirs.back() + dirs.pop_back() + + if dir.open(dir_name) == OK: + var _dirlist_error: int = dir.list_dir_begin() + var file_name := dir.get_next() + while file_name != "": + if not dir_name == "res://": + first = false + # ignore hidden, temporary, or system content + if not file_name.begins_with(".") and not file_name.get_extension() in ["tmp", "import"]: + # If a directory, then add to list of directories to visit + if dir.current_is_dir(): + dirs.push_back(dir.get_current_dir().plus_file(file_name)) + # If a file, check if we already have a record for the same name + else: + var path := dir.get_current_dir() + ("/" if not first else "") + file_name + # grab all + if not p_match: + data.append(path) + # grab matching strings + elif not p_match_is_regex and file_name.find(p_match, 0) != -1: + data.append(path) + # grab matching regex + else: + var regex_match := regex.search(path) + if regex_match != null: + data.append(path) + # Move on to the next file in this directory + file_name = dir.get_next() + # We've exhausted all files in this directory. Close the iterator. + dir.list_dir_end() + return data + + +# 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 + + +# Get the path to the configs folder, with any applicable overrides applied +static func get_path_to_configs() -> String: + var configs_path := MOD_CONFIG_DIR_PATH + if ModLoaderStore: + if ModLoaderStore.ml_options.override_path_to_configs: + configs_path = ModLoaderStore.ml_options.override_path_to_configs + return configs_path + diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index 56addd13..3a6cbc5d 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -59,7 +59,7 @@ var mod_missing_dependencies := {} func _init() -> void: # if mods are not enabled - don't load mods - if REQUIRE_CMD_LINE and not ModLoaderUtils.is_running_with_command_line_arg("--enable-mods"): + if REQUIRE_CMD_LINE and not _ModLoaderCLI.is_running_with_command_line_arg("--enable-mods"): return # Rotate the log files once on startup. Can't be checked in utils, since it's static @@ -69,10 +69,10 @@ func _init() -> void: _check_autoload_positions() # Log the autoloads order. Helpful when providing support to players - ModLoaderLog.debug_json_print("Autoload order", ModLoaderUtils.get_autoload_array(), LOG_NAME) + ModLoaderLog.debug_json_print("Autoload order", _ModLoaderGodot._get_autoload_array(), LOG_NAME) # Log game install dir - ModLoaderLog.info("game_install_directory: %s" % ModLoaderUtils.get_local_folder_dir(), LOG_NAME) + ModLoaderLog.info("game_install_directory: %s" % _ModLoaderPath.get_local_folder_dir(), LOG_NAME) if not ModLoaderStore.ml_options.enable_mods: ModLoaderLog.info("Mods are currently disabled", LOG_NAME) @@ -190,14 +190,14 @@ func _reset_mods() -> void: func _check_autoload_positions() -> void: # 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 - var override_cfg_path := ModLoaderUtils.get_override_path() - var is_override_cfg_setup := ModLoaderUtils.file_exists(override_cfg_path) + var override_cfg_path := _ModLoaderPath.get_override_path() + var is_override_cfg_setup := _ModLoaderFile.file_exists(override_cfg_path) if is_override_cfg_setup: ModLoaderLog.info("override.cfg setup detected, ModLoader will be the last autoload loaded.", LOG_NAME) return - var _pos_ml_store := ModLoaderGodot.check_autoload_position("ModLoaderStore", 0, true) - var _pos_ml_core := ModLoaderGodot.check_autoload_position("ModLoader", 1, true) + var _pos_ml_store := _ModLoaderGodot.check_autoload_position("ModLoaderStore", 0, true) + var _pos_ml_core := _ModLoaderGodot.check_autoload_position("ModLoader", 1, true) # Loop over "res://mods" and add any mod zips to the unpacked virtual directory @@ -206,7 +206,7 @@ func _load_mod_zips() -> int: var zipped_mods_count := 0 if not ModLoaderStore.ml_options.steam_workshop_enabled: - var mods_folder_path := ModLoaderUtils.get_path_to_mods() + 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) @@ -375,11 +375,11 @@ func _setup_mods() -> int: # Load mod config JSONs from res://configs func _load_mod_configs() -> void: var found_configs_count := 0 - var configs_path := ModLoaderUtils.get_path_to_configs() + var configs_path := _ModLoaderPath.get_path_to_configs() for dir_name in ModLoaderStore.mod_data: var json_path := configs_path.plus_file(dir_name + ".json") - var mod_config := ModLoaderUtils.get_json_as_dict(json_path) + var mod_config := _ModLoaderFile.get_json_as_dict(json_path) ModLoaderLog.debug("Config JSON: Looking for config at path: %s" % json_path, LOG_NAME) @@ -398,7 +398,7 @@ func _load_mod_configs() -> void: var new_path: String = mod_config.load_from if not new_path == "" and not new_path == str(dir_name, ".json"): ModLoaderLog.info("Config JSON: Following load_from path: %s" % new_path, LOG_NAME) - var new_config := ModLoaderUtils.get_json_as_dict(configs_path + new_path) + var new_config := _ModLoaderFile.get_json_as_dict(configs_path + new_path) if new_config.size() > 0: mod_config = new_config ModLoaderLog.info("Config JSON: Loaded from custom json: %s" % new_path, LOG_NAME) @@ -419,7 +419,7 @@ func _load_mod_configs() -> void: # 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 := ModLoaderUtils.get_file_name_from_path(mod_folder_path, false, true) + 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 := UNPACKED_DIR.plus_file(dir_name) @@ -427,7 +427,7 @@ func _init_mod_data(mod_folder_path: String) -> void: var mod := ModData.new(local_mod_path) mod.dir_name = dir_name var mod_overwrites_path := mod.get_optional_mod_file_path(ModData.optional_mod_files.OVERWRITES) - mod.is_overwrite = ModLoaderUtils.file_exists(mod_overwrites_path) + mod.is_overwrite = _ModLoaderFile.file_exists(mod_overwrites_path) ModLoaderStore.mod_data[dir_name] = mod # Get the mod file paths @@ -436,7 +436,7 @@ func _init_mod_data(mod_folder_path: String) -> void: # operation if a mod has a large number of files (eg. Brotato's Invasion mod, # which has ~1,000 files). That's why it's disabled by default if DEBUG_ENABLE_STORING_FILEPATHS: - mod.file_paths = ModLoaderUtils.get_flat_view_dict(local_mod_path) + mod.file_paths = _ModLoaderPath.get_flat_view_dict(local_mod_path) # Run dependency checks on a mod, checking any dependencies it lists in its @@ -592,7 +592,7 @@ func _handle_script_extensions()->void: var script_extension_data_array := [] for extension_path in ModLoaderStore.script_extensions: - if not File.new().file_exists(extension_path): + if not _ModLoaderFile.file_exists(extension_path): ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) continue @@ -686,7 +686,7 @@ func _reload_vanilla_child_classes_for(script:Script)->void: func _apply_extension(extension_path)->Script: # Check path to file exists - if not File.new().file_exists(extension_path): + if not _ModLoaderFile.file_exists(extension_path): ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) return null @@ -727,7 +727,7 @@ func _apply_extension(extension_path)->Script: # Used to remove a specific extension func _remove_specific_extension_from_script(extension_path: String) -> void: # Check path to file exists - if not ModLoaderUtils.file_exists(extension_path): + if not _ModLoaderFile.file_exists(extension_path): ModLoaderLog.error("The extension script path \"%s\" does not exist" % [extension_path], LOG_NAME) return @@ -772,7 +772,7 @@ func _remove_specific_extension_from_script(extension_path: String) -> void: # Used to fully reset the provided script to a state prior of any extension func _remove_all_extensions_from_script(parent_script_path: String) -> void: # Check path to file exists - if not ModLoaderUtils.file_exists(parent_script_path): + if not _ModLoaderFile.file_exists(parent_script_path): ModLoaderLog.error("The parent script path \"%s\" does not exist" % [parent_script_path], LOG_NAME) return diff --git a/addons/mod_loader/mod_loader_setup.gd b/addons/mod_loader/mod_loader_setup.gd index 5e4cfe65..3252ccf5 100644 --- a/addons/mod_loader/mod_loader_setup.gd +++ b/addons/mod_loader/mod_loader_setup.gd @@ -57,7 +57,7 @@ const new_global_classes := [ "path": "res://addons/mod_loader/api/mod.gd" }, { "base": "Object", - "class": "ModLoaderGodot", + "class": "_ModLoaderGodot", "language": "GDScript", "path": "res://addons/mod_loader/api/godot.gd" }, { @@ -235,4 +235,3 @@ func setup_file_data() -> void: ModLoaderSetupLog.debug_json_print("path: ", path, LOG_NAME) ModLoaderSetupLog.debug_json_print("file_name: ", file_name, LOG_NAME) - \ No newline at end of file diff --git a/addons/mod_loader/mod_loader_store.gd b/addons/mod_loader/mod_loader_store.gd index 53bc3244..ac096833 100644 --- a/addons/mod_loader/mod_loader_store.gd +++ b/addons/mod_loader/mod_loader_store.gd @@ -75,8 +75,8 @@ var ml_options := { # Only applied if custom settings are provided, either via the options.tres # resource, or via CLI args. Note that CLI args can be tested in the editor # via: Project Settings > Display> Editor > Main Run Args - override_path_to_mods = "", # Default if unspecified: "res://mods" -- get with ModLoaderUtils.get_path_to_mods() - override_path_to_configs = "", # Default if unspecified: "res://configs" -- get with ModLoaderUtils.get_path_to_configs() + override_path_to_mods = "", # Default if unspecified: "res://mods" -- get with _ModLoaderPath.get_path_to_mods() + override_path_to_configs = "", # Default if unspecified: "res://configs" -- get with _ModLoaderPath.get_path_to_configs() # Can be used in the editor to load mods from your Steam workshop directory override_path_to_workshop = "", @@ -106,7 +106,7 @@ func _update_ml_options_from_options_resource() -> void: var ml_options_path := "res://addons/mod_loader/options/options.tres" # Get user options for ModLoader - if File.new().file_exists(ml_options_path): + if _ModLoaderFile.file_exists(ml_options_path): var options_resource := load(ml_options_path) if not options_resource.current_options == null: var current_options: Resource = options_resource.current_options @@ -120,13 +120,13 @@ func _update_ml_options_from_options_resource() -> void: # Update ModLoader's options, via CLI args func _update_ml_options_from_cli_args() -> void: # Disable mods - if ModLoaderUtils.is_running_with_command_line_arg("--disable-mods"): + if _ModLoaderCLI.is_running_with_command_line_arg("--disable-mods"): ml_options.enable_mods = false # Override paths to mods # Set via: --mods-path # Example: --mods-path="C://path/mods" - var cmd_line_mod_path := ModLoaderUtils.get_cmd_line_arg_value("--mods-path") + var cmd_line_mod_path := _ModLoaderCLI.get_cmd_line_arg_value("--mods-path") if cmd_line_mod_path: ml_options.override_path_to_mods = cmd_line_mod_path ModLoaderLog.info("The path mods are loaded from has been changed via the CLI arg `--mods-path`, to: " + cmd_line_mod_path, LOG_NAME) @@ -134,20 +134,20 @@ func _update_ml_options_from_cli_args() -> void: # Override paths to configs # Set via: --configs-path # Example: --configs-path="C://path/configs" - var cmd_line_configs_path := ModLoaderUtils.get_cmd_line_arg_value("--configs-path") + var cmd_line_configs_path := _ModLoaderCLI.get_cmd_line_arg_value("--configs-path") if cmd_line_configs_path: ml_options.override_path_to_configs = cmd_line_configs_path ModLoaderLog.info("The path configs are loaded from has been changed via the CLI arg `--configs-path`, to: " + cmd_line_configs_path, LOG_NAME) # Log level verbosity - if ModLoaderUtils.is_running_with_command_line_arg("-vvv") or ModLoaderUtils.is_running_with_command_line_arg("--log-debug"): + if _ModLoaderCLI.is_running_with_command_line_arg("-vvv") or _ModLoaderCLI.is_running_with_command_line_arg("--log-debug"): ml_options.log_level = ModLoaderLog.VERBOSITY_LEVEL.DEBUG - elif ModLoaderUtils.is_running_with_command_line_arg("-vv") or ModLoaderUtils.is_running_with_command_line_arg("--log-info"): + elif _ModLoaderCLI.is_running_with_command_line_arg("-vv") or _ModLoaderCLI.is_running_with_command_line_arg("--log-info"): ml_options.log_level = ModLoaderLog.VERBOSITY_LEVEL.INFO - elif ModLoaderUtils.is_running_with_command_line_arg("-v") or ModLoaderUtils.is_running_with_command_line_arg("--log-warning"): + elif _ModLoaderCLI.is_running_with_command_line_arg("-v") or _ModLoaderCLI.is_running_with_command_line_arg("--log-warning"): ml_options.log_level = ModLoaderLog.VERBOSITY_LEVEL.WARNING # Ignored mod_names in log - var ignore_mod_names := ModLoaderUtils.get_cmd_line_arg_value("--log-ignore") + var ignore_mod_names := _ModLoaderCLI.get_cmd_line_arg_value("--log-ignore") if not ignore_mod_names == "": ml_options.ignored_mod_names_in_log = ignore_mod_names.split(",") diff --git a/addons/mod_loader/mod_loader_utils.gd b/addons/mod_loader/mod_loader_utils.gd deleted file mode 100644 index 601a0dd8..00000000 --- a/addons/mod_loader/mod_loader_utils.gd +++ /dev/null @@ -1,439 +0,0 @@ -class_name ModLoaderUtils -extends Node - - -const LOG_NAME := "ModLoader:ModLoaderUtils" -const MOD_CONFIG_DIR_PATH := "user://configs" - - -# Logs the error in red and a stack trace. Prefixed FATAL-ERROR -# Stops the execution in editor -# Always logged -static func log_fatal(message: String, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_fatal", "ModLoaderLog.fatal", "6.0.0") - ModLoaderLog.fatal(message, mod_name) - - -# Logs the message and pushed an error. Prefixed ERROR -# Always logged -static func log_error(message: String, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_error", "ModLoaderLog.error", "6.0.0") - ModLoaderLog.error(message, mod_name) - - -# Logs the message and pushes a warning. Prefixed WARNING -# Logged with verbosity level at or above warning (-v) -static func log_warning(message: String, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_warning", "ModLoaderLog.warning", "6.0.0") - ModLoaderLog.warning(message, mod_name) - - -# Logs the message. Prefixed INFO -# Logged with verbosity level at or above info (-vv) -static func log_info(message: String, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_info", "ModLoaderLog.info", "6.0.0") - ModLoaderLog.info(message, mod_name) - - -# Logs the message. Prefixed SUCCESS -# Logged with verbosity level at or above info (-vv) -static func log_success(message: String, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_success", "ModLoaderLog.success", "6.0.0") - ModLoaderLog.success(message, mod_name) - - -# Logs the message. Prefixed DEBUG -# Logged with verbosity level at or above debug (-vvv) -static func log_debug(message: String, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_debug", "ModLoaderLog.debug", "6.0.0") - ModLoaderLog.debug(message, mod_name) - - -# Logs the message formatted with [method JSON.print]. Prefixed DEBUG -# Logged with verbosity level at or above debug (-vvv) -static func log_debug_json_print(message: String, json_printable, mod_name: String) -> void: - ModLoaderDeprecated.deprecated_changed("ModLoader.log_debug_json_print", "ModLoaderLog.debug_json_print", "6.0.0") - ModLoaderLog.debug_json_print(message, json_printable, mod_name) - - -# 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 -# modders in understanding and troubleshooting issues -static func code_note(_msg:String): - pass - - -# Check if the provided command line argument was present when launching the game -static func is_running_with_command_line_arg(argument: String) -> bool: - for arg in OS.get_cmdline_args(): - if argument == arg.split("=")[0]: - return true - - return false - - -# Get the command line argument value if present when launching the game -static func get_cmd_line_arg_value(argument: String) -> String: - var args := get_fixed_cmdline_args() - - for arg_index in args.size(): - var arg := args[arg_index] as String - - var key := arg.split("=")[0] - if key == argument: - # format: `--arg=value` or `--arg="value"` - if "=" in arg: - var value := arg.trim_prefix(argument + "=") - value = value.trim_prefix('"').trim_suffix('"') - value = value.trim_prefix("'").trim_suffix("'") - return value - - # format: `--arg value` or `--arg "value"` - elif arg_index +1 < args.size() and not args[arg_index +1].begins_with("--"): - return args[arg_index + 1] - - return "" - - -static func get_fixed_cmdline_args() -> PoolStringArray: - return fix_godot_cmdline_args_string_space_splitting(OS.get_cmdline_args()) - - -# Reverses a bug in Godot, which splits input strings at spaces even if they are quoted -# e.g. `--arg="some value" --arg-two 'more value'` becomes `[ --arg="some, value", --arg-two, 'more, value' ]` -static func fix_godot_cmdline_args_string_space_splitting(args: PoolStringArray) -> PoolStringArray: - if not OS.has_feature("editor"): # only happens in editor builds - return args - if OS.has_feature("Windows"): # windows is unaffected - return args - - var fixed_args := PoolStringArray([]) - var fixed_arg := "" - # if we encounter an argument that contains `=` followed by a quote, - # or an argument that starts with a quote, take all following args and - # concatenate them into one, until we find the closing quote - for arg in args: - var arg_string := arg as String - if '="' in arg_string or '="' in fixed_arg or \ - arg_string.begins_with('"') or fixed_arg.begins_with('"'): - if not fixed_arg == "": - fixed_arg += " " - fixed_arg += arg_string - if arg_string.ends_with('"'): - fixed_args.append(fixed_arg.trim_prefix(" ")) - fixed_arg = "" - continue - # same thing for single quotes - elif "='" in arg_string or "='" in fixed_arg \ - or arg_string.begins_with("'") or fixed_arg.begins_with("'"): - if not fixed_arg == "": - fixed_arg += " " - fixed_arg += arg_string - if arg_string.ends_with("'"): - fixed_args.append(fixed_arg.trim_prefix(" ")) - fixed_arg = "" - continue - - else: - fixed_args.append(arg_string) - - return fixed_args - - -# Get the path to a local folder. Primarily used to get the (packed) mods -# folder, ie "res://mods" or the OS's equivalent, as well as the configs path -static func get_local_folder_dir(subfolder: String = "") -> String: - var game_install_directory := OS.get_executable_path().get_base_dir() - - if OS.get_name() == "OSX": - game_install_directory = game_install_directory.get_base_dir().get_base_dir() - - # Fix for running the game through the Godot editor (as the EXE path would be - # the editor's own EXE, which won't have any mod ZIPs) - # if OS.is_debug_build(): - if OS.has_feature("editor"): - game_install_directory = "res://" - - return game_install_directory.plus_file(subfolder) - - -# Get the path where override.cfg will be stored. -# Not the same as the local folder dir (for mac) -static func get_override_path() -> String: - var base_path := "" - if OS.has_feature("editor"): - base_path = ProjectSettings.globalize_path("res://") - else: - # this is technically different to res:// in macos, but we want the - # executable dir anyway, so it is exactly what we need - base_path = OS.get_executable_path().get_base_dir() - - return base_path.plus_file("override.cfg") - - -# Provide a path, get the file name at the end of the path -static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String: - var file_name := path.get_file() - - if make_lower_case: - file_name = file_name.to_lower() - - if remove_extension: - file_name = file_name.trim_suffix("." + file_name.get_extension()) - - return file_name - - -# Parses JSON from a given file path and returns a [Dictionary]. -# Returns an empty [Dictionary] if no file exists (check with size() < 1) -static func get_json_as_dict(path: String) -> Dictionary: - var file := File.new() - - if !file.file_exists(path): - file.close() - return {} - - var error = file.open(path, File.READ) - if not error == OK: - ModLoaderLog.error("Error opening file. Code: %s" % error, LOG_NAME) - - var content := file.get_as_text() - return get_json_string_as_dict(content) - - -# Parses JSON from a given [String] and returns a [Dictionary]. -# Returns an empty [Dictionary] on error (check with size() < 1) -static func get_json_string_as_dict(string: String) -> Dictionary: - if string == "": - return {} - var parsed := JSON.parse(string) - if parsed.error: - ModLoaderLog.error("Error parsing JSON", LOG_NAME) - return {} - if not parsed.result is Dictionary: - ModLoaderLog.error("JSON is not a dictionary", LOG_NAME) - return {} - return parsed.result - - -static func file_exists(path: String) -> bool: - var file = File.new() - return file.file_exists(path) - - -static func dir_exists(path: String) -> bool: - var dir = Directory.new() - return dir.dir_exists(path) - - -# Returns an empty String if the key does not exist or is not type of String -static func get_string_from_dict(dict: Dictionary, key: String) -> String: - if not dict.has(key): - return "" - - if not dict[key] is String: - return "" - - return dict[key] - - -# Returns an empty Array if the key does not exist or is not type of Array -static func get_array_from_dict(dict: Dictionary, key: String) -> Array: - if not dict.has(key): - return [] - - if not dict[key] is Array: - return [] - - return dict[key] - - -# 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 - - for key in dict.keys(): - if(required_fields.has(key)): - missing_fields.erase(key) - - if missing_fields.size() > 0: - ModLoaderLog.fatal("Mod manifest is missing required fields: %s" % missing_fields, LOG_NAME) - return false - - return true - - -# 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") - var registered_class_icons: Dictionary = ProjectSettings.get_setting("_global_script_class_icons") - - for new_class in new_global_classes: - if not is_valid_global_class_dict(new_class): - continue - for old_class in registered_classes: - if old_class.class == new_class.class: - if OS.has_feature("editor"): - ModLoaderLog.info('Class "%s" to be registered as global was already registered by the editor. Skipping.' % new_class.class, LOG_NAME) - else: - ModLoaderLog.info('Class "%s" to be registered as global already exists. Skipping.' % new_class.class, LOG_NAME) - continue - - registered_classes.append(new_class) - registered_class_icons[new_class.class] = "" # empty icon, does not matter - - ProjectSettings.set_setting("_global_script_classes", registered_classes) - ProjectSettings.set_setting("_global_script_class_icons", registered_class_icons) - - -# Checks if all required fields are in the given [Dictionary] -# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" } -static func is_valid_global_class_dict(global_class_dict: Dictionary) -> bool: - var required_fields := ["base", "class", "language", "path"] - if not global_class_dict.has_all(required_fields): - ModLoaderLog.fatal("Global class to be registered is missing one of %s" % required_fields, LOG_NAME) - return false - - var file = File.new() - if not file.file_exists(global_class_dict.path): - ModLoaderLog.fatal('Class "%s" to be registered as global could not be found at given path "%s"' % - [global_class_dict.class, global_class_dict.path], LOG_NAME) - return false - - return true - - -# Get an array of all autoloads -> ["autoload/AutoloadName", ...] -static func get_autoload_array() -> Array: - var autoloads := [] - - # Get all autoload settings - for prop in ProjectSettings.get_property_list(): - var name: String = prop.name - if name.begins_with("autoload/"): - autoloads.append(name.trim_prefix("autoload/")) - - return autoloads - - -# Get the index of a specific autoload -static func get_autoload_index(autoload_name: String) -> int: - var autoloads := get_autoload_array() - var autoload_index := autoloads.find(autoload_name) - - return autoload_index - - -# 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. -# Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e -static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_regex := false) -> PoolStringArray: - var data: PoolStringArray = [] - var regex: RegEx - if p_match_is_regex: - regex = RegEx.new() - var _compile_error: int = regex.compile(p_match) - if not regex.is_valid(): - return data - - var dirs := [p_dir] - var first := true - while not dirs.empty(): - var dir := Directory.new() - var dir_name: String = dirs.back() - dirs.pop_back() - - if dir.open(dir_name) == OK: - var _dirlist_error: int = dir.list_dir_begin() - var file_name := dir.get_next() - while file_name != "": - if not dir_name == "res://": - first = false - # ignore hidden, temporary, or system content - if not file_name.begins_with(".") and not file_name.get_extension() in ["tmp", "import"]: - # If a directory, then add to list of directories to visit - if dir.current_is_dir(): - dirs.push_back(dir.get_current_dir().plus_file(file_name)) - # If a file, check if we already have a record for the same name - else: - var path := dir.get_current_dir() + ("/" if not first else "") + file_name - # grab all - if not p_match: - data.append(path) - # grab matching strings - elif not p_match_is_regex and file_name.find(p_match, 0) != -1: - data.append(path) - # grab matching regex - else: - var regex_match := regex.search(path) - if regex_match != null: - data.append(path) - # Move on to the next file in this directory - file_name = dir.get_next() - # We've exhausted all files in this directory. Close the iterator. - dir.list_dir_end() - return data - - -# Saving (Files) -# ============================================================================= - -# Saves a dictionary to a file, as a JSON string -static func save_string_to_file(save_string: String, filepath: String) -> bool: - # Create directory if it doesn't exist yet - var file_directory := filepath.get_base_dir() - var dir := Directory.new() - - code_note(str( - "View error codes here:", - "https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-error" - )) - - if not dir.dir_exists(file_directory): - var makedir_error = dir.make_dir_recursive(file_directory) - if not makedir_error == OK: - ModLoaderLog.fatal("Encountered an error (%s) when attempting to create a directory, with the path: %s" % [makedir_error, file_directory], LOG_NAME) - return false - - var file = File.new() - - # Save data to the file - var fileopen_error = file.open(filepath, File.WRITE) - - if not fileopen_error == OK: - ModLoaderLog.fatal("Encountered an error (%s) when attempting to write to a file, with the path: %s" % [fileopen_error, filepath], LOG_NAME) - return false - - file.store_string(save_string) - file.close() - - return true - - -# Saves a dictionary to a file, as a JSON string -static func save_dictionary_to_json_file(data: Dictionary, filepath: String) -> bool: - var json_string = JSON.print(data, "\t") - return save_string_to_file(json_string, filepath) - - -# 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") - if ModLoaderStore: - if ModLoaderStore.ml_options.override_path_to_mods: - mods_folder_path = ModLoaderStore.ml_options.override_path_to_mods - return mods_folder_path - - -# Get the path to the configs folder, with any applicable overrides applied -static func get_path_to_configs() -> String: - var configs_path := MOD_CONFIG_DIR_PATH - if ModLoaderStore: - if ModLoaderStore.ml_options.override_path_to_configs: - configs_path = ModLoaderStore.ml_options.override_path_to_configs - return configs_path