diff options
| -rw-r--r-- | libs/plugin_python/src/proxy/proxypython.cpp | 2 | ||||
| -rw-r--r-- | src/src/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/src/prefixsetupdialog.cpp | 306 | ||||
| -rw-r--r-- | src/src/prefixsetupdialog.h | 76 | ||||
| -rw-r--r-- | src/src/prefixsetuprunner.cpp | 1172 | ||||
| -rw-r--r-- | src/src/prefixsetuprunner.h | 134 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.cpp | 181 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.h | 8 |
8 files changed, 1723 insertions, 160 deletions
diff --git a/libs/plugin_python/src/proxy/proxypython.cpp b/libs/plugin_python/src/proxy/proxypython.cpp index 0037991..88d8769 100644 --- a/libs/plugin_python/src/proxy/proxypython.cpp +++ b/libs/plugin_python/src/proxy/proxypython.cpp @@ -293,7 +293,9 @@ QStringList ProxyPython::pluginList(const QDir& pluginPath) const } else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { // Skip Windows-only directory plugins that use ctypes.windll etc. + // Skip basic_games — replaced by native libbasic_games_native.so. if (baseName == "crashlogtools" || + baseName == "basic_games" || baseName.compare("rootbuilder", Qt::CaseInsensitive) == 0) { continue; } diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index 146183d..ffa4902 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -32,12 +32,16 @@ file(GLOB ORGANIZER_QRC list(APPEND ORGANIZER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/fluorineconfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/fluorinepaths.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/prefixsetupdialog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/prefixsetuprunner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protonlauncher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/settingsdialogproton.cpp ${CMAKE_CURRENT_SOURCE_DIR}/wineprefix.cpp) list(APPEND ORGANIZER_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/fluorineconfig.h ${CMAKE_CURRENT_SOURCE_DIR}/fluorinepaths.h + ${CMAKE_CURRENT_SOURCE_DIR}/prefixsetupdialog.h + ${CMAKE_CURRENT_SOURCE_DIR}/prefixsetuprunner.h ${CMAKE_CURRENT_SOURCE_DIR}/protonlauncher.h ${CMAKE_CURRENT_SOURCE_DIR}/settingsdialogproton.h ${CMAKE_CURRENT_SOURCE_DIR}/wineprefix.h) diff --git a/src/src/prefixsetupdialog.cpp b/src/src/prefixsetupdialog.cpp new file mode 100644 index 0000000..6395af0 --- /dev/null +++ b/src/src/prefixsetupdialog.cpp @@ -0,0 +1,306 @@ +#include "prefixsetupdialog.h" + +#include "fluorineconfig.h" +#include "fluorinepaths.h" + +#include <QDesktopServices> +#include <QHBoxLayout> +#include <QLabel> +#include <QMessageBox> +#include <QShowEvent> +#include <QSplitter> +#include <QUrl> +#include <QVBoxLayout> + +#include <log.h> +#include <utility.h> + +// ============================================================================ +// Construction / destruction +// ============================================================================ + +PrefixSetupDialog::PrefixSetupDialog(const QString& prefixPath, + const QString& protonPath, + uint32_t appId, + QWidget* parent) + : QDialog(parent) + , m_prefixPath(prefixPath) + , m_protonPath(protonPath) + , m_appId(appId) +{ + setWindowTitle(tr("Wine Prefix Setup")); + setMinimumSize(700, 500); + resize(800, 600); + setModal(true); + + buildUI(); + + // Create the runner (lives on a worker thread). + m_workerThread = new QThread(this); + m_runner = new PrefixSetupRunner(prefixPath, protonPath, appId); + m_runner->moveToThread(m_workerThread); + + // Wire signals. + connect(m_runner, &PrefixSetupRunner::stepStarted, + this, &PrefixSetupDialog::onStepStarted, Qt::QueuedConnection); + connect(m_runner, &PrefixSetupRunner::stepFinished, + this, &PrefixSetupDialog::onStepFinished, Qt::QueuedConnection); + connect(m_runner, &PrefixSetupRunner::logMessage, + this, &PrefixSetupDialog::onLogMessage, Qt::QueuedConnection); + connect(m_runner, &PrefixSetupRunner::progressChanged, + this, &PrefixSetupDialog::onProgressChanged, Qt::QueuedConnection); + connect(m_runner, &PrefixSetupRunner::finished, + this, &PrefixSetupDialog::onFinished, Qt::QueuedConnection); + + // Clean up worker thread when dialog closes. + connect(m_workerThread, &QThread::finished, m_runner, &QObject::deleteLater); + + m_workerThread->start(); + + // Populate step list from the runner's step definitions. + populateStepList(); +} + +PrefixSetupDialog::~PrefixSetupDialog() +{ + m_runner->cancel(); + m_workerThread->quit(); + m_workerThread->wait(5000); +} + +// ============================================================================ +// UI construction +// ============================================================================ + +void PrefixSetupDialog::buildUI() +{ + auto* mainLayout = new QVBoxLayout(this); + + // Status label at top. + m_statusLabel = new QLabel(tr("Preparing..."), this); + m_statusLabel->setStyleSheet("font-weight: bold; font-size: 13px;"); + mainLayout->addWidget(m_statusLabel); + + // Splitter: step list (left) + log (right). + auto* splitter = new QSplitter(Qt::Horizontal, this); + + m_stepList = new QListWidget(this); + m_stepList->setMinimumWidth(250); + m_stepList->setMaximumWidth(350); + m_stepList->setSelectionMode(QAbstractItemView::SingleSelection); + splitter->addWidget(m_stepList); + + m_logView = new QTextEdit(this); + m_logView->setReadOnly(true); + m_logView->setFont(QFont("monospace", 9)); + m_logView->setPlaceholderText(tr("Setup log output will appear here...")); + splitter->addWidget(m_logView); + + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + mainLayout->addWidget(splitter, 1); + + // Progress bar. + m_progressBar = new QProgressBar(this); + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + m_progressBar->setTextVisible(true); + mainLayout->addWidget(m_progressBar); + + // Button row. + auto* buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + + m_cancelBtn = new QPushButton(tr("Cancel"), this); + connect(m_cancelBtn, &QPushButton::clicked, this, &PrefixSetupDialog::onCancel); + buttonLayout->addWidget(m_cancelBtn); + + m_retryBtn = new QPushButton(tr("Retry Failed"), this); + m_retryBtn->setVisible(false); + connect(m_retryBtn, &QPushButton::clicked, this, &PrefixSetupDialog::onRetryFailed); + buttonLayout->addWidget(m_retryBtn); + + m_deleteBtn = new QPushButton(tr("Delete Prefix && Show Log"), this); + m_deleteBtn->setVisible(false); + connect(m_deleteBtn, &QPushButton::clicked, this, &PrefixSetupDialog::onDeleteAndClose); + buttonLayout->addWidget(m_deleteBtn); + + m_closeBtn = new QPushButton(tr("Close"), this); + m_closeBtn->setVisible(false); + connect(m_closeBtn, &QPushButton::clicked, this, &PrefixSetupDialog::onClose); + buttonLayout->addWidget(m_closeBtn); + + mainLayout->addLayout(buttonLayout); +} + +void PrefixSetupDialog::populateStepList() +{ + m_stepList->clear(); + for (const auto& step : m_runner->steps()) { + auto* item = new QListWidgetItem( + QStringLiteral(" %1").arg(step.displayName), m_stepList); + item->setForeground(Qt::gray); + } +} + +void PrefixSetupDialog::updateButtons() +{ + m_cancelBtn->setVisible(m_running); + m_retryBtn->setVisible(!m_running && !m_allSucceeded); + m_deleteBtn->setVisible(!m_running && !m_allSucceeded); + m_closeBtn->setVisible(!m_running); +} + +// ============================================================================ +// Auto-start on show +// ============================================================================ + +void PrefixSetupDialog::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + + if (!m_started) { + m_started = true; + m_running = true; + updateButtons(); + QMetaObject::invokeMethod(m_runner, "start", Qt::QueuedConnection); + } +} + +// ============================================================================ +// Slots — step progress +// ============================================================================ + +void PrefixSetupDialog::onStepStarted(int index) +{ + if (index < 0 || index >= m_stepList->count()) return; + + auto* item = m_stepList->item(index); + const QString name = m_runner->steps().at(index).displayName; + item->setText(QStringLiteral("▶ %1").arg(name)); + item->setForeground(QColor("#4fc3f7")); // light blue + m_stepList->scrollToItem(item); + + m_statusLabel->setText(QStringLiteral("Running: %1").arg(name)); +} + +void PrefixSetupDialog::onStepFinished(int index, bool success, const QString& error) +{ + if (index < 0 || index >= m_stepList->count()) return; + + auto* item = m_stepList->item(index); + const QString name = m_runner->steps().at(index).displayName; + + if (success) { + item->setText(QStringLiteral("✓ %1").arg(name)); + item->setForeground(QColor("#66bb6a")); // green + } else { + item->setText(QStringLiteral("✗ %1").arg(name)); + item->setForeground(QColor("#ef5350")); // red + if (!error.isEmpty()) { + onLogMessage(QStringLiteral("ERROR [%1]: %2").arg(name, error)); + } + } +} + +void PrefixSetupDialog::onLogMessage(const QString& text) +{ + m_logView->append(text); + + // Also forward to MO2 log system. + MOBase::log::info("{}", text); + + // Auto-scroll to bottom. + auto* sb = m_logView->verticalScrollBar(); + if (sb) sb->setValue(sb->maximum()); +} + +void PrefixSetupDialog::onProgressChanged(float progress) +{ + m_progressBar->setValue(static_cast<int>(progress * 100.0f)); +} + +void PrefixSetupDialog::onFinished(bool allSucceeded) +{ + m_allSucceeded = allSucceeded; + m_running = false; + updateButtons(); + + if (allSucceeded) { + m_statusLabel->setText(tr("Setup completed successfully!")); + m_statusLabel->setStyleSheet("font-weight: bold; font-size: 13px; color: #66bb6a;"); + m_progressBar->setValue(100); + } else { + // Count failures. + int failCount = 0; + for (const auto& step : m_runner->steps()) { + if (step.status == SetupStep::Failed) ++failCount; + } + m_statusLabel->setText( + tr("Setup finished with %n failure(s). You can retry or delete the prefix.", + "", failCount)); + m_statusLabel->setStyleSheet("font-weight: bold; font-size: 13px; color: #ef5350;"); + } +} + +// ============================================================================ +// Button handlers +// ============================================================================ + +void PrefixSetupDialog::onCancel() +{ + m_runner->cancel(); + m_statusLabel->setText(tr("Cancelling...")); + m_cancelBtn->setEnabled(false); +} + +void PrefixSetupDialog::onRetryFailed() +{ + m_running = true; + m_statusLabel->setText(tr("Retrying failed steps...")); + m_statusLabel->setStyleSheet("font-weight: bold; font-size: 13px;"); + updateButtons(); + + // Reset failed items to pending appearance. + for (int i = 0; i < m_stepList->count(); ++i) { + if (m_runner->steps().at(i).status == SetupStep::Failed) { + auto* item = m_stepList->item(i); + item->setText(QStringLiteral(" %1").arg(m_runner->steps().at(i).displayName)); + item->setForeground(Qt::gray); + } + } + + QMetaObject::invokeMethod(m_runner, "retryFailed", Qt::QueuedConnection); +} + +void PrefixSetupDialog::onDeleteAndClose() +{ + const auto answer = QMessageBox::warning( + this, tr("Delete Prefix"), + tr("This will delete the Wine prefix at:\n%1\n\n" + "The log folder will be opened so you can share the log.\n\n" + "Continue?").arg(m_prefixPath), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (answer != QMessageBox::Yes) + return; + + // Delete the prefix. + FluorineConfig cfg; + cfg.prefix_path = m_prefixPath; + cfg.destroyPrefix(); + + // Open the logs folder. + const QString logsPath = fluorineDataDir() + "/logs"; + QDesktopServices::openUrl(QUrl::fromLocalFile(logsPath)); + + reject(); +} + +void PrefixSetupDialog::onClose() +{ + if (m_allSucceeded) + accept(); + else + reject(); +} diff --git a/src/src/prefixsetupdialog.h b/src/src/prefixsetupdialog.h new file mode 100644 index 0000000..3c23ff1 --- /dev/null +++ b/src/src/prefixsetupdialog.h @@ -0,0 +1,76 @@ +#ifndef PREFIXSETUPDIALOG_H +#define PREFIXSETUPDIALOG_H + +#include "prefixsetuprunner.h" + +#include <QDialog> +#include <QListWidget> +#include <QProgressBar> +#include <QPushButton> +#include <QTextEdit> +#include <QThread> + +/// Modal dialog that shows step-by-step Wine prefix setup progress. +/// +/// Displays each setup step with status icons, a live log, and +/// allows retrying individual failed steps or cleaning up on failure. +class PrefixSetupDialog : public QDialog +{ + Q_OBJECT + +public: + /// Create the dialog. Does NOT start the setup; call exec() which + /// auto-starts on show. + PrefixSetupDialog(const QString& prefixPath, + const QString& protonPath, + uint32_t appId, + QWidget* parent = nullptr); + ~PrefixSetupDialog() override; + + /// True if all steps succeeded. + bool succeeded() const { return m_allSucceeded; } + +protected: + void showEvent(QShowEvent* event) override; + +private slots: + void onStepStarted(int index); + void onStepFinished(int index, bool success, const QString& error); + void onLogMessage(const QString& text); + void onProgressChanged(float progress); + void onFinished(bool allSucceeded); + + void onCancel(); + void onRetryFailed(); + void onDeleteAndClose(); + void onClose(); + +private: + void buildUI(); + void populateStepList(); + void updateButtons(); + + // -- state ----------------------------------------------------------------- + QString m_prefixPath; + QString m_protonPath; + uint32_t m_appId; + bool m_allSucceeded = false; + bool m_running = false; + bool m_started = false; + + // -- worker ---------------------------------------------------------------- + PrefixSetupRunner* m_runner = nullptr; + QThread* m_workerThread = nullptr; + + // -- widgets --------------------------------------------------------------- + QListWidget* m_stepList = nullptr; + QTextEdit* m_logView = nullptr; + QProgressBar* m_progressBar = nullptr; + QLabel* m_statusLabel = nullptr; + QPushButton* m_cancelBtn = nullptr; + QPushButton* m_retryBtn = nullptr; + QPushButton* m_deleteBtn = nullptr; + QPushButton* m_closeBtn = nullptr; +}; + +#endif // PREFIXSETUPDIALOG_H diff --git a/src/src/prefixsetuprunner.cpp b/src/src/prefixsetuprunner.cpp new file mode 100644 index 0000000..bdabf18 --- /dev/null +++ b/src/src/prefixsetuprunner.cpp @@ -0,0 +1,1172 @@ +#include "prefixsetuprunner.h" + +#include "fluorinepaths.h" + +#include <nak_ffi.h> + +#include <QCoreApplication> +#include <QDir> +#include <QEventLoop> +#include <QFile> +#include <QFileInfo> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QProcess> +#include <QProcessEnvironment> +#include <QRegularExpression> +#include <QStandardPaths> +#include <QTemporaryFile> +#include <QThread> +#include <QUrl> + +#include <log.h> + +// ============================================================================ +// Constants +// ============================================================================ + +static const char* WINETRICKS_URL = + "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"; + +static const char* CABEXTRACT_URL = + "https://github.com/SulfurNitride/NaK/releases/download/Cabextract/" + "cabextract-linux-x86_64.zip"; + +static const char* SEVENZIP_URL = + "https://github.com/ip7z/7zip/releases/download/26.00/7z2600-linux-x86.tar.xz"; + +static const char* DOTNET9_SDK_URL = + "https://builds.dotnet.microsoft.com/dotnet/Sdk/9.0.310/" + "dotnet-sdk-9.0.310-win-x64.exe"; + +static const char* DOTNET_DESKTOP10_URL = + "https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.2/" + "windowsdesktop-runtime-10.0.2-win-x64.exe"; + +static const QStringList WINETRICKS_VERBS = { + "vcrun2022", "dotnet6", "dotnet7", "dotnet8", + "dotnetdesktop6", "d3dcompiler_47", "d3dcompiler_43", + "d3dx9", "d3dx11_43", "xact", "xact_x64", +}; + + +// Allowed drive letters to keep in the prefix. +static const QStringList ALLOWED_DRIVES = {"c:", "z:"}; + +/// Filter out Wine/winetricks noise from process output. +/// Returns true if the line should be shown to the user. +static bool shouldShowLogLine(const QString& line) +{ + // Wine fixme/trace/err spam (e.g. "00d4:fixme:wineusb:query_id ...") + static const QRegularExpression wineDebugRe( + R"(^[0-9a-f]{4}:(?:fixme|trace|err):.*)"); + if (wineDebugRe.match(line).hasMatch()) + return false; + + // Wine diagnostic messages + if (line.contains("ntsync:") || line.contains("winediag:") || + line.contains("pressure-vessel-wrap[")) + return false; + + // Winetricks execution spam + if (line.startsWith("Executing ") || line.startsWith("Using winetricks ") || + line.startsWith("Executing w_do_call ")) + return false; + + // Blank lines and separator lines + if (line.isEmpty() || line == "------------------------------------------------------") + return false; + + return true; +} + +/// Emit process output, splitting by line and filtering noise. +static void emitFilteredOutput(PrefixSetupRunner* self, const QByteArray& data) +{ + const QList<QByteArray> lines = data.split('\n'); + for (const QByteArray& raw : lines) { + const QString line = QString::fromUtf8(raw).trimmed(); + if (shouldShowLogLine(line)) + emit self->logMessage(line); + } +} + +// Wine registry settings (.reg file content). +static const char* WINE_SETTINGS_REG = R"(Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Wine\DllOverrides] +"dwrite.dll"="native,builtin" +"dwrite"="native,builtin" +"winmm.dll"="native,builtin" +"winmm"="native,builtin" +"version.dll"="native,builtin" +"version"="native,builtin" +"ArchiveXL.dll"="native,builtin" +"ArchiveXL"="native,builtin" +"Codeware.dll"="native,builtin" +"Codeware"="native,builtin" +"TweakXL.dll"="native,builtin" +"TweakXL"="native,builtin" +"input_loader.dll"="native,builtin" +"input_loader"="native,builtin" +"RED4ext.dll"="native,builtin" +"RED4ext"="native,builtin" +"mod_settings.dll"="native,builtin" +"mod_settings"="native,builtin" +"scc_lib.dll"="native,builtin" +"scc_lib"="native,builtin" +"dxgi.dll"="native,builtin" +"dxgi"="native,builtin" +"dbghelp.dll"="native,builtin" +"dbghelp"="native,builtin" +"d3d12.dll"="native,builtin" +"d3d12"="native,builtin" +"wininet.dll"="native,builtin" +"wininet"="native,builtin" +"winhttp.dll"="native,builtin" +"winhttp"="native,builtin" +"dinput.dll"="native,builtin" +"dinput8"="native,builtin" +"dinput8.dll"="native,builtin" + +[HKEY_CURRENT_USER\Software\Wine] +"ShowDotFiles"="Y" + +[HKEY_CURRENT_USER\Control Panel\Desktop] +"FontSmoothing"="2" +"FontSmoothingGamma"=dword:00000578 +"FontSmoothingOrientation"=dword:00000001 +"FontSmoothingType"=dword:00000002 + +[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers] +@="~ HIGHDPIAWARE" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Pandora Behaviour Engine+.exe\X11 Driver] +"Decorated"="N" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Vortex.exe\X11 Driver] +"Decorated"="N" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SSEEdit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SSEEdit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO4Edit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO4Edit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\TES4Edit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\TES4Edit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xEdit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SF1Edit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FNVEdit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FNVEdit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xFOEdit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xFOEdit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xSFEEdit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xSFEEdit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xTESEdit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xTESEdit64.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO3Edit.exe] +"Version"="winxp" + +[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO3Edit64.exe] +"Version"="winxp" + +; Native file browser integration +[HKEY_CLASSES_ROOT\Folder\shell\explore\command] +@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" + +[HKEY_CLASSES_ROOT\Directory\shell\explore\command] +@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" + +[HKEY_CLASSES_ROOT\Folder\shell\open\command] +@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" + +[HKEY_CLASSES_ROOT\Directory\shell\open\command] +@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" + +; Native text editor integration +[HKEY_CLASSES_ROOT\txtfile\shell\open\command] +@="C:\\windows\\system32\\winebrowser.exe \"%1\"" + +[HKEY_CLASSES_ROOT\inifile\shell\open\command] +@="C:\\windows\\system32\\winebrowser.exe \"%1\"" + +[HKEY_CLASSES_ROOT\.txt] +@="txtfile" + +[HKEY_CLASSES_ROOT\.ini] +@="inifile" + +[HKEY_CLASSES_ROOT\.cfg] +@="txtfile" + +[HKEY_CLASSES_ROOT\.log] +@="txtfile" + +[HKEY_CLASSES_ROOT\.xml] +@="txtfile" + +[HKEY_CLASSES_ROOT\.json] +@="txtfile" + +[HKEY_CLASSES_ROOT\.yml] +@="txtfile" + +[HKEY_CLASSES_ROOT\.yaml] +@="txtfile" +)"; + +// ============================================================================ +// Construction +// ============================================================================ + +PrefixSetupRunner::PrefixSetupRunner(const QString& prefixPath, + const QString& protonPath, + uint32_t appId, + QObject* parent) + : QObject(parent) + , m_prefixPath(prefixPath) + , m_protonPath(protonPath) + , m_appId(appId) +{ + m_wineBin = findWineBinary(); + m_wineserverBin = findWineserverBinary(); + m_slrRunScript = detectSLRRunScript(); + + buildStepList(); +} + +// ============================================================================ +// Step list construction +// ============================================================================ + +void PrefixSetupRunner::buildStepList() +{ + m_steps.clear(); + m_stepFunctions.clear(); + + auto addStep = [this](const QString& id, const QString& name, + std::function<bool()> fn) { + m_steps.append({id, name, SetupStep::Pending, {}}); + m_stepFunctions.append(std::move(fn)); + }; + + addStep("proton_init", "Initialize Wine Prefix", + [this] { return stepProtonInit(); }); + + addStep("drive_cleanup", "Clean Up Drive Letters", + [this] { return stepDriveCleanup(); }); + + for (const QString& verb : WINETRICKS_VERBS) { + addStep("wt_" + verb, verb, + [this, verb] { return stepWinetricksVerb(verb); }); + } + + addStep("dotnet9_sdk", ".NET 9 SDK", + [this] { return stepDotNetInstall(DOTNET9_SDK_URL, "dotnet-sdk-9"); }); + + addStep("dotnet_desktop10", ".NET Desktop Runtime 10", + [this] { return stepDotNetInstall(DOTNET_DESKTOP10_URL, "dotnet-desktop-10"); }); + + addStep("game_detection", "Auto-Detect Games", + [this] { return stepGameDetection(); }); + + addStep("wine_registry", "Wine Registry Settings", + [this] { return stepWineRegistry(); }); + + addStep("win11_mode", "Windows 11 Mode", + [this] { return stepWin11Mode(); }); + + addStep("post_setup", "Post-Setup (symlinks, dxvk)", + [this] { return stepPostSetup(); }); +} + +// ============================================================================ +// Execution +// ============================================================================ + +void PrefixSetupRunner::start() +{ + m_cancelled.storeRelease(0); + + // Ensure tools are available before starting steps. + if (!ensureWinetricks() || !ensureCabextract()) { + emit finished(false); + return; + } + + const int total = m_steps.size(); + bool allOk = true; + + for (int i = 0; i < total; ++i) { + if (isCancelled()) { + emit logMessage("Cancelled by user."); + allOk = false; + break; + } + + if (m_steps[i].status == SetupStep::Succeeded) + continue; + + allOk = runStep(i) && allOk; + emit progressChanged(static_cast<float>(i + 1) / total); + } + + emit finished(allOk); +} + +void PrefixSetupRunner::retryFailed() +{ + m_cancelled.storeRelease(0); + + const int total = m_steps.size(); + bool allOk = true; + + for (int i = 0; i < total; ++i) { + if (isCancelled()) break; + + if (m_steps[i].status != SetupStep::Failed) + continue; + + allOk = runStep(i) && allOk; + } + + emit finished(allOk); +} + +void PrefixSetupRunner::retryStep(int index) +{ + if (index < 0 || index >= m_steps.size()) return; + m_cancelled.storeRelease(0); + + runStep(index); + + // Check if everything is now good. + bool allOk = true; + for (const auto& s : m_steps) { + if (s.status == SetupStep::Failed) { allOk = false; break; } + } + emit finished(allOk); +} + +bool PrefixSetupRunner::runStep(int index) +{ + m_steps[index].status = SetupStep::Running; + m_steps[index].errorMessage.clear(); + emit stepStarted(index); + + const bool ok = m_stepFunctions[index](); + + m_steps[index].status = ok ? SetupStep::Succeeded : SetupStep::Failed; + if (!ok && m_steps[index].errorMessage.isEmpty()) + m_steps[index].errorMessage = "Step failed (see log for details)"; + + emit stepFinished(index, ok, m_steps[index].errorMessage); + return ok; +} + +// ============================================================================ +// Process execution +// ============================================================================ + +QProcess* PrefixSetupRunner::buildWrappedProcess( + const QString& exe, + const QMap<QString, QString>& extraEnv) +{ + auto* proc = new QProcess(this); + + // Start from the system environment and clean AppImage vars. + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + + // Remove AppImage/Fluorine vars that can confuse Wine. + for (const char* var : {"QT_QPA_PLATFORM_PLUGIN_PATH", "MO2_PLUGINS_DIR", + "MO2_DLLS_DIR", "MO2_PYTHON_DIR", "MO2_BASE_DIR", + "APPIMAGE", "APPDIR", "OWD", "ARGV0", + "APPIMAGE_ORIGINAL_EXEC", "DESKTOPINTEGRATION"}) { + env.remove(var); + } + + // Restore pre-AppImage environment if available. + auto restoreOrStrip = [&env](const QString& var, const QString& origVar) { + if (env.contains(origVar)) { + const QString orig = env.value(origVar); + if (orig.isEmpty()) env.remove(var); + else env.insert(var, orig); + env.remove(origVar); + } + }; + restoreOrStrip("LD_LIBRARY_PATH", "FLUORINE_ORIG_LD_LIBRARY_PATH"); + restoreOrStrip("LD_PRELOAD", "FLUORINE_ORIG_LD_PRELOAD"); + restoreOrStrip("PATH", "FLUORINE_ORIG_PATH"); + restoreOrStrip("XDG_DATA_DIRS", "FLUORINE_ORIG_XDG_DATA_DIRS"); + restoreOrStrip("QT_PLUGIN_PATH", "FLUORINE_ORIG_QT_PLUGIN_PATH"); + + // Apply caller-provided env vars. + for (auto it = extraEnv.begin(); it != extraEnv.end(); ++it) { + env.insert(it.key(), it.value()); + } + + proc->setProcessEnvironment(env); + + // Wrap in SLR if available. + if (!m_slrRunScript.isEmpty()) { + QStringList slrArgs; + + // Expose the executable's parent directory. + const QString exeDir = QFileInfo(exe).absolutePath(); + if (!exeDir.isEmpty() && QDir(exeDir).exists()) + slrArgs << QStringLiteral("--filesystem=%1").arg(exeDir); + + // Expose the Wine prefix. + if (!m_prefixPath.isEmpty()) + slrArgs << QStringLiteral("--filesystem=%1").arg(m_prefixPath); + + // Expose Proton directory. + if (!m_protonPath.isEmpty()) + slrArgs << QStringLiteral("--filesystem=%1").arg(m_protonPath); + + // Expose fluorine bin dir (cabextract, winetricks). + const QString binDir = fluorineBinDir(); + if (QDir(binDir).exists()) + slrArgs << QStringLiteral("--filesystem=%1").arg(binDir); + + // Expose cache dir. + const QString cacheDir = fluorineCacheDir(); + if (QDir(cacheDir).exists()) + slrArgs << QStringLiteral("--filesystem=%1").arg(cacheDir); + + slrArgs << "--" << exe; + + proc->setProgram(m_slrRunScript); + proc->setArguments(slrArgs); + } else { + proc->setProgram(exe); + } + + return proc; +} + +int PrefixSetupRunner::runProcess(const QString& exe, + const QStringList& args, + const QMap<QString, QString>& extraEnv, + int timeoutMs) +{ + QProcess* proc = buildWrappedProcess(exe, extraEnv); + + // Append the actual arguments after the SLR wrapper arguments. + QStringList fullArgs = proc->arguments(); + fullArgs.append(args); + proc->setArguments(fullArgs); + + proc->setProcessChannelMode(QProcess::MergedChannels); + proc->start(); + + // Poll for output and cancellation. + while (proc->state() != QProcess::NotRunning) { + proc->waitForReadyRead(250); + + if (proc->canReadLine() || proc->bytesAvailable() > 0) { + const QByteArray data = proc->readAll(); + if (!data.isEmpty()) + emitFilteredOutput(this, data); + } + + if (isCancelled()) { + proc->kill(); + proc->waitForFinished(5000); + proc->deleteLater(); + return -1; + } + } + + const QByteArray remaining = proc->readAll(); + if (!remaining.isEmpty()) + emitFilteredOutput(this, remaining); + + const int exitCode = proc->exitCode(); + proc->deleteLater(); + return exitCode; +} + +int PrefixSetupRunner::runHostProcess(const QString& exe, + const QStringList& args, + int timeoutMs) +{ + QProcess proc; + proc.setProgram(exe); + proc.setArguments(args); + proc.setProcessChannelMode(QProcess::MergedChannels); + + // Clean AppImage/Fluorine env so host tools find their system libraries. + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + auto restoreOrStrip = [&env](const QString& var, const QString& origVar) { + if (env.contains(origVar)) { + const QString orig = env.value(origVar); + if (orig.isEmpty()) env.remove(var); + else env.insert(var, orig); + env.remove(origVar); + } + }; + restoreOrStrip("LD_LIBRARY_PATH", "FLUORINE_ORIG_LD_LIBRARY_PATH"); + restoreOrStrip("LD_PRELOAD", "FLUORINE_ORIG_LD_PRELOAD"); + restoreOrStrip("PATH", "FLUORINE_ORIG_PATH"); + proc.setProcessEnvironment(env); + + proc.start(); + + while (proc.state() != QProcess::NotRunning) { + proc.waitForReadyRead(250); + if (proc.bytesAvailable() > 0) + emitFilteredOutput(this, proc.readAll()); + if (isCancelled()) { + proc.kill(); + proc.waitForFinished(5000); + return -1; + } + } + + const QByteArray remaining = proc.readAll(); + if (!remaining.isEmpty()) + emitFilteredOutput(this, remaining); + + return proc.exitCode(); +} + +int PrefixSetupRunner::runHostProcessWithEnv(const QString& exe, + const QStringList& args, + const QMap<QString, QString>& extraEnv, + int timeoutMs) +{ + QProcess proc; + proc.setProgram(exe); + proc.setArguments(args); + proc.setProcessChannelMode(QProcess::MergedChannels); + + // Start from cleaned host environment, then apply caller's env vars. + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + auto restoreVar = [&env](const QString& var, const QString& origVar) { + if (env.contains(origVar)) { + const QString orig = env.value(origVar); + if (orig.isEmpty()) env.remove(var); + else env.insert(var, orig); + env.remove(origVar); + } + }; + restoreVar("LD_LIBRARY_PATH", "FLUORINE_ORIG_LD_LIBRARY_PATH"); + restoreVar("LD_PRELOAD", "FLUORINE_ORIG_LD_PRELOAD"); + + for (auto it = extraEnv.begin(); it != extraEnv.end(); ++it) + env.insert(it.key(), it.value()); + + proc.setProcessEnvironment(env); + proc.start(); + + while (proc.state() != QProcess::NotRunning) { + proc.waitForReadyRead(250); + if (proc.bytesAvailable() > 0) + emitFilteredOutput(this, proc.readAll()); + if (isCancelled()) { + proc.kill(); + proc.waitForFinished(5000); + return -1; + } + } + + const QByteArray remaining = proc.readAll(); + if (!remaining.isEmpty()) + emitFilteredOutput(this, remaining); + + return proc.exitCode(); +} + +// ============================================================================ +// Step implementations +// ============================================================================ + +bool PrefixSetupRunner::stepProtonInit() +{ + const QString protonScript = findProtonScript(); + if (protonScript.isEmpty()) { + m_steps.last().errorMessage = "Proton wrapper script not found"; + return false; + } + + const QString steamPath = detectSteamPath(); + + // The compatdata path is the PARENT of the pfx directory. + const QString compatDataPath = QDir(m_prefixPath).filePath(".."); + const QString cleanCompat = QDir::cleanPath(compatDataPath); + + QMap<QString, QString> env; + env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = steamPath; + env["STEAM_COMPAT_DATA_PATH"] = cleanCompat; + env["SteamAppId"] = QString::number(m_appId); + env["SteamGameId"] = QString::number(m_appId); + env["DISPLAY"] = ""; + env["WAYLAND_DISPLAY"] = ""; + env["WINEDEBUG"] = "-all"; + env["WINEDLLOVERRIDES"] = "msdia80.dll=n;conhost.exe=d;cmd.exe=d"; + + emit logMessage("Initializing Wine prefix with Proton..."); + + const int rc = runProcess(protonScript, + {"run", "wineboot", "-u"}, + env); + if (rc != 0) { + m_steps.last().errorMessage = + QStringLiteral("proton wineboot failed (exit code %1)").arg(rc); + return false; + } + + // Wait briefly for files to settle. + QThread::sleep(2); + + if (!QDir(m_prefixPath).exists()) { + m_steps.last().errorMessage = "Prefix directory not created after wineboot"; + return false; + } + + return true; +} + +bool PrefixSetupRunner::stepDriveCleanup() +{ + const QString dosdevices = m_prefixPath + "/dosdevices"; + if (!QDir(dosdevices).exists()) { + emit logMessage("dosdevices not found, skipping drive cleanup"); + return true; + } + + emit logMessage("Removing unwanted drive letters..."); + + QStringList removed; + const QDir dir(dosdevices); + for (const QString& entry : dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries)) { + const QString lower = entry.toLower(); + if (lower.length() != 2 || !lower.endsWith(':')) + continue; + if (!lower.at(0).isLetter()) + continue; + if (ALLOWED_DRIVES.contains(lower)) + continue; + + QFile::remove(dir.filePath(entry)); + removed << entry.toUpper(); + } + + if (!removed.isEmpty()) { + emit logMessage(QStringLiteral("Removed drive symlinks: %1").arg(removed.join(", "))); + + // Clean registry entries for removed drives. + const QString tmpDir = fluorineTmpDir(); + QDir().mkpath(tmpDir); + + QString regContent = "Windows Registry Editor Version 5.00\n\n"; + for (const QString& drive : removed) { + regContent += QStringLiteral( + "[HKEY_LOCAL_MACHINE\\Software\\Wine\\Drives]\n\"%1\"=-\n\n").arg(drive); + } + + const QString regFile = tmpDir + "/drive_cleanup.reg"; + QFile f(regFile); + if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { + f.write(regContent.toUtf8()); + f.close(); + + QMap<QString, QString> env = baseWineEnv(); + env["WINEDLLOVERRIDES"] = "mshtml=d"; + env["PROTON_USE_XALIA"] = "0"; + + runProcess(m_wineBin, {"regedit", regFile}, env); + QFile::remove(regFile); + } + } + + return true; +} + +bool PrefixSetupRunner::stepWinetricksVerb(const QString& verb) +{ + emit logMessage(QStringLiteral("Installing %1 via winetricks...").arg(verb)); + + // Winetricks is a host bash script — do NOT wrap in SLR. + // It calls wine internally via $WINE/$WINESERVER env vars. + // Running it on the host ensures cabextract and other tools are visible. + const QString binDir = fluorineBinDir(); + const QString cacheDir = fluorineCacheDir(); + QDir().mkpath(cacheDir); + + const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment(); + const QString origPath = sysEnv.contains("FLUORINE_ORIG_PATH") + ? sysEnv.value("FLUORINE_ORIG_PATH") + : sysEnv.value("PATH"); + + QMap<QString, QString> env; + env["PATH"] = binDir + ":" + origPath; + env["WINE"] = m_wineBin; + env["WINESERVER"] = m_wineserverBin; + env["WINEPREFIX"] = m_prefixPath; + env["WINETRICKS_CACHE"] = cacheDir; + + const int rc = runHostProcessWithEnv(m_winetricksPath, {"-q", verb}, env); + if (rc != 0) { + m_steps.last().errorMessage = + QStringLiteral("winetricks %1 failed (exit code %2)").arg(verb).arg(rc); + return false; + } + return true; +} + +bool PrefixSetupRunner::stepDotNetInstall(const QString& url, const QString& name) +{ + const QString cacheDir = fluorineCacheDir(); + QDir().mkpath(cacheDir); + + const QString filename = QUrl(url).fileName(); + const QString installerPath = cacheDir + "/" + filename; + + // Download if not cached. + if (!QFileInfo::exists(installerPath)) { + emit logMessage(QStringLiteral("Downloading %1...").arg(name)); + + if (!downloadFile(url, installerPath)) { + m_steps.last().errorMessage = + QStringLiteral("Failed to download %1").arg(name); + return false; + } + } + + emit logMessage(QStringLiteral("Installing %1...").arg(name)); + + QMap<QString, QString> env = baseWineEnv(); + env["WINEDLLOVERRIDES"] = "mshtml=d"; + + const int rc = runProcess( + m_wineBin, + {installerPath, "/install", "/quiet", "/norestart"}, + env); + + if (rc != 0) { + m_steps.last().errorMessage = + QStringLiteral("%1 installer failed (exit code %2)").arg(name).arg(rc); + return false; + } + + return true; +} + +bool PrefixSetupRunner::stepGameDetection() +{ + emit logMessage("Auto-detecting installed games..."); + + NakGameList gameList = nak_detect_all_games(); + if (gameList.count == 0) { + nak_game_list_free(gameList); + emit logMessage("No games detected"); + return true; + } + + int applied = 0; + const QString tmpDir = fluorineTmpDir(); + QDir().mkpath(tmpDir); + + for (size_t i = 0; i < gameList.count; ++i) { + if (isCancelled()) break; + + const NakGame& game = gameList.games[i]; + if (!game.registry_path || !game.registry_value) + continue; + + const QString gameName = QString::fromUtf8(game.name); + const QString installPath = QString::fromUtf8(game.install_path); + const QString rPath = QString::fromUtf8(game.registry_path); + const QString rVal = QString::fromUtf8(game.registry_value); + + // Convert Linux path to Wine Z: drive path with escaped backslashes. + QString winePath = "Z:" + QString(installPath).replace('/', "\\\\"); + + const QString regContent = QStringLiteral( + "Windows Registry Editor Version 5.00\n\n" + "[HKEY_LOCAL_MACHINE\\%1]\n\"%2\"=\"%3\"\n\n" + "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\%4]\n\"%5\"=\"%6\"\n") + .arg(rPath, rVal, winePath, + rPath.mid(rPath.indexOf('\\') + 1), + rVal, winePath); + + const QString regFile = tmpDir + QStringLiteral("/game_reg_%1.reg").arg(i); + QFile f(regFile); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) + continue; + f.write(regContent.toUtf8()); + f.close(); + + QMap<QString, QString> env = baseWineEnv(); + env["WINEDLLOVERRIDES"] = "mshtml=d"; + env["PROTON_USE_XALIA"] = "0"; + + emit logMessage(QStringLiteral("Applying registry for %1...").arg(gameName)); + const int rc = runProcess(m_wineBin, {"regedit", regFile}, env); + QFile::remove(regFile); + + if (rc == 0) { + ++applied; + emit logMessage(QStringLiteral(" -> %1").arg(installPath)); + } + } + + nak_game_list_free(gameList); + emit logMessage(QStringLiteral("Configured %1 game(s) in registry").arg(applied)); + return true; +} + +bool PrefixSetupRunner::stepWineRegistry() +{ + emit logMessage("Applying Wine registry settings..."); + + const QString tmpDir = fluorineTmpDir(); + QDir().mkpath(tmpDir); + + const QString regFile = tmpDir + "/wine_settings.reg"; + QFile f(regFile); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + m_steps.last().errorMessage = "Failed to write registry file"; + return false; + } + f.write(WINE_SETTINGS_REG); + f.close(); + + QMap<QString, QString> env = baseWineEnv(); + env["WINEDLLOVERRIDES"] = "mshtml=d"; + env["PROTON_USE_XALIA"] = "0"; + + const int rc = runProcess(m_wineBin, {"regedit", regFile}, env); + QFile::remove(regFile); + + if (rc != 0) { + m_steps.last().errorMessage = + QStringLiteral("wine regedit failed (exit code %1)").arg(rc); + return false; + } + + emit logMessage("Registry settings applied successfully"); + return true; +} + +bool PrefixSetupRunner::stepWin11Mode() +{ + emit logMessage("Setting Windows 11 mode..."); + + const QString binDir = fluorineBinDir(); + const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment(); + const QString origPath = sysEnv.contains("FLUORINE_ORIG_PATH") + ? sysEnv.value("FLUORINE_ORIG_PATH") + : sysEnv.value("PATH"); + + QMap<QString, QString> env; + env["WINE"] = m_wineBin; + env["WINESERVER"] = m_wineserverBin; + env["WINEPREFIX"] = m_prefixPath; + env["PATH"] = binDir + ":" + origPath; + + const int rc = runHostProcessWithEnv(m_winetricksPath, {"-q", "win11"}, env); + if (rc != 0) { + m_steps.last().errorMessage = + QStringLiteral("winetricks win11 failed (exit code %1)").arg(rc); + return false; + } + + return true; +} + +bool PrefixSetupRunner::stepPostSetup() +{ + emit logMessage("Running post-setup tasks..."); + + // Ensure AppData temp directory exists. + const QByteArray prefixUtf8 = m_prefixPath.toUtf8(); + nak_ensure_temp_directory(prefixUtf8.constData()); + + // Create game symlinks. + nak_create_game_symlinks_auto(prefixUtf8.constData()); + + // Ensure DXVK config. + if (char* err = nak_ensure_dxvk_conf(); err != nullptr) { + emit logMessage(QStringLiteral("Warning: dxvk.conf: %1").arg(QString::fromUtf8(err))); + nak_string_free(err); + // Non-fatal. + } + + emit logMessage("Post-setup complete"); + return true; +} + +// ============================================================================ +// Tool management +// ============================================================================ + +static void setExecPermissions(const QString& path) +{ + QFile::setPermissions(path, + QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | + QFileDevice::ReadGroup | QFileDevice::ExeGroup | + QFileDevice::ReadOther | QFileDevice::ExeOther); +} + +bool PrefixSetupRunner::downloadFile(const QString& url, const QString& destPath) +{ + // Use Qt networking — no dependency on host curl. + QNetworkAccessManager nam; + QNetworkRequest request{QUrl(url)}; + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + request.setHeader(QNetworkRequest::UserAgentHeader, "Fluorine-Manager"); + + QNetworkReply* reply = nam.get(request); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + emit logMessage(QStringLiteral("Download failed: %1").arg(reply->errorString())); + reply->deleteLater(); + return false; + } + + QFile file(destPath); + if (!file.open(QIODevice::WriteOnly)) { + emit logMessage(QStringLiteral("Failed to write: %1").arg(destPath)); + reply->deleteLater(); + return false; + } + + file.write(reply->readAll()); + file.close(); + reply->deleteLater(); + return true; +} + +bool PrefixSetupRunner::ensure7zz() +{ + const QString binDir = fluorineBinDir(); + m_7zzPath = binDir + "/7zz"; + + if (QFileInfo::exists(m_7zzPath)) + return true; + + emit logMessage("Downloading 7-Zip..."); + QDir().mkpath(binDir); + + const QString tarPath = binDir + "/7zz.tar.xz"; + + if (!downloadFile(SEVENZIP_URL, tarPath)) { + emit logMessage("ERROR: Failed to download 7-Zip"); + return false; + } + + // Extract 7zz binary from the tar.xz using host tar (always available). + const int rc = runHostProcess( + "tar", + {"xf", tarPath, "-C", binDir, "7zz"}); + + QFile::remove(tarPath); + + if (rc != 0 || !QFileInfo::exists(m_7zzPath)) { + emit logMessage("ERROR: Failed to extract 7-Zip"); + return false; + } + + setExecPermissions(m_7zzPath); + return true; +} + +bool PrefixSetupRunner::ensureWinetricks() +{ + const QString binDir = fluorineBinDir(); + QDir().mkpath(binDir); + m_winetricksPath = binDir + "/winetricks"; + + emit logMessage("Checking for winetricks..."); + + if (!downloadFile(WINETRICKS_URL, m_winetricksPath)) { + if (!QFileInfo::exists(m_winetricksPath)) { + emit logMessage("ERROR: Failed to download winetricks"); + return false; + } + // Existing copy is fine. + } + + setExecPermissions(m_winetricksPath); + return true; +} + +bool PrefixSetupRunner::ensureCabextract() +{ + const QString binDir = fluorineBinDir(); + const QString cabextractPath = binDir + "/cabextract"; + + if (QFileInfo::exists(cabextractPath)) + return true; + + emit logMessage("Downloading cabextract..."); + QDir().mkpath(binDir); + + const QString zipPath = binDir + "/cabextract.zip"; + + if (!downloadFile(CABEXTRACT_URL, zipPath)) { + emit logMessage("ERROR: Failed to download cabextract"); + return false; + } + + // Extract using our downloaded 7zz (no host unzip dependency). + if (!ensure7zz()) { + QFile::remove(zipPath); + return false; + } + + const int rc = runHostProcess( + m_7zzPath, + {"x", zipPath, "-o" + binDir, "-y"}); + + QFile::remove(zipPath); + + if (rc != 0 || !QFileInfo::exists(cabextractPath)) { + emit logMessage("ERROR: Failed to extract cabextract"); + return false; + } + + setExecPermissions(cabextractPath); + return true; +} + +// ============================================================================ +// Wine environment helpers +// ============================================================================ + +QString PrefixSetupRunner::findWineBinary() const +{ + for (const char* subdir : {"files/bin", "dist/bin"}) { + const QString candidate = QDir(m_protonPath).filePath( + QString::fromLatin1(subdir) + "/wine"); + if (QFileInfo::exists(candidate)) + return candidate; + } + return {}; +} + +QString PrefixSetupRunner::findWineserverBinary() const +{ + for (const char* subdir : {"files/bin", "dist/bin"}) { + const QString candidate = QDir(m_protonPath).filePath( + QString::fromLatin1(subdir) + "/wineserver"); + if (QFileInfo::exists(candidate)) + return candidate; + } + return {}; +} + +QString PrefixSetupRunner::findProtonScript() const +{ + const QString script = QDir(m_protonPath).filePath("proton"); + return QFileInfo::exists(script) ? script : QString(); +} + +QString PrefixSetupRunner::detectSteamPath() const +{ + // Use NaK FFI first. + if (char* path = nak_find_steam_path(); path != nullptr) { + QString result = QString::fromUtf8(path); + nak_string_free(path); + if (!result.isEmpty()) + return result; + } + + // Fallback. + const QString home = QDir::homePath(); + const QStringList candidates = { + home + "/.local/share/Steam", + home + "/.steam/steam", + home + "/.steam/root", + }; + for (const QString& p : candidates) { + if (QFileInfo::exists(p)) + return p; + } + return {}; +} + +QString PrefixSetupRunner::detectSLRRunScript() const +{ + // Check NaK-downloaded SLR first. + const QString nakSlr = fluorineDataDir() + "/steamrt/SteamLinuxRuntime_sniper/run"; + if (QFileInfo::exists(nakSlr)) + return nakSlr; + + const QString steamPath = detectSteamPath(); + + const QStringList candidates = { + steamPath + "/steamapps/common/SteamLinuxRuntime_sniper/run", + QDir::homePath() + "/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/run", + "/usr/lib/pressure-vessel/wrap", + }; + + for (const QString& p : candidates) { + if (!p.isEmpty() && QFileInfo::exists(p)) + return p; + } + return {}; +} + +QString PrefixSetupRunner::fluorineBinDir() const +{ + return fluorineDataDir() + "/bin"; +} + +QString PrefixSetupRunner::fluorineCacheDir() const +{ + return fluorineDataDir() + "/cache"; +} + +QString PrefixSetupRunner::fluorineTmpDir() const +{ + return fluorineDataDir() + "/tmp"; +} + +QMap<QString, QString> PrefixSetupRunner::baseWineEnv() const +{ + QMap<QString, QString> env; + env["WINEPREFIX"] = m_prefixPath; + env["WINE"] = m_wineBin; + env["WINESERVER"] = m_wineserverBin; + return env; +} diff --git a/src/src/prefixsetuprunner.h b/src/src/prefixsetuprunner.h new file mode 100644 index 0000000..ed43d13 --- /dev/null +++ b/src/src/prefixsetuprunner.h @@ -0,0 +1,134 @@ +#ifndef PREFIXSETUPRUNNER_H +#define PREFIXSETUPRUNNER_H + +#include <QAtomicInt> +#include <QObject> +#include <QProcess> +#include <QString> +#include <QVector> + +#include <functional> + +/// Represents a single step in the prefix setup process. +struct SetupStep { + enum Status { Pending, Running, Succeeded, Failed, Skipped }; + + QString id; ///< Machine-readable identifier + QString displayName; ///< Human-readable name for the UI + Status status = Pending; + QString errorMessage; +}; + +/// Runs the Wine prefix setup as a sequence of discrete, retryable steps. +/// +/// Lives on a worker thread. Communicates with the UI via signals. +/// Each step spawns a QProcess wrapped in Steam Linux Runtime if available. +class PrefixSetupRunner : public QObject +{ + Q_OBJECT + +public: + explicit PrefixSetupRunner(const QString& prefixPath, + const QString& protonPath, + uint32_t appId, + QObject* parent = nullptr); + + /// Read-only access to the step list for the UI. + const QVector<SetupStep>& steps() const { return m_steps; } + + /// Request cancellation (thread-safe). + void cancel() { m_cancelled.storeRelease(1); } + +signals: + void stepStarted(int index); + void stepFinished(int index, bool success, const QString& error); + void logMessage(const QString& text); + void progressChanged(float progress); ///< 0.0 – 1.0 + void finished(bool allSucceeded); + +public slots: + /// Run all pending steps from the beginning. + void start(); + + /// Re-run only the steps that previously failed. + void retryFailed(); + + /// Re-run a single step by index. + void retryStep(int index); + +private: + // -- helpers --------------------------------------------------------------- + void buildStepList(); + bool runStep(int index); + bool isCancelled() const { return m_cancelled.loadAcquire() != 0; } + + /// Run an external process with SLR wrapping and log its output. + /// Returns exit code (0 = success). + int runProcess(const QString& exe, + const QStringList& args, + const QMap<QString, QString>& extraEnv, + int timeoutMs = -1); + + /// Run a plain host command (NO SLR wrapping). + /// Used for host utilities like curl, unzip that must not run in the container. + int runHostProcess(const QString& exe, + const QStringList& args, + int timeoutMs = -1); + + /// Run a host command with extra env vars (NO SLR wrapping). + /// Used for winetricks which is a host script that calls wine internally. + int runHostProcessWithEnv(const QString& exe, + const QStringList& args, + const QMap<QString, QString>& extraEnv, + int timeoutMs = -1); + + /// Build a QProcess configured with SLR wrapping + cleaned environment. + QProcess* buildWrappedProcess(const QString& exe, + const QMap<QString, QString>& extraEnv); + + // -- step implementations -------------------------------------------------- + bool stepProtonInit(); + bool stepDriveCleanup(); + bool stepWinetricksVerb(const QString& verb); + bool stepDotNetInstall(const QString& url, const QString& name); + bool stepGameDetection(); + bool stepWineRegistry(); + bool stepWin11Mode(); + bool stepPostSetup(); + + // -- tool management ------------------------------------------------------- + bool downloadFile(const QString& url, const QString& destPath); + bool ensure7zz(); + bool ensureWinetricks(); + bool ensureCabextract(); + + // -- Wine environment helpers ---------------------------------------------- + QString findWineBinary() const; + QString findWineserverBinary() const; + QString findProtonScript() const; + QString detectSteamPath() const; + QString detectSLRRunScript() const; + QString fluorineBinDir() const; + QString fluorineCacheDir() const; + QString fluorineTmpDir() const; + QMap<QString, QString> baseWineEnv() const; + + // -- state ----------------------------------------------------------------- + QString m_prefixPath; + QString m_protonPath; + uint32_t m_appId; + QAtomicInt m_cancelled{0}; + + QString m_wineBin; + QString m_wineserverBin; + QString m_slrRunScript; + QString m_winetricksPath; + QString m_7zzPath; + + QVector<SetupStep> m_steps; + + // Step execution functions indexed parallel to m_steps. + QVector<std::function<bool()>> m_stepFunctions; +}; + +#endif // PREFIXSETUPRUNNER_H diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index 5efc7c6..1875f31 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -2,6 +2,7 @@ #include "fluorineconfig.h" #include "fluorinepaths.h" +#include "prefixsetupdialog.h" #include "ui_settingsdialog.h" #include <QtConcurrent/QtConcurrentRun> @@ -233,13 +234,7 @@ void ProtonSettingsTab::onCreatePrefix() return; } - setBusy(true); - ui->protonStatusLabel->setText(tr("Creating prefix...")); - ui->nakInstallLog->clear(); - ui->nakInstallLog->setVisible(true); - ui->toggleInstallLog->setChecked(true); - - startInstallTask(0, pfxPath, protonName, protonPath); + runPrefixSetupDialog(0, pfxPath, protonName, protonPath); } void ProtonSettingsTab::onDeletePrefix() @@ -281,14 +276,8 @@ void ProtonSettingsTab::onRecreatePrefix() return; } - setBusy(true); - ui->protonStatusLabel->setText(tr("Recreating prefix...")); - ui->nakInstallLog->clear(); - ui->nakInstallLog->setVisible(true); - ui->toggleInstallLog->setChecked(true); - - startInstallTask(cfg->app_id, cfg->prefix_path, cfg->proton_name, - cfg->proton_path); + runPrefixSetupDialog(cfg->app_id, cfg->prefix_path, cfg->proton_name, + cfg->proton_path); } void ProtonSettingsTab::onOpenPrefixFolder() @@ -665,100 +654,33 @@ void ProtonSettingsTab::showGameRegistryDialog() })); } -void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPath, - const QString& protonName, - const QString& protonPath) +void ProtonSettingsTab::runPrefixSetupDialog(uint32_t appId, + const QString& prefixPath, + const QString& protonName, + const QString& protonPath) { - m_pendingAppId = appId; - m_pendingPrefixPath = prefixPath; - m_pendingProtonName = protonName; - m_pendingProtonPath = protonPath; - - ui->protonProgressBar->setValue(0); - - g_activeInstallTab.store(this); - - m_installWatcher.setFuture(QtConcurrent::run([ - appId, - prefixPath, - protonName, - protonPath]() -> InstallResult { - const QByteArray prefixPathUtf8 = prefixPath.toUtf8(); - const QByteArray protonNameUtf8 = protonName.toUtf8(); - const QByteArray protonPathUtf8 = protonPath.toUtf8(); - - // Set WINEPREFIX so NAK (and its child processes like winetricks) always - // target the correct prefix during Proton init. - qputenv("WINEPREFIX", prefixPathUtf8); - - // Point WINE/WINESERVER at Proton's binaries so winetricks and regedit - // use Proton's wine instead of falling back to system wine. - QByteArray protonWineUtf8; - QByteArray protonWineserverUtf8; - for (const char* subdir : {"files/bin", "dist/bin"}) { - const QString candidate = QDir(protonPath).filePath( - QString::fromLatin1(subdir) + "/wine"); - if (QFileInfo::exists(candidate)) { - protonWineUtf8 = candidate.toUtf8(); - protonWineserverUtf8 = - QDir(protonPath) - .filePath(QString::fromLatin1(subdir) + "/wineserver") - .toUtf8(); - break; - } - } - if (!protonWineUtf8.isEmpty()) { - qputenv("WINE", protonWineUtf8); - qputenv("WINESERVER", protonWineserverUtf8); - } - - const auto restoreNakEnv = qScopeGuard([protonWineUtf8] { - qunsetenv("WINEPREFIX"); - if (!protonWineUtf8.isEmpty()) { - qunsetenv("WINE"); - qunsetenv("WINESERVER"); - } - }); + PrefixSetupDialog dialog(prefixPath, protonPath, appId, parentWidget()); + const int result = dialog.exec(); - int cancelFlag = 0; - char* error = nak_install_all_dependencies( - prefixPathUtf8.constData(), protonNameUtf8.constData(), - protonPathUtf8.constData(), &ProtonSettingsTab::statusCallback, - &ProtonSettingsTab::logCallback, &ProtonSettingsTab::progressCallback, - &cancelFlag, appId); + if (result == QDialog::Accepted && dialog.succeeded()) { + // All steps succeeded — save config to mark prefix as complete. + FluorineConfig cfg; + cfg.app_id = appId; + cfg.prefix_path = prefixPath; + cfg.proton_name = protonName; + cfg.proton_path = protonPath; + cfg.created = QDateTime::currentDateTime().toString(Qt::ISODate); - InstallResult r; - if (error != nullptr) { - r.error = QString::fromUtf8(error); - nak_string_free(error); + if (!cfg.save()) { + ui->protonStatusLabel->setText(tr("Error saving Fluorine config")); + } else { + ui->protonStatusLabel->setText(tr("Prefix Active")); } + } else { + ui->protonStatusLabel->setText(tr("Prefix setup incomplete")); + } - return r; - })); -} - -void ProtonSettingsTab::enqueueStatus(const QString& message) -{ - QMetaObject::invokeMethod(this, - [this, message] { - if (m_busy) { - ui->protonStatusLabel->setText(message); - } - }, - Qt::QueuedConnection); -} - -void ProtonSettingsTab::enqueueProgress(float progress) -{ - QMetaObject::invokeMethod(this, - [this, progress] { - if (m_busy) { - const int clamped = - qBound(0, static_cast<int>(progress * 100.0f), 100); - ui->protonProgressBar->setValue(clamped); - } - }, - Qt::QueuedConnection); + refreshState(); } void ProtonSettingsTab::appendInstallLog(const QString& message) @@ -766,22 +688,6 @@ void ProtonSettingsTab::appendInstallLog(const QString& message) ui->nakInstallLog->append(message); } -void ProtonSettingsTab::statusCallback(const char* message) -{ - if (auto* tab = g_activeInstallTab.load(); tab != nullptr) { - const QString msg = QString::fromUtf8(message ? message : ""); - tab->enqueueStatus(msg); - - if (!msg.isEmpty()) { - QMetaObject::invokeMethod(tab, - [tab, msg] { - tab->appendInstallLog(msg); - }, - Qt::QueuedConnection); - } - } -} - void ProtonSettingsTab::logCallback(const char* message) { if (message && *message) { @@ -798,13 +704,6 @@ void ProtonSettingsTab::logCallback(const char* message) } } -void ProtonSettingsTab::progressCallback(float progress) -{ - if (auto* tab = g_activeInstallTab.load(); tab != nullptr) { - tab->enqueueProgress(progress); - } -} - void ProtonSettingsTab::onInstallFinished() { g_activeInstallTab.store(nullptr); @@ -818,32 +717,6 @@ void ProtonSettingsTab::onInstallFinished() return; } - // Set up prefix directory structure (temp dir + game symlinks) - { - const QByteArray prefixPathUtf8 = m_pendingPrefixPath.toUtf8(); - nak_ensure_temp_directory(prefixPathUtf8.constData()); - nak_create_game_symlinks_auto(prefixPathUtf8.constData()); - } - - // Ensure DXVK config exists for game launches - if (char* dxvkErr = nak_ensure_dxvk_conf(); dxvkErr != nullptr) { - MOBase::log::warn("Failed to create dxvk.conf: {}", dxvkErr); - nak_string_free(dxvkErr); - } - - FluorineConfig cfg; - cfg.app_id = m_pendingAppId; - cfg.prefix_path = m_pendingPrefixPath; - cfg.proton_name = m_pendingProtonName; - cfg.proton_path = m_pendingProtonPath; - cfg.created = QDateTime::currentDateTime().toString(Qt::ISODate); - - if (!cfg.save()) { - ui->protonStatusLabel->setText(tr("Error saving Fluorine config")); - refreshState(); - return; - } - - ui->protonStatusLabel->setText(tr("Prefix Active")); + ui->protonStatusLabel->setText(tr("Done")); refreshState(); } diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h index d5f0188..db0770c 100644 --- a/src/src/settingsdialogproton.h +++ b/src/src/settingsdialogproton.h @@ -39,16 +39,12 @@ private: QString ensureWinetricks(); QString findProtonWine(const QString& protonPath); - void startInstallTask(uint32_t appId, const QString& prefixPath, - const QString& protonName, const QString& protonPath); + void runPrefixSetupDialog(uint32_t appId, const QString& prefixPath, + const QString& protonName, const QString& protonPath); - void enqueueStatus(const QString& message); - void enqueueProgress(float progress); void appendInstallLog(const QString& message); - static void statusCallback(const char* message); static void logCallback(const char* message); - static void progressCallback(float progress); private slots: void onInstallFinished(); |
