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/GflessClient.pro b/Launcher/GflessClient.pro index 0a68c70..f2d35ac 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 @@ -31,6 +36,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 \ @@ -56,6 +62,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 \ @@ -88,7 +95,14 @@ qnx: target.path = /tmp/$${TARGET}/bin 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 - diff --git a/Launcher/scripts/copy-openssl.ps1 b/Launcher/scripts/copy-openssl.ps1 new file mode 100644 index 0000000..9b97d71 --- /dev/null +++ b/Launcher/scripts/copy-openssl.ps1 @@ -0,0 +1,82 @@ +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", "Qt5Xml.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-proxy-fallback\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") + ) +} + +$qtRoot = "C:\Qt\Tools" +if (Test-Path -LiteralPath $qtRoot) { + $candidateDirs += Get-ChildItem -Path $qtRoot -Directory -ErrorAction SilentlyContinue | + 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 $_) } | + 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" +} 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/auth/nostaleauth.cpp b/Launcher/src/auth/nostaleauth.cpp index 058c04f..dd65993 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(); @@ -793,3 +774,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 28ebc67..8e39bf5 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); @@ -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()) { 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 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 + + +