Files
Super-Mario-Bros.-Remastere…/addons/mod_loader/resources/mod_data.gd
2025-09-13 16:30:32 +01:00

221 lines
7.2 KiB
GDScript

class_name ModData
extends Resource
##
## Stores and validates all Data required to load a mod successfully
## If some of the data is invalid, [member is_loadable] will be false
const LOG_NAME := "ModLoader:ModData"
const MOD_MAIN := "mod_main.gd"
const MANIFEST := "manifest.json"
const OVERWRITES := "overwrites.gd"
# These 2 files are always required by mods.
# [i]mod_main.gd[/i] = The main init file for the mod
# [i]manifest.json[/i] = Meta data for the mod, including its dependencies
enum RequiredModFiles {
MOD_MAIN,
MANIFEST,
}
enum OptionalModFiles {
OVERWRITES
}
# Specifies the source from which the mod has been loaded:
# UNPACKED = From the mods-unpacked directory ( only when in the editor ).
# LOCAL = From the local mod zip directory, which by default is ../game_dir/mods.
# STEAM_WORKSHOP = Loaded from ../Steam/steamapps/workshop/content/1234567/[..].
enum Sources {
UNPACKED,
LOCAL,
STEAM_WORKSHOP,
}
## Name of the Mod's zip file
var zip_name := ""
## Path to the Mod's zip file
var zip_path := ""
## Directory of the mod. Has to be identical to [method ModManifest.get_mod_id]
var dir_name := ""
## Path to the mod's unpacked directory
var dir_path := ""
## False if any data is invalid
var is_loadable := true
## True if overwrites.gd exists
var is_overwrite := false
## True if mod can't be disabled or enabled in a user profile
var is_locked := false
## Flag indicating whether the mod should be loaded
var is_active := true
## Is increased for every mod depending on this mod. Highest importance is loaded first
var importance := 0
## Contents of the manifest
var manifest: ModManifest
# Updated in load_configs
## All mod configs
var configs := {}
## The currently applied mod config
var current_config: ModConfig: set = _set_current_config
## Specifies the source from which the mod has been loaded
var source: int
var load_errors: Array[String] = []
var load_warnings: Array[String] = []
func _init(_manifest: ModManifest, path: String) -> void:
manifest = _manifest
if _ModLoaderPath.is_zip(path):
zip_name = _ModLoaderPath.get_file_name_from_path(path)
zip_path = path
# Use the dir name of the passed path instead of the manifest data so we can validate
# the mod dir has the same name as the mod id in the manifest
dir_name = _ModLoaderFile.get_mod_dir_name_in_zip(zip_path)
else:
dir_name = path.split("/")[-1]
dir_path = _ModLoaderPath.get_unpacked_mods_dir_path().path_join(dir_name)
source = get_mod_source()
_has_required_files()
# We want to avoid checking if mod_dir_name == mod_id when manifest parsing has failed
# to prevent confusing error messages.
if not manifest.has_parsing_failed:
_is_mod_dir_name_same_as_id(manifest)
is_overwrite = _is_overwrite()
is_locked = manifest.get_mod_id() in ModLoaderStore.ml_options.locked_mods
if not load_errors.is_empty() or not manifest.validation_messages_error.is_empty():
is_loadable = false
# Load each mod config json from the mods config directory.
func load_configs() -> void:
# If the default values in the config schema are invalid don't load configs
if not manifest.load_mod_config_defaults():
return
var config_dir_path := _ModLoaderPath.get_path_to_mod_configs_dir(dir_name)
var config_file_paths := _ModLoaderPath.get_file_paths_in_dir(config_dir_path)
for config_file_path in config_file_paths:
_load_config(config_file_path)
# Set the current_config based on the user profile
if ModLoaderUserProfile.is_initialized() and ModLoaderConfig.has_current_config(dir_name):
current_config = ModLoaderConfig.get_current_config(dir_name)
else:
current_config = ModLoaderConfig.get_config(dir_name, ModLoaderConfig.DEFAULT_CONFIG_NAME)
# Create a new ModConfig instance for each Config JSON and add it to the configs dictionary.
func _load_config(config_file_path: String) -> void:
var config_data := _ModLoaderFile.get_json_as_dict(config_file_path)
var mod_config = ModConfig.new(
dir_name,
config_data,
config_file_path,
manifest.config_schema
)
# Add the config to the configs dictionary
configs[mod_config.name] = mod_config
# Update the mod_list of the current user profile
func _set_current_config(new_current_config: ModConfig) -> void:
ModLoaderUserProfile.set_mod_current_config(dir_name, new_current_config)
current_config = new_current_config
# We can't emit the signal if the ModLoader is not initialized yet
if ModLoader:
ModLoader.current_config_changed.emit(new_current_config)
func set_mod_state(should_activate: bool, force := false) -> bool:
if is_locked and should_activate != is_active:
ModLoaderLog.error(
"Unable to toggle mod \"%s\" since it is marked as locked. Locked mods: %s"
% [manifest.get_mod_id(), ModLoaderStore.ml_options.locked_mods], LOG_NAME)
return false
if should_activate and not is_loadable:
ModLoaderLog.error(
"Unable to activate mod \"%s\" since it has the following load errors: %s"
% [manifest.get_mod_id(), ", ".join(load_errors)], LOG_NAME)
return false
if should_activate and manifest.validation_messages_warning.size() > 0:
if not force:
ModLoaderLog.warning(
"Rejecting to activate mod \"%s\" since it has the following load warnings: %s"
% [manifest.get_mod_id(), ", ".join(load_warnings)], LOG_NAME)
return false
ModLoaderLog.info(
"Forced to activate mod \"%s\" despite the following load warnings: %s"
% [manifest.get_mod_id(), ", ".join(load_warnings)], LOG_NAME)
is_active = should_activate
return true
# Validates if [member dir_name] matches [method ModManifest.get_mod_id]
func _is_mod_dir_name_same_as_id(mod_manifest: ModManifest) -> bool:
var manifest_id := mod_manifest.get_mod_id()
if not dir_name == manifest_id:
load_errors.push_back('Mod directory name "%s" does not match the data in manifest.json. Expected "%s" (Format: {namespace}-{name})' % [ dir_name, manifest_id ])
return false
return true
func _is_overwrite() -> bool:
return _ModLoaderFile.file_exists(get_optional_mod_file_path(OptionalModFiles.OVERWRITES), zip_path)
# Confirms that all files from [member required_mod_files] exist
func _has_required_files() -> bool:
var has_required_files := true
for required_file in RequiredModFiles:
var required_file_path := get_required_mod_file_path(RequiredModFiles[required_file])
if not _ModLoaderFile.file_exists(required_file_path, zip_path):
load_errors.push_back(
"ERROR - %s is missing a required file: %s. For more information, please visit \"%s\"." %
[dir_name, required_file_path, ModLoaderStore.URL_MOD_STRUCTURE_DOCS]
)
has_required_files = false
return has_required_files
# Converts enum indices [member RequiredModFiles] into their respective file paths
# All required mod files should be in the root of the mod directory
func get_required_mod_file_path(required_file: RequiredModFiles) -> String:
match required_file:
RequiredModFiles.MOD_MAIN:
return dir_path.path_join(MOD_MAIN)
RequiredModFiles.MANIFEST:
return dir_path.path_join(MANIFEST)
return ""
func get_optional_mod_file_path(optional_file: OptionalModFiles) -> String:
match optional_file:
OptionalModFiles.OVERWRITES:
return dir_path.path_join(OVERWRITES)
return ""
func get_mod_source() -> Sources:
if zip_path.contains("workshop"):
return Sources.STEAM_WORKSHOP
if zip_path == "":
return Sources.UNPACKED
return Sources.LOCAL