From ce6b7aabe6a52367e882251015c2d263108ecd43 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Thu, 23 Apr 2026 19:24:00 -0500 Subject: Dedicated Updates tab, controller + ntsync fixes, titlebar polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Settings > Updates tab (src/src/settingsdialogupdates.{h,cpp}): current build info (version, channel, commit, timestamp), channel picker, startup-check toggle, and a "Check for updates now" button with live status. Legacy widgets on the General tab are now hidden. - Downgrade warning: suppress the "your version is lower than before" log when lastVersion is 2.x.y (upstream MO2 / pre-Fluorine schema) and currentVersion is 0.x.y — that's a schema switch, not a downgrade. - Titlebar: beta builds show "beta @ " instead of a static semver so each rolling build is identifiable at a glance. Stable still shows semver. Renamed to "Fluorine Manager" in the title. - Controllers: remove the four xinput* "native" overrides from the prefix registry seed. Games that ship their own xinput1_3.dll (Fallout 4, Skyrim, etc.) make Wine's builtin (SDL-backed) lose to a Windows stub that can't talk to /dev/input, so controllers went silent. Leaving xinput unset lets Wine builtin win. - ntsync fallback: detect a missing /dev/ntsync at launch and set PROTON_NO_NTSYNC=1 / WINENTSYNC=0 / WINE_DISABLE_FAST_SYNC=1 so pre-6.14 kernels stop dying with "Cannot open synchronization device: No such file or directory". Co-Authored-By: Claude Opus 4.7 (1M context) --- src/src/mainwindow.cpp | 52 +++++++++--- src/src/prefixsetuprunner.cpp | 5 +- src/src/protonlauncher.cpp | 27 ++++++ src/src/settingsdialog.cpp | 3 + src/src/settingsdialoggeneral.cpp | 23 ++--- src/src/settingsdialogupdates.cpp | 173 ++++++++++++++++++++++++++++++++++++++ src/src/settingsdialogupdates.h | 35 ++++++++ 7 files changed, 284 insertions(+), 34 deletions(-) create mode 100644 src/src/settingsdialogupdates.cpp create mode 100644 src/src/settingsdialogupdates.h (limited to 'src') diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 126f1cd..8c79454 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -20,6 +20,8 @@ along with Mod Organizer. If not, see . #include "mainwindow.h" #include "ui_mainwindow.h" +#include + #include "aboutdialog.h" #ifdef MO2_WEBENGINE #include "browserdialog.h" @@ -694,10 +696,23 @@ MainWindow::~MainWindow() void MainWindow::updateWindowTitle(const APIUserAccount& user) { //"\xe2\x80\x93" is an "em dash", a longer "-" +#if FLUORINE_IS_BETA_BUILD + // Beta builds mutate on every CI run, so a fixed semver is misleading — + // show the short commit hash instead so users can tell exactly which + // build they're on at a glance. + const QString commitShort = QStringLiteral(FLUORINE_BUILD_COMMIT); + const QString versionLabel = + commitShort.isEmpty() + ? QStringLiteral("beta") + : (QStringLiteral("beta @ ") + commitShort); +#else + const QString versionLabel = + m_OrganizerCore.getVersion().string(Version::FormatCondensed); +#endif + QString title = - QString("%1 \xe2\x80\x93 Mod Organizer v%2") - .arg(m_OrganizerCore.managedGame()->displayGameName(), - m_OrganizerCore.getVersion().string(Version::FormatCondensed)); + QString("%1 \xe2\x80\x93 Fluorine Manager %2") + .arg(m_OrganizerCore.managedGame()->displayGameName(), versionLabel); if (!user.name().isEmpty()) { const QString premium = (user.type() == APIUserAccountTypes::Premium ? "*" : ""); @@ -2286,15 +2301,28 @@ void MainWindow::processUpdates() } if (currentVersion < lastVersion) { - const auto text = - tr("Notice: Your current MO version (%1) is lower than the previously used one " - "(%2). " - "The GUI may not downgrade gracefully, so you may experience oddities. " - "However, there should be no serious issues.") - .arg(currentVersion.toString()) - .arg(lastVersion.toString()); - - log::warn("{}", text); + // Fluorine Manager versions its own 0.x.y releases separately from the + // embedded MO2 engine. Instances created under upstream MO2 (or an + // earlier Fluorine build that echoed the engine version) will have + // a stored 2.x.y — the "2 → 0" drop isn't a downgrade, it's a schema + // change. Detect that and suppress the warning. + const bool schemaTransition = + lastVersion.majorVersion() >= 2 && currentVersion.majorVersion() == 0; + if (!schemaTransition) { + const auto text = + tr("Notice: Your current Fluorine Manager version (%1) is lower than " + "the previously used one (%2). The GUI may not downgrade " + "gracefully, so you may experience oddities. However, there " + "should be no serious issues.") + .arg(currentVersion.toString()) + .arg(lastVersion.toString()); + + log::warn("{}", text); + } else { + log::debug( + "skipping downgrade warning: {} -> {} is MO2->Fluorine schema change", + lastVersion.toString(), currentVersion.toString()); + } } } diff --git a/src/src/prefixsetuprunner.cpp b/src/src/prefixsetuprunner.cpp index 8bca32e..f02f824 100644 --- a/src/src/prefixsetuprunner.cpp +++ b/src/src/prefixsetuprunner.cpp @@ -217,10 +217,7 @@ static const char* WINE_SETTINGS_REG = R"(Windows Registry Editor Version 5.00 "d3dx10_43"="native" "d3dx11_42"="native" "d3dx11_43"="native" -"xinput1_1"="native" -"xinput1_2"="native" -"xinput1_3"="native" -"xinput9_1_0"="native" +; xinput left as builtin on Linux — native breaks controllers (SDL path). "xaudio2_0"="native,builtin" "xaudio2_1"="native,builtin" "xaudio2_2"="native,builtin" diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index e0d0d44..d247965 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -603,6 +603,33 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const env.insert("DOTNET_ROOT", ""); env.insert("DOTNET_MULTILEVEL_LOOKUP", "0"); + // Detect ntsync availability: newer Proton/Wine builds prefer the + // in-kernel ntsync driver for synchronization primitives, but it's + // only present on kernels ≥ 6.14 (or with the out-of-tree module + // loaded). When it's missing, games emit "Cannot open synchronization + // device: No such file or directory" and fall back to an unusable + // state (see issue from user on 2026-04-23). Force the esync/fsync + // fallback if /dev/ntsync isn't usable. + if (!QFileInfo::exists(QStringLiteral("/dev/ntsync"))) { + if (!env.contains("PROTON_NO_NTSYNC")) { + env.insert("PROTON_NO_NTSYNC", "1"); + } + if (!env.contains("WINENTSYNC")) { + env.insert("WINENTSYNC", "0"); + } + if (!env.contains("WINE_DISABLE_FAST_SYNC")) { + env.insert("WINE_DISABLE_FAST_SYNC", "1"); + } + static bool warned = false; + if (!warned) { + MOBase::log::info( + "/dev/ntsync missing — disabling Proton fast-sync " + "(set PROTON_NO_NTSYNC=1, WINENTSYNC=0, WINE_DISABLE_FAST_SYNC=1). " + "Kernel ≥ 6.14 or the ntsync kmod is required for ntsync."); + warned = true; + } + } + // Ensure Wine's Unix codepage is UTF-8 so non-ASCII filenames (CJK, // Cyrillic, accented Latin) round-trip correctly between Linux FS and // Win32 WCHAR APIs. Wine picks the codepage from LC_ALL > LC_CTYPE > diff --git a/src/src/settingsdialog.cpp b/src/src/settingsdialog.cpp index 7a4c2b9..057fafe 100644 --- a/src/src/settingsdialog.cpp +++ b/src/src/settingsdialog.cpp @@ -26,6 +26,7 @@ along with Mod Organizer. If not, see . #include "settingsdialogplugins.h" #include "settingsdialogproton.h" #include "settingsdialogtheme.h" +#include "settingsdialogupdates.h" #include "settingsdialogworkarounds.h" #include "ui_settingsdialog.h" @@ -40,6 +41,8 @@ SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& setti m_tabs.push_back( std::unique_ptr(new GeneralSettingsTab(settings, *this))); + m_tabs.push_back( + std::unique_ptr(new UpdatesSettingsTab(settings, *this))); m_tabs.push_back(std::unique_ptr(new ThemeSettingsTab(settings, *this))); m_tabs.push_back( std::unique_ptr(new ModListSettingsTab(settings, *this))); diff --git a/src/src/settingsdialoggeneral.cpp b/src/src/settingsdialoggeneral.cpp index a53f6ba..690dfb4 100644 --- a/src/src/settingsdialoggeneral.cpp +++ b/src/src/settingsdialoggeneral.cpp @@ -21,18 +21,10 @@ GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) ui->hideDownloadInstallBox->setChecked( settings().interface().hideDownloadsAfterInstallation()); - // updates - ui->checkForUpdates->setChecked(settings().checkForUpdates()); - // The "beta versions" checkbox drives both the legacy Nexus-side pre-release - // opt-in and the Fluorine self-update channel, so a single toggle covers the - // user-visible concept of "I want beta builds". - const bool onBeta = - settings().fluorineUpdateChannel() == QStringLiteral("beta") || - settings().usePrereleases(); - ui->usePrereleaseBox->setChecked(onBeta); - ui->usePrereleaseBox->setToolTip( - QObject::tr("Receive rolling beta builds from the main branch. When " - "off, only tagged stable releases are offered.")); + // Update settings have moved to their own "Updates" tab. Hide the + // legacy controls here so the two UIs don't contradict each other. + ui->checkForUpdates->hide(); + ui->usePrereleaseBox->hide(); // profile defaults ui->localINIs->setChecked(settings().profileLocalInis()); @@ -74,12 +66,7 @@ void GeneralSettingsTab::update() settings().interface().setHideDownloadsAfterInstallation( ui->hideDownloadInstallBox->isChecked()); - // updates - settings().setCheckForUpdates(ui->checkForUpdates->isChecked()); - settings().setUsePrereleases(ui->usePrereleaseBox->isChecked()); - settings().setFluorineUpdateChannel(ui->usePrereleaseBox->isChecked() - ? QStringLiteral("beta") - : QStringLiteral("stable")); + // Update settings are persisted by UpdatesSettingsTab (own tab). // profile defaults settings().setProfileLocalInis(ui->localINIs->isChecked()); diff --git a/src/src/settingsdialogupdates.cpp b/src/src/settingsdialogupdates.cpp new file mode 100644 index 0000000..b46ff1e --- /dev/null +++ b/src/src/settingsdialogupdates.cpp @@ -0,0 +1,173 @@ +#include "settingsdialogupdates.h" + +#include "fluorineupdater.h" +#include "ui_settingsdialog.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) + : SettingsTab(s, d) +{ + // Build the tab contents programmatically so we don't have to hand-edit + // settingsdialog.ui — the Updates section used to live on the General + // tab; moving it to its own tab keeps General uncluttered and gives us + // room for a "Check for updates now" button + live status line. + QWidget* page = new QWidget(ui->tabWidget); + auto* layout = new QVBoxLayout(page); + + // --- Current build info ------------------------------------------------ + auto* infoGroup = new QGroupBox(tr("Current build"), page); + auto* infoLayout = new QFormLayout(infoGroup); + + const QString currentVersion = + QStringLiteral(FLUORINE_DISPLAY_VERSION); + const QString channel = + QStringLiteral(FLUORINE_BUILD_CHANNEL); + const QString commit = QStringLiteral(FLUORINE_BUILD_COMMIT); + const QString timestamp = QStringLiteral(FLUORINE_BUILD_TIMESTAMP); + + m_currentVersionLabel = new QLabel(currentVersion, infoGroup); + infoLayout->addRow(tr("Version:"), m_currentVersionLabel); + + QString buildLine = QStringLiteral("channel=%1").arg(channel); + if (!commit.isEmpty()) { + buildLine += QStringLiteral(" commit=%1").arg(commit); + } + if (!timestamp.isEmpty()) { + buildLine += QStringLiteral(" timestamp=%1").arg(timestamp); + } + m_buildInfoLabel = new QLabel(buildLine, infoGroup); + m_buildInfoLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + infoLayout->addRow(tr("Build:"), m_buildInfoLabel); + + layout->addWidget(infoGroup); + + // --- Preferences ------------------------------------------------------- + auto* prefsGroup = new QGroupBox(tr("Update preferences"), page); + auto* prefsLayout = new QVBoxLayout(prefsGroup); + + m_checkForUpdates = new QCheckBox(tr("Check for updates on startup"), + prefsGroup); + m_checkForUpdates->setToolTip( + tr("Query GitHub for a newer Fluorine Manager build when the app " + "starts. Nothing is installed automatically — you'll see a log " + "entry and can download the new build from the release page.")); + prefsLayout->addWidget(m_checkForUpdates); + + auto* channelRow = new QHBoxLayout(); + channelRow->addWidget(new QLabel(tr("Channel:"), prefsGroup)); + m_channelBox = new QComboBox(prefsGroup); + m_channelBox->addItem(tr("Stable (tagged releases)"), + QStringLiteral("stable")); + m_channelBox->addItem(tr("Beta (rolling build from main)"), + QStringLiteral("beta")); + m_channelBox->setToolTip( + tr("Stable: only tagged v* releases. Beta: the rolling `beta` " + "release that's replaced on every successful CI build. Beta " + "builds embed a commit hash and timestamp so the updater can " + "tell whether you're already on the latest one.")); + channelRow->addWidget(m_channelBox, 1); + prefsLayout->addLayout(channelRow); + + layout->addWidget(prefsGroup); + + // --- Actions ----------------------------------------------------------- + auto* actionGroup = new QGroupBox(tr("Actions"), page); + auto* actionLayout = new QVBoxLayout(actionGroup); + + auto* buttonRow = new QHBoxLayout(); + m_checkNowButton = + new QPushButton(tr("Check for updates now"), actionGroup); + buttonRow->addWidget(m_checkNowButton); + buttonRow->addStretch(1); + actionLayout->addLayout(buttonRow); + + m_statusLabel = new QLabel(actionGroup); + m_statusLabel->setTextFormat(Qt::RichText); + m_statusLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + m_statusLabel->setOpenExternalLinks(true); + m_statusLabel->setWordWrap(true); + actionLayout->addWidget(m_statusLabel); + + layout->addWidget(actionGroup); + layout->addStretch(1); + + // Insert as the second tab (right after General) so it's easy to find. + const int insertIndex = + std::max(1, std::min(ui->tabWidget->count(), 1)); + ui->tabWidget->insertTab(insertIndex, page, tr("Updates")); + + // --- State ------------------------------------------------------------- + m_checkForUpdates->setChecked(settings().checkForUpdates()); + const QString currentChannel = settings().fluorineUpdateChannel(); + const int idx = m_channelBox->findData(currentChannel); + m_channelBox->setCurrentIndex(idx >= 0 ? idx : 0); + + m_updater = new FluorineUpdater(&d); + + QObject::connect(m_checkNowButton, &QPushButton::clicked, &d, + [this]() { onCheckNow(); }); + + QObject::connect(m_updater, &FluorineUpdater::updateAvailable, &d, + [this](const FluorineUpdater::ReleaseInfo& info) { + const QString url = info.htmlUrl.isEmpty() + ? QString() + : info.htmlUrl; + QString line = tr("Update available: %1") + .arg(info.name.isEmpty() ? info.tagName + : info.name); + if (!url.isEmpty()) { + line += QStringLiteral( + " — view release") + .arg(url); + } + m_statusLabel->setText(line); + m_checkNowButton->setEnabled(true); + }); + QObject::connect(m_updater, &FluorineUpdater::upToDate, &d, + [this](const FluorineUpdater::ReleaseInfo&) { + m_statusLabel->setText( + tr("You're on the latest build for this channel.")); + m_checkNowButton->setEnabled(true); + }); + QObject::connect(m_updater, &FluorineUpdater::checkFailed, &d, + [this](const QString& reason) { + m_statusLabel->setText( + tr("Update check failed: %1").arg(reason)); + m_checkNowButton->setEnabled(true); + }); +} + +void UpdatesSettingsTab::update() +{ + settings().setCheckForUpdates(m_checkForUpdates->isChecked()); + const QString channel = m_channelBox->currentData().toString(); + settings().setFluorineUpdateChannel(channel); + // Keep the legacy usePrereleases flag aligned so the Nexus-side pre- + // release toggle mirrors the Fluorine channel selection. + settings().setUsePrereleases(channel == QStringLiteral("beta")); +} + +void UpdatesSettingsTab::onCheckNow() +{ + m_statusLabel->setText(tr("Checking…")); + m_checkNowButton->setEnabled(false); + + const QString channel = m_channelBox->currentData().toString(); + const FluorineUpdater::Channel c = FluorineUpdater::channelFromString( + channel, FluorineUpdater::buildChannel()); + m_updater->checkForUpdates(c); +} diff --git a/src/src/settingsdialogupdates.h b/src/src/settingsdialogupdates.h new file mode 100644 index 0000000..7a39311 --- /dev/null +++ b/src/src/settingsdialogupdates.h @@ -0,0 +1,35 @@ +#ifndef SETTINGSDIALOGUPDATES_H +#define SETTINGSDIALOGUPDATES_H + +#include "settingsdialog.h" + +class QCheckBox; +class QComboBox; +class QLabel; +class QPushButton; + +class FluorineUpdater; + +// Dedicated Settings > Updates tab. Hosts the Fluorine self-update +// preferences (channel selector, startup check toggle) plus an explicit +// "Check for updates now" button with live status feedback. +class UpdatesSettingsTab : public SettingsTab +{ +public: + UpdatesSettingsTab(Settings& settings, SettingsDialog& dialog); + + void update() override; + +private: + void onCheckNow(); + + QCheckBox* m_checkForUpdates = nullptr; + QComboBox* m_channelBox = nullptr; + QLabel* m_currentVersionLabel = nullptr; + QLabel* m_buildInfoLabel = nullptr; + QPushButton* m_checkNowButton = nullptr; + QLabel* m_statusLabel = nullptr; + FluorineUpdater* m_updater = nullptr; +}; + +#endif // SETTINGSDIALOGUPDATES_H -- cgit v1.3.1