The perfect PR (#147)

* initial interface and logic to populate the version selector table (only GUI, doesn't do anything yet)

* update LoadVersionComboBox to the new logic

* implement adding a new version to the versions file, and hook it up with "add custom version"

* implement deleting version and update logic for dl-ing a new version from the version manager

* oof

* Clang, Formatter of Night

* fix building (how did it even build locally?)

* +

* Add back shortcut creation (needs better file selection)

* Add the shortcut dialog

* cleanup

* Different file filters per OS

* syntax and filter fix

* change to launcher shortcut instead of emu shortcut (only default for now)

* Update gui_context_menus.h

* fix build

* Version shortcut added

* pass gamepath from args so automatic patch detection works

* Update CMakeLists.txt

* Fix linux path

* support automatic patch detection if gamearg is a serial

* linux fix

* implement --game/-g instead of passing from emulator_args

* fix -d

* add --no-ipc flag

* fix -e

* versions is now using json file

* fixing consts

* introducing portable/non portable launcher dir

* qt settings to launcher dir

* compatibility + meta info (no trophies it will go elsewhere after usermanagement)

* clang

---------

Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com>
Co-authored-by: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com>
This commit is contained in:
georgemoralis
2025-10-24 13:36:04 +03:00
committed by GitHub
parent 450bb7f018
commit 0444827a38
21 changed files with 671 additions and 266 deletions

4
.gitmodules vendored
View File

@@ -30,3 +30,7 @@
path = externals/MoltenVK/MoltenVK
url = https://github.com/KhronosGroup/MoltenVK
shallow = true
[submodule "externals/json"]
path = externals/json
url = https://github.com/nlohmann/json.git
branch = develop

View File

@@ -283,6 +283,8 @@ set(COMMON src/common/logging/backend.cpp
src/common/bounded_threadsafe_queue.h
src/common/config.cpp
src/common/config.h
src/common/versions.cpp
src/common/versions.h
src/common/endian.h
src/common/enum.h
src/common/io_file.cpp
@@ -325,7 +327,7 @@ set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/cheats_patches.h
src/qt_gui/compatibility_info.cpp
src/qt_gui/compatibility_info.h
src/qt_gui/control_settings.cpp
src/qt_gui/control_settings.cpp
src/qt_gui/control_settings.h
src/qt_gui/control_settings.ui
src/qt_gui/kbm_gui.cpp
@@ -371,6 +373,8 @@ set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/version_dialog.cpp
src/qt_gui/version_dialog.h
src/qt_gui/version_dialog.ui
src/qt_gui/create_shortcut.cpp
src/qt_gui/create_shortcut.h
${RESOURCE_FILES}
${TRANSLATIONS}
${UPDATER}
@@ -421,7 +425,7 @@ if (APPLE)
add_dependencies(shadPS4QtLauncher CopyMoltenVK)
endif()
target_link_libraries(shadPS4QtLauncher PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network Qt6::Multimedia)
target_link_libraries(shadPS4QtLauncher PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network Qt6::Multimedia nlohmann_json::nlohmann_json)
if (ENABLE_UPDATER)
add_definitions(-DENABLE_UPDATER)
endif()

View File

@@ -60,4 +60,8 @@ if (APPLE)
endif()
endif()
add_subdirectory(volk)
add_subdirectory(volk)
#nlohmann json
set(JSON_BuildTests OFF CACHE INTERNAL "")
add_subdirectory(json)

1
externals/json vendored Submodule

Submodule externals/json added at 54be9b04f0

View File

@@ -88,7 +88,7 @@ static auto UserPaths = [] {
}
#endif
// Try the portable user directory first.
// Try the portable launcher directory first.
auto user_dir = std::filesystem::current_path() / PORTABLE_DIR;
if (!std::filesystem::exists(user_dir)) {
// If it doesn't exist, use the standard path for the platform instead.
@@ -110,6 +110,29 @@ static auto UserPaths = [] {
#endif
}
// Try the portable user directory first.
auto launcher_dir = std::filesystem::current_path() / PORTABLE_LAUNCHER_DIR;
if (!std::filesystem::exists(launcher_dir)) {
// If it doesn't exist, use the standard path for the platform instead.
// NOTE: On Windows we currently just create the portable directory instead.
#ifdef __APPLE__
launcher_dir = std::filesystem::path(getenv("HOME")) / "Library" / "Application Support" /
"shadPS4QtLauncher";
#elif defined(__linux__)
const char* xdg_data_home = getenv("XDG_DATA_HOME");
if (xdg_data_home != nullptr && strlen(xdg_data_home) > 0) {
launcher_dir = std::filesystem::path(xdg_data_home) / "shadPS4QtLauncher";
} else {
launcher_dir =
std::filesystem::path(getenv("HOME")) / ".local" / "share" / "shadPS4QtLauncher";
}
#elif _WIN32
TCHAR appdata[MAX_PATH] = {0};
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, appdata);
launcher_dir = std::filesystem::path(appdata) / "shadPS4QtLauncher";
#endif
}
std::unordered_map<PathType, fs::path> paths;
const auto create_path = [&](PathType shad_path, const fs::path& new_path) {
@@ -131,7 +154,10 @@ static auto UserPaths = [] {
create_path(PathType::MetaDataDir, user_dir / METADATA_DIR);
create_path(PathType::CustomTrophy, user_dir / CUSTOM_TROPHY);
create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS);
create_path(PathType::VersionDir, user_dir / VERSION_DIR);
create_path(PathType::LauncherDir, launcher_dir);
create_path(PathType::LauncherMetaData, launcher_dir / METADATA_DIR);
create_path(PathType::VersionDir, launcher_dir / VERSION_DIR);
std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt");
if (notice_file.is_open()) {

View File

@@ -12,24 +12,27 @@ class QString; // to avoid including <QString> in this header
namespace Common::FS {
enum class PathType {
UserDir, // Where shadPS4 stores its data.
LogDir, // Where log files are stored.
ScreenshotsDir, // Where screenshots are stored.
ShaderDir, // Where shaders are stored.
TempDataDir, // Where game temp data is stored.
GameDataDir, // Where game data is stored.
SysModuleDir, // Where system modules are stored.
DownloadDir, // Where downloads/temp files are stored.
CapturesDir, // Where rdoc captures are stored.
CheatsDir, // Where cheats are stored.
PatchesDir, // Where patches are stored.
MetaDataDir, // Where game metadata (e.g. trophies and menu backgrounds) is stored.
CustomTrophy, // Where custom files for trophies are stored.
CustomConfigs, // Where custom files for different games are stored.
VersionDir, // Where emulator versions are stored.
UserDir, // Where shadPS4 stores its data.
LogDir, // Where log files are stored.
ScreenshotsDir, // Where screenshots are stored.
ShaderDir, // Where shaders are stored.
TempDataDir, // Where game temp data is stored.
GameDataDir, // Where game data is stored.
SysModuleDir, // Where system modules are stored.
DownloadDir, // Where downloads/temp files are stored.
CapturesDir, // Where rdoc captures are stored.
CheatsDir, // Where cheats are stored.
PatchesDir, // Where patches are stored.
MetaDataDir, // Where game metadata (e.g. trophies and menu backgrounds) is stored.
CustomTrophy, // Where custom files for trophies are stored.
CustomConfigs, // Where custom files for different games are stored.
VersionDir, // Where emulator versions are stored.
LauncherDir, // Where launcher stores its data.
LauncherMetaData // Where launcher stores its game metadata.
};
constexpr auto PORTABLE_DIR = "user";
constexpr auto PORTABLE_LAUNCHER_DIR = "launcher";
// Sub-directories contained within a user data directory
constexpr auto LOG_DIR = "log";

116
src/common/versions.cpp Normal file
View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "logging/log.h"
#include "path_util.h"
#include "versions.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <vector>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace VersionManager {
std::vector<Version> GetVersionList(std::filesystem::path const& path) {
std::filesystem::path cfg_path =
path.empty() ? Common::FS::GetUserPath(Common::FS::PathType::LauncherDir) / "versions.json"
: path;
std::ifstream ifs(cfg_path);
if (!ifs) {
fmt::print(stderr, "VersionManager: Config file not found: {}\n", cfg_path.string());
return {};
}
json data;
try {
ifs >> data;
} catch (const std::exception& ex) {
fmt::print(stderr, "VersionManager: Failed to parse JSON: {}\n", ex.what());
return {};
}
if (!data.is_array()) {
fmt::print(stderr, "VersionManager: Invalid JSON format (expected array)\n");
return {};
}
std::vector<Version> versions;
int id = 0;
for (const auto& entry : data) {
if (!entry.is_object()) {
fmt::print(stderr, "VersionManager: Skipping invalid entry (not an object)\n");
continue;
}
Version v{
.name = entry.value("name", std::string("no name")),
.path = entry.value("path", std::string("")),
.date = entry.value("date", std::string("never")),
.codename = entry.value("codename", std::string("")),
.type = static_cast<VersionType>(
entry.value("type", static_cast<int>(VersionType::Custom))),
.id = id++,
};
versions.push_back(std::move(v));
}
// Sort by id just for consistent ordering
std::sort(versions.begin(), versions.end(),
[](const Version& a, const Version& b) { return a.id < b.id; });
return versions;
}
void SaveVersionList(std::vector<Version> const& versions, std::filesystem::path const& path) {
std::filesystem::path out_path =
path.empty() ? Common::FS::GetUserPath(Common::FS::PathType::LauncherDir) / "versions.json"
: path;
json root = json::array();
for (const auto& v : versions) {
root.push_back({{"name", v.name},
{"path", v.path},
{"date", v.date},
{"codename", v.codename},
{"type", static_cast<int>(v.type)}});
}
std::ofstream ofs(out_path, std::ios::trunc);
if (!ofs) {
fmt::print(stderr, "Failed to open file for writing: {}\n", out_path.string());
return;
}
ofs << std::setw(4) << root;
}
void AddNewVersion(Version const& v, std::filesystem::path const& path) {
auto versions = GetVersionList(path);
versions.push_back(v);
SaveVersionList(versions, path);
}
void RemoveVersion(std::string const& v_name, std::filesystem::path const& path) {
auto versions = GetVersionList(path);
auto it = std::find_if(versions.begin(), versions.end(),
[&](const Version& i) { return i.name == v_name; });
if (it != versions.end()) {
versions.erase(it);
}
SaveVersionList(versions, path);
}
void RemoveVersion(Version const& v, std::filesystem::path const& path) {
RemoveVersion(v.name, path);
}
} // namespace VersionManager

35
src/common/versions.h Normal file
View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include <toml.hpp>
#include "types.h"
namespace VersionManager {
enum VersionType {
Release = 0,
Nightly = 1,
Custom = 2,
};
struct Version {
std::string name;
std::string path;
std::string date;
std::string codename;
VersionType type;
s32 id;
};
std::vector<Version> GetVersionList(std::filesystem::path const& path = "");
void AddNewVersion(Version const& v, std::filesystem::path const& path = "");
void RemoveVersion(Version const& v, std::filesystem::path const& path = "");
void RemoveVersion(std::string const& v, std::filesystem::path const& path = "");
} // namespace VersionManager

View File

@@ -10,8 +10,8 @@
IpcClient::IpcClient(QObject* parent) : QObject(parent) {}
void IpcClient::startEmulator(const QFileInfo& exe, const QStringList& args,
const QString& workDir) {
void IpcClient::startEmulator(const QFileInfo& exe, const QStringList& args, const QString& workDir,
bool disable_ipc) {
if (process) {
process->disconnect();
process->deleteLater();
@@ -26,7 +26,9 @@ void IpcClient::startEmulator(const QFileInfo& exe, const QStringList& args,
process->setProcessChannelMode(QProcess::SeparateChannels);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("SHADPS4_ENABLE_IPC", "true");
if (!disable_ipc) {
env.insert("SHADPS4_ENABLE_IPC", "true");
}
process->setProcessEnvironment(env);
process->setWorkingDirectory(workDir.isEmpty() ? exe.absolutePath() : workDir);

View File

@@ -15,7 +15,7 @@ class IpcClient : public QObject {
public:
explicit IpcClient(QObject* parent = nullptr);
void startEmulator(const QFileInfo& exe, const QStringList& args,
const QString& workDir = QString());
const QString& workDir = QString(), bool disable_ipc = false);
void startGame();
void pauseGame();
void resumeGame();

View File

@@ -7,6 +7,7 @@
#include "common/config.h"
#include "common/logging/backend.h"
#include "common/versions.h"
#include "qt_gui/game_install_dialog.h"
#include "qt_gui/main_window.h"
#ifdef _WIN32
@@ -35,9 +36,10 @@ int main(int argc, char* argv[]) {
const bool has_command_line_argument = argc > 1;
bool has_emulator_argument = false;
bool show_gui = false;
bool show_gui = false, no_ipc = false;
std::string emulator;
QStringList emulator_args{};
QString game_arg = "";
// Ignore Qt logs
qInstallMessageHandler(customMessageHandler);
@@ -52,11 +54,13 @@ int main(int argc, char* argv[]) {
" No arguments: Opens the GUI.\n"
" -e, --emulator <name|path> Specify the emulator version/path you want to "
"use, or 'default' for using the version selected in the config.\n"
" -g, --game <ID|path> Specify game to launch.\n"
" -d Alias for '-e default'.\n"
" -- ... Parameters passed to the emulator core. "
"Needs to be at the end of the line, and everything after '--' is an "
"emulator argument.\n"
" -s, --show-gui Show the GUI.\n"
" -i, --no-ipc Disable IPC.\n"
" -h, --help Display this help message.\n";
exit(0);
}},
@@ -64,14 +68,26 @@ int main(int argc, char* argv[]) {
{"-s", [&](int&) { show_gui = true; }},
{"--show-gui", [&](int& i) { arg_map["-s"](i); }},
{"-i", [&](int&) { no_ipc = true; }},
{"--no-ipc", [&](int& i) { arg_map["-i"](i); }},
{"-g",
[&](int& i) {
if (i + 1 < argc) {
game_arg = argv[++i];
} else {
std::cerr << "Error: Missing argument for -g/--game\n";
exit(1);
}
}},
{"--game", [&](int& i) { arg_map["-g"](i); }},
{"-e",
[&](int& i) {
if (i + 1 < argc) {
emulator = argv[++i];
has_emulator_argument = true;
} else {
std::cerr << "Error: Missing argument for -g/--game\n";
std::cerr << "Error: Missing argument for -e/--emulator\n";
exit(1);
}
}},
@@ -137,13 +153,12 @@ int main(int argc, char* argv[]) {
emulator_path = emulator;
} else if (emulator == "default") {
gui_settings settings{};
emulator_path = *std::filesystem::directory_iterator(
settings.GetValue(gui::vm_versionSelected).toString().toStdString());
emulator_path = settings.GetValue(gui::vm_versionSelected).toString().toStdString();
} else {
std::filesystem::path version_dir = user_dir / "versions";
for (auto const& version : std::filesystem::directory_iterator(version_dir)) {
if (version.is_directory() && version.path().filename() == emulator) {
emulator_path = *std::filesystem::directory_iterator(version);
auto const& versions = VersionManager::GetVersionList();
for (auto const& v : versions) {
if (v.name == emulator) {
emulator_path = v.path;
break;
}
}
@@ -155,7 +170,7 @@ int main(int argc, char* argv[]) {
if (!show_gui) {
m_main_window->m_ipc_client->gameClosedFunc = StopProgram;
}
m_main_window->StartEmulatorExecutable(emulator_path, emulator_args);
m_main_window->StartEmulatorExecutable(emulator_path, game_arg, emulator_args, no_ipc);
}
if (!has_emulator_argument || show_gui) {

View File

@@ -13,7 +13,7 @@ CompatibilityInfoClass::CompatibilityInfoClass()
: m_network_manager(new QNetworkAccessManager(this)) {
QStringList file_paths;
std::filesystem::path compatibility_file_path =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / "compatibility_data.json";
Common::FS::GetUserPath(Common::FS::PathType::LauncherDir) / "compatibility_data.json";
Common::FS::PathToQString(m_compatibility_filename, compatibility_file_path);
};
CompatibilityInfoClass::~CompatibilityInfoClass() = default;

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialogButtonBox>
#include <QDirIterator>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
#include "common/path_util.h"
#include "create_shortcut.h"
ShortcutDialog::ShortcutDialog(std::shared_ptr<gui_settings> settings, QWidget* parent)
: QDialog(parent), m_gui_settings(std::move(settings)) {
setupUI();
resize(600, 50);
this->setWindowTitle(tr("Select Version"));
}
void ShortcutDialog::setupUI() {
QVBoxLayout* mainLayout = new QVBoxLayout(this);
QLabel* versionLabel =
new QLabel(QString("<b>%1</b>").arg(tr("Select version for shortcut creation")));
listWidget = new QListWidget(this);
QString versionFolder = m_gui_settings->GetValue(gui::vm_versionPath).toString();
QDirIterator versions(versionFolder, QDir::Dirs | QDir::NoDotAndDotDot);
while (versions.hasNext()) {
versions.next();
new QListWidgetItem(versions.fileName(), listWidget);
}
QDialogButtonBox* buttonBox =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
mainLayout->addWidget(versionLabel);
mainLayout->addWidget(listWidget);
mainLayout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QWidget::close);
connect(buttonBox, &QDialogButtonBox::accepted, this, &ShortcutDialog::createShortcut);
}
void ShortcutDialog::createShortcut() {
if (listWidget->selectedItems().empty()) {
QMessageBox::information(this, tr("No Version Selected"), tr("Select a version first"));
return;
}
QString versionFolder = m_gui_settings->GetValue(gui::vm_versionPath).toString();
QString versionName = "/" + listWidget->currentItem()->text();
QString exeName;
#ifdef Q_OS_WIN
exeName = "/shadPS4.exe";
#elif defined(Q_OS_LINUX)
exeName = "/Shadps4-sdl.AppImage";
#elif defined(Q_OS_MACOS)
exeName = "/shadps4";
#endif
emit shortcutRequested(versionFolder + versionName + exeName);
QWidget::close();
}
ShortcutDialog::~ShortcutDialog() {}

View File

@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <QListWidget>
#include "gui_settings.h"
class ShortcutDialog : public QDialog {
Q_OBJECT
public:
explicit ShortcutDialog(std::shared_ptr<gui_settings> settings, QWidget* parent = nullptr);
~ShortcutDialog();
signals:
void shortcutRequested(QString version);
private:
void setupUI();
void createShortcut();
QListWidget* listWidget;
std::shared_ptr<gui_settings> m_gui_settings;
};

View File

@@ -66,7 +66,7 @@ public:
// Cache path
QDir cacheDir =
QDir(Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game.serial);
QDir(Common::FS::GetUserPath(Common::FS::PathType::LauncherMetaData) / game.serial);
if (!cacheDir.exists()) {
cacheDir.mkpath(".");
}

View File

@@ -11,7 +11,7 @@
#include <QMessageBox>
#include <QTreeWidgetItem>
#include <qt_gui/background_music_player.h>
#include "background_music_player.h"
#include "cheats_patches.h"
#include "common/config.h"
#include "common/log_analyzer.h"
@@ -19,6 +19,7 @@
#include "common/path_util.h"
#include "common/scm_rev.h"
#include "compatibility_info.h"
#include "create_shortcut.h"
#include "game_info.h"
#include "gui_settings.h"
#include "ipc/ipc_client.h"
@@ -26,6 +27,11 @@
#include "trophy_viewer.h"
#ifdef Q_OS_WIN
#include <ShlObj.h>
#include <Windows.h>
#include <objbase.h>
#include <shlguid.h>
#include <shobjidl.h>
#include <wrl/client.h>
#endif
@@ -113,7 +119,15 @@ public:
QAction openCheats(tr("Cheats / Patches"), widget);
QAction openTrophyViewer(tr("Trophy Viewer"), widget);
QAction openSfoViewer(tr("SFO Viewer"), widget);
QAction createDefaultShortcut(tr("Create Shortcut for Selected Emulator Version"), widget);
QAction createVersionShortcut(tr("Create Shortcut for Specified Emulator Version"), widget);
#ifndef Q_OS_APPLE
QMenu* shortcutMenu = new QMenu(tr("Create Shortcut"), widget);
menu.addMenu(shortcutMenu);
shortcutMenu->addAction(&createDefaultShortcut);
shortcutMenu->addAction(&createVersionShortcut);
#endif
menu.addAction(toggleFavorite);
menu.addAction(&openCheats);
menu.addAction(&openTrophyViewer);
@@ -446,6 +460,20 @@ public:
}
}
if (selected == &createDefaultShortcut) {
requestShortcut(m_games[itemID]);
}
if (selected == &createVersionShortcut) {
auto shortcutWindow = new ShortcutDialog(m_gui_settings);
QObject::connect(
shortcutWindow, &ShortcutDialog::shortcutRequested, this,
[=, this](QString version) { requestShortcut(m_games[itemID], version); });
shortcutWindow->exec();
}
// Handle the "Copy" actions
if (selected == copyName) {
QClipboard* clipboard = QGuiApplication::clipboard();
@@ -651,4 +679,170 @@ public:
}
return -1;
}
private:
void requestShortcut(const GameInfo& selectedInfo, QString emuPath = "") {
// Path to shortcut/link
QString linkPath;
// Eboot path
QString targetPath;
Common::FS::PathToQString(targetPath, selectedInfo.path);
QString ebootPath = targetPath + "/eboot.bin";
// Get the full path to the icon
QString iconPath;
Common::FS::PathToQString(iconPath, selectedInfo.icon_path);
QFileInfo iconFileInfo(iconPath);
QString icoPath = iconFileInfo.absolutePath() + "/" + iconFileInfo.baseName() + ".ico";
QString exePath;
#ifdef Q_OS_WIN
linkPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "/" +
QString::fromStdString(selectedInfo.name)
.remove(QRegularExpression("[\\\\/:*?\"<>|]")) +
".lnk";
exePath = QCoreApplication::applicationFilePath().replace("\\", "/");
#else
linkPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "/" +
QString::fromStdString(selectedInfo.name)
.remove(QRegularExpression("[\\\\/:*?\"<>|]")) +
".desktop";
#endif
// Convert the icon to .ico if necessary
if (iconFileInfo.suffix().toLower() == "png") {
// Convert icon from PNG to ICO
if (convertPngToIco(iconPath, icoPath)) {
#ifdef Q_OS_WIN
if (createShortcutWin(linkPath, ebootPath, icoPath, exePath, emuPath)) {
#else
if (createShortcutLinux(linkPath, selectedInfo.name, ebootPath, iconPath,
emuPath)) {
#endif
QMessageBox::information(
nullptr, tr("Shortcut creation"),
QString(tr("Shortcut created successfully!") + "\n%1").arg(linkPath));
} else {
QMessageBox::critical(
nullptr, tr("Error"),
QString(tr("Error creating shortcut!") + "\n%1").arg(linkPath));
}
} else {
QMessageBox::critical(nullptr, tr("Error"), tr("Failed to convert icon."));
}
// If the icon is already in ICO format, we just create the shortcut
} else {
#ifdef Q_OS_WIN
if (createShortcutWin(linkPath, ebootPath, iconPath, exePath, emuPath)) {
#else
if (createShortcutLinux(linkPath, selectedInfo.name, ebootPath, iconPath, emuPath)) {
#endif
QMessageBox::information(
nullptr, tr("Shortcut creation"),
QString(tr("Shortcut created successfully!") + "\n%1").arg(linkPath));
} else {
QMessageBox::critical(
nullptr, tr("Error"),
QString(tr("Error creating shortcut!") + "\n%1").arg(linkPath));
}
}
}
bool convertPngToIco(const QString& pngFilePath, const QString& icoFilePath) {
// Load the PNG image
QImage image(pngFilePath);
if (image.isNull()) {
return false;
}
// Scale the image to the default icon size (256x256 pixels)
QImage scaledImage =
image.scaled(QSize(256, 256), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// Convert the image to QPixmap
QPixmap pixmap = QPixmap::fromImage(scaledImage);
// Save the pixmap as an ICO file
if (pixmap.save(icoFilePath, "ICO")) {
return true;
} else {
return false;
}
}
#ifdef Q_OS_WIN
bool createShortcutWin(const QString& linkPath, const QString& targetPath,
const QString& iconPath, const QString& exePath, QString emuPath) {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
// Create the ShellLink object
Microsoft::WRL::ComPtr<IShellLink> pShellLink;
HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&pShellLink));
if (SUCCEEDED(hres)) {
// Defines the path to the program executable
pShellLink->SetPath((LPCWSTR)exePath.utf16());
// Sets the home directory ("Start in")
pShellLink->SetWorkingDirectory((LPCWSTR)QFileInfo(exePath).absolutePath().utf16());
// Set arguments, eboot.bin file location
QString arguments;
if (emuPath == "") {
arguments = QString("-d -g \"%1\"").arg(targetPath);
} else {
arguments = QString("-e \"%1\" -g \"%2\"").arg(emuPath, targetPath);
}
pShellLink->SetArguments((LPCWSTR)arguments.utf16());
// Set the icon for the shortcut
pShellLink->SetIconLocation((LPCWSTR)iconPath.utf16(), 0);
// Save the shortcut
Microsoft::WRL::ComPtr<IPersistFile> pPersistFile;
hres = pShellLink.As(&pPersistFile);
if (SUCCEEDED(hres)) {
hres = pPersistFile->Save((LPCWSTR)linkPath.utf16(), TRUE);
}
}
CoUninitialize();
return SUCCEEDED(hres);
}
#else
bool createShortcutLinux(const QString& linkPath, const std::string& name,
const QString& targetPath, const QString& iconPath, QString emuPath) {
QFile shortcutFile(linkPath);
if (!shortcutFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(nullptr, "Error",
QString("Error creating shortcut!\n %1").arg(linkPath));
return false;
}
QTextStream out(&shortcutFile);
out << "[Desktop Entry]\n";
out << "Version=1.0\n";
out << "Name=" << QString::fromStdString(name) << "\n";
if (emuPath == "") {
out << "Exec=" << QCoreApplication::applicationFilePath() << " -d -g" << " \""
<< targetPath << "\"\n";
} else {
out << "Exec=" << QCoreApplication::applicationFilePath() << " -e" << " \"" << emuPath
<< "\"" << " -g" << " \"" << targetPath << "\"\n";
}
out << "Icon=" << iconPath << "\n";
out << "Terminal=false\n";
out << "Type=Application\n";
shortcutFile.close();
return true;
}
#endif
};

View File

@@ -19,6 +19,7 @@
#include "common/memory_patcher.h"
#include "common/path_util.h"
#include "common/scm_rev.h"
#include "common/versions.h"
#include "control_settings.h"
#include "game_install_dialog.h"
#include "hotkeys.h"
@@ -1268,16 +1269,7 @@ tr("No emulator version was selected.\nThe Version Manager menu will then open.\
Config::setGameRunning(true);
last_game_path = path;
QString exeName;
#ifdef Q_OS_WIN
exeName = "/shadPS4.exe";
#elif defined(Q_OS_LINUX)
exeName = "/Shadps4-sdl.AppImage";
#elif defined(Q_OS_MACOS)
exeName = "/shadps4";
#endif
QString exe = selectedVersion + exeName;
QFileInfo fileInfo(exe);
QFileInfo fileInfo(selectedVersion);
if (!fileInfo.exists()) {
QMessageBox::critical(nullptr, "shadPS4",
QString(tr("Could not find the emulator executable")));
@@ -1294,26 +1286,71 @@ tr("No emulator version was selected.\nThe Version Manager menu will then open.\
m_ipc_client->setActiveController(GamepadSelect::GetSelectedGamepad());
}
void MainWindow::StartEmulatorExecutable(std::filesystem::path path, QStringList args) {
void MainWindow::StartEmulatorExecutable(std::filesystem::path emuPath, QString gameArg,
QStringList args, bool disable_ipc) {
if (Config::getGameRunning()) {
QMessageBox::critical(nullptr, tr("Run Emulator"),
QString(tr("Emulator is already running!")));
return;
}
Config::setGameRunning(true);
QFileInfo fileInfo(path);
bool gameFound = false;
if (std::filesystem::exists(Common::FS::PathFromQString(gameArg))) {
last_game_path = Common::FS::PathFromQString(gameArg);
gameFound = true;
} else {
// In install folders, find game folder with same name as gameArg
const auto install_dir_array = Config::getGameInstallDirs();
std::vector<bool> install_dirs_enabled;
try {
install_dirs_enabled = Config::getGameInstallDirsEnabled();
} catch (...) {
// If it does not exist, assume that all are enabled.
install_dirs_enabled.resize(install_dir_array.size(), true);
}
for (size_t i = 0; i < install_dir_array.size(); i++) {
std::filesystem::path dir = install_dir_array[i];
bool enabled = install_dirs_enabled[i];
if (enabled && std::filesystem::exists(dir)) {
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
if (entry.is_directory()) {
if (entry.path().filename().string() == gameArg.toStdString()) {
last_game_path = entry.path() / "eboot.bin";
gameFound = true;
break;
}
}
}
}
if (gameFound)
break;
}
}
if (!gameArg.isEmpty()) {
if (!gameFound) {
QMessageBox::critical(nullptr, "shadPS4",
QString(tr("Invalid game argument provided")));
quick_exit(1);
}
QStringList game_args{"--game", QString::fromStdWString(last_game_path.wstring())};
args.append(game_args);
}
QFileInfo fileInfo(emuPath);
if (!fileInfo.exists()) {
QMessageBox::critical(nullptr, "shadPS4",
QString(tr("Could not find the emulator executable")));
Config::setGameRunning(false);
return;
}
Config::setGameRunning(true);
QString workDir = QDir::currentPath();
m_ipc_client->startEmulator(fileInfo, args, workDir);
m_ipc_client->setActiveController(GamepadSelect::GetSelectedGamepad());
m_ipc_client->startEmulator(fileInfo, args, workDir, disable_ipc);
}
void MainWindow::RunGame() {
@@ -1360,75 +1397,17 @@ void MainWindow::RestartEmulator() {
}
void MainWindow::LoadVersionComboBox() {
QString savedVersionPath = m_gui_settings->GetValue(gui::vm_versionSelected).toString();
if (savedVersionPath.isEmpty() || !QDir(savedVersionPath).exists()) {
ui->versionComboBox->clear();
ui->versionComboBox->addItem(tr("No Version Selected"));
ui->versionComboBox->setCurrentIndex(0);
ui->versionComboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
ui->versionComboBox->adjustSize();
return;
}
QString path = m_gui_settings->GetValue(gui::vm_versionPath).toString();
if (path.isEmpty() || !QDir(path).exists())
return;
ui->versionComboBox->clear();
ui->versionComboBox->addItem(tr("None"));
ui->versionComboBox->setCurrentIndex(0);
ui->versionComboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
QStringList folders = QDir(path).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QRegularExpression versionRegex("^v(\\d+)\\.(\\d+)\\.(\\d+)$");
QString savedVersionPath = m_gui_settings->GetValue(gui::vm_versionSelected).toString();
QVector<QPair<QVector<int>, QString>> versionedDirs;
QStringList otherDirs;
for (const QString& folder : folders) {
if (folder == "Pre-release") {
otherDirs.append(folder);
continue;
}
QRegularExpressionMatch match = versionRegex.match(folder.section(" - ", 0, 0));
if (match.hasMatch()) {
QVector<int> versionParts = {match.captured(1).toInt(), match.captured(2).toInt(),
match.captured(3).toInt()};
versionedDirs.append({versionParts, folder});
} else {
otherDirs.append(folder);
}
}
std::sort(otherDirs.begin(), otherDirs.end());
std::sort(versionedDirs.begin(), versionedDirs.end(), [](const auto& a, const auto& b) {
if (a.first[0] != b.first[0])
return a.first[0] > b.first[0];
if (a.first[1] != b.first[1])
return a.first[1] > b.first[1];
return a.first[2] > b.first[2];
});
auto addEntry = [&](const QString& folder) {
QString fullPath = QDir(path).filePath(folder);
QString label;
if (folder.startsWith("Pre-release-shadPS4")) {
label = "Pre-release";
} else if (folder.contains(" - ")) {
label = folder.section(" - ", 0, 0);
} else {
label = folder;
}
ui->versionComboBox->addItem(label, fullPath);
};
for (const QString& folder : otherDirs) {
addEntry(folder);
}
for (const auto& pair : versionedDirs) {
addEntry(pair.second);
auto const& versions = VersionManager::GetVersionList();
for (auto const& v : versions) {
ui->versionComboBox->addItem(QString::fromStdString(v.name),
QString::fromStdString(v.path));
}
int selectedIndex = ui->versionComboBox->findData(savedVersionPath);

View File

@@ -36,7 +36,8 @@ public:
void StartGame();
void StartGameWithArgs(QStringList args = {});
void StartEmulator(std::filesystem::path path, QStringList args = {});
void StartEmulatorExecutable(std::filesystem::path path, QStringList args = {});
void StartEmulatorExecutable(std::filesystem::path emupath, QString gameArg,
QStringList args = {}, bool disable_ipc = false);
void PauseGame();
void StopGame();
void RestartGame();

View File

@@ -21,7 +21,7 @@ QString settings::GetSettingsDir() const {
}
QString settings::ComputeSettingsDir() {
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::LauncherDir);
return QString::fromStdString(config_dir.string() + "/");
}

View File

@@ -328,17 +328,17 @@ SettingsDialog::SettingsDialog(std::shared_ptr<gui_settings> gui_settings,
});
connect(ui->PortableUserButton, &QPushButton::clicked, this, []() {
QString userDir;
Common::FS::PathToQString(userDir, std::filesystem::current_path() / "user");
if (std::filesystem::exists(std::filesystem::current_path() / "user")) {
QMessageBox::information(NULL, tr("Cannot create portable user folder"),
tr("%1 already exists").arg(userDir));
QString launcherDir;
Common::FS::PathToQString(launcherDir, std::filesystem::current_path() / "launcher");
if (std::filesystem::exists(std::filesystem::current_path() / "launcher")) {
QMessageBox::information(NULL, tr("Cannot create portable launcher folder"),
tr("%1 already exists").arg(launcherDir));
} else {
std::filesystem::copy(Common::FS::GetUserPath(Common::FS::PathType::UserDir),
std::filesystem::current_path() / "user",
std::filesystem::copy(Common::FS::GetUserPath(Common::FS::PathType::LauncherDir),
std::filesystem::current_path() / "launcher",
std::filesystem::copy_options::recursive);
QMessageBox::information(NULL, tr("Portable user folder created"),
tr("%1 successfully created.").arg(userDir));
QMessageBox::information(NULL, tr("Portable launcherDir folder created"),
tr("%1 successfully created.").arg(launcherDir));
}
});
}

View File

@@ -18,6 +18,7 @@
#include <QTimer>
#include <QVBoxLayout>
#include <common/path_util.h>
#include <common/versions.h>
#include "common/config.h"
#include "gui_settings.h"
@@ -29,6 +30,8 @@ VersionDialog::VersionDialog(std::shared_ptr<gui_settings> gui_settings, QWidget
: QDialog(parent), ui(new Ui::VersionDialog), m_gui_settings(std::move(gui_settings)) {
ui->setupUi(this);
auto const& version_list = VersionManager::GetVersionList();
ui->checkOnStartupCheckBox->setChecked(
m_gui_settings->GetValue(gui::vm_checkOnStartup).toBool());
ui->showChangelogCheckBox->setChecked(m_gui_settings->GetValue(gui::vm_showChangeLog).toBool());
@@ -81,7 +84,7 @@ VersionDialog::VersionDialog(std::shared_ptr<gui_settings> gui_settings, QWidget
connect(ui->checkChangesVersionButton, &QPushButton::clicked, this,
[this]() { LoadInstalledList(); });
connect(ui->addCustomVersionButton, &QPushButton::clicked, this, [this]() {
connect(ui->addCustomVersionButton, &QPushButton::clicked, this, [this, version_list]() {
QString exePath;
#ifdef Q_OS_WIN
@@ -89,46 +92,40 @@ VersionDialog::VersionDialog(std::shared_ptr<gui_settings> gui_settings, QWidget
tr("Executable (*.exe)"));
#elif defined(Q_OS_LINUX)
exePath = QFileDialog::getOpenFileName(this, tr("Select executable"), QDir::rootPath(),
tr("Executable (*.AppImage)"));
"Executable (*)");
#elif defined(Q_OS_MACOS)
exePath = QFileDialog::getOpenFileName(this, tr("Select executable"), QDir::rootPath(),
tr("Executable (*.*)"));
"Executable (*.*)");
#endif
if (exePath.isEmpty())
return;
bool ok;
QString folderName =
QString version_name =
QInputDialog::getText(this, tr("Version name"),
tr("Enter the name of this version as it appears in the list."),
QLineEdit::Normal, "", &ok);
if (!ok || folderName.trimmed().isEmpty())
if (!ok || version_name.trimmed().isEmpty())
return;
folderName = folderName.trimmed();
version_name = version_name.trimmed();
QString basePath = m_gui_settings->GetValue(gui::vm_versionPath).toString();
QString newFolderPath = QDir(basePath).filePath(folderName);
QDir dir;
if (dir.exists(newFolderPath)) {
QMessageBox::warning(this, tr("Error"), tr("A folder with that name already exists."));
if (std::find_if(version_list.cbegin(), version_list.cend(), [version_name](auto i) {
return i.name == version_name.toStdString();
}) != version_list.cend()) {
QMessageBox::warning(this, tr("Error"), tr("A version with that name already exists."));
return;
}
if (!dir.mkpath(newFolderPath)) {
QMessageBox::critical(this, tr("Error"), tr("Failed to create folder."));
return;
}
QFileInfo exeInfo(exePath);
QString targetFilePath = QDir(newFolderPath).filePath(exeInfo.fileName());
if (!QFile::copy(exePath, targetFilePath)) {
QMessageBox::critical(this, tr("Error"), tr("Failed to copy executable."));
return;
}
VersionManager::Version new_version = {
.name = version_name.toStdString(),
.path = exePath.toStdString(),
.date = QDateTime::currentDateTime().toString("yyyy.MM.dd. HH:mm").toStdString(),
.codename = "",
.type = VersionManager::VersionType::Custom,
};
VersionManager::AddNewVersion(new_version);
QMessageBox::information(this, tr("Success"), tr("Version added successfully."));
LoadInstalledList();
@@ -149,21 +146,13 @@ VersionDialog::VersionDialog(std::shared_ptr<gui_settings> gui_settings, QWidget
QMessageBox::critical(this, tr("Error"), tr("Failed to determine the folder path."));
return;
}
QString folderName = QDir(fullPath).dirName();
auto reply = QMessageBox::question(this, tr("Delete version"),
tr("Do you want to delete the version") +
QString(" \"%1\" ?").arg(folderName),
QString(" \"%1\" ?").arg(selectedItem->text(1)),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
QDir dirToRemove(fullPath);
if (dirToRemove.exists()) {
if (!dirToRemove.removeRecursively()) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to delete folder.") +
QString("\n \"%1\"").arg(folderName));
return;
}
}
// not removing any files, as that might be a problem with local ones
VersionManager::RemoveVersion(selectedItem->text(1).toStdString());
LoadInstalledList();
}
});
@@ -339,7 +328,7 @@ tr("First you need to choose a location to save the versions in\n'Path to save v
.arg(versionName);
}
{ // Menssage yes/no
{ // Message yes/no
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, tr("Confirm Download"),
tr("Do you want to download the version") +
@@ -514,7 +503,8 @@ tr("First you need to choose a location to save the versions in\n'Path to save v
QProcess::startDetached(process, args);
QTimer::singleShot(
4000, this, [this, folderName, progressDialog, versionName]() {
4000, this,
[this, folderName, progressDialog, versionName, release]() {
progressDialog->close();
progressDialog->deleteLater();
@@ -522,13 +512,35 @@ tr("First you need to choose a location to save the versions in\n'Path to save v
m_gui_settings->GetValue(gui::vm_versionPath).toString();
QString fullPath = QDir(userPath).filePath(folderName);
m_gui_settings->SetValue(gui::vm_versionSelected, fullPath);
QMessageBox::information(
this, tr("Confirm Download"),
tr("Version %1 has been downloaded and selected.")
.arg(versionName));
bool is_release = !versionName.contains("Pre-release");
auto release_name = release["name"].toString();
QString code_name = "";
static constexpr QStringView marker = u" - codename ";
int idx = release_name.indexOf(u" - codename ");
if (idx != -1) {
code_name = release_name.mid(idx + marker.size());
}
std::filesystem::path exe_path =
*std::filesystem::directory_iterator{
fullPath.toStdString()};
VersionManager::Version new_version{
.name = versionName.toStdString(),
.path = exe_path.generic_string(),
.date = release["published_at"]
.toString()
.left(10)
.toStdString(),
.codename = code_name.toStdString(),
.type = is_release ? VersionManager::VersionType::Release
: VersionManager::VersionType::Nightly,
};
m_gui_settings->SetValue(gui::vm_versionSelected,
QString(exe_path.c_str()));
VersionManager::AddNewVersion(new_version);
LoadInstalledList();
});
} else {
@@ -543,110 +555,24 @@ tr("First you need to choose a location to save the versions in\n'Path to save v
}
void VersionDialog::LoadInstalledList() {
QString path = m_gui_settings->GetValue(gui::vm_versionPath).toString();
QDir dir(path);
if (!dir.exists() || path.isEmpty())
return;
const auto path = Common::FS::GetUserPath(Common::FS::PathType::LauncherDir) / "versions.json";
auto versions = VersionManager::GetVersionList(path);
auto const& selected_version =
m_gui_settings->GetValue(gui::vm_versionSelected).toString().toStdString();
ui->installedTreeWidget->clear();
ui->installedTreeWidget->setColumnCount(5);
ui->installedTreeWidget->setColumnHidden(4, true);
QStringList folders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QRegularExpression versionRegex("^v(\\d+)\\.(\\d+)\\.(\\d+)$");
QVector<QPair<QVector<int>, QString>> versionedDirs;
QStringList otherDirs;
QString savedVersionPath = m_gui_settings->GetValue(gui::vm_versionSelected).toString();
for (const QString& folder : folders) {
if (folder == "Pre-release") {
otherDirs.append(folder);
continue;
}
QRegularExpressionMatch match = versionRegex.match(folder.section(" - ", 0, 0));
if (match.hasMatch()) {
QVector<int> versionParts = {match.captured(1).toInt(), match.captured(2).toInt(),
match.captured(3).toInt()};
versionedDirs.append({versionParts, folder});
} else {
otherDirs.append(folder);
}
}
std::sort(otherDirs.begin(), otherDirs.end());
std::sort(versionedDirs.begin(), versionedDirs.end(), [](const auto& a, const auto& b) {
if (a.first[0] != b.first[0])
return a.first[0] > b.first[0];
if (a.first[1] != b.first[1])
return a.first[1] > b.first[1];
return a.first[2] > b.first[2];
});
// Add (Pre-release, Test Build...)
for (const QString& folder : otherDirs) {
for (auto const& v : versions) {
QTreeWidgetItem* item = new QTreeWidgetItem(ui->installedTreeWidget);
QString fullPath = QDir(path).filePath(folder);
item->setText(4, fullPath);
item->setCheckState(0, Qt::Unchecked);
if (folder.startsWith("Pre-release-shadPS4")) {
QStringList parts = folder.split('-');
item->setText(1, "Pre-release");
QString shortHash;
if (parts.size() >= 7) {
shortHash = parts[6].left(7);
} else {
shortHash = "";
}
item->setText(2, shortHash);
if (parts.size() >= 6) {
QString date = QString("%1-%2-%3").arg(parts[3], parts[4], parts[5]);
item->setText(3, date);
} else {
item->setText(3, "");
}
} else if (folder.contains(" - ")) {
QStringList parts = folder.split(" - ");
item->setText(1, parts.value(0));
item->setText(2, parts.value(1));
item->setText(3, parts.value(2));
} else {
item->setText(1, folder);
item->setText(2, "");
item->setText(3, "");
}
if (fullPath == savedVersionPath) {
item->setCheckState(0, Qt::Checked);
}
item->setText(1, QString::fromStdString(v.name));
item->setText(2, QString::fromStdString(v.codename));
item->setText(3, QString::fromStdString(v.date));
item->setText(4, QString::fromStdString(v.path));
item->setCheckState(0, (selected_version == v.path) ? Qt::Checked : Qt::Unchecked);
}
// Add versions
for (const auto& pair : versionedDirs) {
QTreeWidgetItem* item = new QTreeWidgetItem(ui->installedTreeWidget);
QString fullPath = QDir(path).filePath(pair.second);
item->setText(4, fullPath);
item->setCheckState(0, Qt::Unchecked);
if (pair.second.contains(" - ")) {
QStringList parts = pair.second.split(" - ");
item->setText(1, parts.value(0));
item->setText(2, parts.value(1));
item->setText(3, parts.value(2));
} else {
item->setText(1, pair.second);
item->setText(2, "");
item->setText(3, "");
}
if (fullPath == savedVersionPath) {
item->setCheckState(0, Qt::Checked);
}
}
ui->installedTreeWidget->resizeColumnToContents(0);
ui->installedTreeWidget->resizeColumnToContents(1);
ui->installedTreeWidget->resizeColumnToContents(2);