Skip to content

Commit

Permalink
feat: ✨ ModLoaderStore: New singleton to store data (#172)
Browse files Browse the repository at this point in the history
* ModLoaderStore: Initial, including integration

* ModLoaderStore: Cleanup

* ModLoaderStore: Cleanup [2]

* ModLoaderStore: Cleanup [3]

* Store - Don't store main vars in a dictionary

* Store - Revert to debug log level if ModLoaderStore isn't the first autoload

* Store - Check autoload position sooner
  • Loading branch information
ithinkandicode authored Mar 9, 2023
1 parent 09cf37e commit 4f2ea00
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 169 deletions.
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:
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 = ""
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"):
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()

# 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)
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

0 comments on commit 4f2ea00

Please sign in to comment.