#include "metadata_bridge.h" #include "assets_dir.h" #include "diagnostics.h" #include "glyph_version.h" #include "ryujinx_saves.h" #if defined(__PROSPERO__) #include "native_directory.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/fs/path_util.h" #include "core/file_sys/card_image.h" #include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/control_metadata.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs.h" #include "core/file_sys/submission_package.h" #include "core/file_sys/vfs/vfs_real.h" #include "core/hle/service/ns/language.h" #include "core/hle/service/set/settings_types.h" #include "core/loader/loader.h" namespace { constexpr std::array language_names{ "AmericanEnglish", "BritishEnglish", "Japanese", "French", "German", "LatinAmericanSpanish", "Spanish", "Italian", "Dutch", "CanadianFrench", "Portuguese", "Russian", "Korean", "TraditionalChinese", "SimplifiedChinese", "BrazilianPortuguese", "Polish", "Thai", }; FileSys::VirtualDir OpenControlRomFs(const FileSys::VirtualFile& file, bool xci) { std::shared_ptr control; if (xci) { FileSys::XCI image(file); if (image.GetStatus() != Loader::ResultStatus::Success) return {}; control = image.GetNCAByType(FileSys::NCAContentType::Control); } else { FileSys::NSP package(file); if (package.GetStatus() != Loader::ResultStatus::Success) return {}; control = package.GetNCA(FileSys::GetBaseTitleID(package.GetProgramTitleID()), FileSys::ContentRecordType::Control); } if (!control || control->GetStatus() != Loader::ResultStatus::Success) return {}; const auto romfs = control->GetRomFS(); return romfs ? FileSys::ExtractRomFS(romfs) : FileSys::VirtualDir{}; } std::string ReadTitle(const FileSys::VirtualDir& romfs) { auto nacp = romfs->GetFile("control.nacp"); if (!nacp) nacp = romfs->GetFile("Control.nacp"); if (!nacp || nacp->GetSize() < 0x3000) return {}; std::array name{}; for (std::size_t language = 0; language < 16; ++language) { if (nacp->Read(reinterpret_cast(name.data()), name.size(), language * 0x300) != name.size()) return {}; name.back() = '\0'; if (name.front()) return name.data(); } return {}; } // Read the source game's own NACP display version; this is game metadata, // NOT the executable NSO Build ID used by EdiZon cheat tables. Unknown or // unsupported encodings retain original in-game Nintendo artwork. std::string ReadGlyphDisplayVersion(const FileSys::VirtualDir& romfs) { if (!romfs) return {}; auto nacp = romfs->GetFile("control.nacp"); if (!nacp) nacp = romfs->GetFile("Control.nacp"); FileSys::RawNACP raw{}; if (!nacp || nacp->ReadObject(&raw) != sizeof(raw)) return {}; return Eden::GlyphVersion::FromNacp(raw.version_string); } // The supported-language flags of a control RomFS's NACP (bit n is NS ApplicationLanguage n); 0 // when they cannot be read. uint32_t ReadLanguages(const FileSys::VirtualDir& romfs) { auto nacp = romfs->GetFile("control.nacp"); if (!nacp) nacp = romfs->GetFile("Control.nacp"); FileSys::RawNACP raw{}; if (!nacp || nacp->ReadObject(&raw) != sizeof(raw)) return 0; return static_cast(raw.supported_languages); } // Game file -> its language flags, kept from the Library's metadata pass over the same data. std::map& GameLanguages() { static std::map languages; return languages; } FileSys::VirtualFile FindIcon(const FileSys::VirtualDir& romfs) { for (const char* language : language_names) { if (auto icon = romfs->GetFile(std::string{"icon_"} + language + ".dat")) return icon; } return {}; } bool WriteTga(const FileSys::VirtualFile& icon, const char* output) { const auto encoded = icon->ReadAllBytes(); int width = 0; int height = 0; int channels = 0; unsigned char* rgba = stbi_load_from_memory(encoded.data(), static_cast(encoded.size()), &width, &height, &channels, 4); if (!rgba || width <= 0 || height <= 0 || width > 4096 || height > 4096) { stbi_image_free(rgba); return false; } std::FILE* file = std::fopen(output, "wb"); if (!file) { stbi_image_free(rgba); return false; } unsigned char header[18]{}; header[2] = 2; header[12] = static_cast(width); header[13] = static_cast(width >> 8); header[14] = static_cast(height); header[15] = static_cast(height >> 8); header[16] = 32; header[17] = 0x28; bool ok = std::fwrite(header, 1, sizeof(header), file) == sizeof(header); std::vector row(static_cast(width) * 4); for (int y = 0; ok && y < height; ++y) { const unsigned char* source = rgba + static_cast(y) * row.size(); for (int x = 0; x < width; ++x) { row[4 * x + 0] = source[4 * x + 2]; row[4 * x + 1] = source[4 * x + 1]; row[4 * x + 2] = source[4 * x + 0]; row[4 * x + 3] = source[4 * x + 3]; } ok = std::fwrite(row.data(), 1, row.size(), file) == row.size(); } ok = std::fclose(file) == 0 && ok; stbi_image_free(rgba); return ok; } } uint64_t eden_game_title_id(const char* rom_path) { if (!rom_path) return 0; Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, Eden::AssetsPath("keys")); FileSys::RealVfsFilesystem vfs; const auto file = vfs.OpenFile(rom_path, FileSys::OpenMode::Read); if (!file) return 0; std::string path = rom_path; std::transform(path.begin(), path.end(), path.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (path.ends_with(".xci")) { FileSys::XCI image(file); return image.GetStatus() == Loader::ResultStatus::Success ? image.GetProgramTitleID() : 0; } if (path.ends_with(".nsp")) { FileSys::NSP package(file); return package.GetStatus() == Loader::ResultStatus::Success ? package.GetProgramTitleID() : 0; } return 0; } int eden_extract_game_metadata(const char* rom_path, const char* keys_dir, const char* cover_tga_path, char* title, size_t title_capacity) { if (!rom_path || !keys_dir || !cover_tga_path || !title || title_capacity == 0) return 0; title[0] = '\0'; Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, keys_dir); FileSys::RealVfsFilesystem vfs; const auto file = vfs.OpenFile(rom_path, FileSys::OpenMode::Read); if (!file) return 0; std::string path = rom_path; std::transform(path.begin(), path.end(), path.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); const bool xci = path.size() >= 4 && path.substr(path.size() - 4) == ".xci"; const auto romfs = OpenControlRomFs(file, xci); if (!romfs) return 0; GameLanguages()[rom_path] = ReadLanguages(romfs); int result = 0; const auto extracted_title = ReadTitle(romfs); if (!extracted_title.empty()) { std::snprintf(title, title_capacity, "%s", extracted_title.c_str()); result |= EDEN_METADATA_TITLE; } if (const auto icon = FindIcon(romfs); icon && WriteTga(icon, cover_tga_path)) result |= EDEN_METADATA_COVER; return result; } uint32_t eden_game_supported_languages(const char* rom_path, const char* keys_dir) { if (!rom_path || !keys_dir) return 0; if (const auto known = GameLanguages().find(rom_path); known != GameLanguages().end()) return known->second; try { Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, keys_dir); FileSys::RealVfsFilesystem vfs; const auto file = vfs.OpenFile(rom_path, FileSys::OpenMode::Read); if (!file) return 0; std::string path = rom_path; std::transform(path.begin(), path.end(), path.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); const auto romfs = OpenControlRomFs(file, path.ends_with(".xci")); if (!romfs) return 0; return GameLanguages()[rom_path] = ReadLanguages(romfs); } catch (const std::exception& error) { std::fprintf(stderr, "[ProsperoEden] languages: %s\n", error.what()); return 0; } } namespace { struct AddOns { bool update_present = false; std::string update; unsigned dlc = 0; uint32_t languages = 0; // the update's own language flags (its control data replaces the game's) }; // Base title ID -> its update and DLC files, from the last eden_scan_addons. std::map& ScannedAddOns() { static std::map scanned; return scanned; } bool& UpdatesScanCompleted() { static bool completed = false; return completed; } std::mutex& ScannedAddOnsMutex() { static std::mutex mutex; return mutex; } std::mutex& ScanAddOnsJobMutex() { static std::mutex mutex; return mutex; } // Both the map and completion flag are protected by ScannedAddOnsMutex. // A scanner builds its snapshot privately, publishing only at completion. // Never hold this mutex while reading/decrypting game files. } void eden_scan_addons(const char* updates_dir, const char* keys_dir) { // Serialise concurrent library rescans: otherwise an older scan can // publish after a later refresh, replacing fresh update metadata. // The job lock is independent of the short reader snapshot lock. const std::lock_guard scan_job_guard{ScanAddOnsJobMutex()}; // The library scan may overlap a game launch. Mutating a std::map while // the launcher reads metadata is undefined behavior, including crashes. // Build off-thread/off-lock and atomically publish a complete snapshot. std::map scanned; const auto publish = [&](bool complete) { std::lock_guard lock{ScannedAddOnsMutex()}; ScannedAddOns().swap(scanned); UpdatesScanCompleted() = complete; }; { std::lock_guard lock{ScannedAddOnsMutex()}; ScannedAddOns().clear(); UpdatesScanCompleted() = false; } if (!updates_dir || !keys_dir) return; try { Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, keys_dir); FileSys::RealVfsFilesystem vfs; auto directory = vfs.OpenDirectory(updates_dir, FileSys::OpenMode::Read); if (!directory) { // A player with no updates/ folder has *zero updates*, not an // unreadable update. Allow verified base-NACP art selection only // if the directory is genuinely ABSENT; permission/I/O failures // remain unknown and fail closed to original Nintendo visuals. std::error_code error; const bool exists = std::filesystem::exists(updates_dir, error); if (!error && !exists) publish(true); return; } const FileSys::ExternalContentProvider provider({std::move(directory)}); for (const auto& entry : provider.ListEntriesFilter(FileSys::TitleType::Update, std::nullopt, std::nullopt)) { auto& addons = scanned[FileSys::GetBaseTitleID(entry.title_id)]; addons.update_present = true; if (!addons.update.empty()) continue; // Newest first; the display version comes from the update's own control data. if (const auto versions = provider.ListUpdateVersions(entry.title_id); !versions.empty()) { addons.update = versions.front().version_string.empty() ? "v" + std::to_string(versions.front().version) : versions.front().version_string; } else if (const auto version = provider.GetEntryVersion(entry.title_id)) { addons.update = "v" + std::to_string(*version); } if (const auto control = provider.GetEntry(entry.title_id, FileSys::ContentRecordType::Control); control && control->GetStatus() == Loader::ResultStatus::Success) if (const auto romfs = control->GetRomFS()) if (const auto files = FileSys::ExtractRomFS(romfs)) addons.languages = ReadLanguages(files); } std::set dlc; for (const auto& entry : provider.ListEntriesFilter(FileSys::TitleType::AOC, std::nullopt, std::nullopt)) if (dlc.insert(entry.title_id).second) ++scanned[FileSys::GetBaseTitleID(entry.title_id)].dlc; publish(true); } catch (const std::exception& error) { std::fprintf(stderr, "[ProsperoEden] updates: %s\n", error.what()); // Fail closed. The incomplete scan never becomes visible to readers. } } // Native launch-time display-art version: take the detected update when one // exists, otherwise parse the base game's NACP. A missing/failed updates // scan must NOT mistake an updated game for an unpatched base version. int eden_game_glyph_display_version(const char* rom_path, const char* keys_dir, uint64_t title_id, char* output, size_t capacity) { if (output && capacity) output[0] = '\0'; if (!rom_path || !keys_dir || !title_id || !output || capacity == 0) return 0; try { std::string version; bool scanned_update = false; { const std::lock_guard lock{ScannedAddOnsMutex()}; if (!UpdatesScanCompleted()) return 0; const auto& entries = ScannedAddOns(); const auto found = entries.find(FileSys::GetBaseTitleID(title_id)); if (found != entries.end() && found->second.update_present) { // An installed update with unreadable version fails closed. scanned_update = true; version = found->second.update; } } if (!scanned_update) { Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, keys_dir); FileSys::RealVfsFilesystem vfs; const auto file = vfs.OpenFile(rom_path, FileSys::OpenMode::Read); if (!file) return 0; std::string extension = std::filesystem::path(rom_path).extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); const bool is_xci = extension == ".xci"; const auto control = OpenControlRomFs(file, is_xci); version = ReadGlyphDisplayVersion(control); } if (version.empty() || version.size() >= capacity) return 0; std::snprintf(output, capacity, "%s", version.c_str()); return 1; } catch (const std::exception& error) { std::fprintf(stderr, "[ProsperoEden] glyph source version: %s\n", error.what()); return 0; } } int eden_game_language(const char* rom_path, const char* keys_dir, uint64_t title_id, int chosen) { const auto& codes = Service::Set::available_language_codes; if (chosen < 0 || chosen >= static_cast(codes.size())) return chosen; uint32_t supported = 0; { const std::lock_guard lock{ScannedAddOnsMutex()}; const auto& scanned = ScannedAddOns(); if (const auto found = scanned.find(FileSys::GetBaseTitleID(title_id)); title_id && found != scanned.end() && !found->second.update.empty()) supported = found->second.languages; } if (!supported) supported = eden_game_supported_languages(rom_path, keys_dir); namespace NS = Service::NS; const auto application = NS::ConvertToApplicationLanguage(codes[static_cast(chosen)]); const auto* priorities = application ? NS::GetApplicationLanguagePriorityList(*application) : nullptr; if (!supported || !priorities) return chosen; for (const auto language : *priorities) { if ((supported & NS::GetSupportedLanguageFlag(language)) == 0) continue; const auto match = NS::ConvertToLanguageCode(language); for (std::size_t i = 0; match && i < codes.size(); ++i) if (codes[i] == *match) return static_cast(i); } return chosen; } int eden_game_addons(uint64_t title_id, char* update_version, size_t capacity, unsigned* dlc_count) { if (update_version && capacity) update_version[0] = '\0'; if (dlc_count) *dlc_count = 0; AddOns snapshot; { const std::lock_guard lock{ScannedAddOnsMutex()}; const auto& scanned = ScannedAddOns(); const auto found = scanned.find(FileSys::GetBaseTitleID(title_id)); if (!title_id || found == scanned.end()) return 0; snapshot = found->second; } if (update_version && capacity) std::snprintf(update_version, capacity, "%s", snapshot.update.c_str()); if (dlc_count) *dlc_count = snapshot.dlc; return !snapshot.update.empty() || snapshot.dlc != 0; } namespace { // The save folder name of the profile games use: Eden's user 0, the first valid entry of // profiles.dat (0x10-byte header, then 0xC8-byte entries starting with the 16-byte UUID), written // as Eden's save paths spell it (upper 64 bits, then lower). Eden creates the file on the first // game boot; without it the one existing user save folder is used. std::string EdenUserFolder(const std::filesystem::path& saves, std::string& error) { std::vector data; const std::string profiles = Eden::UserDir() + "/nand/system/save/8000000000000010/su/avators/profiles.dat"; if (Eden::RyujinxSaves::ReadFile(profiles, data) && data.size() >= 0x650) { for (std::size_t offset = 0x10; offset + 0xC8 <= 0x650; offset += 0xC8) { uint64_t low = 0, high = 0; std::memcpy(&low, data.data() + offset, sizeof(low)); std::memcpy(&high, data.data() + offset + 8, sizeof(high)); if (!low && !high) continue; char name[33]; std::snprintf(name, sizeof(name), "%016llX%016llX", static_cast(high), static_cast(low)); return name; } } std::error_code list_error; std::vector users; for (const auto& entry : Eden::RyujinxSaves::ListFolder(saves, list_error)) { const std::string name = entry.path().filename().string(); if (name.size() == 32 && name != std::string(32, '0')) users.push_back(name); } if (users.size() == 1) return users.front(); error = "start any game once first, so ProsperoEden creates its user"; return {}; } } // namespace namespace { struct SaveSource { int kind = EDEN_SAVE_NONE; std::vector saves; }; // A save folder copied by hand comes before a Ryujinx data folder: it names the game itself. SaveSource FindSaveSource(uint64_t title_id) { std::string error; SaveSource source; source.saves = Eden::RyujinxSaves::FindFolderSaves(Eden::AssetsPath("save-import"), title_id, error); if (!source.saves.empty()) { source.kind = EDEN_SAVE_FOLDER; return source; } source.saves = Eden::RyujinxSaves::FindSaves(Eden::AssetsPath("ryujinx"), title_id, error); if (!source.saves.empty()) source.kind = EDEN_SAVE_RYUJINX; return source; } std::string TitleName(uint64_t title_id) { char title[17]; std::snprintf(title, sizeof(title), "%016llX", static_cast(title_id)); return title; } std::string TimeStamp() { char stamp[32] = "now"; const std::time_t now = std::time(nullptr); if (const std::tm* local = std::localtime(&now)) std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", local); return stamp; } void PutPath(char* path, size_t capacity, const std::string& value) { if (path && capacity) std::snprintf(path, capacity, "%s", value.c_str()); } } // namespace int eden_save_import_source(uint64_t title_id) { return title_id ? FindSaveSource(title_id).kind : EDEN_SAVE_NONE; } int eden_save_import(uint64_t title_id, char* path, size_t capacity) { namespace fs = std::filesystem; PutPath(path, capacity, ""); const SaveSource source = title_id ? FindSaveSource(title_id) : SaveSource{}; if (source.saves.empty()) return EDEN_SAVE_NOTHING; std::string error; const fs::path root = fs::path{Eden::UserDir()} / "nand" / "user" / "save" / "0000000000000000"; const std::string user = EdenUserFolder(root, error); if (user.empty()) { Eden::Report("save import", error.c_str()); return EDEN_SAVE_NO_USER; } const std::string title = TitleName(title_id); const fs::path backups = fs::path{Eden::BackupDir()} / "save-import"; bool replaced = false; if (!Eden::RyujinxSaves::Import(source.saves, root / user / title, root / std::string(32, '0') / title, backups, title + "-" + TimeStamp(), error, &replaced)) { Eden::Report("save import", error.c_str()); return EDEN_SAVE_FAILED; } if (replaced) PutPath(path, capacity, backups.string()); Eden::Report("save import", (title + (source.kind == EDEN_SAVE_FOLDER ? " from save-import" : " from ryujinx")).c_str()); return EDEN_SAVE_DONE; } int eden_save_export(uint64_t title_id, char* path, size_t capacity) { namespace fs = std::filesystem; PutPath(path, capacity, ""); if (!title_id) return EDEN_SAVE_NOTHING; std::string error; const fs::path root = fs::path{Eden::UserDir()} / "nand" / "user" / "save" / "0000000000000000"; const std::string user = EdenUserFolder(root, error); if (user.empty()) return EDEN_SAVE_NOTHING; const std::string title = TitleName(title_id); const fs::path target = fs::path{Eden::AssetsPath("save-export")} / (title + "-" + TimeStamp()); if (!Eden::RyujinxSaves::Export(root / user / title, root / std::string(32, '0') / title, target, error)) { Eden::Report("save export", error.c_str()); return error == "no save yet" ? EDEN_SAVE_NOTHING : EDEN_SAVE_FAILED; } PutPath(path, capacity, target.string()); Eden::Report("save export", target.string().c_str()); return EDEN_SAVE_DONE; } const char* eden_startup_error() { static const std::string error = []() -> std::string { try { const std::string keys = Eden::AssetsPath("keys"); const std::string firmware = Eden::AssetsPath("firmware"); Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, keys); FileSys::RealVfsFilesystem vfs; const auto prod = vfs.OpenFile(std::string{keys} + "/prod.keys", FileSys::OpenMode::Read); if (!prod || !prod->GetSize()) return "Missing or empty keys/prod.keys in " + Eden::AssetsDir() + "."; auto& manager = Core::Crypto::KeyManager::Instance(); if (!manager.HasKey(Core::Crypto::S256KeyType::Header)) return "prod.keys could not supply an NCA header key. Replace it with a valid key dump."; std::error_code ec; #if defined(__PROSPERO__) const auto entries = Eden::ReadNativeDirectory(firmware, ec); #else std::vector entries; for (std::filesystem::directory_iterator it{firmware, ec}, end; !ec && it != end; it.increment(ec)) entries.push_back(*it); #endif if (ec) return "Cannot read firmware/ in " + Eden::AssetsDir() + ". Install extracted firmware NCAs."; unsigned count = 0; bool version = false; for (const auto& entry : entries) { if (entry.path().extension() != ".nca") continue; ++count; const auto file = vfs.OpenFile(entry.path().string(), FileSys::OpenMode::Read); if (!file) return "A firmware NCA cannot be read. Reinstall the firmware dump."; FileSys::NCA nca(file); if (nca.GetStatus() != Loader::ResultStatus::Success) return "Firmware NCA validation failed (code " + std::to_string(static_cast(nca.GetStatus())) + "). Check that firmware and prod.keys are compatible."; if (nca.GetTitleId() == 0x0100000000000809ULL && nca.GetRomFS()) { const auto romfs = FileSys::ExtractRomFS(nca.GetRomFS()); const auto file = romfs ? romfs->GetFile("file") : FileSys::VirtualFile{}; version |= file && file->GetSize() >= 0x100 && file->ReadBytes(0x100).size() == 0x100; } } if (!count) return "No firmware NCAs found in " + Eden::AssetsPath("firmware") + "."; if (!version) return "Firmware SystemVersion data is missing or unreadable. Install a complete firmware dump."; return {}; } catch (...) { return "Setup validation failed. Check that firmware and key files are readable and valid."; } }(); return error.c_str(); }