Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ ModLoaderStore: New singleton to store data #172

Merged
merged 8 commits into from
Mar 9, 2023
10 changes: 3 additions & 7 deletions addons/mod_loader/api/config.gd
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ static func get_mod_config(mod_dir_name: String = "", key: String = "") -> Dicti
# No user config file exists. Low importance as very likely to trigger
var full_msg = "Config JSON Notice: %s" % status_msg
# Only log this once, to avoid flooding the log
if not ModLoader.logged_messages.has(full_msg):
if not ModLoaderStore.logged_messages.has(full_msg):
ModLoaderUtils.log_debug(full_msg, mod_dir_name)
ModLoader.logged_messages.push_back(full_msg)
ModLoaderStore.logged_messages.push_back(full_msg)
else:
# Code error (eg. invalid mod ID)
ModLoaderUtils.log_fatal("Config JSON Error (%s): %s" % [status_code, status_msg], mod_dir_name)
Expand Down Expand Up @@ -119,11 +119,7 @@ static func save_mod_config_dictionary(mod_id: String, data: Dictionary, update_
data_new = data_original.duplicate(true)
data_new.merge(data, true)

# Note: This bit of code is duped from `_load_mod_configs`
var configs_path := ModLoaderUtils.get_local_folder_dir("configs")
if not ModLoader.os_configs_path_override == "":
configs_path = ModLoader.os_configs_path_override

var configs_path := ModLoaderUtils.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)
Expand Down
26 changes: 26 additions & 0 deletions addons/mod_loader/api/godot.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class_name ModLoaderGodot
extends Object

# API methods for interacting with Godot

const LOG_NAME := "ModLoader:Godot"


# Check the index position of the provided autoload (0 = 1st, 1 = 2nd, etc).
# 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:
ithinkandicode marked this conversation as resolved.
Show resolved Hide resolved
var autoload_array := ModLoaderUtils.get_autoload_array()
var autoload_index := autoload_array.find(autoload_name)
var position_matches := autoload_index == position_index

if not position_matches and trigger_error:
var error_msg := "Expected %s to be the autoload in position %s, but this is currently %s." % [autoload_name, str(position_index + 1), autoload_array[position_index]]
var help_msg := ""

if OS.has_feature("editor"):
help_msg = " To configure your autoloads, go to Project > Project Settings > Autoload."

ModLoaderUtils.log_fatal(error_msg + help_msg, LOG_NAME)

return position_matches
5 changes: 4 additions & 1 deletion addons/mod_loader/api/third_party/steam.gd
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam"
# Eg. Brotato:
# GAME = Steam/steamapps/common/Brotato
# WORKSHOP = Steam/steamapps/workshop/content/1942280
static func get_steam_workshop_dir() -> String:
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 path := ""

Expand Down
10 changes: 5 additions & 5 deletions addons/mod_loader/classes/options_profile.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ extends Resource
# export (Array, Resource) var elites: = []

export (bool) var enable_mods = true
export (ModLoaderUtils.verbosity_level) var log_level: = ModLoaderUtils.verbosity_level.DEBUG
export (String, DIR) var path_to_mods = "res://mods"
export (String, DIR) var path_to_configs = "res://configs"
export (bool) var steam_workshop_enabled = false
export (String, DIR) var steam_workshop_path_override = ""
export (ModLoaderUtils.VERBOSITY_LEVEL) var log_level: = ModLoaderUtils.VERBOSITY_LEVEL.DEBUG
export (Array, String) var disabled_mods = []
export (bool) var steam_workshop_enabled = false
export (String, DIR) var override_path_to_mods = ""
export (String, DIR) var override_path_to_configs = ""
export (String, DIR) var override_path_to_workshop = ""
Qubus0 marked this conversation as resolved.
Show resolved Hide resolved
138 changes: 22 additions & 116 deletions addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,6 @@ var mod_data := {}
# Order for mods to be loaded in, set by `_get_load_order`
var mod_load_order := []

# Override for the path mods are loaded from. Only set if the CLI arg is present.
# Can be tested in the editor via: Project Settings > Display> Editor > Main Run Args
# Default: "res://mods"
# Set via: --mods-path
# Example: --mods-path="C://path/mods"
var os_mods_path_override := ""

# Override for the path config JSONs are loaded from
# Default: "res://configs"
# Set via: --configs-path
# Example: --configs-path="C://path/configs"
var os_configs_path_override := ""

# Any mods that are missing their dependancies are added to this
# Example property: "mod_id": ["dep_mod_id_0", "dep_mod_id_2"]
var mod_missing_dependencies := {}
Expand All @@ -87,73 +74,30 @@ var loaded_vanilla_parents_cache := {}
# Helps to decide whether a script extension should go through the _handle_script_extensions process
var is_initializing := true

