From 8b74ea8757889eed03e3e11975e78ceb9b2a4faa Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 10 Mar 2026 20:20:12 +0100 Subject: [PATCH] Qt: Add steam shortcuts --- rpcs3/rpcs3.cpp | 2 +- rpcs3/rpcs3.vcxproj | 2 + rpcs3/rpcs3.vcxproj.filters | 6 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/debugger_frame.cpp | 2 +- rpcs3/rpcs3qt/game_list_actions.cpp | 23 +- rpcs3/rpcs3qt/game_list_context_menu.cpp | 27 + rpcs3/rpcs3qt/main_window.cpp | 95 +-- rpcs3/rpcs3qt/main_window.h | 3 +- rpcs3/rpcs3qt/pkg_install_dialog.cpp | 12 + rpcs3/rpcs3qt/pkg_install_dialog.h | 2 + rpcs3/rpcs3qt/shortcut_utils.cpp | 125 +++- rpcs3/rpcs3qt/shortcut_utils.h | 1 + rpcs3/rpcs3qt/steam_utils.cpp | 807 +++++++++++++++++++++++ rpcs3/rpcs3qt/steam_utils.h | 104 +++ 15 files changed, 1136 insertions(+), 76 deletions(-) create mode 100644 rpcs3/rpcs3qt/steam_utils.cpp create mode 100644 rpcs3/rpcs3qt/steam_utils.h diff --git a/rpcs3/rpcs3.cpp b/rpcs3/rpcs3.cpp index 6c2130eca6..cb2058874e 100644 --- a/rpcs3/rpcs3.cpp +++ b/rpcs3/rpcs3.cpp @@ -131,7 +131,7 @@ std::set get_one_drive_paths() do { path_buffer.resize(path_buffer.size() + MAX_PATH); - DWORD buffer_size = static_cast(path_buffer.size() - 1); + DWORD buffer_size = static_cast((path_buffer.size() - 1) * sizeof(wchar_t)); status = RegQueryValueExW(hkey, L"UserFolder", NULL, &type, reinterpret_cast(path_buffer.data()), &buffer_size); } while (status == ERROR_MORE_DATA); diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index cd5d7c8bc4..5401efb323 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -891,6 +891,7 @@ + @@ -1719,6 +1720,7 @@ "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\libsdl-org\SDL\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 6c7841b4fc..d9ebc34ef6 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1278,6 +1278,9 @@ Gui\settings + + Gui\utils + @@ -1523,6 +1526,9 @@ Gui\settings + + Gui\utils + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 6fe8d9db13..5c330c7ae4 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -104,6 +104,7 @@ add_library(rpcs3_ui STATIC shortcut_settings.cpp skylander_dialog.cpp sound_effect_manager_dialog.cpp + steam_utils.cpp syntax_highlighter.cpp system_cmd_dialog.cpp table_item_delegate.cpp diff --git a/rpcs3/rpcs3qt/debugger_frame.cpp b/rpcs3/rpcs3qt/debugger_frame.cpp index f1dd9cf892..0859e19856 100644 --- a/rpcs3/rpcs3qt/debugger_frame.cpp +++ b/rpcs3/rpcs3qt/debugger_frame.cpp @@ -1093,7 +1093,7 @@ void debugger_frame::UpdateUnitList() idm::select>(on_select, idm::unlocked); } - m_hw_ppu_idx = cpu_list.size() + 1; // Account for NoThreadString thread + m_hw_ppu_idx = ::size32(cpu_list) + 1; // Account for NoThreadString thread for (u32 i = 0; i < g_cfg.core.ppu_threads + 0u; i++) { diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 5e3e305860..af295b5176 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -1496,13 +1496,6 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con gameid_token_value = gameinfo->info.serial; } -#ifdef __linux__ - const std::string target_cli_args = gameinfo->info.path.starts_with(dev_flash) ? fmt::format("--no-gui \"%%%%RPCS3_VFS%%%%:dev_flash/%s\"", gameinfo->info.path.substr(dev_flash.size())) - : fmt::format("--no-gui \"%%%%RPCS3_GAMEID%%%%:%s\"", gameid_token_value); -#else - const std::string target_cli_args = gameinfo->info.path.starts_with(dev_flash) ? fmt::format("--no-gui \"%%RPCS3_VFS%%:dev_flash/%s\"", gameinfo->info.path.substr(dev_flash.size())) - : fmt::format("--no-gui \"%%RPCS3_GAMEID%%:%s\"", gameid_token_value); -#endif const std::string target_icon_dir = fmt::format("%sIcons/game_icons/%s/", fs::get_config_dir(), gameinfo->info.serial); if (!fs::create_path(target_icon_dir)) @@ -1512,7 +1505,11 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con continue; } - for (const gui::utils::shortcut_location& location : locations) + const bool is_vsh = gameinfo->info.path.starts_with(dev_flash); + const std::string cli_arg_token = is_vsh ? "RPCS3_VFS" : "RPCS3_GAMEID"; + const std::string cli_arg_value = is_vsh ? ("dev_flash/" + gameinfo->info.path.substr(dev_flash.size())) : gameid_token_value; + + for (gui::utils::shortcut_location location : locations) { std::string destination; @@ -1524,6 +1521,9 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con case gui::utils::shortcut_location::applications: destination = "application menu"; break; + case gui::utils::shortcut_location::steam: + destination = "Steam"; + break; #ifdef _WIN32 case gui::utils::shortcut_location::rpcs3_shortcuts: destination = "/games/shortcuts/"; @@ -1531,6 +1531,13 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con #endif } +#ifdef __linux__ + const std::string percent = location == gui::utils::shortcut_location::steam ? "%" : "%%"; +#else + const std::string percent = "%"; +#endif + const std::string target_cli_args = fmt::format("--no-gui \"%s%s%s:%s\"", percent, cli_arg_token, percent, cli_arg_value); + if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->icon_in_archive ? gameinfo->info.path : "", gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, location)) { game_list_log.success("Created %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 720aac8596..81f6f6e973 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -6,6 +6,7 @@ #include "input_dialog.h" #include "qt_utils.h" #include "shortcut_utils.h" +#include "steam_utils.h" #include "settings_dialog.h" #include "pad_settings_dialog.h" #include "patch_manager_dialog.h" @@ -273,6 +274,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& { m_game_list_actions->CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::desktop}); }); + #ifdef _WIN32 QAction* create_start_menu_shortcut = manage_game_menu->addAction(tr("&Create Start Menu Shortcut")); #elif defined(__APPLE__) @@ -285,6 +287,17 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& m_game_list_actions->CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::applications}); }); + if (gui::utils::steam_shortcut::steam_installed()) + { + const bool steam_running = gui::utils::steam_shortcut::is_steam_running(); + QAction* create_steam_shortcut = manage_game_menu->addAction(steam_running ? tr("&Create Steam Shortcut (Steam must be closed)") : tr("&Create Steam Shortcut")); + connect(create_steam_shortcut, &QAction::triggered, this, [this, gameinfo]() + { + m_game_list_actions->CreateShortcuts({gameinfo}, {gui::utils::shortcut_location::steam}); + }); + create_steam_shortcut->setEnabled(!steam_running); + } + manage_game_menu->addSeparator(); // Hide/rename game in game list @@ -843,6 +856,20 @@ void game_list_context_menu::show_multi_selection_context_menu(const std::vector m_game_list_actions->CreateShortcuts(games, {gui::utils::shortcut_location::applications}); }); + if (gui::utils::steam_shortcut::steam_installed()) + { + const bool steam_running = gui::utils::steam_shortcut::is_steam_running(); + QAction* create_steam_shortcut = manage_game_menu->addAction(steam_running ? tr("&Create Steam Shortcut (Steam must be closed)") : tr("&Create Steam Shortcut")); + connect(create_steam_shortcut, &QAction::triggered, this, [this, games]() + { + if (QMessageBox::question(m_game_list_frame, tr("Confirm Creation"), tr("Create Steam shortcut?")) != QMessageBox::Yes) + return; + + m_game_list_actions->CreateShortcuts(games, {gui::utils::shortcut_location::steam}); + }); + create_steam_shortcut->setEnabled(!steam_running); + } + manage_game_menu->addSeparator(); // Hide game in game list diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 687358d625..1b77f8f2b1 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -35,9 +35,9 @@ #include "camera_settings_dialog.h" #include "ps_move_tracker_dialog.h" #include "ipc_settings_dialog.h" -#include "shortcut_utils.h" #include "config_checker.h" #include "shortcut_dialog.h" +#include "steam_utils.h" #include "system_cmd_dialog.h" #include "emulated_pad_settings_dialog.h" #include "emulated_logitech_g27_settings_dialog.h" @@ -935,9 +935,8 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo } std::vector packages; + std::set shortcut_locations; bool precompile_caches = false; - bool create_desktop_shortcuts = false; - bool create_app_shortcut = false; bool canceled = false; game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr; @@ -954,8 +953,15 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo packages = dlg.get_paths_to_install(); precompile_caches = dlg.precompile_caches(); - create_desktop_shortcuts = dlg.create_desktop_shortcuts(); - create_app_shortcut = dlg.create_app_shortcut(); + + if (dlg.create_desktop_shortcuts()) + shortcut_locations.insert(gui::utils::shortcut_location::desktop); + + if (dlg.create_app_shortcut()) + shortcut_locations.insert(gui::utils::shortcut_location::applications); + + if (dlg.create_steam_shortcut()) + shortcut_locations.insert(gui::utils::shortcut_location::steam); }); dlg.exec(); @@ -1136,7 +1142,7 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo if (!bootable_paths_installed.empty()) { - m_game_list_frame->AddRefreshedSlot([this, create_desktop_shortcuts, precompile_caches, create_app_shortcut, paths = std::move(bootable_paths_installed)](std::set& claimed_paths) mutable + m_game_list_frame->AddRefreshedSlot([this, shortcut_locations, precompile_caches, paths = std::move(bootable_paths_installed)](std::set& claimed_paths) mutable { // Try to claim operations on ID for (auto it = paths.begin(); it != paths.end();) @@ -1154,7 +1160,7 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo } } - CreateShortCuts(paths, create_desktop_shortcuts, create_app_shortcut); + CreateShortCuts(paths, shortcut_locations); if (precompile_caches) { @@ -2370,6 +2376,7 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri #else QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)")); #endif + QLabel* label = new QLabel(tr("%1\nWould you like to precompile caches and install shortcuts to the installed software? (%2 new software detected)\n\n").arg(message).arg(bootable_paths.size()), dlg); vlayout->addWidget(label); @@ -2381,6 +2388,17 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri vlayout->addWidget(quick_check); vlayout->addStretch(3); + QCheckBox* steam_check = nullptr; + if (gui::utils::steam_shortcut::steam_installed()) + { + const bool steam_running = gui::utils::steam_shortcut::is_steam_running(); + steam_check = new QCheckBox(steam_running ? tr("Add Steam Shortcut(s) (Steam must be closed)") : tr("Add Steam shortcut(s)")); + steam_check->setEnabled(!steam_running); + + vlayout->addWidget(steam_check); + vlayout->addStretch(3); + } + QDialogButtonBox* btn_box = new QDialogButtonBox(QDialogButtonBox::Ok); vlayout->addWidget(btn_box); @@ -2389,13 +2407,22 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri connect(btn_box, &QDialogButtonBox::accepted, this, [=, this, paths = std::move(bootable_paths)]() { const bool precompile_caches = precompile_check->isChecked(); - const bool create_desktop_shortcuts = desk_check->isChecked(); - const bool create_app_shortcut = quick_check->isChecked(); + + std::set shortcut_locations; + + if (desk_check->isChecked()) + shortcut_locations.insert(gui::utils::shortcut_location::desktop); + + if (quick_check->isChecked()) + shortcut_locations.insert(gui::utils::shortcut_location::applications); + + if (steam_check && steam_check->isChecked()) + shortcut_locations.insert(gui::utils::shortcut_location::steam); dlg->hide(); dlg->accept(); - CreateShortCuts(paths, create_desktop_shortcuts, create_app_shortcut); + CreateShortCuts(paths, shortcut_locations); if (precompile_caches) { @@ -2407,52 +2434,40 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri dlg->open(); } -void main_window::CreateShortCuts(const std::map& paths, bool create_desktop_shortcuts, bool create_app_shortcut) +void main_window::CreateShortCuts(const std::map& paths, std::set locations) { if (paths.empty()) return; - std::set locations; - #ifdef _WIN32 locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts); +#else + if (locations.empty()) + { + return; + } #endif - if (create_desktop_shortcuts) - { - locations.insert(gui::utils::shortcut_location::desktop); - } - if (create_app_shortcut) - { - locations.insert(gui::utils::shortcut_location::applications); - } + std::vector game_data_shortcuts; - if (!locations.empty()) + for (const auto& [boot_path, title_id] : paths) { - std::vector game_data_shortcuts; - - for (const auto& [boot_path, title_id] : paths) + for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) { - for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) + if (gameinfo && gameinfo->info.serial == title_id.toStdString()) { - if (gameinfo && gameinfo->info.serial == title_id.toStdString()) + if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) { - if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) - { - if (!locations.empty()) - { - game_data_shortcuts.push_back(gameinfo); - } - } - - break; + game_data_shortcuts.push_back(gameinfo); } + + break; } } + } - if (!game_data_shortcuts.empty() && !locations.empty()) - { - m_game_list_frame->actions()->CreateShortcuts(game_data_shortcuts, locations); - } + if (!game_data_shortcuts.empty()) + { + m_game_list_frame->actions()->CreateShortcuts(game_data_shortcuts, locations); } } diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index 588c6e6918..7f0bb8e2ea 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -10,6 +10,7 @@ #include "update_manager.h" #include "settings.h" #include "shortcut_handler.h" +#include "shortcut_utils.h" #include "Emu/config_mode.h" #include "Emu/System.h" @@ -145,7 +146,7 @@ private: static bool InstallFileInExData(const std::string& extension, const QString& path, const std::string& filename); bool HandlePackageInstallation(QStringList file_paths, bool from_boot); - void CreateShortCuts(const std::map& paths, bool create_desktop_shortcuts, bool create_app_shortcut); + void CreateShortCuts(const std::map& paths, std::set locations); void HandlePupInstallation(const QString& file_path, const QString& dir_path = ""); void ExtractPup(); diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.cpp b/rpcs3/rpcs3qt/pkg_install_dialog.cpp index 9b0f926484..3d4193d4e5 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.cpp +++ b/rpcs3/rpcs3qt/pkg_install_dialog.cpp @@ -3,6 +3,7 @@ #include "numbered_widget_item.h" #include "richtext_item_delegate.h" #include "qt_utils.h" +#include "steam_utils.h" #include "Emu/system_utils.hpp" #include "Utilities/File.h" @@ -194,6 +195,17 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil vbox->addWidget(precompile_check); vbox->addWidget(desk_check); vbox->addWidget(quick_check); + + if (gui::utils::steam_shortcut::steam_installed()) + { + const bool steam_running = gui::utils::steam_shortcut::is_steam_running(); + QCheckBox* steam_check = new QCheckBox(steam_running ? tr("Add Steam Shortcut(s) (Steam must be closed)") : tr("Add Steam shortcut(s)")); + connect(steam_check, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ m_create_steam_shortcut = state != Qt::CheckState::Unchecked; }); + steam_check->setEnabled(!steam_running); + + vbox->addWidget(steam_check); + } + vbox->addWidget(installation_info); vbox->addWidget(buttons); diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.h b/rpcs3/rpcs3qt/pkg_install_dialog.h index f1a623009d..b1f3dbeb9d 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.h +++ b/rpcs3/rpcs3qt/pkg_install_dialog.h @@ -22,6 +22,7 @@ public: bool precompile_caches() const { return m_precompile_caches; } bool create_desktop_shortcuts() const { return m_create_desktop_shortcuts; } bool create_app_shortcut() const { return m_create_app_shortcut; } + bool create_steam_shortcut() const { return m_create_steam_shortcut; } private: void update_info(QLabel* installation_info, QDialogButtonBox* buttons) const; @@ -31,4 +32,5 @@ private: bool m_precompile_caches = false; bool m_create_desktop_shortcuts = false; bool m_create_app_shortcut = false; + bool m_create_steam_shortcut = false; }; diff --git a/rpcs3/rpcs3qt/shortcut_utils.cpp b/rpcs3/rpcs3qt/shortcut_utils.cpp index 2bc2bfc578..8194bd184b 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.cpp +++ b/rpcs3/rpcs3qt/shortcut_utils.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "shortcut_utils.h" +#include "steam_utils.h" #include "qt_utils.h" #include "Emu/VFS.h" #include "Utilities/File.h" @@ -30,6 +31,25 @@ LOG_CHANNEL(sys_log, "SYS"); +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + format_enum(out, arg, [](gui::utils::shortcut_location value) + { + switch (value) + { + case gui::utils::shortcut_location::desktop: return "desktop"; + case gui::utils::shortcut_location::applications: return "applications"; + case gui::utils::shortcut_location::steam: return "steam"; +#ifdef _WIN32 + case gui::utils::shortcut_location::rpcs3_shortcuts: return "rpcs3"; +#endif + } + + return unknown; + }); +} + namespace gui::utils { #ifdef _WIN32 @@ -109,6 +129,7 @@ namespace gui::utils } std::string link_path; + bool append_rpcs3 = false; switch (location) { @@ -117,6 +138,13 @@ namespace gui::utils break; case shortcut_location::applications: link_path = QStandardPaths::writableLocation(QStandardPaths::StandardLocation::ApplicationsLocation).toStdString(); + append_rpcs3 = true; + break; + case shortcut_location::steam: +#ifdef __APPLE__ + link_path = QStandardPaths::writableLocation(QStandardPaths::StandardLocation::ApplicationsLocation).toStdString(); + append_rpcs3 = true; +#endif break; #ifdef _WIN32 case shortcut_location::rpcs3_shortcuts: @@ -126,13 +154,13 @@ namespace gui::utils #endif } - if (!fs::is_dir(link_path) && !fs::create_dir(link_path)) + if (!link_path.empty() && !fs::is_dir(link_path) && !fs::create_dir(link_path)) { sys_log.error("Failed to create shortcut. Folder does not exist: %s", link_path); return false; } - if (location == shortcut_location::applications) + if (append_rpcs3) { link_path += "/RPCS3"; @@ -144,6 +172,29 @@ namespace gui::utils } #ifdef _WIN32 + const std::string working_dir{fs::get_executable_dir()}; + const std::string rpcs3_path{fs::get_executable_path()}; + std::string target_icon_path; + + if (!src_icon_path.empty() && !target_icon_dir.empty()) + { + if (!create_square_shortcut_icon_file(path, src_icon_path, target_icon_dir, target_icon_path, 512)) + { + sys_log.error("Failed to create shortcut: .ico creation failed"); + return false; + } + } + + if (location == shortcut_location::steam) + { + sys_log.notice("Creating %s shortcut with arguments '%s' and icon path '%s'", location, target_cli_args, target_icon_path); + steam_shortcut steam_sc{}; + steam_sc.add_shortcut(simple_name, rpcs3_path, working_dir, target_cli_args, target_icon_path); + return steam_sc.write_file(); + } + + sys_log.notice("Creating %s shortcut '%s' with arguments '%s' and .ico dir '%s'", location, link_path, target_cli_args, target_icon_dir); + const auto str_error = [](HRESULT hr) -> std::string { _com_error err(hr); @@ -153,8 +204,6 @@ namespace gui::utils fmt::append(link_path, "/%s.lnk", simple_name); - sys_log.notice("Creating shortcut '%s' with arguments '%s' and .ico dir '%s'", link_path, target_cli_args, target_icon_dir); - // https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32 HRESULT res = CoInitialize(NULL); if (FAILED(res)) @@ -177,9 +226,6 @@ namespace gui::utils if (FAILED(res)) return cleanup(false, "CoCreateInstance failed"); - const std::string working_dir{ fs::get_executable_dir() }; - const std::string rpcs3_path{ working_dir + "rpcs3.exe" }; - const std::wstring w_target_file = utf8_to_wchar(rpcs3_path); res = pShellLink->SetPath(w_target_file.c_str()); if (FAILED(res)) @@ -206,12 +252,8 @@ namespace gui::utils return cleanup(false, fmt::format("SetDescription failed (%s)", str_error(res))); } - if (!src_icon_path.empty() && !target_icon_dir.empty()) + if (!target_icon_path.empty()) { - std::string target_icon_path; - if (!create_square_shortcut_icon_file(path, src_icon_path, target_icon_dir, target_icon_path, 512)) - return cleanup(false, ".ico creation failed"); - const std::wstring w_icon_path = utf8_to_wchar(target_icon_path); res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0); if (FAILED(res)) @@ -241,9 +283,10 @@ namespace gui::utils return cleanup(true, {}); #elif defined(__APPLE__) + fmt::append(link_path, "/%s.app", simple_name); - sys_log.notice("Creating shortcut '%s' with arguments '%s'", link_path, target_cli_args); + sys_log.notice("Creating %s shortcut '%s' with arguments '%s'", location, link_path, target_cli_args); const std::string contents_dir = link_path + "/Contents/"; const std::string macos_dir = contents_dir + "MacOS/"; @@ -320,9 +363,10 @@ namespace gui::utils } plist_file.close(); + std::string target_icon_path; if (!src_icon_path.empty()) { - std::string target_icon_path = resources_dir; + target_icon_path = resources_dir; if (!create_square_shortcut_icon_file(path, src_icon_path, resources_dir, target_icon_path, 512)) { // Error is logged in create_square_shortcut_icon_file @@ -330,10 +374,16 @@ namespace gui::utils } } + if (location == shortcut_location::steam) + { + steam_shortcut steam_sc{}; + steam_sc.add_shortcut(simple_name, launcher_path, macos_dir, ""/*target_cli_args are already in the launcher*/, target_icon_path); + return steam_sc.write_file(); + } + return true; #else - const std::string exe_path = fs::get_executable_path(); if (exe_path.empty()) { @@ -341,9 +391,28 @@ namespace gui::utils return false; } + std::string target_icon_path; + if (!src_icon_path.empty() && !target_icon_dir.empty()) + { + if (!create_square_shortcut_icon_file(path, src_icon_path, target_icon_dir, target_icon_path, 512)) + { + // Error is logged in create_square_shortcut_icon_file + return false; + } + } + + if (location == shortcut_location::steam) + { + sys_log.notice("Creating %s shortcut with arguments '%s' and icon path '%s'", location, target_cli_args, target_icon_path); + const std::string working_dir{fs::get_executable_dir()}; + steam_shortcut steam_sc{}; + steam_sc.add_shortcut(simple_name, exe_path, working_dir, target_cli_args, target_icon_path); + return steam_sc.write_file(); + } + fmt::append(link_path, "/%s.desktop", simple_name); - sys_log.notice("Creating shortcut '%s' for '%s' with arguments '%s'", link_path, exe_path, target_cli_args); + sys_log.notice("Creating %s shortcut '%s' for '%s' with arguments '%s'", location, link_path, exe_path, target_cli_args); std::string file_content; fmt::append(file_content, "[Desktop Entry]\n"); @@ -360,15 +429,8 @@ namespace gui::utils fmt::append(file_content, "Comment=%s\n", QString::fromStdString(description).simplified()); } - if (!src_icon_path.empty() && !target_icon_dir.empty()) + if (!target_icon_path.empty()) { - std::string target_icon_path; - if (!create_square_shortcut_icon_file(path, src_icon_path, target_icon_dir, target_icon_path, 512)) - { - // Error is logged in create_square_shortcut_icon_file - return false; - } - fmt::append(file_content, "Icon=%s\n", target_icon_path); } @@ -438,7 +500,8 @@ namespace gui::utils std::vector locations = { shortcut_location::desktop, - shortcut_location::applications + shortcut_location::applications, + shortcut_location::steam, }; #ifdef _WIN32 locations.push_back(shortcut_location::rpcs3_shortcuts); @@ -457,6 +520,18 @@ namespace gui::utils link_path = QStandardPaths::writableLocation(QStandardPaths::StandardLocation::ApplicationsLocation).toStdString(); link_path += "/RPCS3"; break; + case shortcut_location::steam: + { + const std::string exe_path = fs::get_executable_path(); + const std::string working_dir = fs::get_executable_dir(); + steam_shortcut steam_sc{}; + steam_sc.remove_shortcut(simple_name, exe_path, working_dir); + if (!steam_sc.write_file()) + { + sys_log.error("Failed to remove steam shortcut for '%s'", simple_name); + } + continue; + } #ifdef _WIN32 case shortcut_location::rpcs3_shortcuts: link_path = rpcs3::utils::get_games_shortcuts_dir(); diff --git a/rpcs3/rpcs3qt/shortcut_utils.h b/rpcs3/rpcs3qt/shortcut_utils.h index 4e7fcdf0ce..2b5d302710 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.h +++ b/rpcs3/rpcs3qt/shortcut_utils.h @@ -6,6 +6,7 @@ namespace gui::utils { desktop, applications, + steam, #ifdef _WIN32 rpcs3_shortcuts, #endif diff --git a/rpcs3/rpcs3qt/steam_utils.cpp b/rpcs3/rpcs3qt/steam_utils.cpp new file mode 100644 index 0000000000..24ceec273e --- /dev/null +++ b/rpcs3/rpcs3qt/steam_utils.cpp @@ -0,0 +1,807 @@ +#include "stdafx.h" +#include "steam_utils.h" + +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +LOG_CHANNEL(sys_log, "SYS"); + +namespace gui::utils +{ + void steam_shortcut::add_shortcut( + const std::string& app_name, + const std::string& exe, + const std::string& start_dir, + const std::string& launch_options, + const std::string& icon_path) + { + shortcut_entry entry{}; + entry.app_name = app_name; + entry.exe = quote(fix_slashes(exe), true); + entry.start_dir = quote(fix_slashes(start_dir), false); + entry.launch_options = launch_options; + entry.icon = quote(fix_slashes(icon_path), false); + entry.appid = steam_appid(exe, app_name); + + m_entries_to_add.push_back(std::move(entry)); + } + + void steam_shortcut::remove_shortcut( + const std::string& app_name, + const std::string& exe, + const std::string& start_dir) + { + shortcut_entry entry{}; + entry.app_name = app_name; + entry.exe = quote(fix_slashes(exe), true); + entry.start_dir = quote(fix_slashes(start_dir), false); + entry.appid = steam_appid(exe, app_name); + + m_entries_to_remove.push_back(std::move(entry)); + } + + bool steam_shortcut::parse_file(const std::string& path) + { + m_vdf_entries.clear(); + + fs::file vdf(path); + if (!vdf) + { + sys_log.error("Failed to parse steam shortcut file '%s': %s", path, fs::g_tls_error); + return false; + } + + const std::vector data = vdf.to_vector(); + usz last_pos = 0; + usz pos = 0; + + const auto read_type_id = [&]() -> u8 + { + if (pos >= data.size()) + { + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: read_type_id: end of file", path, pos); + return umax; + } + + last_pos = pos; + return data[pos++]; + }; + + const auto read_u32 = [&]() -> std::optional + { + if ((pos + sizeof(u32)) > data.size()) + { + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: read_u32: end of file", path, pos); + return {}; + } + + last_pos = pos; + + u32 v {}; + std::memcpy(&v, &data[pos], sizeof(u32)); + pos += sizeof(u32); + return v; + }; + + const auto read_string = [&]() -> std::optional + { + if (pos >= data.size()) + { + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: read_string: end of file", path, pos); + return {}; + } + + last_pos = pos; + + std::string str; + + while (pos < data.size()) + { + const u8 c = data[pos++]; + if (!c) break; + str += c; + } + + if (pos >= data.size()) + { + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: read_string: not null terminated", path, last_pos); + return {}; + } + + return str; + }; + + #define CHECK_VDF(cond, msg) \ + if (!(cond)) \ + { \ + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: %s", path, last_pos, msg); \ + return false; \ + } + + #define READ_VDF_STRING(name) \ + const std::optional name##_opt = read_string(); \ + if (!name##_opt.has_value()) return false; \ + std::string name = name##_opt.value(); + + #define READ_VDF_U32(name) \ + const std::optional name##_opt = read_u32(); \ + if (!name##_opt.has_value()) return false; \ + const u32 name = name##_opt.value(); + + CHECK_VDF(read_type_id() == type_id::Start, "expected type_id::Start for shortcuts"); + READ_VDF_STRING(shortcuts); + CHECK_VDF(shortcuts == "shortcuts", "expected 'shortcuts' key"); + + for (usz index = 0; true; index++) + { + vdf_shortcut_entry entry {}; + + u8 type = read_type_id(); + if (type == type_id::End) + { + // End of shortcuts + break; + } + + CHECK_VDF(type == type_id::Start, "expected type_id::Start for entry"); + + READ_VDF_STRING(entry_index_str); + u64 entry_index = 0; + CHECK_VDF(try_to_uint64(&entry_index, entry_index_str, 0, umax), "failed to convert entry index"); + CHECK_VDF(entry_index == index, "unexpected entry index"); + + type = umax; + while (type != type_id::Start) + { + type = read_type_id(); + + switch (type) + { + case type_id::String: + { + READ_VDF_STRING(key); + READ_VDF_STRING(value); + CHECK_VDF(!key.empty(), "key is empty"); + entry.values.push_back({std::move(key), std::move(value)}); + break; + } + case type_id::Integer: + { + READ_VDF_STRING(key); + READ_VDF_U32(value); + CHECK_VDF(!key.empty(), "key is empty"); + entry.values.push_back({std::move(key), value}); + break; + } + case type_id::Start: + // Expect tags next + break; + default: + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: unexpected type id 0x%x", path, last_pos, type); + return false; + } + } + + CHECK_VDF(type == type_id::Start, "expected type_id::Start for tags"); + READ_VDF_STRING(tags); + CHECK_VDF(tags == "tags", "key is empty"); + type = umax; + while (type != type_id::End) + { + type = read_type_id(); + + switch (type) + { + case type_id::String: + { + READ_VDF_STRING(key); + READ_VDF_STRING(value); + CHECK_VDF(!key.empty(), "key is empty"); + entry.tags.push_back({std::move(key), std::move(value)}); + break; + } + case type_id::End: + break; + default: + sys_log.error("Failed to parse steam shortcut file '%s' at pos 0x%x: unexpected type id 0x%x", path, last_pos, type); + return false; + } + } + CHECK_VDF(type == type_id::End, "expected type_id::End for tags"); + + CHECK_VDF(read_type_id() == type_id::End, "expected type_id::End for entry"); + + m_vdf_entries.push_back(std::move(entry)); + } + + CHECK_VDF(read_type_id() == type_id::End, "expected type_id::End for end of file"); + CHECK_VDF(pos == data.size(), fmt::format("bytes found at end of file (pos=%d, size=%d)", pos, data.size())); + + #undef CHECK_VDF_OPT + #undef CHECK_VDF + return true; + } + + bool steam_shortcut::write_file() + { + if (m_entries_to_add.empty() && m_entries_to_remove.empty()) + { + sys_log.error("Failed to create steam shortcut: No entries."); + return false; + } + + const std::string steam_path = get_steam_path(); + if (steam_path.empty()) + { + sys_log.error("Failed to create steam shortcut: Steam directory not found."); + return false; + } + + if (!fs::is_dir(steam_path)) + { + sys_log.error("Failed to create steam shortcut: '%s' not a directory.", steam_path); + return false; + } + + const std::string user_id = get_last_active_steam_user(steam_path); + if (user_id.empty()) + { + sys_log.error("Failed to create steam shortcut: last active user not found."); + return false; + } + + const std::string user_dir = steam_path + "/userdata/" + user_id + "/config/"; + if (!fs::is_dir(user_dir)) + { + sys_log.error("Failed to create steam shortcut: '%s' not a directory.", user_dir); + return false; + } + + if (is_steam_running()) + { + sys_log.error("Failed to create steam shortcut: steam is running."); + return false; + } + + const std::string file_path = user_dir + "shortcuts.vdf"; + const std::string backup_path = fs::get_config_dir() + "/shortcuts.vdf.backup"; + + if (fs::is_file(file_path)) + { + if (!fs::copy_file(file_path, backup_path, true)) + { + sys_log.error("Failed to backup steam shortcut file '%s'", file_path); + return false; + } + + if (!parse_file(file_path)) + { + sys_log.error("Failed to parse steam shortcut file '%s'", file_path); + return false; + } + } + + std::vector removed_entries; + + for (const shortcut_entry& entry : m_entries_to_remove) + { + bool removed_entry = false; + for (auto it = m_vdf_entries.begin(); it != m_vdf_entries.end();) + { + const auto appid = it->value("appid"); + const auto exe = it->value("Exe"); + const auto start_dir = it->value("StartDir"); + + if (appid.has_value() && appid.value() == entry.appid && + exe.has_value() && exe.value() == entry.exe && + start_dir.has_value() && start_dir.value() == entry.start_dir) + { + sys_log.notice("Removing steam shortcut for '%s'", entry.app_name); + it = m_vdf_entries.erase(it); + removed_entry = true; + } + else + { + it++; + } + } + + if (removed_entry) + { + removed_entries.push_back(entry); + } + + if (m_vdf_entries.empty()) + { + break; + } + } + + for (const vdf_shortcut_entry& entry : m_vdf_entries) + { + for (auto it = m_entries_to_add.begin(); it != m_entries_to_add.end();) + { + const auto appid = entry.value("appid"); + const auto exe = entry.value("Exe"); + const auto start_dir = entry.value("StartDir"); + const auto launch_options = entry.value("LaunchOptions"); + const auto icon = entry.value("icon"); + + if (appid.has_value() && appid.value() == it->appid && + exe.has_value() && exe.value() == it->exe && + start_dir.has_value() && start_dir.value() == it->start_dir && + launch_options.has_value() && launch_options.value() == it->launch_options && + icon.has_value() && icon.value() == it->icon) + { + sys_log.notice("Entry '%s' already exists in steam shortcut file '%s'.", it->app_name, file_path); + it = m_entries_to_add.erase(it); + } + else + { + it++; + } + } + + if (m_entries_to_add.empty()) + { + break; + } + } + + if (m_entries_to_add.empty() && removed_entries.empty()) + { + sys_log.notice("No matching entries found in steam shortcut file '%s'.", file_path); + return true; + } + + usz index = 0; + std::string content; + + content += type_id::Start; + append(content, "shortcuts"); + for (const vdf_shortcut_entry& entry : m_vdf_entries) + { + const auto val = entry.build_binary_blob(index++); + if (!val.has_value()) + { + sys_log.error("Failed to create steam shortcut '%s': '%s'", file_path, val.error()); + return false; + } + content += val.value(); + } + for (const shortcut_entry& entry : m_entries_to_add) + { + content += entry.build_binary_blob(index++); + } + content += type_id::End; + content += type_id::End; // End of file + + if (!fs::write_file(file_path, fs::rewrite, content)) + { + sys_log.error("Failed to create steam shortcut '%s': '%s'", file_path, fs::g_tls_error); + + if (!fs::copy_file(backup_path, file_path, true)) + { + sys_log.error("Failed to restore steam shortcuts backup: '%s'", fs::g_tls_error); + } + + return false; + } + + for (const shortcut_entry& entry : m_entries_to_add) + { + sys_log.success("Created steam shortcut for '%s'", entry.app_name); + } + for (const shortcut_entry& entry : removed_entries) + { + sys_log.success("Removed steam shortcut(s) for '%s'", entry.app_name); + } + return true; + } + + u32 steam_shortcut::crc32(const std::string& data) + { + u32 crc = 0xFFFFFFFF; + + for (u8 c : data) + { + crc ^= c; + + for (int i = 0; i < 8; i++) + { + crc = (crc >> 1) ^ (0xEDB88320 & -static_cast(crc & 1)); + } + } + + return ~crc; + } + + bool steam_shortcut::steam_installed() + { + const std::string path = get_steam_path(); + return !path.empty() && fs::is_dir(path); + } + + u32 steam_shortcut::steam_appid(const std::string& exe, const std::string& name) + { + return crc32(exe + name) | 0x80000000; + } + + void steam_shortcut::append(std::string& s, const std::string& val) + { + s += val; + s += '\0'; // append null terminator + } + + std::string steam_shortcut::quote(const std::string& s, bool force) + { + if (force || s.contains(" ")) + { + return "\"" + s + "\""; + } + + return s; + } + + std::string steam_shortcut::fix_slashes(const std::string& s) + { +#ifdef _WIN32 + return fmt::replace_all(s, "/", "\\"); +#else + return s; +#endif + } + + std::string steam_shortcut::kv_string(const std::string& key, const std::string& value) + { + std::string ret; + ret += type_id::String; + append(ret, key); + append(ret, value); + return ret; + } + + std::string steam_shortcut::kv_int(const std::string& key, u32 value) + { + std::string str; + str.reserve(64); + str += type_id::Integer; + append(str, key); + str += static_cast(value & 0xFF); + str += static_cast((value >> 8) & 0xFF); + str += static_cast((value >> 16) & 0xFF); + str += static_cast((value >> 24) & 0xFF); + return str; + } + + std::string steam_shortcut::shortcut_entry::build_binary_blob(usz index) const + { + std::string str; + str.reserve(1024); + str += type_id::Start; + + append(str, std::to_string(index)); + + str += kv_int("appid", appid); + str += kv_string("AppName", app_name); + str += kv_string("Exe", exe); + str += kv_string("StartDir", start_dir); + str += kv_string("icon", icon); + str += kv_string("ShortcutPath", ""); + str += kv_string("LaunchOptions", launch_options); + str += kv_int("IsHidden", 0); + str += kv_int("AllowDesktopConfig", 1); + str += kv_int("AllowOverlay", 1); + str += kv_int("OpenVR", 0); + str += kv_int("Devkit", 0); + str += kv_string("DevkitGameID", ""); + str += kv_int("DevkitOverrideAppID", 0); + str += kv_int("LastPlayTime", 0); + str += kv_string("FlatpakAppID", ""); + str += kv_string("sortas", ""); + + str += type_id::Start; + append(str, "tags"); + str += type_id::End; + + str += type_id::End; + return str; + } + + std::expected steam_shortcut::vdf_shortcut_entry::build_binary_blob(usz index) const + { + std::string str; + str.reserve(1024); + str += type_id::Start; + + append(str, std::to_string(index)); + + std::optional error = std::nullopt; + for (const auto& [key, value] : values) + { + std::visit([&key, &str, &error](const auto& value) + { + using T = std::decay_t; + + if constexpr (std::is_same_v) + { + str += kv_string(key, value); + } + else if constexpr (std::is_same_v) + { + str += kv_int(key, value); + } + else + { + error = fmt::format("vdf entry for key '%s' has unexpected type '%s'", key, typeid(value).name()); + } + }, + value); + + if (error.has_value()) + { + return std::unexpected(error.value()); + } + } + + str += type_id::Start; + append(str, "tags"); + for (const auto& [key, value] : tags) + { + str += kv_string(key, value); + } + str += type_id::End; + + str += type_id::End; + return str; + } + +#ifdef _WIN32 + std::string get_registry_string(const wchar_t* key, const wchar_t* name) + { + HKEY hkey = NULL; + LSTATUS status = RegOpenKeyW(HKEY_CURRENT_USER, key, &hkey); + if (status != ERROR_SUCCESS) + { + sys_log.trace("get_registry_string: RegOpenKeyW failed: %s (key='%s', name='%s')", fmt::win_error{static_cast(status), nullptr}, wchar_to_utf8(key), wchar_to_utf8(name)); + return ""; + } + + std::wstring path_buffer; + DWORD type = 0U; + do + { + path_buffer.resize(path_buffer.size() + MAX_PATH); + DWORD buffer_size = static_cast((path_buffer.size() - 1) * sizeof(wchar_t)); + status = RegQueryValueExW(hkey, name, NULL, &type, reinterpret_cast(path_buffer.data()), &buffer_size); + } + while (status == ERROR_MORE_DATA); + + const LSTATUS close_status = RegCloseKey(hkey); + if (close_status != ERROR_SUCCESS) + { + sys_log.error("get_registry_string: RegCloseKey failed: %s (key='%s', name='%s')", fmt::win_error{static_cast(close_status), nullptr}, wchar_to_utf8(key), wchar_to_utf8(name)); + } + + if (status != ERROR_SUCCESS) + { + sys_log.trace("get_registry_string: RegQueryValueExW failed: %s (key='%s', name='%s')", fmt::win_error{static_cast(status), nullptr}, wchar_to_utf8(key), wchar_to_utf8(name)); + return ""; + } + + if ((type == REG_SZ) || (type == REG_EXPAND_SZ) || (type == REG_MULTI_SZ)) + { + return wchar_to_utf8(path_buffer.data()); + } + + return ""; + } +#endif + + std::string steam_shortcut::steamid64_to_32(const std::string& steam_id) + { + u64 id = 0; + if (!try_to_uint64(&id, steam_id, 0, umax)) + { + sys_log.error("Failed to convert steam id '%s' to u64", steam_id); + return ""; + } + constexpr u64 base = 76561197960265728ULL; + const u32 id32 = static_cast(id - base); + return std::to_string(id32); + } + + std::string steam_shortcut::get_steam_path() + { +#ifdef _WIN32 + const std::string path = get_registry_string(L"Software\\Valve\\Steam", L"SteamPath"); + if (path.empty()) + { + sys_log.notice("get_steam_path: SteamPath not found in registry"); + return ""; + } + + // The path might be lowercase... sigh + std::error_code ec; + const std::filesystem::path canonical_path = std::filesystem::canonical(std::filesystem::path(path), ec); + if (ec) + { + sys_log.error("get_steam_path: Failed to canonicalize path '%s': %s", path, ec.message()); + return ""; + } + + const std::string path_fixed = canonical_path.string(); + sys_log.notice("get_steam_path: Found steam registry path: '%s'", path_fixed); + return path_fixed; +#else + if (const char* home = ::getenv("HOME")) + { +#if __APPLE__ + const std::string path = std::string(home) + "/Library/Application Support/Steam/"; +#else + const std::string path = std::string(home) + "/.local/share/Steam/"; +#endif + return path; + } + + return ""; +#endif + } + + std::string steam_shortcut::get_last_active_steam_user(const std::string& steam_path) + { + const std::string vdf_path = steam_path + "/config/loginusers.vdf"; + fs::file vdf(vdf_path); + if (!vdf) + { + sys_log.error("get_last_active_steam_user: Failed to parse steam loginusers file '%s': %s", vdf_path, fs::g_tls_error); + return ""; + } + + // The file looks roughly like this. We need the numerical ID. + // "users" + // { + // "12345678901234567" + // { + // "AccountName" "myusername" + // "MostRecent" "1" + // ... + // } + // ... + // } + + const std::string content = vdf.to_string(); + + usz user_count = 0; + + const auto find_user_id = [&content, &user_count](const std::string& key, const std::string& comp) -> std::string + { + user_count = 0; + usz pos = 0; + while (true) + { + pos = content.find(key, pos); + if (pos == umax) break; + + user_count++; + + const usz val_start = content.find('"', pos + key.size()); + if (val_start == umax) break; + + const usz val_end = content.find('"', val_start + 1); + if (val_end == umax) break; + + const std::string value = content.substr(val_start + 1, val_end - val_start - 1); + + if (value != comp) + { + pos = val_end + 1; + continue; + } + + const usz pos_brace = content.rfind('{', pos - 2); + if (pos_brace == umax) return ""; + + const usz pos_end = content.rfind('"', pos_brace - 2); + if (pos_end == umax) return ""; + + const usz pos_start = content.rfind('"', pos_end - 1); + if (pos_start == umax) return ""; + + const std::string user_id_64 = content.substr(pos_start + 1, pos_end - pos_start - 1); + return steamid64_to_32(user_id_64); + } + + return ""; + }; + + if (const std::string id = find_user_id("\"MostRecent\"", "1"); !id.empty()) + { + return id; + } + +#ifdef _WIN32 + // Fallback to AutoLoginUser + const std::string username = get_registry_string(L"Software\\Valve\\Steam", L"AutoLoginUser"); + if (username.empty()) + { + sys_log.notice("get_last_active_steam_user: AutoLoginUser not found in registry"); + return ""; + } + + sys_log.notice("get_last_active_steam_user: Found steam user: '%s'", username); + + if (const std::string id = find_user_id("\"AccountName\"", username); !id.empty()) + { + return id; + } +#endif + + sys_log.error("get_last_active_steam_user: Failed to parse steam loginusers file '%s' (user_count=%d)", vdf_path, user_count); + return ""; + } + + bool steam_shortcut::is_steam_running() + { +#ifdef _WIN32 + if (HANDLE mutex = OpenMutexA(SYNCHRONIZE, FALSE, "Global\\SteamClientRunning")) + { + CloseHandle(mutex); + return true; + } + + // Fallback to check process + PROCESSENTRY32 entry{}; + entry.dwSize = sizeof(entry); + + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (Process32First(snapshot, &entry)) + { + do + { + if (lstrcmpiW(entry.szExeFile, L"steam.exe") == 0) + { + CloseHandle(snapshot); + return true; + } + } while (Process32Next(snapshot, &entry)); + } + + CloseHandle(snapshot); +#else + std::vector pid_paths = { get_steam_path() }; +#ifdef __linux__ + if (const char* home = ::getenv("HOME")) + { + pid_paths.push_back(std::string(home) + "/.steam"); + pid_paths.push_back(std::string(home) + "/.steam/steam"); + } +#endif + for (const std::string& pid_path : pid_paths) + { + if (fs::file pid_file(pid_path + "/steam.pid"); pid_file) + { + const std::string pid = pid_file.to_string(); + pid_file.close(); + + if (pid.empty()) + { + continue; + } + + const pid_t pid_val = std::stoi(pid); + return kill(pid_val, 0) == 0 || errno != ESRCH; + } + } +#endif + return false; + } +} diff --git a/rpcs3/rpcs3qt/steam_utils.h b/rpcs3/rpcs3qt/steam_utils.h new file mode 100644 index 0000000000..3a1ce2c3a1 --- /dev/null +++ b/rpcs3/rpcs3qt/steam_utils.h @@ -0,0 +1,104 @@ +#pragma once + +#include "util/types.hpp" +#include "Utilities/StrFmt.h" +#include +#include + +namespace gui::utils +{ + class steam_shortcut + { + public: + steam_shortcut() {} + ~steam_shortcut() {} + + void add_shortcut( + const std::string& app_name, + const std::string& exe, + const std::string& start_dir, + const std::string& launch_options, + const std::string& icon_path); + + void remove_shortcut( + const std::string& app_name, + const std::string& exe, + const std::string& start_dir); + + bool write_file(); + + static bool steam_installed(); + static bool is_steam_running(); + + private: + enum type_id + { + Null = 0x00, + Start = Null, + String = 0x01, + Integer = 0x02, + Float = 0x03, + Pointer = 0x04, + Nested = 0x05, + Array = 0x06, + Bool = 0x07, + End = 0x08, + }; + + struct shortcut_entry + { + std::string app_name; + std::string exe; + std::string start_dir; + std::string launch_options; + std::string icon; + u32 appid = 0; + + std::string build_binary_blob(usz index) const; + }; + + struct vdf_shortcut_entry + { + std::vector>> values; + std::vector> tags; + + template + std::expected value(const std::string& key) const + { + const auto it = std::find_if(values.cbegin(), values.cend(), [&key](const auto& v){ return v.first == key; }); + if (it == values.cend()) + { + return std::unexpected(fmt::format("key '%s' not found", key)); + } + + if (const auto* p = std::get_if(&it->second)) + { + return *p; + } + + return std::unexpected(fmt::format("value for key '%s' has wrong type", key)); + } + + std::expected build_binary_blob(usz index) const; + }; + + bool parse_file(const std::string& path); + + static u32 crc32(const std::string& data); + static u32 steam_appid(const std::string& exe, const std::string& name); + + static void append(std::string& s, const std::string& val); + + static std::string quote(const std::string& s, bool force); + static std::string fix_slashes(const std::string& s); + static std::string kv_string(const std::string& key, const std::string& value); + static std::string kv_int(const std::string& key, u32 value); + static std::string steamid64_to_32(const std::string& steam_id); + static std::string get_steam_path(); + static std::string get_last_active_steam_user(const std::string& steam_path); + + std::vector m_entries_to_add; + std::vector m_entries_to_remove; + std::vector m_vdf_entries; + }; +}