@tool extends Object class_name AssetLoader #region Declaration --- # === CONST === ## If you want to change the mod extension. Keep in mind that it's still a json under the hood const MOD_INFOS_EXTENSION = "modinfo" ## If you want to change the localization extension. Keep in mind that it's still a json under the hood const LOC_EXTENSION = "loc" ## Path to the mods folder : [param res://mods] by default ## [br] ## (is not a const but should be treated as such, so the uppercase) var ASSETS_PATH # === VAR === var dir:DirAccess var mod_folders:PackedStringArray var critical_error := false var mod_manifests: Dictionary[StringName,ModManifest] var mod_localizations: Dictionary[StringName,Dictionary] # {mod_id: {locale: {key: value}}} # === SIGNALS === signal loading_finished #endregion --- func _init() -> void: print_verbose("------ MOD LOADING STARTED ------") # Use res://mods/ in editor, executable's mods/ folder in exported builds if OS.has_feature("editor"): ASSETS_PATH = "res://"+ProjectSettings.get_setting("game/mods/mod_folder_name", "mods") else: # fallbakc to use mods folder next to executable ASSETS_PATH = OS.get_executable_path().get_base_dir() + "/" + ProjectSettings.get_setting("game/mods/mod_folder_name", "mods") dir = DirAccess.open(ASSETS_PATH) print_debug(ASSETS_PATH) if not dir: push_error("AssetLoader: Mods folder not found at '%s'" % ASSETS_PATH) push_error("AssetLoader:",DirAccess.get_open_error()) _show_error_popup("Mods folder not found at '%s'" % ASSETS_PATH) critical_error = true return mod_folders = dir.get_directories() if mod_folders.is_empty(): push_error("AssetLoader: Mods folder '%s' is empty — no mods to load." % ASSETS_PATH) _show_error_popup("Mods folder '%s' is empty — no mods to load." % ASSETS_PATH) critical_error = true return # === PRIVATE === ## This will show a native MessageBox on Windows, ## a native dialog on macOS, and GTK/QT dialog on Linux. func _show_error_popup(message: String) -> void: OS.alert("AssetLoader:"+message, "AssetLoader:Error") # === PUBLIC === func load_all(): load_mods() load_mods_content() ## Load and unpack all .pck before serialization func load_mods(): for mod in mod_folders: var mod_name = mod var mod_path = ASSETS_PATH + "/" + mod_name var manifest_path = mod_path + "/" + mod_name + "." + MOD_INFOS_EXTENSION if FileAccess.file_exists(manifest_path): var manifest_file := FileAccess.open(manifest_path, FileAccess.READ) var manifest: ModManifest = ModManifest.new_from_file(manifest_file) if !manifest:continue if !mod_manifests.has(manifest.id): mod_manifests[manifest.id] = manifest manifest.path = mod_path print_verbose("Mod manifest is loaded: %s" % manifest.name) else: push_warning("Another mod as the same id:\n %s will not be loaded" % manifest_path) else: push_warning("No manifest found in %s" % manifest_path) for mod in mod_manifests: print("Mod loaded: %s" % mod) func load_localizations(): if mod_manifests.size() == 0: print_verbose("No mods to load localizations from.") return for mod_id in mod_manifests: var mod_path: String = mod_manifests[mod_id]["path"] var mod_name: String = mod_path.get_file() mod_localizations[mod_id] = {} dir.change_dir(mod_path) dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": if file_name.ends_with("." + LOC_EXTENSION): var locale = file_name.get_basename().get_extension() var loc_path = mod_path + "/" + file_name var loc_file = FileAccess.open(loc_path, FileAccess.READ) if loc_file: var loc_data: Dictionary = JSON.parse_string(loc_file.get_as_text()) if typeof(loc_data) == TYPE_DICTIONARY: mod_localizations[mod_id][locale] = loc_data print_verbose("Loaded localization '%s' for mod '%s'" % [locale, mod_id]) else: push_warning("Invalid localization file: %s" % loc_path) else: push_warning("Failed to open localization file: %s" % loc_path) file_name = dir.get_next() dir.list_dir_end() func register_localizations(): for mod_id in mod_localizations: for locale in mod_localizations[mod_id]: var translation = Translation.new() translation.locale = locale for key in mod_localizations[mod_id][locale]: translation.add_message(key, mod_localizations[mod_id][locale][key]) TranslationServer.add_translation(translation) func load_mods_content(): if OS.has_feature("editor"): # Assets should already be loaded/imported. # FIXME: here we should implement something to serialize ? maybe not. return if mod_manifests.size() == 0: print_verbose("No mods to load content from.") return for mod_id in mod_manifests: var modpacks:Array = mod_manifests[mod_id].get_mod_packs() for pack in modpacks: # Load assets from the PCKs (mounted at res://) var pck_path:StringName = mod_manifests[mod_id].path+ "/" + pack + ".pck" if dir.file_exists(pck_path): if ProjectSettings.load_resource_pack(pck_path): print_verbose("Loaded PCK for mod '%s': %s" % [mod_id, pck_path]) mod_manifests[mod_id]._validated = true else: push_warning("Failed to load PCK: %s" % pck_path) else: push_warning("PCK not found: %s" % pck_path)