From ad3ea966cbb3b7d4df7badd18ca5cd3908fe0692 Mon Sep 17 00:00:00 2001 From: Elad Ashkenazi Date: Fri, 2 Dec 2022 12:18:07 +0200 Subject: [PATCH] Add RPCS3/games/ for automatic games detection, support PSN games outside HDD0 (#12982) * SFO: Do not load PARAM.SFO with illegal TITLE_ID * Add support for PSN games outside HDD0 * Add RPCS3/games/ for automatic game detection --- Utilities/File.cpp | 3 + rpcs3/Emu/System.cpp | 103 ++++++++++++++++++++++- rpcs3/Emu/System.h | 1 + rpcs3/Loader/PSF.cpp | 8 ++ rpcs3/rpcs3qt/game_list_frame.cpp | 20 ++++- rpcs3/rpcs3qt/main_window.cpp | 20 +---- rpcs3/rpcs3qt/vfs_dialog_path_widget.cpp | 2 +- 7 files changed, 135 insertions(+), 22 deletions(-) diff --git a/Utilities/File.cpp b/Utilities/File.cpp index 2ac13f7b2b..6ea3e8b74a 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -437,6 +437,9 @@ std::string fs::get_parent_dir(std::string_view path, u32 levels) bool fs::stat(const std::string& path, stat_t& info) { + // Ensure consistent information on failure + info = {}; + if (auto device = get_virtual_device(path)) { return device->stat(path, info); diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 9a88105dd8..1b91cda033 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -374,7 +374,10 @@ void Emulator::Init(bool add_only) if (!fs::create_path(path)) { sys_log.fatal("Failed to create path: %s (%s)", path, fs::g_tls_error); + return false; } + + return true; }; const std::string save_path = dev_hdd0 + "home/" + m_usr + "/savedata/"; @@ -419,6 +422,11 @@ void Emulator::Init(bool add_only) make_path_verbose(fs::get_config_dir() + "sounds/"); make_path_verbose(patch_engine::get_patches_path()); + if (const std::string games_common = fs::get_config_dir() + "/games"; make_path_verbose(games_common)) + { + fs::write_file(user_path + "/Disc Games Can Be Put Here For Automatic Detection.txt", fs::create + fs::excl + fs::write, ""s); + } + if (add_only) { // We don't need to initialize the rest if we only add games @@ -961,6 +969,21 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool { m_path = rpcs3::utils::get_hdd0_dir(); m_path += std::string_view(argv[0]).substr(9); + + constexpr auto game0_path = "/dev_hdd0/game/"sv; + + if (argv[0].starts_with(game0_path) && !fs::is_file(vfs::get(argv[0]))) + { + std::string title_id = argv[0].substr(game0_path.size()); + title_id = title_id.substr(0, title_id.find_last_not_of('/')); + + // Try to load game directory from list if available + if (auto node = (title_id.empty() ? YAML::Node{} : games[title_id])) + { + disc = node.Scalar(); + m_path = disc + argv[0].substr(game0_path.size() + title_id.size()); + } + } } else if (argv[0].starts_with("/dev_flash"sv)) { @@ -1332,7 +1355,6 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool // Detect boot location const std::string hdd0_game = vfs::get("/dev_hdd0/game/"); - const std::string hdd0_disc = vfs::get("/dev_hdd0/disc/"); const bool from_hdd0_game = IsPathInsideDir(m_path, hdd0_game); const bool from_dev_flash = IsPathInsideDir(m_path, g_cfg_vfs.get_dev_flash()); @@ -1358,6 +1380,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool // Booting disc game from wrong location sys_log.error("Disc game %s found at invalid location /dev_hdd0/game/", m_title_id); + const std::string hdd0_disc = vfs::get("/dev_hdd0/disc/"); const std::string dst_dir = hdd0_disc + sfb_dir.substr(hdd0_game.size()); // Move and retry from correct location @@ -1483,6 +1506,23 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool else if (m_cat != "DG" && m_cat != "GD") { // Don't need /dev_bdvd + + if (!m_title_id.empty() && !from_hdd0_game && m_cat == "HG") + { + // Add HG games not in HDD0 to games.yml + games[m_title_id] = m_sfo_dir; + YAML::Emitter out; + out << games; + + fs::pending_file temp(fs::get_config_dir() + "/games.yml"); + + if (!temp.file || temp.file.write(out.c_str(), out.size()), !temp.commit()) + { + sys_log.error("Failed to save HG game location of title '%s' (%s)", m_title_id, fs::g_tls_error); + } + + vfs::mount("/dev_hdd0/game/" + m_title_id, m_sfo_dir + '/'); + } } else if (m_cat == "DG" && from_hdd0_game && disc.empty()) { @@ -1812,6 +1852,12 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool argv[0] = "/dev_flash" + resolved_path.substr(GetCallbacks().resolve_path(g_cfg_vfs.get_dev_flash()).size()); m_dir = fs::get_parent_dir(argv[0]) + '/'; } + else if (!m_title_id.empty() && m_cat == "HG") + { + m_dir = "/dev_hdd0/game/" + m_title_id + '/'; + argv[0] = m_dir + unescape(resolved_path.substr(GetCallbacks().resolve_path(m_sfo_dir).size())); + sys_log.notice("Boot path: %s", m_dir); + } else if (g_cfg.vfs.host_root) { // For homebrew @@ -2899,6 +2945,61 @@ std::set Emulator::GetGameDirs() const return dirs; } +void Emulator::AddGamesFromDir(const std::string& path) +{ + if (!IsStopped()) + return; + + const std::string games_yml = fs::get_cache_dir() + "/games.yml"; + + std::string content_before, content_after; + + if (fs::file fd{games_yml}) + { + content_before = fd.to_string(); + } + + // search dropped path first or else the direct parent to an elf is wrongly skipped + if (const auto error = BootGame(path, "", false, true); error == game_boot_result::no_errors) + { + if (fs::file fd{games_yml, fs::read + fs::isfile}) + { + content_after = fd.to_string(); + } + + if (content_before != content_after) + { + sys_log.notice("Registered game directory: %s", path); + content_before = content_after; + } + } + + // search direct subdirectories, that way we can drop one folder containing all games + for (auto&& dir_entry : fs::dir(path)) + { + if (!dir_entry.is_directory || dir_entry.name == "." || dir_entry.name == "..") + { + continue; + } + + const std::string dir_path = path + '/' + dir_entry.name; + + if (const auto error = BootGame(dir_path, "", false, true); error == game_boot_result::no_errors) + { + if (fs::file fd{games_yml, fs::read + fs::isfile}) + { + content_after = fd.to_string(); + } + + if (content_before != content_after) + { + sys_log.notice("Registered game directory: %s", dir_path); + content_before = content_after; + } + } + } +} + bool Emulator::IsPathInsideDir(std::string_view path, std::string_view dir) const { const std::string dir_path = GetCallbacks().resolve_path(dir); diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 5ab1412dc1..d2b8caa365 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -329,6 +329,7 @@ public: void ConfigurePPUCache() const; std::set GetGameDirs() const; + void AddGamesFromDir(const std::string& path); // Check if path is inside the specified directory bool IsPathInsideDir(std::string_view path, std::string_view dir) const; diff --git a/rpcs3/Loader/PSF.cpp b/rpcs3/Loader/PSF.cpp index 956926737b..6144c10345 100644 --- a/rpcs3/Loader/PSF.cpp +++ b/rpcs3/Loader/PSF.cpp @@ -251,6 +251,14 @@ namespace psf PSF_CHECK(false, corrupt); } + const auto tid = get_string(pair.sfo, "TITLE_ID", ""); + + if (std::find_if(tid.begin(), tid.end(), [](char ch){ return !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')); }) != tid.end()) + { + psf_log.error("Invalid title ID ('%s')", tid); + PSF_CHECK(false, corrupt); + } + #undef PSF_CHECK return pair; } diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 1d56ad30b6..07603c786b 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -449,6 +449,11 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) m_notes.clear(); m_games.pop_all(); + if (Emu.IsStopped()) + { + Emu.AddGamesFromDir(fs::get_config_dir() + "/games"); + } + const std::string _hdd = rpcs3::utils::get_hdd0_dir(); const auto add_disc_dir = [&](const std::string& path) @@ -531,7 +536,14 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) game_dir.resize(game_dir.find_last_not_of('/') + 1); - if (fs::is_file(game_dir + "/PS3_DISC.SFB")) + if (game_dir.empty()) + { + continue; + } + + const bool has_sfo = fs::is_file(game_dir + "/PARAM.SFO"); + + if (!has_sfo && fs::is_file(game_dir + "/PS3_DISC.SFB")) { // Check if a path loaded from games.yml is already registered in add_dir(_hdd + "disc/"); if (game_dir.starts_with(_hdd)) @@ -554,9 +566,13 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) add_disc_dir(game_dir); } + else if (has_sfo) + { + m_path_list.emplace_back(game_dir); + } else { - game_list_log.trace("Invalid disc path registered for %s: %s", pair.first.Scalar(), pair.second.Scalar()); + game_list_log.trace("Invalid game path registered for %s: %s", pair.first.Scalar(), pair.second.Scalar()); } } diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 4b65a32777..9d4e34370d 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2871,27 +2871,11 @@ Add valid disc games to gamelist (games.yml) void main_window::AddGamesFromDir(const QString& path) { if (!QFileInfo(path).isDir()) + { return; - - const std::string s_path = sstr(path); - - // search dropped path first or else the direct parent to an elf is wrongly skipped - if (const auto error = Emu.BootGame(s_path, "", false, true); error == game_boot_result::no_errors) - { - gui_log.notice("Returned from game addition by drag and drop: %s", s_path); } - // search direct subdirectories, that way we can drop one folder containing all games - QDirIterator dir_iter(path, QDir::Dirs | QDir::NoDotAndDotDot); - while (dir_iter.hasNext()) - { - const std::string dir_path = sstr(dir_iter.next()); - - if (const auto error = Emu.BootGame(dir_path, "", false, true); error == game_boot_result::no_errors) - { - gui_log.notice("Returned from game addition by drag and drop: %s", dir_path); - } - } + Emu.AddGamesFromDir(sstr(path)); } /** diff --git a/rpcs3/rpcs3qt/vfs_dialog_path_widget.cpp b/rpcs3/rpcs3qt/vfs_dialog_path_widget.cpp index 9e188eb632..41d0cfe291 100644 --- a/rpcs3/rpcs3qt/vfs_dialog_path_widget.cpp +++ b/rpcs3/rpcs3qt/vfs_dialog_path_widget.cpp @@ -43,7 +43,7 @@ vfs_dialog_path_widget::vfs_dialog_path_widget(const QString& name, const QStrin QHBoxLayout* selected_config_layout = new QHBoxLayout; m_selected_config_label = new QLabel(current_path.isEmpty() ? EmptyPath : current_path); - selected_config_layout->addWidget(new QLabel(tr("%0 directory:").arg(name))); + selected_config_layout->addWidget(new QLabel(tr("Used %0 directory:").arg(name))); selected_config_layout->addWidget(m_selected_config_label); selected_config_layout->addStretch(); selected_config_layout->addWidget(add_directory_button);