From 9dcde37e507e95ac21c41a77e2f036a558ba09df Mon Sep 17 00:00:00 2001 From: Vishrut Sachan Date: Tue, 14 Apr 2026 06:43:55 +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 | 1 + rpcs3/Loader/iso_cache.cpp | 10 +++---- rpcs3/Loader/iso_cache.h | 4 +-- rpcs3/rpcs3qt/game_list_frame.cpp | 44 +++++++++++++++++++++++++++---- rpcs3/rpcs3qt/main_window.cpp | 4 +++ rpcs3/rpcs3qt/qt_utils.cpp | 2 +- 9 files changed, 56 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 f198156d47..b79eb5a52a 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1490,9 +1490,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 a38b65089f..58d9e66ef4 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -211,6 +211,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 8a88e876bf..c67239a3ed 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -142,6 +142,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; } const std::shared_ptr get_dec() { return m_dec; } iso_fs_node* retrieve(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 fff6f33362..726ab5a5fe 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -528,17 +528,19 @@ 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{}; const bool is_iso = is_file_iso(dir_or_elf); + std::string iso_cache_key; if (is_iso) { + const std::string 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) // when no valid cache entry exists for this ISO path + mtime. - if (!iso_cache::load(dir_or_elf, cache_entry)) + if (!iso_cache::load(dir_or_elf, iso_cache_key, cache_entry)) { archive = std::make_unique(dir_or_elf); } @@ -558,10 +560,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. @@ -744,7 +747,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); } } @@ -869,7 +872,38 @@ void game_list_frame::OnParsingFinished() } else if (is_file_iso(entry.path)) { - push_path(entry.path, legit_paths); + 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 { diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 2a366705ab..7ac04e5707 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -3643,6 +3643,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 fe7bb6f8d6..b81fb52944 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -712,7 +712,7 @@ namespace gui // Check cache first — avoids constructing a full iso_archive just for the icon. iso_metadata_cache_entry cache_entry{}; - if (iso_cache::load(archive_path, cache_entry) && !cache_entry.icon_data.empty()) + if (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()));