# True if ModLoader has displayed the warning about using zipped mods
var has_shown_editor_warning := false

# Keeps track of logged messages, to avoid flooding the log with duplicate notices
var logged_messages := []

# Path to the options resource
# See: res://addons/mod_loader/options/options_current_data.gd
var ml_options_path := "res://addons/mod_loader/options/options.tres"

# These variables handle various options, which can be changed via Godot's GUI
# by adding a ModLoaderOptions resource to the resource file specified by
# `ml_options_path`. See res://addons/mod_loader/options_examples for some
# resource files you can add to the options_curent file.
# See: res://addons/mod_loader/options/classes/options_profile.gd
var ml_options := {
enable_mods = true,
log_level = ModLoaderUtils.verbosity_level.DEBUG,
path_to_mods = "res://mods",
path_to_configs = "res://configs",

# If true, ModLoader will load mod ZIPs from the Steam workshop directory,
# instead of the default location (res://mods)
steam_workshop_enabled = false,

# Can be used in the editor to load mods from your Steam workshop directory
steam_workshop_path_override = "",

# Array of mod ID strings to skip in `_setup_mods`
disabled_mods = []
}


# Main
# =============================================================================

func _init() -> void:
_update_ml_options()

# if mods are not enabled - don't load mods
if REQUIRE_CMD_LINE and not ModLoaderUtils.is_running_with_command_line_arg("--enable-mods"):
Qubus0 marked this conversation as resolved.
Show resolved Hide resolved
return

if not ml_options.enable_mods:
ModLoaderUtils.log_info("Mods are currently disabled", LOG_NAME)
return

# Rotate the log files once on startup. Can't be checked in utils, since it's static
ModLoaderUtils.rotate_log_file()

# Ensure ModLoader is the first autoload
_check_first_autoload()
# Ensure ModLoaderStore and ModLoader are the 1st and 2nd autoloads
_check_autoload_positions()
ithinkandicode marked this conversation as resolved.
Show resolved Hide resolved

# Log the autoloads order. Helpful when providing support to players
ModLoaderUtils.log_debug_json_print("Autoload order", ModLoaderUtils.get_autoload_array(), LOG_NAME)

# Log game install dir
ModLoaderUtils.log_info("game_install_directory: %s" % ModLoaderUtils.get_local_folder_dir(), LOG_NAME)

# check if we want to use a different mods path that is provided as a command line argument
var cmd_line_mod_path := ModLoaderUtils.get_cmd_line_arg_value("--mods-path")
if not cmd_line_mod_path == "":
os_mods_path_override = cmd_line_mod_path
ModLoaderUtils.log_info("The path mods are loaded from has been changed via the CLI arg `--mods-path`, to: " + cmd_line_mod_path, LOG_NAME)

# Check for the CLI arg that overrides the configs path
var cmd_line_configs_path := ModLoaderUtils.get_cmd_line_arg_value("--configs-path")
if not cmd_line_configs_path == "":
os_configs_path_override = cmd_line_configs_path
ModLoaderUtils.log_info("The path configs are loaded from has been changed via the CLI arg `--configs-path`, to: " + cmd_line_configs_path, LOG_NAME)
if not ModLoaderStore.ml_options.enable_mods:
ModLoaderUtils.log_info("Mods are currently disabled", LOG_NAME)
return

# Loop over "res://mods" and add any mod zips to the unpacked virtual
# directory (UNPACKED_DIR)
Expand Down Expand Up @@ -220,58 +164,28 @@ func _init() -> void:
is_initializing = false


# Update ModLoader's options, via the custom options resource
func _update_ml_options() -> void:
# Get user options for ModLoader
if File.new().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
# Update from the options in the resource
for key in ml_options:
ml_options[key] = current_options[key]
else:
ModLoaderUtils.log_fatal(str("A critical file is missing: ", ml_options_path), LOG_NAME)


# Ensure ModLoader is the first autoload
func _check_first_autoload() -> void:
var autoload_array = ModLoaderUtils.get_autoload_array()
var mod_loader_index = autoload_array.find("ModLoader")
var is_mod_loader_first = mod_loader_index == 0

var override_cfg_path = ModLoaderUtils.get_override_path()
var is_override_cfg_setup = ModLoaderUtils.file_exists(override_cfg_path)

# Log the autoloads order. Might seem superflous but could help when providing support
ModLoaderUtils.log_debug_json_print("Autoload order", autoload_array, LOG_NAME)

# Check autoload positions:
# Ensure 1st autoload is `ModLoaderStore`, and 2nd is `ModLoader`.
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)
if is_override_cfg_setup:
ModLoaderUtils.log_info("override.cfg setup detected, ModLoader will be the last autoload loaded.", LOG_NAME)
return

