aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/src/mainwindow.cpp50
-rw-r--r--src/src/prefixsetuprunner.cpp5
-rw-r--r--src/src/protonlauncher.cpp27
-rw-r--r--src/src/settingsdialog.cpp3
-rw-r--r--src/src/settingsdialoggeneral.cpp23
-rw-r--r--src/src/settingsdialogupdates.cpp173
-rw-r--r--src/src/settingsdialogupdates.h35
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(
+ " &mdash; <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