aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-11-02 17:52:11 +0100
committercirakg <cirakg@cirakg.xyz>2026-11-02 17:52:11 +0100
commit7fe395e5ab7a42a73262be3ca69b640d2ccd6ec2 (patch)
treef0b9eea55b3e1864492d19842950562d3cb17427 /src/core
parentbf2a94eb29f13073848a16754c983d53a3fc6d44 (diff)
parentead60c761e4d58cb4adc8b3b08f67dc10cf1e36b (diff)
Merge upstream Djihads80/Eglantine nx2 into nx2
Diffstat (limited to 'src/core')
-rw-r--r--src/core/file_sys/nup_writer.cpp99
-rw-r--r--src/core/file_sys/nup_writer.h17
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp47
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 {};
}