From 8ca0062f1301b8569aa944dcc2f8deedf0d1c277 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:18:56 +0900 Subject: [PATCH] [Emulator] Zar creation fixes Make sure to respect user selected zar name and properly extract STFS --- src/xenia/app/emulator_window.cc | 85 ++++--- src/xenia/emulator.cc | 233 +++++++++++++----- src/xenia/emulator.h | 3 + src/xenia/vfs/devices/disc_zarchive_device.cc | 27 +- 4 files changed, 257 insertions(+), 91 deletions(-) diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 3320783d5..dae171c6c 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1521,11 +1521,53 @@ void EmulatorWindow::CreateZarchive() { return; } + // Scan for STFS content to get game name/icon before showing save dialog. + struct SourceInfo { + std::filesystem::path stfs_path; + std::string title_name; + std::vector icon_data; + }; + std::vector source_infos(content_dirs.size()); + + for (size_t i = 0; i < content_dirs.size(); i++) { + auto abs_dir = std::filesystem::absolute(content_dirs[i]); + std::error_code ec; + // Top-level only to avoid false-positives on embedded STFS (DLC, etc.) + for (auto const& dirEntry : + std::filesystem::directory_iterator(abs_dir, ec)) { + if (!source_infos[i].icon_data.empty()) break; + if (dirEntry.is_regular_file()) { + const auto header = + vfs::XContentContainerDevice::ReadContainerHeader(dirEntry.path()); + if (header && header->content_header.is_magic_valid()) { + source_infos[i].stfs_path = dirEntry.path(); + + source_infos[i].title_name = xe::to_utf8( + header->content_metadata.display_name(XLanguage::kEnglish)); + + if (header->content_metadata.title_thumbnail_size > 0 && + header->content_metadata.title_thumbnail_size <= + vfs::XContentMetadata::kThumbLengthV1) { + source_infos[i].icon_data.assign( + header->content_metadata.title_thumbnail, + header->content_metadata.title_thumbnail + + header->content_metadata.title_thumbnail_size); + } + } + } + } + } + + std::string default_name = content_dirs.front().stem().string(); + if (content_dirs.size() == 1 && !source_infos[0].title_name.empty()) { + default_name = source_infos[0].title_name; + } + if (content_dirs.size() == 1) { file_picker->set_mode(ui::FilePicker::Mode::kSave); file_picker->set_type(ui::FilePicker::Type::kFile); file_picker->set_multi_selection(false); - file_picker->set_file_name(content_dirs.front().stem().string()); + file_picker->set_file_name(default_name); file_picker->set_default_extension("zar"); file_picker->set_title("Zarchive File"); file_picker->set_extensions({ @@ -1546,13 +1588,16 @@ void EmulatorWindow::CreateZarchive() { auto zarchive_entries = std::make_shared>(); - for (auto& content_path : content_dirs) { - auto abs_content_dir = std::filesystem::absolute(content_path); + for (size_t i = 0; i < content_dirs.size(); i++) { + auto abs_content_dir = std::filesystem::absolute(content_dirs[i]); std::filesystem::path abs_zarchive_file; if (content_dirs.size() > 1) { + std::string stem = !source_infos[i].title_name.empty() + ? source_infos[i].title_name + : abs_content_dir.stem().string(); abs_zarchive_file = std::filesystem::absolute( - (zarchive_dir / abs_content_dir.stem()).replace_extension("zar")); + (zarchive_dir / stem).replace_extension("zar")); } else { abs_zarchive_file = std::filesystem::absolute(zarchive_dir); } @@ -1561,36 +1606,8 @@ void EmulatorWindow::CreateZarchive() { Emulator::ZarchiveOperation::Create); auto& entry = zarchive_entries->back(); entry.name_ = xe::path_to_utf8(abs_zarchive_file.filename()); - - // Extract icon/title from STFS content, use title for output filename - std::error_code ec; - for (auto const& dirEntry : - std::filesystem::recursive_directory_iterator(abs_content_dir, ec)) { - if (!entry.icon_data_.empty()) break; - if (dirEntry.is_regular_file()) { - const auto header = - vfs::XContentContainerDevice::ReadContainerHeader(dirEntry.path()); - if (header && header->content_header.is_magic_valid()) { - auto title_name = xe::to_utf8( - header->content_metadata.display_name(XLanguage::kEnglish)); - if (!title_name.empty()) { - auto new_zarchive_file = - abs_zarchive_file.parent_path() / (title_name + ".zar"); - entry.data_installation_path_ = new_zarchive_file; - entry.name_ = title_name + ".zar"; - } - - if (header->content_metadata.title_thumbnail_size > 0 && - header->content_metadata.title_thumbnail_size <= - vfs::XContentMetadata::kThumbLengthV1) { - entry.icon_data_.assign( - header->content_metadata.title_thumbnail, - header->content_metadata.title_thumbnail + - header->content_metadata.title_thumbnail_size); - } - } - } - } + entry.stfs_path_ = source_infos[i].stfs_path; + entry.icon_data_ = std::move(source_infos[i].icon_data); } // Show dialog first, then start creation diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index f7fc8abb6..f019c4bc5 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -693,7 +693,9 @@ X_STATUS Emulator::LaunchDiscImage(const std::filesystem::path& path) { X_STATUS Emulator::LaunchDiscArchive(const std::filesystem::path& path) { std::string module_path = FindLaunchModule(); + XELOGI("LaunchDiscArchive: FindLaunchModule returned '{}'", module_path); X_STATUS result = CompleteLaunch(path, module_path); + XELOGI("LaunchDiscArchive: CompleteLaunch returned {:08X}", result); if (result == X_STATUS_NOT_FOUND && !cvars::launch_module.empty()) { return LaunchDefaultModule(path); @@ -1164,12 +1166,46 @@ X_STATUS Emulator::CreateZarchivePackage(ZarchiveEntry& entry) { return X_ERROR_CANCELLED; } + // Mount STFS content via VFS to pack the real game files. + std::unique_ptr stfs_device; + if (!entry.stfs_path_.empty()) { + stfs_device = + vfs::XContentContainerDevice::CreateContentDevice("", entry.stfs_path_); + if (!stfs_device || !stfs_device->Initialize()) { + XELOGE("CreateZarchivePackage: Failed to mount STFS content at '{}'", + xe::path_to_utf8(entry.stfs_path_)); + entry.installation_result_ = X_STATUS_UNSUCCESSFUL; + entry.installation_error_message_ = "Failed to mount STFS content"; + entry.installation_state_ = InstallState::failed; + return X_STATUS_UNSUCCESSFUL; + } + XELOGI("CreateZarchivePackage: Mounted STFS content from '{}'", + xe::path_to_utf8(entry.stfs_path_)); + } + std::error_code ec; entry.content_size_ = 0; - for (auto const& dirEntry : - std::filesystem::recursive_directory_iterator(inputDirectory, ec)) { - if (dirEntry.is_regular_file() && dirEntry.path() != outputFile) { - entry.content_size_ += std::filesystem::file_size(dirEntry.path(), ec); + + if (stfs_device) { + auto* root = stfs_device->ResolvePath("/"); + if (root) { + std::function calc_size = [&](vfs::Entry* e) { + if (e->attributes() & vfs::kFileAttributeDirectory) { + for (auto& child : e->children()) { + calc_size(child.get()); + } + } else { + entry.content_size_ += e->size(); + } + }; + calc_size(root); + } + } else { + for (auto const& dirEntry : + std::filesystem::recursive_directory_iterator(inputDirectory, ec)) { + if (dirEntry.is_regular_file() && dirEntry.path() != outputFile) { + entry.content_size_ += std::filesystem::file_size(dirEntry.path(), ec); + } } } @@ -1216,72 +1252,159 @@ X_STATUS Emulator::CreateZarchivePackage(ZarchiveEntry& entry) { return status; }; - for (auto const& dirEntry : - std::filesystem::recursive_directory_iterator(inputDirectory)) { - if (entry.cancelled_.load()) { - return cleanup_and_fail("Cancelled", X_ERROR_CANCELLED); - } - - std::filesystem::path pathEntry = - std::filesystem::relative(dirEntry.path(), inputDirectory, ec); - - if (ec) { - XELOGI("Failed to get relative path {}\n", pathEntry.string()); - return cleanup_and_fail("Failed to get relative path", + if (stfs_device) { + // Pack from mounted STFS VFS device + auto* root = stfs_device->ResolvePath("/"); + if (!root) { + return cleanup_and_fail("Failed to resolve STFS root", X_STATUS_UNSUCCESSFUL); } - if (dirEntry.is_directory()) { - if (!zWriter.MakeDir(pathEntry.generic_string().c_str(), false)) { - XELOGI("Failed to create directory {}\n", pathEntry.string()); - return cleanup_and_fail("Failed to create directory in archive", - X_STATUS_UNSUCCESSFUL); - } - } else if (dirEntry.is_regular_file()) { - // Don't pack itself to prevent infinite packing. - if (dirEntry == outputFile) { - continue; + std::function pack_entry = + [&](vfs::Entry* e) -> X_STATUS { + if (entry.cancelled_.load()) { + return X_ERROR_CANCELLED; } - XELOGI("Adding file: {}\n", pathEntry.string()); - - if (!zWriter.StartNewFile(pathEntry.generic_string().c_str())) { - XELOGI("Failed to create archive file {}\n", pathEntry.string()); - return cleanup_and_fail("Failed to create file in archive", - X_STATUS_UNSUCCESSFUL); + // Use forward slashes for zarchive paths, skip leading separator + std::string entry_path = utf8::fix_path_separators(e->path(), '/'); + if (!entry_path.empty() && entry_path[0] == '/') { + entry_path = entry_path.substr(1); } - std::filesystem::path file_to_pack_path = inputDirectory / pathEntry; - FILE* file = xe::filesystem::OpenFile(file_to_pack_path, "rb"); + if (e->attributes() & vfs::kFileAttributeDirectory) { + if (!entry_path.empty()) { + if (!zWriter.MakeDir(entry_path.c_str(), false)) { + XELOGI("Failed to create directory {}", entry_path); + return X_STATUS_UNSUCCESSFUL; + } + } + for (auto& child : e->children()) { + X_STATUS result = pack_entry(child.get()); + if (result != X_STATUS_SUCCESS) { + return result; + } + } + } else { + XELOGI("Adding file: {}", entry_path); - if (!file) { - XELOGI("Failed to open input file {}\n", pathEntry.string()); - return cleanup_and_fail("Failed to open input file", - X_STATUS_UNSUCCESSFUL); - } - - const uint64_t file_size = std::filesystem::file_size(file_to_pack_path); - uint64_t total_bytes_read = 0; - - while (total_bytes_read < file_size) { - if (entry.cancelled_.load()) { - fclose(file); - return cleanup_and_fail("Cancelled", X_ERROR_CANCELLED); + if (!zWriter.StartNewFile(entry_path.c_str())) { + XELOGI("Failed to create archive file {}", entry_path); + return X_STATUS_UNSUCCESSFUL; } - uint64_t bytes_read = fread(buffer.data(), 1, buffer.size(), file); + vfs::File* vfs_file = nullptr; + X_STATUS result = e->Open(vfs::FileAccess::kFileReadData, &vfs_file); + if (result != X_STATUS_SUCCESS || !vfs_file) { + XELOGI("Failed to open VFS file {}", entry_path); + return X_STATUS_UNSUCCESSFUL; + } - total_bytes_read += bytes_read; - entry.currently_installed_size_ += bytes_read; + size_t remaining = e->size(); + size_t offset = 0; + while (remaining > 0) { + if (entry.cancelled_.load()) { + vfs_file->Destroy(); + return X_ERROR_CANCELLED; + } - zWriter.AppendData(buffer.data(), bytes_read); + size_t bytes_read = 0; + vfs_file->ReadSync(std::span(buffer.data(), buffer.size()), + offset, &bytes_read); + if (bytes_read == 0) break; + + zWriter.AppendData(buffer.data(), bytes_read); + offset += bytes_read; + remaining -= bytes_read; + entry.currently_installed_size_ += bytes_read; + } + vfs_file->Destroy(); } - fclose(file); - } + if (packContext.hasError) { + return X_STATUS_UNSUCCESSFUL; + } + return X_STATUS_SUCCESS; + }; - if (packContext.hasError) { - return cleanup_and_fail("Write error", X_STATUS_UNSUCCESSFUL); + X_STATUS result = pack_entry(root); + if (result == X_ERROR_CANCELLED) { + return cleanup_and_fail("Cancelled", X_ERROR_CANCELLED); + } + if (result != X_STATUS_SUCCESS) { + return cleanup_and_fail("Failed to pack STFS content", + X_STATUS_UNSUCCESSFUL); + } + } else { + // Pack from raw filesystem directory + for (auto const& dirEntry : + std::filesystem::recursive_directory_iterator(inputDirectory)) { + if (entry.cancelled_.load()) { + return cleanup_and_fail("Cancelled", X_ERROR_CANCELLED); + } + + std::filesystem::path pathEntry = + std::filesystem::relative(dirEntry.path(), inputDirectory, ec); + + if (ec) { + XELOGI("Failed to get relative path {}\n", pathEntry.string()); + return cleanup_and_fail("Failed to get relative path", + X_STATUS_UNSUCCESSFUL); + } + + if (dirEntry.is_directory()) { + if (!zWriter.MakeDir(pathEntry.generic_string().c_str(), false)) { + XELOGI("Failed to create directory {}\n", pathEntry.string()); + return cleanup_and_fail("Failed to create directory in archive", + X_STATUS_UNSUCCESSFUL); + } + } else if (dirEntry.is_regular_file()) { + // Don't pack itself to prevent infinite packing. + if (dirEntry == outputFile) { + continue; + } + + XELOGI("Adding file: {}\n", pathEntry.string()); + + if (!zWriter.StartNewFile(pathEntry.generic_string().c_str())) { + XELOGI("Failed to create archive file {}\n", pathEntry.string()); + return cleanup_and_fail("Failed to create file in archive", + X_STATUS_UNSUCCESSFUL); + } + + std::filesystem::path file_to_pack_path = inputDirectory / pathEntry; + FILE* file = xe::filesystem::OpenFile(file_to_pack_path, "rb"); + + if (!file) { + XELOGI("Failed to open input file {}\n", pathEntry.string()); + return cleanup_and_fail("Failed to open input file", + X_STATUS_UNSUCCESSFUL); + } + + const uint64_t file_size = + std::filesystem::file_size(file_to_pack_path); + uint64_t total_bytes_read = 0; + + while (total_bytes_read < file_size) { + if (entry.cancelled_.load()) { + fclose(file); + return cleanup_and_fail("Cancelled", X_ERROR_CANCELLED); + } + + uint64_t bytes_read = fread(buffer.data(), 1, buffer.size(), file); + + total_bytes_read += bytes_read; + entry.currently_installed_size_ += bytes_read; + + zWriter.AppendData(buffer.data(), bytes_read); + } + + fclose(file); + } + + if (packContext.hasError) { + return cleanup_and_fail("Write error", X_STATUS_UNSUCCESSFUL); + } } } diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index d3e74eaf1..5a5201c26 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -313,6 +313,7 @@ class Emulator { : name_(std::move(other.name_)), path_(std::move(other.path_)), data_installation_path_(std::move(other.data_installation_path_)), + stfs_path_(std::move(other.stfs_path_)), operation_(other.operation_), content_size_(other.content_size_), currently_installed_size_(other.currently_installed_size_), @@ -328,6 +329,7 @@ class Emulator { name_ = std::move(other.name_); path_ = std::move(other.path_); data_installation_path_ = std::move(other.data_installation_path_); + stfs_path_ = std::move(other.stfs_path_); operation_ = other.operation_; content_size_ = other.content_size_; currently_installed_size_ = other.currently_installed_size_; @@ -347,6 +349,7 @@ class Emulator { std::string name_{}; std::filesystem::path path_; std::filesystem::path data_installation_path_; + std::filesystem::path stfs_path_; // Set when source contains STFS content ZarchiveOperation operation_; uint64_t content_size_ = 0; diff --git a/src/xenia/vfs/devices/disc_zarchive_device.cc b/src/xenia/vfs/devices/disc_zarchive_device.cc index 31cf263f9..dd847f48e 100644 --- a/src/xenia/vfs/devices/disc_zarchive_device.cc +++ b/src/xenia/vfs/devices/disc_zarchive_device.cc @@ -36,6 +36,8 @@ bool DiscZarchiveDevice::Initialize() { constexpr std::string_view root_path = "/"; const ZArchiveNodeHandle handle = reader_->LookUp(root_path); + XELOGI("DiscZarchiveDevice::Initialize: root LookUp('{}') -> handle={}", + root_path, static_cast(handle)); auto root_entry = new DiscZarchiveEntry(this, nullptr, root_path); root_entry->attributes_ = kFileAttributeDirectory; root_entry->handle_ = static_cast(handle); @@ -43,7 +45,12 @@ bool DiscZarchiveDevice::Initialize() { root_entry->absolute_path_ = root_path; root_entry_ = std::unique_ptr(root_entry); - return ReadAllEntries("", root_entry, nullptr); + bool result = ReadAllEntries("", root_entry, nullptr); + XELOGI( + "DiscZarchiveDevice::Initialize: ReadAllEntries returned {}, " + "root has {} children", + result, root_entry->children().size()); + return result; } void DiscZarchiveDevice::Dump(StringBuffer* string_buffer) { @@ -56,17 +63,25 @@ Entry* DiscZarchiveDevice::ResolvePath(const std::string_view path) { // be in the form: // some\PATH.foo XELOGFS("DiscZarchiveDevice::ResolvePath({})", path); + XELOGI("DiscZarchiveDevice::ResolvePath: path='{}'", path); if (!reader_) { + XELOGI("DiscZarchiveDevice::ResolvePath: reader_ is null"); return nullptr; } const ZArchiveNodeHandle handle = reader_->LookUp(path); + XELOGI( + "DiscZarchiveDevice::ResolvePath: LookUp('{}') -> handle={} (invalid={})", + path, static_cast(handle), handle == ZARCHIVE_INVALID_NODE); if (handle == ZARCHIVE_INVALID_NODE) { return nullptr; } - return root_entry_->ResolvePath(path); + Entry* result = root_entry_->ResolvePath(path); + XELOGI("DiscZarchiveDevice::ResolvePath: entry tree walk -> {}", + result ? result->absolute_path() : "(null)"); + return result; } bool DiscZarchiveDevice::ReadAllEntries(const std::string& path, @@ -99,7 +114,15 @@ bool DiscZarchiveDevice::ReadAllEntries(const std::string& path, const std::string full_path = path + std::string(dirEntry.name); const ZArchiveNodeHandle fileHandle = reader_->LookUp(full_path); + XELOGI( + "DiscZarchiveDevice::ReadAllEntries: entry='{}' type={} " + "handle={} size={}", + full_path, + dirEntry.isDirectory ? "dir" : (dirEntry.isFile ? "file" : "unknown"), + static_cast(fileHandle), dirEntry.size); if (fileHandle == ZARCHIVE_INVALID_NODE) { + XELOGE("DiscZarchiveDevice::ReadAllEntries: LookUp failed for '{}'", + full_path); return false; }