diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 10745db81..d130bc286 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -507,6 +507,9 @@ bool EmulatorWindow::Initialize() { file_menu->AddChild( MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O", std::bind(&EmulatorWindow::FileOpen, this))); + file_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Install Content...", + std::bind(&EmulatorWindow::InstallContent, this))); #ifdef DEBUG file_menu->AddChild( MenuItem::Create(MenuItem::Type::kString, "Close", @@ -860,6 +863,35 @@ void EmulatorWindow::FileClose() { } } +void EmulatorWindow::InstallContent() { + std::filesystem::path path; + + auto file_picker = xe::ui::FilePicker::Create(); + file_picker->set_mode(ui::FilePicker::Mode::kOpen); + file_picker->set_type(ui::FilePicker::Type::kFile); + file_picker->set_multi_selection(false); + file_picker->set_title("Select Content Package"); + file_picker->set_extensions({ + {"All Files (*.*)", "*.*"}, + }); + if (file_picker->Show(window_.get())) { + auto selected_files = file_picker->selected_files(); + if (!selected_files.empty()) { + path = selected_files[0]; + } + } + + if (!path.empty()) { + // Normalize the path and make absolute. + auto abs_path = std::filesystem::absolute(path); + auto result = emulator_->InstallContentPackage(abs_path); + if (XFAILED(result)) { + // TODO: Display a message box. + XELOGE("Failed to install content: {:08X}", result); + } + } +} + void EmulatorWindow::ShowContentDirectory() { std::filesystem::path target_path; diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 9f529e745..00e197475 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -130,6 +130,7 @@ class EmulatorWindow { void FileDrop(const std::filesystem::path& filename); void FileOpen(); void FileClose(); + void InstallContent(); void ShowContentDirectory(); void CpuTimeScalarReset(); void CpuTimeScalarSetHalf(); diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index e970a626a..da9e059d0 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -353,6 +353,93 @@ X_STATUS Emulator::LaunchStfsContainer(const std::filesystem::path& path) { return CompleteLaunch(path, module_path); } +X_STATUS Emulator::InstallContentPackage(const std::filesystem::path& path) { + std::unique_ptr device = + std::make_unique("", path); + if (!device->Initialize()) { + XELOGE("Failed to initialize device"); + return X_STATUS_INVALID_PARAMETER; + } + + std::filesystem::path installation_path = + content_root() / fmt::format("{:08X}", device->title_id()) / + fmt::format("{:08X}", device->content_type()); + + if (std::filesystem::exists(installation_path / path.filename())) { + // TODO: Popup + // Do you want to overwrite already existing data? + } else { + std::filesystem::create_directories(installation_path / path.filename()); + } + + // Run through all the files, breadth-first style. + std::queue queue; + auto root = device->ResolvePath("/"); + queue.push(root); + + // Allocate a buffer when needed. + size_t buffer_size = 0; + uint8_t* buffer = nullptr; + + while (!queue.empty()) { + auto entry = queue.front(); + queue.pop(); + for (auto& entry : entry->children()) { + queue.push(entry.get()); + } + + auto dest_name = + installation_path / path.filename() / xe::to_path(entry->path()); + if (entry->attributes() & vfs::kFileAttributeDirectory) { + std::filesystem::create_directories(dest_name); + continue; + } + + vfs::File* in_file = nullptr; + if (entry->Open(vfs::FileAccess::kFileReadData, &in_file) != + X_STATUS_SUCCESS) { + continue; + } + + auto file = xe::filesystem::OpenFile(dest_name, "wb"); + if (!file) { + in_file->Destroy(); + continue; + } + + if (entry->can_map()) { + auto map = entry->OpenMapped(xe::MappedMemory::Mode::kRead); + fwrite(map->data(), map->size(), 1, file); + map->Close(); + } else { + // Can't map the file into memory. Read it into a temporary buffer. + if (!buffer || entry->size() > buffer_size) { + // Resize the buffer. + if (buffer) { + delete[] buffer; + } + + // Allocate a buffer rounded up to the nearest 512MB. + buffer_size = xe::round_up(entry->size(), 512_MiB); + buffer = new uint8_t[buffer_size]; + } + + size_t bytes_read = 0; + in_file->ReadSync(buffer, entry->size(), 0, &bytes_read); + fwrite(buffer, bytes_read, 1, file); + } + + fclose(file); + in_file->Destroy(); + } + + if (buffer) { + delete[] buffer; + } + + return X_STATUS_SUCCESS; +} + void Emulator::Pause() { if (paused_) { return; diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index 6fad1d28b..ff7916ccd 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -196,6 +196,9 @@ const std::unique_ptr CreateVfsDeviceBasedOnPath( // Launches a game from an STFS container file. X_STATUS LaunchStfsContainer(const std::filesystem::path& path); + // Extract content of package to content specific directory. + X_STATUS InstallContentPackage(const std::filesystem::path& path); + void Pause(); void Resume(); bool is_paused() const { return paused_; } diff --git a/src/xenia/vfs/devices/stfs_container_device.h b/src/xenia/vfs/devices/stfs_container_device.h index e34f7c7f1..65bd6d7d3 100644 --- a/src/xenia/vfs/devices/stfs_container_device.h +++ b/src/xenia/vfs/devices/stfs_container_device.h @@ -83,6 +83,9 @@ class StfsContainerDevice : public Device { return files_total_size_ - sizeof(StfsHeader); } + uint32_t title_id() const { return header_.metadata.execution_info.title_id; } + XContentType content_type() const { return header_.metadata.content_type; } + private: const uint32_t kBlocksPerHashLevel[3] = {170, 28900, 4913000}; const uint32_t kEndOfChain = 0xFFFFFF;