aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/src/CMakeLists.txt4
-rw-r--r--src/src/prefixsetupdialog.cpp306
-rw-r--r--src/src/prefixsetupdialog.h76
-rw-r--r--src/src/prefixsetuprunner.cpp1172
-rw-r--r--src/src/prefixsetuprunner.h134
-rw-r--r--src/src/settingsdialogproton.cpp181
-rw-r--r--src/src/settingsdialogproton.h8
7 files changed, 1721 insertions, 160 deletions
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();