diff --git a/src/citron/configuration/configure_general.cpp b/src/citron/configuration/configure_general.cpp index 891cab7ea..a1a2f7d3e 100644 --- a/src/citron/configuration/configure_general.cpp +++ b/src/citron/configuration/configure_general.cpp @@ -2,21 +2,23 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include #include #include #include +#include +#include +#include +#include #include -#include "common/settings.h" -#include "core/core.h" -#include "ui_configure_general.h" #include "citron/configuration/configuration_shared.h" #include "citron/configuration/configure_general.h" #include "citron/configuration/shared_widget.h" #include "citron/uisettings.h" +#include "common/settings.h" +#include "core/core.h" +#include "ui_configure_general.h" -ConfigureGeneral::ConfigureGeneral(const Core::System& system_, +ConfigureGeneral::ConfigureGeneral(Core::System& system_, std::shared_ptr> group_, const ConfigurationShared::Builder& builder, QWidget* parent) : Tab(group_, parent), ui{std::make_unique()}, system{system_} { @@ -33,12 +35,20 @@ ConfigureGeneral::ConfigureGeneral(const Core::System& system_, ui->button_reset_defaults->setVisible(false); } - ui->check_for_updates_checkbox->setChecked(UISettings::values.check_for_updates_on_start.GetValue()); + ui->check_for_updates_checkbox->setChecked( + UISettings::values.check_for_updates_on_start.GetValue()); + + connect(ui->button_add_external_content, &QPushButton::clicked, this, + &ConfigureGeneral::AddExternalContentDir); + connect(ui->button_remove_external_content, &QPushButton::clicked, this, + &ConfigureGeneral::RemoveExternalContentDir); } ConfigureGeneral::~ConfigureGeneral() = default; -void ConfigureGeneral::SetConfiguration() {} +void ConfigureGeneral::SetConfiguration() { + RefreshExternalContentList(); +} void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) { QLayout& general_layout = *ui->general_widget->layout(); @@ -58,10 +68,10 @@ void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) { push(UISettings::values.linkage.by_category[Settings::Category::UiGeneral]); push(Settings::values.linkage.by_category[Settings::Category::Linux]); - // Only show Linux group on Unix - #ifndef __unix__ +// Only show Linux group on Unix +#ifndef __unix__ ui->LinuxGroupBox->setVisible(false); - #endif +#endif for (const auto setting : settings) { auto* widget = builder.BuildWidget(setting, apply_funcs); @@ -75,14 +85,14 @@ void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) { } switch (setting->GetCategory()) { - case Settings::Category::UiGeneral: - general_hold.emplace(setting->Id(), widget); - break; - case Settings::Category::Linux: - linux_hold.emplace(setting->Id(), widget); - break; - default: - widget->deleteLater(); + case Settings::Category::UiGeneral: + general_hold.emplace(setting->Id(), widget); + break; + case Settings::Category::Linux: + linux_hold.emplace(setting->Id(), widget); + break; + default: + widget->deleteLater(); } } @@ -93,26 +103,26 @@ void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) { linux_layout.addWidget(widget); } - // --- Manually add Wayland setting to the Linux UI group --- - #ifdef __linux__ +// --- Manually add Wayland setting to the Linux UI group --- +#ifdef __linux__ // This logic only runs if the user is on a Wayland session. if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland"))) { // Create a new, clean checkbox. auto wayland_checkbox = new QCheckBox(tr("Enable Wayland Performance Optimizations")); - wayland_checkbox->setToolTip(tr("Use Wayland-specific presentation modes to reduce input latency and improve smoothness.")); + wayland_checkbox->setToolTip(tr("Use Wayland-specific presentation modes to reduce input " + "latency and improve smoothness.")); // Set its initial checked state from our hidden setting. wayland_checkbox->setChecked(Settings::values.is_wayland_platform.GetValue()); // Connect the checkbox so it toggles our hidden setting. - connect(wayland_checkbox, &QCheckBox::toggled, this, [](bool checked) { - Settings::values.is_wayland_platform.SetValue(checked); - }); + connect(wayland_checkbox, &QCheckBox::toggled, this, + [](bool checked) { Settings::values.is_wayland_platform.SetValue(checked); }); // Add our new, clean checkbox to the Linux layout. linux_layout.addWidget(wayland_checkbox); } - #endif +#endif } // Called to set the callback when resetting settings to defaults @@ -136,7 +146,16 @@ void ConfigureGeneral::ResetDefaults() { void ConfigureGeneral::ApplyConfiguration() { - UISettings::values.check_for_updates_on_start.SetValue(ui->check_for_updates_checkbox->isChecked()); + UISettings::values.check_for_updates_on_start.SetValue( + ui->check_for_updates_checkbox->isChecked()); + + std::vector new_external_dirs; + for (int i = 0; i < ui->external_content_list->count(); ++i) { + new_external_dirs.push_back(ui->external_content_list->item(i)->text().toStdString()); + } + Settings::values.external_content_dirs = std::move(new_external_dirs); + + system.RefreshExternalContent(); bool powered_on = system.IsPoweredOn(); for (const auto& func : apply_funcs) { @@ -144,6 +163,31 @@ void ConfigureGeneral::ApplyConfiguration() { } } +void ConfigureGeneral::RefreshExternalContentList() { + ui->external_content_list->clear(); + for (const auto& dir : Settings::values.external_content_dirs) { + ui->external_content_list->addItem(QString::fromStdString(dir)); + } +} + +void ConfigureGeneral::AddExternalContentDir() { + const QString dir = QFileDialog::getExistingDirectory(this, tr("Select Content Directory")); + if (dir.isEmpty()) { + return; + } + // Check if valid and not duplicate (optional but good UI) + for (int i = 0; i < ui->external_content_list->count(); ++i) { + if (ui->external_content_list->item(i)->text() == dir) { + return; + } + } + ui->external_content_list->addItem(dir); +} + +void ConfigureGeneral::RemoveExternalContentDir() { + qDeleteAll(ui->external_content_list->selectedItems()); +} + void ConfigureGeneral::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { RetranslateUI(); diff --git a/src/citron/configuration/configure_general.h b/src/citron/configuration/configure_general.h index 872b58b59..e4059b07c 100644 --- a/src/citron/configuration/configure_general.h +++ b/src/citron/configuration/configure_general.h @@ -28,7 +28,7 @@ class ConfigureGeneral : public ConfigurationShared::Tab { Q_OBJECT public: - explicit ConfigureGeneral(const Core::System& system_, + explicit ConfigureGeneral(Core::System& system_, std::shared_ptr> group, const ConfigurationShared::Builder& builder, QWidget* parent = nullptr); @@ -41,6 +41,9 @@ public: private: void Setup(const ConfigurationShared::Builder& builder); + void RefreshExternalContentList(); + void AddExternalContentDir(); + void RemoveExternalContentDir(); void changeEvent(QEvent* event) override; void RetranslateUI(); @@ -51,5 +54,5 @@ private: std::vector> apply_funcs{}; - const Core::System& system; + Core::System& system; }; diff --git a/src/citron/configuration/configure_general.ui b/src/citron/configuration/configure_general.ui index fe11e1c62..aeb57c13a 100644 --- a/src/citron/configuration/configure_general.ui +++ b/src/citron/configuration/configure_general.ui @@ -80,6 +80,53 @@ + + + + External Content Directories + + + + + + true + + + + + + + + + Add + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + diff --git a/src/citron/configuration/qt_config.cpp b/src/citron/configuration/qt_config.cpp index ab299e830..92ac78856 100644 --- a/src/citron/configuration/qt_config.cpp +++ b/src/citron/configuration/qt_config.cpp @@ -97,9 +97,12 @@ void QtConfig::ReadQtPlayerValues(const std::size_t player_index) { } } - const auto body_color_str = ReadStringSetting(std::string(player_prefix).append("body_color"), fmt::format("{:x}", Settings::DEFAULT_CONTROLLER_COLOR)); + const auto body_color_str = + ReadStringSetting(std::string(player_prefix).append("body_color"), + fmt::format("{:x}", Settings::DEFAULT_CONTROLLER_COLOR)); player.body_color = std::stoul(body_color_str, nullptr, 16); - player.gyro_overlay_visible = ReadBooleanSetting(std::string(player_prefix).append("gyro_overlay_visible"), true); + player.gyro_overlay_visible = + ReadBooleanSetting(std::string(player_prefix).append("gyro_overlay_visible"), true); for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]); @@ -229,6 +232,18 @@ void QtConfig::ReadPathValues() { UISettings::values.game_dirs.append(game_dir); } } + + const int external_dirs_size = BeginArray(std::string("external_content_dirs")); + Settings::values.external_content_dirs.clear(); + for (int i = 0; i < external_dirs_size; ++i) { + SetArrayIndex(i); + const std::string path = ReadStringSetting(std::string("path")); + if (!path.empty()) { + Settings::values.external_content_dirs.push_back(path); + } + } + EndArray(); + UISettings::values.recent_files = QString::fromStdString(ReadStringSetting(std::string("recentFiles"))) .split(QStringLiteral(", "), Qt::SkipEmptyParts, Qt::CaseSensitive); @@ -371,8 +386,11 @@ void QtConfig::SaveQtPlayerValues(const std::size_t player_index) { return; } - WriteStringSetting(std::string(player_prefix).append("body_color"), fmt::format("{:x}", player.body_color), fmt::format("{:x}", Settings::DEFAULT_CONTROLLER_COLOR)); - WriteBooleanSetting(std::string(player_prefix).append("gyro_overlay_visible"), player.gyro_overlay_visible, true); + WriteStringSetting(std::string(player_prefix).append("body_color"), + fmt::format("{:x}", player.body_color), + fmt::format("{:x}", Settings::DEFAULT_CONTROLLER_COLOR)); + WriteBooleanSetting(std::string(player_prefix).append("gyro_overlay_visible"), + player.gyro_overlay_visible, true); for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]); @@ -451,6 +469,13 @@ void QtConfig::SavePathValues() { } EndArray(); + BeginArray(std::string("external_content_dirs")); + for (size_t i = 0; i < Settings::values.external_content_dirs.size(); ++i) { + SetArrayIndex(static_cast(i)); + WriteStringSetting(std::string("path"), Settings::values.external_content_dirs[i]); + } + EndArray(); + WriteStringSetting(std::string("recentFiles"), UISettings::values.recent_files.join(QStringLiteral(", ")).toStdString()); diff --git a/src/citron/configuration/shared_translation.cpp b/src/citron/configuration/shared_translation.cpp index 2b427c8e0..2c6e42fe3 100644 --- a/src/citron/configuration/shared_translation.cpp +++ b/src/citron/configuration/shared_translation.cpp @@ -299,6 +299,8 @@ std::unique_ptr InitializeTranslations(QWidget* parent) { // Renderer (Debug) + // Renderer (Debug) + // System INSERT(Settings, rng_seed, tr("RNG Seed"), tr("Controls the seed of the random number generator.\nMainly used for speedrunning " diff --git a/src/common/settings.h b/src/common/settings.h index 3bd91a73e..876239f62 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -793,6 +793,8 @@ struct Values { // Add-Ons std::map> disabled_addons; + std::vector external_content_dirs; + // Cheats // Key: build_id (hex string), Value: set of disabled cheat names std::map> disabled_cheats; diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 93e0c1360..09fae1eee 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -901,6 +901,8 @@ inline u32 EnumMetadata::Index() { return 28; } +ENUM(SpirvOptimizeMode, Never, Always, BestEffort); + template inline std::string CanonicalizeEnum(Type id) { const auto group = EnumMetadata::Canonicalizations(); diff --git a/src/core/core.cpp b/src/core/core.cpp index 0e45db065..f444f5045 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -24,8 +24,9 @@ #ifndef NOMINMAX #define NOMINMAX #endif -#include #include +#include + #undef GetCurrentTime #undef ERROR @@ -196,8 +197,12 @@ struct System::Impl { } void ReinitializeIfNecessary(System& system) { - const bool layout_changed = extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb); - const bool must_reinitialize = !device_memory || is_multicore != Settings::values.use_multi_core.GetValue() || layout_changed; + const bool layout_changed = + extended_memory_layout != + (Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb); + const bool must_reinitialize = !device_memory || + is_multicore != Settings::values.use_multi_core.GetValue() || + layout_changed; if (!must_reinitialize) { return; @@ -211,7 +216,8 @@ struct System::Impl { // Update the tracked values before re-initializing is_multicore = Settings::values.use_multi_core.GetValue(); - extended_memory_layout = (Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb); + extended_memory_layout = + (Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_4Gb); Initialize(system); } @@ -459,10 +465,14 @@ struct System::Impl { if (perf_stats) { const auto perf_results = GetAndResetPerfStats(); constexpr auto performance = Common::Telemetry::FieldType::Performance; - telemetry_session->AddField(performance, "Shutdown_EmulationSpeed", perf_results.emulation_speed * 100.0); - telemetry_session->AddField(performance, "Shutdown_Framerate", perf_results.average_game_fps); - telemetry_session->AddField(performance, "Shutdown_Frametime", perf_results.frametime * 1000.0); - telemetry_session->AddField(performance, "Mean_Frametime_MS", perf_stats->GetMeanFrametime()); + telemetry_session->AddField(performance, "Shutdown_EmulationSpeed", + perf_results.emulation_speed * 100.0); + telemetry_session->AddField(performance, "Shutdown_Framerate", + perf_results.average_game_fps); + telemetry_session->AddField(performance, "Shutdown_Frametime", + perf_results.frametime * 1000.0); + telemetry_session->AddField(performance, "Mean_Frametime_MS", + perf_stats->GetMeanFrametime()); } is_powered_on = false; @@ -504,27 +514,29 @@ struct System::Impl { Network::RestartSocketOperations(); arp_manager.ResetAll(); - if (device_memory) { - #ifdef __linux__ - madvise(device_memory->buffer.BackingBasePointer(), device_memory->buffer.backing_size, MADV_DONTNEED); +#ifdef __linux__ + madvise(device_memory->buffer.BackingBasePointer(), device_memory->buffer.backing_size, + MADV_DONTNEED); - // Only call malloc_trim on non-Android Linux (glibc) - #ifndef __ANDROID__ +// Only call malloc_trim on non-Android Linux (glibc) +#ifndef __ANDROID__ malloc_trim(0); - #endif +#endif // Give the kernel time to update /proc/stats std::this_thread::sleep_for(std::chrono::milliseconds(20)); - #elif defined(_WIN32) - VirtualAlloc(device_memory->buffer.BackingBasePointer(), device_memory->buffer.backing_size, MEM_RESET, PAGE_READWRITE); - #endif +#elif defined(_WIN32) + VirtualAlloc(device_memory->buffer.BackingBasePointer(), + device_memory->buffer.backing_size, MEM_RESET, PAGE_READWRITE); +#endif } const u64 mem_after = GetCurrentRSS(); const u64 shaved = (mem_before > mem_after) ? (mem_before - mem_after) : 0; - LOG_INFO(Core, "Shutdown Memory Audit: [Before: {}MB] -> [After: {}MB] | Total Shaved: {}MB", + LOG_INFO(Core, + "Shutdown Memory Audit: [Before: {}MB] -> [After: {}MB] | Total Shaved: {}MB", mem_before, mem_after, shaved); LOG_DEBUG(Core, "Shutdown OK"); @@ -1101,6 +1113,13 @@ void System::ApplySettings() { if (IsPoweredOn()) { Renderer().RefreshBaseSettings(); } + if (IsPoweredOn()) { + Renderer().RefreshBaseSettings(); + } +} + +void System::RefreshExternalContent() { + impl->fs_controller.RefreshExternalContentProvider(); } } // namespace Core diff --git a/src/core/core.h b/src/core/core.h index c6835602b..7517a819f 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -467,6 +467,9 @@ public: /// Applies any changes to settings to this core instance. void ApplySettings(); + /// Refreshes the external content provider with the latest settings. + void RefreshExternalContent(); + private: struct Impl; std::unique_ptr impl; diff --git a/src/core/file_sys/directory_save_data_filesystem.cpp b/src/core/file_sys/directory_save_data_filesystem.cpp index cbcc75c3b..c4eee713f 100644 --- a/src/core/file_sys/directory_save_data_filesystem.cpp +++ b/src/core/file_sys/directory_save_data_filesystem.cpp @@ -5,8 +5,8 @@ #include #include "common/logging/log.h" #include "common/settings.h" -#include "core/file_sys/errors.h" #include "core/file_sys/directory_save_data_filesystem.h" +#include "core/file_sys/errors.h" namespace FileSys { @@ -21,12 +21,9 @@ constexpr int RetryWaitTimeMs = 100; DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem, VirtualDir mirror_filesystem) - : base_fs(std::move(base_filesystem)), - backup_fs(std::move(backup_filesystem)), - mirror_fs(std::move(mirror_filesystem)), - extra_data_accessor(base_fs), - journaling_enabled(true), - open_writable_files(0) {} + : base_fs(std::move(base_filesystem)), backup_fs(std::move(backup_filesystem)), + mirror_fs(std::move(mirror_filesystem)), extra_data_accessor(base_fs), + journaling_enabled(true), open_writable_files(0) {} DirectorySaveDataFileSystem::~DirectorySaveDataFileSystem() = default; @@ -166,7 +163,7 @@ bool DirectorySaveDataFileSystem::HasUncommittedChanges() const { } Result DirectorySaveDataFileSystem::SynchronizeDirectory(const char* dest_name, - const char* source_name) { + const char* source_name) { auto source_dir = base_fs->GetSubdirectory(source_name); if (source_dir == nullptr) { return ResultPathNotFound; @@ -217,7 +214,8 @@ Result DirectorySaveDataFileSystem::CopyDirectoryRecursively(VirtualDir dest, Vi return ResultSuccess; } -Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(std::function operation) { +Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked( + std::function operation) { int remaining_retries = MaxRetryCount; while (true) { @@ -240,7 +238,8 @@ Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(std::functionIsWritable()) { return; diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 5a68afd3d..936990e3a 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -105,22 +105,26 @@ void AppendCommaIfNotEmpty(std::string& to, std::string_view with) { } bool IsValidModDir(const VirtualDir& dir) { - if (!dir) return false; + if (!dir) + return false; return FindSubdirectoryCaseless(dir, "exefs") != nullptr || FindSubdirectoryCaseless(dir, "romfs") != nullptr || FindSubdirectoryCaseless(dir, "romfslite") != nullptr || FindSubdirectoryCaseless(dir, "cheats") != nullptr; } -std::vector GetEnabledModsList(u64 title_id, const Service::FileSystem::FileSystemController& fs_controller) { +std::vector GetEnabledModsList( + u64 title_id, const Service::FileSystem::FileSystemController& fs_controller) { std::vector mods; const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); - if (!load_dir) return mods; + if (!load_dir) + return mods; const auto& disabled = Settings::values.disabled_addons[title_id]; for (const auto& top_dir : load_dir->GetSubdirectories()) { - if (!top_dir) continue; + if (!top_dir) + continue; // If it's a mod directory (has exefs/romfs), check if it's disabled. if (IsValidModDir(top_dir)) { @@ -132,11 +136,14 @@ std::vector GetEnabledModsList(u64 title_id, const Service::FileSyst for (const auto& sub_dir : top_dir->GetSubdirectories()) { if (sub_dir && IsValidModDir(sub_dir)) { std::string internal_name = top_dir->GetName() + "/" + sub_dir->GetName(); - // First check if the full nested path is disabled, then check if just the subfolder name is disabled. - if (std::find(disabled.begin(), disabled.end(), internal_name) == disabled.end() && - std::find(disabled.begin(), disabled.end(), sub_dir->GetName()) == disabled.end()) { + // First check if the full nested path is disabled, then check if just the + // subfolder name is disabled. + if (std::find(disabled.begin(), disabled.end(), internal_name) == + disabled.end() && + std::find(disabled.begin(), disabled.end(), sub_dir->GetName()) == + disabled.end()) { mods.push_back(sub_dir); - } + } } } } @@ -172,15 +179,21 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); const auto updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); if (updates_dir) { - const auto base_program_nca = content_provider.GetEntry(title_id, ContentRecordType::Program); - if(base_program_nca){ + const auto base_program_nca = + content_provider.GetEntry(title_id, ContentRecordType::Program); + if (base_program_nca) { for (const auto& mod : updates_dir->GetSubdirectories()) { - if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == disabled.cend()) { + if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == + disabled.cend()) { for (const auto& file : mod->GetFiles()) { if (file->GetExtension() == "nca") { NCA nca(file, base_program_nca.get()); - if (nca.GetStatus() == Loader::ResultStatus::Success && nca.GetType() == NCAContentType::Program) { - LOG_INFO(Loader, " ExeFS: Autoloader Update ({}) applied successfully", mod->GetName()); + if (nca.GetStatus() == Loader::ResultStatus::Success && + nca.GetType() == NCAContentType::Program) { + LOG_INFO( + Loader, + " ExeFS: Autoloader Update ({}) applied successfully", + mod->GetName()); exefs = nca.GetExeFS(); autoloader_update_applied = true; break; @@ -188,21 +201,110 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { } } } - if (autoloader_update_applied) break; + if (autoloader_update_applied) + break; } } } } // --- NAND UPDATE (FALLBACK) --- + // --- NAND/External UPDATE (FALLBACK) --- if (!autoloader_update_applied) { - const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + // Find the highest version enabled update + u32 best_version = 0; + VirtualFile best_update_raw = nullptr; + bool found_best = false; + + // 1. External Updates + const auto* content_provider_union = + dynamic_cast(&content_provider); + if (content_provider_union) { + const auto* external_provider = content_provider_union->GetExternalProvider(); + if (external_provider) { + const auto updates = external_provider->ListUpdateVersions(title_id); + for (const auto& update : updates) { + const auto name = fmt::format("Update {}", update.version_string); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + if (!patch_disabled) { + if (!found_best || update.version > best_version) { + best_version = update.version; + best_update_raw = external_provider->GetEntryForVersion( + title_id, ContentRecordType::Program, update.version); + found_best = true; + } + } + } + } + } + + // 2. System Updates const auto update_tid = GetUpdateTitleID(title_id); - const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program); - if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) { - LOG_INFO(Loader, " ExeFS: NAND Update ({}) applied successfully", - FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); - exefs = update->GetExeFS(); + PatchManager update_mgr{update_tid, fs_controller, content_provider}; + const auto metadata = update_mgr.GetControlMetadata(); + const auto& nacp = metadata.first; + + if (nacp) { + const auto version_str = nacp->GetVersionString(); + const auto name = + fmt::format("Update v{}", version_str); // Matches GetPatches NACP branch + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + if (!patch_disabled) { + const auto sys_ver = content_provider.GetEntryVersion(update_tid).value_or(0); + if (!found_best || sys_ver > best_version) { + best_version = sys_ver; + best_update_raw = + content_provider.GetEntryRaw(update_tid, ContentRecordType::Program); + found_best = true; + } + } + } else if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { + const auto meta_ver = content_provider.GetEntryVersion(update_tid); + if (meta_ver.value_or(0) != 0) { + const auto version_str = FormatTitleVersion(*meta_ver); + const auto name = + fmt::format("Update {}", version_str); // Matches GetPatches fallback + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + if (!patch_disabled) { + const auto sys_ver = *meta_ver; + if (!found_best || sys_ver > best_version) { + best_version = sys_ver; + best_update_raw = + content_provider.GetEntryRaw(update_tid, ContentRecordType::Program); + found_best = true; + } + } + } + } + + // Apply the best update found + if (found_best && best_update_raw != nullptr) { + // We need base_program_nca for patching + const auto base_program_nca = + content_provider.GetEntry(title_id, ContentRecordType::Program); + + if (base_program_nca) { + const auto new_nca = std::make_shared(best_update_raw, base_program_nca.get()); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetExeFS() != nullptr) { + LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully", + FormatTitleVersion(best_version)); + exefs = new_nca->GetExeFS(); + } + } else { + // Fallback if no base program (unlikely for patching ExeFS) + // Or if it's a type that doesn't strictly need base? + const auto new_nca = std::make_shared(best_update_raw, nullptr); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetExeFS() != nullptr) { + LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully (No Base)", + FormatTitleVersion(best_version)); + exefs = new_nca->GetExeFS(); + } + } } } @@ -215,22 +317,24 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { patch_dirs.push_back(sdmc_load_dir); } - std::sort(patch_dirs.begin(), patch_dirs.end(), - [](const VirtualDir& l, const VirtualDir& r) { - if (!l) return true; - if (!r) return false; - return l->GetName() < r->GetName(); - }); + std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { + if (!l) + return true; + if (!r) + return false; + return l->GetName() < r->GetName(); + }); std::vector layers; layers.reserve(patch_dirs.size() + 1); for (const auto& subdir : patch_dirs) { - if (!subdir) continue; + if (!subdir) + continue; auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs"); if (exefs_dir != nullptr) layers.push_back(std::move(exefs_dir)); } - if(exefs) { + if (exefs) { layers.push_back(exefs); } auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); @@ -255,19 +359,24 @@ std::vector PatchManager::CollectPatches(const std::vector out; out.reserve(patch_dirs.size()); for (const auto& subdir : patch_dirs) { - if (!subdir) continue; + if (!subdir) + continue; auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs"); if (exefs_dir != nullptr) { for (const auto& file : exefs_dir->GetFiles()) { if (file->GetExtension() == "ips") { auto name = file->GetName(); - const auto this_build_id = fmt::format("{:0<64}", name.substr(0, name.find('.'))); - if (nso_build_id == this_build_id) out.push_back(file); + const auto this_build_id = + fmt::format("{:0<64}", name.substr(0, name.find('.'))); + if (nso_build_id == this_build_id) + out.push_back(file); } else if (file->GetExtension() == "pchtxt") { IPSwitchCompiler compiler{file}; - if (!compiler.IsValid()) continue; + if (!compiler.IsValid()) + continue; const auto this_build_id = Common::HexToString(compiler.GetBuildID()); - if (nso_build_id == this_build_id) out.push_back(file); + if (nso_build_id == this_build_id) + out.push_back(file); } } } @@ -381,19 +490,21 @@ static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType t patch_dirs.push_back(sdmc_load_dir); } - std::sort(patch_dirs.begin(), patch_dirs.end(), - [](const VirtualDir& l, const VirtualDir& r) { - if (!l) return true; - if (!r) return false; - return l->GetName() < r->GetName(); - }); + std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { + if (!l) + return true; + if (!r) + return false; + return l->GetName() < r->GetName(); + }); std::vector layers; std::vector layers_ext; layers.reserve(patch_dirs.size() + 1); layers_ext.reserve(patch_dirs.size() + 1); for (const auto& subdir : patch_dirs) { - if (!subdir) continue; + if (!subdir) + continue; auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs"); if (romfs_dir != nullptr) layers.emplace_back(std::move(romfs_dir)); // REMOVED CachedVfsDirectory hang @@ -448,18 +559,23 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs if (type == ContentRecordType::Program) { VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); + const auto autoloader_updates_path = + fmt::format("autoloader/{:016X}/Updates", title_id); const auto updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); if (updates_dir) { for (const auto& mod : updates_dir->GetSubdirectories()) { - if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == disabled.cend()) { + if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == + disabled.cend()) { for (const auto& file : mod->GetFiles()) { if (file->GetExtension() == "nca") { const auto new_nca = std::make_shared(file, base_nca); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetType() == NCAContentType::Program && new_nca->GetRomFS() != nullptr) { - LOG_INFO(Loader, " RomFS: Autoloader Update ({}) applied successfully", mod->GetName()); + LOG_INFO( + Loader, + " RomFS: Autoloader Update ({}) applied successfully", + mod->GetName()); romfs = new_nca->GetRomFS(); autoloader_update_applied = true; break; @@ -467,29 +583,131 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs } } } - if (autoloader_update_applied) break; + if (autoloader_update_applied) + break; } } } } if (!autoloader_update_applied) { - const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); - const auto update_tid = GetUpdateTitleID(title_id); - const auto update_raw = content_provider.GetEntryRaw(update_tid, type); - if (!update_disabled && update_raw != nullptr && base_nca != nullptr) { - const auto new_nca = std::make_shared(update_raw, base_nca); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetRomFS() != nullptr) { - LOG_INFO(Loader, " RomFS: NAND Update ({}) applied successfully", - FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); - romfs = new_nca->GetRomFS(); + // Find the highest version enabled update + u32 best_version = 0; + VirtualFile best_update_raw = nullptr; + bool found_best = false; + + // 1. External Updates + const auto* content_provider_union = + dynamic_cast(&content_provider); + if (content_provider_union) { + const auto* external_provider = content_provider_union->GetExternalProvider(); + if (external_provider) { + const auto update_tid = GetUpdateTitleID(title_id); + const auto updates = external_provider->ListUpdateVersions(update_tid); + for (const auto& update : updates) { + std::string version_str; + const auto control_file = external_provider->GetEntryForVersion( + update_tid, ContentRecordType::Control, update.version); + + if (control_file) { + NCA control_nca(control_file); + if (control_nca.GetStatus() == Loader::ResultStatus::Success) { + if (auto control_romfs = control_nca.GetRomFS()) { + if (auto extracted = ExtractRomFS(control_romfs)) { + auto nacp_file = extracted->GetFile("control.nacp"); + if (!nacp_file) { + nacp_file = extracted->GetFile("Control.nacp"); + } + if (nacp_file) { + NACP nacp(nacp_file); + version_str = nacp.GetVersionString(); + } + } + } + } + } + + if (version_str.empty()) { + version_str = FormatTitleVersion(update.version); + } + + const auto name = fmt::format("Update v{}", version_str); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + if (!patch_disabled) { + if (!found_best || update.version > best_version) { + best_version = update.version; + best_update_raw = external_provider->GetEntryForVersion( + update_tid, type, update.version); + found_best = true; + } + } + } } - } else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) { - const auto new_nca = std::make_shared(packed_update_raw, base_nca); + } + + // 2. System Updates + const auto update_tid = GetUpdateTitleID(title_id); + if (update_tid != title_id) { + PatchManager update_mgr{update_tid, fs_controller, content_provider}; + const auto metadata = update_mgr.GetControlMetadata(); + const auto& nacp = metadata.first; + + if (nacp) { + const auto version_str = nacp->GetVersionString(); + const auto name = fmt::format("Update v{}", version_str); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + if (!patch_disabled) { + const auto sys_ver = content_provider.GetEntryVersion(update_tid).value_or(0); + if (!found_best || sys_ver > best_version) { + best_version = sys_ver; + best_update_raw = content_provider.GetEntryRaw(update_tid, type); + found_best = true; + } + } + } else if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { + const auto meta_ver = content_provider.GetEntryVersion(update_tid); + if (meta_ver.value_or(0) != 0) { + const auto version_str = FormatTitleVersion(*meta_ver); + const auto name = fmt::format("Update {}", version_str); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + if (!patch_disabled) { + const auto sys_ver = *meta_ver; + if (!found_best || sys_ver > best_version) { + best_version = sys_ver; + best_update_raw = content_provider.GetEntryRaw(update_tid, type); + found_best = true; + } + } + } + } + } + + // 3. Packed Update (Fallback) + // If we still haven't found a best update, or if the packed one is enabled (named "Update" + // usually? Or "Update (PACKED)"?) The GetPatches logic names it "Update" with version + // "PACKED". + if (!found_best && + packed_update_raw != + nullptr) { // Only if no specific update found, or check strict priorities? + // Usually packed update is last resort. + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + if (!patch_disabled) { + best_update_raw = packed_update_raw; + found_best = true; + // Version? Unknown/Packed. + } + } + + if (found_best && best_update_raw != nullptr && base_nca != nullptr) { + const auto new_nca = std::make_shared(best_update_raw, base_nca); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetRomFS() != nullptr) { - LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully"); + LOG_INFO(Loader, " RomFS: Update ({}) applied successfully", + FormatTitleVersion(best_version)); romfs = new_nca->GetRomFS(); } } @@ -503,14 +721,16 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs if (dlc_dir) { std::map dlc_ncas; for (const auto& mod : dlc_dir->GetSubdirectories()) { - if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == disabled.cend()) { + if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == + disabled.cend()) { u64 dlc_title_id = 0; VirtualFile data_nca_file = nullptr; for (const auto& file : mod->GetFiles()) { if (file->GetName().ends_with(".cnmt.nca")) { NCA meta_nca(file); - if (meta_nca.GetStatus() == Loader::ResultStatus::Success && !meta_nca.GetSubdirectories().empty()) { + if (meta_nca.GetStatus() == Loader::ResultStatus::Success && + !meta_nca.GetSubdirectories().empty()) { auto section0 = meta_nca.GetSubdirectories()[0]; if (!section0->GetFiles().empty()) { CNMT cnmt(section0->GetFiles()[0]); @@ -543,17 +763,20 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs auto extracted_dlc_romfs = ExtractRomFS(dlc_nca->GetRomFS()); if (extracted_dlc_romfs) { layers.push_back(std::move(extracted_dlc_romfs)); - LOG_INFO(Loader, " RomFS: Staging Autoloader DLC TID {:016X}", tid); + LOG_INFO(Loader, + " RomFS: Staging Autoloader DLC TID {:016X}", tid); } } } if (layers.size() > 1) { - auto layered_dir = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); + auto layered_dir = + LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); auto packed = CreateRomFS(std::move(layered_dir), nullptr); if (packed) { romfs = std::move(packed); - LOG_INFO(Loader, " RomFS: Autoloader DLCs layered successfully."); + LOG_INFO(Loader, + " RomFS: Autoloader DLCs layered successfully."); } } } @@ -569,49 +792,139 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs } std::vector PatchManager::GetPatches(VirtualFile update_raw) const { - if (title_id == 0) return {}; + if (title_id == 0) + return {}; std::vector out; const auto& disabled = Settings::values.disabled_addons[title_id]; - // --- 1. NAND Update --- + // --- 1. Update (NAND/External) --- + // --- 1. Update (NAND/External) --- + // First, check for system updates (NAND/SDMC) const auto update_tid = GetUpdateTitleID(title_id); PatchManager update_mgr{update_tid, fs_controller, content_provider}; const auto metadata = update_mgr.GetControlMetadata(); const auto& nacp = metadata.first; - const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); - - Patch update_patch = {.enabled = !update_disabled, - .name = "Update", - .version = "", - .type = PatchType::Update, - .program_id = title_id, - .title_id = title_id}; + const auto update_disabled = + std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); if (nacp != nullptr) { - update_patch.version = nacp->GetVersionString(); + // System update found + const auto version_str = nacp->GetVersionString(); + const auto name = fmt::format("Update v{}", version_str); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + + Patch update_patch = {.enabled = !patch_disabled, + .name = name, + .version = version_str, + .type = PatchType::Update, + .program_id = title_id, + .title_id = title_id}; out.push_back(update_patch); } else if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { + // Fallback for system update without control NCA (rare) const auto meta_ver = content_provider.GetEntryVersion(update_tid); if (meta_ver.value_or(0) != 0) { - update_patch.version = FormatTitleVersion(*meta_ver); + const auto version_str = FormatTitleVersion(*meta_ver); + const auto name = fmt::format("Update {}", version_str); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + + Patch update_patch = {.enabled = !patch_disabled, + .name = name, + .version = version_str, + .type = PatchType::Update, + .program_id = title_id, + .title_id = title_id}; out.push_back(update_patch); } - } else if (update_raw != nullptr) { - update_patch.version = "PACKED"; + } + + // Next, check for external updates + const auto* content_provider_union = + dynamic_cast(&content_provider); + if (content_provider_union) { + const auto* external_provider = content_provider_union->GetExternalProvider(); + if (external_provider) { + const auto updates = external_provider->ListUpdateVersions(update_tid); + for (const auto& update : updates) { + std::string version_str; + const auto control_file = external_provider->GetEntryForVersion( + update_tid, ContentRecordType::Control, update.version); + + if (control_file) { + NCA control_nca(control_file); + if (control_nca.GetStatus() == Loader::ResultStatus::Success) { + if (auto control_romfs = control_nca.GetRomFS()) { + if (auto extracted = ExtractRomFS(control_romfs)) { + auto nacp_file = extracted->GetFile("control.nacp"); + if (!nacp_file) { + nacp_file = extracted->GetFile("Control.nacp"); + } + if (nacp_file) { + NACP control_nacp(nacp_file); + version_str = control_nacp.GetVersionString(); + } + } + } + } + } + + if (version_str.empty()) { + version_str = FormatTitleVersion(update.version); + } + + const auto name = fmt::format("Update v{}", version_str); + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); + + // Deduplicate against installed system update if versions match + bool exists = false; + for (const auto& existing : out) { + if (existing.type == PatchType::Update && existing.version == version_str) { + exists = true; + break; + } + } + if (exists) + continue; + + Patch update_patch = {.enabled = !patch_disabled, + .name = name, + .version = version_str, + .type = PatchType::Update, + .program_id = title_id, + .title_id = title_id}; + out.push_back(update_patch); + } + } + } + + if (out.empty() && update_raw != nullptr) { + Patch update_patch = {.enabled = !update_disabled, + .name = "Update", + .version = "PACKED", + .type = PatchType::Update, + .program_id = title_id, + .title_id = title_id}; out.push_back(update_patch); } // --- 2. Autoloader Content --- VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto scan_autoloader_content = [&](const std::string& content_type_folder, PatchType patch_type) { - const auto autoloader_path = fmt::format("autoloader/{:016X}/{}", title_id, content_type_folder); + const auto scan_autoloader_content = [&](const std::string& content_type_folder, + PatchType patch_type) { + const auto autoloader_path = + fmt::format("autoloader/{:016X}/{}", title_id, content_type_folder); const auto content_dir = sdmc_root->GetSubdirectory(autoloader_path); - if (!content_dir) return; + if (!content_dir) + return; for (const auto& mod : content_dir->GetSubdirectories()) { - if (!mod) continue; + if (!mod) + continue; std::string mod_name_str = mod->GetName(); std::string version_str = "Unknown"; @@ -621,7 +934,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { for (const auto& file : mod->GetFiles()) { if (file->GetName().ends_with(".cnmt.nca")) { NCA meta_nca(file); - if (meta_nca.GetStatus() == Loader::ResultStatus::Success && !meta_nca.GetSubdirectories().empty()) { + if (meta_nca.GetStatus() == Loader::ResultStatus::Success && + !meta_nca.GetSubdirectories().empty()) { auto section0 = meta_nca.GetSubdirectories()[0]; if (!section0->GetFiles().empty()) { CNMT cnmt(section0->GetFiles()[0]); @@ -632,7 +946,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { } } if (dlc_title_id != 0) { - version_str = fmt::format("{}", (dlc_title_id - GetBaseTitleID(dlc_title_id)) / 0x1000); + version_str = fmt::format( + "{}", (dlc_title_id - GetBaseTitleID(dlc_title_id)) / 0x1000); } else { version_str = "DLC"; } @@ -640,7 +955,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { for (const auto& file : mod->GetFiles()) { if (file->GetExtension() == "nca") { NCA nca_check(file); - if (nca_check.GetStatus() == Loader::ResultStatus::Success && nca_check.GetType() == NCAContentType::Control) { + if (nca_check.GetStatus() == Loader::ResultStatus::Success && + nca_check.GetType() == NCAContentType::Control) { if (auto rfs = nca_check.GetRomFS()) { if (auto ext = ExtractRomFS(rfs)) { if (auto nacp_f = ext->GetFile("control.nacp")) { @@ -654,8 +970,14 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { } } } - const auto mod_disabled = std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); - out.push_back({.enabled = !mod_disabled, .name = mod_name_str, .version = version_str, .type = patch_type, .program_id = title_id, .title_id = title_id}); + const auto mod_disabled = + std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); + out.push_back({.enabled = !mod_disabled, + .name = mod_name_str, + .version = version_str, + .type = patch_type, + .program_id = title_id, + .title_id = title_id}); } }; scan_autoloader_content("Updates", PatchType::Update); @@ -667,45 +989,73 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { if (mod_dir != nullptr) { auto get_mod_types = [](const VirtualDir& dir) -> std::string { std::string types; - if (FindSubdirectoryCaseless(dir, "exefs")) AppendCommaIfNotEmpty(types, "IPSwitch/IPS"); - if (FindSubdirectoryCaseless(dir, "romfs") || FindSubdirectoryCaseless(dir, "romfslite")) AppendCommaIfNotEmpty(types, "LayeredFS"); - if (FindSubdirectoryCaseless(dir, "cheats")) AppendCommaIfNotEmpty(types, "Cheats"); + if (FindSubdirectoryCaseless(dir, "exefs")) + AppendCommaIfNotEmpty(types, "IPSwitch/IPS"); + if (FindSubdirectoryCaseless(dir, "romfs") || + FindSubdirectoryCaseless(dir, "romfslite")) + AppendCommaIfNotEmpty(types, "LayeredFS"); + if (FindSubdirectoryCaseless(dir, "cheats")) + AppendCommaIfNotEmpty(types, "Cheats"); return types; }; for (const auto& top_dir : mod_dir->GetSubdirectories()) { - if (!top_dir) continue; + if (!top_dir) + continue; auto process_mod = [&](const VirtualDir& dir, bool nested) { std::string types = get_mod_types(dir); - if (types.empty()) return; - std::string identifier = nested ? (top_dir->GetName() + "/" + dir->GetName()) : dir->GetName(); - const auto mod_disabled = std::find(disabled.begin(), disabled.end(), identifier) != disabled.end(); - out.push_back({.enabled = !mod_disabled, .name = identifier, .version = types, .type = PatchType::Mod, .program_id = title_id, .title_id = title_id}); + if (types.empty()) + return; + std::string identifier = + nested ? (top_dir->GetName() + "/" + dir->GetName()) : dir->GetName(); + const auto mod_disabled = + std::find(disabled.begin(), disabled.end(), identifier) != disabled.end(); + out.push_back({.enabled = !mod_disabled, + .name = identifier, + .version = types, + .type = PatchType::Mod, + .program_id = title_id, + .title_id = title_id}); }; - if (IsValidModDir(top_dir)) process_mod(top_dir, false); - else for (const auto& sd : top_dir->GetSubdirectories()) if (sd) process_mod(sd, true); + if (IsValidModDir(top_dir)) + process_mod(top_dir, false); + else + for (const auto& sd : top_dir->GetSubdirectories()) + if (sd) + process_mod(sd, true); } } const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); if (sdmc_mod_dir != nullptr) { std::string types; - if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs"))) AppendCommaIfNotEmpty(types, "LayeredExeFS"); - if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) || IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) AppendCommaIfNotEmpty(types, "LayeredFS"); + if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs"))) + AppendCommaIfNotEmpty(types, "LayeredExeFS"); + if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) || + IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) + AppendCommaIfNotEmpty(types, "LayeredFS"); if (!types.empty()) { - const auto mod_disabled = std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); - out.push_back({.enabled = !mod_disabled, .name = "SDMC", .version = types, .type = PatchType::Mod, .program_id = title_id, .title_id = title_id}); + const auto mod_disabled = + std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); + out.push_back({.enabled = !mod_disabled, + .name = "SDMC", + .version = types, + .type = PatchType::Mod, + .program_id = title_id, + .title_id = title_id}); } } // --- 4. NAND DLC --- - const auto dlc_entries = content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data); + const auto dlc_entries = + content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data); std::vector dlc_match; dlc_match.reserve(dlc_entries.size()); std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match), [this](const ContentProviderEntry& entry) { return GetBaseTitleID(entry.title_id) == title_id && - content_provider.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success; + content_provider.GetEntry(entry)->GetStatus() == + Loader::ResultStatus::Success; }); if (!dlc_match.empty()) { std::sort(dlc_match.begin(), dlc_match.end()); @@ -714,8 +1064,14 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { list += fmt::format("{}, ", dlc_match[i].title_id & 0x7FF); list += fmt::format("{}", dlc_match.back().title_id & 0x7FF); - const auto dlc_disabled = std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); - out.push_back({.enabled = !dlc_disabled, .name = "DLC", .version = std::move(list), .type = PatchType::DLC, .program_id = title_id, .title_id = title_id}); + const auto dlc_disabled = + std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); + out.push_back({.enabled = !dlc_disabled, + .name = "DLC", + .version = std::move(list), + .type = PatchType::DLC, + .program_id = title_id, + .title_id = title_id}); } // Scan for Game-Specific Tools @@ -736,13 +1092,14 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { } // Scan for Global Tools (NX-Optimizer) - const std::vector optimizer_supported_ids = { - 0x0100F2C0115B6000, 0x01007EF00011E000, 0x0100A3D008C5C000, 0x01008F6008C5E000, - 0x01008CF01BAAC000, 0x100453019AA8000 - }; + const std::vector optimizer_supported_ids = {0x0100F2C0115B6000, 0x01007EF00011E000, + 0x0100A3D008C5C000, 0x01008F6008C5E000, + 0x01008CF01BAAC000, 0x100453019AA8000}; - if (std::find(optimizer_supported_ids.begin(), optimizer_supported_ids.end(), title_id) != optimizer_supported_ids.end()) { - auto global_tools_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools"; + if (std::find(optimizer_supported_ids.begin(), optimizer_supported_ids.end(), title_id) != + optimizer_supported_ids.end()) { + auto global_tools_path = + Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools"; if (std::filesystem::exists(global_tools_path)) { for (const auto& entry : std::filesystem::directory_iterator(global_tools_path)) { @@ -774,7 +1131,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { std::optional PatchManager::GetGameVersion() const { const auto& disabled = Settings::values.disabled_addons[title_id]; - const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + const auto update_disabled = + std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); if (!update_disabled) { const auto update_tid = GetUpdateTitleID(title_id); @@ -790,19 +1148,24 @@ PatchManager::Metadata PatchManager::GetControlMetadata() const { std::unique_ptr control_nca = nullptr; const auto& disabled_map = Settings::values.disabled_addons; const auto it = disabled_map.find(title_id); - const auto& disabled_for_game = (it != disabled_map.end()) ? it->second : std::vector{}; + const auto& disabled_for_game = + (it != disabled_map.end()) ? it->second : std::vector{}; VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); - if (const auto autoloader_updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path)) { + if (const auto autoloader_updates_dir = + sdmc_root->GetSubdirectory(autoloader_updates_path)) { for (const auto& update_mod : autoloader_updates_dir->GetSubdirectories()) { - if (!update_mod) continue; - if (std::find(disabled_for_game.begin(), disabled_for_game.end(), update_mod->GetName()) == disabled_for_game.end()) { + if (!update_mod) + continue; + if (std::find(disabled_for_game.begin(), disabled_for_game.end(), + update_mod->GetName()) == disabled_for_game.end()) { for (const auto& file : update_mod->GetFiles()) { if (file->GetExtension() == "nca") { NCA nca_check(file); - if (nca_check.GetStatus() == Loader::ResultStatus::Success && nca_check.GetType() == NCAContentType::Control) { + if (nca_check.GetStatus() == Loader::ResultStatus::Success && + nca_check.GetType() == NCAContentType::Control) { control_nca = std::make_unique(file); return ParseControlNCA(*control_nca); } @@ -814,7 +1177,8 @@ PatchManager::Metadata PatchManager::GetControlMetadata() const { } // Only fetch the Update metadata if the user hasn't disabled it - const auto update_disabled = std::find(disabled_for_game.begin(), disabled_for_game.end(), "Update") != disabled_for_game.end(); + const auto update_disabled = std::find(disabled_for_game.begin(), disabled_for_game.end(), + "Update") != disabled_for_game.end(); const auto update_tid = GetUpdateTitleID(title_id); if (!update_disabled) { @@ -825,28 +1189,37 @@ PatchManager::Metadata PatchManager::GetControlMetadata() const { control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control); } - if (control_nca == nullptr) return {}; + if (control_nca == nullptr) + return {}; return ParseControlNCA(*control_nca); } PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { const auto base_romfs = nca.GetRomFS(); - if (base_romfs == nullptr) return {}; + if (base_romfs == nullptr) + return {}; const auto romfs = PatchRomFS(&nca, base_romfs, ContentRecordType::Control); - if (romfs == nullptr) return {}; + if (romfs == nullptr) + return {}; const auto extracted = ExtractRomFS(romfs); - if (extracted == nullptr) return {}; + if (extracted == nullptr) + return {}; auto nacp_file = extracted->GetFile("control.nacp"); - if (nacp_file == nullptr) nacp_file = extracted->GetFile("Control.nacp"); + if (nacp_file == nullptr) + nacp_file = extracted->GetFile("Control.nacp"); auto nacp = nacp_file == nullptr ? nullptr : std::make_unique(nacp_file); - const auto language_code = Service::Set::GetLanguageCodeFromIndex(static_cast(Settings::values.language_index.GetValue())); - const auto application_language = Service::NS::ConvertToApplicationLanguage(language_code).value_or(Service::NS::ApplicationLanguage::AmericanEnglish); - const auto language_priority_list = Service::NS::GetApplicationLanguagePriorityList(application_language); + const auto language_code = Service::Set::GetLanguageCodeFromIndex( + static_cast(Settings::values.language_index.GetValue())); + const auto application_language = + Service::NS::ConvertToApplicationLanguage(language_code) + .value_or(Service::NS::ApplicationLanguage::AmericanEnglish); + const auto language_priority_list = + Service::NS::GetApplicationLanguagePriorityList(application_language); auto priority_language_names = FileSys::LANGUAGE_NAMES; if (language_priority_list) { @@ -861,7 +1234,8 @@ PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { VirtualFile icon_file; for (const auto& language : priority_language_names) { icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat")); - if (icon_file != nullptr) break; + if (icon_file != nullptr) + break; } return {std::move(nacp), icon_file}; diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index b6c61d715..7c5ab8b24 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -2,8 +2,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include +#include +#include #include #include "common/assert.h" #include "common/fs/path_util.h" @@ -827,13 +830,28 @@ bool RegisteredCache::RawInstallCitronMeta(const CNMT& cnmt) { Refresh(); return std::find_if(citron_meta.begin(), citron_meta.end(), [&cnmt](const std::pair& kv) { - return kv.second.GetType() == cnmt.GetType() && - kv.second.GetTitleID() == cnmt.GetTitleID(); + return kv.second.GetTitleID() == cnmt.GetTitleID(); }) != citron_meta.end(); } ContentProviderUnion::~ContentProviderUnion() = default; +const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const { + auto it = providers.find(ContentProviderUnionSlot::External); + if (it != providers.end()) { + return dynamic_cast(it->second); + } + return nullptr; +} + +const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnionSlot slot) const { + auto it = providers.find(slot); + if (it != providers.end()) { + return it->second; + } + return nullptr; +} + void ContentProviderUnion::SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider) { providers[slot] = provider; } @@ -980,8 +998,49 @@ void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType con entries.insert_or_assign({title_type, content_type, title_id}, file); } +void ManualContentProvider::AddEntryWithVersion(TitleType title_type, + ContentRecordType content_type, u64 title_id, + u32 version, const std::string& version_string, + VirtualFile file) { + if (title_type == TitleType::Update) { + auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(), + [title_id, version](const ExternalUpdateEntry& entry) { + return entry.title_id == title_id && entry.version == version; + }); + + if (it != multi_version_entries.end()) { + it->files[content_type] = file; + if (!version_string.empty()) { + it->version_string = version_string; + } + } else { + ExternalUpdateEntry new_entry; + new_entry.title_id = title_id; + new_entry.version = version; + new_entry.version_string = version_string; + new_entry.files[content_type] = file; + multi_version_entries.push_back(new_entry); + } + + auto existing = entries.find({title_type, content_type, title_id}); + if (existing == entries.end()) { + entries.insert_or_assign({title_type, content_type, title_id}, file); + } else { + for (const auto& entry : multi_version_entries) { + if (entry.title_id == title_id && entry.version > version) { + return; + } + } + entries.insert_or_assign({title_type, content_type, title_id}, file); + } + } else { + entries.insert_or_assign({title_type, content_type, title_id}, file); + } +} + void ManualContentProvider::ClearAllEntries() { entries.clear(); + multi_version_entries.clear(); } void ManualContentProvider::Refresh() {} @@ -1036,4 +1095,315 @@ std::vector ManualContentProvider::ListEntriesFilter( return out; } +ExternalContentProvider::ExternalContentProvider(std::vector load_directories) + : load_dirs(std::move(load_directories)) { + Refresh(); +} + +ExternalContentProvider::~ExternalContentProvider() = default; + +void ExternalContentProvider::AddDirectory(VirtualDir directory) { + load_dirs.push_back(std::move(directory)); + Refresh(); +} + +void ExternalContentProvider::ClearDirectories() { + load_dirs.clear(); + Refresh(); +} + +void ExternalContentProvider::Refresh() { + entries.clear(); + versions.clear(); + multi_version_entries.clear(); + for (const auto& dir : load_dirs) { + ScanDirectory(dir); + } +} + +void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) { + if (dir == nullptr) + return; + + LOG_INFO(Service_FS, "Scanning directory: {}", dir->GetFullPath()); + + for (const auto& file : dir->GetFiles()) { + const auto extension = file->GetExtension(); + if (extension == "nsp") { + LOG_INFO(Service_FS, "Found NSP: {}", file->GetName()); + ProcessNSP(file); + } else if (extension == "xci") { + LOG_INFO(Service_FS, "Found XCI: {}", file->GetName()); + ProcessXCI(file); + } + } + + for (const auto& subdir : dir->GetSubdirectories()) { + ScanDirectory(subdir); + } +} + +void ExternalContentProvider::ProcessNSP(const VirtualFile& file) { + if (file == nullptr) + return; + LOG_DEBUG(Service_FS, "Processing NSP: {}", file->GetName()); + NSP nsp(file); + if (nsp.GetStatus() != Loader::ResultStatus::Success) { + LOG_ERROR(Service_FS, "Failed to load NSP: {}", file->GetName()); + return; + } + + const auto files = nsp.GetFiles(); + for (const auto& nca_file : files) { + if (nca_file->GetExtension() != "nca") + continue; + + NCA nca(nca_file); + if (nca.GetStatus() != Loader::ResultStatus::Success) + continue; + if (nca.GetType() != NCAContentType::Meta) + continue; + + const auto subdirs = nca.GetSubdirectories(); + if (subdirs.empty() || subdirs[0]->GetFiles().empty()) + continue; + + CNMT cnmt(subdirs[0]->GetFiles()[0]); + const auto title_id = cnmt.GetTitleID(); + const auto title_type = cnmt.GetType(); + const auto version = cnmt.GetTitleVersion(); + + LOG_INFO(Service_FS, "Found CNMT in {}: TitleID={:016X}, Type={:02X}, Version={}", + file->GetName(), title_id, static_cast(title_type), version); + + if (versions.find(title_id) == versions.end() || versions[title_id] < version) { + versions[title_id] = version; + } + + if (title_type == TitleType::Update) { + size_t entry_index = std::numeric_limits::max(); + + // Find existing entry index + for (size_t i = 0; i < multi_version_entries.size(); ++i) { + if (multi_version_entries[i].title_id == title_id && + multi_version_entries[i].version == version) { + entry_index = i; + break; + } + } + + if (entry_index == std::numeric_limits::max()) { + ExternalUpdateEntry new_entry; + new_entry.title_id = title_id; + new_entry.version = version; + new_entry.version_string = fmt::format("v{}", version); + multi_version_entries.push_back(std::move(new_entry)); + entry_index = multi_version_entries.size() - 1; + } + + for (const auto& record : cnmt.GetContentRecords()) { + const auto nca_id_str = Common::HexToString(record.nca_id); + auto content_file = nsp.GetFile(fmt::format("{}.nca", nca_id_str)); + if (!content_file) + content_file = nsp.GetFile(nca_id_str); + + if (!content_file) { + std::string nca_id_lower = nca_id_str; + std::transform(nca_id_lower.begin(), nca_id_lower.end(), nca_id_lower.begin(), + ::tolower); + content_file = nsp.GetFile(fmt::format("{}.nca", nca_id_lower)); + if (!content_file) + content_file = nsp.GetFile(nca_id_lower); + } + + if (content_file) { + multi_version_entries[entry_index].files[record.type] = content_file; + + if (versions[title_id] == version) { + entries.insert_or_assign(std::make_tuple(title_id, record.type, title_type), + content_file); + } + } + } + + if (versions[title_id] == version) { + entries.insert_or_assign( + std::make_tuple(title_id, ContentRecordType::Meta, title_type), nca_file); + } + } else { + for (const auto& record : cnmt.GetContentRecords()) { + const auto nca_id_str = Common::HexToString(record.nca_id); + auto content_file = nsp.GetFile(fmt::format("{}.nca", nca_id_str)); + if (!content_file) + content_file = nsp.GetFile(nca_id_str); + + if (!content_file) { + std::string nca_id_lower = nca_id_str; + std::transform(nca_id_lower.begin(), nca_id_lower.end(), nca_id_lower.begin(), + ::tolower); + content_file = nsp.GetFile(fmt::format("{}.nca", nca_id_lower)); + if (!content_file) + content_file = nsp.GetFile(nca_id_lower); + } + + if (content_file) { + if (versions[title_id] == version) { + entries.insert_or_assign(std::make_tuple(title_id, record.type, title_type), + content_file); + } + } + } + if (versions[title_id] == version) { + entries.insert_or_assign( + std::make_tuple(title_id, ContentRecordType::Meta, title_type), nca_file); + } + } + } +} + +void ExternalContentProvider::ProcessXCI(const VirtualFile& file) { + if (file == nullptr) + return; + XCI xci(file); + if (xci.GetStatus() != Loader::ResultStatus::Success) + return; + + const std::array partitions = {XCIPartition::Secure, XCIPartition::Update, + XCIPartition::Normal}; + + for (const auto partition_type : partitions) { + const auto partition = xci.GetPartition(partition_type); + if (!partition) + continue; + + for (const auto& part_file : partition->GetFiles()) { + if (part_file->GetExtension() != "nca") + continue; + + NCA nca(part_file); + if (nca.GetStatus() != Loader::ResultStatus::Success) + continue; + if (nca.GetType() != NCAContentType::Meta) + continue; + + const auto subdirs = nca.GetSubdirectories(); + if (subdirs.empty() || subdirs[0]->GetFiles().empty()) + continue; + + CNMT cnmt(subdirs[0]->GetFiles()[0]); + const auto title_id = cnmt.GetTitleID(); + const auto title_type = cnmt.GetType(); + const auto version = cnmt.GetTitleVersion(); + + if (versions.find(title_id) == versions.end() || versions[title_id] < version) { + versions[title_id] = version; + } + + for (const auto& record : cnmt.GetContentRecords()) { + const auto nca_id_str = Common::HexToString(record.nca_id); + auto content_file = partition->GetFile(fmt::format("{}.nca", nca_id_str)); + if (content_file) { + if (versions[title_id] == version) { + entries.insert_or_assign(std::make_tuple(title_id, record.type, title_type), + content_file); + } + } + } + if (versions[title_id] == version) { + entries.insert_or_assign( + std::make_tuple(title_id, ContentRecordType::Meta, title_type), part_file); + } + } + } +} + +bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const { + for (const auto& [key, val] : entries) { + if (std::get<0>(key) == title_id && std::get<1>(key) == type) + return true; + } + return false; +} + +std::optional ExternalContentProvider::GetEntryVersion(u64 title_id) const { + if (auto it = versions.find(title_id); it != versions.end()) { + return it->second; + } + return std::nullopt; +} + +VirtualFile ExternalContentProvider::GetEntryUnparsed(u64 title_id, ContentRecordType type) const { + return GetEntryRaw(title_id, type); +} + +VirtualFile ExternalContentProvider::GetEntryRaw(u64 title_id, ContentRecordType type) const { + for (const auto& [key, val] : entries) { + if (std::get<0>(key) == title_id && std::get<1>(key) == type) + return val; + } + return nullptr; +} + +std::unique_ptr ExternalContentProvider::GetEntry(u64 title_id, ContentRecordType type) const { + auto file = GetEntryRaw(title_id, type); + if (!file) + return nullptr; + return std::make_unique(file); +} + +std::vector ExternalContentProvider::ListEntriesFilter( + std::optional title_type, std::optional record_type, + std::optional title_id) const { + std::vector out; + for (const auto& [key, val] : entries) { + const auto [e_title_id, e_record_type, e_title_type] = key; + + if (title_type && *title_type != e_title_type) + continue; + if (record_type && *record_type != e_record_type) + continue; + if (title_id && *title_id != e_title_id) + continue; + + out.push_back({e_title_id, e_record_type}); + } + + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return out; +} + +std::vector ExternalContentProvider::ListUpdateVersions(u64 title_id) const { + std::vector out; + std::copy_if( + multi_version_entries.begin(), multi_version_entries.end(), std::back_inserter(out), + [title_id](const ExternalUpdateEntry& entry) { return entry.title_id == title_id; }); + return out; +} + +VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, + u32 version) const { + const auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(), + [title_id, version](const ExternalUpdateEntry& entry) { + return entry.title_id == title_id && entry.version == version; + }); + + if (it != multi_version_entries.end()) { + const auto file_it = it->files.find(type); + if (file_it != it->files.end()) { + return file_it->second; + } + } + return nullptr; +} + +bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const { + // Only updates (type check usually handled by caller, but good to be safe if strictly for + // updates) Multi_version_entries only stores updates currently. + return std::count_if(multi_version_entries.begin(), multi_version_entries.end(), + [title_id](const ExternalUpdateEntry& entry) { + return entry.title_id == title_id; + }) > 1; +} + } // namespace FileSys diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index 5b56c67c5..f7acd383a 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -6,12 +6,16 @@ #include #include +#include #include +#include #include +#include #include #include #include "common/common_types.h" #include "core/crypto/key_manager.h" +#include "core/file_sys/nca_metadata.h" #include "core/file_sys/vfs/vfs.h" namespace FileSys { @@ -49,6 +53,13 @@ struct ContentProviderEntry { std::string DebugInfo() const; }; +struct ExternalUpdateEntry { + u64 title_id; + u32 version; + std::string version_string; + std::map files; +}; + constexpr u64 GetUpdateTitleID(u64 base_title_id) { return base_title_id | 0x800; } @@ -205,6 +216,7 @@ private: }; enum class ContentProviderUnionSlot { + External, ///< External content dirs (NAND-less updates/DLC) SysNAND, ///< System NAND UserNAND, ///< User NAND SDMC, ///< SD Card @@ -239,6 +251,9 @@ public: std::optional GetSlotForEntry(u64 title_id, ContentRecordType type) const; + const class ExternalContentProvider* GetExternalProvider() const; + const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const; + private: std::map providers; }; @@ -249,6 +264,8 @@ public: void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id, VirtualFile file); + void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id, + u32 version, const std::string& version_string, VirtualFile file); void ClearAllEntries(); void Refresh() override; @@ -261,8 +278,46 @@ public: std::optional title_type, std::optional record_type, std::optional title_id) const override; + std::vector ListUpdateVersions(u64 title_id) const; + VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const; + bool HasMultipleVersions(u64 title_id, ContentRecordType type) const; + private: std::map, VirtualFile> entries; + std::vector multi_version_entries; +}; + +class ExternalContentProvider : public ContentProvider { +public: + explicit ExternalContentProvider(std::vector load_directories = {}); + ~ExternalContentProvider() override; + + void AddDirectory(VirtualDir directory); + void ClearDirectories(); + + void Refresh() override; + bool HasEntry(u64 title_id, ContentRecordType type) const override; + std::optional GetEntryVersion(u64 title_id) const override; + VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override; + VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override; + std::unique_ptr GetEntry(u64 title_id, ContentRecordType type) const override; + std::vector ListEntriesFilter( + std::optional title_type = {}, std::optional record_type = {}, + std::optional title_id = {}) const override; + + std::vector ListUpdateVersions(u64 title_id) const; + VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const; + bool HasMultipleVersions(u64 title_id, ContentRecordType type) const; + +private: + void ScanDirectory(const VirtualDir& dir); + void ProcessNSP(const VirtualFile& file); + void ProcessXCI(const VirtualFile& file); + + std::vector load_dirs; + std::map, VirtualFile> entries; + std::map versions; + std::vector multi_version_entries; }; } // namespace FileSys diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index 351ed1e34..0d76c5d88 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -644,6 +644,10 @@ FileSys::PlaceholderCache* FileSystemController::GetSDMCPlaceholder() const { return sdmc_factory->GetSDMCPlaceholder(); } +FileSys::ExternalContentProvider* FileSystemController::GetExternalContentProvider() const { + return external_provider.get(); +} + FileSys::RegisteredCache* FileSystemController::GetRegisteredCacheForStorage( FileSys::StorageId id) const { switch (id) { @@ -794,6 +798,7 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove if (overwrite) { bis_factory = nullptr; sdmc_factory = nullptr; + external_provider = nullptr; } using CitronPath = Common::FS::CitronPath; @@ -827,12 +832,41 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove sdmc_factory->GetSDMCContents()); } + if (external_provider == nullptr) { + std::vector load_dirs; + for (const auto& path : Settings::values.external_content_dirs) { + auto dir = vfs.OpenDirectory(path, FileSys::OpenMode::Read); + if (dir != nullptr) { + load_dirs.push_back(std::move(dir)); + } + } + external_provider = + std::make_unique(std::move(load_dirs)); + system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External, + external_provider.get()); + } + // factory that handles sync tasks before a game is even selected if (global_save_data_factory == nullptr || overwrite) { global_save_data_factory = CreateSaveDataFactory(ProgramId{}); } } +void FileSystemController::RefreshExternalContentProvider() { + auto vfs = system.GetFilesystem(); + std::vector load_dirs; + for (const auto& path : Settings::values.external_content_dirs) { + auto dir = vfs->OpenDirectory(path, FileSys::OpenMode::Read); + if (dir != nullptr) { + load_dirs.push_back(std::move(dir)); + } + } + + external_provider = std::make_unique(std::move(load_dirs)); + system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External, + external_provider.get()); +} + void FileSystemController::Reset() { std::scoped_lock lk{registration_lock}; registrations.clear(); diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h index 665d03ebc..7bc9700b8 100644 --- a/src/core/hle/service/filesystem/filesystem.h +++ b/src/core/hle/service/filesystem/filesystem.h @@ -25,6 +25,7 @@ class RomFSFactory; class SaveDataFactory; class SDMCFactory; class XCI; +class ExternalContentProvider; enum class BisPartitionId : u32; enum class ContentRecordType : u8; @@ -97,6 +98,8 @@ public: FileSys::PlaceholderCache* GetSDMCPlaceholder() const; FileSys::PlaceholderCache* GetGameCardPlaceholder() const; + FileSys::ExternalContentProvider* GetExternalContentProvider() const; + FileSys::RegisteredCache* GetRegisteredCacheForStorage(FileSys::StorageId id) const; FileSys::PlaceholderCache* GetPlaceholderCacheForStorage(FileSys::StorageId id) const; @@ -116,6 +119,8 @@ public: FileSys::VirtualDir GetBCATDirectory(u64 title_id) const; + void RefreshExternalContentProvider(); + // Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function // above is called. void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true); @@ -146,6 +151,8 @@ private: std::unique_ptr gamecard_registered; std::unique_ptr gamecard_placeholder; + std::unique_ptr external_provider; + // Global factory for startup tasks and mirroring std::shared_ptr global_save_data_factory;