diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-11-02 22:58:00 +0100 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-11-02 22:58:00 +0100 |
| commit | cf0502ad274022480cb264e0e75390863cd49608 (patch) | |
| tree | fd672b27a4cbddffb3acbb09ee59d3dd96ba489c /src/core | |
| parent | ff234849d6a41d2c2d29cf09408ac66e202d68e7 (diff) | |
| parent | 0da830e94169b9040007b69c8147ab00b6574e36 (diff) | |
Merge branch 'nx2' into ldn/room-handshake
Diffstat (limited to 'src/core')
| -rw-r--r-- | src/core/file_sys/nup_writer.cpp | 99 | ||||
| -rw-r--r-- | src/core/file_sys/nup_writer.h | 17 | ||||
| -rw-r--r-- | src/core/loader/deconstructed_rom_directory.cpp | 47 |
3 files changed, 143 insertions, 20 deletions
diff --git a/src/core/file_sys/nup_writer.cpp b/src/core/file_sys/nup_writer.cpp index 8c7728ed01..4161969f98 100644 --- a/src/core/file_sys/nup_writer.cpp +++ b/src/core/file_sys/nup_writer.cpp @@ -8,6 +8,7 @@ #include <cctype> #include <charconv> #include <cstring> +#include <functional> #include <limits> #include <map> #include <memory> @@ -516,8 +517,9 @@ std::unique_ptr<Loader::AppLoader_DeconstructedRomDirectory> OpenLoader( class Discovery { public: - Discovery(const fs::path& dump_root_, std::vector<fs::path> excluded_) - : dump_root{dump_root_}, excluded{std::move(excluded_)} {} + Discovery(const fs::path& dump_root_, std::vector<fs::path> excluded_, + const Metadata& overrides_) + : dump_root{dump_root_}, excluded{std::move(excluded_)}, overrides{overrides_} {} Plan Run() { const auto base = FindDirectory(dump_root, "base"); @@ -678,6 +680,34 @@ private: tlvs.emplace_back(TlvIcon, std::move(icon)); } } + + if (!overrides.title_name.empty()) { + SetTlv(tlvs, TlvTitleName, StringBytes(overrides.title_name)); + } + if (!overrides.icon.empty()) { + SetTlv(tlvs, TlvIcon, overrides.icon); + } + if (!overrides.display_version.empty()) { + // The loader reports the newest update's version when there is one. + SetTlv(tlvs, TlvDisplayVersion, StringBytes(overrides.display_version)); + std::optional<u32> newest; + for (u32 i = 0; i < plan.packages.size(); ++i) { + if (plan.packages[i].kind == KindUpdate && + (!newest || plan.packages[i].version >= plan.packages[*newest].version)) { + newest = i; + } + } + if (newest && *newest != target) { + SetTlv(package_tlvs[*newest], TlvDisplayVersion, + StringBytes(overrides.display_version)); + } + } + } + + static void SetTlv(std::vector<std::pair<u16, std::vector<u8>>>& tlvs, u16 type, + std::vector<u8> payload) { + std::erase_if(tlvs, [type](const auto& tlv) { return tlv.first == type; }); + tlvs.emplace_back(type, std::move(payload)); } bool IsExcluded(const fs::path& path) const { @@ -722,6 +752,7 @@ private: fs::path dump_root; std::vector<fs::path> excluded; + Metadata overrides; RealVfsFilesystem vfs; Plan plan; std::vector<std::vector<std::pair<u16, std::vector<u8>>>> package_tlvs; @@ -838,7 +869,7 @@ public: for (const auto& path : {archive, part, journal}) { excluded.push_back(fs::absolute(path).lexically_normal()); } - plan = Discovery{dump_root, std::move(excluded)}.Run(); + plan = Discovery{dump_root, std::move(excluded), options.metadata}.Run(); layout = BuildLayout(plan); OpenNew(); } @@ -1421,6 +1452,68 @@ std::optional<fs::path> DumpRootFromMain(const fs::path& main) { return root; } +Metadata ProbeMetadata(const fs::path& dump_root) { + RealVfsFilesystem vfs; + const auto base = FindDirectory(dump_root, "base"); + const auto updates = FindDirectory(dump_root, "updates"); + + // Newest update first, then the base (or a bare dump's root). + std::vector<std::pair<u64, fs::path>> update_dirs; + std::error_code ec; + if (!updates.empty()) { + for (const auto& entry : fs::directory_iterator(updates, ec)) { + const auto version = ParseDecimal(ToUtf8(entry.path().filename())); + if (version && entry.is_directory(ec)) { + update_dirs.emplace_back(*version, entry.path()); + } + } + } + std::ranges::sort(update_dirs, std::greater<>{}); + const fs::path base_dir = !base.empty() ? base : (updates.empty() ? dump_root : fs::path{}); + + Metadata result; + const auto probe = [&](const fs::path& dir, const std::string& label, bool names) { + const auto loader = OpenLoader(vfs, dir); + if (!loader) { + return; + } + if (result.display_version.empty()) { + std::string version; + if (loader->ReadDisplayVersion(version) == Loader::ResultStatus::Success && + !version.empty()) { + result.display_version = version; + result.version_source = label; + } + } + if (names) { + NACP control; + if (result.title_name.empty() && + loader->ReadControlData(control) == Loader::ResultStatus::Success) { + result.title_name = control.GetApplicationName(); + } + std::vector<u8> icon; + if (result.icon.empty() && + loader->ReadIcon(icon) == Loader::ResultStatus::Success) { + result.icon = std::move(icon); + } + } + }; + for (const auto& [version, dir] : update_dirs) { + probe(dir, "update " + std::to_string(version), false); + } + if (!base_dir.empty()) { + probe(base_dir, "base", true); + } + // Names and icons fall back to the updates when the base has none. + for (const auto& [version, dir] : update_dirs) { + if (!result.title_name.empty() && !result.icon.empty()) { + break; + } + probe(dir, "update " + std::to_string(version), true); + } + return result; +} + fs::path DefaultArchivePath(const fs::path& dump_root) { auto root = dump_root; if (!root.has_filename()) { diff --git a/src/core/file_sys/nup_writer.h b/src/core/file_sys/nup_writer.h index 231387b156..d2423dd1f4 100644 --- a/src/core/file_sys/nup_writer.h +++ b/src/core/file_sys/nup_writer.h @@ -8,6 +8,7 @@ #include <functional> #include <optional> #include <string> +#include <vector> #include "common/common_types.h" @@ -30,6 +31,17 @@ enum class PackResult { Success, Cancelled, InvalidSource, IoError }; // that one file does not evict everything else from the cache. constexpr u64 DefaultRawThreshold = 256ULL * 1024 * 1024; +// Application metadata written to the base package (or the first package when there is no base). +// In PackOptions an empty field keeps what the dump supplies; ProbeMetadata fills all of them. +struct Metadata { + std::string title_name; + // JPEG or PNG bytes; 256x256 JPEG as distributed on-console. + std::vector<u8> icon; + std::string display_version; + // Where ProbeMetadata found display_version, e.g. "update 458752" or "base". + std::string version_source; +}; + struct PackOptions { // Low-storage mode: delete each source file once its extent is synced and journaled. bool delete_sources = false; @@ -37,6 +49,8 @@ struct PackOptions { int zstd_level = 3; // Files larger than this are always stored raw. u64 raw_threshold = DefaultRawThreshold; + // Overrides for the name, icon and display version found in the dump. + Metadata metadata; }; // A loose dump holds exefs/main and main.npdm, directly or under base/ or updates/<version>/, @@ -45,6 +59,9 @@ bool IsLooseDump(const std::filesystem::path& dir); // The dump folder a game list entry's exefs/main (or exefs folder) belongs to, if it is a loose // dump. std::optional<std::filesystem::path> DumpRootFromMain(const std::filesystem::path& main); +// Reads the name, icon and display version a dump would be packed with. The version comes from +// the newest update that reports one, else the base. +Metadata ProbeMetadata(const std::filesystem::path& dump_root); std::filesystem::path DefaultArchivePath(const std::filesystem::path& dump_root); // Writes <archive>.part and renames it when done; an existing archive is never overwritten. A diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 60cf87db14..4b1168a59b 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -117,27 +117,19 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { return nullptr; } -// The display version of a dump without control data, from its RomFS: Nintendo EPD titles name -// their resource size tables after it (System/Resource/ResourceSizeTable.Product.180.* in Mario -// Kart World 1.8.0). A hotfix can keep its minor version's tables (Product.110 in a 1.1.1). -std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) { - if (romfs_dir == nullptr) { - return {}; - } - const auto system_dir = romfs_dir->GetSubdirectory("System"); - const auto resource_dir = system_dir != nullptr ? system_dir->GetSubdirectory("Resource") : nullptr; - if (resource_dir == nullptr) { - return {}; - } - constexpr std::string_view Prefix = "ResourceSizeTable.Product."; +// The newest three-digit tag in a directory's file names of the form <prefix>NNN.<anything>. +std::string NewestProductTag(const FileSys::VirtualDir& dir, std::string_view prefix) { std::string best; - for (const auto& file : resource_dir->GetFiles()) { + if (dir == nullptr) { + return best; + } + for (const auto& file : dir->GetFiles()) { const std::string file_name = file->GetName(); - if (!file_name.starts_with(Prefix) || file_name.size() < Prefix.size() + 4) { + if (!file_name.starts_with(prefix) || file_name.size() < prefix.size() + 4) { continue; } - const std::string_view digits = std::string_view{file_name}.substr(Prefix.size(), 3); - if (file_name[Prefix.size() + 3] != '.' || + const std::string_view digits = std::string_view{file_name}.substr(prefix.size(), 3); + if (file_name[prefix.size() + 3] != '.' || !std::all_of(digits.begin(), digits.end(), [](char c) { return c >= '0' && c <= '9'; })) { continue; } @@ -145,6 +137,27 @@ std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) { best = version; } } + return best; +} + +// The display version of a dump without control data, from its RomFS: Nintendo EPD titles name +// their resource size tables after it (System/Resource/ResourceSizeTable.Product.180.* in Mario +// Kart World 1.8.0), and some name their game data list instead (GameData/GameDataList.Product.320.* +// in Donkey Kong Bananza 3.2.0). A hotfix can keep its minor version's tables (Product.110 in a +// 1.1.1). +std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) { + if (romfs_dir == nullptr) { + return {}; + } + const auto system_dir = romfs_dir->GetSubdirectory("System"); + std::string best = NewestProductTag( + system_dir != nullptr ? system_dir->GetSubdirectory("Resource") : nullptr, + "ResourceSizeTable.Product."); + if (const auto game_data = NewestProductTag(romfs_dir->GetSubdirectory("GameData"), + "GameDataList.Product."); + game_data > best) { + best = game_data; + } if (best.empty()) { return {}; } |
