diff --git a/Utilities/File.h b/Utilities/File.h index a5a4c53d75..77dc5be553 100644 --- a/Utilities/File.h +++ b/Utilities/File.h @@ -155,7 +155,7 @@ namespace fs // Virtual device struct device_base { - const std::string fs_prefix; + std::string fs_prefix; device_base(); virtual ~device_base(); diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 83e8a28398..e5e2e71c9e 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -125,6 +125,7 @@ target_sources(rpcs3_emu PRIVATE ../Loader/PSF.cpp ../Loader/PUP.cpp ../Loader/TAR.cpp + ../Loader/ISO.cpp ../Loader/TROPUSR.cpp ../Loader/TRP.cpp ) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 651c220b60..8cd25fdf35 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -34,6 +34,7 @@ #include "Loader/PSF.h" #include "Loader/TAR.h" +#include "Loader/ISO.h" #include "Loader/ELF.h" #include "Loader/disc.h" @@ -782,7 +783,7 @@ bool Emulator::BootRsxCapture(const std::string& path) std::unique_ptr frame = std::make_unique(); utils::serial load; load.set_reading_state(); - + const std::string lower = fmt::to_lower(path); if (lower.ends_with(".gz") || lower.ends_with(".zst")) @@ -1078,6 +1079,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, sys_log.notice("Path: %s", m_path); std::string inherited_ps3_game_path; + bool launching_from_disc_archive = false; { Init(); @@ -1406,6 +1408,10 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, m_path = rpcs3::utils::get_hdd1_dir(); m_path += std::string_view(argv[0]).substr(9); } + else if (is_file_iso(argv[0])) + { + m_path = argv[0]; + } else { sys_log.error("Unknown source for path redirection: %s", argv[0]); @@ -1421,6 +1427,28 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } const std::string resolved_path = GetCallbacks().resolve_path(m_path); + if (is_file_iso(m_path)) + { + load_iso(m_path); + + std::string path = iso_device::virtual_device_name + "/"; + + vfs::mount("/dev_bdvd/"sv, path); + + // ISOs that are install discs will error if set to EBOOT.BIN + // so this should cover both of them + if (fs::exists(path + "PS3_GAME/USRDIR/EBOOT.BIN")) + { + path = path + "PS3_GAME/USRDIR/EBOOT.BIN"; + } + + m_path = path; + + m_dir = "/dev_bdvd/PS3_GAME/"; + m_cat = "DG"sv; + + launching_from_disc_archive = true; + } const std::string elf_dir = fs::get_parent_dir(m_path); @@ -1595,8 +1623,14 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + // ISO PKG INSTALL HACK! + if (!m_path.ends_with("EBOOT.BIN") && launching_from_disc_archive) + { + bdvd_dir = m_path; + } + // Special boot mode (directory scan) - if (fs::is_dir(m_path)) + if (fs::is_dir(m_path) && !launching_from_disc_archive) { m_state = system_state::ready; GetCallbacks().on_ready(); @@ -1788,6 +1822,10 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, sys_log.error("Failed to move disc game %s to '%s' (%s)", m_title_id, dst_dir, fs::g_tls_error); return game_boot_result::wrong_disc_location; } + else if (launching_from_disc_archive) + { + bdvd_dir = iso_device::virtual_device_name + "/"; + } } if (bdvd_dir.empty() && disc.empty() && !is_disc_patch) @@ -2076,6 +2114,13 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + // ISO has no USRDIR/EBOOT.BIN, and we've examined its PKGDIR and extras. + // time to wrap up + if (!m_path.ends_with("EBOOT.BIN") && launching_from_disc_archive) + { + return game_boot_result::nothing_to_boot; + } + // Check firmware version if (const std::string_view game_fw_version = psf::get_string(_psf, "PS3_SYSTEM_VER", ""); !game_fw_version.empty()) { @@ -3368,7 +3413,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s { if (spu.first->pc != spu.second || spu.first->unsavable) { - std::string dump; + std::string dump; spu.first->dump_all(dump); sys_log.error("SPU thread continued after being paused. (old_pc=0x%x, pc=0x%x, unsavable=%d)", spu.second, spu.first->pc, spu.first->unsavable); @@ -3546,7 +3591,17 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s ar(std::array{}); // Reserved for future use - if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) + // Game mounted from archive + if (m_path.starts_with(iso_device::virtual_device_name + "/")) + { + auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + ensure(device); + + auto iso_device = dynamic_cast(device.get()); + ar(iso_device->get_loaded_iso()); + ar(m_title_id); + } + else if (auto dir = vfs::get("/dev_bdvd/PS3_GAME"); fs::is_dir(dir) && !fs::is_file(fs::get_parent_dir(dir) + "/PS3_DISC.SFB")) { // Fake /dev_bdvd/PS3_GAME detected, use HDD0 for m_path restoration ensure(vfs::unmount("/dev_bdvd/PS3_GAME")); @@ -3831,6 +3886,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s disc.clear(); klic.clear(); hdd1.clear(); + unload_iso(); init_mem_containers = nullptr; m_config_path.clear(); m_config_mode = cfg_mode::custom; @@ -4331,7 +4387,7 @@ const std::string& Emulator::GetFakeCat() const { const std::string mount_point = vfs::get("/dev_bdvd"); - if (mount_point.empty() || !IsPathInsideDir(m_path, mount_point)) + if (mount_point.empty() || (!IsPathInsideDir(m_path, mount_point) && !m_path.starts_with(iso_device::virtual_device_name))) { static const std::string s_hg = "HG"; return s_hg; diff --git a/rpcs3/Emu/games_config.cpp b/rpcs3/Emu/games_config.cpp index cfa43f6e15..ad6daff193 100644 --- a/rpcs3/Emu/games_config.cpp +++ b/rpcs3/Emu/games_config.cpp @@ -4,6 +4,8 @@ #include "util/yaml.hpp" #include "Utilities/File.h" +#include "Loader/ISO.h" + LOG_CHANNEL(cfg_log, "CFG"); games_config::games_config() @@ -44,6 +46,15 @@ std::string games_config::get_path(const std::string& title_id) const games_config::result games_config::add_game(const std::string& key, const std::string& path) { + if (path == iso_device::virtual_device_name + "/") + { + auto device = fs::get_virtual_device(iso_device::virtual_device_name + "/"); + if (!device) return result::failure; + + auto iso_device = dynamic_cast(device.get()); + return add_game(key, iso_device->get_loaded_iso()); + } + std::lock_guard lock(m_mutex); // Access or create node if does not exist diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp new file mode 100644 index 0000000000..bee017a59d --- /dev/null +++ b/rpcs3/Loader/ISO.cpp @@ -0,0 +1,613 @@ +#include "stdafx.h" + +#include "ISO.h" + +#include +#include +#include +#include +#include + +bool is_file_iso(const std::string& path) +{ + if (fs::is_dir(path)) return false; + + return is_file_iso(fs::file(path)); +} + +bool is_file_iso(const fs::file& file) +{ + if (!file) return false; + if (file.size() < 32768 + 6) return false; + + file.seek(32768); + + char magic[5]; + file.read_at(32768 + 1, magic, 5); + + return magic[0] == 'C' && magic[1] == 'D' + && magic[2] == '0' && magic[3] == '0' + && magic[4] == '1'; +} + +const int ISO_BLOCK_SIZE = 2048; + +template +inline T read_both_endian_int(fs::file& file) +{ + T out; + + if (std::endian::little == std::endian::native) + { + out = file.read(); + file.seek(sizeof(T), fs::seek_cur); + } + else + { + file.seek(sizeof(T), fs::seek_cur); + out = file.read(); + } + + return out; +} + +// assumed that directory_entry is at file head +std::optional iso_read_directory_entry(fs::file& file, bool names_in_ucs2 = false) +{ + auto start_pos = file.pos(); + u8 entry_length = file.read(); + + if (entry_length == 0) return std::nullopt; + + file.seek(1, fs::seek_cur); + u32 start_sector = read_both_endian_int(file); + u32 file_size = read_both_endian_int(file); + + std::tm file_date = {}; + file_date.tm_year = file.read(); + file_date.tm_mon = file.read() - 1; + file_date.tm_mday = file.read(); + file_date.tm_hour = file.read(); + file_date.tm_min = file.read(); + file_date.tm_sec = file.read(); + s16 timezone_value = file.read(); + s16 timezone_offset = (timezone_value - 50) * 15 * 60; + + std::time_t date_time = std::mktime(&file_date) + timezone_offset; + + u8 flags = file.read(); + + // 2nd flag bit indicates whether a given fs node is a directory + bool is_directory = flags & 0b00000010; + bool has_more_extents = flags & 0b10000000; + + file.seek(6, fs::seek_cur); + + u8 file_name_length = file.read(); + + std::string file_name; + file.read(file_name, file_name_length); + + if (file_name_length == 1 && file_name[0] == 0) + { + file_name = "."; + } + else if (file_name == "\1") + { + file_name = ".."; + } + else if (names_in_ucs2) // for strings in joliet descriptor + { + std::string new_file_name = ""; + int read = 0; + const u8* raw_str = reinterpret_cast(file_name.c_str()); + while(read < file_name_length) + { + // characters are stored in big endian format. + const u16 upper = raw_str[read]; + const u8 lower = raw_str[read + 1]; + + const u16 code_point = (upper << 8) + lower; + + std::wstring_convert, char16_t> convert; + new_file_name += convert.to_bytes(code_point); + + read += 2; + } + + file_name = new_file_name; + } + + if (file_name.ends_with(";1")) + { + file_name.erase(file_name.end() - 2, file_name.end()); + } + + if (file_name_length > 1 && file_name.ends_with(".")) + { + file_name.pop_back(); + } + + // skip the rest of the entry. + file.seek(entry_length + start_pos); + + return iso_fs_metadata + { + .name = file_name, + .time = date_time, + .is_directory = is_directory, + .has_multiple_extents = has_more_extents, + .extents = + { + iso_extent_info + { + .start = start_sector, + .size = file_size + } + } + }; +} + +void iso_form_hierarchy(fs::file& file, iso_fs_node& node, + bool use_ucs2_decoding = false, std::string parent_path = "") +{ + if (!node.metadata.is_directory) return; + + std::vector multi_extent_node_indices; + + // assuming the directory spans a single extent + const auto& directory_extent = node.metadata.extents[0]; + + file.seek(directory_extent.start * ISO_BLOCK_SIZE); + + u64 end_pos = directory_extent.size + (directory_extent.start * ISO_BLOCK_SIZE); + + while(file.pos() < end_pos) + { + auto entry = iso_read_directory_entry(file, use_ucs2_decoding); + if (!entry) + { + u64 new_sector = (file.pos() / ISO_BLOCK_SIZE) + 1; + file.seek(new_sector * ISO_BLOCK_SIZE); + continue; + } + + bool extent_added = false; + + // find previous extent and merge into it, otherwise we push this node's index + for (int index : multi_extent_node_indices) + { + auto& selected_node = node.children.at(index); + if (selected_node->metadata.name.compare(entry->name) == 0) + { + // merge into selected_node + selected_node->metadata.extents.push_back(entry->extents[0]); + + extent_added = true; + } + } + + if (extent_added) continue; + + if (entry->has_multiple_extents) + { + // haven't pushed entry to node.children yet so node.children::size() == entry_index + multi_extent_node_indices.push_back(node.children.size()); + } + + node.children.push_back(std::make_unique(iso_fs_node{ + .metadata = *entry + })); + } + + for (auto& child_node : node.children) + { + if (child_node->metadata.name != "." && child_node->metadata.name != "..") + { + iso_form_hierarchy(file, *child_node, use_ucs2_decoding, parent_path + "/" + node.metadata.name); + } + } +} + +u64 iso_fs_metadata::size() const +{ + u64 total_size = 0; + for (const auto& extent : extents) + { + total_size += extent.size; + } + + return total_size; +} + +iso_archive::iso_archive(const std::string& path) +{ + m_path = path; + m_file = fs::file(path); + + if (!is_file_iso(m_file)) + { + // not iso... TODO: throw something?? + return; + } + + u8 descriptor_type = -2; + bool use_ucs2_decoding = false; + + do + { + auto descriptor_start = m_file.pos(); + + descriptor_type = m_file.read(); + + // 1 = primary vol descriptor, 2 = joliet SVD + if (descriptor_type == 1 || descriptor_type == 2) + { + use_ucs2_decoding = descriptor_type == 2; + + // skip the rest of descriptor's data + m_file.seek(155, fs::seek_cur); + + m_root = iso_fs_node + { + .metadata = iso_read_directory_entry(m_file, use_ucs2_decoding).value(), + }; + } + + m_file.seek(descriptor_start + ISO_BLOCK_SIZE); + } + while(descriptor_type != 255); + + iso_form_hierarchy(m_file, m_root, use_ucs2_decoding); +} + +iso_fs_node* iso_archive::retrieve(const std::string& passed_path) +{ + std::string path = std::filesystem::path(passed_path).string(); + + size_t start = 0; + size_t end = path.find_first_of(fs::delim); + + std::stack search_stack; + search_stack.push(&m_root); + + do + { + if (search_stack.empty()) return nullptr; + auto* top_entry = search_stack.top(); + + if (end == std::string::npos) + { + end = path.size(); + } + + auto path_component = path.substr(start, end-start); + + bool found = false; + + if (path_component == ".") + { + found = true; + } + else if (path_component == "..") + { + search_stack.pop(); + found = true; + } + else + { + for (const auto& entry : top_entry->children) + { + if (entry->metadata.name.compare(path_component) == 0) + { + search_stack.push(entry.get()); + + found = true; + break; + } + } + } + + if (!found) return nullptr; + + start = end + 1; + end = path.find_first_of(fs::delim, start); + } + while(start < path.size()); + + if (search_stack.empty()) return nullptr; + + return search_stack.top(); +} + +bool iso_archive::exists(const std::string& path) +{ + return retrieve(path) != nullptr; +} + +bool iso_archive::is_file(const std::string& path) +{ + auto file_node = retrieve(path); + if (!file_node) return false; + + return !file_node->metadata.is_directory; +} + +iso_file iso_archive::open(const std::string& path) +{ + return iso_file(fs::file(m_path), *retrieve(path)); +} + +iso_file::iso_file(fs::file&& iso_handle, const iso_fs_node& node) + : m_file(std::move(iso_handle)), m_meta(node.metadata), m_pos(0) +{ + m_file.seek(ISO_BLOCK_SIZE * node.metadata.extents[0].start); +} + +fs::stat_t iso_file::get_stat() +{ + return fs::stat_t + { + .is_directory = false, + .is_symlink = false, + .is_writable = false, + .size = size(), + .atime = m_meta.time, + .mtime = m_meta.time, + .ctime = m_meta.time + }; +} + +bool iso_file::trunc(u64) +{ + fs::g_tls_error = fs::error::readonly; + return false; +} + +std::pair iso_file::get_extent_pos(u64 pos) const +{ + auto it = m_meta.extents.begin(); + + while(pos >= it->size && it != m_meta.extents.end() - 1) + { + pos -= it->size; + + it++; + } + + return {pos, *it}; +} + +// assumed valid and in bounds. +u64 iso_file::file_offset(u64 pos) const +{ + auto [local_pos, extent] = get_extent_pos(pos); + + return (extent.start * ISO_BLOCK_SIZE) + local_pos; +} + +u64 iso_file::local_extent_remaining(u64 pos) const +{ + auto [local_pos, extent] = get_extent_pos(pos); + + return extent.size - local_pos; +} + +u64 iso_file::local_extent_size(u64 pos) const +{ + return get_extent_pos(pos).second.size; +} + +u64 iso_file::read(void* buffer, u64 size) +{ + auto r = read_at(m_pos, buffer, size); + m_pos += r; + return r; +} + +u64 iso_file::read_at(u64 offset, void* buffer, u64 size) +{ + u64 local_remaining = local_extent_remaining(offset); + + u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); + + auto total_size = this->size(); + + if (size > total_read && (offset + total_read) < total_size) + { + u64 second_total_read = read_at(offset + total_read, + reinterpret_cast(buffer) + total_read, + size - total_read + ); + + return total_read + second_total_read; + } + + return total_read; +} + +u64 iso_file::write(const void*, u64) +{ + fs::g_tls_error = fs::error::readonly; + return 0; +} + +u64 iso_file::seek(s64 offset, fs::seek_mode whence) +{ + const s64 total_size = size(); + const s64 new_pos = + whence == fs::seek_set ? offset : + whence == fs::seek_cur ? offset + m_pos : + whence == fs::seek_end ? offset + total_size : -1; + + if (new_pos < 0) + { + fs::g_tls_error = fs::error::inval; + return -1; + } + + const u64 bad_res = -1; + + u64 result = m_file.seek(file_offset(m_pos)); + if (result == bad_res) return -1; + + m_pos = new_pos; + return m_pos; +} + +u64 iso_file::size() +{ + u64 extent_sizes = 0; + for (const auto& extent : m_meta.extents) + { + extent_sizes += extent.size; + } + + return extent_sizes; +} + +void iso_file::release() +{ + m_file.release(); +} + +bool iso_dir::read(fs::dir_entry& entry) +{ + if (m_pos < m_node.children.size()) + { + auto& selected = m_node.children[m_pos].get()->metadata; + u64 size = selected.size(); + + entry.name = selected.name; + entry.atime = selected.time; + entry.mtime = selected.time; + entry.ctime = selected.time; + entry.is_directory = selected.is_directory; + entry.is_symlink = false; + entry.is_writable = false; + entry.size = size; + + m_pos++; + + return true; + } + + return false; +} + +bool iso_device::stat(const std::string& path, fs::stat_t& info) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + auto& meta = node->metadata; + u64 size = meta.size(); + + info = fs::stat_t + { + .is_directory = meta.is_directory, + .is_symlink = false, + .is_writable = false, + .size = size, + .atime = meta.time, + .mtime = meta.time, + .ctime = meta.time + }; + + return true; +} + +bool iso_device::statfs(const std::string& path, fs::device_stat& info) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + auto& meta = node->metadata; + u64 size = meta.size(); + + info = fs::device_stat + { + .block_size=size, + .total_size=size, + .total_free=0, + .avail_free=0 + }; + + return false; +} + +std::unique_ptr iso_device::open(const std::string& path, bs_t mode) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return nullptr; + } + + if (node->metadata.is_directory) + { + fs::g_tls_error = fs::error::isdir; + return nullptr; + } + + return std::make_unique(fs::file(iso_path), *node); +} + +std::unique_ptr iso_device::open_dir(const std::string& path) +{ + auto relative_path = std::filesystem::relative(std::filesystem::path(path), + std::filesystem::path(fs_prefix)).string(); + + auto node = m_archive.retrieve(relative_path); + if (!node) + { + fs::g_tls_error = fs::error::noent; + return nullptr; + } + + if (!node->metadata.is_directory) + { + // fs::dir::open -> ::readdir should return ENOTDIR when path is + // pointing to a file instead of a folder, which translates to error::unknown. + // doing the same here. + fs::g_tls_error = fs::error::unknown; + return nullptr; + } + + return std::make_unique(*node); +} + +void iso_dir::rewind() +{ + m_pos = 0; +} + +void load_iso(const std::string& path) +{ + fs::set_virtual_device("iso_overlay_fs_dev", + stx::make_shared(path)); +} + +void unload_iso() +{ + fs::set_virtual_device("iso_overlay_fs_dev", + stx::shared_ptr()); +} diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h new file mode 100644 index 0000000000..f986899db8 --- /dev/null +++ b/rpcs3/Loader/ISO.h @@ -0,0 +1,113 @@ +#pragma once + +#include "Utilities/File.h" +#include "util/types.hpp" + +bool is_file_iso(const std::string& path); +bool is_file_iso(const fs::file& path); + +void load_iso(const std::string& path); +void unload_iso(); + +struct iso_extent_info +{ + u64 start; + u64 size; +}; + +struct iso_fs_metadata +{ + std::string name; + s64 time; + bool is_directory; + bool has_multiple_extents; + std::vector extents; + + u64 size() const; +}; + +struct iso_fs_node +{ + iso_fs_metadata metadata; + std::vector> children; +}; + +class iso_file : public fs::file_base +{ + fs::file m_file; + iso_fs_metadata m_meta; + u64 m_pos; + + std::pair get_extent_pos(u64 pos) const; + u64 file_offset(u64 pos) const; + u64 local_extent_remaining(u64 pos) const; + u64 local_extent_size(u64 pos) const; + + public: + iso_file(fs::file&& iso_handle, const iso_fs_node& node); + + fs::stat_t get_stat() override; + bool trunc(u64 length) override; + u64 read(void* buffer, u64 size) override; + u64 read_at(u64 offset, void* buffer, u64 size) override; + u64 write(const void* buffer, u64 size) override; + u64 seek(s64 offset, fs::seek_mode whence) override; + u64 size() override; + + void release() override; +}; + +class iso_dir : public fs::dir_base +{ + const iso_fs_node& m_node; + u64 m_pos; + + public: + iso_dir(const iso_fs_node& node) + : m_node(node), m_pos(0) + {} + + bool read(fs::dir_entry&) override; + void rewind() override; +}; + +// represents the .iso file itself. +class iso_archive +{ + std::string m_path; + iso_fs_node m_root; + fs::file m_file; + + public: + iso_archive(const std::string& path); + + iso_fs_node* retrieve(const std::string& path); + bool exists(const std::string& path); + bool is_file(const std::string& path); + + iso_file open(const std::string& path); +}; + +class iso_device : public fs::device_base +{ + iso_archive m_archive; + std::string iso_path; + + public: + inline static std::string virtual_device_name = "/vfsv0_virtual_iso_overlay_fs_dev"; + + iso_device(const std::string& iso_path, const std::string& device_name = virtual_device_name) + : m_archive(iso_path), iso_path(iso_path) + { + fs_prefix = device_name; + } + ~iso_device() override = default; + + const std::string& get_loaded_iso() const { return iso_path; } + + bool stat(const std::string& path, fs::stat_t& info) override; + bool statfs(const std::string& path, fs::device_stat& info) override; + + std::unique_ptr open(const std::string& path, bs_t mode) override; + std::unique_ptr open_dir(const std::string& path) override; +}; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 437d79df8a..1eeee95567 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -538,6 +538,7 @@ + @@ -1017,6 +1018,7 @@ + @@ -1092,4 +1094,4 @@ - \ No newline at end of file + diff --git a/rpcs3/rpcs3qt/game_list_base.cpp b/rpcs3/rpcs3qt/game_list_base.cpp index 51e43c95bf..e4fb012b17 100644 --- a/rpcs3/rpcs3qt/game_list_base.cpp +++ b/rpcs3/rpcs3qt/game_list_base.cpp @@ -1,6 +1,8 @@ #include "stdafx.h" #include "game_list_base.h" +#include "Loader/ISO.h" + #include #include @@ -50,7 +52,20 @@ void game_list_base::IconLoadFunction(game_info game, qreal device_pixel_ratio, if (game->icon.isNull() && (game->info.icon_path.empty() || !game->icon.load(QString::fromStdString(game->info.icon_path)))) { - if (game_list_log.warning) + if (game->icon_in_archive) + { + iso_archive archive(game->info.path); + auto icon_file = archive.open(game->info.icon_path); + auto icon_size = icon_file.size(); + QByteArray data(icon_size, 0); + icon_file.read(data.data(), icon_size); + QImage iconImage; + if (iconImage.loadFromData(data)) + { + game->icon = QPixmap::fromImage(iconImage); + } + } + else if (game_list_log.warning) { bool logged = false; { diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index f68bc44896..1da704f7f7 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -18,6 +18,7 @@ #include "Emu/vfs_config.h" #include "Emu/system_utils.hpp" #include "Loader/PSF.h" +#include "Loader/ISO.h" #include "util/types.hpp" #include "Utilities/File.h" #include "util/sysinfo.hpp" @@ -634,13 +635,46 @@ void game_list_frame::OnParsingFinished() const auto add_game = [this, localized_title, localized_icon, localized_movie, dev_flash, cat_unknown_localized = localized.category.unknown.toStdString(), cat_unknown = cat::cat_unknown.toStdString(), game_icon_path, _hdd, play_hover_movies = m_play_hover_movies, show_custom_icons = m_show_custom_icons](const std::string& dir_or_elf) { + const auto load_game_psf = [&dir_or_elf](const std::string& sfo_path, const std::unique_ptr& archive) + { + if (!archive) return psf::load_object(sfo_path); + + // HACK: psf does not accept a file_base argument, + // so we are creating a dummy fs:file and replacing the internal file_base handle with an iso_file + // instead. + fs::file psf_file(sfo_path); + psf_file.reset(std::make_unique(fs::file(dir_or_elf), *archive->retrieve(sfo_path))); + + return psf::load_object(psf_file, sfo_path); + }; + + std::unique_ptr archive; + if (is_file_iso(dir_or_elf)) + { + archive = std::make_unique(dir_or_elf); + } + + const auto file_exists = [&archive](const std::string& path) + { + if (!archive) return fs::is_file(path); + return archive->is_file(path); + }; + gui_game_info game{}; game.info.path = dir_or_elf; + if (archive) + { + fs::stat_t iso_stat; + fs::get_stat(game.info.path, iso_stat); + + game.info.size_on_disk = iso_stat.size; + } + const Localized thread_localized; - const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); - const psf::registry psf = psf::load_object(sfo_dir + "/PARAM.SFO"); + const std::string sfo_dir = !archive ? rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf) : "PS3_GAME"; + const psf::registry psf = load_game_psf(sfo_dir + "/PARAM.SFO", archive); const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); if (title_id.empty()) @@ -708,7 +742,7 @@ void game_list_frame::OnParsingFinished() if (game.info.icon_path.empty()) { - if (std::string icon_path = sfo_dir + "/" + localized_icon; fs::is_file(icon_path)) + if (std::string icon_path = sfo_dir + "/" + localized_icon; file_exists(icon_path)) { game.info.icon_path = std::move(icon_path); } @@ -716,19 +750,26 @@ void game_list_frame::OnParsingFinished() { game.info.icon_path = sfo_dir + "/ICON0.PNG"; } + + if (!game.info.icon_path.empty() && archive) + { + if (!archive->exists(game.info.icon_path)) return; + + game.icon_in_archive = true; + } } - if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; fs::is_file(movie_path)) + if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_gif = true; } - else if (std::string movie_path = sfo_dir + "/" + localized_movie; fs::is_file(movie_path)) + else if (std::string movie_path = sfo_dir + "/" + localized_movie; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_pam = true; } - else if (std::string movie_path = sfo_dir + "/ICON1.PAM"; fs::is_file(movie_path)) + else if (std::string movie_path = sfo_dir + "/ICON1.PAM"; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_pam = true; @@ -853,6 +894,10 @@ void game_list_frame::OnParsingFinished() add_disc_dir(entry.path, legit_paths); } + else if (is_file_iso(entry.path)) + { + push_path(entry.path, legit_paths); + } else { game_list_log.trace("Invalid game path registered: %s", entry.path); diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index 8d721f86ec..31283465d1 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -11,6 +11,8 @@ #include "Emu/vfs_config.h" #include "Utilities/StrUtil.h" +#include "Loader/ISO.h" + #include #include #include @@ -242,6 +244,12 @@ void game_list_table::populate( custom_table_widget_item* icon_item = new custom_table_widget_item; game->item = icon_item; + if (is_file_iso(game->info.path) && !game->info.movie_path.empty() + && !fs::exists(game->info.movie_path)) + { + icon_item->set_source_path(game->info.path); + } + icon_item->set_image_change_callback([this, icon_item, game](const QVideoFrame& frame) { if (!icon_item || !game) diff --git a/rpcs3/rpcs3qt/gui_game_info.h b/rpcs3/rpcs3qt/gui_game_info.h index 984f8eb7bf..3b19d27458 100644 --- a/rpcs3/rpcs3qt/gui_game_info.h +++ b/rpcs3/rpcs3qt/gui_game_info.h @@ -20,6 +20,7 @@ struct gui_game_info bool has_custom_icon = false; bool has_hover_gif = false; bool has_hover_pam = false; + bool icon_in_archive = false; movie_item_base* item = nullptr; // Returns the visible version string in the game list diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 80d71a44c2..5b527b9fe6 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -568,6 +568,7 @@ void main_window::BootElf() "SELF files (EBOOT.BIN *.self);;" "BOOT files (*BOOT.BIN);;" "BIN files (*.bin);;" + "ISO files (*.iso);;" "All executable files (*.SAVESTAT.zst *.SAVESTAT.gz *.SAVESTAT *.sprx *.SPRX *.self *.SELF *.bin *.BIN *.prx *.PRX *.elf *.ELF *.o *.O);;" "All files (*.*)"), Q_NULLPTR, QFileDialog::DontResolveSymlinks); @@ -4122,7 +4123,7 @@ main_window::drop_type main_window::IsValidFile(const QMimeData& md, QStringList type = drop_type::drop_rrc; } // The emulator allows to execute ANY filetype, just not from drag-and-drop because it is confusing to users - else if (path.toLower().endsWith(".savestat.gz") || path.toLower().endsWith(".savestat.zst") || suffix_lo == "savestat" || suffix_lo == "sprx" || suffix_lo == "self" || suffix_lo == "bin" || suffix_lo == "prx" || suffix_lo == "elf" || suffix_lo == "o") + else if (path.toLower().endsWith(".savestat.gz") || path.toLower().endsWith(".savestat.zst") || suffix_lo == "savestat" || suffix_lo == "sprx" || suffix_lo == "self" || suffix_lo == "bin" || suffix_lo == "prx" || suffix_lo == "elf" || suffix_lo == "o" || suffix_lo == "iso") { type = drop_type::drop_game; } diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp index 107e5ef850..81e6d98e55 100644 --- a/rpcs3/rpcs3qt/qt_video_source.cpp +++ b/rpcs3/rpcs3qt/qt_video_source.cpp @@ -2,6 +2,8 @@ #include "Emu/System.h" #include "qt_video_source.h" +#include "Loader/ISO.h" + #include qt_video_source::qt_video_source() @@ -19,6 +21,11 @@ void qt_video_source::set_video_path(const std::string& video_path) m_video_path = QString::fromStdString(video_path); } +void qt_video_source::set_source_path(const std::string& source_path) +{ + m_source_path = QString::fromStdString(source_path); +} + void qt_video_source::set_active(bool active) { if (m_active.exchange(active) == active) return; @@ -55,7 +62,7 @@ void qt_video_source::init_movie() return; } - if (!m_image_change_callback || m_video_path.isEmpty() || !QFile::exists(m_video_path)) + if (!m_image_change_callback || m_video_path.isEmpty() || (!QFile::exists(m_video_path) && m_source_path.isEmpty())) { m_video_path.clear(); return; @@ -65,8 +72,25 @@ void qt_video_source::init_movie() if (lower.endsWith(".gif")) { - m_movie = std::make_unique(m_video_path); - m_video_path.clear(); + if (m_source_path.isEmpty()) + { + m_movie = std::make_unique(m_video_path); + m_video_path.clear(); + } + else + { + iso_archive archive(m_source_path.toStdString()); + auto movie_file = archive.open(m_video_path.toStdString()); + auto movie_size = movie_file.size(); + m_video_data = QByteArray(movie_size, 0); + movie_file.read(m_video_data.data(), movie_size); + + QBuffer buffer(&m_video_data); + buffer.open(QIODevice::ReadOnly); + m_movie = std::make_unique(&buffer); + + m_video_path.clear(); + } if (!m_movie->isValid()) { @@ -85,17 +109,30 @@ void qt_video_source::init_movie() if (lower.endsWith(".pam")) { // We can't set PAM files as source of the video player, so we have to feed them as raw data. - QFile file(m_video_path); - if (!file.open(QFile::OpenModeFlag::ReadOnly)) + if (m_source_path.isEmpty()) { - return; - } + QFile file(m_video_path); + if (!file.open(QFile::OpenModeFlag::ReadOnly)) + { + return; + } - // TODO: Decode the pam properly before pushing it to the player - m_video_data = file.readAll(); - if (m_video_data.isEmpty()) + // TODO: Decode the pam properly before pushing it to the player + m_video_data = file.readAll(); + if (m_video_data.isEmpty()) + { + return; + } + } + else { - return; + auto source_path = m_source_path.toStdString(); + auto video_path = m_video_path.toStdString(); + iso_archive archive(source_path.c_str()); + auto movie_file = archive.open(video_path); + auto movie_size = movie_file.size(); + m_video_data = QByteArray(movie_size, 0); + movie_file.read(m_video_data.data(), movie_size); } m_video_buffer = std::make_unique(&m_video_data); diff --git a/rpcs3/rpcs3qt/qt_video_source.h b/rpcs3/rpcs3qt/qt_video_source.h index a2710eea33..1f195def02 100644 --- a/rpcs3/rpcs3qt/qt_video_source.h +++ b/rpcs3/rpcs3qt/qt_video_source.h @@ -17,6 +17,7 @@ public: qt_video_source(); virtual ~qt_video_source(); + void set_source_path(const std::string& source_path); void set_video_path(const std::string& video_path) override; const QString& video_path() const { return m_video_path; } @@ -43,6 +44,7 @@ protected: atomic_t m_has_new = false; QString m_video_path; + QString m_source_path; // path of the source archive QByteArray m_video_data{}; QImage m_image{}; std::vector m_image_path; diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp index ed664e1b65..1af19a197c 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.cpp @@ -7,6 +7,8 @@ #include "gui_settings.h" #include "progress_dialog.h" +#include "Loader/ISO.h" + #include #include #include @@ -208,7 +210,7 @@ savestate_manager_dialog::savestate_manager_dialog(std::shared_ptr m_savestate_table->create_header_actions(m_savestate_column_acts, [this](int col) { return m_gui_settings->GetSavestateListColVisibility(static_cast(col)); }, [this](int col, bool visible) { m_gui_settings->SetSavestateListColVisibility(static_cast(col), visible); }); - + m_game_table->create_header_actions(m_game_column_acts, [this](int col) { return m_gui_settings->GetSavestateGamelistColVisibility(static_cast(col)); }, [this](int col, bool visible) { m_gui_settings->SetSavestateGamelistColVisibility(static_cast(col), visible); }); @@ -417,8 +419,9 @@ void savestate_manager_dialog::ResizeGameIcons() const qreal dpr = devicePixelRatioF(); const int savestate_index = item->data(GameUserRole::GameIndex).toInt(); const std::string icon_path = m_savestate_db[savestate_index]->game_icon_path; + const std::string archive_path = m_savestate_db[savestate_index]->archive_path; - item->set_icon_load_func([this, icon_path, savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) + item->set_icon_load_func([this, icon_path, archive_path, savestate_index, cancel = item->icon_loading_aborted(), dpr](int index) { if (cancel && cancel->load()) { @@ -445,6 +448,20 @@ void savestate_manager_dialog::ResizeGameIcons() } } + if (!archive_path.empty()) + { + iso_archive archive(archive_path); + auto icon_file = archive.open(icon_path); + auto icon_size = icon_file.size(); + QByteArray data(icon_size, 0); + icon_file.read(data.data(), icon_size); + QImage iconImage; + if (iconImage.loadFromData(data)) + { + icon = QPixmap::fromImage(iconImage); + } + } + if (cancel && cancel->load()) { return; @@ -617,6 +634,11 @@ void savestate_manager_dialog::StartSavestateLoadThreads() { game_data_ptr->game_name = gameinfo->info.name; game_data_ptr->game_icon_path = gameinfo->info.icon_path; + if (gameinfo->icon_in_archive) + { + game_data_ptr->archive_path = gameinfo->info.path; + } + break; } } diff --git a/rpcs3/rpcs3qt/savestate_manager_dialog.h b/rpcs3/rpcs3qt/savestate_manager_dialog.h index 28c3eddc8c..7a6a39a700 100644 --- a/rpcs3/rpcs3qt/savestate_manager_dialog.h +++ b/rpcs3/rpcs3qt/savestate_manager_dialog.h @@ -48,6 +48,7 @@ private: std::string game_name; std::string game_icon_path; std::string dir_path; + std::string archive_path; }; bool LoadSavestateFolderToDB(std::unique_ptr&& game_savestates);