| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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
|