diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f84ac330..b06a9c2d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,10 @@ on: pull_request: branches: [ "*" ] +concurrency: + group: ci-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'push' }} + env: BUILD_TYPE: Release diff --git a/documents/building-linux.md b/documents/building-linux.md index 6c2794347..989669f4f 100644 --- a/documents/building-linux.md +++ b/documents/building-linux.md @@ -14,7 +14,7 @@ sudo apt install build-essential clang git cmake libasound2-dev libpulse-dev lib #### Fedora ``` -sudo dnf install clang cmake libatomic alsa-lib-devel pipewire-jack-audio-connection-kit-devel openal-devel openssl-devel libevdev-devel libudev-devel qt6-qtbase-devel qt6-qtbase-private-devel qt6-qtmultimedia-devel qt6-qtsvg-devel qt6-qttools-devel vulkan-devel vulkan-validation-layers +sudo dnf install clang git cmake libatomic alsa-lib-devel pipewire-jack-audio-connection-kit-devel openal-devel openssl-devel libevdev-devel libudev-devel libXext-devel qt6-qtbase-devel qt6-qtbase-private-devel qt6-qtmultimedia-devel qt6-qtsvg-devel qt6-qttools-devel vulkan-devel vulkan-validation-layers ``` #### Arch Linux diff --git a/src/audio_core/sdl_audio.cpp b/src/audio_core/sdl_audio.cpp index 9aec70c24..bd27e4823 100644 --- a/src/audio_core/sdl_audio.cpp +++ b/src/audio_core/sdl_audio.cpp @@ -100,6 +100,7 @@ s32 SDLAudio::AudioOutOutput(s32 handle, const void* ptr) { if (ptr == nullptr) { return 0; } + lock.unlock(); // TODO mixing channels SDL_bool result = SDL_PutAudioStreamData( port.stream, ptr, port.samples_num * port.sample_size * port.channels_num); diff --git a/src/common/config.cpp b/src/common/config.cpp index 0d9ff093d..f04678eb7 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -8,6 +8,7 @@ #include // for wstring support #include #include "common/logging/formatter.h" +#include "common/path_util.h" #include "config.h" namespace toml { @@ -59,6 +60,7 @@ static bool vkCrashDiagnostic = false; // Gui std::filesystem::path settings_install_dir = {}; +std::filesystem::path settings_addon_install_dir = {}; u32 main_window_geometry_x = 400; u32 main_window_geometry_y = 400; u32 main_window_geometry_w = 1280; @@ -299,6 +301,9 @@ void setMainWindowGeometry(u32 x, u32 y, u32 w, u32 h) { void setGameInstallDir(const std::filesystem::path& dir) { settings_install_dir = dir; } +void setAddonInstallDir(const std::filesystem::path& dir) { + settings_addon_install_dir = dir; +} void setMainWindowTheme(u32 theme) { mw_themes = theme; } @@ -355,6 +360,13 @@ u32 getMainWindowGeometryH() { std::filesystem::path getGameInstallDir() { return settings_install_dir; } +std::filesystem::path getAddonInstallDir() { + if (settings_addon_install_dir.empty()) { + // Default for users without a config file or a config file from before this option existed + return Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "addcont"; + } + return settings_addon_install_dir; +} u32 getMainWindowTheme() { return mw_themes; } @@ -482,6 +494,7 @@ void load(const std::filesystem::path& path) { m_window_size_W = toml::find_or(gui, "mw_width", 0); m_window_size_H = toml::find_or(gui, "mw_height", 0); settings_install_dir = toml::find_fs_path_or(gui, "installDir", {}); + settings_addon_install_dir = toml::find_fs_path_or(gui, "addonInstallDir", {}); main_window_geometry_x = toml::find_or(gui, "geometry_x", 0); main_window_geometry_y = toml::find_or(gui, "geometry_y", 0); main_window_geometry_w = toml::find_or(gui, "geometry_w", 0); @@ -556,6 +569,8 @@ void save(const std::filesystem::path& path) { data["GUI"]["mw_width"] = m_window_size_W; data["GUI"]["mw_height"] = m_window_size_H; data["GUI"]["installDir"] = std::string{fmt::UTF(settings_install_dir.u8string()).data}; + data["GUI"]["addonInstallDir"] = + std::string{fmt::UTF(settings_addon_install_dir.u8string()).data}; data["GUI"]["geometry_x"] = main_window_geometry_x; data["GUI"]["geometry_y"] = main_window_geometry_y; data["GUI"]["geometry_w"] = main_window_geometry_w; diff --git a/src/common/config.h b/src/common/config.h index eeeff0b2e..402b8660e 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -76,6 +76,7 @@ bool vkCrashDiagnosticEnabled(); // Gui void setMainWindowGeometry(u32 x, u32 y, u32 w, u32 h); void setGameInstallDir(const std::filesystem::path& dir); +void setAddonInstallDir(const std::filesystem::path& dir); void setMainWindowTheme(u32 theme); void setIconSize(u32 size); void setIconSizeGrid(u32 size); @@ -94,6 +95,7 @@ u32 getMainWindowGeometryY(); u32 getMainWindowGeometryW(); u32 getMainWindowGeometryH(); std::filesystem::path getGameInstallDir(); +std::filesystem::path getAddonInstallDir(); u32 getMainWindowTheme(); u32 getIconSize(); u32 getIconSizeGrid(); diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index d7274fc74..f602f3513 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -119,7 +119,6 @@ static auto UserPaths = [] { create_path(PathType::CapturesDir, user_dir / CAPTURES_DIR); create_path(PathType::CheatsDir, user_dir / CHEATS_DIR); create_path(PathType::PatchesDir, user_dir / PATCHES_DIR); - create_path(PathType::AddonsDir, user_dir / ADDONS_DIR); create_path(PathType::MetaDataDir, user_dir / METADATA_DIR); return paths; diff --git a/src/common/path_util.h b/src/common/path_util.h index d40f4aab4..af0e91832 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -26,7 +26,6 @@ enum class PathType { CapturesDir, // Where rdoc captures are stored. CheatsDir, // Where cheats are stored. PatchesDir, // Where patches are stored. - AddonsDir, // Where additional content is stored. MetaDataDir, // Where game metadata (e.g. trophies and menu backgrounds) is stored. }; diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp index a1b47dfc8..9c53c8588 100644 --- a/src/core/file_format/trp.cpp +++ b/src/core/file_format/trp.cpp @@ -69,7 +69,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath) { file.Read(entry); std::string_view name(entry.entry_name); if (entry.flag == 0 && name.find("TROP") != std::string::npos) { // PNG - if (file.Seek(entry.entry_pos)) { + if (!file.Seek(entry.entry_pos)) { LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); return false; } @@ -79,7 +79,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath) { } if (entry.flag == 3 && np_comm_id[0] == 'N' && np_comm_id[1] == 'P') { // ESFM, encrypted. - if (file.Seek(entry.entry_pos)) { + if (!file.Seek(entry.entry_pos)) { LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); return false; } @@ -88,7 +88,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath) { // clean xml file. std::vector ESFM(entry.entry_len - iv_len); std::vector XML(entry.entry_len - iv_len); - if (file.Seek(entry.entry_pos + iv_len)) { + if (!file.Seek(entry.entry_pos + iv_len)) { LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset"); return false; } diff --git a/src/core/libraries/app_content/app_content.cpp b/src/core/libraries/app_content/app_content.cpp index 754343eef..f912639eb 100644 --- a/src/core/libraries/app_content/app_content.cpp +++ b/src/core/libraries/app_content/app_content.cpp @@ -5,6 +5,7 @@ #include "app_content.h" #include "common/assert.h" +#include "common/config.h" #include "common/io_file.h" #include "common/logging/log.h" #include "common/path_util.h" @@ -59,8 +60,7 @@ int PS4_SYSV_ABI sceAppContentAddcontMount(u32 service_label, OrbisAppContentMountPoint* mount_point) { LOG_INFO(Lib_AppContent, "called"); - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::AddonsDir) / title_id / - entitlement_label->data; + const auto& mount_dir = Config::getAddonInstallDir() / title_id / entitlement_label->data; auto* mnt = Common::Singleton::Instance(); for (int i = 0; i < addcont_count; i++) { @@ -246,7 +246,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar LOG_ERROR(Lib_AppContent, "(DUMMY) called"); auto* param_sfo = Common::Singleton::Instance(); - const auto addons_dir = Common::FS::GetUserPath(Common::FS::PathType::AddonsDir); + const auto addons_dir = Config::getAddonInstallDir(); if (const auto value = param_sfo->GetString("TITLE_ID"); value.has_value()) { title_id = *value; } else { diff --git a/src/qt_gui/cheats_patches.cpp b/src/qt_gui/cheats_patches.cpp index 655478d2b..c044c2c3c 100644 --- a/src/qt_gui/cheats_patches.cpp +++ b/src/qt_gui/cheats_patches.cpp @@ -456,10 +456,9 @@ void CheatsPatches::downloadCheats(const QString& source, const QString& gameSer if (source == "GoldHEN") { url = "https://raw.githubusercontent.com/GoldHEN/GoldHEN_Cheat_Repository/main/json.txt"; } else if (source == "wolf2022") { - url = "https://wolf2022.ir/trainer/" + gameSerial + "_" + gameVersion + ".json"; + url = "https://wolf2022.ir/trainer/list.json"; } else if (source == "shadPS4") { - url = "https://raw.githubusercontent.com/shadps4-emu/ps4_cheats/main/" - "CHEATS_JSON.txt"; + url = "https://raw.githubusercontent.com/shadps4-emu/ps4_cheats/main/CHEATS_JSON.txt"; } else { QMessageBox::warning(this, tr("Invalid Source"), QString(tr("The selected source is invalid.") + "\n%1").arg(source)); @@ -474,44 +473,32 @@ void CheatsPatches::downloadCheats(const QString& source, const QString& gameSer QByteArray jsonData = reply->readAll(); bool foundFiles = false; - if (source == "GoldHEN" || source == "shadPS4") { - QString textContent(jsonData); - QRegularExpression regex( - QString("%1_%2[^=]*\\.json").arg(gameSerial).arg(gameVersion)); - QRegularExpressionMatchIterator matches = regex.globalMatch(textContent); - QString baseUrl; + if (source == "wolf2022") { + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData); + QJsonArray gamesArray = jsonDoc.object().value("games").toArray(); - if (source == "GoldHEN") { - baseUrl = "https://raw.githubusercontent.com/GoldHEN/GoldHEN_Cheat_Repository/" - "main/json/"; - } else { - baseUrl = "https://raw.githubusercontent.com/shadps4-emu/ps4_cheats/" - "main/CHEATS/"; - } + foreach (const QJsonValue& value, gamesArray) { + QJsonObject gameObject = value.toObject(); + QString title = gameObject.value("title").toString(); + QString version = gameObject.value("version").toString(); - while (matches.hasNext()) { - QRegularExpressionMatch match = matches.next(); - QString fileName = match.captured(0); + if (title == gameSerial && + (version == gameVersion || version == gameVersion.mid(1))) { + QString fileUrl = + "https://wolf2022.ir/trainer/" + gameObject.value("url").toString(); - if (!fileName.isEmpty()) { - QString newFileName = fileName; - int dotIndex = newFileName.lastIndexOf('.'); - if (dotIndex != -1) { + QString localFileName = gameObject.value("url").toString(); + localFileName = + localFileName.left(localFileName.lastIndexOf('.')) + "_wolf2022.json"; - if (source == "GoldHEN") { - newFileName.insert(dotIndex, "_GoldHEN"); - } else { - newFileName.insert(dotIndex, "_shadPS4"); - } - } - QString fileUrl = baseUrl + fileName; - QString localFilePath = dir.filePath(newFileName); + QString localFilePath = dir.filePath(localFileName); if (QFile::exists(localFilePath) && showMessageBox) { QMessageBox::StandardButton reply; reply = QMessageBox::question( this, tr("File Exists"), - tr("File already exists. Do you want to replace it?"), + tr("File already exists. Do you want to replace it?") + "\n" + + localFileName, QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { continue; @@ -549,38 +536,81 @@ void CheatsPatches::downloadCheats(const QString& source, const QString& gameSer if (!foundFiles && showMessageBox) { QMessageBox::warning(this, tr("Cheats Not Found"), tr("CheatsNotFound_MSG")); } - } else if (source == "wolf2022") { - QString fileName = QFileInfo(QUrl(url).path()).fileName(); - QString baseFileName = fileName; - int dotIndex = baseFileName.lastIndexOf('.'); - if (dotIndex != -1) { - baseFileName.insert(dotIndex, "_wolf2022"); + } else if (source == "GoldHEN" || source == "shadPS4") { + QString textContent(jsonData); + QRegularExpression regex( + QString("%1_%2[^=]*\\.json").arg(gameSerial).arg(gameVersion)); + QRegularExpressionMatchIterator matches = regex.globalMatch(textContent); + QString baseUrl; + + if (source == "GoldHEN") { + baseUrl = "https://raw.githubusercontent.com/GoldHEN/GoldHEN_Cheat_Repository/" + "main/json/"; + } else { + baseUrl = "https://raw.githubusercontent.com/shadps4-emu/ps4_cheats/" + "main/CHEATS/"; } - QString filePath; - Common::FS::PathToQString(filePath, - Common::FS::GetUserPath(Common::FS::PathType::CheatsDir)); - filePath += "/" + baseFileName; - if (QFile::exists(filePath) && showMessageBox) { - QMessageBox::StandardButton reply2; - reply2 = - QMessageBox::question(this, tr("File Exists"), - tr("File already exists. Do you want to replace it?"), - QMessageBox::Yes | QMessageBox::No); - if (reply2 == QMessageBox::No) { - reply->deleteLater(); - return; + + while (matches.hasNext()) { + QRegularExpressionMatch match = matches.next(); + QString fileName = match.captured(0); + + if (!fileName.isEmpty()) { + QString newFileName = fileName; + int dotIndex = newFileName.lastIndexOf('.'); + if (dotIndex != -1) { + + if (source == "GoldHEN") { + newFileName.insert(dotIndex, "_GoldHEN"); + } else { + newFileName.insert(dotIndex, "_shadPS4"); + } + } + QString fileUrl = baseUrl + fileName; + QString localFilePath = dir.filePath(newFileName); + + if (QFile::exists(localFilePath) && showMessageBox) { + QMessageBox::StandardButton reply; + reply = QMessageBox::question( + this, tr("File Exists"), + tr("File already exists. Do you want to replace it?") + "\n" + + newFileName, + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::No) { + continue; + } + } + QNetworkRequest fileRequest(fileUrl); + QNetworkReply* fileReply = manager->get(fileRequest); + + connect(fileReply, &QNetworkReply::finished, [=, this]() { + if (fileReply->error() == QNetworkReply::NoError) { + QByteArray fileData = fileReply->readAll(); + QFile localFile(localFilePath); + if (localFile.open(QIODevice::WriteOnly)) { + localFile.write(fileData); + localFile.close(); + } else { + QMessageBox::warning( + this, tr("Error"), + QString(tr("Failed to save file:") + "\n%1") + .arg(localFilePath)); + } + } else { + QMessageBox::warning(this, tr("Error"), + QString(tr("Failed to download file:") + + "%1\n\n" + tr("Error:") + "%2") + .arg(fileUrl) + .arg(fileReply->errorString())); + } + fileReply->deleteLater(); + }); + + foundFiles = true; } } - QFile cheatFile(filePath); - if (cheatFile.open(QIODevice::WriteOnly)) { - cheatFile.write(jsonData); - cheatFile.close(); - foundFiles = true; - populateFileListCheats(); - } else { - QMessageBox::warning( - this, tr("Error"), - QString(tr("Failed to save file:") + "\n%1").arg(filePath)); + if (!foundFiles && showMessageBox) { + QMessageBox::warning(this, tr("Cheats Not Found"), tr("CheatsNotFound_MSG")); } } if (foundFiles && showMessageBox) { @@ -910,11 +940,16 @@ void CheatsPatches::addCheatsToLayout(const QJsonArray& modsArray, const QJsonAr void CheatsPatches::populateFileListCheats() { QString cheatsDir; Common::FS::PathToQString(cheatsDir, Common::FS::GetUserPath(Common::FS::PathType::CheatsDir)); - QString pattern = m_gameSerial + "_" + m_gameVersion + "*.json"; + + QString fullGameVersion = m_gameVersion; + QString modifiedGameVersion = m_gameVersion.mid(1); + + QString patternWithFirstChar = m_gameSerial + "_" + fullGameVersion + "*.json"; + QString patternWithoutFirstChar = m_gameSerial + "_" + modifiedGameVersion + "*.json"; QDir dir(cheatsDir); QStringList filters; - filters << pattern; + filters << patternWithFirstChar << patternWithoutFirstChar; dir.setNameFilters(filters); QFileInfoList fileList = dir.entryInfoList(QDir::Files); @@ -1248,4 +1283,4 @@ void CheatsPatches::onPatchCheckBoxHovered(QCheckBox* checkBox, bool hovered) { } else { instructionsTextEdit->setText(defaultTextEdit); } -} +} \ No newline at end of file diff --git a/src/qt_gui/game_install_dialog.cpp b/src/qt_gui/game_install_dialog.cpp index d8cc7a837..8f7ffd5d1 100644 --- a/src/qt_gui/game_install_dialog.cpp +++ b/src/qt_gui/game_install_dialog.cpp @@ -18,6 +18,7 @@ GameInstallDialog::GameInstallDialog() : m_gamesDirectory(nullptr) { auto layout = new QVBoxLayout(this); layout->addWidget(SetupGamesDirectory()); + layout->addWidget(SetupAddonsDirectory()); layout->addStretch(); layout->addWidget(SetupDialogActions()); @@ -27,7 +28,7 @@ GameInstallDialog::GameInstallDialog() : m_gamesDirectory(nullptr) { GameInstallDialog::~GameInstallDialog() {} -void GameInstallDialog::Browse() { +void GameInstallDialog::BrowseGamesDirectory() { auto path = QFileDialog::getExistingDirectory(this, tr("Directory to install games")); if (!path.isEmpty()) { @@ -35,6 +36,14 @@ void GameInstallDialog::Browse() { } } +void GameInstallDialog::BrowseAddonsDirectory() { + auto path = QFileDialog::getExistingDirectory(this, tr("Directory to install DLC")); + + if (!path.isEmpty()) { + m_addonsDirectory->setText(QDir::toNativeSeparators(path)); + } +} + QWidget* GameInstallDialog::SetupGamesDirectory() { auto group = new QGroupBox(tr("Directory to install games")); auto layout = new QHBoxLayout(group); @@ -51,7 +60,30 @@ QWidget* GameInstallDialog::SetupGamesDirectory() { // Browse button. auto browse = new QPushButton(tr("Browse")); - connect(browse, &QPushButton::clicked, this, &GameInstallDialog::Browse); + connect(browse, &QPushButton::clicked, this, &GameInstallDialog::BrowseGamesDirectory); + + layout->addWidget(browse); + + return group; +} + +QWidget* GameInstallDialog::SetupAddonsDirectory() { + auto group = new QGroupBox(tr("Directory to install DLC")); + auto layout = new QHBoxLayout(group); + + // Input. + m_addonsDirectory = new QLineEdit(); + QString install_dir; + Common::FS::PathToQString(install_dir, Config::getAddonInstallDir()); + m_addonsDirectory->setText(install_dir); + m_addonsDirectory->setMinimumWidth(400); + + layout->addWidget(m_addonsDirectory); + + // Browse button. + auto browse = new QPushButton(tr("Browse")); + + connect(browse, &QPushButton::clicked, this, &GameInstallDialog::BrowseAddonsDirectory); layout->addWidget(browse); @@ -70,6 +102,7 @@ QWidget* GameInstallDialog::SetupDialogActions() { void GameInstallDialog::Save() { // Check games directory. auto gamesDirectory = m_gamesDirectory->text(); + auto addonsDirectory = m_addonsDirectory->text(); if (gamesDirectory.isEmpty() || !QDir(gamesDirectory).exists() || !QDir::isAbsolutePath(gamesDirectory)) { @@ -78,7 +111,15 @@ void GameInstallDialog::Save() { return; } + if (addonsDirectory.isEmpty() || !QDir(addonsDirectory).exists() || + !QDir::isAbsolutePath(addonsDirectory)) { + QMessageBox::critical(this, tr("Error"), + "The value for location to install DLC is not valid."); + return; + } + Config::setGameInstallDir(Common::FS::PathFromQString(gamesDirectory)); + Config::setAddonInstallDir(Common::FS::PathFromQString(addonsDirectory)); const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::save(config_dir / "config.toml"); accept(); diff --git a/src/qt_gui/game_install_dialog.h b/src/qt_gui/game_install_dialog.h index 6f439e81d..0a4e29357 100644 --- a/src/qt_gui/game_install_dialog.h +++ b/src/qt_gui/game_install_dialog.h @@ -16,13 +16,16 @@ public: ~GameInstallDialog(); private slots: - void Browse(); + void BrowseGamesDirectory(); + void BrowseAddonsDirectory(); private: QWidget* SetupGamesDirectory(); + QWidget* SetupAddonsDirectory(); QWidget* SetupDialogActions(); void Save(); private: QLineEdit* m_gamesDirectory; + QLineEdit* m_addonsDirectory; }; \ No newline at end of file diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 837cdece3..6f4b8ae7e 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -687,8 +687,8 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int } std::string entitlement_label = Common::SplitString(content_id, '-')[2]; - auto addon_extract_path = Common::FS::GetUserPath(Common::FS::PathType::AddonsDir) / - pkg.GetTitleID() / entitlement_label; + auto addon_extract_path = + Config::getAddonInstallDir() / pkg.GetTitleID() / entitlement_label; QString addonDirPath; Common::FS::PathToQString(addonDirPath, addon_extract_path); QDir addon_dir(addonDirPath);