var base_msg = "ModLoader needs to be the first autoload to work correctly, "
var help_msg = ""

if OS.has_feature("editor"):
help_msg = "To configure your autoloads, go to Project > Project Settings > Autoload, and add ModLoader as the first item. For more info, see the 'Godot Project Setup' page on the ModLoader GitHub wiki."
else:
help_msg = "If you're seeing this error, something must have gone wrong in the setup process."

if not is_mod_loader_first:
ModLoaderUtils.log_fatal(str(base_msg, 'but the first autoload is currently: "%s". ' % autoload_array[0], help_msg), LOG_NAME)
var _pos_ml_store := ModLoaderGodot.check_autoload_position("ModLoaderStore", 0, true)
ithinkandicode marked this conversation as resolved.
Show resolved Hide resolved
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
# (UNPACKED_DIR)
func _load_mod_zips() -> int:
var zipped_mods_count := 0

if not ml_options.steam_workshop_enabled:
# Path to the games mod folder
var mods_folder_path := ModLoaderUtils.get_local_folder_dir("mods")
if not ModLoaderStore.ml_options.steam_workshop_enabled:
var mods_folder_path := ModLoaderUtils.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)
Expand Down Expand Up @@ -326,12 +240,12 @@ func _load_zips_in_folder(folder_path: String) -> int:
# "don't use ZIPs with unpacked mods!"
# https://github.com/godotengine/godot/issues/19815
# https://github.com/godotengine/godot/issues/16798
if OS.has_feature("editor") and not has_shown_editor_warning:
if OS.has_feature("editor") and not ModLoaderStore.has_shown_editor_zips_warning:
ModLoaderUtils.log_warning(str(
"Loading any resource packs (.zip/.pck) with `load_resource_pack` will WIPE the entire virtual res:// directory. ",
"If you have any unpacked mods in ", UNPACKED_DIR, ", they will not be loaded. ",
"Please unpack your mod ZIPs instead, and add them to ", UNPACKED_DIR), LOG_NAME)
has_shown_editor_warning = true
ModLoaderStore.has_shown_editor_zips_warning = true

ModLoaderUtils.log_debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME)

Expand All @@ -355,10 +269,7 @@ func _load_zips_in_folder(folder_path: String) -> int:
# inside each workshop item's folder
func _load_steam_workshop_zips() -> int:
var temp_zipped_mods_count := 0
var workshop_folder_path := ModLoaderSteam.get_steam_workshop_dir()

if not ml_options.steam_workshop_path_override == "":
workshop_folder_path = ml_options.steam_workshop_path_override
var workshop_folder_path := ModLoaderSteam.get_path_to_workshop()

ModLoaderUtils.log_info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME)

Expand Down Expand Up @@ -428,7 +339,7 @@ func _setup_mods() -> int:
if mod_dir_name == "." or mod_dir_name == "..":
continue

if ml_options.disabled_mods.has(mod_dir_name):
if ModLoaderStore.ml_options.disabled_mods.has(mod_dir_name):
ModLoaderUtils.log_info("Skipped setting up mod: \"%s\"" % mod_dir_name, LOG_NAME)
continue

Expand All @@ -443,12 +354,7 @@ 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_local_folder_dir("configs")

# CLI override, set with `--configs-path="C://path/configs"`
# (similar to os_mods_path_override)
if not os_configs_path_override == "":
configs_path = os_configs_path_override
var configs_path := ModLoaderUtils.get_path_to_configs()

for dir_name in mod_data:
var json_path := configs_path.plus_file(dir_name + ".json")
Expand Down
10 changes: 9 additions & 1 deletion addons/mod_loader/mod_loader_setup.gd
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const new_global_classes := [
"class": "ModLoaderDeprecated",
"language": "GDScript",
"path": "res://addons/mod_loader/api/deprecated.gd"
}, {
"base": "Object",
"class": "ModLoaderGodot",
"language": "GDScript",
"path": "res://addons/mod_loader/api/godot.gd"
}, {
"base": "Node",
"class": "ModLoaderSteam",
Expand Down Expand Up @@ -147,7 +152,10 @@ func reorder_autoloads() -> void:
for autoload in original_autoloads.keys():
ProjectSettings.set_setting(autoload, null)

# add ModLoader autoload (the * marks the path as autoload)
# Add ModLoaderStore autoload (the * marks the path as autoload)
ProjectSettings.set_setting("autoload/ModLoaderStore", "*" + "res://addons/mod_loader/mod_loader_store.gd")

# Add ModLoader autoload (the * marks the path as autoload)
ProjectSettings.set_setting("autoload/ModLoader", "*" + "res://addons/mod_loader/mod_loader.gd")

# add all previous autoloads back again
Expand Down
Loading