From ca87d103fdc43adcae137098df0acff53b9e437d Mon Sep 17 00:00:00 2001 From: Vishrut Sachan Date: Sun, 3 May 2026 11:11:18 +0530 Subject: [PATCH] game_list: Add multi-game collection support for ISO format discs --- rpcs3/Emu/GameInfo.h | 1 + rpcs3/Emu/System.cpp | 4 +-- rpcs3/Emu/System.h | 2 ++ rpcs3/Loader/ISO.h | 2 +- rpcs3/Loader/iso_cache.cpp | 10 +++---- rpcs3/Loader/iso_cache.h | 4 +-- rpcs3/rpcs3qt/game_list_frame.cpp | 47 ++++++++++++++++++++++++++++--- rpcs3/rpcs3qt/main_window.cpp | 4 +++ rpcs3/rpcs3qt/qt_utils.cpp | 2 +- 9 files changed, 60 insertions(+), 16 deletions(-) diff --git a/rpcs3/Emu/GameInfo.h b/rpcs3/Emu/GameInfo.h index da8b2638ba..a99e708521 100644 --- a/rpcs3/Emu/GameInfo.h +++ b/rpcs3/Emu/GameInfo.h @@ -9,6 +9,7 @@ struct GameInfo std::string icon_path; std::string movie_path; std::string audio_path; + std::string game_dir; std::string name; std::string serial; diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 39172da368..199aad38ee 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1495,9 +1495,9 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // 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")) + if (fs::exists(path + m_game_dir + "/USRDIR/EBOOT.BIN")) { - path = path + "PS3_GAME/USRDIR/EBOOT.BIN"; + path = path + m_game_dir + "/USRDIR/EBOOT.BIN"; } m_path_real = m_path; diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 8bee2dc7a5..99c570b809 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -215,6 +215,8 @@ public: m_cb = std::move(cb); } + void SetGameDir(const std::string& game_dir) { m_game_dir = game_dir; } + const auto& GetCallbacks() const { return m_cb; diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h index ba39956239..7287146a2f 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -172,7 +172,7 @@ public: iso_archive(const std::string& path); const std::string& path() const { return m_path; } - + const iso_fs_node& root() const { return m_root; } iso_fs_node* retrieve(const std::string& path); bool exists(const std::string& path); bool is_file(const std::string& path); diff --git a/rpcs3/Loader/iso_cache.cpp b/rpcs3/Loader/iso_cache.cpp index 17453489e8..768948e421 100644 --- a/rpcs3/Loader/iso_cache.cpp +++ b/rpcs3/Loader/iso_cache.cpp @@ -34,7 +34,7 @@ namespace namespace iso_cache { - bool load(const std::string& iso_path, iso_metadata_cache_entry& out_entry) + bool load(const std::string& iso_path, const std::string& cache_key, iso_metadata_cache_entry& out_entry) { fs::stat_t iso_stat{}; if (!fs::get_stat(iso_path, iso_stat) || iso_stat.is_directory) @@ -42,7 +42,7 @@ namespace iso_cache return false; } - const std::string stem = get_cache_stem(iso_path); + const std::string stem = get_cache_stem(cache_key); const std::string dir = get_cache_dir(); const std::string yml_path = dir + stem + ".yml"; const std::string sfo_path = dir + stem + ".sfo"; @@ -89,11 +89,9 @@ namespace iso_cache return true; } - void save(const std::string& iso_path, const iso_metadata_cache_entry& entry) + void save(const std::string& iso_path, const std::string& cache_key, const iso_metadata_cache_entry& entry) { - iso_cache_log.notice("Saving cache for '%s'", iso_path); - - const std::string stem = get_cache_stem(iso_path); + const std::string stem = get_cache_stem(cache_key); const std::string dir = get_cache_dir(); const std::string yml_path = dir + stem + ".yml"; const std::string sfo_path = dir + stem + ".sfo"; diff --git a/rpcs3/Loader/iso_cache.h b/rpcs3/Loader/iso_cache.h index 8f6594b76c..a6c66797e6 100644 --- a/rpcs3/Loader/iso_cache.h +++ b/rpcs3/Loader/iso_cache.h @@ -22,10 +22,10 @@ struct iso_metadata_cache_entry namespace iso_cache { // Returns false if no valid cache entry exists or mtime has changed. - bool load(const std::string& iso_path, iso_metadata_cache_entry& out_entry); + bool load(const std::string& iso_path, const std::string& cache_key, iso_metadata_cache_entry& out_entry); // Persists a populated cache entry to disk. - void save(const std::string& iso_path, const iso_metadata_cache_entry& entry); + void save(const std::string& iso_path, const std::string& cache_key, const iso_metadata_cache_entry& entry); // Remove cache entries for ISOs that are no longer in the scanned set. void cleanup(const std::unordered_set& valid_iso_paths); diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 6e745f448c..ed34df171b 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -547,21 +547,24 @@ void game_list_frame::OnParsingFinished() const auto add_game = [this, localized_title, localized_icon, localized_movie, dev_flash, game_icon_path, _hdd, cat_unknown_localized = localized.category.unknown.toStdString(), cat_unknown = cat::cat_unknown.toStdString(), play_hover_movies = m_play_hover_movies, play_hover_music = m_play_hover_music, show_custom_icons = m_show_custom_icons] - (const std::string& dir_or_elf) + (const std::string& dir_or_elf, const std::string& game_dir = "PS3_GAME") { std::unique_ptr archive; iso_metadata_cache_entry cache_entry{}; bool is_raw_device = false; const bool is_archive = is_iso_file(dir_or_elf, nullptr, &is_raw_device); + std::string iso_cache_key; if (is_archive) { + iso_cache_key = (game_dir == "PS3_GAME") ? dir_or_elf : dir_or_elf + "//" + game_dir; // Only construct iso_archive (which walks the full directory tree) in case of raw device or // when no valid cache entry exists for this ISO path + mtime - if (is_raw_device || !iso_cache::load(dir_or_elf, cache_entry)) + if (is_raw_device || !iso_cache::load(dir_or_elf, iso_cache_key, cache_entry)) { archive = std::make_unique(dir_or_elf); } + // Track this ISO path for cache cleanup after scan completes. std::lock_guard lock(m_path_mutex); m_scanned_iso_paths.insert(dir_or_elf); @@ -578,10 +581,11 @@ void game_list_frame::OnParsingFinished() gui_game_info game{}; game.info.path = dir_or_elf; + game.info.game_dir = (game_dir == "PS3_GAME") ? "" : game_dir; const Localized thread_localized; - const std::string sfo_dir = (archive || !cache_entry.psf_data.empty()) ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); + const std::string sfo_dir = (archive || !cache_entry.psf_data.empty()) ? game_dir : rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); const std::string sfo_path = sfo_dir + "/PARAM.SFO"; // Load PSF: from archive on cache miss, rehydrate from cached SFO bytes on hit. @@ -764,7 +768,7 @@ void game_list_frame::OnParsingFinished() } } - iso_cache::save(dir_or_elf, cache_entry); + iso_cache::save(dir_or_elf, (game_dir == "PS3_GAME") ? dir_or_elf : dir_or_elf + "//" + game_dir, cache_entry); } } @@ -892,6 +896,41 @@ void game_list_frame::OnParsingFinished() add_disc_dir(entry.path, legit_paths); } + else if (is_iso_file(entry.path)) + { + iso_archive archive(entry.path); + const iso_fs_node& root = archive.root(); + const std::regex ps3_gm_regex("^PS3_GM[[:digit:]]{2}$"); + bool found = false; + + for (const auto& child : root.children) + { + if (m_refresh_watcher.isCanceled()) + { + break; + } + + if (!child->metadata.is_directory) + { + continue; + } + + const std::string& name = child->metadata.name; + + if (name == "PS3_GAME" || std::regex_match(name, ps3_gm_regex)) + { + add_game(entry.path, name); + found = true; + } + } + + if (!found) + { + add_game(entry.path); + } + + return; + } else { game_list_log.trace("Invalid game path registered: %s", entry.path); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 4333934cda..97081aca4a 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -3691,6 +3691,10 @@ void main_window::CreateDockWindows() connect(m_game_list_frame, &game_list_frame::RequestBoot, this, [this](const game_info& game, cfg_mode config_mode, const std::string& config_path, const std::string& savestate) { + if (!game->info.game_dir.empty()) + { + Emu.SetGameDir(game->info.game_dir); + } Boot(savestate.empty() ? game->info.path : savestate, game->info.serial, false, false, config_mode, config_path); }); diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index b648688bf8..84de92d692 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -716,7 +716,7 @@ namespace gui // With the exception of raw device, check cache first — avoids constructing a full iso_archive just for the icon. iso_metadata_cache_entry cache_entry{}; - if (!is_raw_device && iso_cache::load(archive_path, cache_entry) && !cache_entry.icon_data.empty()) + if (!is_raw_device && iso_cache::load(archive_path, archive_path, cache_entry) && !cache_entry.icon_data.empty()) { const QByteArray data(reinterpret_cast(cache_entry.icon_data.data()), static_cast(cache_entry.icon_data.size()));