Mount the game from a .iso and DLC from raw containers directly

The game now mounts straight from a .iso beside the exe (selected by
title id; the assets folder still wins, and loose files layer over the
image so mods need no extraction), and DLC mounts straight from raw
STFS containers in a dlc folder, any layout, title updates ignored.
Disc image device ported from Xenia (BSD); one log line per source.
This commit is contained in:
Dipshet
2026-08-06 02:37:42 +02:00
parent 33c2cf60cc
commit 9b81d7b7e6
16 changed files with 1219 additions and 45 deletions
+4
View File
@@ -36,6 +36,10 @@ class Ac6recompApp : public rex::ReXApp {
}
protected:
// Ace Combat 6 title id: selects the right .iso when several are present
// and rejects wrong/corrupt images with a clear message.
uint32_t OnGetExpectedTitleId() const override { return 0x4E4D07D1; }
void OnPreSetup(rex::RuntimeConfig& config) override {
REXLOG_INFO("Ac6recompApp::OnPreSetup");
rex::ReXApp::OnPreSetup(config);
@@ -24,6 +24,13 @@ class Device {
const std::string& mount_path() const { return mount_path_; }
virtual bool is_read_only() const { return true; }
// Layered mounts: when true, a path that this device fails to resolve falls
// through to the next registered device whose mount path also matches
// (registration order = priority, so earlier devices win). Default false =
// this device is terminal for its mount, exactly the historical behaviour.
bool layered() const { return layered_; }
void set_layered(bool layered) { layered_ = layered; }
virtual void Dump(string::StringBuffer* string_buffer) = 0;
virtual Entry* ResolvePath(const std::string_view path) = 0;
@@ -39,6 +46,7 @@ class Device {
protected:
rex::thread::global_critical_region global_critical_region_;
std::string mount_path_;
bool layered_ = false;
};
} // namespace rex::filesystem
@@ -0,0 +1,84 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <memory>
#include <set>
#include <string>
#include <rex/filesystem/device.h>
#include <rex/memory/mapped_memory.h>
namespace rex::filesystem {
class DiscImageEntry;
// Read-only XDVDFS (GDFX) disc image device. Mounts a user-supplied .iso
// directly so no extraction step is required. Accepts both full dumps (game
// partition at a known base offset) and rebuilt game-partition-only images.
class DiscImageDevice : public Device {
public:
DiscImageDevice(const std::string_view mount_path, const std::filesystem::path& host_path);
~DiscImageDevice() override;
bool Initialize() override;
void Dump(string::StringBuffer* string_buffer) override;
Entry* ResolvePath(const std::string_view path) override;
const std::string& name() const override { return name_; }
uint32_t attributes() const override { return 0; }
uint32_t component_name_max_length() const override { return 255; }
const std::filesystem::path& host_path() const { return host_path_; }
uint32_t total_allocation_units() const override {
return uint32_t(mmap_->size() / sectors_per_allocation_unit() / bytes_per_sector());
}
uint32_t available_allocation_units() const override { return 0; }
uint32_t sectors_per_allocation_unit() const override { return 1; }
uint32_t bytes_per_sector() const override { return 0x200; }
private:
enum class Error {
kSuccess = 0,
kErrorOutOfMemory = -1,
kErrorReadError = -10,
kErrorFileMismatch = -30,
kErrorDamagedFile = -31,
};
struct ParseState {
uint8_t* ptr = nullptr;
size_t size = 0; // Size (bytes) of total image.
size_t game_offset = 0; // Offset (bytes) of game partition.
size_t root_sector = 0; // Offset (sector) of root.
size_t root_offset = 0; // Offset (bytes) of root.
size_t root_size = 0; // Size (bytes) of root.
// Directory table offsets already walked; a repeat means a cycle in a
// damaged/malicious image and aborts the parse instead of recursing
// forever.
std::set<size_t> visited_tables;
};
Error Verify(ParseState* state);
bool VerifyMagic(ParseState* state, size_t offset);
Error ReadAllEntries(ParseState* state);
bool ReadDirectory(ParseState* state, size_t table_offset, size_t table_size,
DiscImageEntry* parent, uint32_t depth);
std::string name_;
std::filesystem::path host_path_;
std::unique_ptr<Entry> root_entry_;
std::unique_ptr<memory::MappedMemory> mmap_;
};
} // namespace rex::filesystem
@@ -0,0 +1,53 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <string>
#include <vector>
#include <rex/filesystem/entry.h>
#include <rex/filesystem/file.h>
#include <rex/memory/mapped_memory.h>
namespace rex::filesystem {
class DiscImageDevice;
class DiscImageEntry : public Entry {
public:
DiscImageEntry(Device* device, Entry* parent, const std::string_view path,
memory::MappedMemory* mmap);
~DiscImageEntry() override;
static std::unique_ptr<DiscImageEntry> Create(Device* device, Entry* parent,
const std::string_view name,
memory::MappedMemory* mmap);
memory::MappedMemory* mmap() const { return mmap_; }
size_t data_offset() const { return data_offset_; }
size_t data_size() const { return data_size_; }
X_STATUS Open(uint32_t desired_access, File** out_file) override;
bool can_map() const override { return true; }
std::unique_ptr<memory::MappedMemory> OpenMapped(memory::MappedMemory::Mode mode, size_t offset,
size_t length) override;
private:
friend class DiscImageDevice;
memory::MappedMemory* mmap_;
size_t data_offset_;
size_t data_size_;
};
} // namespace rex::filesystem
@@ -0,0 +1,45 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <rex/filesystem/file.h>
namespace rex::filesystem {
class DiscImageEntry;
class DiscImageFile : public File {
public:
DiscImageFile(uint32_t file_access, DiscImageEntry* entry);
~DiscImageFile() override;
void Destroy() override;
X_STATUS ReadSync(std::span<uint8_t> buffer, size_t byte_offset,
size_t* out_bytes_read) override;
X_STATUS WriteSync(std::span<const uint8_t> buffer, size_t byte_offset,
size_t* out_bytes_written) override {
(void)buffer;
(void)byte_offset;
(void)out_bytes_written;
return X_STATUS_ACCESS_DENIED;
}
X_STATUS SetLength(size_t length) override {
(void)length;
return X_STATUS_ACCESS_DENIED;
}
private:
DiscImageEntry* entry_;
};
} // namespace rex::filesystem
+11
View File
@@ -102,6 +102,11 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win
/// Override to adjust game/user/update data paths programmatically.
virtual void OnConfigurePaths(PathConfig& paths) { (void)paths; }
/// Expected xex title id used to select a disc image when several are
/// present (and to reject wrong/corrupt images with a clear message).
/// Return 0 to accept any image that contains a readable default.xex.
virtual uint32_t OnGetExpectedTitleId() const { return 0; }
// --- Accessors for subclass use ---
Runtime* runtime() const { return runtime_.get(); }
ui::Window* window() const { return window_.get(); }
@@ -120,6 +125,12 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win
bool OnInitialize() override;
void OnDestroy() override;
// Resolves where the game's data comes from: the loose assets directory
// (always first), else a user-supplied disc image. On success out_image is
// the image to mount below the loose layer (empty = loose only). On failure
// a clear, actionable message has been shown and logged.
bool ResolveGameSource(const std::filesystem::path& exe_dir, std::filesystem::path& out_image);
// WindowListener overrides
void OnClosing(ui::UIEvent& e) override;
+8
View File
@@ -110,6 +110,13 @@ class Runtime {
const std::filesystem::path& user_data_root() const { return user_data_root_; }
const std::filesystem::path& update_data_root() const { return update_data_root_; }
// Optional disc image mounted as the game volume (call before Setup).
// When set together with an existing game_data_root, the loose directory is
// mounted as a layered top (loose files win, the image fills the gaps);
// when game_data_root is empty or missing, the image alone backs game:.
void set_game_image_path(const std::filesystem::path& path) { game_image_path_ = path; }
const std::filesystem::path& game_image_path() const { return game_image_path_; }
// Set the app context for presentation (call before Setup)
void set_app_context(ui::WindowedAppContext* context) {
app_context_ = context;
@@ -161,6 +168,7 @@ class Runtime {
std::filesystem::path game_data_root_;
std::filesystem::path user_data_root_;
std::filesystem::path update_data_root_;
std::filesystem::path game_image_path_;
ui::WindowedAppContext* app_context_ = nullptr;
ui::Window* display_window_ = nullptr;
@@ -119,6 +119,9 @@ static_assert_size(XCONTENT_AGGREGATE_DATA, 0x148);
class ContentPackage {
public:
// package_path may be an extracted folder (mounted as a host-path device,
// the historical behaviour) or a raw STFS/LIVE container file (mounted
// directly through the STFS device, no extraction required).
ContentPackage(KernelState* kernel_state, const std::string_view root_name,
const XCONTENT_AGGREGATE_DATA& data, const std::filesystem::path& package_path);
~ContentPackage();
@@ -129,7 +132,13 @@ class ContentPackage {
const std::filesystem::path& package_path() const { return package_path_; }
uint32_t GetPackageLicense() const { return license_; }
// The .header sidecar wins when present; a directly-mounted container falls
// back to the license bits carried in its own STFS header.
uint32_t GetPackageLicense() const { return license_ ? license_ : container_license_; }
bool is_container() const { return is_container_; }
// False when a container file failed to mount (corrupt/truncated package).
bool device_mounted() const { return device_mounted_; }
private:
KernelState* kernel_state_;
@@ -138,6 +147,18 @@ class ContentPackage {
std::filesystem::path package_path_;
XCONTENT_AGGREGATE_DATA content_data_;
uint32_t license_ = 0;
uint32_t container_license_ = 0;
bool is_container_ = false;
bool device_mounted_ = false;
};
// A raw content container discovered on disk, mountable without extraction.
struct DiscoveredContainer {
std::filesystem::path path;
XContentType content_type;
uint32_t title_id = 0;
std::u16string display_name;
std::string source; // short label for the startup report ("dlc", "content root")
};
class ContentManager {
@@ -183,6 +204,15 @@ 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).
void DiscoverContainers(const std::filesystem::path& dlc_dir);
private:
std::filesystem::path ResolvePackageRoot(uint64_t xuid, XContentType content_type,
uint32_t title_id = -1);
@@ -197,6 +227,15 @@ class ContentManager {
ContentPackage* DetachPackage(std::unordered_map<string::string_key_case, ContentPackage*,
string::string_key_case::Hash>::iterator it);
// Where a package's data actually lives: a discovered container if one
// exists (containers take priority), else the extracted folder / container
// file at the canonical package path, else empty.
std::filesystem::path ResolvePackageDataPath(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data);
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);
KernelState* kernel_state_;
std::filesystem::path root_path_;
@@ -204,6 +243,10 @@ class ContentManager {
rex::thread::global_critical_region global_critical_region_;
std::unordered_map<string::string_key_case, ContentPackage*, string::string_key_case::Hash>
open_packages_;
// Discovered raw containers, keyed by the (42-char-truncated) file name the
// game sees in enumeration results.
std::unordered_map<string::string_key_case, DiscoveredContainer, string::string_key_case::Hash>
containers_;
};
} // namespace xam
@@ -5,6 +5,9 @@ add_library(rexfilesystem STATIC
device.cpp
entry.cpp
virtual_file_system.cpp
devices/disc_image_device.cpp
devices/disc_image_entry.cpp
devices/disc_image_file.cpp
devices/host_path_device.cpp
devices/host_path_entry.cpp
devices/host_path_file.cpp
@@ -0,0 +1,244 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime; parser hardened
* against rebuilt/truncated images (0xFFFF node terminators,
* per-entry bounds checks, directory-cycle detection).
*/
#include <rex/filesystem/devices/disc_image_device.h>
#include <rex/filesystem/devices/disc_image_entry.h>
#include <cstring>
#include <vector>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/memory.h>
namespace rex::filesystem {
// XDVDFS sector size. Independent of the reported bytes_per_sector (0x200),
// which mirrors what the console reports for the mounted volume.
const size_t kXESectorSize = 2048;
// Directory-entry ordinals are uint16 indexes of 4-byte words into the
// directory table. 0 and 0xFFFF both terminate a branch in the wild: 0 in
// original pressings, 0xFFFF in images rebuilt by common extraction tools.
const uint16_t kOrdinalTerminator = 0xFFFF;
// Fixed part of a directory entry before the name bytes.
const size_t kEntryHeaderSize = 14;
// Real discs nest a handful of levels; anything deeper is a damaged image.
const uint32_t kMaxDirectoryDepth = 64;
DiscImageDevice::DiscImageDevice(const std::string_view mount_path,
const std::filesystem::path& host_path)
: Device(mount_path), name_("GDFX"), host_path_(host_path) {}
DiscImageDevice::~DiscImageDevice() = default;
bool DiscImageDevice::Initialize() {
mmap_ = memory::MappedMemory::Open(host_path_, memory::MappedMemory::Mode::kRead);
if (!mmap_) {
REXFS_ERROR("DiscImageDevice: could not map disc image: {}", rex::path_to_utf8(host_path_));
return false;
}
ParseState state;
state.ptr = mmap_->data();
state.size = mmap_->size();
auto result = Verify(&state);
if (result != Error::kSuccess) {
REXFS_ERROR("DiscImageDevice: failed to verify disc image header ({}): {}", int(result),
rex::path_to_utf8(host_path_));
return false;
}
result = ReadAllEntries(&state);
if (result != Error::kSuccess) {
REXFS_ERROR("DiscImageDevice: failed to read GDFX directory tree ({}): {}", int(result),
rex::path_to_utf8(host_path_));
return false;
}
return true;
}
void DiscImageDevice::Dump(string::StringBuffer* string_buffer) {
auto global_lock = global_critical_region_.Acquire();
root_entry_->Dump(string_buffer, 0);
}
Entry* DiscImageDevice::ResolvePath(const std::string_view path) {
// The filesystem will have stripped our prefix off already, so the path will
// be in the form:
// some\PATH.foo
REXFS_TRACE("DiscImageDevice::ResolvePath({})", path);
return root_entry_->ResolvePath(path);
}
DiscImageDevice::Error DiscImageDevice::Verify(ParseState* state) {
// Find sector 32 of the game partition - try at a few known base offsets:
// 0 (rebuilt game-partition-only image), the XSF variants, XGD1/XGD2/XGD3
// full-dump bases.
static const size_t likely_offsets[] = {
0x00000000, 0x0000FB20, 0x00020600, 0x02080000, 0x0FD90000, 0x18300000,
};
bool magic_found = false;
for (size_t offset : likely_offsets) {
state->game_offset = offset;
if (VerifyMagic(state, state->game_offset + (32 * kXESectorSize))) {
magic_found = true;
break;
}
}
if (!magic_found) {
// File doesn't have the magic values - likely not a real GDFX source.
return Error::kErrorFileMismatch;
}
// Read sector 32 to get FS state.
size_t vd_offset = state->game_offset + (32 * kXESectorSize);
if (state->size < vd_offset + kXESectorSize) {
return Error::kErrorReadError;
}
const uint8_t* fs_ptr = state->ptr + vd_offset;
// The volume descriptor carries the magic at both ends of the sector; a
// missing tail magic means a truncated or corrupt descriptor.
if (std::memcmp(fs_ptr + 0x7EC, "MICROSOFT*XBOX*MEDIA", 20) != 0) {
return Error::kErrorDamagedFile;
}
state->root_sector = memory::load<uint32_t>(fs_ptr + 20);
state->root_size = memory::load<uint32_t>(fs_ptr + 24);
state->root_offset = state->game_offset + (state->root_sector * kXESectorSize);
if (state->root_size < kEntryHeaderSize - 1 || state->root_size > 32 * 1024 * 1024) {
return Error::kErrorDamagedFile;
}
if (state->root_offset >= state->size || state->root_size > state->size - state->root_offset) {
return Error::kErrorDamagedFile;
}
return Error::kSuccess;
}
bool DiscImageDevice::VerifyMagic(ParseState* state, size_t offset) {
if (offset + 20 > state->size) {
return false;
}
// Simple check to see if the given offset contains the magic value.
return std::memcmp(state->ptr + offset, "MICROSOFT*XBOX*MEDIA", 20) == 0;
}
DiscImageDevice::Error DiscImageDevice::ReadAllEntries(ParseState* state) {
auto root_entry = new DiscImageEntry(this, nullptr, "", mmap_.get());
root_entry->attributes_ = kFileAttributeDirectory;
root_entry_ = std::unique_ptr<Entry>(root_entry);
if (!ReadDirectory(state, state->root_offset, state->root_size, root_entry, 0)) {
return Error::kErrorDamagedFile;
}
return Error::kSuccess;
}
bool DiscImageDevice::ReadDirectory(ParseState* state, size_t table_offset, size_t table_size,
DiscImageEntry* parent, uint32_t depth) {
if (depth > kMaxDirectoryDepth) {
return false;
}
if (table_offset >= state->size || table_size > state->size - table_offset) {
return false;
}
if (!state->visited_tables.insert(table_offset).second) {
// A directory table referenced twice = a cycle in a damaged image.
return false;
}
const uint8_t* table = state->ptr + table_offset;
// Iterative walk over the entry AVL tree. Ordinals 0 and 0xFFFF are branch
// terminators; a seen-set guards against self-referencing nodes.
std::vector<uint16_t> pending;
std::set<uint16_t> seen;
pending.push_back(0);
while (!pending.empty()) {
uint16_t ordinal = pending.back();
pending.pop_back();
if (!seen.insert(ordinal).second) {
continue;
}
size_t offset = size_t(ordinal) * 4;
if (offset + kEntryHeaderSize > table_size) {
// Padding at the tail of a directory sector; nothing to read here.
continue;
}
const uint8_t* p = table + offset;
uint16_t node_l = memory::load<uint16_t>(p + 0);
uint16_t node_r = memory::load<uint16_t>(p + 2);
size_t sector = memory::load<uint32_t>(p + 4);
size_t length = memory::load<uint32_t>(p + 8);
uint8_t attributes = memory::load<uint8_t>(p + 12);
uint8_t name_length = memory::load<uint8_t>(p + 13);
if (node_l && node_l != kOrdinalTerminator) {
pending.push_back(node_l);
}
if (node_r && node_r != kOrdinalTerminator) {
pending.push_back(node_r);
}
if (!name_length || offset + kEntryHeaderSize + name_length > table_size) {
continue;
}
auto name = std::string(reinterpret_cast<const char*>(p + kEntryHeaderSize), name_length);
auto entry = DiscImageEntry::Create(this, parent, name, mmap_.get());
entry->attributes_ = attributes | kFileAttributeReadOnly;
entry->size_ = length;
entry->allocation_size_ = rex::round_up(length, bytes_per_sector());
// Set to January 1, 1970 (UTC) in 100-nanosecond intervals
entry->create_timestamp_ = 10000 * 11644473600000LL;
entry->access_timestamp_ = 10000 * 11644473600000LL;
entry->write_timestamp_ = 10000 * 11644473600000LL;
if (attributes & kFileAttributeDirectory) {
// Folder.
entry->data_offset_ = 0;
entry->data_size_ = 0;
auto* dir = entry.get();
parent->children_.emplace_back(std::move(entry));
if (length) {
// Not a leaf - read in children.
size_t child_offset = state->game_offset + (sector * kXESectorSize);
if (!ReadDirectory(state, child_offset, length, dir, depth + 1)) {
return false;
}
}
} else {
// File.
entry->data_offset_ = state->game_offset + (sector * kXESectorSize);
entry->data_size_ = length;
if (entry->data_offset_ > state->size || length > state->size - entry->data_offset_) {
// File data extends past the end of the image: truncated download or
// bad dump. Fail the mount so the caller reports it clearly.
return false;
}
parent->children_.emplace_back(std::move(entry));
}
}
return true;
}
} // namespace rex::filesystem
@@ -0,0 +1,51 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/filesystem/devices/disc_image_entry.h>
#include <algorithm>
#include <rex/filesystem/devices/disc_image_file.h>
#include <rex/string.h>
namespace rex::filesystem {
DiscImageEntry::DiscImageEntry(Device* device, Entry* parent, const std::string_view path,
memory::MappedMemory* mmap)
: Entry(device, parent, path), mmap_(mmap), data_offset_(0), data_size_(0) {}
DiscImageEntry::~DiscImageEntry() = default;
std::unique_ptr<DiscImageEntry> DiscImageEntry::Create(Device* device, Entry* parent,
const std::string_view name,
memory::MappedMemory* mmap) {
auto path = rex::string::utf8_join_guest_paths(parent->path(), name);
return std::make_unique<DiscImageEntry>(device, parent, path, mmap);
}
X_STATUS DiscImageEntry::Open(uint32_t desired_access, File** out_file) {
*out_file = new DiscImageFile(desired_access, this);
return X_STATUS_SUCCESS;
}
std::unique_ptr<memory::MappedMemory> DiscImageEntry::OpenMapped(memory::MappedMemory::Mode mode,
size_t offset, size_t length) {
if (mode != memory::MappedMemory::Mode::kRead) {
// Only allow reads.
return nullptr;
}
size_t real_offset = data_offset_ + offset;
size_t real_length = length ? std::min(length, data_size_) : data_size_;
return mmap_->Slice(real_offset, real_length);
}
} // namespace rex::filesystem
@@ -0,0 +1,42 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/filesystem/devices/disc_image_file.h>
#include <algorithm>
#include <cstring>
#include <rex/filesystem/devices/disc_image_entry.h>
namespace rex::filesystem {
DiscImageFile::DiscImageFile(uint32_t file_access, DiscImageEntry* entry)
: File(file_access, entry), entry_(entry) {}
DiscImageFile::~DiscImageFile() = default;
void DiscImageFile::Destroy() {
delete this;
}
X_STATUS DiscImageFile::ReadSync(std::span<uint8_t> buffer, size_t byte_offset,
size_t* out_bytes_read) {
if (byte_offset >= entry_->size()) {
return X_STATUS_END_OF_FILE;
}
size_t real_offset = entry_->data_offset() + byte_offset;
size_t real_length = std::min(buffer.size(), entry_->data_size() - byte_offset);
std::memcpy(buffer.data(), entry_->mmap()->data() + real_offset, real_length);
*out_bytes_read = real_length;
return X_STATUS_SUCCESS;
}
} // namespace rex::filesystem
@@ -104,11 +104,24 @@ Entry* VirtualFileSystem::ResolvePath(const std::string_view path) {
normalized_path = resolved_path;
}
// Find the device.
auto it = std::find_if(devices_.cbegin(), devices_.cend(), [&](const auto& d) {
return rex::string::utf8_starts_with_case(normalized_path, d->mount_path());
});
if (it == devices_.cend()) {
// Find the device. Registration order decides priority; a device marked
// layered() lets a resolution miss fall through to the next device with a
// matching mount path (loose files win, an image mounted below fills the
// gaps). Non-layered devices are terminal, the historical behaviour.
Device* device = nullptr;
Entry* entry = nullptr;
for (const auto& d : devices_) {
if (!rex::string::utf8_starts_with_case(normalized_path, d->mount_path())) {
continue;
}
device = d.get();
auto relative_path = normalized_path.substr(device->mount_path().size());
entry = device->ResolvePath(relative_path);
if (entry || !device->layered()) {
break;
}
}
if (!device) {
REXFS_WARN("VFS: '{}' -> [no device]", path);
// Supress logging the error for ShaderDumpxe:\CompareBackEnds as this is
// not an actual problem nor something we care about.
@@ -118,10 +131,6 @@ Entry* VirtualFileSystem::ResolvePath(const std::string_view path) {
return nullptr;
}
const auto& device = *it;
auto relative_path = normalized_path.substr(device->mount_path().size());
auto* entry = device->ResolvePath(relative_path);
if (entry) {
if (had_symlink) {
REXFS_TRACE("VFS resolved '{}' via symlink '{}' on device '{}' -> '{}'", path,
@@ -221,7 +230,9 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view p
}
// If the cached entry does not exist on host anymore, invalidate it.
if (parent_entry) {
// Only applies when the entry actually lives on the same host device as
// the parent (a layered lower-level entry is not backed by that host dir).
if (parent_entry && entry->device() == parent_entry->device()) {
const auto* host_path_entry = dynamic_cast<const HostPathEntry*>(parent_entry);
if (host_path_entry) {
const auto file_path = host_path_entry->host_path() / rex::to_path(entry->name());
@@ -233,6 +244,15 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view p
}
}
// Layered mounts: the parent directory may resolve on the top layer while
// the requested child exists only in a lower layer. Retry with a full-path
// resolution, which walks the layers. Inert unless the parent's device
// opted into layering.
if (!entry && !root_entry && parent_entry && parent_entry->device() &&
parent_entry->device()->layered()) {
entry = ResolvePath(path);
}
// Check if exists (if we need it to), or that it doesn't (if it shouldn't).
switch (creation_disposition) {
case FileDisposition::kOpen:
+297 -3
View File
@@ -36,13 +36,44 @@
#include <rex/ui/keybinds.h>
#include <rex/version.h>
#include <rex/filesystem/devices/disc_image_device.h>
#include <rex/system/util/xex2_info.h>
#include <fmt/format.h>
#include <imgui.h>
#include <algorithm>
#include <cctype>
#if REX_PLATFORM_WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
#include <filesystem>
REXCVAR_DEFINE_STRING(user_data_root, "", "Runtime", "Override user data path");
REXCVAR_DEFINE_STRING(update_data_root, "", "Runtime", "Override update data path");
REXCVAR_DEFINE_BOOL(iso_direct, true, "Runtime",
"Allow mounting the game directly from a disc image (.iso). The loose "
"assets folder always takes priority when present; set false to ignore "
"disc images entirely.");
REXCVAR_DEFINE_STRING(game_iso, "", "Runtime",
"Path to the game's disc image (.iso). Empty = automatic: use the "
"assets folder if present, else scan for a matching .iso next to the "
"executable.");
REXCVAR_DEFINE_BOOL(dlc_containers, true, "Runtime",
"Mount DLC content containers (STFS/LIVE packages) directly from the dlc "
"folder and the content root, no extraction needed. Containers take "
"priority over extracted package folders of the same name.");
REXCVAR_DEFINE_STRING(dlc_dir, "", "Runtime",
"Override the DLC container folder. Empty = 'dlc' next to the "
"executable.");
REXCVAR_DEFINE_BOOL(use_shader_disk_cache, true, "GPU",
"Pre-compile the game's GPU pipelines from a persistent on-disk cache at "
"launch (during load) and record new ones, so pipelines do not compile "
@@ -52,6 +83,138 @@ REXCVAR_DEFINE_BOOL(use_shader_disk_cache, true, "GPU",
namespace rex {
namespace {
// A fatal setup problem the user must fix (missing/wrong game data). The log
// carries the details; the box exists so a double-clicked exe does not just
// silently vanish.
void ShowFatalMessageBox(const std::string& title, const std::string& message) {
#if REX_PLATFORM_WIN32
auto wtitle = rex::string::to_utf16(title);
auto wmessage = rex::string::to_utf16(message);
MessageBoxW(nullptr, reinterpret_cast<const wchar_t*>(wmessage.c_str()),
reinterpret_cast<const wchar_t*>(wtitle.c_str()), MB_OK | MB_ICONERROR);
#else
(void)title;
(void)message;
#endif
}
// Result of probing a candidate disc image: does it parse as a disc, does it
// contain a readable default.xex, and which title is it for.
struct ImageProbe {
bool readable = false;
uint32_t title_id = 0;
std::string reason;
};
// Reads the title id out of a xex2 image's execution-info optional header.
// All offsets are validated against the buffer; a malformed header simply
// fails the probe instead of crashing.
bool ReadXexTitleId(const std::vector<uint8_t>& buf, uint32_t* out_title_id) {
if (buf.size() < sizeof(xex2_header)) {
return false;
}
const auto* header = reinterpret_cast<const xex2_header*>(buf.data());
if (header->magic != 0x58455832) { // 'XEX2'
return false;
}
const uint32_t header_count = header->header_count;
if (header_count > 1024 || 0x18 + size_t(header_count) * 8 > buf.size()) {
return false;
}
for (uint32_t i = 0; i < header_count; i++) {
const xex2_opt_header& opt = header->headers[i];
if (opt.key != XEX_HEADER_EXECUTION_INFO) {
continue;
}
const uint32_t offset = opt.offset;
if (offset + sizeof(xex2_opt_execution_info) > buf.size()) {
return false;
}
const auto* info = reinterpret_cast<const xex2_opt_execution_info*>(buf.data() + offset);
*out_title_id = info->title_id;
return true;
}
return false;
}
// Mounts a candidate image standalone (no VFS registration) and reads the
// title id from its default.xex.
ImageProbe ProbeGameImage(const std::filesystem::path& path) {
ImageProbe probe;
rex::filesystem::DiscImageDevice device("", path);
if (!device.Initialize()) {
probe.reason = "not a readable Xbox 360 disc image (or a damaged dump)";
return probe;
}
auto* entry = device.ResolvePath("default.xex");
if (!entry) {
probe.reason = "image contains no default.xex";
return probe;
}
rex::filesystem::File* file = nullptr;
if (XFAILED(entry->Open(rex::filesystem::FileAccess::kFileReadData, &file)) || !file) {
probe.reason = "could not open default.xex inside the image";
return probe;
}
std::vector<uint8_t> buf(std::min<size_t>(entry->size(), 64 * 1024));
size_t bytes_read = 0;
auto status = file->ReadSync(std::span<uint8_t>(buf.data(), buf.size()), 0, &bytes_read);
file->Destroy();
if (XFAILED(status) || bytes_read != buf.size()) {
probe.reason = "could not read default.xex inside the image";
return probe;
}
uint32_t title_id = 0;
if (!ReadXexTitleId(buf, &title_id)) {
probe.reason = "default.xex inside the image has no readable title id";
return probe;
}
probe.readable = true;
probe.title_id = title_id;
return probe;
}
// Scans a directory (non-recursive) for *.iso files, probes each in name
// order, and returns the first whose title id matches (any title if
// expected_title_id is 0). Rejects are logged, not fatal.
std::filesystem::path ScanForGameImage(const std::filesystem::path& dir,
uint32_t expected_title_id) {
std::vector<std::filesystem::path> candidates;
std::error_code ec;
for (auto it = std::filesystem::directory_iterator(dir, ec);
!ec && it != std::filesystem::directory_iterator(); it.increment(ec)) {
if (!it->is_regular_file(ec)) {
continue;
}
auto ext = rex::path_to_utf8(it->path().extension());
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return char(std::tolower(c)); });
if (ext == ".iso") {
candidates.push_back(it->path());
}
}
std::sort(candidates.begin(), candidates.end());
for (const auto& candidate : candidates) {
auto probe = ProbeGameImage(candidate);
if (!probe.readable) {
REXLOG_INFO("Game source: ignoring {}: {}", rex::path_to_utf8(candidate.filename()),
probe.reason);
continue;
}
if (expected_title_id && probe.title_id != expected_title_id) {
REXLOG_INFO("Game source: ignoring {}: title id {:08X} (expected {:08X})",
rex::path_to_utf8(candidate.filename()), probe.title_id, expected_title_id);
continue;
}
return candidate;
}
return {};
}
} // namespace
// --- ReXApp ---
ReXApp::~ReXApp() = default;
@@ -62,6 +225,119 @@ ReXApp::ReXApp(ui::WindowedAppContext& ctx, std::string_view name, PPCImageInfo
AddPositionalOption("game_directory");
}
bool ReXApp::ResolveGameSource(const std::filesystem::path& exe_dir,
std::filesystem::path& out_image) {
out_image.clear();
const uint32_t expected_title_id = OnGetExpectedTitleId();
const bool iso_direct_enabled = REXCVAR_GET(iso_direct);
const std::string app_name(GetName());
// path_to_utf8 throughout: path::string() converts through the ANSI
// codepage on Windows, which garbles non-ASCII file names in the log and
// in the (UTF-8-consuming) message box.
auto reject_image = [&](const std::filesystem::path& path, const std::string& reason) {
REXLOG_ERROR("Game source: rejected image {}: {}", rex::path_to_utf8(path), reason);
auto message = fmt::format(
"The disc image was rejected:\n\n{}\n\n{}\n\n"
"Provide your own legally obtained copy of the game.",
rex::path_to_utf8(path), reason);
ShowFatalMessageBox(app_name, message);
return false;
};
auto check_expected_title = [&](const ImageProbe& probe, const std::filesystem::path& path,
std::string& reason_out) {
if (!probe.readable) {
reason_out = probe.reason;
return false;
}
if (expected_title_id && probe.title_id != expected_title_id) {
reason_out = fmt::format("image is for title id {:08X}, this game expects {:08X}",
probe.title_id, expected_title_id);
return false;
}
return true;
};
// The game_directory argument may point straight at a disc image file.
if (!game_data_root_.empty() && std::filesystem::is_regular_file(game_data_root_)) {
auto image_path = game_data_root_;
std::string reason;
if (!iso_direct_enabled) {
return reject_image(image_path, "iso_direct is disabled in the config");
}
if (!check_expected_title(ProbeGameImage(image_path), image_path, reason)) {
return reject_image(image_path, reason);
}
game_data_root_.clear();
out_image = image_path;
REXLOG_INFO("Game source: iso: {}", rex::path_to_utf8(image_path));
return true;
}
const bool have_loose = std::filesystem::is_directory(game_data_root_) &&
std::filesystem::exists(game_data_root_ / "default.xex");
if (iso_direct_enabled) {
const std::string iso_cvar = REXCVAR_GET(game_iso);
if (!iso_cvar.empty()) {
// An explicit config path must be honoured or fail loudly, never
// silently fall back. to_path: the toml string is UTF-8, not the ANSI
// codepage a bare path construction would assume on Windows.
std::filesystem::path image_path = rex::to_path(iso_cvar);
std::string reason;
if (!std::filesystem::is_regular_file(image_path)) {
return reject_image(image_path, "game_iso does not point at a file");
}
if (!check_expected_title(ProbeGameImage(image_path), image_path, reason)) {
return reject_image(image_path, reason);
}
out_image = image_path;
} else {
out_image = ScanForGameImage(exe_dir, expected_title_id);
}
}
if (have_loose) {
// Loose assets are the primary source, exactly as before; an image below
// only fills per-file gaps.
REXLOG_INFO("Game source: assets: {}",
rex::path_to_utf8(std::filesystem::absolute(game_data_root_)));
if (!out_image.empty()) {
REXLOG_INFO("Game source: iso underlay: {} (loose files override the image)",
rex::path_to_utf8(out_image));
}
return true;
}
if (!out_image.empty()) {
REXLOG_INFO("Game source: iso: {}", rex::path_to_utf8(std::filesystem::absolute(out_image)));
std::error_code ec;
if (std::filesystem::is_directory(game_data_root_) &&
!std::filesystem::is_empty(game_data_root_, ec) && !ec) {
// A partial assets folder on top of an image: the modding path.
REXLOG_INFO("Game source: loose overlay: {} (loose files override the image)",
rex::path_to_utf8(std::filesystem::absolute(game_data_root_)));
} else {
game_data_root_.clear();
}
return true;
}
// Neither an assets folder nor a usable image: name both options clearly.
auto assets_hint = game_data_root_.empty() ? (exe_dir / "assets") : game_data_root_;
REXLOG_ERROR("Game source: none found - expected game files at {} or a .iso next to {}",
rex::path_to_utf8(assets_hint), rex::path_to_utf8(exe_dir));
auto message = fmt::format(
"No game data was found.\n\n"
"Provide your own legally obtained copy of the game in one of two ways:\n\n"
"1. Extract the game's files into:\n {}\n\n"
"2. Place the game's disc image (.iso) next to the executable:\n {}\n\n"
"Advanced: set game_iso = \"path/to/image.iso\" in {}.toml.",
rex::path_to_utf8(assets_hint), rex::path_to_utf8(exe_dir), app_name);
ShowFatalMessageBox(app_name, message);
return false;
}
bool ReXApp::OnInitialize() {
auto exe_dir = rex::filesystem::GetExecutableFolder();
@@ -127,16 +403,25 @@ bool ReXApp::OnInitialize() {
}
REXLOG_INFO("{} starting", GetName());
REXLOG_INFO(" Game directory: {}", game_data_root_.string());
// Resolve where the game's data comes from. Runs after LoadConfig so the
// game_iso / iso_direct toml overrides apply.
std::filesystem::path game_image;
if (!ResolveGameSource(exe_dir, game_image)) {
return false;
}
if (!user_data_root_.empty()) {
REXLOG_INFO(" User data: {}", user_data_root_.string());
REXLOG_INFO(" User data: {}", rex::path_to_utf8(user_data_root_));
}
if (!update_data_root_.empty()) {
REXLOG_INFO(" Update data: {}", update_data_root_.string());
REXLOG_INFO(" Update data: {}", rex::path_to_utf8(update_data_root_));
}
// Create runtime
runtime_ = std::make_unique<rex::Runtime>(game_data_root_, user_data_root_, update_data_root_);
if (!game_image.empty()) {
runtime_->set_game_image_path(game_image);
}
runtime_->set_app_context(&app_context());
// Build runtime config with default platform backends
@@ -172,6 +457,15 @@ bool ReXApp::OnInitialize() {
return false;
}
// Discover raw DLC containers now that the title id is known. Logs one
// line per package found; extracted folders keep priority.
if (REXCVAR_GET(dlc_containers)) {
const std::string dlc_dir_cvar = REXCVAR_GET(dlc_dir);
const std::filesystem::path dlc_dir =
dlc_dir_cvar.empty() ? (exe_dir / "dlc") : rex::to_path(dlc_dir_cvar);
runtime_->kernel_state()->content_manager()->DiscoverContainers(dlc_dir);
}
// Initialize rexcrt heap after LoadXexImage to avoid guest memory writes
// corrupting the heap region. rexcrt_heap is set by codegen (REXCRT_HEAP)
// when [rexcrt] contains heap functions -- originals are stripped so init
+47 -17
View File
@@ -13,6 +13,7 @@
#include <native/filesystem/devices/host_path_device.h>
#include <native/filesystem/devices/null_device.h>
#include <native/filesystem/vfs.h>
#include <rex/filesystem/devices/disc_image_device.h>
#include <rex/graphics/graphics_system.h>
#include <rex/input/input_system.h>
#include <rex/logging.h>
@@ -222,29 +223,58 @@ uint8_t* Runtime::virtual_membase() const {
}
bool Runtime::SetupVfs() {
if (game_data_root_.empty()) {
const bool have_image = !game_image_path_.empty();
if (game_data_root_.empty() && !have_image) {
REXSYS_WARN("Runtime::SetupVfs: No game_data_root specified, skipping VFS setup");
return true;
}
auto abs_game_root = std::filesystem::absolute(game_data_root_);
if (!std::filesystem::exists(abs_game_root)) {
REXSYS_ERROR("Runtime::SetupVfs: game_data_root does not exist: {}", abs_game_root.string());
return false;
auto mount_path = "\\Device\\Harddisk0\\Partition1";
// Loose directory first: with an image below it, loose files always win and
// the image only fills the gaps (layered). Without an image this is the
// historical single-device path, bit for bit.
if (!game_data_root_.empty()) {
auto abs_game_root = std::filesystem::absolute(game_data_root_);
if (!std::filesystem::exists(abs_game_root)) {
if (!have_image) {
REXSYS_ERROR("Runtime::SetupVfs: game_data_root does not exist: {}",
rex::path_to_utf8(abs_game_root));
return false;
}
// Image-only launch; the missing loose directory is fine.
} else {
auto device =
std::make_unique<rex::filesystem::HostPathDevice>(mount_path, abs_game_root, true);
if (!device->Initialize()) {
REXSYS_ERROR("Runtime::SetupVfs: Failed to initialize host path device");
return false;
}
device->set_layered(have_image);
if (!file_system_->RegisterDevice(std::move(device))) {
REXSYS_ERROR("Runtime::SetupVfs: Failed to register host path device");
return false;
}
REXSYS_INFO(" Mounted {} at {}{}", rex::path_to_utf8(abs_game_root), mount_path,
have_image ? " (loose layer, overrides image)" : "");
}
}
// Mount game_data_root as \Device\Harddisk0\Partition1
auto mount_path = "\\Device\\Harddisk0\\Partition1";
auto device = std::make_unique<rex::filesystem::HostPathDevice>(mount_path, abs_game_root, true);
if (!device->Initialize()) {
REXSYS_ERROR("Runtime::SetupVfs: Failed to initialize host path device");
return false;
// Disc image below the loose layer (or alone).
if (have_image) {
auto image_device =
std::make_unique<rex::filesystem::DiscImageDevice>(mount_path, game_image_path_);
if (!image_device->Initialize()) {
REXSYS_ERROR("Runtime::SetupVfs: Failed to mount disc image: {}",
rex::path_to_utf8(game_image_path_));
return false;
}
if (!file_system_->RegisterDevice(std::move(image_device))) {
REXSYS_ERROR("Runtime::SetupVfs: Failed to register disc image device");
return false;
}
REXSYS_INFO(" Mounted {} at {}", rex::path_to_utf8(game_image_path_), mount_path);
}
if (!file_system_->RegisterDevice(std::move(device))) {
REXSYS_ERROR("Runtime::SetupVfs: Failed to register host path device");
return false;
}
REXSYS_INFO(" Mounted {} at {}", abs_game_root.string(), mount_path);
// Register symbolic links for game: and D:
file_system_->RegisterSymbolicLink("game:", mount_path);
@@ -260,7 +290,7 @@ bool Runtime::SetupVfs() {
std::make_unique<rex::filesystem::HostPathDevice>(update_mount, abs_update_root, true);
if (update_device->Initialize() && file_system_->RegisterDevice(std::move(update_device))) {
file_system_->RegisterSymbolicLink("update:", update_mount);
REXSYS_INFO(" Mounted {} at update:", abs_update_root.string());
REXSYS_INFO(" Mounted {} at update:", rex::path_to_utf8(abs_update_root));
}
}
}
+248 -14
View File
@@ -9,6 +9,7 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <array>
#include <cstring>
#include <queue>
@@ -20,6 +21,7 @@
#include <rex/filesystem.h>
#include <rex/filesystem/devices/host_path_device.h>
#include <rex/filesystem/devices/stfs_container_device.h>
#include <rex/logging.h>
#include <rex/string.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xam/content_device.h>
@@ -60,10 +62,31 @@ ContentPackage::ContentPackage(KernelState* kernel_state, const std::string_view
content_data_ = data;
auto fs = kernel_state_->file_system();
auto device =
std::make_unique<rex::filesystem::HostPathDevice>(device_path_, package_path, false);
device->Initialize();
fs->RegisterDevice(std::move(device));
std::error_code ec;
if (std::filesystem::is_regular_file(package_path, ec)) {
// A raw STFS/LIVE container: mount it directly, no extraction step.
is_container_ = true;
auto device =
std::make_unique<rex::filesystem::StfsContainerDevice>(device_path_, package_path);
if (device->Initialize()) {
for (size_t i = 0; i < 0x10; i++) {
if (device->header().header.licenses[i].license_flags) {
container_license_ |= device->header().header.licenses[i].license_bits;
}
}
fs->RegisterDevice(std::move(device));
device_mounted_ = true;
} else {
REXSYS_ERROR("ContentPackage: failed to mount container {}", rex::path_to_utf8(package_path));
}
} else {
// An extracted folder: the historical path, unchanged.
auto device =
std::make_unique<rex::filesystem::HostPathDevice>(device_path_, package_path, false);
device->Initialize();
fs->RegisterDevice(std::move(device));
device_mounted_ = true;
}
fs->RegisterSymbolicLink(root_name_ + ":", device_path_);
}
@@ -165,7 +188,7 @@ std::vector<XCONTENT_AGGREGATE_DATA> ContentManager::ListContent(uint32_t device
auto file_infos = rex::filesystem::ListFiles(package_root);
for (const auto& file_info : file_infos) {
if (file_info.type != rex::filesystem::FileInfo::Type::kDirectory) {
// Directories only.
// Directories only; raw container files are handled below.
continue;
}
@@ -184,22 +207,78 @@ std::vector<XCONTENT_AGGREGATE_DATA> ContentManager::ListContent(uint32_t device
}
}
// Discovered raw containers take priority over an ambient extracted install
// for the same package name - the user placed them next to the exe, so they
// win, same rule as the assets folder (and Windows' local-DLL search order).
// A container replaces its extracted twin in the listing; unique names
// simply append.
if (!containers_.empty() && title_id == kernel_state_->title_id()) {
for (const auto& [key, container] : containers_) {
if (container.content_type != content_type) {
continue;
}
XCONTENT_AGGREGATE_DATA content_data;
content_data.device_id = device_id;
content_data.content_type = content_type;
content_data.set_display_name(container.display_name.empty()
? rex::path_to_utf16(container.path.filename())
: container.display_name);
content_data.set_file_name(key.view());
content_data.title_id = title_id;
content_data.xuid = xuid;
auto existing = std::find_if(result.begin(), result.end(), [&](const auto& data) {
return rex::string::utf8_equal_case(data.file_name(), key.view());
});
if (existing != result.end()) {
*existing = content_data;
} else {
result.emplace_back(std::move(content_data));
}
}
}
return result;
}
const DiscoveredContainer* ContentManager::FindContainer(const std::string_view file_name,
XContentType content_type) const {
auto it = containers_.find(string::string_key_case(file_name));
if (it == containers_.end() || it->second.content_type != content_type) {
return nullptr;
}
return &it->second;
}
std::filesystem::path ContentManager::ResolvePackageDataPath(uint64_t xuid,
const XCONTENT_AGGREGATE_DATA& data) {
// A container the user placed (the dlc folder, or dropped into the content
// root) takes priority over an ambient extracted install - same rule as the
// assets folder and Windows' local-DLL search order.
if (const auto* container = FindContainer(data.file_name(), data.content_type)) {
return container->path;
}
// Else the canonical package path - an extracted folder, or a container
// file sitting at the package's canonical place in the content root.
auto package_path = ResolvePackagePath(xuid, data);
std::error_code ec;
if (std::filesystem::exists(package_path, ec)) {
return package_path;
}
return {};
}
std::unique_ptr<ContentPackage> ContentManager::ResolvePackage(
const std::string_view root_name, uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data) {
auto package_path = ResolvePackagePath(xuid, data);
if (!std::filesystem::exists(package_path)) {
auto data_path = ResolvePackageDataPath(xuid, data);
if (data_path.empty()) {
return nullptr;
}
auto package = std::make_unique<ContentPackage>(kernel_state_, root_name, data, package_path);
auto package = std::make_unique<ContentPackage>(kernel_state_, root_name, data, data_path);
return package;
}
bool ContentManager::ContentExists(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data) {
auto path = ResolvePackagePath(xuid, data);
return std::filesystem::exists(path);
return !ResolvePackageDataPath(xuid, data).empty();
}
X_RESULT ContentManager::WriteContentHeaderFile(uint64_t xuid, XCONTENT_AGGREGATE_DATA data,
@@ -308,12 +387,15 @@ X_RESULT ContentManager::OpenContent(const std::string_view root_name, uint64_t
}
}
auto package_path = ResolvePackagePath(xuid, data);
if (!std::filesystem::exists(package_path)) {
auto package = ResolvePackage(root_name, xuid, data);
if (!package) {
return X_ERROR_FILE_NOT_FOUND;
}
if (!package->device_mounted()) {
// A corrupt/truncated container must read as "content missing", not as an
// empty package that succeeds and then mysteriously has no files.
return X_ERROR_FILE_NOT_FOUND;
}
auto package = ResolvePackage(root_name, xuid, data);
assert_not_null(package);
package->LoadPackageLicenseMask(ResolvePackageHeaderPath(
data.file_name(), xuid, kernel_state_->title_id(), data.content_type));
content_license = package->GetPackageLicense();
@@ -397,6 +479,18 @@ X_RESULT ContentManager::DeleteContent(uint64_t xuid, const XCONTENT_AGGREGATE_D
return X_ERROR_ACCESS_DENIED;
}
// Never delete a directly-mounted container: it is the user's only copy of
// that package, not something this port installed.
{
std::error_code ec;
auto data_path = ResolvePackageDataPath(xuid, data);
if (!data_path.empty() && std::filesystem::is_regular_file(data_path, ec)) {
REXSYS_WARN("ContentManager: refusing to delete container-backed content {}",
rex::path_to_utf8(data_path));
return X_ERROR_ACCESS_DENIED;
}
}
auto package_path = ResolvePackagePath(xuid, data);
std::error_code ec;
auto dir_removed = std::filesystem::remove_all(package_path, ec);
@@ -443,6 +537,17 @@ X_RESULT ContentManager::UnmountAndDeleteContent(uint64_t xuid,
}
delete package;
// Never delete a directly-mounted container (the user's only copy).
{
std::error_code ec;
auto data_path = ResolvePackageDataPath(xuid, data);
if (!data_path.empty() && std::filesystem::is_regular_file(data_path, ec)) {
REXSYS_WARN("ContentManager: refusing to delete container-backed content {}",
rex::path_to_utf8(data_path));
return X_ERROR_ACCESS_DENIED;
}
}
// Delete phase: remove package directory and .header file
auto package_path = ResolvePackagePath(xuid, data);
@@ -668,6 +773,135 @@ X_RESULT ContentManager::InstallContent(const std::filesystem::path& package_pat
return WriteContentHeaderFile(0, content_data, license_mask);
}
// Xbox 360 title update content type. Deliberately not mounted: this port
// statically recompiles the base executable at build time, so a TU's
// executable delta patch cannot apply and is not needed. Content sets in the
// wild usually ship one; that is normal and harmless.
static constexpr uint32_t kTitleUpdateContentType = 0x000B0000;
void ContentManager::DiscoverContainersInDir(const std::filesystem::path& dir, const char* source,
bool recursive, uint32_t title_id) {
std::error_code ec;
if (dir.empty() || !std::filesystem::is_directory(dir, ec) || ec) {
return;
}
auto handle_file = [&](const std::filesystem::path& path) {
auto header = rex::filesystem::StfsContainerDevice::ReadPackageHeader(path);
if (!header) {
// Not an STFS/LIVE/PIRS container (thumbnails, .header sidecars, ...).
REXSYS_DEBUG("ContentManager: ignoring non-container file {}", rex::path_to_utf8(path));
return;
}
const uint32_t container_title = header->metadata.execution_info.title_id;
if (container_title != title_id) {
REXSYS_DEBUG("ContentManager: ignoring container for another title ({:08X}): {}",
container_title, rex::path_to_utf8(path));
return;
}
const XContentType content_type = header->metadata.content_type;
if (uint32_t(content_type) == kTitleUpdateContentType) {
REXSYS_DEBUG("ContentManager: title update package present (not used by this port): {}",
rex::path_to_utf8(path.filename()));
return;
}
if (content_type != XContentType::kMarketplaceContent) {
REXSYS_DEBUG("ContentManager: ignoring container of content type {:08X}: {}",
uint32_t(content_type), rex::path_to_utf8(path));
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;
}
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 (recursive) {
for (auto it = std::filesystem::recursive_directory_iterator(
dir, std::filesystem::directory_options::skip_permission_denied, ec);
!ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) {
if (it->is_regular_file(ec)) {
handle_file(it->path());
}
}
} else {
for (auto it = std::filesystem::directory_iterator(
dir, std::filesystem::directory_options::skip_permission_denied, ec);
!ec && it != std::filesystem::directory_iterator(); it.increment(ec)) {
if (it->is_regular_file(ec)) {
handle_file(it->path());
}
}
}
}
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);
// Containers dropped straight into the content root's marketplace folder.
DiscoverContainersInDir(ResolvePackageRoot(0, XContentType::kMarketplaceContent, title_id),
"content root", 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
// a same-named container is the overridden one.
size_t folder_count = 0;
auto package_root = ResolvePackageRoot(0, XContentType::kMarketplaceContent, title_id);
auto file_infos = rex::filesystem::ListFiles(package_root);
for (const auto& file_info : file_infos) {
if (file_info.type != rex::filesystem::FileInfo::Type::kDirectory) {
continue;
}
auto name = rex::path_to_utf8(file_info.name);
std::string display;
XCONTENT_AGGREGATE_DATA content_data;
if (XSUCCEEDED(ReadContentHeaderFile(name, 0, title_id, XContentType::kMarketplaceContent,
content_data))) {
display = rex::string::to_utf8(content_data.display_name());
}
const bool overridden =
FindContainer(name, XContentType::kMarketplaceContent) != nullptr;
REXSYS_INFO("DLC: \"{}\" [{}] - extracted folder{}", display.empty() ? name : display, name,
overridden ? " (overridden by a container)" : "");
folder_count++;
}
std::vector<const decltype(containers_)::value_type*> sorted_containers;
sorted_containers.reserve(containers_.size());
for (const auto& entry : containers_) {
sorted_containers.push_back(&entry);
}
std::sort(sorted_containers.begin(), sorted_containers.end(),
[](const auto* a, const auto* b) { return a->first.view() < b->first.view(); });
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));
}
REXSYS_INFO("DLC: {} extracted folder(s), {} container(s) mounted with priority", folder_count,
containers_.size());
}
} // namespace xam
} // namespace system
} // namespace rex