From fe309dc75512a5fbe69a9615a330b45022791ee7 Mon Sep 17 00:00:00 2001 From: valde Date: Wed, 8 Apr 2026 15:44:58 -0300 Subject: [PATCH 1/8] feat: edit and patch identity, custom path and installation id --- Launcher/src/auth/nostaleauth.cpp | 129 +++- Launcher/src/auth/nostaleauth.h | 20 + Launcher/src/gameforgeaccount.cpp | 28 + Launcher/src/gameforgeaccount.h | 15 + Launcher/src/gui/editproxydialog.cpp | 157 +++++ Launcher/src/gui/editproxydialog.h | 51 ++ Launcher/src/gui/mainwindow.cpp | 868 ++++++++++++++++++++++++++- 7 files changed, 1230 insertions(+), 38 deletions(-) create mode 100644 Launcher/src/gui/editproxydialog.cpp create mode 100644 Launcher/src/gui/editproxydialog.h diff --git a/Launcher/src/auth/nostaleauth.cpp b/Launcher/src/auth/nostaleauth.cpp index c4a4800..f7c458c 100644 --- a/Launcher/src/auth/nostaleauth.cpp +++ b/Launcher/src/auth/nostaleauth.cpp @@ -12,35 +12,16 @@ NostaleAuth::NostaleAuth(const QString &identityPath, const QString& installatio , proxyUsername(proxyUser) , proxyPassword(proxyPasswd) , useProxy(proxy) + , identityPath(identityPath) { - if (identityPath.isEmpty()) { - identity = nullptr; - } - else { - identity = std::make_shared(identityPath, proxyHost, proxyPort, proxyUser, proxyPasswd, proxy); - } - + rebuildIdentity(); this->locale = QLocale().name().replace("_", "-"); this->browserUserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"; this->eventsSessionId = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); networkManager = new SyncNetworAccesskManager(this); - - if (useProxy) { - QNetworkProxy proxy( - QNetworkProxy::ProxyType::Socks5Proxy, - proxyIp, - socksPort.toUInt() - ); - - if (!proxyUsername.isEmpty()) { - proxy.setUser(proxyUsername); - proxy.setPassword(proxyPassword); - } - - networkManager->setProxy(proxy); - } + applyProxyConfiguration(); initGfVersion(); initCert(); @@ -740,10 +721,14 @@ bool NostaleAuth::createGameAccount(const QString &email, const QString& name, c request.setRawHeader("Connection", "Keep-Alive"); request.setRawHeader("Authorization", "Bearer " + token.toUtf8()); - identity->update(); - BlackBox blackbox(identity, QJsonValue::Null); - - content["blackbox"] = blackbox.encoded(); + if (identity == nullptr) { + content["blackbox"] = BlackboxGenerator::generate(); + } + else { + identity->update(); + BlackBox blackbox(identity, QJsonValue::Null); + content["blackbox"] = blackbox.encoded(); + } content["displayName"] = name; //content["email"] = email; content["gameEnvironmentId"] = "732876de-012f-4e8d-a501-2e0816cf22f2"; @@ -786,3 +771,95 @@ bool NostaleAuth::getUseProxy() const { return useProxy; } + +bool NostaleAuth::isProxyActive() const +{ + return useProxy && !forceNoProxy; +} + +void NostaleAuth::setProxyConfig( + bool proxyEnabled, + const QString& proxyHost, + const QString& proxyPortValue, + const QString& proxyUser, + const QString& proxyPasswd +) +{ + useProxy = proxyEnabled; + proxyIp = proxyHost; + socksPort = proxyPortValue; + proxyUsername = proxyUser; + proxyPassword = proxyPasswd; + rebuildIdentity(); + applyProxyConfiguration(); +} + +void NostaleAuth::setForceNoProxy(bool forceNoProxyValue) +{ + forceNoProxy = forceNoProxyValue; + applyProxyConfiguration(); +} + +void NostaleAuth::applyProxyConfiguration() +{ + if (!networkManager) { + return; + } + + if (!isProxyActive()) { + networkManager->setProxy(QNetworkProxy::NoProxy); + return; + } + + QNetworkProxy proxy( + QNetworkProxy::ProxyType::Socks5Proxy, + proxyIp, + socksPort.toUInt() + ); + + if (!proxyUsername.isEmpty()) { + proxy.setUser(proxyUsername); + proxy.setPassword(proxyPassword); + } + + networkManager->setProxy(proxy); +} + +QString NostaleAuth::getIdentityPath() const +{ + return identityPath; +} + +void NostaleAuth::setIdentityPath(const QString& newIdentityPath) +{ + identityPath = newIdentityPath.trimmed(); + rebuildIdentity(); +} + +void NostaleAuth::setInstallationId(const QString& newInstallationId) +{ + installationId = newInstallationId.trimmed(); + if (installationId.isEmpty()) { + initInstallationId(); + } + if (installationId.isEmpty()) { + installationId = QUuid::createUuid().toString(QUuid::WithoutBraces); + } +} + +void NostaleAuth::rebuildIdentity() +{ + if (identityPath.trimmed().isEmpty()) { + identity = nullptr; + return; + } + + identity = std::make_shared( + identityPath, + proxyIp, + socksPort, + proxyUsername, + proxyPassword, + useProxy + ); +} diff --git a/Launcher/src/auth/nostaleauth.h b/Launcher/src/auth/nostaleauth.h index 535a6f7..fd8b6e6 100644 --- a/Launcher/src/auth/nostaleauth.h +++ b/Launcher/src/auth/nostaleauth.h @@ -39,11 +39,26 @@ class NostaleAuth : public QObject QString getProxyIp() const; QString getSocksPort() const; bool getUseProxy() const; + bool isProxyActive() const; QString getProxyUsername() const; QString getProxyPassword() const; + void setProxyConfig( + bool proxyEnabled, + const QString& proxyHost, + const QString& proxyPortValue, + const QString& proxyUser, + const QString& proxyPasswd + ); + + void setForceNoProxy(bool forceNoProxyValue); + + QString getIdentityPath() const; + void setIdentityPath(const QString& newIdentityPath); + void setInstallationId(const QString& newInstallationId); + SyncNetworAccesskManager *getNetworkManager() const; bool createGameAccount(const QString& email, const QString& name, const QString& gfLang, QJsonObject& response) const; @@ -122,6 +137,11 @@ class NostaleAuth : public QObject QString proxyUsername; QString proxyPassword; bool useProxy; + bool forceNoProxy = false; + QString identityPath; + + void applyProxyConfiguration(); + void rebuildIdentity(); }; #endif // NOSTALEAUTH_H diff --git a/Launcher/src/gameforgeaccount.cpp b/Launcher/src/gameforgeaccount.cpp index d39fa21..1ef99cc 100644 --- a/Launcher/src/gameforgeaccount.cpp +++ b/Launcher/src/gameforgeaccount.cpp @@ -79,11 +79,39 @@ QString GameforgeAccount::getIdentityPath() const return identityPath; } +NostaleAuth *GameforgeAccount::getAuth() +{ + return auth; +} + const NostaleAuth *GameforgeAccount::getAuth() const { return auth; } +void GameforgeAccount::setProxyConfig( + bool proxyEnabled, + const QString& proxyHost, + const QString& proxyPort, + const QString& proxyUsername, + const QString& proxyPassword +) +{ + auth->setProxyConfig(proxyEnabled, proxyHost, proxyPort, proxyUsername, proxyPassword); +} + +void GameforgeAccount::setAdvancedConfig( + const QString& identPath, + const QString& installationId, + const QString& customGamePath +) +{ + identityPath = identPath.trimmed(); + customClientPath = customGamePath.trimmed(); + auth->setIdentityPath(identityPath); + auth->setInstallationId(installationId.trimmed()); +} + QString GameforgeAccount::getcustomClientPath() const { return customClientPath; diff --git a/Launcher/src/gameforgeaccount.h b/Launcher/src/gameforgeaccount.h index 8e24a55..02de670 100644 --- a/Launcher/src/gameforgeaccount.h +++ b/Launcher/src/gameforgeaccount.h @@ -40,8 +40,23 @@ class GameforgeAccount : public QObject QString getIdentityPath() const; + NostaleAuth *getAuth(); const NostaleAuth *getAuth() const; + void setProxyConfig( + bool proxyEnabled, + const QString& proxyHost, + const QString& proxyPort, + const QString& proxyUsername, + const QString& proxyPassword + ); + + void setAdvancedConfig( + const QString& identPath, + const QString& installationId, + const QString& customGamePath + ); + QString getcustomClientPath() const; private: diff --git a/Launcher/src/gui/editproxydialog.cpp b/Launcher/src/gui/editproxydialog.cpp new file mode 100644 index 0000000..feacad9 --- /dev/null +++ b/Launcher/src/gui/editproxydialog.cpp @@ -0,0 +1,157 @@ +#include "editproxydialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +EditProxyDialog::EditProxyDialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle("Edit account settings"); + setWindowFlag(Qt::WindowContextHelpButtonHint, false); + + useProxyCheckBox = new QCheckBox("Use SOCKS5 proxy", this); + proxyIpLineEdit = new QLineEdit(this); + socksPortLineEdit = new QLineEdit(this); + proxyUsernameLineEdit = new QLineEdit(this); + proxyPasswordLineEdit = new QLineEdit(this); + identityPathLineEdit = new QLineEdit(this); + customGamePathLineEdit = new QLineEdit(this); + installationIdLineEdit = new QLineEdit(this); + + proxyPasswordLineEdit->setEchoMode(QLineEdit::Password); + + QFormLayout* formLayout = new QFormLayout(); + formLayout->addRow(useProxyCheckBox); + formLayout->addRow("Proxy IP:", proxyIpLineEdit); + formLayout->addRow("Port:", socksPortLineEdit); + formLayout->addRow("Username:", proxyUsernameLineEdit); + formLayout->addRow("Password:", proxyPasswordLineEdit); + QWidget* customGamePathWidget = new QWidget(this); + QHBoxLayout* customGamePathLayout = new QHBoxLayout(customGamePathWidget); + customGamePathLayout->setContentsMargins(0, 0, 0, 0); + customGamePathLayout->setSpacing(6); + QPushButton* browseCustomGamePathButton = new QPushButton("...", customGamePathWidget); + browseCustomGamePathButton->setToolTip("Select custom game client (.exe)"); + browseCustomGamePathButton->setFixedWidth(32); + customGamePathLayout->addWidget(customGamePathLineEdit); + customGamePathLayout->addWidget(browseCustomGamePathButton); + + formLayout->addRow("Identity path (optional):", identityPathLineEdit); + formLayout->addRow("Custom game path (optional):", customGamePathWidget); + formLayout->addRow("Custom installation id (optional):", installationIdLineEdit); + + QDialogButtonBox* buttonBox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, + Qt::Horizontal, + this + ); + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->addLayout(formLayout); + mainLayout->addWidget(buttonBox); + setLayout(mainLayout); + + connect(useProxyCheckBox, &QCheckBox::toggled, this, &EditProxyDialog::updateProxyFieldsEnabled); + connect(browseCustomGamePathButton, &QPushButton::clicked, this, [this]() { + const QString path = QFileDialog::getOpenFileName( + this, + "Select custom game client", + QDir::rootPath(), + "(*.exe)" + ); + + if (!path.isEmpty()) { + customGamePathLineEdit->setText(path); + } + }); + connect(buttonBox, &QDialogButtonBox::accepted, this, &EditProxyDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &EditProxyDialog::reject); + + updateProxyFieldsEnabled(false); +} + +void EditProxyDialog::setValues( + bool useProxyValue, + const QString& ipValue, + const QString& portValue, + const QString& usernameValue, + const QString& passwordValue, + const QString& identityPathValue, + const QString& customGamePathValue, + const QString& installationIdValue +) +{ + useProxyCheckBox->setChecked(useProxyValue); + proxyIpLineEdit->setText(ipValue); + socksPortLineEdit->setText(portValue); + proxyUsernameLineEdit->setText(usernameValue); + proxyPasswordLineEdit->setText(passwordValue); + identityPathLineEdit->setText(identityPathValue); + customGamePathLineEdit->setText(customGamePathValue); + installationIdLineEdit->setText(installationIdValue); + updateProxyFieldsEnabled(useProxyValue); +} + +bool EditProxyDialog::getUseProxy() const +{ + return useProxyCheckBox->isChecked(); +} + +QString EditProxyDialog::getProxyIp() const +{ + return proxyIpLineEdit->text(); +} + +QString EditProxyDialog::getSocksPort() const +{ + return socksPortLineEdit->text(); +} + +QString EditProxyDialog::getProxyUsername() const +{ + return proxyUsernameLineEdit->text(); +} + +QString EditProxyDialog::getProxyPassword() const +{ + return proxyPasswordLineEdit->text(); +} + +QString EditProxyDialog::getIdentityPath() const +{ + return identityPathLineEdit->text(); +} + +QString EditProxyDialog::getCustomGamePath() const +{ + return customGamePathLineEdit->text(); +} + +QString EditProxyDialog::getInstallationId() const +{ + return installationIdLineEdit->text(); +} + +void EditProxyDialog::accept() +{ + if (getUseProxy() && (getProxyIp().trimmed().isEmpty() || getSocksPort().trimmed().isEmpty())) { + QMessageBox::critical(this, "Error", "If proxy is enabled, IP and port are required."); + return; + } + + QDialog::accept(); +} + +void EditProxyDialog::updateProxyFieldsEnabled(bool enabled) +{ + proxyIpLineEdit->setEnabled(enabled); + socksPortLineEdit->setEnabled(enabled); + proxyUsernameLineEdit->setEnabled(enabled); + proxyPasswordLineEdit->setEnabled(enabled); +} diff --git a/Launcher/src/gui/editproxydialog.h b/Launcher/src/gui/editproxydialog.h new file mode 100644 index 0000000..e1e9bb8 --- /dev/null +++ b/Launcher/src/gui/editproxydialog.h @@ -0,0 +1,51 @@ +#ifndef EDITPROXYDIALOG_H +#define EDITPROXYDIALOG_H + +#include +#include +#include + +class EditProxyDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EditProxyDialog(QWidget* parent = nullptr); + + void setValues( + bool useProxyValue, + const QString& ipValue, + const QString& portValue, + const QString& usernameValue, + const QString& passwordValue, + const QString& identityPathValue, + const QString& customGamePathValue, + const QString& installationIdValue + ); + + bool getUseProxy() const; + QString getProxyIp() const; + QString getSocksPort() const; + QString getProxyUsername() const; + QString getProxyPassword() const; + QString getIdentityPath() const; + QString getCustomGamePath() const; + QString getInstallationId() const; + +protected: + void accept() override; + +private: + void updateProxyFieldsEnabled(bool enabled); + + QCheckBox* useProxyCheckBox; + QLineEdit* proxyIpLineEdit; + QLineEdit* socksPortLineEdit; + QLineEdit* proxyUsernameLineEdit; + QLineEdit* proxyPasswordLineEdit; + QLineEdit* identityPathLineEdit; + QLineEdit* customGamePathLineEdit; + QLineEdit* installationIdLineEdit; +}; + +#endif // EDITPROXYDIALOG_H diff --git a/Launcher/src/gui/mainwindow.cpp b/Launcher/src/gui/mainwindow.cpp index cd3c369..b9b44da 100644 --- a/Launcher/src/gui/mainwindow.cpp +++ b/Launcher/src/gui/mainwindow.cpp @@ -5,9 +5,20 @@ #include "captchadialog.h" #include "identitydialog.h" #include "editmultipleprofileaccountsdialog.h" +#include "editproxydialog.h" #include "gameupdatedialog.h" #include "creategameaccountdialog.h" +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) @@ -20,6 +31,7 @@ MainWindow::MainWindow(QWidget *parent) gflessServer = new QLocalServer(this); gflessServer->listen("GflessClient"); createTrayIcon(); + setupProxyControls(); ui->accountsListWidget->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->accountsListWidget, &QListWidget::customContextMenuRequested, this, &MainWindow::showContextMenu); @@ -41,6 +53,7 @@ MainWindow::~MainWindow() void MainWindow::loadSettings() { + loadingStoredAccounts = true; QSettings settings; settings.beginGroup("MainWindow"); @@ -59,6 +72,7 @@ void MainWindow::loadSettings() defaultServer = settings.value("default_server", 0).toInt(); defaultChannel = settings.value("default_channel", 0).toInt(); defaultCharacter = settings.value("default_character", 0).toInt(); + useProxiesGlobally = settings.value("use_proxies_globally", true).toBool(); settings.endGroup(); @@ -80,20 +94,31 @@ void MainWindow::loadSettings() QString customClientPath = settings.value("custom_client", "").toString(); bool useProxy = settings.value("use_proxy", false).toBool(); - if (token.isEmpty()) - addGameforgeAccount(email, password, identity, installationId, customClientPath, proxyIp, socksPort, proxyUsername, proxyPassword, useProxy); - else - addGameforgeAccount(email, password, token, identity, installationId, customClientPath, proxyIp, socksPort, proxyUsername, proxyPassword, useProxy); + // Always load persisted accounts without forcing online authentication at startup. + // This prevents account loss when auth/proxy fails or app closes during startup. + addGameforgeAccount(email, password, token, identity, installationId, customClientPath, proxyIp, socksPort, proxyUsername, proxyPassword, useProxy); } settings.endArray(); settings.endGroup(); + applyGlobalProxyMode(); + updateProxyModeButtonText(); + updateAllGameforgeAccountVisuals(); + writeAccountIpsJson(); +#ifdef NO_PROXY_MODE + syncProxifierProfile(); +#endif displayProfile(ui->profileComboBox->currentIndex()); + loadingStoredAccounts = false; } void MainWindow::saveSettings() { + if (loadingStoredAccounts) { + return; + } + QSettings settings; settings.beginGroup("MainWindow"); @@ -110,6 +135,7 @@ void MainWindow::saveSettings() settings.setValue("default_server", defaultServer); settings.setValue("default_channel", defaultChannel); settings.setValue("default_character", defaultCharacter); + settings.setValue("use_proxies_globally", useProxiesGlobally); settings.endGroup(); settings.beginGroup("Gameforge Accounts"); @@ -135,6 +161,10 @@ void MainWindow::saveSettings() settings.endArray(); settings.endGroup(); + writeAccountIpsJson(); +#ifdef NO_PROXY_MODE + syncProxifierProfile(); +#endif } void MainWindow::setupDefaultProfile() @@ -243,6 +273,8 @@ void MainWindow::addGameforgeAccount(const QString &email, const QString &passwo bool captcha = false; bool wrongCredentials = false; QString gfChallengeId; + bool authenticated = false; + bool switchedToNoProxy = false; GameforgeAccount* gfAcc = new GameforgeAccount( email, password, @@ -256,8 +288,36 @@ void MainWindow::addGameforgeAccount(const QString &email, const QString &passwo proxyPassword, this ); + gfAcc->getAuth()->setForceNoProxy(!useProxiesGlobally); + + authenticated = gfAcc->authenticate(captcha, gfChallengeId, wrongCredentials); + +#ifdef NO_PROXY_MODE + // In NoIP build, only disable proxy for accounts that fail through proxy. + if (!authenticated && useProxy && !captcha && !wrongCredentials) { + gfAcc->setProxyConfig(false, "", "", "", ""); + gfAcc->getAuth()->setForceNoProxy(!useProxiesGlobally); + + bool retryCaptcha = false; + bool retryWrongCredentials = false; + QString retryChallengeId; + const bool retryAuthenticated = gfAcc->authenticate(retryCaptcha, retryChallengeId, retryWrongCredentials); + + if (retryAuthenticated) { + authenticated = true; + switchedToNoProxy = true; + } else { + // Keep original proxy config if no-proxy retry did not solve it. + gfAcc->setProxyConfig(useProxy, proxyIp, socksPort, proxyUsername, proxyPassword); + gfAcc->getAuth()->setForceNoProxy(!useProxiesGlobally); + captcha = retryCaptcha; + wrongCredentials = retryWrongCredentials; + gfChallengeId = retryChallengeId; + } + } +#endif - if (!gfAcc->authenticate(captcha, gfChallengeId, wrongCredentials)) { + if (!authenticated) { if (captcha) { CaptchaDialog captcha(gfChallengeId, gfAcc->getAuth()->getNetworkManager(), this); int res = captcha.exec(); @@ -280,6 +340,14 @@ void MainWindow::addGameforgeAccount(const QString &email, const QString &passwo gfAccounts.push_back(gfAcc); ui->gameforgeAccountComboBox->addItem(email); + updateGameforgeAccountVisual(gfAccounts.size() - 1); + + if (switchedToNoProxy) { + ui->statusbar->showMessage( + "Proxy failed for " + email + ". Account loaded with no proxy.", + 12000 + ); + } // Update default profile @@ -290,11 +358,13 @@ void MainWindow::addGameforgeAccount(const QString &email, const QString &passwo profiles.first()->addAccount(gameAccount); } + writeAccountIpsJson(); displayProfile(ui->profileComboBox->currentIndex()); } void MainWindow::addGameforgeAccount(const QString &email, const QString& password, const QString &token, const QString &identityPath, const QString &installationId, const QString &customClientPath, const QString &proxyIp, const QString &socksPort, const QString &proxyUsername, const QString &proxyPassword, const bool useProxy) { + bool switchedToNoProxy = false; GameforgeAccount* gfAcc = new GameforgeAccount( email, password, @@ -308,23 +378,53 @@ void MainWindow::addGameforgeAccount(const QString &email, const QString& passwo proxyPassword, this ); + gfAcc->getAuth()->setForceNoProxy(!useProxiesGlobally); gfAcc->setToken(token); + // Update default profile + gfAcc->updateGameAccounts(); + QMap gameAccs = gfAcc->getGameAccounts(); + +#ifdef NO_PROXY_MODE + // In NoIP build, fallback to no proxy only when proxy path fails to load accounts. + if (useProxy && gameAccs.isEmpty()) { + gfAcc->setProxyConfig(false, "", "", "", ""); + gfAcc->getAuth()->setForceNoProxy(!useProxiesGlobally); + gfAcc->updateGameAccounts(); + QMap fallbackAccounts = gfAcc->getGameAccounts(); + + if (!fallbackAccounts.isEmpty()) { + gameAccs = fallbackAccounts; + switchedToNoProxy = true; + } else { + // Keep original proxy config if no-proxy retry did not improve loading. + gfAcc->setProxyConfig(useProxy, proxyIp, socksPort, proxyUsername, proxyPassword); + gfAcc->getAuth()->setForceNoProxy(!useProxiesGlobally); + gfAcc->updateGameAccounts(); + gameAccs = gfAcc->getGameAccounts(); + } + } +#endif + gfAccounts.push_back(gfAcc); ui->gameforgeAccountComboBox->addItem(email); + updateGameforgeAccountVisual(gfAccounts.size() - 1); - - // Update default profile - gfAcc->updateGameAccounts(); - QMap gameAccs = gfAcc->getGameAccounts(); + if (switchedToNoProxy) { + ui->statusbar->showMessage( + "Proxy failed for " + email + ". Account loaded with no proxy.", + 12000 + ); + } for (auto it = gameAccs.begin(); it != gameAccs.end(); ++it) { GameAccount gameAccount(gfAcc, it.value(), it.key(), it.value(), defaultServerLocation, defaultServer, defaultChannel, defaultCharacter, defaultAutoLogin); profiles.first()->addAccount(gameAccount); } + writeAccountIpsJson(); displayProfile(ui->profileComboBox->currentIndex()); } @@ -362,6 +462,8 @@ void MainWindow::displayProfile(int index) QListWidgetItem* item = new QListWidgetItem(ui->accountsListWidget); item->setText(acc.toString()); item->setData(Qt::UserRole, acc.getId()); + const bool hasProxy = acc.getGfAcc()->getAuth()->getUseProxy(); + item->setForeground(hasProxy ? QColor(0, 140, 0) : QColor(180, 30, 30)); } } @@ -407,7 +509,9 @@ void MainWindow::createTrayIcon() void MainWindow::closeEvent(QCloseEvent *event) { hide(); - saveSettings(); + if (!loadingStoredAccounts) { + saveSettings(); + } event->ignore(); } @@ -452,9 +556,60 @@ void MainWindow::on_removeGameforgeAccountButton_clicked() gfAccounts.remove(index); ui->gameforgeAccountComboBox->removeItem(index); + writeAccountIpsJson(); displayProfile(ui->profileComboBox->currentIndex()); } +void MainWindow::on_editGameforgeAccountButton_clicked() +{ + int index = ui->gameforgeAccountComboBox->currentIndex(); + if (index < 0 || index >= gfAccounts.size()) { + return; + } + + GameforgeAccount* acc = gfAccounts[index]; + NostaleAuth* auth = acc->getAuth(); + + EditProxyDialog dialog(this); + dialog.setValues( + auth->getUseProxy(), + auth->getProxyIp(), + auth->getSocksPort(), + auth->getProxyUsername(), + auth->getProxyPassword(), + acc->getIdentityPath(), + acc->getcustomClientPath(), + auth->getInstallationId() + ); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + acc->setProxyConfig( + dialog.getUseProxy(), + dialog.getProxyIp().trimmed(), + dialog.getSocksPort().trimmed(), + dialog.getProxyUsername().trimmed(), + dialog.getProxyPassword() + ); + acc->setAdvancedConfig( + dialog.getIdentityPath(), + dialog.getInstallationId(), + dialog.getCustomGamePath() + ); + + // Keep global mode behavior after editing. + acc->getAuth()->setForceNoProxy(!useProxiesGlobally); + + saveSettings(); + updateGameforgeAccountVisual(index); + if (dialog.getUseProxy()) { + ui->statusbar->showMessage("Proxy settings updated for " + acc->getEmail(), 8000); + } + on_gameforgeAccountComboBox_currentIndexChanged(index); +} + void MainWindow::on_gameSettingsButton_clicked() { @@ -879,12 +1034,17 @@ void MainWindow::on_gameforgeAccountComboBox_currentIndexChanged(int index) GameforgeAccount* gf = gfAccounts.at(index); - if (gf->getAuth()->getUseProxy()) { + if (gf->getAuth()->isProxyActive()) { tooltipText += "Proxy IP: " + gf->getAuth()->getProxyIp() + "\n"; tooltipText += "Proxy port: " + gf->getAuth()->getSocksPort(); } else { - tooltipText += "No proxy"; + if (gf->getAuth()->getUseProxy() && !useProxiesGlobally) { + tooltipText += "No proxy (global toggle)"; + } + else { + tooltipText += "No proxy"; + } } if (gf->getcustomClientPath().isEmpty()) { @@ -894,6 +1054,12 @@ void MainWindow::on_gameforgeAccountComboBox_currentIndexChanged(int index) tooltipText += "\nCustom client: " + gf->getcustomClientPath().right(gf->getcustomClientPath().size() - gf->getcustomClientPath().lastIndexOf("/") - 1); } + tooltipText += "\nIdentity: "; + tooltipText += (gf->getIdentityPath().isEmpty() ? "default" : gf->getIdentityPath()); + + tooltipText += "\nInstallation ID: "; + tooltipText += (gf->getAuth()->getInstallationId().isEmpty() ? "default" : gf->getAuth()->getInstallationId()); + ui->gameforgeAccountComboBox->setToolTip(tooltipText); } @@ -921,6 +1087,684 @@ void MainWindow::on_repairButton_clicked() updateGame(); } +void MainWindow::setupProxyControls() +{ + QWidget* cornerWidget = new QWidget(this); + QHBoxLayout* layout = new QHBoxLayout(cornerWidget); + layout->setContentsMargins(4, 0, 4, 0); + layout->setSpacing(4); + + toggleProxyModeButton = new QToolButton(cornerWidget); + toggleProxyModeButton->setCheckable(true); + toggleProxyModeButton->setChecked(true); + + patchNewProxiesButton = new QToolButton(cornerWidget); + patchNewProxiesButton->setText("Patch new proxies"); + + layout->addWidget(toggleProxyModeButton); + layout->addWidget(patchNewProxiesButton); + + ui->menubar->setCornerWidget(cornerWidget, Qt::TopRightCorner); + + connect(toggleProxyModeButton, &QToolButton::toggled, this, [&](bool checked) { + useProxiesGlobally = checked; + applyGlobalProxyMode(); + updateProxyModeButtonText(); + on_gameforgeAccountComboBox_currentIndexChanged(ui->gameforgeAccountComboBox->currentIndex()); + }); + + connect(patchNewProxiesButton, &QToolButton::clicked, this, [&]() { + // Persist current UI/account state to registry and JSON before patching. + saveSettings(); + int patched = patchNewProxiesFromJson(); +#ifdef NO_PROXY_MODE + // Always refresh Proxifier profile on explicit patch action. + syncProxifierProfile(); +#endif + if (patched <= 0) { + QMessageBox::information(this, "Patch new proxies", "No account entries were patched from accountIPS.json.\nProxifier profile was still refreshed."); + return; + } + + QMessageBox::information( + this, + "Patch new proxies", + "Patched " + QString::number(patched) + " account(s) from accountIPS.json." + ); + }); + + updateProxyModeButtonText(); +} + +void MainWindow::applyGlobalProxyMode() +{ + for (GameforgeAccount* acc : gfAccounts) { + acc->getAuth()->setForceNoProxy(!useProxiesGlobally); + } +} + +void MainWindow::updateProxyModeButtonText() +{ + if (!toggleProxyModeButton) { + return; + } + + toggleProxyModeButton->blockSignals(true); + toggleProxyModeButton->setChecked(useProxiesGlobally); + toggleProxyModeButton->setText(useProxiesGlobally ? "Use proxies" : "No use proxies"); + toggleProxyModeButton->blockSignals(false); +} + +void MainWindow::updateGameforgeAccountVisual(int index) +{ + if (index < 0 || index >= gfAccounts.size() || index >= ui->gameforgeAccountComboBox->count()) { + return; + } + + const bool hasProxy = gfAccounts.at(index)->getAuth()->getUseProxy(); + const QColor color = hasProxy ? QColor(0, 140, 0) : QColor(180, 30, 30); + ui->gameforgeAccountComboBox->setItemData(index, QBrush(color), Qt::ForegroundRole); +} + +void MainWindow::updateAllGameforgeAccountVisuals() +{ + for (int i = 0; i < gfAccounts.size(); ++i) { + updateGameforgeAccountVisual(i); + } +} + +QString MainWindow::resolveProxifierProfilePath() const +{ + QString appData = qEnvironmentVariable("APPDATA").trimmed(); + if (appData.isEmpty()) { + appData = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + } + if (appData.isEmpty()) { + return QString(); + } + + QDir profilesDir(QDir(appData).filePath("Proxifier4/Profiles")); + if (!profilesDir.exists()) { + profilesDir.mkpath("."); + } + + return profilesDir.filePath("GFLESSCLIENT.ppx"); +} + +QDomDocument MainWindow::createDefaultProxifierProfile() const +{ + QDomDocument doc; + const QString xmlTemplate = QStringLiteral(R"( + + + + + + + %ComputerName%; localhost; *.local + 0 + + + + + + + + + + + + + + + + localhost; 127.0.0.1; %ComputerName%; ::1 + Localhost + + + + Default + + + +)"); + if (!doc.setContent(xmlTemplate)) { + doc.clear(); + } + + return doc; +} + +void MainWindow::syncProxifierProfile() +{ + const QString profilePath = resolveProxifierProfilePath(); + if (profilePath.isEmpty()) { + return; + } + + const QFileInfo targetInfo(profilePath); + const QDir profileDir(targetInfo.absolutePath()); + const QStringList candidatePaths = { + profilePath, + profileDir.filePath("gflessclient.ppx"), + profileDir.filePath("GFLESSDLL.ppx") + }; + + QDomDocument doc; + bool parsed = false; + for (const QString& candidate : candidatePaths) { + QFile inFile(candidate); + if (!inFile.exists() || !inFile.open(QIODevice::ReadOnly)) { + continue; + } + parsed = doc.setContent(&inFile); + inFile.close(); + if (parsed) { + break; + } + } + + QDomElement root = doc.documentElement(); + if (!parsed || root.isNull() || root.tagName() != "ProxifierProfile") { + doc = createDefaultProxifierProfile(); + root = doc.documentElement(); + } else { + // Rebase to a known-good Proxifier schema and preserve sections from the existing profile. + QDomDocument sanitized = createDefaultProxifierProfile(); + QDomElement sanitizedRoot = sanitized.documentElement(); + auto copySection = [&](const QString& tagName) { + const QDomElement existing = root.firstChildElement(tagName); + if (existing.isNull()) { + return; + } + const QDomElement target = sanitizedRoot.firstChildElement(tagName); + const QDomNode imported = sanitized.importNode(existing, true); + if (target.isNull()) { + sanitizedRoot.appendChild(imported); + } else { + sanitizedRoot.replaceChild(imported, target); + } + }; + copySection("ProxyList"); + copySection("ChainList"); + copySection("RuleList"); + doc = sanitized; + root = doc.documentElement(); + } + + QDomElement proxyList = root.firstChildElement("ProxyList"); + if (proxyList.isNull()) { + proxyList = doc.createElement("ProxyList"); + root.appendChild(proxyList); + } + + QDomElement ruleList = root.firstChildElement("RuleList"); + if (ruleList.isNull()) { + ruleList = doc.createElement("RuleList"); + root.appendChild(ruleList); + } + + auto normalizedPath = [](const QString& path) { + return QDir::toNativeSeparators(path).trimmed().toLower(); + }; + + auto splitApplications = [&](const QString& applicationsText) { + QStringList parts = applicationsText.split(';', Qt::SkipEmptyParts); + for (QString& part : parts) { + part = part.trimmed(); + if (part.startsWith("\"") && part.endsWith("\"") && part.size() >= 2) { + part = part.mid(1, part.size() - 2); + } + part = normalizedPath(part); + } + return parts; + }; + + QMap proxyKeyToId; + int maxProxyId = 99; + + for (QDomElement proxy = proxyList.firstChildElement("Proxy"); !proxy.isNull(); proxy = proxy.nextSiblingElement("Proxy")) { + bool ok = false; + const int id = proxy.attribute("id").toInt(&ok); + if (ok && id > maxProxyId) { + maxProxyId = id; + } + + const QString address = proxy.firstChildElement("Address").text().trimmed(); + const QString port = proxy.firstChildElement("Port").text().trimmed(); + QString username; + QString password; + QDomElement auth = proxy.firstChildElement("Authentication"); + if (!auth.isNull() && auth.attribute("enabled").toLower() == "true") { + username = auth.firstChildElement("Username").text(); + password = auth.firstChildElement("Password").text(); + } + + const QString key = address + "|" + port + "|" + username + "|" + password; + if (!address.isEmpty() && !port.isEmpty() && ok) { + proxyKeyToId[key] = id; + } + } + + struct ProxyAccountEntry { + QString customPath; + QString proxyIp; + QString proxyPort; + QString proxyUser; + QString proxyPass; + }; + + QVector patchEntries; + auto addPatchEntry = [&](const QString& customPathRaw, + const QString& proxyIpRaw, + const QString& proxyPortRaw, + const QString& proxyUserRaw, + const QString& proxyPassRaw, + bool useProxy) { + if (!useProxy) { + return; + } + + QString customPath = customPathRaw.trimmed(); + QString proxyIp = proxyIpRaw.trimmed(); + QString proxyPort = proxyPortRaw.trimmed(); + QString proxyUser = proxyUserRaw.trimmed(); + QString proxyPass = proxyPassRaw; + + if (customPath.startsWith("\"") && customPath.endsWith("\"") && customPath.size() >= 2) { + customPath = customPath.mid(1, customPath.size() - 2).trimmed(); + } + + if (proxyIp.isEmpty() || proxyPort.isEmpty()) { + return; + } + + patchEntries.push_back({customPath, proxyIp, proxyPort, proxyUser, proxyPass}); + }; + + for (GameforgeAccount* acc : gfAccounts) { + if (!acc) { + continue; + } + + const NostaleAuth* auth = acc->getAuth(); + if (!auth) { + continue; + } + + addPatchEntry( + acc->getcustomClientPath(), + auth->getProxyIp(), + auth->getSocksPort(), + auth->getProxyUsername(), + auth->getProxyPassword(), + auth->getUseProxy() + ); + } + + // Merge with persisted accounts from registry to avoid losing rules/proxies that are not currently loaded in memory. + { + QSettings settings; + settings.beginGroup("Gameforge Accounts"); + const int numAccs = settings.beginReadArray("GF accounts data"); + for (int i = 0; i < numAccs; ++i) { + settings.setArrayIndex(i); + addPatchEntry( + settings.value("custom_client", "").toString(), + settings.value("proxy_ip", "").toString(), + settings.value("socks_port", "").toString(), + settings.value("proxy_username", "").toString(), + settings.value("proxy_password", "").toString(), + settings.value("use_proxy", false).toBool() + ); + } + settings.endArray(); + settings.endGroup(); + } + + QSet managedPaths; + + for (const ProxyAccountEntry& entry : patchEntries) { + const QString proxyIp = entry.proxyIp; + const QString proxyPort = entry.proxyPort; + const QString proxyUser = entry.proxyUser; + const QString proxyPass = entry.proxyPass; + + const QString proxyKey = proxyIp + "|" + proxyPort + "|" + proxyUser + "|" + proxyPass; + int proxyId = proxyKeyToId.value(proxyKey, -1); + + if (proxyId < 0) { + proxyId = ++maxProxyId; + QDomElement proxyNode = doc.createElement("Proxy"); + proxyNode.setAttribute("id", QString::number(proxyId)); + proxyNode.setAttribute("type", "SOCKS5"); + + QDomElement authNode = doc.createElement("Authentication"); + const bool authEnabled = (!proxyUser.isEmpty() || !proxyPass.isEmpty()); + authNode.setAttribute("enabled", authEnabled ? "true" : "false"); + if (authEnabled) { + QDomElement passNode = doc.createElement("Password"); + passNode.appendChild(doc.createTextNode(proxyPass)); + authNode.appendChild(passNode); + QDomElement userNode = doc.createElement("Username"); + userNode.appendChild(doc.createTextNode(proxyUser)); + authNode.appendChild(userNode); + } + proxyNode.appendChild(authNode); + + QDomElement optionsNode = doc.createElement("Options"); + optionsNode.appendChild(doc.createTextNode("48")); + proxyNode.appendChild(optionsNode); + + QDomElement portNode = doc.createElement("Port"); + portNode.appendChild(doc.createTextNode(proxyPort)); + proxyNode.appendChild(portNode); + + QDomElement addressNode = doc.createElement("Address"); + addressNode.appendChild(doc.createTextNode(proxyIp)); + proxyNode.appendChild(addressNode); + + proxyList.appendChild(proxyNode); + proxyKeyToId[proxyKey] = proxyId; + } + + const QString customPath = entry.customPath; + if (customPath.isEmpty()) { + continue; + } + + const QString appPathNormalized = normalizedPath(customPath); + const QString appPathForRule = "\"" + QDir::toNativeSeparators(customPath) + "\""; + managedPaths.insert(appPathNormalized); + + QDomElement matchedRule; + QList duplicates; + for (QDomElement rule = ruleList.firstChildElement("Rule"); !rule.isNull(); rule = rule.nextSiblingElement("Rule")) { + QDomElement applications = rule.firstChildElement("Applications"); + if (applications.isNull()) { + continue; + } + + const QStringList appEntries = splitApplications(applications.text()); + if (!appEntries.contains(appPathNormalized)) { + continue; + } + + if (matchedRule.isNull()) { + matchedRule = rule; + } else { + duplicates.push_back(rule); + } + } + + for (QDomElement duplicate : duplicates) { + ruleList.removeChild(duplicate); + } + + if (matchedRule.isNull()) { + matchedRule = doc.createElement("Rule"); + matchedRule.setAttribute("enabled", "true"); + QDomElement defaultRule; + for (QDomElement rule = ruleList.firstChildElement("Rule"); !rule.isNull(); rule = rule.nextSiblingElement("Rule")) { + if (rule.firstChildElement("Name").text().trimmed().compare("Default", Qt::CaseInsensitive) == 0) { + defaultRule = rule; + break; + } + } + if (!defaultRule.isNull()) { + ruleList.insertBefore(matchedRule, defaultRule); + } else { + ruleList.appendChild(matchedRule); + } + } + + QDomElement action = matchedRule.firstChildElement("Action"); + if (action.isNull()) { + action = doc.createElement("Action"); + matchedRule.appendChild(action); + } + action.setAttribute("type", "Proxy"); + while (action.firstChild().isNull() == false) { + action.removeChild(action.firstChild()); + } + action.appendChild(doc.createTextNode(QString::number(proxyId))); + + QDomElement applications = matchedRule.firstChildElement("Applications"); + if (applications.isNull()) { + applications = doc.createElement("Applications"); + matchedRule.appendChild(applications); + } + while (applications.firstChild().isNull() == false) { + applications.removeChild(applications.firstChild()); + } + applications.appendChild(doc.createTextNode(appPathForRule)); + + QDomElement name = matchedRule.firstChildElement("Name"); + if (name.isNull()) { + name = doc.createElement("Name"); + matchedRule.appendChild(name); + } + while (name.firstChild().isNull() == false) { + name.removeChild(name.firstChild()); + } + name.appendChild(doc.createTextNode(QFileInfo(customPath).fileName())); + + matchedRule.setAttribute("enabled", "true"); + } + + // Disable managed rules when the related account no longer has custom path/proxy. + for (QDomElement rule = ruleList.firstChildElement("Rule"); !rule.isNull(); rule = rule.nextSiblingElement("Rule")) { + QDomElement applications = rule.firstChildElement("Applications"); + if (applications.isNull()) { + continue; + } + const QStringList appEntries = splitApplications(applications.text()); + if (appEntries.size() != 1) { + continue; + } + const QString appPath = appEntries.first(); + if (managedPaths.contains(appPath)) { + continue; + } + + const QString ruleName = rule.firstChildElement("Name").text().trimmed(); + if (ruleName.endsWith(".exe", Qt::CaseInsensitive) && appPath.contains("\\") && appPath.endsWith(".exe")) { + QDomElement action = rule.firstChildElement("Action"); + if (!action.isNull() && action.attribute("type").compare("Proxy", Qt::CaseInsensitive) == 0) { + rule.setAttribute("enabled", "false"); + } + } + } + + QDir().mkpath(targetInfo.absolutePath()); + const QStringList outputPaths = { + profilePath, + profileDir.filePath("gflessclient.ppx"), + profileDir.filePath("GFLESSDLL.ppx") + }; + + bool wroteAny = false; + QString xmlText = doc.toString(1); + const QString forcedXmlDeclaration = QStringLiteral(""); + if (xmlText.startsWith(""); + if (declarationEnd >= 0) { + xmlText = forcedXmlDeclaration + xmlText.mid(declarationEnd + 2); + } else { + xmlText.prepend(forcedXmlDeclaration + "\n"); + } + } else { + xmlText.prepend(forcedXmlDeclaration + "\n"); + } + + for (const QString& outputPath : outputPaths) { + QFile outFile(outputPath); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + continue; + } + QTextStream out(&outFile); + out.setCodec("UTF-8"); + out << xmlText; + outFile.close(); + wroteAny = true; + } + + if (!wroteAny) { + return; + } +} + +QString MainWindow::getAccountIpsJsonPath() const +{ + return QDir(QCoreApplication::applicationDirPath()).filePath("accountIPS.json"); +} + +void MainWindow::writeAccountIpsJson() const +{ + QJsonArray accounts; + + for (const GameforgeAccount* acc : gfAccounts) { + const NostaleAuth* auth = acc->getAuth(); + + QJsonObject obj; + obj["email"] = acc->getEmail(); + obj["use_proxy"] = auth->getUseProxy(); + obj["proxy_ip"] = auth->getProxyIp(); + obj["socks_port"] = auth->getSocksPort(); + obj["proxy_username"] = auth->getProxyUsername(); + obj["proxy_password"] = auth->getProxyPassword(); + obj["identity_path"] = acc->getIdentityPath(); + obj["custom_client"] = acc->getcustomClientPath(); + obj["custom_game_path"] = acc->getcustomClientPath(); + obj["installation_id"] = auth->getInstallationId(); + obj["custom_installation_id"] = auth->getInstallationId(); + accounts.append(obj); + } + + QJsonObject root; + root["generated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + root["accounts"] = accounts; + + QFile out(getAccountIpsJsonPath()); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return; + } + + out.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + out.close(); +} + +int MainWindow::patchNewProxiesFromJson() +{ + QFile input(getAccountIpsJsonPath()); + if (!input.open(QIODevice::ReadOnly)) { + return 0; + } + + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(input.readAll(), &error); + input.close(); + + if (error.error != QJsonParseError::NoError || doc.isNull()) { + return 0; + } + + QJsonArray sourceAccounts; + + if (doc.isArray()) { + sourceAccounts = doc.array(); + } else if (doc.isObject()) { + sourceAccounts = doc.object().value("accounts").toArray(); + } + + if (sourceAccounts.isEmpty()) { + return 0; + } + + QMap byEmail; + for (const QJsonValue& value : sourceAccounts) { + const QJsonObject obj = value.toObject(); + const QString email = obj.value("email").toString().trimmed(); + if (email.isEmpty()) { + continue; + } + byEmail[email] = obj; + } + + int patched = 0; + for (GameforgeAccount* acc : gfAccounts) { + if (!byEmail.contains(acc->getEmail())) { + continue; + } + + QJsonObject obj = byEmail.value(acc->getEmail()); + NostaleAuth* auth = acc->getAuth(); + + QString ip = auth->getProxyIp(); + QString port = auth->getSocksPort(); + QString user = auth->getProxyUsername(); + QString pass = auth->getProxyPassword(); + QString identityPath = acc->getIdentityPath(); + QString installationId = auth->getInstallationId(); + QString customClientPath = acc->getcustomClientPath(); + bool useProxy = auth->getUseProxy(); + + if (obj.contains("proxy_ip")) { + ip = obj.value("proxy_ip").toString().trimmed(); + } + if (obj.contains("socks_port")) { + port = obj.value("socks_port").toString().trimmed(); + } + if (obj.contains("proxy_username")) { + user = obj.value("proxy_username").toString(); + } + if (obj.contains("proxy_password")) { + pass = obj.value("proxy_password").toString(); + } + if (obj.contains("identity_path")) { + identityPath = obj.value("identity_path").toString().trimmed(); + } + if (obj.contains("installation_id")) { + installationId = obj.value("installation_id").toString().trimmed(); + } else if (obj.contains("custom_installation_id")) { + installationId = obj.value("custom_installation_id").toString().trimmed(); + } + if (obj.contains("custom_client")) { + customClientPath = obj.value("custom_client").toString().trimmed(); + } else if (obj.contains("custom_game_path")) { + customClientPath = obj.value("custom_game_path").toString().trimmed(); + } + + if (obj.contains("use_proxy")) { + const QJsonValue useProxyValue = obj.value("use_proxy"); + if (useProxyValue.isBool()) { + useProxy = useProxyValue.toBool(); + } else if (useProxyValue.isString()) { + const QString raw = useProxyValue.toString().trimmed().toLower(); + if (!raw.isEmpty()) { + useProxy = (raw == "true" || raw == "1" || raw == "yes"); + } + } else if (useProxyValue.isDouble()) { + useProxy = (useProxyValue.toInt() != 0); + } + } + + acc->setAdvancedConfig(identityPath, installationId, customClientPath); + acc->setProxyConfig(useProxy, ip, port, user, pass); + acc->getAuth()->setForceNoProxy(!useProxiesGlobally); + patched++; + } + + if (patched > 0) { + saveSettings(); + updateAllGameforgeAccountVisuals(); + displayProfile(ui->profileComboBox->currentIndex()); + on_gameforgeAccountComboBox_currentIndexChanged(ui->gameforgeAccountComboBox->currentIndex()); + } + + return patched; +} + void MainWindow::openAccount(const Profile *profile, QQueue accountIndexes) { if (accountIndexes.empty()) { From 1940c0fd9640c8347a2d7dd40b5322fd6419b658 Mon Sep 17 00:00:00 2001 From: valde Date: Wed, 8 Apr 2026 15:50:08 -0300 Subject: [PATCH 2/8] feat(noip): fallback failed proxies per account and proxy status colors --- Launcher/src/gui/mainwindow.h | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Launcher/src/gui/mainwindow.h b/Launcher/src/gui/mainwindow.h index b9a71dc..a11e236 100644 --- a/Launcher/src/gui/mainwindow.h +++ b/Launcher/src/gui/mainwindow.h @@ -19,9 +19,14 @@ #include #include #include +#include #include #include #include +#include +#include +#include +#include QT_BEGIN_NAMESPACE @@ -43,6 +48,7 @@ private slots: void on_addGameforgeAccountButton_clicked(); void on_removeGameforgeAccountButton_clicked(); + void on_editGameforgeAccountButton_clicked(); void on_gameSettingsButton_clicked(); @@ -119,6 +125,17 @@ private slots: void displayProfile(int index); void updateGame(); + void setupProxyControls(); + void applyGlobalProxyMode(); + void updateProxyModeButtonText(); + void updateGameforgeAccountVisual(int index); + void updateAllGameforgeAccountVisuals(); + void syncProxifierProfile(); + QString resolveProxifierProfilePath() const; + QDomDocument createDefaultProxifierProfile() const; + QString getAccountIpsJsonPath() const; + void writeAccountIpsJson() const; + int patchNewProxiesFromJson(); Ui::MainWindow *ui; SettingsDialog* settingsDialog; @@ -135,6 +152,10 @@ private slots: int defaultServer = 0; int defaultChannel = 0; int defaultCharacter = 0; + bool loadingStoredAccounts = false; + bool useProxiesGlobally = true; + QToolButton* toggleProxyModeButton = nullptr; + QToolButton* patchNewProxiesButton = nullptr; }; #endif // MAINWINDOW_H From ce00484b01f07670b6303640cfb8be45b3ccfc03 Mon Sep 17 00:00:00 2001 From: valde Date: Wed, 8 Apr 2026 15:57:40 -0300 Subject: [PATCH 3/8] fix(build): auto-copy OpenSSL DLLs to release to prevent TLS initialization failures --- Launcher/GflessClient.pro | 5 +++ Launcher/scripts/copy-openssl.ps1 | 73 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 Launcher/scripts/copy-openssl.ps1 diff --git a/Launcher/GflessClient.pro b/Launcher/GflessClient.pro index b198666..0b76f4b 100644 --- a/Launcher/GflessClient.pro +++ b/Launcher/GflessClient.pro @@ -86,6 +86,11 @@ qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target +win32 { + OPENSSL_COPY_SCRIPT = $$shell_path($$PWD/scripts/copy-openssl.ps1) + OPENSSL_COPY_CMD = powershell -NoProfile -ExecutionPolicy Bypass -File \"$$OPENSSL_COPY_SCRIPT\" -TargetDir \"$$OUT_PWD/release\" -SourceRoot \"$$PWD\" + QMAKE_POST_LINK += $$OPENSSL_COPY_CMD +} RESOURCES += \ resources.qrc diff --git a/Launcher/scripts/copy-openssl.ps1 b/Launcher/scripts/copy-openssl.ps1 new file mode 100644 index 0000000..3b621de --- /dev/null +++ b/Launcher/scripts/copy-openssl.ps1 @@ -0,0 +1,73 @@ +param( + [Parameter(Mandatory = $true)] + [string]$TargetDir, + [Parameter(Mandatory = $false)] + [string]$SourceRoot = "" +) + +$ErrorActionPreference = "Stop" + +function Resolve-CandidatePath { + param([string]$PathValue) + if ([string]::IsNullOrWhiteSpace($PathValue)) { return $null } + try { + return (Resolve-Path -LiteralPath $PathValue -ErrorAction Stop).Path + } catch { + return $null + } +} + +if (-not (Test-Path -LiteralPath $TargetDir)) { + New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null +} + +$dllNames = @("libssl-1_1-x64.dll", "libcrypto-1_1-x64.dll") + +$candidateDirs = @() +if (-not [string]::IsNullOrWhiteSpace($env:OPENSSL_DLL_DIR)) { + $candidateDirs += $env:OPENSSL_DLL_DIR +} + +if (-not [string]::IsNullOrWhiteSpace($SourceRoot)) { + $candidateDirs += @( + (Join-Path $SourceRoot "build-noip-msvc150-release\release"), + (Join-Path $SourceRoot "build-noip-msvc-release\release"), + (Join-Path $SourceRoot "build-noip-proxy-fallback\release"), + (Join-Path $SourceRoot "build-proxy-tools-check\release") + ) +} + +$qtRoot = "C:\Qt\Tools" +if (Test-Path -LiteralPath $qtRoot) { + $candidateDirs += Get-ChildItem -Path $qtRoot -Directory -ErrorAction SilentlyContinue | + ForEach-Object { Join-Path $_.FullName "opt\bin" } +} + +$resolvedDirs = $candidateDirs | + ForEach-Object { Resolve-CandidatePath $_ } | + Where-Object { $_ -and (Test-Path -LiteralPath $_) } | + Select-Object -Unique + +foreach ($dllName in $dllNames) { + $targetFile = Join-Path $TargetDir $dllName + if (Test-Path -LiteralPath $targetFile) { + continue + } + + $sourceFile = $null + foreach ($dir in $resolvedDirs) { + $candidate = Join-Path $dir $dllName + if (Test-Path -LiteralPath $candidate) { + $sourceFile = $candidate + break + } + } + + if ($null -eq $sourceFile) { + Write-Host "[copy-openssl] Missing $dllName (no source found)." + continue + } + + Copy-Item -LiteralPath $sourceFile -Destination $targetFile -Force + Write-Host "[copy-openssl] Copied $dllName from $sourceFile" +} From 30c9c1d98d86948cc451aa59a6074576515ff422 Mon Sep 17 00:00:00 2001 From: valde Date: Wed, 8 Apr 2026 16:13:27 -0300 Subject: [PATCH 4/8] feat(noip): sync Proxifier proxy servers and rules from account proxy settings --- Launcher/GflessClient.pro | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Launcher/GflessClient.pro b/Launcher/GflessClient.pro index 0b76f4b..c4517c0 100644 --- a/Launcher/GflessClient.pro +++ b/Launcher/GflessClient.pro @@ -1,8 +1,13 @@ -QT += core gui network webenginecore +QT += core gui network webenginecore webenginewidgets xml greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 +contains(DEFINES, NO_PROXY_MODE) { + TARGET = GflessClientNoIP +} else { + TARGET = GflessClient +} RC_ICONS = resources/gfless_icon.ico @@ -29,6 +34,7 @@ SOURCES += \ src/gui/addprofiledialog.cpp \ src/auth/blackbox.cpp \ src/gui/captchadialog.cpp \ + src/gui/editproxydialog.cpp \ src/auth/captchasolver.cpp \ src/auth/fingerprint.cpp \ src/auth/gflessclient.cpp \ @@ -54,6 +60,7 @@ HEADERS += \ src/gui/addprofiledialog.h \ src/auth/blackbox.h \ src/gui/captchadialog.h \ + src/gui/editproxydialog.h \ src/auth/captchasolver.h \ src/auth/fingerprint.h \ src/auth/gflessclient.h \ @@ -87,11 +94,13 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target win32 { + WINDEPLOYQT = $$shell_path($$[QT_INSTALL_BINS]/windeployqt.exe) + WINDEPLOYQT_CMD = \"$$WINDEPLOYQT\" --release --compiler-runtime \"$$OUT_PWD/release/$${TARGET}.exe\" OPENSSL_COPY_SCRIPT = $$shell_path($$PWD/scripts/copy-openssl.ps1) OPENSSL_COPY_CMD = powershell -NoProfile -ExecutionPolicy Bypass -File \"$$OPENSSL_COPY_SCRIPT\" -TargetDir \"$$OUT_PWD/release\" -SourceRoot \"$$PWD\" + QMAKE_POST_LINK += $$WINDEPLOYQT_CMD$$escape_expand(\n\t) QMAKE_POST_LINK += $$OPENSSL_COPY_CMD } RESOURCES += \ resources.qrc - From 0e7b4ab425c12626b4860d32eacde930d6971473 Mon Sep 17 00:00:00 2001 From: valde Date: Wed, 8 Apr 2026 16:20:12 -0300 Subject: [PATCH 5/8] fix(build): include Qt5Xml.dll in automatic runtime dependency copy --- Launcher/scripts/copy-openssl.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Launcher/scripts/copy-openssl.ps1 b/Launcher/scripts/copy-openssl.ps1 index 3b621de..9b97d71 100644 --- a/Launcher/scripts/copy-openssl.ps1 +++ b/Launcher/scripts/copy-openssl.ps1 @@ -21,7 +21,7 @@ if (-not (Test-Path -LiteralPath $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null } -$dllNames = @("libssl-1_1-x64.dll", "libcrypto-1_1-x64.dll") +$dllNames = @("libssl-1_1-x64.dll", "libcrypto-1_1-x64.dll", "Qt5Xml.dll") $candidateDirs = @() if (-not [string]::IsNullOrWhiteSpace($env:OPENSSL_DLL_DIR)) { @@ -30,10 +30,10 @@ if (-not [string]::IsNullOrWhiteSpace($env:OPENSSL_DLL_DIR)) { if (-not [string]::IsNullOrWhiteSpace($SourceRoot)) { $candidateDirs += @( - (Join-Path $SourceRoot "build-noip-msvc150-release\release"), - (Join-Path $SourceRoot "build-noip-msvc-release\release"), (Join-Path $SourceRoot "build-noip-proxy-fallback\release"), - (Join-Path $SourceRoot "build-proxy-tools-check\release") + (Join-Path $SourceRoot "build-proxy-tools-check\release"), + (Join-Path $SourceRoot "build-noip-msvc150-release\release"), + (Join-Path $SourceRoot "build-noip-msvc-release\release") ) } @@ -43,6 +43,15 @@ if (Test-Path -LiteralPath $qtRoot) { ForEach-Object { Join-Path $_.FullName "opt\bin" } } +if (-not [string]::IsNullOrWhiteSpace($env:QTDIR)) { + $candidateDirs += (Join-Path $env:QTDIR "bin") +} + +$candidateDirs += @( + "C:\Qt\5.15.0\msvc2019_64\bin", + "C:\Qt\5.15.2\msvc2019\bin" +) + $resolvedDirs = $candidateDirs | ForEach-Object { Resolve-CandidatePath $_ } | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | From b23883f93a0d7996e13089f81e39cbd9cf2a9a52 Mon Sep 17 00:00:00 2001 From: valde Date: Wed, 8 Apr 2026 15:35:37 -0300 Subject: [PATCH 6/8] feat: proxy backup json, proxy mode toggle and proxy editor --- Launcher/CMakeLists.txt | 90 +++++++++++++++++++++++++ Launcher/src/auth/blackboxgenerator.cpp | 10 ++- Launcher/src/gui/mainwindow.ui | 9 ++- 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 Launcher/CMakeLists.txt diff --git a/Launcher/CMakeLists.txt b/Launcher/CMakeLists.txt new file mode 100644 index 0000000..9777d45 --- /dev/null +++ b/Launcher/CMakeLists.txt @@ -0,0 +1,90 @@ +cmake_minimum_required(VERSION 3.21) +project(GflessClientNoIP LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network WebEngineCore WebChannel) + +qt_add_executable(GflessClientNoIP WIN32 + src/auth/blackboxgenerator.cpp + src/auth/gameupdater.cpp + src/gameaccount.cpp + src/gameforgeaccount.cpp + src/gui/addaccountdialog.cpp + src/gui/addprofileaccountdialog.cpp + src/gui/addprofiledialog.cpp + src/auth/blackbox.cpp + src/gui/captchadialog.cpp + src/auth/captchasolver.cpp + src/auth/fingerprint.cpp + src/auth/gflessclient.cpp + src/auth/identity.cpp + src/gui/creategameaccountdialog.cpp + src/gui/editmultipleprofileaccountsdialog.cpp + src/gui/gameupdatedialog.cpp + src/gui/identitydialog.cpp + src/auth/nostaleauth.cpp + src/profile.cpp + src/gui/settingsdialog.cpp + src/syncnetworkaccessmanager.cpp + src/main.cpp + src/gui/mainwindow.cpp + + src/auth/blackboxgenerator.h + src/auth/gameupdater.h + src/gameaccount.h + src/gameforgeaccount.h + src/gui/addaccountdialog.h + src/gui/addprofileaccountdialog.h + src/gui/addprofiledialog.h + src/auth/blackbox.h + src/gui/captchadialog.h + src/auth/captchasolver.h + src/auth/fingerprint.h + src/auth/gflessclient.h + src/auth/identity.h + src/gui/creategameaccountdialog.h + src/gui/editmultipleprofileaccountsdialog.h + src/gui/gameupdatedialog.h + src/gui/identitydialog.h + src/gui/mainwindow.h + src/auth/nostaleauth.h + src/processchecker.h + src/profile.h + src/gui/settingsdialog.h + src/syncnetworkaccessmanager.h + + src/gui/addaccountdialog.ui + src/gui/addprofileaccountdialog.ui + src/gui/addprofiledialog.ui + src/gui/captchadialog.ui + src/gui/creategameaccountdialog.ui + src/gui/editmultipleprofileaccountsdialog.ui + src/gui/gameupdatedialog.ui + src/gui/identitydialog.ui + src/gui/mainwindow.ui + src/gui/settingsdialog.ui + + resources.qrc +) + +target_include_directories(GflessClientNoIP PRIVATE + src + src/gui + src/auth +) + +target_compile_definitions(GflessClientNoIP PRIVATE NO_PROXY_MODE) + +target_link_libraries(GflessClientNoIP PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Network + Qt6::WebEngineCore + Qt6::WebChannel +) diff --git a/Launcher/src/auth/blackboxgenerator.cpp b/Launcher/src/auth/blackboxgenerator.cpp index 8a5c347..91d54ff 100644 --- a/Launcher/src/auth/blackboxgenerator.cpp +++ b/Launcher/src/auth/blackboxgenerator.cpp @@ -45,9 +45,6 @@ QString BlackboxGenerator::generate(const QString &gsid, const QString &installa QEventLoop loop; QString result; - if (BlackboxGenerator::getInstance()->page->isLoading()) - return {}; - connect(getInstance(), &BlackboxGenerator::blackboxCreated, &loop, [&](const QString& blackbox) { result = blackbox; loop.quit(); @@ -63,7 +60,8 @@ QString BlackboxGenerator::generate(const QString &gsid, const QString &installa } else { QJsonObject request = createRequest(gsid, installationId); - QString script = QString("game1(callbackHandler.callback, %1)").arg(QJsonDocument(request).toJson()); + QString json = QString::fromUtf8(QJsonDocument(request).toJson(QJsonDocument::Compact)); + QString script = QString("game1(callbackHandler.callback, %1)").arg(json); connect(getInstance()->page, &QWebEnginePage::loadFinished, getInstance(), [&](bool ok) { if (ok) @@ -85,9 +83,9 @@ QByteArray BlackboxGenerator::encrypt(const QByteArray &blackbox, const QString key = QCryptographicHash::hash(key, QCryptographicHash::Sha512).toHex(); - for (size_t i = 0; i < blackbox.size(); ++i) + for (int i = 0; i < blackbox.size(); ++i) { - size_t key_index = i % key.size(); + int key_index = i % key.size(); encrypted[i] = blackbox[i] ^ key[key_index] ^ key[key.size() - key_index - 1]; } diff --git a/Launcher/src/gui/mainwindow.ui b/Launcher/src/gui/mainwindow.ui index 7f778aa..c611bdf 100644 --- a/Launcher/src/gui/mainwindow.ui +++ b/Launcher/src/gui/mainwindow.ui @@ -29,7 +29,7 @@ - + @@ -47,6 +47,13 @@ + + + + Edit + + + From 5c4820f827ed75df5c47bfe3cc41f5fff314ac8d Mon Sep 17 00:00:00 2001 From: Dherene Date: Tue, 14 Jul 2026 13:50:44 -0300 Subject: [PATCH 7/8] fix(qt5): use QThread::sleep(1) and add NoIP compile script Qt 5.15 does not support the chrono overload from upstream; keep NoIP builds working. Co-authored-by: Cursor --- Launcher/src/auth/nostaleauth.cpp | 3 +- compile_noip.bat | 89 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 compile_noip.bat diff --git a/Launcher/src/auth/nostaleauth.cpp b/Launcher/src/auth/nostaleauth.cpp index 0f2547c..d0b1cab 100644 --- a/Launcher/src/auth/nostaleauth.cpp +++ b/Launcher/src/auth/nostaleauth.cpp @@ -197,7 +197,8 @@ QString NostaleAuth::getToken(const QString &accountId) } // This sleep is a MUST, otherwise the request to thin/codes will fail - QThread::sleep(std::chrono::seconds(1)); + // Qt 5.15: sleep(unsigned long secs). Qt 6 chrono overload is not available here. + QThread::sleep(1); // if (!sendGameLaunch(accountId)) { // return {}; diff --git a/compile_noip.bat b/compile_noip.bat new file mode 100644 index 0000000..c10260e --- /dev/null +++ b/compile_noip.bat @@ -0,0 +1,89 @@ +@echo off +setlocal EnableExtensions + +REM Builds GflessClientNoIP (DEFINES+=NO_PROXY_MODE) with Qt 5.15.0 MSVC 64-bit. +REM Usage: double-click or run from cmd: compile_noip.bat +REM Optional overrides: set QTDIR / VCVARS before running. + +set "ROOT=%~dp0" +set "LAUNCHER=%ROOT%Launcher" +set "BUILD_DIR=%ROOT%build-noip" +set "LOG=%BUILD_DIR%\build.log" + +if not defined QTDIR set "QTDIR=C:\Qt\5.15.0\msvc2019_64" +if not defined VCVARS set "VCVARS=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat" + +if not exist "%LAUNCHER%\GflessClient.pro" ( + echo [ERROR] Project not found: "%LAUNCHER%\GflessClient.pro" + goto :fail +) +if not exist "%QTDIR%\bin\qmake.exe" ( + echo [ERROR] qmake not found: "%QTDIR%\bin\qmake.exe" + echo Set QTDIR to your Qt MSVC kit, e.g. C:\Qt\5.15.0\msvc2019_64 + goto :fail +) +if not exist "%VCVARS%" ( + echo [ERROR] vcvars64.bat not found: "%VCVARS%" + echo Set VCVARS to your Visual Studio vcvars64.bat path. + goto :fail +) + +echo === MSVC environment === +call "%VCVARS%" +if errorlevel 1 ( + echo [ERROR] Failed to load Visual Studio build environment. + goto :fail +) + +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" +pushd "%BUILD_DIR%" || goto :fail + +echo === qmake (release + NO_PROXY_MODE) === +"%QTDIR%\bin\qmake.exe" "%LAUNCHER%\GflessClient.pro" -spec win32-msvc "CONFIG+=release" "DEFINES+=NO_PROXY_MODE" > "%LOG%" 2>&1 +if errorlevel 1 ( + echo [ERROR] qmake failed. See "%LOG%" + type "%LOG%" + popd + goto :fail +) + +echo === nmake release === +echo.>> "%LOG%" +echo ===== nmake release =====>> "%LOG%" +nmake release >> "%LOG%" 2>&1 +if errorlevel 1 ( + echo [ERROR] Build failed. Last lines of log: + echo ---------------------------------------- + powershell -NoProfile -Command "Get-Content -LiteralPath '%LOG%' -Tail 60" + echo ---------------------------------------- + echo Full log: "%LOG%" + popd + goto :fail +) + +set "EXE=" +for /f "delims=" %%F in ('dir /s /b "GflessClientNoIP.exe" 2^>nul') do set "EXE=%%F" + +if not defined EXE ( + echo [ERROR] Build reported success but GflessClientNoIP.exe was not found. + echo Check "%LOG%" + popd + goto :fail +) + +echo. +echo === Done === +echo EXE: %EXE% +popd +echo Build dir: "%BUILD_DIR%" +echo Log: "%LOG%" +if /I "%~1"=="nopause" exit /b 0 +pause +exit /b 0 + +:fail +echo. +echo Build FAILED. +if /I "%~1"=="nopause" exit /b 1 +pause +exit /b 1 From 4afaa7faa8520e243647ea75edf4c4a2ec33a525 Mon Sep 17 00:00:00 2001 From: Dherene Date: Sat, 18 Jul 2026 11:11:01 -0300 Subject: [PATCH 8/8] feat: import/export accounts, iovation retries and NoIP polish Add accounts.json export/import with custom exe remap, safer remove confirm, edit identity browse, and harden login against intermittent iovation 403s. Co-authored-by: Cursor --- Launcher/GflessClient.pro | 7 +- Launcher/src/auth/fingerprint.cpp | 6 + Launcher/src/auth/gflessclient.cpp | 13 +- Launcher/src/auth/nostaleauth.cpp | 77 +++++-- Launcher/src/auth/nostaleauth.h | 4 + Launcher/src/gui/editproxydialog.cpp | 24 +- Launcher/src/gui/mainwindow.cpp | 331 ++++++++++++++++++++++++++- Launcher/src/gui/mainwindow.h | 8 + Launcher/src/gui/mainwindow.ui | 21 +- Launcher/src/gui/settingsdialog.cpp | 10 + Launcher/src/gui/settingsdialog.h | 4 + Launcher/src/gui/settingsdialog.ui | 7 + compile_noip.bat | 18 ++ 13 files changed, 494 insertions(+), 36 deletions(-) diff --git a/Launcher/GflessClient.pro b/Launcher/GflessClient.pro index a8ec931..ab03318 100644 --- a/Launcher/GflessClient.pro +++ b/Launcher/GflessClient.pro @@ -97,10 +97,13 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target win32 { + # DESTDIR=bin puts the exe in $$OUT_PWD/bin, not release/ WINDEPLOYQT = $$shell_path($$[QT_INSTALL_BINS]/windeployqt.exe) - WINDEPLOYQT_CMD = \"$$WINDEPLOYQT\" --release --compiler-runtime \"$$OUT_PWD/release/$${TARGET}.exe\" + TARGET_EXE = $$shell_path($$OUT_PWD/$$DESTDIR/$${TARGET}.exe) + DEPLOY_DIR = $$shell_path($$OUT_PWD/$$DESTDIR) + WINDEPLOYQT_CMD = \"$$WINDEPLOYQT\" --release --compiler-runtime \"$$TARGET_EXE\" OPENSSL_COPY_SCRIPT = $$shell_path($$PWD/scripts/copy-openssl.ps1) - OPENSSL_COPY_CMD = powershell -NoProfile -ExecutionPolicy Bypass -File \"$$OPENSSL_COPY_SCRIPT\" -TargetDir \"$$OUT_PWD/release\" -SourceRoot \"$$PWD\" + OPENSSL_COPY_CMD = powershell -NoProfile -ExecutionPolicy Bypass -File \"$$OPENSSL_COPY_SCRIPT\" -TargetDir \"$$DEPLOY_DIR\" -SourceRoot \"$$PWD\" QMAKE_POST_LINK += $$WINDEPLOYQT_CMD$$escape_expand(\n\t) QMAKE_POST_LINK += $$OPENSSL_COPY_CMD } diff --git a/Launcher/src/auth/fingerprint.cpp b/Launcher/src/auth/fingerprint.cpp index 7281d24..ba3376c 100644 --- a/Launcher/src/auth/fingerprint.cpp +++ b/Launcher/src/auth/fingerprint.cpp @@ -122,5 +122,11 @@ QString Fingerprint::getServerDate() const reply->deleteLater(); + // If the Date header is missing/unparseable (proxy blip, timeout), fall back to UTC now. + // An empty serverTimeInMS produces a bad blackbox and can cause intermittent iovation 403s. + if (!dateTime.isValid()) { + dateTime = QDateTime::currentDateTimeUtc(); + } + return dateTime.toString(Qt::DateFormat::ISODateWithMs); } diff --git a/Launcher/src/auth/gflessclient.cpp b/Launcher/src/auth/gflessclient.cpp index bf585e0..739ded7 100644 --- a/Launcher/src/auth/gflessclient.cpp +++ b/Launcher/src/auth/gflessclient.cpp @@ -1,5 +1,7 @@ #include "gflessclient.h" #include +#include +#include GflessClient::GflessClient(QObject *parent) : QObject(parent) { @@ -51,7 +53,12 @@ bool GflessClient::openClient(const QString& displayName, const QString& token, threadId = procInfo->dwThreadId; - QString dllPath = QDir::currentPath() + "/GflessDLL.dll"; + QString dllPath = QCoreApplication::applicationDirPath() + "/GflessDLL.dll"; + + if (!QFile::exists(dllPath)) { + QMessageBox::critical((QWidget*)this->parent(), "Error", "GflessDLL.dll not found next to GflessClient.\nMake sure your antivirus has not deleted it"); + return false; + } if (!injectDll(pid, dllPath)) qDebug() << "Dll injection failed"; @@ -137,12 +144,12 @@ QByteArray GflessClient::prepareResponse(const QJsonObject &request, const QStri bool GflessClient::injectDll(DWORD pid, const QString &dllPath) { - QString injectorPath = QDir::currentPath() + "/Injector.exe"; + QString injectorPath = QCoreApplication::applicationDirPath() + "/Injector.exe"; QStringList arguments; arguments << QString::number(pid) << dllPath; if (!QFile::exists(injectorPath)) { - QMessageBox::critical((QWidget*)this->parent(), "Error", "Injector.exe not found in the current directory.\nMake sure your antivirus has not deleted it"); + QMessageBox::critical((QWidget*)this->parent(), "Error", "Injector.exe not found next to GflessClient.\nMake sure your antivirus has not deleted it"); return false; } diff --git a/Launcher/src/auth/nostaleauth.cpp b/Launcher/src/auth/nostaleauth.cpp index d0b1cab..48dbfaf 100644 --- a/Launcher/src/auth/nostaleauth.cpp +++ b/Launcher/src/auth/nostaleauth.cpp @@ -1,6 +1,7 @@ #include "nostaleauth.h" #include "blackbox.h" #include "blackboxgenerator.h" +#include #include #include #include @@ -187,28 +188,55 @@ bool NostaleAuth::authenticate(const QString &email, const QString &password, bo QString NostaleAuth::getToken(const QString &accountId) { if (token.isEmpty()) { + lastError = "Missing auth token. Re-add the Gameforge account."; return {}; } - generateGameSessionId(); - - if (!sendIovation(accountId)) { - return {}; + // Space out rapid token requests on the same Gameforge session (same mail / identity). + // Upstream now sends a real blackbox, so GF can reject bursts per accountId with 403. + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + if (lastTokenRequestMs > 0) { + const qint64 elapsedMs = nowMs - lastTokenRequestMs; + const qint64 minGapMs = 3000; + if (elapsedMs < minGapMs) { + QThread::msleep(static_cast(minGapMs - elapsedMs)); + } } - // This sleep is a MUST, otherwise the request to thin/codes will fail - // Qt 5.15: sleep(unsigned long secs). Qt 6 chrono overload is not available here. - QThread::sleep(1); + lastError.clear(); - // if (!sendGameLaunch(accountId)) { - // return {}; - // } + for (int attempt = 0; attempt < 3; ++attempt) { + if (attempt > 0) { + // Fresh identity state + pause before retrying a forbidden/failed iovation. + rebuildIdentity(); + QThread::sleep(2); + } - // if (!sendGameStarted(accountId)) { - // return {}; - // } + generateGameSessionId(); + lastTokenRequestMs = QDateTime::currentMSecsSinceEpoch(); + + if (!sendIovation(accountId)) { + continue; + } + + // This sleep is a MUST, otherwise the request to thin/codes will fail + // Qt 5.15: sleep(unsigned long secs). Qt 6 chrono overload is not available here. + QThread::sleep(1); + + const QString code = sendThinCodes(accountId); + if (!code.isEmpty()) { + lastError.clear(); + return code; + } + + lastError = "thin/codes failed for account " + accountId; + } - return sendThinCodes(accountId); + if (lastError.isEmpty()) { + lastError = "auth/iovation failed for account " + accountId; + } + + return {}; } QChar NostaleAuth::getFirstNumber(QString uuid) @@ -461,19 +489,25 @@ bool NostaleAuth::sendIovation(const QString& accountId) QByteArray body = QJsonDocument(content).toJson(QJsonDocument::JsonFormat::Compact); reply = networkManager->post(request, body); + const QByteArray response = reply->readAll(); + const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); - QByteArray response = reply->readAll(); - - qDebug() << "NostaleAuth::sendIovation" << response; + qDebug() << "NostaleAuth::sendIovation" << statusCode << response; - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) + if (statusCode != 200) { + lastError = QString("auth/iovation server replied: %1").arg( + statusCode == 403 ? "forbidden" : QString::number(statusCode) + ); return false; + } jsonResponse = QJsonDocument::fromJson(response).object(); - if (jsonResponse["status"] != "ok") + if (jsonResponse["status"] != "ok") { + lastError = "auth/iovation status not ok"; return false; + } return true; } @@ -683,6 +717,11 @@ QString NostaleAuth::getToken() const return token; } +QString NostaleAuth::getLastError() const +{ + return lastError; +} + QString NostaleAuth::getInstallationId() const { return installationId; diff --git a/Launcher/src/auth/nostaleauth.h b/Launcher/src/auth/nostaleauth.h index 93339bc..f81abbf 100644 --- a/Launcher/src/auth/nostaleauth.h +++ b/Launcher/src/auth/nostaleauth.h @@ -68,6 +68,8 @@ class NostaleAuth : public QObject void setToken(const QString &newToken); QString getToken() const; + QString getLastError() const; + signals: void captchaStart(); void captchaEnd(); @@ -141,6 +143,8 @@ class NostaleAuth : public QObject bool useProxy; bool forceNoProxy = false; QString identityPath; + QString lastError; + qint64 lastTokenRequestMs = 0; void applyProxyConfiguration(); void rebuildIdentity(); diff --git a/Launcher/src/gui/editproxydialog.cpp b/Launcher/src/gui/editproxydialog.cpp index feacad9..febd262 100644 --- a/Launcher/src/gui/editproxydialog.cpp +++ b/Launcher/src/gui/editproxydialog.cpp @@ -32,6 +32,16 @@ EditProxyDialog::EditProxyDialog(QWidget* parent) formLayout->addRow("Port:", socksPortLineEdit); formLayout->addRow("Username:", proxyUsernameLineEdit); formLayout->addRow("Password:", proxyPasswordLineEdit); + QWidget* identityPathWidget = new QWidget(this); + QHBoxLayout* identityPathLayout = new QHBoxLayout(identityPathWidget); + identityPathLayout->setContentsMargins(0, 0, 0, 0); + identityPathLayout->setSpacing(6); + QPushButton* browseIdentityPathButton = new QPushButton("...", identityPathWidget); + browseIdentityPathButton->setToolTip("Select identity file (.json)"); + browseIdentityPathButton->setFixedWidth(32); + identityPathLayout->addWidget(identityPathLineEdit); + identityPathLayout->addWidget(browseIdentityPathButton); + QWidget* customGamePathWidget = new QWidget(this); QHBoxLayout* customGamePathLayout = new QHBoxLayout(customGamePathWidget); customGamePathLayout->setContentsMargins(0, 0, 0, 0); @@ -42,7 +52,7 @@ EditProxyDialog::EditProxyDialog(QWidget* parent) customGamePathLayout->addWidget(customGamePathLineEdit); customGamePathLayout->addWidget(browseCustomGamePathButton); - formLayout->addRow("Identity path (optional):", identityPathLineEdit); + formLayout->addRow("Identity path (optional):", identityPathWidget); formLayout->addRow("Custom game path (optional):", customGamePathWidget); formLayout->addRow("Custom installation id (optional):", installationIdLineEdit); @@ -58,6 +68,18 @@ EditProxyDialog::EditProxyDialog(QWidget* parent) setLayout(mainLayout); connect(useProxyCheckBox, &QCheckBox::toggled, this, &EditProxyDialog::updateProxyFieldsEnabled); + connect(browseIdentityPathButton, &QPushButton::clicked, this, [this]() { + const QString path = QFileDialog::getOpenFileName( + this, + "Select identity file", + QDir::rootPath(), + "(*.json)" + ); + + if (!path.isEmpty()) { + identityPathLineEdit->setText(path); + } + }); connect(browseCustomGamePathButton, &QPushButton::clicked, this, [this]() { const QString path = QFileDialog::getOpenFileName( this, diff --git a/Launcher/src/gui/mainwindow.cpp b/Launcher/src/gui/mainwindow.cpp index db91b7b..7621d59 100644 --- a/Launcher/src/gui/mainwindow.cpp +++ b/Launcher/src/gui/mainwindow.cpp @@ -9,6 +9,7 @@ #include "gameupdatedialog.h" #include "creategameaccountdialog.h" #include +#include #include #include #include @@ -68,6 +69,7 @@ void MainWindow::loadSettings() settingsDialog->setThemeComboBox(settings.value("theme", 0).toInt()); settingsDialog->setDisabledNosmall(settings.value("disable_nosmall", false).toBool()); settingsDialog->setCheckUpdates(settings.value("check_updates", true).toBool()); + settingsDialog->setConfirmRemoveAccount(settings.value("confirm_remove_account", true).toBool()); defaultAutoLogin = settings.value("default_autologin", false).toBool(); defaultServerLocation = settings.value("default_serverlocation", 0).toInt(); @@ -132,6 +134,7 @@ void MainWindow::saveSettings() settings.setValue("theme", settingsDialog->getTheme()); settings.setValue("disable_nosmall", settingsDialog->getDisabledNosmall()); settings.setValue("check_updates", settingsDialog->getCheckUpdates()); + settings.setValue("confirm_remove_account", settingsDialog->getConfirmRemoveAccount()); settings.setValue("default_autologin", defaultAutoLogin); settings.setValue("default_serverlocation", defaultServerLocation); settings.setValue("default_server", defaultServer); @@ -543,22 +546,41 @@ void MainWindow::on_addGameforgeAccountButton_clicked() void MainWindow::on_removeGameforgeAccountButton_clicked() { - int res = QMessageBox::warning(this, "Warning", "Do you want to remove this account?\n(This will not delete your real account)", QMessageBox::Yes | QMessageBox::No); - - if (res == QMessageBox::No) - return; - int index = ui->gameforgeAccountComboBox->currentIndex(); if (index < 0) return; - removeAccountsFromDefaultProfile(ui->gameforgeAccountComboBox->currentText()); + const QString email = ui->gameforgeAccountComboBox->currentText(); + + if (settingsDialog->getConfirmRemoveAccount()) { + QMessageBox box(this); + box.setIcon(QMessageBox::Warning); + box.setWindowTitle("Warning"); + box.setText("Do you want to remove the account " + email + "?\n(This will not delete your real account)"); + box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + box.setDefaultButton(QMessageBox::No); + + QCheckBox* dontAskCheckBox = new QCheckBox("Don't ask me again", &box); + box.setCheckBox(dontAskCheckBox); + + const int res = box.exec(); + + if (res != QMessageBox::Yes) + return; + + if (dontAskCheckBox->isChecked()) { + settingsDialog->setConfirmRemoveAccount(false); + } + } + + removeAccountsFromDefaultProfile(email); gfAccounts.remove(index); ui->gameforgeAccountComboBox->removeItem(index); writeAccountIpsJson(); + saveSettings(); displayProfile(ui->profileComboBox->currentIndex()); } @@ -945,6 +967,297 @@ void MainWindow::on_actionSave_profiles_triggered() saveAccountProfiles(path); } +void MainWindow::on_actionExport_triggered() +{ + const QString path = QDir(QCoreApplication::applicationDirPath()).filePath("accounts.json"); + + if (exportData(path)) { + QMessageBox::information(this, "Export", "Exported to:\n" + QDir::toNativeSeparators(path)); + } + else { + QMessageBox::critical(this, "Export", "Could not write the export file."); + } +} + +void MainWindow::on_actionImport_triggered() +{ + const QString defaultPath = QDir(QCoreApplication::applicationDirPath()).filePath("accounts.json"); + QString path = QFileDialog::getOpenFileName(this, "Import accounts and profiles", defaultPath, "(*.json)"); + + if (path.isEmpty()) + return; + + int res = QMessageBox::question( + this, + "Import", + "This will load all accounts and profiles from the file.\n" + "It may take a while if there are many accounts. Continue?", + QMessageBox::Yes | QMessageBox::No + ); + + if (res != QMessageBox::Yes) + return; + + setCursor(Qt::WaitCursor); + importData(path); + unsetCursor(); +} + +bool MainWindow::exportData(const QString &path) +{ + QJsonArray accountsArray; + + for (const GameforgeAccount* acc : gfAccounts) { + const NostaleAuth* auth = acc->getAuth(); + + QJsonObject obj; + obj["email"] = acc->getEmail(); + obj["password"] = acc->getPassword(); + obj["token"] = auth->getToken(); + obj["identity_path"] = acc->getIdentityPath(); + obj["custom_game_path"] = acc->getcustomClientPath(); + obj["custom_installation_id"] = auth->getInstallationId(); + obj["use_proxy"] = auth->getUseProxy(); + obj["proxy_ip"] = auth->getProxyIp(); + obj["socks_port"] = auth->getSocksPort(); + obj["proxy_username"] = auth->getProxyUsername(); + obj["proxy_password"] = auth->getProxyPassword(); + accountsArray.append(obj); + } + + QJsonArray profilesArray; + + // Skip the default profile (index 0); it is auto-populated from the accounts. + for (int i = 1; i < profiles.size(); ++i) { + Profile* profile = profiles[i]; + + QJsonObject profileObj; + profileObj["profile_name"] = profile->getProfileName(); + + QJsonArray gameAccountsArray; + for (const GameAccount& ga : profile->getAccounts()) { + if (ga.getGfAcc() == nullptr) + continue; + + QJsonObject gaObj; + gaObj["email"] = ga.getGfAcc()->getEmail(); + gaObj["account_name"] = ga.getName(); + gaObj["id"] = ga.getId(); + gaObj["display_name"] = ga.getDisplayName(); + gaObj["server_location"] = ga.getServerLocation(); + gaObj["server"] = ga.getServer(); + gaObj["channel"] = ga.getChannel(); + gaObj["character"] = ga.getSlot(); + gaObj["auto_login"] = ga.getAutoLogin(); + gameAccountsArray.append(gaObj); + } + profileObj["game_accounts"] = gameAccountsArray; + profilesArray.append(profileObj); + } + + QJsonObject root; + root["version"] = 1; + root["generated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + root["gameforge_accounts"] = accountsArray; + root["profiles"] = profilesArray; + + QFile out(path); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return false; + } + + out.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + out.close(); + return true; +} + +QString MainWindow::resolveImportedCustomClientPath(const QString &originalCustomPath) +{ + const QString original = originalCustomPath.trimmed(); + if (original.isEmpty()) + return QString(); + + // The exported machine path most likely does not exist here. Keep only the + // exe file name and derive a local copy inside the current nostale directory. + const QString fileName = QFileInfo(original).fileName(); + if (fileName.isEmpty()) + return QString(); + + const QString nostalePath = settingsDialog->getGameClientPath(); + if (nostalePath.isEmpty()) + return QString(); + + const QFileInfo nostaleInfo(nostalePath); + const QString nostaleDir = nostaleInfo.absolutePath(); + if (nostaleDir.isEmpty()) + return QString(); + + // Forward slashes are required by openClient(), which splits on '/'. + const QString newPath = QDir(nostaleDir).filePath(fileName); + + // If the derived path is the base client itself, no copy is required. + if (QFileInfo(newPath).absoluteFilePath().compare(nostaleInfo.absoluteFilePath(), Qt::CaseInsensitive) == 0) { + return newPath; + } + + if (!QFile::exists(newPath)) { + if (!QFile::exists(nostalePath)) { + return QString(); + } + QFile::copy(nostalePath, newPath); + } + + return newPath; +} + +bool MainWindow::importData(const QString &path) +{ + QFile in(path); + if (!in.open(QIODevice::ReadOnly)) { + QMessageBox::critical(this, "Import", "Could not open the selected file."); + return false; + } + + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(in.readAll(), &error); + in.close(); + + if (error.error != QJsonParseError::NoError || !doc.isObject()) { + QMessageBox::critical(this, "Import", "The selected file is not a valid Gfless export."); + return false; + } + + const QJsonObject root = doc.object(); + const QJsonArray accountsArray = root.value("gameforge_accounts").toArray(); + const QJsonArray profilesArray = root.value("profiles").toArray(); + + QSet existingEmails; + for (GameforgeAccount* acc : gfAccounts) { + existingEmails.insert(acc->getEmail()); + } + + int importedAccounts = 0; + int skippedAccounts = 0; + + for (const QJsonValue& value : accountsArray) { + const QJsonObject obj = value.toObject(); + const QString email = obj.value("email").toString().trimmed(); + + if (email.isEmpty() || existingEmails.contains(email)) { + ++skippedAccounts; + continue; + } + + const QString password = obj.value("password").toString(); + const QString token = obj.value("token").toString(); + const QString identityPath = obj.value("identity_path").toString().trimmed(); + const QString installationId = obj.value("custom_installation_id").toString().trimmed(); + const QString customGamePath = resolveImportedCustomClientPath(obj.value("custom_game_path").toString()); + const bool useProxy = obj.value("use_proxy").toBool(); + const QString proxyIp = obj.value("proxy_ip").toString().trimmed(); + const QString socksPort = obj.value("socks_port").toString().trimmed(); + const QString proxyUsername = obj.value("proxy_username").toString(); + const QString proxyPassword = obj.value("proxy_password").toString(); + + ui->statusbar->showMessage("Importing account " + email + "...", 5000); + QCoreApplication::processEvents(); + + const int before = gfAccounts.size(); + + if (!token.isEmpty()) { + addGameforgeAccount(email, password, token, identityPath, installationId, customGamePath, proxyIp, socksPort, proxyUsername, proxyPassword, useProxy); + } + else { + addGameforgeAccount(email, password, identityPath, installationId, customGamePath, proxyIp, socksPort, proxyUsername, proxyPassword, useProxy); + } + + if (gfAccounts.size() > before) { + existingEmails.insert(email); + ++importedAccounts; + } + else { + ++skippedAccounts; + } + } + + int importedProfiles = 0; + + for (const QJsonValue& value : profilesArray) { + const QJsonObject profileObj = value.toObject(); + const QString profileName = profileObj.value("profile_name").toString().trimmed(); + + if (profileName.isEmpty()) + continue; + + Profile* profile = nullptr; + for (int i = 1; i < profiles.size(); ++i) { + if (profiles[i]->getProfileName() == profileName) { + profile = profiles[i]; + break; + } + } + + if (profile == nullptr) { + profile = new Profile(profileName, this); + profiles.push_back(profile); + ui->profileComboBox->addItem(profileName); + ++importedProfiles; + } + + const QJsonArray gameAccountsArray = profileObj.value("game_accounts").toArray(); + for (const QJsonValue& gaValue : gameAccountsArray) { + const QJsonObject gaObj = gaValue.toObject(); + const QString email = gaObj.value("email").toString(); + + GameforgeAccount* gfacc = nullptr; + for (GameforgeAccount* candidate : gfAccounts) { + if (candidate->getEmail() == email) { + gfacc = candidate; + break; + } + } + + if (gfacc == nullptr) + continue; + + GameAccount gameAcc( + gfacc, + gaObj.value("account_name").toString(), + gaObj.value("id").toString(), + gaObj.value("display_name").toString(), + gaObj.value("server_location").toInt(), + gaObj.value("server").toInt(), + gaObj.value("channel").toInt(), + gaObj.value("character").toInt(), + gaObj.value("auto_login").toBool() + ); + + profile->addAccount(gameAcc); + } + } + + writeAccountIpsJson(); + saveSettings(); +#ifdef NO_PROXY_MODE + syncProxifierProfile(); +#endif + updateAllGameforgeAccountVisuals(); + displayProfile(ui->profileComboBox->currentIndex()); + on_gameforgeAccountComboBox_currentIndexChanged(ui->gameforgeAccountComboBox->currentIndex()); + ui->statusbar->clearMessage(); + + QMessageBox::information( + this, + "Import", + QString("Imported %1 account(s), skipped %2.\nImported %3 new profile(s).") + .arg(importedAccounts) + .arg(skippedAccounts) + .arg(importedProfiles) + ); + + return true; +} + void MainWindow::on_actionIdentity_generator_triggered() { IdentityDialog dialog(this); @@ -1787,7 +2100,11 @@ void MainWindow::openAccount(const Profile *profile, QQueue accountIndexes) QString token = gameAccount.getGfAcc()->getToken(gameAccount.getId()); if (token.isEmpty()) { - ui->statusbar->showMessage("Couldn't get token for account " + gameAccount.getName(), 10000); + QString detail = gameAccount.getGfAcc()->getAuth()->getLastError(); + if (detail.isEmpty()) { + detail = "Couldn't get token"; + } + ui->statusbar->showMessage(detail + " (" + gameAccount.getName() + ")", 12000); } else { DWORD pid = 0; diff --git a/Launcher/src/gui/mainwindow.h b/Launcher/src/gui/mainwindow.h index a11e236..11a26b2 100644 --- a/Launcher/src/gui/mainwindow.h +++ b/Launcher/src/gui/mainwindow.h @@ -68,6 +68,10 @@ private slots: void on_actionSave_profiles_triggered(); + void on_actionExport_triggered(); + + void on_actionImport_triggered(); + void on_actionIdentity_generator_triggered(); void on_addProfileButton_clicked(); @@ -124,6 +128,10 @@ private slots: void removeAccountsFromDefaultProfile(const QString& email); void displayProfile(int index); + bool exportData(const QString& path); + bool importData(const QString& path); + QString resolveImportedCustomClientPath(const QString& originalCustomPath); + void updateGame(); void setupProxyControls(); void applyGlobalProxyMode(); diff --git a/Launcher/src/gui/mainwindow.ui b/Launcher/src/gui/mainwindow.ui index c611bdf..f7a73d8 100644 --- a/Launcher/src/gui/mainwindow.ui +++ b/Launcher/src/gui/mainwindow.ui @@ -41,16 +41,16 @@ - + - Remove + Edit - + - Edit + Remove @@ -193,6 +193,9 @@ Options + + + @@ -239,6 +242,16 @@ Identity generator + + + Export + + + + + Import + + diff --git a/Launcher/src/gui/settingsdialog.cpp b/Launcher/src/gui/settingsdialog.cpp index 87460a4..eb7f93d 100644 --- a/Launcher/src/gui/settingsdialog.cpp +++ b/Launcher/src/gui/settingsdialog.cpp @@ -107,6 +107,16 @@ bool SettingsDialog::getCheckUpdates() const return ui->checkUpdatesCheckbox->isChecked(); } +void SettingsDialog::setConfirmRemoveAccount(bool b) +{ + ui->confirmRemoveAccountCheckbox->setChecked(b); +} + +bool SettingsDialog::getConfirmRemoveAccount() const +{ + return ui->confirmRemoveAccountCheckbox->isChecked(); +} + void SettingsDialog::on_selectGamePathButton_clicked() { QString path = QFileDialog::getOpenFileName(this, "Select NostaleClientX.exe", QDir::rootPath(), "NostaleClientX.exe (NostaleClientX.exe)"); diff --git a/Launcher/src/gui/settingsdialog.h b/Launcher/src/gui/settingsdialog.h index dc31032..491ead6 100644 --- a/Launcher/src/gui/settingsdialog.h +++ b/Launcher/src/gui/settingsdialog.h @@ -48,6 +48,10 @@ class SettingsDialog : public QDialog bool getCheckUpdates() const; + void setConfirmRemoveAccount(bool b); + + bool getConfirmRemoveAccount() const; + signals: void profilesPathSelected(QString); diff --git a/Launcher/src/gui/settingsdialog.ui b/Launcher/src/gui/settingsdialog.ui index d8e9a26..9992a00 100644 --- a/Launcher/src/gui/settingsdialog.ui +++ b/Launcher/src/gui/settingsdialog.ui @@ -154,6 +154,13 @@ + + + + Ask for confirmation before removing an account + + + diff --git a/compile_noip.bat b/compile_noip.bat index c10260e..588f75c 100644 --- a/compile_noip.bat +++ b/compile_noip.bat @@ -74,6 +74,24 @@ if not defined EXE ( echo. echo === Done === echo EXE: %EXE% + +REM Copy Injector + GflessDLL next to the exe (required at Play time) +set "BIN_DIR=%BUILD_DIR%\bin" +set "RUNTIME=%ROOT%runtime" +if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" +if exist "%RUNTIME%\Injector.exe" ( + copy /Y "%RUNTIME%\Injector.exe" "%BIN_DIR%\Injector.exe" >nul + echo Copied Injector.exe +) else ( + echo [WARN] runtime\Injector.exe missing - Play will fail until you add it. +) +if exist "%RUNTIME%\GflessDLL.dll" ( + copy /Y "%RUNTIME%\GflessDLL.dll" "%BIN_DIR%\GflessDLL.dll" >nul + echo Copied GflessDLL.dll +) else ( + echo [WARN] runtime\GflessDLL.dll missing - Play will fail until you add it. +) + popd echo Build dir: "%BUILD_DIR%" echo Log: "%LOG%"