mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Add support for loading unpacked DLCs from dlc/ folder
This commit is contained in:
@@ -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_;
|
||||
|
||||
+154
-29
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
@@ -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 <title id>/<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
|
||||
|
||||
Reference in New Issue
Block a user