diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-23 19:24:00 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-23 19:24:00 -0500 |
| commit | ce6b7aabe6a52367e882251015c2d263108ecd43 (patch) | |
| tree | 30fc352438cf8c48b52846a612065896503b71f9 /src | |
| parent | e725213d14d02908b29079d6d5b40361d719e9f3 (diff) | |
Dedicated Updates tab, controller + ntsync fixes, titlebar polish
- 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 @ <short-sha>" 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) <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/mainwindow.cpp | 50 | ||||
| -rw-r--r-- | src/src/prefixsetuprunner.cpp | 5 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 27 | ||||
| -rw-r--r-- | src/src/settingsdialog.cpp | 3 | ||||
| -rw-r--r-- | src/src/settingsdialoggeneral.cpp | 23 | ||||
| -rw-r--r-- | src/src/settingsdialogupdates.cpp | 173 | ||||
| -rw-r--r-- | src/src/settingsdialogupdates.h | 35 |
7 files changed, 283 insertions, 33 deletions
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 <http://www.gnu.org/licenses/>. #include "mainwindow.h"
#include "ui_mainwindow.h"
+#include <fluorine_build_info.h>
+
#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());
+ // 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);
+ 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 <http://www.gnu.org/licenses/>. #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<SettingsTab>(new GeneralSettingsTab(settings, *this)));
+ m_tabs.push_back(
+ std::unique_ptr<SettingsTab>(new UpdatesSettingsTab(settings, *this)));
m_tabs.push_back(std::unique_ptr<SettingsTab>(new ThemeSettingsTab(settings, *this)));
m_tabs.push_back(
std::unique_ptr<SettingsTab>(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 <fluorine_build_info.h> + +#include <QCheckBox> +#include <QComboBox> +#include <QFormLayout> +#include <QGroupBox> +#include <QHBoxLayout> +#include <QLabel> +#include <QPushButton> +#include <QTabWidget> +#include <QVBoxLayout> +#include <QWidget> + +#include <algorithm> + +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("<b>Update available:</b> %1") + .arg(info.name.isEmpty() ? info.tagName + : info.name); + if (!url.isEmpty()) { + line += QStringLiteral( + " — <a href=\"%1\">view release</a>") + .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("<i>Update check failed:</i> %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 |
