From 0164b919f8c1ee99bbb89fe3fefccdc8b671380f Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:20:42 +0200 Subject: [PATCH] Add support for loading unpacked DLCs from dlc/ folder --- .../include/rex/system/xam/content_manager.h | 20 +- .../src/system/xam/content_manager.cpp | 183 +++++++++++++++--- 2 files changed, 166 insertions(+), 37 deletions(-) diff --git a/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h b/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h index d6319711..f32b5ff5 100644 --- a/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h +++ b/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h @@ -159,6 +159,9 @@ struct DiscoveredContainer { uint32_t title_id = 0; std::u16string display_name; std::string source; // short label for the startup report ("dlc", "content root") + // A package can be a raw container file or an already-extracted folder; both + // mount, ContentPackage picks the device by what the path is. + bool is_extracted_folder = false; }; class ContentManager { @@ -204,13 +207,14 @@ class ContentManager { // and writes a .header file for XAM enumeration. X_RESULT InstallContent(const std::filesystem::path& package_path); - // Scans for raw content containers - a dlc folder (recursive, so a flat - // dump and Xenia's content-directory layout both work) plus container files - // dropped straight into the content root - and logs one line per package - // found. Only containers for the running title are indexed, so call after - // the module is loaded. Containers take priority over an ambient extracted - // install: the user placed them next to the exe, so they win - same rule as - // the assets folder (and Windows' local-DLL search order). + // Scans the dlc folder (recursive, so a flat dump and Xenia's + // content-directory layout both work) plus container files dropped straight + // into the content root, and logs one line per package found. In the dlc + // folder a package may be either a raw container file or an ALREADY-EXTRACTED + // package folder - both mount. Only packages for the running title are + // indexed, so call after the module is loaded. What the user placed in dlc + // takes priority over an ambient extracted install - same rule as the assets + // folder (and Windows' local-DLL search order). void DiscoverContainers(const std::filesystem::path& dlc_dir); private: @@ -234,7 +238,7 @@ class ContentManager { const DiscoveredContainer* FindContainer(const std::string_view file_name, XContentType content_type) const; void DiscoverContainersInDir(const std::filesystem::path& dir, const char* source, - bool recursive, uint32_t title_id); + bool recursive, bool index_directories, uint32_t title_id); KernelState* kernel_state_; std::filesystem::path root_path_; diff --git a/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp b/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp index 64182069..29acd0b6 100644 --- a/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp +++ b/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -779,13 +780,80 @@ X_RESULT ContentManager::InstallContent(const std::filesystem::path& package_pat // wild usually ship one; that is normal and harmless. static constexpr uint32_t kTitleUpdateContentType = 0x000B0000; +// Package folder names are the content id in hex (42 chars in practice); this +// is the fallback rule for an extracted package sitting loose in the dlc +// folder, without the title-id/content-type directories around it. +static constexpr size_t kMinPackageNameLength = 40; + +// Parses exactly eight hex digits - the form the title-id and content-type +// directories take in the standard content layout. +static bool ParseHex8(const std::string& text, uint32_t* out_value) { + if (text.size() != 8) { + return false; + } + uint32_t value = 0; + for (char c : text) { + uint32_t digit; + if (c >= '0' && c <= '9') { + digit = uint32_t(c - '0'); + } else if (c >= 'a' && c <= 'f') { + digit = uint32_t(c - 'a') + 10; + } else if (c >= 'A' && c <= 'F') { + digit = uint32_t(c - 'A') + 10; + } else { + return false; + } + value = (value << 4) | digit; + } + *out_value = value; + return true; +} + +static bool IsHexName(const std::string& text) { + return !text.empty() && std::all_of(text.cbegin(), text.cend(), [](unsigned char c) { + return std::isxdigit(c) != 0; + }); +} + void ContentManager::DiscoverContainersInDir(const std::filesystem::path& dir, const char* source, - bool recursive, uint32_t title_id) { + bool recursive, bool index_directories, + uint32_t title_id) { std::error_code ec; if (dir.empty() || !std::filesystem::is_directory(dir, ec) || ec) { return; } + auto index_package = [&](const std::filesystem::path& path, XContentType content_type, + std::u16string display_name, bool is_extracted_folder) { + // Key = the name enumeration hands to the game (42-char field). + auto key_name = rex::path_to_utf8(path.filename()); + if (key_name.size() > 42) { + key_name.resize(42); + } + if (containers_.count(string::string_key_case(key_name))) { + REXSYS_DEBUG("ContentManager: duplicate package name {}, keeping the first found", key_name); + return; + } + if (display_name.empty()) { + // An extracted folder carries no header of its own, but the .header + // sidecar in the content root still names it when one was written. + XCONTENT_AGGREGATE_DATA header_data; + if (XSUCCEEDED(ReadContentHeaderFile(key_name, 0, title_id, + XContentType::kMarketplaceContent, header_data))) { + display_name = header_data.display_name(); + } + } + + DiscoveredContainer package; + package.path = path; + package.content_type = content_type; + package.title_id = title_id; + package.display_name = std::move(display_name); + package.source = source; + package.is_extracted_folder = is_extracted_folder; + containers_.insert({string::string_key_case::create(key_name), std::move(package)}); + }; + auto handle_file = [&](const std::filesystem::path& path) { auto header = rex::filesystem::StfsContainerDevice::ReadPackageHeader(path); if (!header) { @@ -811,24 +879,59 @@ void ContentManager::DiscoverContainersInDir(const std::filesystem::path& dir, c return; } - // Key = the file name enumeration hands to the game (42-char field). - auto key_name = rex::path_to_utf8(path.filename()); - if (key_name.size() > 42) { - key_name.resize(42); - } - if (containers_.count(string::string_key_case(key_name))) { - REXSYS_DEBUG("ContentManager: duplicate container name {}, keeping the first found", - key_name); - return; + index_package(path, content_type, header->metadata.display_name(XLanguage::kEnglish), false); + }; + + // Recognises an ALREADY-EXTRACTED package folder, so users who unpacked + // their DLC do not have to repack it. A folder is a package when it either + // sits directly under /<content type>/ (the standard content + // layout, whatever the folder itself is called) or is named after the + // content id in hex. Returns true when the directory IS a package folder, + // so the caller stops the walk from descending into its assets. + auto handle_directory = [&](const std::filesystem::path& path) { + const auto name = rex::path_to_utf8(path.filename()); + XContentType content_type = XContentType::kMarketplaceContent; + uint32_t parent_type = 0; + uint32_t grandparent_title = 0; + const bool anchored = + ParseHex8(rex::path_to_utf8(path.parent_path().filename()), &parent_type) && + ParseHex8(rex::path_to_utf8(path.parent_path().parent_path().filename()), + &grandparent_title) && + grandparent_title == title_id; + if (anchored) { + content_type = XContentType(parent_type); + } else if (name.size() < kMinPackageNameLength || !IsHexName(name)) { + // An ordinary folder on the way down; keep walking into it. + return false; } - DiscoveredContainer container; - container.path = path; - container.content_type = content_type; - container.title_id = container_title; - container.display_name = header->metadata.display_name(XLanguage::kEnglish); - container.source = source; - containers_.insert({string::string_key_case::create(key_name), std::move(container)}); + if (uint32_t(content_type) == kTitleUpdateContentType) { + REXSYS_DEBUG("ContentManager: title update package present (not used by this port): {}", + name); + return true; + } + if (content_type != XContentType::kMarketplaceContent) { + REXSYS_DEBUG("ContentManager: ignoring extracted package of content type {:08X}: {}", + uint32_t(content_type), rex::path_to_utf8(path)); + return true; + } + + // A package folder holds the package's files; an empty one is not content. + bool has_file = false; + std::error_code dir_ec; + for (auto it = std::filesystem::directory_iterator(path, dir_ec); + !dir_ec && it != std::filesystem::directory_iterator(); it.increment(dir_ec)) { + if (it->is_regular_file(dir_ec)) { + has_file = true; + break; + } + } + if (!has_file) { + return false; + } + + index_package(path, content_type, std::u16string(), true); + return true; }; if (recursive) { @@ -837,6 +940,9 @@ void ContentManager::DiscoverContainersInDir(const std::filesystem::path& dir, c !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { if (it->is_regular_file(ec)) { handle_file(it->path()); + } else if (index_directories && it->is_directory(ec) && handle_directory(it->path())) { + // It is a package folder, not a step on the way to one. + it.disable_recursion_pending(); } } } else { @@ -845,6 +951,8 @@ void ContentManager::DiscoverContainersInDir(const std::filesystem::path& dir, c !ec && it != std::filesystem::directory_iterator(); it.increment(ec)) { if (it->is_regular_file(ec)) { handle_file(it->path()); + } else if (index_directories && it->is_directory(ec)) { + handle_directory(it->path()); } } } @@ -854,12 +962,18 @@ void ContentManager::DiscoverContainers(const std::filesystem::path& dlc_dir) { const uint32_t title_id = kernel_state_->title_id(); containers_.clear(); - // The dlc folder next to the exe: recursive, so a flat dump, an unzipped - // content set, and Xenia's content-directory layout all work unchanged. - DiscoverContainersInDir(dlc_dir, "dlc", true, title_id); + // The dlc folder next to the exe: recursive and accepting extracted package + // folders as well as containers, so a flat dump, an unzipped content set, + // Xenia's content-directory layout, and an already-extracted install all + // work unchanged. + DiscoverContainersInDir(dlc_dir, "dlc", /*recursive=*/true, /*index_directories=*/true, + title_id); // Containers dropped straight into the content root's marketplace folder. + // Directories there are the canonical install, already found by the normal + // package path, so they are not indexed as placed packages. DiscoverContainersInDir(ResolvePackageRoot(0, XContentType::kMarketplaceContent, title_id), - "content root", false, title_id); + "content root", /*recursive=*/false, /*index_directories=*/false, + title_id); // Startup report, one line per package: "did my DLC install work?" becomes // a log grep instead of a guess. Containers take priority, so a folder with @@ -878,10 +992,13 @@ void ContentManager::DiscoverContainers(const std::filesystem::path& dlc_dir) { content_data))) { display = rex::string::to_utf8(content_data.display_name()); } - const bool overridden = - FindContainer(name, XContentType::kMarketplaceContent) != nullptr; + const auto* overriding = FindContainer(name, XContentType::kMarketplaceContent); + std::string note; + if (overriding) { + note = fmt::format(" (overridden by {})", overriding->source); + } REXSYS_INFO("DLC: \"{}\" [{}] - extracted folder{}", display.empty() ? name : display, name, - overridden ? " (overridden by a container)" : ""); + note); folder_count++; } @@ -892,14 +1009,22 @@ void ContentManager::DiscoverContainers(const std::filesystem::path& dlc_dir) { } std::sort(sorted_containers.begin(), sorted_containers.end(), [](const auto* a, const auto* b) { return a->first.view() < b->first.view(); }); + size_t extracted_count = 0; for (const auto* entry : sorted_containers) { const auto& container = entry->second; - REXSYS_INFO("DLC: \"{}\" [{}] - container ({}): {}", - rex::string::to_utf8(container.display_name), entry->first.view(), - container.source, rex::path_to_utf8(container.path)); + auto display = rex::string::to_utf8(container.display_name); + if (display.empty()) { + display = std::string(entry->first.view()); + } + REXSYS_INFO("DLC: \"{}\" [{}] - {} ({}): {}", display, entry->first.view(), + container.is_extracted_folder ? "extracted folder" : "container", container.source, + rex::path_to_utf8(container.path)); + extracted_count += container.is_extracted_folder ? 1 : 0; } - REXSYS_INFO("DLC: {} extracted folder(s), {} container(s) mounted with priority", folder_count, - containers_.size()); + REXSYS_INFO( + "DLC: {} extracted folder(s) in the content root, {} package(s) mounted with priority " + "({} container(s), {} extracted folder(s))", + folder_count, containers_.size(), containers_.size() - extracted_count, extracted_count); } } // namespace xam