[Emulator] Zar creation fixes

Make sure to respect user selected zar name and properly extract STFS
This commit is contained in:
Herman S.
2026-02-09 12:18:56 +09:00
parent 65b481ed29
commit 8ca0062f13
4 changed files with 257 additions and 91 deletions
+51 -34
View File
@@ -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<uint8_t> icon_data;
};
std::vector<SourceInfo> 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<std::vector<Emulator::ZarchiveEntry>>();
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
+178 -55
View File
@@ -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<vfs::Device> 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<void(vfs::Entry*)> 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<X_STATUS(vfs::Entry*)> 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<uint8_t>(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);
}
}
}
+3
View File
@@ -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;
+25 -2
View File
@@ -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<uint32_t>(handle));
auto root_entry = new DiscZarchiveEntry(this, nullptr, root_path);
root_entry->attributes_ = kFileAttributeDirectory;
root_entry->handle_ = static_cast<uint32_t>(handle);
@@ -43,7 +45,12 @@ bool DiscZarchiveDevice::Initialize() {
root_entry->absolute_path_ = root_path;
root_entry_ = std::unique_ptr<Entry>(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<uint32_t>(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<uint32_t>(fileHandle), dirEntry.size);
if (fileHandle == ZARCHIVE_INVALID_NODE) {
XELOGE("DiscZarchiveDevice::ReadAllEntries: LookUp failed for '{}'",
full_path);
return false;
}