PKGInstall/mainWindow.cpp

594 lines
23 KiB
C++

#include <QFileDialog>
#include <QFutureWatcher>
#include <QMessageBox>
#include <QProgressBar>
#include <QProgressDialog>
#include <QStyle>
#include <QStyleHints>
#include <QtConcurrent/QtConcurrentMap>
#include <toml.hpp>
#include "./ui_mainWindow.h"
#include "mainWindow.h"
#include "src/loader.h"
#ifndef MAX_PATH
#ifdef _WIN32
#include <Shlobj.h>
#include <windows.h>
// This is the maximum number of UTF-16 code units permissible in Windows file paths
#define MAX_PATH 260
#else
// This is the maximum number of UTF-8 code units permissible in all other OSes' file paths
#define MAX_PATH 1024
#endif
#endif
namespace toml {
template <typename TC, typename K>
std::filesystem::path find_fs_path_or(const basic_value<TC>& v, const K& ky,
std::filesystem::path opt) {
try {
auto str = find<std::string>(v, ky);
if (str.empty()) {
return opt;
}
std::u8string u8str{(char8_t*)&str.front(), (char8_t*)&str.back() + 1};
return std::filesystem::path{u8str};
} catch (...) {
return opt;
}
}
} // namespace toml
namespace fmt {
template <typename T = std::string_view>
struct UTF {
T data;
explicit UTF(const std::u8string_view view) {
data = view.empty() ? T{} : T{(const char*)&view.front(), (const char*)&view.back() + 1};
}
explicit UTF(const std::u8string& str) : UTF(std::u8string_view{str}) {}
};
} // namespace fmt
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
this->setWindowTitle("PKGInstall");
this->setFixedSize(this->width(), this->height());
GetSettingsFileLocation();
LoadSettings();
connect(ui->browsePkgButton, &QPushButton::clicked, this, &MainWindow::pkgButtonClicked);
connect(ui->browseFolderButton, &QPushButton::clicked, this, &MainWindow::folderButtonClicked);
connect(ui->dlcFolderButton, &QPushButton::clicked, this, &MainWindow::dlcButtonClicked);
connect(ui->closeButton, &QPushButton::clicked, this, &MainWindow::close);
connect(ui->extractButton, &QPushButton::clicked, this,
[this]() { InstallDragDropPkg(pkgPath); });
connect(ui->settingsButton, &QPushButton::clicked, this, [this]() { SaveSettings(); });
connect(ui->loadConfigButton, &QPushButton::clicked, this,
[this]() { LoadFoldersFromShadps4File(); });
connect(ui->setOutputButton, &QPushButton::clicked, this, [this]() {
if (!ui->folderComboBox->currentText().isEmpty()) {
ui->outputLineEdit->setText(ui->folderComboBox->currentText());
outputPath = PathFromQString(ui->folderComboBox->currentText());
} else {
QMessageBox::information(
this, "Error",
"Folder list is empty, load a shadPS4 config.toml file to get list.");
}
});
connect(ui->separateUpdateCheckBox, &QCheckBox::checkStateChanged, this,
[this](Qt::CheckState state) { useSeparateUpdate = state; });
}
void MainWindow::GetSettingsFileLocation() {
#if defined(__linux__)
const char* xdg_data_home = getenv("XDG_DATA_HOME");
if (xdg_data_home != nullptr && strlen(xdg_data_home) > 0) {
settingsFile = std::filesystem::path(xdg_data_home) / "PKGInstall" / "settings.toml";
} else {
settingsFile = std::filesystem::path(getenv("HOME")) / ".local" / "share" / "PKGInstall" /
"settings.toml";
}
#elif _WIN32
TCHAR appdata[MAX_PATH] = {0};
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, appdata);
settingsFile = std::filesystem::path(appdata) / "PKGInstall" / "settings.toml";
#endif
}
void MainWindow::LoadSettings() {
if (!std::filesystem::exists(settingsFile.parent_path())) {
std::filesystem::create_directories(settingsFile.parent_path());
}
if (!std::filesystem::exists(settingsFile))
return;
toml::value data;
try {
std::ifstream ifs;
ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
ifs.open(settingsFile, std::ios_base::binary);
data = toml::parse(ifs, std::string{fmt::UTF(settingsFile.filename().u8string()).data});
} catch (std::exception& ex) {
QMessageBox::critical(NULL, "Filesystem error", ex.what());
return;
}
useSeparateUpdate = toml::find_or<bool>(data, "Settings", "UseSeparateUpdateFolder", true);
ui->separateUpdateCheckBox->setChecked(useSeparateUpdate);
if (data.contains("Paths")) {
const toml::value& launcher = data.at("Paths");
outputPath = toml::find_fs_path_or(launcher, "outputPath", {});
dlcPath = toml::find_fs_path_or(launcher, "dlcPath", {});
}
QString outputPathString;
PathToQString(outputPathString, outputPath);
ui->outputLineEdit->setText(outputPathString);
QString dlcPathString;
PathToQString(dlcPathString, dlcPath);
ui->dlcLineEdit->setText(dlcPathString);
std::vector<std::string> install_dirs;
if (data.contains("ShadPS4InstallFolders")) {
const toml::value& installFolders = data.at("ShadPS4InstallFolders");
toml::array arr = toml::find_or<toml::array>(installFolders, "Folders", {});
for (const auto& folder : arr) {
if (folder.is_string()) {
install_dirs.push_back(folder.as_string());
}
}
}
for (size_t i = 0; i < install_dirs.size(); i++) {
QString path_string;
PathToQString(path_string, install_dirs[i]);
ui->folderComboBox->addItem(path_string);
}
}
void MainWindow::SaveSettings() {
toml::value data;
try {
std::ifstream ifs;
ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
ifs.open(settingsFile, std::ios_base::binary);
data = toml::parse(ifs, std::string{fmt::UTF(settingsFile.filename().u8string()).data});
} catch (std::exception& ex) {
// if it doesn't exist, the save function will create the file
if (std::filesystem::exists(settingsFile)) {
QMessageBox::critical(NULL, "Filesystem error", ex.what());
return;
}
}
std::vector<std::string> game_dirs;
for (int i = 0; i < ui->folderComboBox->count(); ++i) {
std::string itemText = ui->folderComboBox->itemText(i).toStdString();
game_dirs.push_back(itemText);
}
data["ShadPS4InstallFolders"]["Folders"] = game_dirs;
data["Paths"]["outputPath"] = std::string{fmt::UTF(outputPath.u8string()).data};
data["Paths"]["dlcPath"] = std::string{fmt::UTF(dlcPath.u8string()).data};
data["Settings"]["UseSeparateUpdateFolder"] = useSeparateUpdate;
std::ofstream file(settingsFile, std::ios::binary);
file << data;
file.close();
}
void MainWindow::LoadFoldersFromShadps4File() {
QString shadConfig =
QFileDialog::getOpenFileName(this, "Load shadPS4 config.toml file", QDir::currentPath(),
"shadPS4 config file (config.toml)");
if (shadConfig.isEmpty())
return;
std::filesystem::path shadConfigFile = PathFromQString(shadConfig);
toml::value data;
try {
std::ifstream ifs;
ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
ifs.open(shadConfigFile, std::ios_base::binary);
data = toml::parse(ifs, std::string{fmt::UTF(shadConfigFile.filename().u8string()).data});
} catch (std::exception& ex) {
QMessageBox::critical(NULL, "Filesystem error", ex.what());
return;
}
std::vector<std::string> install_dirs;
if (data.contains("GUI")) {
const toml::value& installFolders = data.at("GUI");
toml::array arr = toml::find_or<toml::array>(installFolders, "installDirs", {});
if (!arr.empty()) {
ui->folderComboBox->clear();
} else {
QMessageBox::information(this, "PKGInstall",
"No game install folders found in this file.");
}
for (const auto& folder : arr) {
if (folder.is_string()) {
install_dirs.push_back(folder.as_string());
}
}
dlcPath = toml::find_fs_path_or(installFolders, "addonInstallDir", dlcPath);
QString path;
PathToQString(path, dlcPath);
ui->dlcLineEdit->setText(path);
}
for (size_t i = 0; i < install_dirs.size(); i++) {
QString path_string;
PathToQString(path_string, install_dirs[i]);
ui->folderComboBox->addItem(path_string);
}
}
void MainWindow::folderButtonClicked() {
QString folder = QFileDialog::getExistingDirectory(nullptr,
"Set Output folder",
QDir::homePath());
ui->outputLineEdit->setText(folder);
outputPath = PathFromQString(folder);
}
void MainWindow::dlcButtonClicked() {
QString folder = QFileDialog::getExistingDirectory(nullptr, "Set DLC folder", QDir::homePath());
ui->dlcLineEdit->setText(folder);
dlcPath = PathFromQString(folder);
}
void MainWindow::pkgButtonClicked() {
QString file = QFileDialog::getOpenFileName(nullptr,
"Set Output folder",
QDir::homePath(),
"PKGs (*.pkg)");
pkgPath = PathFromQString(file);
ui->pkgLineEdit->setText(file);
}
void MainWindow::InstallDragDropPkg(std::filesystem::path file) {
if (!std::filesystem::exists(pkgPath) || !std::filesystem::exists(outputPath)) {
QMessageBox::information(this, "Error", "Existing PKG file and output folder must be set");
return;
}
if (Loader::DetectFileType(file) == Loader::FileTypes::Pkg) {
std::string failreason;
pkg = PKG();
if (!pkg.Open(file, failreason)) {
QMessageBox::critical(nullptr, tr("PKG ERROR"), QString::fromStdString(failreason));
return;
}
if (!psf.Open(pkg.sfo)) {
QMessageBox::critical(nullptr,
tr("PKG ERROR"),
"Could not read SFO. Check log for details");
return;
}
auto category = psf.GetString("CATEGORY");
std::filesystem::path game_install_dir = outputPath;
QString pkgType = QString::fromStdString(pkg.GetPkgFlags());
bool use_game_update = pkgType.contains("PATCH") && useSeparateUpdate;
// Default paths
auto game_folder_path = game_install_dir / pkg.GetTitleID();
auto game_update_path = use_game_update ? game_folder_path.parent_path()
/ (std::string{pkg.GetTitleID()} + "-patch")
: game_folder_path;
const int max_depth = 5;
if (pkgType.contains("PATCH")) {
// For patches, try to find the game recursively
auto found_game = FindGameByID(game_install_dir,
std::string{pkg.GetTitleID()},
max_depth);
if (found_game.has_value()) {
game_folder_path = found_game.value().parent_path();
game_update_path = use_game_update
? game_folder_path.parent_path()
/ (std::string{pkg.GetTitleID()} + "-patch")
: game_folder_path;
}
} else {
// For base games, we check if the game is already installed
auto found_game = FindGameByID(game_install_dir,
std::string{pkg.GetTitleID()},
max_depth);
if (found_game.has_value()) {
game_folder_path = found_game.value().parent_path();
}
// If the game is not found, we install it in the game install directory
else {
game_folder_path = game_install_dir / pkg.GetTitleID();
}
game_update_path = use_game_update ? game_folder_path.parent_path()
/ (std::string{pkg.GetTitleID()} + "-patch")
: game_folder_path;
}
QString gameDirPath;
PathToQString(gameDirPath, game_folder_path);
QDir game_dir(gameDirPath);
if (game_dir.exists()) {
QMessageBox msgBox;
msgBox.setWindowTitle(tr("PKG Installation"));
std::string content_id;
if (auto value = psf.GetString("CONTENT_ID"); value.has_value()) {
content_id = std::string{*value};
} else {
QMessageBox::critical(this, tr("PKG ERROR"), "PSF file there is no CONTENT_ID");
return;
}
std::string entitlement_label = SplitString(content_id, '-')[2];
auto addon_extract_path = dlcPath;
QString addonDirPath;
PathToQString(addonDirPath, addon_extract_path);
QDir addon_dir(addonDirPath);
if (pkgType.contains("PATCH")) {
QString pkg_app_version;
if (auto app_ver = psf.GetString("APP_VER"); app_ver.has_value()) {
pkg_app_version = QString::fromStdString(std::string{*app_ver});
} else {
QMessageBox::critical(this, tr("PKG ERROR"), "PSF file there is no APP_VER");
return;
}
std::filesystem::path sce_folder_path
= std::filesystem::exists(game_update_path / "sce_sys" / "param.sfo")
? game_update_path / "sce_sys" / "param.sfo"
: game_folder_path / "sce_sys" / "param.sfo";
psf.Open(sce_folder_path);
QString game_app_version;
if (auto app_ver = psf.GetString("APP_VER"); app_ver.has_value()) {
game_app_version = QString::fromStdString(std::string{*app_ver});
} else {
QMessageBox::critical(this, tr("PKG ERROR"), "PSF file there is no APP_VER");
return;
}
double appD = game_app_version.toDouble();
double pkgD = pkg_app_version.toDouble();
if (pkgD == appD) {
msgBox.setText(QString(tr("Patch detected!") + "\n"
+ tr("PKG and Game versions match: ") + pkg_app_version
+ "\n" + tr("Would you like to overwrite?")));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
} else if (pkgD < appD) {
msgBox.setText(QString(
tr("Patch detected!") + "\n"
+ tr("PKG Version %1 is older than existing version: ").arg(pkg_app_version)
+ game_app_version + "\n" + tr("Would you like to overwrite?")));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
} else {
msgBox.setText(QString(
tr("Patch detected!") + "\n" + tr("Game exists: ") + game_app_version + "\n"
+ tr("Would you like to apply Patch: ") + pkg_app_version + " ?"));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
}
int result = msgBox.exec();
if (result == QMessageBox::Yes) {
// Do nothing.
} else {
return;
}
} else if (category == "ac") {
if (!addon_dir.exists()) {
QMessageBox addonMsgBox;
addonMsgBox.setWindowTitle(tr("DLC Install"));
addonMsgBox.setText(QString(tr("Would you like to install DLC: %1?"))
.arg(QString::fromStdString(entitlement_label)));
addonMsgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
addonMsgBox.setDefaultButton(QMessageBox::No);
int result = addonMsgBox.exec();
if (result == QMessageBox::Yes) {
game_update_path = addon_extract_path;
} else {
return;
}
} else {
msgBox.setText(QString(tr("DLC already installed:") + "\n" + addonDirPath
+ "\n\n" + tr("Would you like to overwrite?")));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
int result = msgBox.exec();
if (result == QMessageBox::Yes) {
game_update_path = addon_extract_path;
} else {
return;
}
}
} else {
msgBox.setText(QString(tr("Game already installed") + "\n" + gameDirPath + "\n"
+ tr("Would you like to overwrite?")));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
int result = msgBox.exec();
if (result == QMessageBox::Yes) {
// Do nothing.
} else {
return;
}
}
} else {
// Do nothing;
if (pkgType.contains("PATCH") || category == "ac") {
QMessageBox::information(
this,
tr("PKG Installation"),
tr("PKG is a patch or DLC, please install base game first!"));
return;
}
// what else?
}
if (!pkg.Extract(file, game_update_path, failreason)) {
QMessageBox::critical(this, tr("PKG ERROR"), QString::fromStdString(failreason));
} else {
int nfiles = pkg.GetNumberOfFiles();
if (nfiles > 0) {
QVector<int> indices;
for (int i = 0; i < nfiles; i++) {
indices.append(i);
}
QProgressDialog dialog;
dialog.setWindowTitle(tr("PKG Installation"));
dialog.setWindowModality(Qt::WindowModal);
QString extractmsg = QString(tr("Installing PKG"));
dialog.setLabelText(extractmsg);
dialog.setAutoClose(true);
dialog.setRange(0, nfiles);
bool isSystemDarkMode;
#if defined(__linux__)
const QPalette defaultPalette;
const auto text = defaultPalette.color(QPalette::WindowText);
const auto window = defaultPalette.color(QPalette::Window);
if (text.lightness() > window.lightness()) {
isSystemDarkMode = true;
} else {
isSystemDarkMode = false;
}
#else
if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark) {
isSystemDarkMode = true;
} else {
isSystemDarkMode = false;
}
#endif
if (isSystemDarkMode) {
dialog.setStyleSheet(
"QProgressBar::chunk { background-color: #0000A3; border-radius: 5px; }"
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: "
"center; }");
} else {
dialog.setStyleSheet(
"QProgressBar::chunk { background-color: #aaaaaa; border-radius: 5px; }"
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: "
"center; }");
}
QFutureWatcher<void> futureWatcher;
connect(&futureWatcher, &QFutureWatcher<void>::finished, this, [=, this]() {
QString path;
// We want to show the parent path instead of the full path
PathToQString(path, game_folder_path.parent_path());
QIcon windowIcon(
PathToUTF8String(game_folder_path / "sce_sys/icon0.png").c_str());
QMessageBox extractMsgBox(this);
extractMsgBox.setWindowTitle(tr("Installation Finished"));
if (!windowIcon.isNull()) {
extractMsgBox.setWindowIcon(windowIcon);
}
extractMsgBox.setText(
QString(tr("Game successfully installed at %1")).arg(path));
extractMsgBox.addButton(QMessageBox::Ok);
extractMsgBox.setDefaultButton(QMessageBox::Ok);
connect(&extractMsgBox,
&QMessageBox::buttonClicked,
this,
[&](QAbstractButton *button) {
if (extractMsgBox.button(QMessageBox::Ok) == button) {
extractMsgBox.close();
// emit ExtractionFinished();
}
});
extractMsgBox.exec();
//if (delete_file_on_install) {
// std::filesystem::remove(file);
//}
});
connect(&dialog, &QProgressDialog::canceled, [&]() { futureWatcher.cancel(); });
connect(&futureWatcher,
&QFutureWatcher<void>::progressValueChanged,
&dialog,
&QProgressDialog::setValue);
futureWatcher.setFuture(
QtConcurrent::map(indices, [&](int index) { pkg.ExtractFiles(index); }));
dialog.exec();
}
}
} else {
QMessageBox::critical(this,
tr("PKG ERROR"),
tr("File doesn't appear to be a valid PKG file"));
}
}
std::optional<std::filesystem::path> MainWindow::FindGameByID(const std::filesystem::path &dir,
const std::string &game_id,
int max_depth)
{
if (max_depth < 0) {
return std::nullopt;
}
// Check if this is the game we're looking for
if (dir.filename() == game_id && std::filesystem::exists(dir / "sce_sys" / "param.sfo")) {
auto eboot_path = dir / "eboot.bin";
if (std::filesystem::exists(eboot_path)) {
return eboot_path;
}
}
// Recursively search subdirectories
std::error_code ec;
for (const auto &entry : std::filesystem::directory_iterator(dir, ec)) {
if (!entry.is_directory()) {
continue;
}
if (auto found = FindGameByID(entry.path(), game_id, max_depth - 1)) {
return found;
}
}
return std::nullopt;
}
MainWindow::~MainWindow()
{
delete ui;
}