mirror of
https://github.com/JHDev2006/Super-Mario-Bros.-Remastered-Public.git
synced 2025-10-22 15:38:14 +00:00
221 lines
7.2 KiB
GDScript
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
|