mirror of
https://github.com/JHDev2006/Super-Mario-Bros.-Remastered-Public.git
synced 2025-10-23 07:58:09 +00:00
added the game
This commit is contained in:
82
addons/mod_loader/resources/mod_config.gd
Normal file
82
addons/mod_loader/resources/mod_config.gd
Normal file
@@ -0,0 +1,82 @@
|
||||
class_name ModConfig
|
||||
extends Resource
|
||||
##
|
||||
## This Class is used to represent a configuration for a mod.[br]
|
||||
## The Class provides functionality to initialize, validate, save, and remove a mod's configuration.
|
||||
##
|
||||
## @tutorial(Creating a Mod Config Schema with JSON-Schemas): https://wiki.godotmodding.com/guides/modding/config_json/
|
||||
|
||||
|
||||
const LOG_NAME := "ModLoader:ModConfig"
|
||||
|
||||
## Name of the config - must be unique
|
||||
var name: String
|
||||
## The mod_id this config belongs to
|
||||
var mod_id: String
|
||||
## The JSON-Schema this config uses for validation
|
||||
var schema: Dictionary
|
||||
## The data this config holds
|
||||
var data: Dictionary
|
||||
## The path where the JSON file for this config is stored
|
||||
var save_path: String
|
||||
## False if any data is invalid
|
||||
var valid := false
|
||||
|
||||
|
||||
func _init(_mod_id: String, _data: Dictionary, _save_path: String, _schema: Dictionary = {}) -> void:
|
||||
name = _ModLoaderPath.get_file_name_from_path(_save_path, true, true)
|
||||
mod_id = _mod_id
|
||||
schema = ModLoaderStore.mod_data[_mod_id].manifest.config_schema if _schema.is_empty() else _schema
|
||||
data = _data
|
||||
save_path = _save_path
|
||||
|
||||
var error_message := validate()
|
||||
|
||||
if not error_message == "":
|
||||
ModLoaderLog.error("Mod Config for mod \"%s\" failed JSON Schema Validation with error message: \"%s\"" % [mod_id, error_message], LOG_NAME)
|
||||
return
|
||||
|
||||
valid = true
|
||||
|
||||
|
||||
func get_data_as_string() -> String:
|
||||
return JSON.stringify(data)
|
||||
|
||||
|
||||
func get_schema_as_string() -> String:
|
||||
return JSON.stringify(schema)
|
||||
|
||||
|
||||
# Empty string if validation was successful
|
||||
func validate() -> String:
|
||||
var json_schema := JSONSchema.new()
|
||||
var error := json_schema.validate(get_data_as_string(), get_schema_as_string())
|
||||
|
||||
if error.is_empty():
|
||||
valid = true
|
||||
else:
|
||||
valid = false
|
||||
|
||||
return error
|
||||
|
||||
|
||||
# Runs the JSON-Schema validation and returns true if valid
|
||||
func is_valid() -> bool:
|
||||
if validate() == "":
|
||||
valid = true
|
||||
return true
|
||||
|
||||
valid = false
|
||||
return false
|
||||
|
||||
|
||||
## Saves the config data to the config file
|
||||
func save_to_file() -> bool:
|
||||
var is_success := _ModLoaderFile.save_dictionary_to_json_file(data, save_path)
|
||||
return is_success
|
||||
|
||||
|
||||
## Removes the config file
|
||||
func remove_file() -> bool:
|
||||
var is_success := _ModLoaderFile.remove_file(save_path)
|
||||
return is_success
|
1
addons/mod_loader/resources/mod_config.gd.uid
Normal file
1
addons/mod_loader/resources/mod_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bjfjju1edaxwv
|
220
addons/mod_loader/resources/mod_data.gd
Normal file
220
addons/mod_loader/resources/mod_data.gd
Normal file
@@ -0,0 +1,220 @@
|
||||
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
|
1
addons/mod_loader/resources/mod_data.gd.uid
Normal file
1
addons/mod_loader/resources/mod_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bfnhjikkx0g5s
|
536
addons/mod_loader/resources/mod_manifest.gd
Normal file
536
addons/mod_loader/resources/mod_manifest.gd
Normal file
@@ -0,0 +1,536 @@
|
||||
class_name ModManifest
|
||||
extends Resource
|
||||
##
|
||||
## Stores and validates contents of the manifest set by the user
|
||||
|
||||
|
||||
const LOG_NAME := "ModLoader:ModManifest"
|
||||
|
||||
# Validated by [method is_name_or_namespace_valid]
|
||||
## Mod name.
|
||||
var name := ""
|
||||
# Validated by [method is_name_or_namespace_valid]
|
||||
## Mod namespace, most commonly the main author.
|
||||
var mod_namespace := ""
|
||||
# Validated by [method is_semver_valid]
|
||||
## Semantic version. Not a number, but required to be named like this by Thunderstore
|
||||
var version_number := "0.0.0"
|
||||
var description := ""
|
||||
var website_url := ""
|
||||
## Used to determine mod load order
|
||||
var dependencies: PackedStringArray = []
|
||||
## Used to determine mod load order
|
||||
var optional_dependencies: PackedStringArray = []
|
||||
## only used for information
|
||||
var authors: PackedStringArray = []
|
||||
## only used for information
|
||||
var compatible_game_version: PackedStringArray = []
|
||||
# Validated by [method _handle_compatible_mod_loader_version]
|
||||
## only used for information
|
||||
var compatible_mod_loader_version: PackedStringArray = []
|
||||
## only used for information
|
||||
var incompatibilities: PackedStringArray = []
|
||||
## Used to determine mod load order
|
||||
var load_before: PackedStringArray = []
|
||||
## only used for information
|
||||
var tags : PackedStringArray = []
|
||||
## Schema for mod configs
|
||||
var config_schema := {}
|
||||
var description_rich := ""
|
||||
var image: CompressedTexture2D
|
||||
## only used for information
|
||||
var steam_workshop_id := ""
|
||||
|
||||
var validation_messages_error : Array[String] = []
|
||||
var validation_messages_warning : Array[String] = []
|
||||
|
||||
var is_valid := false
|
||||
var has_parsing_failed := false
|
||||
|
||||
# Required keys in a mod's manifest.json file
|
||||
const REQUIRED_MANIFEST_KEYS_ROOT: Array[String] = [
|
||||
"name",
|
||||
"namespace",
|
||||
"version_number",
|
||||
"website_url",
|
||||
"description",
|
||||
"dependencies",
|
||||
"extra",
|
||||
]
|
||||
|
||||
# Required keys in manifest's `json.extra.godot`
|
||||
const REQUIRED_MANIFEST_KEYS_EXTRA: Array[String] = [
|
||||
"authors",
|
||||
"compatible_mod_loader_version",
|
||||
"compatible_game_version",
|
||||
]
|
||||
|
||||
|
||||
# Takes the manifest as [Dictionary] and validates everything.
|
||||
# Will return null if something is invalid.
|
||||
func _init(manifest: Dictionary, path: String) -> void:
|
||||
if manifest.is_empty():
|
||||
validation_messages_error.push_back("The manifest cannot be validated due to missing data, most likely because parsing the manifest.json file failed.")
|
||||
has_parsing_failed = true
|
||||
else:
|
||||
is_valid = validate(manifest, path)
|
||||
|
||||
|
||||
func validate(manifest: Dictionary, path: String) -> bool:
|
||||
var missing_fields: Array[String] = []
|
||||
|
||||
missing_fields.append_array(ModLoaderUtils.get_missing_dict_fields(manifest, REQUIRED_MANIFEST_KEYS_ROOT))
|
||||
missing_fields.append_array(ModLoaderUtils.get_missing_dict_fields(manifest.extra, ["godot"]))
|
||||
missing_fields.append_array(ModLoaderUtils.get_missing_dict_fields(manifest.extra.godot, REQUIRED_MANIFEST_KEYS_EXTRA))
|
||||
|
||||
if not missing_fields.is_empty():
|
||||
validation_messages_error.push_back("Manifest is missing required fields: %s" % str(missing_fields))
|
||||
|
||||
name = manifest.name
|
||||
mod_namespace = manifest.namespace
|
||||
version_number = manifest.version_number
|
||||
|
||||
is_name_or_namespace_valid(name)
|
||||
is_name_or_namespace_valid(mod_namespace)
|
||||
|
||||
var mod_id = get_mod_id()
|
||||
|
||||
is_semver_valid(mod_id, version_number, "version_number")
|
||||
|
||||
description = manifest.description
|
||||
website_url = manifest.website_url
|
||||
dependencies = manifest.dependencies
|
||||
|
||||
var godot_details: Dictionary = manifest.extra.godot
|
||||
authors = ModLoaderUtils.get_array_from_dict(godot_details, "authors")
|
||||
optional_dependencies = ModLoaderUtils.get_array_from_dict(godot_details, "optional_dependencies")
|
||||
incompatibilities = ModLoaderUtils.get_array_from_dict(godot_details, "incompatibilities")
|
||||
load_before = ModLoaderUtils.get_array_from_dict(godot_details, "load_before")
|
||||
compatible_game_version = ModLoaderUtils.get_array_from_dict(godot_details, "compatible_game_version")
|
||||
compatible_mod_loader_version = _handle_compatible_mod_loader_version(mod_id, godot_details)
|
||||
description_rich = ModLoaderUtils.get_string_from_dict(godot_details, "description_rich")
|
||||
tags = ModLoaderUtils.get_array_from_dict(godot_details, "tags")
|
||||
config_schema = ModLoaderUtils.get_dict_from_dict(godot_details, "config_schema")
|
||||
steam_workshop_id = ModLoaderUtils.get_string_from_dict(godot_details, "steam_workshop_id")
|
||||
|
||||
if ModLoaderStore.ml_options.game_version_validation == ModLoaderOptionsProfile.VERSION_VALIDATION.DEFAULT:
|
||||
_is_game_version_compatible(mod_id)
|
||||
|
||||
if ModLoaderStore.ml_options.game_version_validation == ModLoaderOptionsProfile.VERSION_VALIDATION.CUSTOM:
|
||||
if ModLoaderStore.ml_options.custom_game_version_validation_callable:
|
||||
ModLoaderStore.ml_options.custom_game_version_validation_callable.call(self)
|
||||
else:
|
||||
ModLoaderLog.error("No custom game version validation callable detected. Please provide a valid validation callable.", LOG_NAME)
|
||||
|
||||
is_mod_id_array_valid(mod_id, dependencies, "dependency")
|
||||
is_mod_id_array_valid(mod_id, incompatibilities, "incompatibility")
|
||||
is_mod_id_array_valid(mod_id, optional_dependencies, "optional_dependency")
|
||||
is_mod_id_array_valid(mod_id, load_before, "load_before")
|
||||
|
||||
validate_distinct_mod_ids_in_arrays(mod_id, dependencies, incompatibilities, ["dependencies", "incompatibilities"])
|
||||
validate_distinct_mod_ids_in_arrays(mod_id, optional_dependencies, dependencies, ["optional_dependencies", "dependencies"])
|
||||
validate_distinct_mod_ids_in_arrays(mod_id, optional_dependencies, incompatibilities, ["optional_dependencies", "incompatibilities"])
|
||||
validate_distinct_mod_ids_in_arrays(
|
||||
mod_id,
|
||||
load_before,
|
||||
dependencies,
|
||||
["load_before", "dependencies"],
|
||||
"\"load_before\" should be handled as optional dependency adding it to \"dependencies\" will cancel out the desired effect."
|
||||
)
|
||||
validate_distinct_mod_ids_in_arrays(
|
||||
mod_id,
|
||||
load_before,
|
||||
optional_dependencies,
|
||||
["load_before", "optional_dependencies"],
|
||||
"\"load_before\" can be viewed as optional dependency, please remove the duplicate mod-id."
|
||||
)
|
||||
validate_distinct_mod_ids_in_arrays(mod_id,load_before,incompatibilities,["load_before", "incompatibilities"])
|
||||
|
||||
_validate_workshop_id(path)
|
||||
|
||||
return validation_messages_error.is_empty()
|
||||
|
||||
|
||||
# Mod ID used in the mod loader
|
||||
# Format: {namespace}-{name}
|
||||
func get_mod_id() -> String:
|
||||
return "%s-%s" % [mod_namespace, name]
|
||||
|
||||
|
||||
# Package ID used by Thunderstore
|
||||
# Format: {namespace}-{name}-{version_number}
|
||||
func get_package_id() -> String:
|
||||
return "%s-%s-%s" % [mod_namespace, name, version_number]
|
||||
|
||||
|
||||
# Returns the Manifest values as a dictionary
|
||||
func get_as_dict() -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"namespace": mod_namespace,
|
||||
"version_number": version_number,
|
||||
"description": description,
|
||||
"website_url": website_url,
|
||||
"dependencies": dependencies,
|
||||
"optional_dependencies": optional_dependencies,
|
||||
"authors": authors,
|
||||
"compatible_game_version": compatible_game_version,
|
||||
"compatible_mod_loader_version": compatible_mod_loader_version,
|
||||
"incompatibilities": incompatibilities,
|
||||
"load_before": load_before,
|
||||
"tags": tags,
|
||||
"config_schema": config_schema,
|
||||
"description_rich": description_rich,
|
||||
"image": image,
|
||||
}
|
||||
|
||||
|
||||
# Returns the Manifest values as JSON, in the manifest.json format
|
||||
func to_json() -> String:
|
||||
return JSON.stringify({
|
||||
"name": name,
|
||||
"namespace": mod_namespace,
|
||||
"version_number": version_number,
|
||||
"description": description,
|
||||
"website_url": website_url,
|
||||
"dependencies": dependencies,
|
||||
"extra": {
|
||||
"godot":{
|
||||
"authors": authors,
|
||||
"optional_dependencies": optional_dependencies,
|
||||
"compatible_game_version": compatible_game_version,
|
||||
"compatible_mod_loader_version": compatible_mod_loader_version,
|
||||
"incompatibilities": incompatibilities,
|
||||
"load_before": load_before,
|
||||
"tags": tags,
|
||||
"config_schema": config_schema,
|
||||
"description_rich": description_rich,
|
||||
"image": image,
|
||||
}
|
||||
}
|
||||
}, "\t")
|
||||
|
||||
|
||||
# Loads the default configuration for a mod.
|
||||
func load_mod_config_defaults() -> ModConfig:
|
||||
var default_config_save_path := _ModLoaderPath.get_path_to_mod_config_file(get_mod_id(), ModLoaderConfig.DEFAULT_CONFIG_NAME)
|
||||
var config := ModConfig.new(
|
||||
get_mod_id(),
|
||||
{},
|
||||
default_config_save_path,
|
||||
config_schema
|
||||
)
|
||||
|
||||
# Check if there is no default.json file in the mods config directory
|
||||
if not _ModLoaderFile.file_exists(config.save_path):
|
||||
# Generate config_default based on the default values in config_schema
|
||||
config.data = _generate_default_config_from_schema(config.schema.properties)
|
||||
|
||||
# If the default.json file exists
|
||||
else:
|
||||
var current_schema_md5 := config.get_schema_as_string().md5_text()
|
||||
var cache_schema_md5s := _ModLoaderCache.get_data("config_schemas")
|
||||
var cache_schema_md5: String = cache_schema_md5s[config.mod_id] if cache_schema_md5s.has(config.mod_id) else ''
|
||||
|
||||
# Generate a new default config if the config schema has changed or there is nothing cached
|
||||
if not current_schema_md5 == cache_schema_md5 or cache_schema_md5.is_empty():
|
||||
config.data = _generate_default_config_from_schema(config.schema.properties)
|
||||
|
||||
# If the config schema has not changed just load the json file
|
||||
else:
|
||||
config.data = _ModLoaderFile.get_json_as_dict(config.save_path)
|
||||
|
||||
# Validate the config defaults
|
||||
if config.is_valid():
|
||||
# Create the default config file
|
||||
config.save_to_file()
|
||||
|
||||
# Store the md5 of the config schema in the cache
|
||||
_ModLoaderCache.update_data("config_schemas", {config.mod_id: config.get_schema_as_string().md5_text()} )
|
||||
|
||||
# Return the default ModConfig
|
||||
return config
|
||||
|
||||
ModLoaderLog.fatal("The default config values for %s-%s are invalid. Configs will not be loaded." % [mod_namespace, name], LOG_NAME)
|
||||
return null
|
||||
|
||||
|
||||
# Recursively searches for default values
|
||||
func _generate_default_config_from_schema(property: Dictionary, current_prop := {}) -> Dictionary:
|
||||
# Exit function if property is empty
|
||||
if property.is_empty():
|
||||
return current_prop
|
||||
|
||||
for property_key in property.keys():
|
||||
var prop = property[property_key]
|
||||
|
||||
# If this property contains nested properties, we recursively call this function
|
||||
if "properties" in prop:
|
||||
current_prop[property_key] = {}
|
||||
_generate_default_config_from_schema(prop.properties, current_prop[property_key])
|
||||
# Return early here because a object will not have a "default" key
|
||||
return current_prop
|
||||
|
||||
# If this property contains a default value, add it to the global config_defaults dictionary
|
||||
if JSONSchema.JSKW_DEFAULT in prop:
|
||||
# Initialize the current_key if it is missing in config_defaults
|
||||
if not current_prop.has(property_key):
|
||||
current_prop[property_key] = {}
|
||||
|
||||
# Add the default value to the config_defaults
|
||||
current_prop[property_key] = prop.default
|
||||
|
||||
return current_prop
|
||||
|
||||
|
||||
# Handles deprecation of the single string value in the compatible_mod_loader_version.
|
||||
func _handle_compatible_mod_loader_version(mod_id: String, godot_details: Dictionary) -> Array:
|
||||
var link_manifest_docs := "https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files#manifestjson"
|
||||
var array_value := ModLoaderUtils.get_array_from_dict(godot_details, "compatible_mod_loader_version")
|
||||
|
||||
# If there are array values
|
||||
if array_value.size() > 0:
|
||||
# Check for valid versions
|
||||
if not is_semver_version_array_valid(mod_id, array_value, "compatible_mod_loader_version"):
|
||||
return []
|
||||
|
||||
return array_value
|
||||
|
||||
# If the array is empty check if a string was passed
|
||||
var string_value := ModLoaderUtils.get_string_from_dict(godot_details, "compatible_mod_loader_version")
|
||||
# If an empty string was passed
|
||||
if string_value == "":
|
||||
# Using str() here because format strings caused an error
|
||||
validation_messages_error.push_back(
|
||||
str (
|
||||
"%s - \"compatible_mod_loader_version\" is a required field." +
|
||||
" For more details visit %s"
|
||||
) % [mod_id, link_manifest_docs])
|
||||
return []
|
||||
|
||||
return [string_value]
|
||||
|
||||
|
||||
# A valid namespace may only use letters (any case), numbers and underscores
|
||||
# and has to be longer than 3 characters
|
||||
# a-z A-Z 0-9 _ (longer than 3 characters)
|
||||
func is_name_or_namespace_valid(check_name: String, is_silent := false) -> bool:
|
||||
var re := RegEx.new()
|
||||
var _compile_error_1 = re.compile("^[a-zA-Z0-9_]*$") # alphanumeric and _
|
||||
|
||||
if re.search(check_name) == null:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back("Invalid name or namespace: \"%s\". You may only use letters, numbers and underscores." % check_name)
|
||||
return false
|
||||
|
||||
var _compile_error_2 = re.compile("^[a-zA-Z0-9_]{3,}$") # at least 3 long
|
||||
if re.search(check_name) == null:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back("Invalid name or namespace: \"%s\". Must be longer than 3 characters." % check_name)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func is_semver_version_array_valid(mod_id: String, version_array: PackedStringArray, version_array_descripton: String, is_silent := false) -> bool:
|
||||
var is_valid := true
|
||||
|
||||
for version in version_array:
|
||||
if not is_semver_valid(mod_id, version, version_array_descripton, is_silent):
|
||||
is_valid = false
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
# A valid semantic version should follow this format: {mayor}.{minor}.{patch}
|
||||
# reference https://semver.org/ for details
|
||||
# {0-9}.{0-9}.{0-9} (no leading 0, shorter than 16 characters total)
|
||||
func is_semver_valid(mod_id: String, check_version_number: String, field_name: String, is_silent := false) -> bool:
|
||||
var re := RegEx.new()
|
||||
var _compile_error = re.compile("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$")
|
||||
|
||||
if re.search(check_version_number) == null:
|
||||
if not is_silent:
|
||||
# Using str() here because format strings caused an error
|
||||
validation_messages_error.push_back(
|
||||
str(
|
||||
"Invalid semantic version: \"%s\" in field \"%s\" of mod \"%s\". " +
|
||||
"You may only use numbers without leading zero and periods " +
|
||||
"following this format {mayor}.{minor}.{patch}"
|
||||
) % [check_version_number, field_name, mod_id]
|
||||
)
|
||||
return false
|
||||
|
||||
if check_version_number.length() > 16:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back(
|
||||
str(
|
||||
"Invalid semantic version: \"%s\" in field \"%s\" of mod \"%s\". " +
|
||||
"Version number must be shorter than 16 characters."
|
||||
) % [check_version_number, field_name, mod_id]
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func validate_distinct_mod_ids_in_arrays(
|
||||
mod_id: String,
|
||||
array_one: PackedStringArray,
|
||||
array_two: PackedStringArray,
|
||||
array_description: PackedStringArray,
|
||||
additional_info := "",
|
||||
is_silent := false
|
||||
) -> bool:
|
||||
# Initialize an empty array to hold any overlaps.
|
||||
var overlaps: PackedStringArray = []
|
||||
|
||||
# Loop through each incompatibility and check if it is also listed as a dependency.
|
||||
for loop_mod_id in array_one:
|
||||
if array_two.has(loop_mod_id):
|
||||
overlaps.push_back(loop_mod_id)
|
||||
|
||||
# If no overlaps were found
|
||||
if overlaps.size() == 0:
|
||||
return true
|
||||
|
||||
# If any overlaps were found
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back(
|
||||
(
|
||||
"The mod -> %s lists the same mod(s) -> %s - in \"%s\" and \"%s\". %s"
|
||||
% [mod_id, overlaps, array_description[0], array_description[1], additional_info]
|
||||
)
|
||||
)
|
||||
return false
|
||||
|
||||
# If silent just return false
|
||||
return false
|
||||
|
||||
|
||||
func is_mod_id_array_valid(own_mod_id: String, mod_id_array: PackedStringArray, mod_id_array_description: String, is_silent := false) -> bool:
|
||||
var is_valid := true
|
||||
|
||||
# If there are mod ids
|
||||
if mod_id_array.size() > 0:
|
||||
for mod_id in mod_id_array:
|
||||
# Check if mod id is the same as the mods mod id.
|
||||
if mod_id == own_mod_id:
|
||||
is_valid = false
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back("The mod \"%s\" lists itself as \"%s\" in its own manifest.json file" % [mod_id, mod_id_array_description])
|
||||
|
||||
# Check if the mod id is a valid mod id.
|
||||
if not is_mod_id_valid(own_mod_id, mod_id, mod_id_array_description, is_silent):
|
||||
is_valid = false
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
func is_mod_id_valid(original_mod_id: String, check_mod_id: String, type := "", is_silent := false) -> bool:
|
||||
var intro_text = "A %s for the mod \"%s\" is invalid: " % [type, original_mod_id] if not type == "" else ""
|
||||
|
||||
# contains hyphen?
|
||||
if not check_mod_id.count("-") == 1:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back(str(intro_text, "Expected a single hyphen in the mod ID, but the %s was: \"%s\"" % [type, check_mod_id]))
|
||||
return false
|
||||
|
||||
# at least 7 long (1 for hyphen, 3 each for namespace/name)
|
||||
var mod_id_length = check_mod_id.length()
|
||||
if mod_id_length < 7:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back(str(intro_text, "Mod ID for \"%s\" is too short. It must be at least 7 characters long, but its length is: %s" % [check_mod_id, mod_id_length]))
|
||||
return false
|
||||
|
||||
var split = check_mod_id.split("-")
|
||||
var check_namespace = split[0]
|
||||
var check_name = split[1]
|
||||
var re := RegEx.new()
|
||||
re.compile("^[a-zA-Z0-9_]{3,}$") # alphanumeric and _ and at least 3 characters
|
||||
|
||||
if re.search(check_namespace) == null:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back(str(intro_text, "Mod ID has an invalid namespace (author) for \"%s\". Namespace can only use letters, numbers and underscores, but was: \"%s\"" % [check_mod_id, check_namespace]))
|
||||
return false
|
||||
|
||||
if re.search(check_name) == null:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back(str(intro_text, "Mod ID has an invalid name for \"%s\". Name can only use letters, numbers and underscores, but was: \"%s\"" % [check_mod_id, check_name]))
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func is_string_length_valid(mod_id: String, field: String, string: String, required_length: int, is_silent := false) -> bool:
|
||||
if not string.length() == required_length:
|
||||
if not is_silent:
|
||||
validation_messages_error.push_back("Invalid length in field \"%s\" of mod \"%s\" it should be \"%s\" but it is \"%s\"." % [field, mod_id, required_length, string.length()])
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
# Validates the workshop id separately from the rest since it needs the ModData
|
||||
func _validate_workshop_id(path: String) -> void:
|
||||
var steam_workshop_id_from_path := _ModLoaderPath.get_steam_workshop_id(path)
|
||||
var is_mod_source_workshop := not steam_workshop_id_from_path.is_empty()
|
||||
|
||||
if not _is_steam_workshop_id_valid(get_mod_id(), steam_workshop_id_from_path, steam_workshop_id, is_mod_source_workshop):
|
||||
# Override the invalid steam_workshop_id if we load from the workshop
|
||||
if is_mod_source_workshop:
|
||||
steam_workshop_id = steam_workshop_id_from_path
|
||||
|
||||
|
||||
func _is_steam_workshop_id_valid(mod_id: String, steam_workshop_id_from_path: String, steam_workshop_id_to_validate: String, is_mod_source_workshop := false, is_silent := false) -> bool:
|
||||
if steam_workshop_id_to_validate.is_empty():
|
||||
# Workshop id is optional, so we return true if no id is given
|
||||
return true
|
||||
|
||||
# Validate the steam_workshop_id based on the zip_path if the mod is loaded from the workshop
|
||||
if is_mod_source_workshop:
|
||||
if not steam_workshop_id_to_validate == steam_workshop_id_from_path:
|
||||
if not is_silent:
|
||||
ModLoaderLog.warning("The \"steam_workshop_id\": \"%s\" provided by the mod manifest of mod \"%s\" is incorrect, it should be \"%s\"." % [steam_workshop_id_to_validate, mod_id, steam_workshop_id_from_path], LOG_NAME)
|
||||
return false
|
||||
else:
|
||||
if not is_string_length_valid(mod_id, "steam_workshop_id", steam_workshop_id_to_validate, 10, is_silent):
|
||||
# Invalidate the manifest in this case because the mod is most likely in development if it is not loaded from the steam workshop.
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func _is_game_version_compatible(mod_id: String) -> bool:
|
||||
var game_version: String = ModLoaderStore.ml_options.semantic_version
|
||||
var game_major := int(game_version.get_slice(".", 0))
|
||||
var game_minor := int(game_version.get_slice(".", 1))
|
||||
|
||||
var valid_major := false
|
||||
var valid_minor := false
|
||||
for version in compatible_game_version:
|
||||
var compat_major := int(version.get_slice(".", 0))
|
||||
var compat_minor := int(version.get_slice(".", 1))
|
||||
if compat_major < game_major:
|
||||
continue
|
||||
valid_major = true
|
||||
|
||||
if compat_minor < game_minor:
|
||||
continue
|
||||
valid_minor = true
|
||||
|
||||
if not valid_major:
|
||||
validation_messages_error.push_back(
|
||||
"The mod \"%s\" is incompatible with the current game version.
|
||||
(current game version: %s, mod compatible with game versions: %s)" %
|
||||
[mod_id, game_version, compatible_game_version]
|
||||
)
|
||||
return false
|
||||
if not valid_minor:
|
||||
validation_messages_warning.push_back(
|
||||
"The mod \"%s\" may not be compatible with the current game version.
|
||||
Enable at your own risk. (current game version: %s, mod compatible with game versions: %s)" %
|
||||
[mod_id, game_version, compatible_game_version]
|
||||
)
|
||||
return true
|
||||
|
||||
return true
|
1
addons/mod_loader/resources/mod_manifest.gd.uid
Normal file
1
addons/mod_loader/resources/mod_manifest.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bleh3oamdbmnr
|
22
addons/mod_loader/resources/mod_user_profile.gd
Normal file
22
addons/mod_loader/resources/mod_user_profile.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
class_name ModUserProfile
|
||||
extends Resource
|
||||
## This Class is used to represent a User Profile for the ModLoader.
|
||||
|
||||
|
||||
## The name of the profile
|
||||
var name := ""
|
||||
## A list of all installed mods
|
||||
## [codeblock]
|
||||
## "mod_list": {
|
||||
## "Namespace-ModName": {
|
||||
## "current_config": "default",
|
||||
## "is_active": false,
|
||||
## "zip_path": "",
|
||||
## },
|
||||
## [/codeblock]
|
||||
var mod_list := {}
|
||||
|
||||
|
||||
func _init(_name := "", _mod_list := {}) -> void:
|
||||
name = _name
|
||||
mod_list = _mod_list
|
1
addons/mod_loader/resources/mod_user_profile.gd.uid
Normal file
1
addons/mod_loader/resources/mod_user_profile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ddrlbkscua6n0
|
15
addons/mod_loader/resources/options_current.gd
Normal file
15
addons/mod_loader/resources/options_current.gd
Normal file
@@ -0,0 +1,15 @@
|
||||
class_name ModLoaderCurrentOptions
|
||||
extends Resource
|
||||
|
||||
# The default options set for the mod loader
|
||||
@export var current_options: Resource = preload(
|
||||
"res://addons/mod_loader/options/profiles/default.tres"
|
||||
)
|
||||
|
||||
# Overrides for all available feature tags through OS.has_feature()
|
||||
# Format: Dictionary[String: ModLoaderOptionsProfile] where the string is a tag
|
||||
# Warning: Some tags can occur at the same time (Windows + editor for example) -
|
||||
# In a case where multiple apply, the last one in the dict will override all others
|
||||
@export var feature_override_options: Dictionary = {
|
||||
"editor": preload("res://addons/mod_loader/options/profiles/editor.tres")
|
||||
}
|
1
addons/mod_loader/resources/options_current.gd.uid
Normal file
1
addons/mod_loader/resources/options_current.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmxtu4snlj1bb
|
119
addons/mod_loader/resources/options_profile.gd
Normal file
119
addons/mod_loader/resources/options_profile.gd
Normal file
@@ -0,0 +1,119 @@
|
||||
class_name ModLoaderOptionsProfile
|
||||
extends Resource
|
||||
##
|
||||
## Class to define and store Mod Loader Options.
|
||||
##
|
||||
## @tutorial(Example Customization Script): https://wiki.godotmodding.com/guides/integration/mod_loader_options/#game-version-validation
|
||||
|
||||
|
||||
## Settings for game version validation.
|
||||
enum VERSION_VALIDATION {
|
||||
## Uses the default semantic versioning (semver) validation.
|
||||
DEFAULT,
|
||||
|
||||
## Disables validation of the game version specified in [member semantic_version]
|
||||
## and the mod's [member ModManifest.compatible_game_version].
|
||||
DISABLED,
|
||||
|
||||
## Enables custom game version validation.
|
||||
## Use [member customize_script_path] to specify a script that customizes the Mod Loader options.
|
||||
## In this script, you must set [member custom_game_version_validation_callable]
|
||||
## to a custom validation [Callable].
|
||||
## [br]
|
||||
## ===[br]
|
||||
## [b]Note:[color=note "Easier Mod Loader Updates"][/color][/b][br]
|
||||
## Using a custom script allows you to keep your code outside the addons directory,
|
||||
## making it easier to update the mod loader without affecting your modifications. [br]
|
||||
## ===[br]
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
## Can be used to disable mods for specific plaforms by using feature overrides
|
||||
@export var enable_mods: bool = true
|
||||
## List of mod ids that can't be turned on or off
|
||||
@export var locked_mods: Array[String] = []
|
||||
## List of mods that will not be loaded
|
||||
@export var disabled_mods: Array[String] = []
|
||||
## Disables the requirement for the mod loader autoloads to be first
|
||||
@export var allow_modloader_autoloads_anywhere: bool = false
|
||||
## This script is loaded after [member ModLoaderStore.ml_options] has been initialized.
|
||||
## It is instantiated with [member ModLoaderStore.ml_options] as an argument.
|
||||
## Use this script to apply settings that cannot be configured through the editor UI.
|
||||
##
|
||||
## For an example, see [enum VERSION_VALIDATION] [code]CUSTOM[/code] or
|
||||
## [code]res://addons/mod_loader/options/example_customize_script.gd[/code].
|
||||
@export_file var customize_script_path: String
|
||||
|
||||
@export_group("Logging")
|
||||
## Sets the logging verbosity level.
|
||||
## Refer to [enum ModLoaderLog.VERBOSITY_LEVEL] for more details.
|
||||
@export var log_level := ModLoaderLog.VERBOSITY_LEVEL.DEBUG
|
||||
## Stops the mod loader from logging any deprecation related errors.
|
||||
@export var ignore_deprecated_errors: bool = false
|
||||
## Ignore messages from these namespaces.[br]
|
||||
## Accepts * as wildcard. [br]
|
||||
## [code]ModLoader:Dependency[/code] - ignore the exact name [br]
|
||||
## [code]ModLoader:*[/code] - ignore all beginning with this name [br]
|
||||
@export var ignored_mod_names_in_log: Array[String] = []
|
||||
@export var hint_color := Color("#70bafa")
|
||||
|
||||
@export_group("Game Data")
|
||||
## Steam app id, can be found in the steam page url
|
||||
@export var steam_id: int = 0:
|
||||
get:
|
||||
return steam_id
|
||||
|
||||
## Semantic game version. [br]
|
||||
## Replace the getter in options_profile.gd if your game stores the version somewhere else
|
||||
@export var semantic_version := "0.0.0":
|
||||
get:
|
||||
return semantic_version
|
||||
|
||||
@export_group("Mod Sources")
|
||||
## Indicates whether to load mods from the Steam Workshop directory, or the overridden workshop path.
|
||||
@export var load_from_steam_workshop: bool = false
|
||||
## Indicates whether to load mods from the "mods" folder located at the game's install directory, or the overridden mods path.
|
||||
@export var load_from_local: bool = true
|
||||
## Indicates whether to load mods from [code]"res://mods-unpacked"[/code] in the exported game.[br]
|
||||
## ===[br]
|
||||
## [b]Note:[color=note "Load from unpacked in the editor"][/color][/b][br]
|
||||
## In the editor, mods inside [code]"res://mods-unpacked"[/code] are always loaded. Use [member enable_mods] to disable mod loading completely.[br]
|
||||
## ===[br]
|
||||
@export var load_from_unpacked: bool = true
|
||||
## Path to a folder containing mods [br]
|
||||
## Mod zips should be directly in this folder
|
||||
@export_dir var override_path_to_mods = ""
|
||||
## Use this option to override the default path where configs are stored.
|
||||
@export_dir var override_path_to_configs = ""
|
||||
## Path to a folder containing workshop items.[br]
|
||||
## Mods zips are placed in another folder, usually[br]
|
||||
## [code]/<workshop id>/mod.zip[/code][br]
|
||||
## The real workshop path ends with [br]
|
||||
## [code]/workshop/content[/code] [br]
|
||||
@export_dir var override_path_to_workshop = ""
|
||||
|
||||
@export_group("Mod Hooks")
|
||||
## Can be used to override the default hook pack path, the hook pack is located inside the game's install directory by default.
|
||||
## To override the path specify a new absolute path.
|
||||
@export_global_dir var override_path_to_hook_pack := ""
|
||||
## Can be used to override the default hook pack name, by default it is [constant ModLoaderStore.MOD_HOOK_PACK_NAME]
|
||||
@export var override_hook_pack_name := ""
|
||||
## Can be used to specify your own scene that is displayed if a game restart is required.
|
||||
## For example if new mod hooks were generated.
|
||||
@export_dir var restart_notification_scene_path := "res://addons/mod_loader/restart_notification.tscn"
|
||||
## Can be used to disable the mod loader's restart logic. Use the [signal ModLoader.new_hooks_created] to implement your own restart logic.
|
||||
@export var disable_restart := false
|
||||
|
||||
@export_group("Mod Validation")
|
||||
## Defines how the game version should be validated.
|
||||
## This setting controls validation for the game version specified in [member semantic_version]
|
||||
## and the mod's [member ModManifest.compatible_game_version].
|
||||
@export var game_version_validation := VERSION_VALIDATION.DEFAULT
|
||||
|
||||
## Callable that is executed during [ModManifest] validation
|
||||
## if [member game_version_validation] is set to [enum VERSION_VALIDATION] [code]CUSTOM[/code].
|
||||
## See the example under [enum VERSION_VALIDATION] [code]CUSTOM[/code] to learn how to set this up.
|
||||
var custom_game_version_validation_callable: Callable
|
||||
|
||||
## Stores the instance of the script specified in [member customize_script_path].
|
||||
var customize_script_instance: RefCounted
|
1
addons/mod_loader/resources/options_profile.gd.uid
Normal file
1
addons/mod_loader/resources/options_profile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsbicisgihjet
|
Reference in New Issue
Block a user