aboutsummaryrefslogtreecommitdiff
path: root/src/core/loader
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/loader')
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp53
-rw-r--r--src/core/loader/deconstructed_rom_directory.h6
-rw-r--r--src/core/loader/loader.cpp35
-rw-r--r--src/core/loader/loader.h3
-rw-r--r--src/core/loader/nup.cpp280
-rw-r--r--src/core/loader/nup.h65
6 files changed, 411 insertions, 31 deletions
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 94dd3395ba..463a8c8930 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -8,6 +8,8 @@
#include <cctype>
#include <chrono>
#include <cstring>
+#include <map>
+#include <memory>
#include <mutex>
#include <string_view>
#include "common/logging.h"
@@ -92,31 +94,24 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) {
}
if (const auto parent = exefs_dir->GetParentDirectory()) {
if (const auto romfs_dir = parent->GetSubdirectory("romfs")) {
- // Keep no ownership here: launch preparation hands its image to the real loader.
+ // Building it walks every file and keeps an object for each (title 1: 206k files, ~150
+ // MiB), so loaders of the same directory share one build while any of them holds it.
static std::mutex cache_mutex;
- static std::string cached_path;
- static std::weak_ptr<FileSys::VfsFile> cached_romfs;
- const auto path = romfs_dir->GetFullPath();
- {
- std::scoped_lock lock{cache_mutex};
- if (cached_path == path) {
- if (auto cached = cached_romfs.lock()) {
- return cached;
- }
- }
+ static std::map<std::string, std::weak_ptr<FileSys::VfsFile>> cache;
+ std::scoped_lock lk{cache_mutex};
+ auto& cached = cache[romfs_dir->GetFullPath()];
+ if (auto built = cached.lock()) {
+ return built;
}
+ LOG_INFO(Loader, "Using RomFS directory next to the ExeFS");
const auto started = std::chrono::steady_clock::now();
- LOG_INFO(Loader, "Building loose RomFS directory image");
- auto image = FileSys::CreateRomFS(romfs_dir);
+ auto built = FileSys::CreateRomFS(romfs_dir);
LOG_INFO(Loader, "Loose RomFS image ready: elapsed_ms={}",
std::chrono::duration_cast<std::chrono::milliseconds>(
- std::chrono::steady_clock::now() - started).count());
- {
- std::scoped_lock lock{cache_mutex};
- cached_path = path;
- cached_romfs = image;
- }
- return image;
+ std::chrono::steady_clock::now() - started)
+ .count());
+ cached = built;
+ return built;
}
}
return nullptr;
@@ -258,6 +253,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys
name = control->GetApplicationName();
}
+ romfs_exefs_dir = file_dir;
if (const auto title_dir = file_dir->GetParentDirectory()) {
romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs"));
}
@@ -269,13 +265,17 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(
, dir(std::move(directory))
, override_update(override_update_)
{
+ romfs_exefs_dir = dir;
}
-void AppLoader_DeconstructedRomDirectory::EnsureRomFS() {
- if (!romfs_initialized) {
- romfs = FindLooseRomFS(dir ? dir : file->GetContainingDirectory());
- romfs_initialized = true;
+// Loaders made only for metadata (the game list, title lookups) never need the RomFS, and several
+// of them were alive with a whole copy each.
+const FileSys::VirtualFile& AppLoader_DeconstructedRomDirectory::LooseRomFS() {
+ if (!romfs_resolved) {
+ romfs_resolved = true;
+ romfs = FindLooseRomFS(romfs_exefs_dir);
}
+ return romfs;
}
FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& dir_file) {
@@ -336,7 +336,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
// Loose-ExeFS loads bypass the NCA/NSP loaders, which are the ones that normally apply
// LayeredFS to the RomFS; apply it here so mods in user/load/<titleid>/ work too.
- if (romfs != nullptr) {
+ if (LooseRomFS() != nullptr) {
const FileSys::PatchManager patch_manager(metadata.GetTitleID(),
system.GetFileSystemController(),
system.GetContentProvider());
@@ -446,8 +446,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
}
ResultStatus AppLoader_DeconstructedRomDirectory::ReadRomFS(FileSys::VirtualFile& out_dir) {
- EnsureRomFS();
- if (romfs == nullptr) {
+ if (LooseRomFS() == nullptr) {
return ResultStatus::ErrorNoRomFS;
}
diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h
index 477638defe..db0120f4ad 100644
--- a/src/core/loader/deconstructed_rom_directory.h
+++ b/src/core/loader/deconstructed_rom_directory.h
@@ -58,11 +58,13 @@ public:
ResultStatus ReadNSOModules(Modules& out_modules) override;
private:
- void EnsureRomFS();
+ /// The loose RomFS next to the ExeFS, found (and built) on first use.
+ const FileSys::VirtualFile& LooseRomFS();
FileSys::ProgramMetadata metadata;
FileSys::VirtualFile romfs;
- bool romfs_initialized{};
+ FileSys::VirtualDir romfs_exefs_dir; ///< Where LooseRomFS looks
+ bool romfs_resolved{};
FileSys::VirtualDir dir;
std::vector<u8> icon_data;
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index 33a93573cf..839bb4d80b 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -29,6 +29,7 @@
#include "core/loader/nro.h"
#include "core/loader/nso.h"
#include "core/loader/nsp.h"
+#include "core/loader/nup.h"
#include "core/loader/xci.h"
namespace Loader {
@@ -130,6 +131,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
return *nsp_type;
} else if (const auto xci_type = IdentifyFileLoader<AppLoader_XCI>(file)) {
return *xci_type;
+ } else if (const auto nup_type = IdentifyFileLoader<AppLoader_NUP>(file)) {
+ return *nup_type;
} else if (const auto nro_type = IdentifyFileLoader<AppLoader_NRO>(file)) {
return *nro_type;
} else if (const auto nca_type = IdentifyFileLoader<AppLoader_NCA>(file)) {
@@ -150,7 +153,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
bool IsContainerType(FileType type) {
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
- type == FileType::XCZ;
+ type == FileType::XCZ || type == FileType::NUP;
}
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
@@ -163,6 +166,24 @@ bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 progr
type = IdentifyFile(file);
}
+ if (type == FileType::NUP) {
+ FileSys::NupContainer nup{file};
+ if (nup.GetStatus() != ResultStatus::Success) {
+ return false;
+ }
+ // A base package boots normally; a standalone update (no base) carries
+ // a complete patched tree and boots on its own (SPEC section 4.1.2).
+ if (const auto base = nup.FindBasePackage();
+ base.has_value() && nup.GetExeFSDir(*base) != nullptr) {
+ return true;
+ }
+ if (const auto update = nup.FindBestUpdatePackage();
+ update.has_value() && nup.GetExeFSDir(*update) != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
if (!IsContainerType(type)) {
return false;
}
@@ -190,6 +211,8 @@ FileType GuessFromFilename(const std::string& name) {
return FileType::XCZ;
else if (extension == "nsp")
return FileType::NSP;
+ else if (extension == "nup")
+ return FileType::NUP;
else if (extension == "nsz")
return FileType::NSZ;
else if (extension == "kip")
@@ -215,6 +238,8 @@ std::string GetFileTypeString(FileType type) {
return "NSP";
case FileType::NSZ:
return "NSZ";
+ case FileType::NUP:
+ return "NUP";
case FileType::KIP:
return "KIP";
case FileType::DeconstructedRomDirectory:
@@ -227,7 +252,7 @@ std::string GetFileTypeString(FileType type) {
return "unknown";
}
-constexpr std::array<const char*, 68> RESULT_MESSAGES{
+constexpr std::array<const char*, 70> RESULT_MESSAGES{
"The operation completed successfully.",
"The loader requested to load is already loaded.",
"The operation is not implemented.",
@@ -296,6 +321,8 @@ constexpr std::array<const char*, 68> RESULT_MESSAGES{
"The INI file contains more than the maximum allowable number of KIP files.",
"Integrity verification could not be performed for this file.",
"Integrity verification failed.",
+ "The NUP container is malformed or its TOC hash does not match.",
+ "The NUP container does not contain a loadable base package.",
};
std::string GetResultStatusString(ResultStatus status) {
@@ -380,6 +407,10 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
case FileType::KIP:
return std::make_unique<AppLoader_KIP>(std::move(file));
+ // NUP (NX Package) container format.
+ case FileType::NUP:
+ return std::make_unique<AppLoader_NUP>(std::move(file));
+
// NX deconstructed ROM directory.
case FileType::DeconstructedRomDirectory:
return std::make_unique<AppLoader_DeconstructedRomDirectory>(std::move(file));
diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h
index 431aeace73..23be3597f5 100644
--- a/src/core/loader/loader.h
+++ b/src/core/loader/loader.h
@@ -47,6 +47,7 @@ enum class FileType {
XCZ,
NAX,
KIP,
+ NUP,
DeconstructedRomDirectory,
};
@@ -157,6 +158,8 @@ enum class ResultStatus : u16 {
ErrorINITooManyKIPs,
ErrorIntegrityVerificationNotImplemented,
ErrorIntegrityVerificationFailed,
+ ErrorNUPBadFormat,
+ ErrorNUPNoBasePackage,
};
std::string GetResultStatusString(ResultStatus status);
diff --git a/src/core/loader/nup.cpp b/src/core/loader/nup.cpp
new file mode 100644
index 0000000000..7ddfb8947e
--- /dev/null
+++ b/src/core/loader/nup.cpp
@@ -0,0 +1,280 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/loader/nup.h"
+
+#include <algorithm>
+#include <cstring>
+
+#include "common/logging.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/file_sys/nup_container.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/romfs.h"
+#include "core/file_sys/romfs_factory.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/loader/deconstructed_rom_directory.h"
+
+namespace Loader {
+
+namespace {
+
+using FileSys::NupContainer;
+
+// NACP stores 16 uncompressed language entries; map BCP-47 tags onto them.
+struct LanguageSlot {
+ std::string_view tag;
+ std::size_t index;
+};
+
+constexpr std::array<LanguageSlot, 18> LanguageSlots{{
+ {"en-us", 0}, {"en-gb", 1}, {"ja", 2}, {"fr", 3}, {"de", 4},
+ {"es-419", 5}, {"es", 6}, {"it", 7}, {"nl", 8}, {"fr-ca", 9},
+ {"pt", 10}, {"ru", 11}, {"ko", 12}, {"zh-hant", 13}, {"zh-hans", 14},
+ {"pt-br", 15}, {"pl", 16}, {"th", 17},
+}};
+
+void CopyNameToEntry(FileSys::LanguageEntry& entry, std::string_view name) {
+ const std::size_t length = std::min(name.size(), entry.application_name.size() - 1);
+ std::memcpy(entry.application_name.data(), name.data(), length);
+ entry.application_name[length] = '\0';
+}
+
+std::unique_ptr<FileSys::NACP> BuildNacp(const NupContainer::PackageInfo& base,
+ const std::string& version) {
+ if (base.title_name.empty() && base.localized_titles.empty() && version.empty()) {
+ return nullptr;
+ }
+ auto nacp = std::make_unique<FileSys::NACP>();
+ nacp->raw.supported_languages = static_cast<FileSys::SupportedLanguage>(0x3FFFF);
+
+ for (const auto& [lang, name] : base.localized_titles) {
+ for (const auto& slot : LanguageSlots) {
+ // The uncompressed RawNACP only stores 16 entries; Polish/Thai exist
+ // only in the compressed format, so they fall back to the default.
+ if (slot.index < 16 && slot.tag == lang) {
+ CopyNameToEntry(nacp->raw.language_entries.language_entries[slot.index], name);
+ }
+ }
+ }
+ if (!base.title_name.empty()) {
+ CopyNameToEntry(nacp->raw.language_entries.language_entries[0], base.title_name);
+ }
+ if (!version.empty()) {
+ const std::size_t length = std::min(version.size(), nacp->raw.version_string.size() - 1);
+ std::memcpy(nacp->raw.version_string.data(), version.data(), length);
+ nacp->raw.version_string[length] = '\0';
+ }
+ nacp->raw.save_data_owner_id = base.title_id;
+
+ nacp->language_entries.resize(16);
+ std::memcpy(nacp->language_entries.data(), nacp->raw.language_entries.language_entries.data(),
+ sizeof(nacp->raw.language_entries.language_entries));
+ return nacp;
+}
+
+std::string DecodeVersion(u32 version) {
+ if (version == 0) {
+ return {};
+ }
+ return fmt::format("{}.{}.{}", version >> 16, (version >> 8) & 0xFF, version & 0xFF);
+}
+
+} // namespace
+
+AppLoader_NUP::AppLoader_NUP(FileSys::VirtualFile file_)
+ : AppLoader(std::move(file_)), nup(std::make_unique<NupContainer>(file)) {
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return;
+ }
+
+ const auto base_index = nup->FindBasePackage();
+ const auto update_index = nup->FindBestUpdatePackage();
+
+ // The program comes from the base package; a standalone update container
+ // (SPEC §4.1.2) carries a complete patched tree and boots on its own.
+ std::optional<std::size_t> program_index;
+ std::optional<std::size_t> exefs_index;
+ if (base_index && nup->GetExeFSDir(*base_index) != nullptr) {
+ program_index = base_index;
+ exefs_index = update_index && nup->GetExeFSDir(*update_index) != nullptr
+ ? update_index
+ : base_index;
+ } else if (update_index && nup->GetExeFSDir(*update_index) != nullptr) {
+ LOG_INFO(Loader,
+ "NUP container is a standalone update; booting the update program directly");
+ program_index = update_index;
+ exefs_index = update_index;
+ }
+ if (!program_index || !exefs_index) {
+ LOG_ERROR(Loader, "NUP container has no package with a usable ExeFS");
+ return;
+ }
+
+ const auto& program = nup->GetPackages()[*program_index];
+ base_title_id = program.title_id;
+ has_program = true;
+
+ const auto exefs = nup->GetExeFSDir(*exefs_index);
+ if (exefs == nullptr || exefs->GetFile("main") == nullptr ||
+ exefs->GetFile("main.npdm") == nullptr) {
+ LOG_ERROR(Loader, "NUP package has no usable ExeFS");
+ return;
+ }
+ secondary_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs, true);
+
+ if (update_index) {
+ const auto& update = nup->GetPackages()[*update_index];
+ display_version = update.display_version.empty() ? DecodeVersion(update.version)
+ : update.display_version;
+ } else {
+ display_version = program.display_version.empty() ? DecodeVersion(program.version)
+ : program.display_version;
+ }
+ nacp_file = BuildNacp(program, display_version);
+ if (nacp_file != nullptr && !program.icon.empty()) {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(program.icon, "icon.dat");
+ } else if (nacp_file != nullptr) {
+ for (const auto& [lang, icon] : program.localized_icons) {
+ if (lang == "en-us") {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(icon, "icon_" + lang + ".dat");
+ break;
+ }
+ }
+ if (icon_file == nullptr && !program.localized_icons.empty()) {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(
+ program.localized_icons.front().second, "icon.dat");
+ }
+ }
+
+ const auto addon_count = std::count_if(nup->GetPackages().begin(), nup->GetPackages().end(),
+ [](const auto& package) {
+ return package.kind ==
+ NupContainer::PackageKind::Addon;
+ });
+ if (addon_count > 0) {
+ LOG_WARNING(Loader,
+ "NUP container has {} add-on content package(s); AoC mounting is not "
+ "supported yet",
+ addon_count);
+ }
+}
+
+AppLoader_NUP::~AppLoader_NUP() = default;
+
+FileType AppLoader_NUP::IdentifyType(const FileSys::VirtualFile& nup_file) {
+ return FileSys::NupContainer::IsNupFile(nup_file) ? FileType::NUP : FileType::Error;
+}
+
+AppLoader::LoadResult AppLoader_NUP::Load(Kernel::KProcess& process, Core::System& system) {
+ if (is_loaded) {
+ return {ResultStatus::ErrorAlreadyLoaded, {}};
+ }
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return {nup->GetStatus(), {}};
+ }
+ if (!has_program || secondary_loader == nullptr) {
+ return {ResultStatus::ErrorNUPNoBasePackage, {}};
+ }
+
+ const auto result = secondary_loader->Load(process, system);
+ if (result.first != ResultStatus::Success) {
+ return result;
+ }
+
+ system.GetFileSystemController().RegisterProcess(
+ process.GetProcessId(), base_title_id,
+ std::make_shared<FileSys::RomFSFactory>(*this, system.GetContentProvider(),
+ system.GetFileSystemController()));
+
+ is_loaded = true;
+ return result;
+}
+
+ResultStatus AppLoader_NUP::VerifyIntegrity(
+ std::function<bool(size_t, size_t)> progress_callback) {
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return nup->GetStatus();
+ }
+ return nup->VerifyIntegrity(std::move(progress_callback));
+}
+
+ResultStatus AppLoader_NUP::ReadRomFS(FileSys::VirtualFile& out_file) {
+ const auto base_index = nup->FindBasePackage();
+ const auto base_romfs = base_index ? nup->GetRomFSDir(*base_index) : FileSys::VirtualDir{};
+ const auto update_index = nup->FindBestUpdatePackage();
+ const auto update_romfs =
+ update_index ? nup->GetRomFSDir(*update_index) : FileSys::VirtualDir{};
+ // A standalone update carries the complete merged tree; a base+update pair
+ // overlays the update onto the base (SPEC §4.1.4).
+ const auto merged = FileSys::NupContainer::MergeShadowDirs(base_romfs, update_romfs);
+ if (merged == nullptr) {
+ return ResultStatus::ErrorNoRomFS;
+ }
+ out_file = FileSys::CreateRomFS(merged);
+ if (out_file == nullptr) {
+ return ResultStatus::ErrorNoRomFS;
+ }
+ return ResultStatus::Success;
+}
+
+bool AppLoader_NUP::IsRomFSUpdatable() const {
+ // The update is already merged into the exposed RomFS.
+ return false;
+}
+
+ResultStatus AppLoader_NUP::ReadProgramId(u64& out_program_id) {
+ if (!has_program) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ out_program_id = base_title_id;
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadProgramIds(std::vector<u64>& out_program_ids) {
+ if (!has_program) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ out_program_ids.push_back(base_title_id);
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadIcon(std::vector<u8>& buffer) {
+ if (icon_file == nullptr) {
+ return ResultStatus::ErrorNoIcon;
+ }
+ buffer = icon_file->ReadAllBytes();
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadTitle(std::string& title) {
+ if (nacp_file == nullptr) {
+ return ResultStatus::ErrorNoControl;
+ }
+ title = nacp_file->GetApplicationName();
+ if (title.empty()) {
+ return ResultStatus::ErrorNoControl;
+ }
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadControlData(FileSys::NACP& control) {
+ if (nacp_file == nullptr) {
+ return ResultStatus::ErrorNoControl;
+ }
+ control = *nacp_file;
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadNSOModules(Modules& modules) {
+ if (secondary_loader == nullptr) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ return secondary_loader->ReadNSOModules(modules);
+}
+
+} // namespace Loader
diff --git a/src/core/loader/nup.h b/src/core/loader/nup.h
new file mode 100644
index 0000000000..1f9dd5922b
--- /dev/null
+++ b/src/core/loader/nup.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "core/file_sys/nup_container.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+class NACP;
+class NupContainer;
+} // namespace FileSys
+
+namespace Loader {
+
+/// Loads a NUP (NX Package) container: loose ExeFS/RomFS trees for a base
+/// program plus its updates in one file. The newest update package is layered
+/// over the base; add-on content packages are recognized but not yet mounted.
+class AppLoader_NUP final : public AppLoader {
+public:
+ explicit AppLoader_NUP(FileSys::VirtualFile file_);
+ ~AppLoader_NUP() override;
+
+ /**
+ * Identifies whether or not the given file is a NUP container.
+ *
+ * @param nup_file The file to identify.
+ *
+ * @return FileType::NUP, or FileType::Error if the file is not a NUP.
+ */
+ static FileType IdentifyType(const FileSys::VirtualFile& nup_file);
+
+ FileType GetFileType() const override {
+ return FileType::NUP;
+ }
+
+ LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
+
+ ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override;
+
+ ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override;
+ bool IsRomFSUpdatable() const override;
+ ResultStatus ReadProgramId(u64& out_program_id) override;
+ ResultStatus ReadProgramIds(std::vector<u64>& out_program_ids) override;
+ ResultStatus ReadIcon(std::vector<u8>& buffer) override;
+ ResultStatus ReadTitle(std::string& title) override;
+ ResultStatus ReadControlData(FileSys::NACP& control) override;
+ ResultStatus ReadNSOModules(Modules& modules) override;
+
+private:
+ std::unique_ptr<FileSys::NupContainer> nup;
+ std::unique_ptr<AppLoader> secondary_loader;
+
+ std::unique_ptr<FileSys::NACP> nacp_file;
+ FileSys::VirtualFile icon_file;
+ u64 base_title_id{};
+ std::string display_version;
+ bool has_program{};
+};
+
+} // namespace Loader