aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-17 01:46:09 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-17 01:46:09 -0500
commit05aa96d20ca726908a7d8b1943f86f0c4e11bf18 (patch)
treebd45913d1c7a033088f3456e40c8b44c72818ace /src
parent30d2c801a01eeb140ddd5adf8676f2626ecff3fa (diff)
Prepare Nexus-safe release cleanup
Diffstat (limited to 'src')
-rw-r--r--src/src/envshortcut.cpp75
-rw-r--r--src/src/envshortcut.h26
-rw-r--r--src/src/fuseconnector.cpp93
-rw-r--r--src/src/instancemanager.cpp9
-rw-r--r--src/src/instancemanager.h11
-rw-r--r--src/src/instancemanagerdialog.cpp15
-rw-r--r--src/src/instancemanagerdialog.ui10
-rw-r--r--src/src/mainwindow.cpp6
-rw-r--r--src/src/moapplication.cpp101
-rw-r--r--src/src/modlistview.cpp109
-rw-r--r--src/src/modlistview.h16
-rw-r--r--src/src/organizercore.cpp90
-rw-r--r--src/src/organizercore.h23
-rw-r--r--src/src/plugincontainer.cpp65
-rw-r--r--src/src/processrunner.cpp49
-rw-r--r--src/src/profile.cpp9
-rw-r--r--src/src/protonlauncher.cpp85
-rw-r--r--src/src/protonlauncher.h2
-rw-r--r--src/src/settings.cpp22
-rw-r--r--src/src/settings.h5
-rw-r--r--src/src/settingsdialog.ui36
-rw-r--r--src/src/settingsdialogproton.cpp177
-rw-r--r--src/src/settingsdialogproton.h2
-rw-r--r--src/src/settingsdialogtheme.cpp51
-rw-r--r--src/src/settingsdialogtheme.h11
-rw-r--r--src/src/spawn.cpp61
-rw-r--r--src/src/steamdetection.cpp49
-rw-r--r--src/src/vfs/mo2filesystem.cpp66
-rw-r--r--src/src/vfs/mo2filesystem.h2
29 files changed, 609 insertions, 667 deletions
diff --git a/src/src/envshortcut.cpp b/src/src/envshortcut.cpp
index 60786d2..2baeeac 100644
--- a/src/src/envshortcut.cpp
+++ b/src/src/envshortcut.cpp
@@ -46,6 +46,13 @@ static QString sanitizeDesktopName(const QString& s)
return result;
}
+static QString shellQuote(const QString& s)
+{
+ QString result = s;
+ result.replace("'", "'\"'\"'");
+ return "'" + result + "'";
+}
+
// ---------------------------------------------------------------------------
// PE icon extractor — reads the largest icon embedded in a Windows .exe
// and returns it as a QImage. Returns a null QImage on failure.
@@ -443,6 +450,33 @@ bool Shortcut::add(Locations loc)
QDir().mkpath(QFileInfo(path).absolutePath());
+ const QString script = scriptPath(loc);
+ if (script.isEmpty()) {
+ return false;
+ }
+
+ QFile scriptFile(script);
+ if (!scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ return false;
+ }
+
+ QTextStream scriptOut(&scriptFile);
+ scriptOut << "#!/usr/bin/env bash\n";
+ scriptOut << "set -euo pipefail\n";
+ if (!m_workingDirectory.isEmpty()) {
+ scriptOut << "cd " << shellQuote(m_workingDirectory) << "\n";
+ }
+ scriptOut << "exec " << shellQuote(m_target);
+ if (!m_arguments.isEmpty()) {
+ scriptOut << " " << m_arguments;
+ }
+ scriptOut << "\n";
+ scriptFile.close();
+ scriptFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
+ QFileDevice::ExeOwner | QFileDevice::ReadGroup |
+ QFileDevice::ExeGroup | QFileDevice::ReadOther |
+ QFileDevice::ExeOther);
+
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return false;
@@ -463,12 +497,7 @@ bool Shortcut::add(Locations loc)
if (!m_description.isEmpty()) {
out << "Comment=" << m_description << "\n";
}
- // .desktop Exec values require quoting paths that contain spaces
- out << "Exec=\"" << m_target << "\"";
- if (!m_arguments.isEmpty()) {
- out << " " << m_arguments;
- }
- out << "\n";
+ out << "Exec=\"" << script << "\"\n";
if (!m_workingDirectory.isEmpty()) {
out << "Path=" << m_workingDirectory << "\n";
}
@@ -481,7 +510,10 @@ bool Shortcut::add(Locations loc)
file.close();
// Make it executable (required by some desktop environments)
- file.setPermissions(file.permissions() | QFileDevice::ExeUser);
+ file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
+ QFileDevice::ExeOwner | QFileDevice::ReadGroup |
+ QFileDevice::ExeGroup | QFileDevice::ReadOther |
+ QFileDevice::ExeOther);
return true;
}
@@ -492,7 +524,12 @@ bool Shortcut::remove(Locations loc)
if (path.isEmpty()) {
return false;
}
- return QFile::remove(path);
+ const bool removedShortcut = QFile::remove(path);
+ const QString script = scriptPath(loc);
+ if (!script.isEmpty()) {
+ QFile::remove(script);
+ }
+ return removedShortcut;
}
QString Shortcut::shortcutPath(Locations loc) const
@@ -504,6 +541,15 @@ QString Shortcut::shortcutPath(Locations loc) const
return dir + "/" + shortcutFilename();
}
+QString Shortcut::scriptPath(Locations loc) const
+{
+ const QString dir = shortcutDirectory(loc);
+ if (dir.isEmpty()) {
+ return {};
+ }
+ return dir + "/" + scriptFilename();
+}
+
QString Shortcut::shortcutDirectory(Locations loc)
{
if (loc == Desktop) {
@@ -535,6 +581,19 @@ QString Shortcut::shortcutFilename() const
return base + ".desktop";
}
+QString Shortcut::scriptFilename() const
+{
+ if (m_name.isEmpty()) {
+ return "fluorine-launch.sh";
+ }
+
+ QString base = m_name;
+ if (!m_instanceName.isEmpty()) {
+ base = sanitizeDesktopName(m_instanceName) + "-" + base;
+ }
+ return sanitizeDesktopName(base) + ".sh";
+}
+
QString toString(Shortcut::Locations loc)
{
switch (loc) {
diff --git a/src/src/envshortcut.h b/src/src/envshortcut.h
index 4c9675b..66cfdda 100644
--- a/src/src/envshortcut.h
+++ b/src/src/envshortcut.h
@@ -89,18 +89,20 @@ private:
int m_iconIndex;
QString m_workingDirectory;
- // returns the path where the shortcut file should be saved
- //
- QString shortcutPath(Locations loc) const;
-
- // returns the directory where the shortcut file should be saved
- //
- static QString shortcutDirectory(Locations loc) ;
-
- // returns the filename of the shortcut file that should be used when saving
- //
- QString shortcutFilename() const;
-};
+ // returns the path where the shortcut file should be saved
+ //
+ QString shortcutPath(Locations loc) const;
+ QString scriptPath(Locations loc) const;
+
+ // returns the directory where the shortcut file should be saved
+ //
+ static QString shortcutDirectory(Locations loc);
+
+ // returns the filename of the shortcut file that should be used when saving
+ //
+ QString shortcutFilename() const;
+ QString scriptFilename() const;
+};
// returns a string representation of the given location
//
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index fb2a518..c5b5718 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -56,69 +56,6 @@ namespace
{
namespace fs = std::filesystem;
-bool parseVersionTriplet(const char* text, int* major, int* minor, int* patch)
-{
- if (text == nullptr || major == nullptr || minor == nullptr || patch == nullptr) {
- return false;
- }
-
- char* end = nullptr;
- const long ma = std::strtol(text, &end, 10);
- if (end == text || *end != '.') {
- return false;
- }
- const long mi = std::strtol(end + 1, &end, 10);
- if (*end != '.') {
- return false;
- }
- const long pa = std::strtol(end + 1, &end, 10);
- if (ma < 0 || mi < 0 || pa < 0) {
- return false;
- }
-
- *major = static_cast<int>(ma);
- *minor = static_cast<int>(mi);
- *patch = static_cast<int>(pa);
- return true;
-}
-
-bool runtimeLibfuseHasStableIoUring()
-{
-#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 18)
- int major = 0;
- int minor = 0;
- int patch = 0;
- if (!parseVersionTriplet(fuse_pkgversion(), &major, &minor, &patch)) {
- return false;
- }
- return major > 3 || (major == 3 && (minor > 18 || (minor == 18 && patch >= 2)));
-#else
- return false;
-#endif
-}
-
-void recordRequestTransport(fuse_req_t req) noexcept
-{
- if (req == nullptr) {
- return;
- }
-
- auto* ctx = static_cast<Mo2FsContext*>(fuse_req_userdata(req));
- if (ctx == nullptr) {
- return;
- }
-
-#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 18)
- if (fuse_req_is_uring(req)) {
- ctx->uring_request_count.fetch_add(1, std::memory_order_relaxed);
- } else {
- ctx->legacy_request_count.fetch_add(1, std::memory_order_relaxed);
- }
-#else
- ctx->legacy_request_count.fetch_add(1, std::memory_order_relaxed);
-#endif
-}
-
std::string decodeProcMountField(const std::string& in)
{
std::string out;
@@ -272,21 +209,18 @@ void wrap_init(void* userdata, struct fuse_conn_info* conn) noexcept
void wrap_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept
{
- recordRequestTransport(req);
try { mo2_lookup(req, parent, name); }
MO2_TRY_REPLY(req, "lookup", parent, EIO)
}
void wrap_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_getattr(req, ino, fi); }
MO2_TRY_REPLY(req, "getattr", ino, EIO)
}
void wrap_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_opendir(req, ino, fi); }
MO2_TRY_REPLY(req, "opendir", ino, EIO)
}
@@ -294,7 +228,6 @@ void wrap_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noe
void wrap_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_readdir(req, ino, size, off, fi); }
MO2_TRY_REPLY(req, "readdir", ino, EIO)
}
@@ -302,14 +235,12 @@ void wrap_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
void wrap_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_readdirplus(req, ino, size, off, fi); }
MO2_TRY_REPLY(req, "readdirplus", ino, EIO)
}
void wrap_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_open(req, ino, fi); }
MO2_TRY_REPLY(req, "open", ino, EIO)
}
@@ -317,7 +248,6 @@ void wrap_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexce
void wrap_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_read(req, ino, size, off, fi); }
MO2_TRY_REPLY(req, "read", ino, EIO)
}
@@ -325,7 +255,6 @@ void wrap_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
void wrap_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size,
off_t off, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_write(req, ino, buf, size, off, fi); }
MO2_TRY_REPLY(req, "write", ino, EIO)
}
@@ -333,7 +262,6 @@ void wrap_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size,
void wrap_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode,
struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_create(req, parent, name, mode, fi); }
MO2_TRY_REPLY(req, "create", parent, EIO)
}
@@ -341,7 +269,6 @@ void wrap_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mod
void wrap_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
fuse_ino_t newparent, const char* newname, unsigned int flags) noexcept
{
- recordRequestTransport(req);
try { mo2_rename(req, parent, name, newparent, newname, flags); }
MO2_TRY_REPLY(req, "rename", parent, EIO)
}
@@ -349,56 +276,48 @@ void wrap_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
void wrap_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_setattr(req, ino, attr, to_set, fi); }
MO2_TRY_REPLY(req, "setattr", ino, EIO)
}
void wrap_unlink(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept
{
- recordRequestTransport(req);
try { mo2_unlink(req, parent, name); }
MO2_TRY_REPLY(req, "unlink", parent, EIO)
}
void wrap_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode) noexcept
{
- recordRequestTransport(req);
try { mo2_mkdir(req, parent, name, mode); }
MO2_TRY_REPLY(req, "mkdir", parent, EIO)
}
void wrap_rmdir(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept
{
- recordRequestTransport(req);
try { mo2_rmdir(req, parent, name); }
MO2_TRY_REPLY(req, "rmdir", parent, EIO)
}
void wrap_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_release(req, ino, fi); }
MO2_TRY_REPLY(req, "release", ino, EIO)
}
void wrap_releasedir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_releasedir(req, ino, fi); }
MO2_TRY_REPLY(req, "releasedir", ino, EIO)
}
void wrap_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) noexcept
{
- recordRequestTransport(req);
try { mo2_forget(req, ino, nlookup); }
catch (...) { fuse_reply_none(req); }
}
void wrap_flush(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_flush(req, ino, fi); }
MO2_TRY_REPLY(req, "flush", ino, EIO)
}
@@ -406,7 +325,6 @@ void wrap_flush(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexc
void wrap_fsync(fuse_req_t req, fuse_ino_t ino, int datasync,
struct fuse_file_info* fi) noexcept
{
- recordRequestTransport(req);
try { mo2_fsync(req, ino, datasync, fi); }
MO2_TRY_REPLY(req, "fsync", ino, EIO)
}
@@ -640,15 +558,8 @@ bool FuseConnector::mount(
std::vector<std::string> argvStorage = {
"mo2fuse", "-o", "fsname=mo2linux", "-o", "noatime",
"-o", "default_permissions", "-o", "max_read=1048576"};
- const bool requestIoUring = runtimeLibfuseHasStableIoUring();
- if (requestIoUring) {
- argvStorage.push_back("-o");
- argvStorage.push_back("io_uring");
- }
- std::fprintf(stderr,
- "[VFS] libfuse=%s headers=%d.%d io_uring_mount_option=%d\n",
- fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION,
- requestIoUring ? 1 : 0);
+ std::fprintf(stderr, "[VFS] libfuse=%s headers=%d.%d\n",
+ fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION);
std::vector<char*> argv;
argv.reserve(argvStorage.size());
diff --git a/src/src/instancemanager.cpp b/src/src/instancemanager.cpp
index 066c0bd..1b95b5e 100644
--- a/src/src/instancemanager.cpp
+++ b/src/src/instancemanager.cpp
@@ -228,7 +228,12 @@ void Instance::updateIni()
// updating the settings since some of these values might have been missing
s.game().setName(m_gameName);
- s.game().setDirectory(m_gameDir);
+ if (!m_gameDirAutoDetectedFromInvalidIni) {
+ s.game().setDirectory(m_gameDir);
+ } else {
+ log::warn("preserving existing gamePath in ini {} after auto-detecting {}",
+ iniPath(), m_gameDir);
+ }
s.game().setSelectedProfileName(m_profile);
if (!m_gameVariant.isEmpty()) {
@@ -241,6 +246,7 @@ void Instance::setGame(const QString& name, const QString& dir)
{
m_gameName = name;
m_gameDir = dir;
+ m_gameDirAutoDetectedFromInvalidIni = false;
}
void Instance::setVariant(const QString& name)
@@ -276,6 +282,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins)
game->gameDirectory().absolutePath());
m_gameDir = game->gameDirectory().absolutePath();
+ m_gameDirAutoDetectedFromInvalidIni = true;
} else {
// game seems to be gone completely
log::warn("game plugin {} found no game installation at all",
diff --git a/src/src/instancemanager.h b/src/src/instancemanager.h
index 0d3becd..0208e3e 100644
--- a/src/src/instancemanager.h
+++ b/src/src/instancemanager.h
@@ -190,11 +190,12 @@ public:
std::vector<Object> objectsForDeletion() const;
private:
- QString m_dir;
- bool m_portable;
- QString m_gameName, m_gameDir, m_gameVariant, m_baseDir;
- MOBase::IPluginGame* m_plugin{nullptr};
- QString m_profile;
+ QString m_dir;
+ bool m_portable;
+ QString m_gameName, m_gameDir, m_gameVariant, m_baseDir;
+ bool m_gameDirAutoDetectedFromInvalidIni{false};
+ MOBase::IPluginGame* m_plugin{nullptr};
+ QString m_profile;
// figures out the game plugin for this instance
//
diff --git a/src/src/instancemanagerdialog.cpp b/src/src/instancemanagerdialog.cpp
index 06522dd..567026f 100644
--- a/src/src/instancemanagerdialog.cpp
+++ b/src/src/instancemanagerdialog.cpp
@@ -240,15 +240,6 @@ InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* paren
s.setValue("fluorine/vfs_root_builder", checked);
});
- connect(ui->steamOverlayCheckBox, &QCheckBox::toggled, [&](bool checked) {
- const auto* inst = singleSelection();
- if (!inst) return;
- const QString ini = inst->iniPath();
- if (ini.isEmpty()) return;
- QSettings s(ini, QSettings::IniFormat);
- s.setValue("fluorine/steam_overlay", checked);
- });
-
connect(ui->switchToInstance, &QPushButton::clicked, [&] {
openSelectedInstance();
});
@@ -835,9 +826,6 @@ void InstanceManagerDialog::fillData(const Instance& ii)
ui->vfsRootBuilderCheckBox->setChecked(s.value("fluorine/vfs_root_builder", true).toBool());
ui->vfsRootBuilderCheckBox->blockSignals(false);
- ui->steamOverlayCheckBox->blockSignals(true);
- ui->steamOverlayCheckBox->setChecked(s.value("fluorine/steam_overlay", false).toBool());
- ui->steamOverlayCheckBox->blockSignals(false);
} else {
ui->prefixPath->clear();
ui->protonVersion->clear();
@@ -850,9 +838,6 @@ void InstanceManagerDialog::fillData(const Instance& ii)
ui->vfsRootBuilderCheckBox->blockSignals(true);
ui->vfsRootBuilderCheckBox->setChecked(false);
ui->vfsRootBuilderCheckBox->blockSignals(false);
- ui->steamOverlayCheckBox->blockSignals(true);
- ui->steamOverlayCheckBox->setChecked(false);
- ui->steamOverlayCheckBox->blockSignals(false);
}
}
diff --git a/src/src/instancemanagerdialog.ui b/src/src/instancemanagerdialog.ui
index 6a4e5ee..64928f5 100644
--- a/src/src/instancemanagerdialog.ui
+++ b/src/src/instancemanagerdialog.ui
@@ -353,16 +353,6 @@
</property>
</widget>
</item>
- <item row="13" column="0" colspan="2">
- <widget class="QCheckBox" name="steamOverlayCheckBox">
- <property name="text">
- <string>Enable Steam Overlay</string>
- </property>
- <property name="toolTip">
- <string>Inject the Steam overlay (Shift+Tab) into the game. Requires Steam running, the game's Steam App ID set, and that you own the game on Steam. Combines LD_PRELOAD of gameoverlayrenderer.so with the Steam Vulkan overlay layer (needed for DXVK-rendered games like Skyrim/Fallout).</string>
- </property>
- </widget>
- </item>
</layout>
</widget>
</item>
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp
index ca92499..c70772b 100644
--- a/src/src/mainwindow.cpp
+++ b/src/src/mainwindow.cpp
@@ -673,6 +673,8 @@ void MainWindow::resetButtonIcons()
MainWindow::~MainWindow()
{
try {
+ m_ArchiveListWriter.writeImmediately(true);
+ m_OrganizerCore.pluginsWriter().writeImmediately(true);
cleanup();
m_OrganizerCore.setUserInterface(nullptr);
@@ -2245,7 +2247,8 @@ void MainWindow::readSettings()
s.geometry().restoreToolbars(this);
s.geometry().restoreState(ui->splitter);
s.geometry().restoreState(ui->categoriesSplitter);
- s.geometry().restoreVisibility(ui->menuBar);
+ ui->menuBar->show();
+ ui->toolBar->show();
s.geometry().restoreVisibility(ui->statusBar);
FilterWidget::setOptions(s.interface().filterOptions());
@@ -2341,7 +2344,6 @@ void MainWindow::storeSettings()
s.geometry().saveGeometry(this);
s.geometry().saveDocks(this);
- s.geometry().saveVisibility(ui->menuBar);
s.geometry().saveVisibility(ui->statusBar);
s.geometry().saveToolbars(this);
s.geometry().saveState(ui->splitter);
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp
index 94e5b02..16ffb62 100644
--- a/src/src/moapplication.cpp
+++ b/src/src/moapplication.cpp
@@ -43,9 +43,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "tutorialmanager.h"
#include <QDebug>
#include <QDesktopServices>
+#include <QEvent>
#include <QFile>
#include <QPainter>
#include <QProxyStyle>
+#include <QRegularExpression>
#include <QSslSocket>
#include <QStringList>
#include <QStyleFactory>
@@ -498,6 +500,16 @@ int MOApplication::run(MOMultiProcess& multiProcess)
// main window is about to be destroyed
m_nexus->getAccessManager()->setTopLevelWidget(nullptr);
+
+ try {
+ if (m_core != nullptr) {
+ m_core->saveCurrentProfileForShutdown();
+ }
+ } catch (const std::exception& e) {
+ log::error("failed to save current profile during shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to save current profile during shutdown: unknown exception");
+ }
}
// reset geometry if the flag was set from the settings dialog
@@ -650,11 +662,19 @@ void MOApplication::resetForRestart()
// clear instance and profile overrides
InstanceManager::singleton().clearOverrides();
- m_core = {};
+ if (m_core != nullptr) {
+ m_core->saveCurrentProfileForShutdown();
+ }
+
m_plugins = {};
+ QCoreApplication::removePostedEvents(nullptr);
+
+ m_core = {};
m_nexus = {};
m_settings = {};
m_instance = {};
+
+ QCoreApplication::removePostedEvents(nullptr);
}
bool MOApplication::setStyleFile(const QString& styleName)
@@ -772,6 +792,78 @@ bool MOApplication::notify(QObject* receiver, QEvent* event)
namespace
{
+QString styleSheetFontSizeOverride(int fontSize)
+{
+ if (fontSize <= 0) {
+ return {};
+ }
+
+ return QStringLiteral("\n\n/* Fluorine QSS font size override */\n"
+ "QWidget { font-size: %1px; }\n")
+ .arg(fontSize);
+}
+
+QString resolveStyleSheetUrl(const QString& url, const QString& baseDir)
+{
+ const QString trimmed = url.trimmed();
+ if (trimmed.isEmpty() || trimmed.startsWith(':') || trimmed.startsWith('/') ||
+ trimmed.contains("://")) {
+ return trimmed;
+ }
+
+ return QUrl::fromLocalFile(QDir(baseDir).absoluteFilePath(trimmed)).toString();
+}
+
+QString resolveRelativeStyleSheetUrls(const QString& stylesheet,
+ const QString& baseDir)
+{
+ static const QRegularExpression urlRe(
+ QStringLiteral(R"(url\(\s*(['"]?)([^'")]+)\1\s*\))"),
+ QRegularExpression::CaseInsensitiveOption);
+
+ QString result;
+ qsizetype last = 0;
+ auto matches = urlRe.globalMatch(stylesheet);
+ while (matches.hasNext()) {
+ const auto match = matches.next();
+ result += stylesheet.mid(last, match.capturedStart() - last);
+ result += QStringLiteral("url(%1)")
+ .arg(resolveStyleSheetUrl(match.captured(2), baseDir));
+ last = match.capturedEnd();
+ }
+ result += stylesheet.mid(last);
+ return result;
+}
+
+QString applyQssFontSize(const QString& stylesheet, int fontSize)
+{
+ if (fontSize <= 0) {
+ return stylesheet;
+ }
+
+ static const QRegularExpression fontSizeRe(
+ QStringLiteral(R"(font-size\s*:\s*[^;{}]+;)"),
+ QRegularExpression::CaseInsensitiveOption);
+
+ QString result = stylesheet;
+ result.replace(fontSizeRe, QStringLiteral("font-size: %1px;").arg(fontSize));
+ result += styleSheetFontSizeOverride(fontSize);
+ return result;
+}
+
+std::optional<QString> loadStyleSheet(const QString& fileName, int fontSize)
+{
+ QFile stylesheet(fileName);
+ if (!stylesheet.open(QFile::ReadOnly | QFile::Text)) {
+ log::error("failed to open stylesheet file {}", fileName);
+ return {};
+ }
+
+ QString content = QString::fromUtf8(stylesheet.readAll());
+ content = resolveRelativeStyleSheetUrls(content, QFileInfo(fileName).absolutePath());
+ return applyQssFontSize(content, fontSize);
+}
+
QStringList extractTopStyleSheetComments(QFile& stylesheet)
{
if (!stylesheet.open(QFile::ReadOnly)) {
@@ -903,6 +995,9 @@ static void createStylesheetCaseShims(const QString& dirPath)
void MOApplication::updateStyle(const QString& fileName)
{
+ const int qssFontSize =
+ m_settings != nullptr ? m_settings->interface().qssFontSize() : 0;
+
if (QStyleFactory::keys().contains(fileName)) {
setStyleSheet("");
setStyle(new ProxyStyle(QStyleFactory::create(fileName)));
@@ -914,7 +1009,9 @@ void MOApplication::updateStyle(const QString& fileName)
createStylesheetCaseShims(QFileInfo(fileName).absolutePath());
setStyle(new ProxyStyle(QStyleFactory::create(
extractBaseStyleFromStyleSheet(stylesheet, m_defaultStyle))));
- setStyleSheet(QString("file:///%1").arg(fileName));
+ if (auto content = loadStyleSheet(fileName, qssFontSize)) {
+ setStyleSheet(*content);
+ }
} else {
log::warn("invalid stylesheet: {}", fileName);
}
diff --git a/src/src/modlistview.cpp b/src/src/modlistview.cpp
index 89e0faa..36bee5f 100644
--- a/src/src/modlistview.cpp
+++ b/src/src/modlistview.cpp
@@ -702,6 +702,66 @@ void ModListView::updateGroupByProxy()
}
}
+void ModListView::applyDefaultHeaderState()
+{
+ // hide these columns by default
+ header()->setSectionHidden(ModList::COL_CONTENT, true);
+ header()->setSectionHidden(ModList::COL_MODID, true);
+ header()->setSectionHidden(ModList::COL_UPLOADER, true);
+ header()->setSectionHidden(ModList::COL_GAME, true);
+ header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
+ header()->setSectionHidden(ModList::COL_NOTES, true);
+
+ // resize mod list to fit content
+ for (int i = 0; i < header()->count(); ++i) {
+ header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+
+ header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
+}
+
+void ModListView::forceHeaderVisibilityRefresh()
+{
+ // restoreState() does not reliably emit resize/visibility signals for each
+ // section, so force our proxy's visible-column bookkeeping to catch up.
+ for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
+ const int sectionSize = header()->sectionSize(column);
+ header()->resizeSection(column, sectionSize + 1);
+ header()->resizeSection(column, sectionSize);
+ }
+}
+
+bool ModListView::headerStateLooksBroken() const
+{
+ int visibleCount = 0;
+ int tinyCount = 0;
+
+ for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
+ if (header()->isSectionHidden(column)) {
+ continue;
+ }
+
+ ++visibleCount;
+ if (header()->sectionSize(column) < 50) {
+ ++tinyCount;
+ }
+ }
+
+ return visibleCount >= 8 && tinyCount >= visibleCount - 1;
+}
+
+void ModListView::syncColumnVisibilityFromHeader()
+{
+ if (m_sortProxy == nullptr) {
+ return;
+ }
+
+ for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
+ m_sortProxy->setColumnVisible(
+ column, !header()->isSectionHidden(column) && header()->sectionSize(column) > 0);
+ }
+}
+
void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw,
Ui::MainWindow* mwui)
{
@@ -798,6 +858,9 @@ void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindo
[=, this](int logicalIndex, int oldSize, int newSize) {
m_sortProxy->setColumnVisible(logicalIndex, newSize != 0);
});
+ connect(header(), &QHeaderView::geometriesChanged, [=, this] {
+ syncColumnVisibilityFromHeader();
+ });
setItemDelegateForColumn(ModList::COL_FLAGS,
new ModFlagIconDelegate(this, ModList::COL_FLAGS, 120));
@@ -809,38 +872,6 @@ void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindo
setItemDelegateForColumn(ModList::COL_VERSION,
new ModListVersionDelegate(this, core.settings()));
- m_restoringHeaderState = true;
- const bool headerRestored = m_core->settings().geometry().restoreState(header());
- m_restoringHeaderState = false;
-
- if (headerRestored) {
- // hack: force the resize-signal to be triggered because restoreState doesn't seem
- // to do that
- for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
- int sectionSize = header()->sectionSize(column);
- header()->resizeSection(column, sectionSize + 1);
- header()->resizeSection(column, sectionSize);
- }
- } else {
- // hide these columns by default
- header()->setSectionHidden(ModList::COL_CONTENT, true);
- header()->setSectionHidden(ModList::COL_MODID, true);
- header()->setSectionHidden(ModList::COL_UPLOADER, true);
- header()->setSectionHidden(ModList::COL_GAME, true);
- header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
- header()->setSectionHidden(ModList::COL_NOTES, true);
-
- // resize mod list to fit content
- for (int i = 0; i < header()->count(); ++i) {
- header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
- }
-
- header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
- }
-
- // prevent the name-column from being hidden
- header()->setSectionHidden(ModList::COL_NAME, false);
-
// we need QueuedConnection for the download/archive dropped otherwise the
// installation starts within the drop-event and it's not possible to drag&drop
// in the manual installer
@@ -901,8 +932,20 @@ void ModListView::restoreState(const Settings& s)
s.widgets().restoreIndex(ui.groupBy);
m_restoringHeaderState = true;
- s.geometry().restoreState(header());
+ const bool headerRestored = s.geometry().restoreState(header());
m_restoringHeaderState = false;
+ if (headerRestored && !headerStateLooksBroken()) {
+ forceHeaderVisibilityRefresh();
+ } else {
+ if (headerRestored) {
+ log::warn("discarding broken mod list header state");
+ }
+ applyDefaultHeaderState();
+ }
+
+ // prevent the name-column from being hidden
+ header()->setSectionHidden(ModList::COL_NAME, false);
+ syncColumnVisibilityFromHeader();
s.widgets().restoreTreeExpandState(this);
diff --git a/src/src/modlistview.h b/src/src/modlistview.h
index a5c079c..839e777 100644
--- a/src/src/modlistview.h
+++ b/src/src/modlistview.h
@@ -314,12 +314,16 @@ private: // private functions
//
void refreshExpandedItems();
- // refresh the group-by proxy, if the index is -1 will refresh the
- // current one (e.g. when changing the sort column)
- //
- void updateGroupByProxy();
-
-public: // member variables
+ // refresh the group-by proxy, if the index is -1 will refresh the
+ // current one (e.g. when changing the sort column)
+ //
+ void updateGroupByProxy();
+ void applyDefaultHeaderState();
+ void forceHeaderVisibilityRefresh();
+ bool headerStateLooksBroken() const;
+ void syncColumnVisibilityFromHeader();
+
+public: // member variables
OrganizerCore* m_core{nullptr};
std::unique_ptr<FilterList> m_filters;
CategoryFactory* m_categories;
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 242ae1c..64772fa 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -340,23 +340,44 @@ OrganizerCore::OrganizerCore(Settings& settings)
OrganizerCore::~OrganizerCore()
{
- m_RefresherThread.exit();
- m_RefresherThread.wait();
-
- if (m_StructureDeleter.joinable()) {
- m_StructureDeleter.join();
+ try {
+ saveCurrentProfileForShutdown();
+ } catch (const std::exception& e) {
+ log::error("failed to save current profile during OrganizerCore shutdown: {}",
+ e.what());
+ } catch (...) {
+ log::error(
+ "failed to save current profile during OrganizerCore shutdown: unknown exception");
}
- saveCurrentProfile();
+ try {
+ m_RefresherThread.exit();
+ m_RefresherThread.wait();
+
+ if (m_StructureDeleter.joinable()) {
+ m_StructureDeleter.join();
+ }
+ } catch (const std::exception& e) {
+ log::error("failed while stopping OrganizerCore worker threads: {}", e.what());
+ } catch (...) {
+ log::error("failed while stopping OrganizerCore worker threads: unknown exception");
+ }
- // profile has to be cleaned up before the modinfo-buffer is cleared
- m_CurrentProfile.reset();
+ try {
+ // profile has to be cleaned up before the modinfo-buffer is cleared
+ m_CurrentProfile.reset();
- ModInfo::clear();
- m_ModList.setProfile(nullptr);
- // NexusInterface::instance()->cleanup();
+ ModInfo::clear();
+ m_ModList.setProfile(nullptr);
+ // NexusInterface::instance()->cleanup();
- delete m_DirectoryStructure;
+ delete m_DirectoryStructure;
+ m_DirectoryStructure = nullptr;
+ } catch (const std::exception& e) {
+ log::error("failed while clearing OrganizerCore state: {}", e.what());
+ } catch (...) {
+ log::error("failed while clearing OrganizerCore state: unknown exception");
+ }
}
void OrganizerCore::storeSettings()
@@ -2462,6 +2483,51 @@ void OrganizerCore::saveCurrentProfile()
storeSettings();
}
+void OrganizerCore::saveCurrentProfileForShutdown()
+{
+ if (m_CurrentProfileSavedForShutdown) {
+ return;
+ }
+
+ try {
+ saveCurrentProfile();
+ } catch (const std::exception& e) {
+ log::error("failed to save current profile during shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to save current profile during shutdown: unknown exception");
+ }
+
+ try {
+ if (m_CurrentProfile != nullptr) {
+ m_CurrentProfile->writeModlistNow(true);
+ }
+ } catch (const std::exception& e) {
+ log::error("failed to flush mod list during shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to flush mod list during shutdown: unknown exception");
+ }
+
+ try {
+ m_PluginListsWriter.writeImmediately(true);
+ } catch (const std::exception& e) {
+ log::error("failed to flush plugin list during shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to flush plugin list during shutdown: unknown exception");
+ }
+
+ try {
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->archivesWriter().writeImmediately(true);
+ }
+ } catch (const std::exception& e) {
+ log::error("failed to flush archive list during shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to flush archive list during shutdown: unknown exception");
+ }
+
+ m_CurrentProfileSavedForShutdown = true;
+}
+
ProcessRunner OrganizerCore::processRunner()
{
return ProcessRunner(*this, m_UserInterface);
diff --git a/src/src/organizercore.h b/src/src/organizercore.h
index 973ee64..f5aad22 100644
--- a/src/src/organizercore.h
+++ b/src/src/organizercore.h
@@ -442,12 +442,14 @@ public:
// - group to add the function to
// - if immediateIfReady is true, the function will be called immediately if no
// directory update is running
- boost::signals2::connection onNextRefresh(std::function<void()> const& func,
- RefreshCallbackGroup group,
- RefreshCallbackMode mode);
-
-public: // IPluginDiagnose interface
- std::vector<unsigned int> activeProblems() const override;
+ boost::signals2::connection onNextRefresh(std::function<void()> const& func,
+ RefreshCallbackGroup group,
+ RefreshCallbackMode mode);
+
+ void saveCurrentProfileForShutdown();
+
+public: // IPluginDiagnose interface
+ std::vector<unsigned int> activeProblems() const override;
QString shortDescription(unsigned int key) const override;
QString fullDescription(unsigned int key) const override;
bool hasGuidedFix(unsigned int key) const override;
@@ -559,10 +561,11 @@ private:
QString m_GameName;
MOBase::IPluginGame* m_GamePlugin{nullptr};
ModDataContentHolder m_Contents;
-
- std::shared_ptr<Profile> m_CurrentProfile;
-
- Settings& m_Settings;
+
+ std::shared_ptr<Profile> m_CurrentProfile;
+ bool m_CurrentProfileSavedForShutdown = false;
+
+ Settings& m_Settings;
SelfUpdater m_Updater;
class FluorineUpdater* m_FluorineUpdater = nullptr;
diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp
index e2ca421..277b1d9 100644
--- a/src/src/plugincontainer.cpp
+++ b/src/src/plugincontainer.cpp
@@ -8,9 +8,11 @@
#include <QCoreApplication>
#include <QDirIterator>
#include <QMessageBox>
+#include <QSet>
#include <QToolButton>
#include <QSettings>
#include <cstdio>
+#include <utility>
#include <boost/fusion/algorithm/iteration/for_each.hpp>
#include <boost/fusion/include/at_key.hpp>
#include <boost/fusion/include/for_each.hpp>
@@ -333,8 +335,14 @@ PluginContainer::PluginContainer(OrganizerCore* organizer)
PluginContainer::~PluginContainer()
{
+ try {
+ unloadPlugins();
+ } catch (const std::exception& e) {
+ log::error("failed to unload plugins during shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to unload plugins during shutdown: unknown exception");
+ }
m_Organizer = nullptr;
- unloadPlugins();
}
void PluginContainer::startPlugins(IUserInterface* userInterface)
@@ -1140,6 +1148,48 @@ void PluginContainer::unloadPlugins()
m_Organizer->settings().plugins().clearPlugins();
}
+ QSet<QString> seenProxiedPaths;
+ std::vector<std::pair<IPluginProxy*, QString>> proxiedPaths;
+ const auto objects = bf::at_key<QObject>(m_Plugins);
+ for (QObject* object : objects) {
+ auto* plugin = qobject_cast<IPlugin*>(object);
+ if (!plugin) {
+ continue;
+ }
+
+ const auto req = m_Requirements.find(plugin);
+ if (req == m_Requirements.end()) {
+ continue;
+ }
+
+ if (auto* proxy = req->second.m_Organizer) {
+ proxy->disconnectSignals();
+ }
+
+ if (auto* game = qobject_cast<IPluginGame*>(object)) {
+ unregisterGame(game);
+ }
+
+ auto* pluginProxy = req->second.proxy();
+ if (pluginProxy) {
+ const QString path = filepath(plugin);
+ if (!seenProxiedPaths.contains(path)) {
+ proxiedPaths.emplace_back(pluginProxy, path);
+ seenProxiedPaths.insert(path);
+ }
+ }
+ }
+
+ for (const auto& [proxy, path] : proxiedPaths) {
+ try {
+ proxy->unload(path);
+ } catch (const std::exception& e) {
+ log::error("failed to unload proxied plugin '{}': {}", path, e.what());
+ } catch (...) {
+ log::error("failed to unload proxied plugin '{}': unknown exception", path);
+ }
+ }
+
bf::for_each(m_Plugins, [](auto& t) {
t.second.clear();
});
@@ -1151,8 +1201,17 @@ void PluginContainer::unloadPlugins()
while (!m_PluginLoaders.empty()) {
QPluginLoader* loader = m_PluginLoaders.back();
m_PluginLoaders.pop_back();
- if ((loader != nullptr) && !loader->unload()) {
- log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString());
+ if (loader != nullptr) {
+ try {
+ if (!loader->unload()) {
+ log::debug("failed to unload {}: {}", loader->fileName(),
+ loader->errorString());
+ }
+ } catch (const std::exception& e) {
+ log::error("failed to unload {}: {}", loader->fileName(), e.what());
+ } catch (...) {
+ log::error("failed to unload {}: unknown exception", loader->fileName());
+ }
}
delete loader;
}
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp
index a0deae2..36108b8 100644
--- a/src/src/processrunner.cpp
+++ b/src/src/processrunner.cpp
@@ -1,10 +1,8 @@
#include "processrunner.h"
#include "env.h"
#include "envmodule.h"
-#include "fluorineconfig.h"
#include "instancemanager.h"
#include "iuserinterface.h"
-#include "knowngames.h"
#include "organizercore.h"
#include <iplugingame.h>
@@ -19,7 +17,6 @@
#include <QMetaObject>
#include <QPointer>
#include <QProcess>
-#include <QSettings>
#include <QThread>
#include <cerrno>
@@ -34,31 +31,6 @@
using namespace MOBase;
-static QString steamAppIdFromFluorineConfig()
-{
- if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->app_id != 0) {
- return QString::number(cfg->app_id);
- }
-
- return {};
-}
-
-static QString steamAppIdFromKnownGame(const QString& gameName)
-{
- if (const auto* knownGame = findKnownGameByTitle(gameName);
- knownGame != nullptr && knownGame->steam_app_id != nullptr) {
- return QString::fromLatin1(knownGame->steam_app_id);
- }
-
- return {};
-}
-
-static bool steamOverlayEnabled(const Settings& settings)
-{
- const QSettings instanceIni(settings.filename(), QSettings::IniFormat);
- return instanceIni.value("fluorine/steam_overlay", false).toBool();
-}
-
void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp,
const Settings& settings)
{
@@ -1212,27 +1184,6 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
}
}
- const bool resolveOverlaySteamAppId = m_sp.useProton && steamOverlayEnabled(settings);
-
- if (resolveOverlaySteamAppId && m_sp.steamAppID.trimmed().isEmpty()) {
- const QString knownSteamId = steamAppIdFromKnownGame(game->gameName());
- if (!knownSteamId.isEmpty()) {
- m_sp.steamAppID = knownSteamId;
- log::debug("process runner: using known-game steam app id '{}' for '{}'",
- m_sp.steamAppID, game->gameName());
- }
- }
-
- if (resolveOverlaySteamAppId && m_sp.steamAppID.trimmed().isEmpty()) {
- const QString configSteamId = steamAppIdFromFluorineConfig();
- if (!configSteamId.isEmpty()) {
- m_sp.steamAppID = configSteamId;
- log::debug("process runner: using Fluorine config steam app id '{}' "
- "for launch",
- m_sp.steamAppID);
- }
- }
-
if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) {
return Error;
}
diff --git a/src/src/profile.cpp b/src/src/profile.cpp
index d7dc83e..212da8d 100644
--- a/src/src/profile.cpp
+++ b/src/src/profile.cpp
@@ -151,8 +151,15 @@ Profile::Profile(const Profile& reference)
Profile::~Profile()
{
+ try {
+ m_ModListWriter.writeImmediately(true);
+ } catch (const std::exception& e) {
+ log::error("failed to flush mod list during profile shutdown: {}", e.what());
+ } catch (...) {
+ log::error("failed to flush mod list during profile shutdown: unknown exception");
+ }
+
delete m_Settings;
- m_ModListWriter.writeImmediately(true);
}
void Profile::findProfileSettings()
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp
index e7da03a..0c876ec 100644
--- a/src/src/protonlauncher.cpp
+++ b/src/src/protonlauncher.cpp
@@ -633,12 +633,6 @@ ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm)
return *this;
}
-ProtonLauncher& ProtonLauncher::setSteamOverlay(bool useSteamOverlay)
-{
- m_useSteamOverlay = useSteamOverlay;
- return *this;
-}
-
ProtonLauncher& ProtonLauncher::setUseSLR(bool useSLR)
{
m_useSLR = useSLR;
@@ -714,7 +708,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
return false;
}
- if (m_useSteamDrm || m_useSteamOverlay) {
+ if (m_useSteamDrm) {
ensureSteamRunning();
}
@@ -766,32 +760,26 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
pressureVesselImportantPaths << protonDir;
}
- // Steam overlay needs gameoverlayrenderer.so visible inside the
- // pressure-vessel container. Steam dirs are usually mapped already,
- // but bind them explicitly to be safe — pressure-vessel's defaults
- // change between SLR versions.
- if (m_useSteamOverlay) {
- const QString steamPath = detectSteamPath();
- if (!steamPath.isEmpty()) {
- slrArgs << QStringLiteral("--filesystem=%1/ubuntu12_32").arg(steamPath)
- << QStringLiteral("--filesystem=%1/ubuntu12_64").arg(steamPath);
- }
- }
-
// Expose dedicated xrandr dir so container sees our injected xrandr
// (steamrt4 ships without it, required by Proton-GE protonfixes).
// Pressure-vessel forces PATH=/usr/bin:/bin inside the container and
// ignores host PATH, so we prepend via `env PATH=...` as the command.
QStringList containerCmd;
+ containerCmd << QStringLiteral("/usr/bin/env");
{
const QString xrandrDir =
QDir::homePath() + "/.local/share/fluorine/steamrt/xrandr-bin";
if (QDir(xrandrDir).exists()) {
slrArgs << QStringLiteral("--filesystem=%1").arg(xrandrDir);
- containerCmd << QStringLiteral("/usr/bin/env")
- << QStringLiteral("PATH=%1:/usr/bin:/bin").arg(xrandrDir);
+ containerCmd << QStringLiteral("PATH=%1:/usr/bin:/bin").arg(xrandrDir);
}
}
+ if (m_steamAppId != 0) {
+ const QString appId = QString::number(m_steamAppId);
+ containerCmd << QStringLiteral("STEAM_COMPAT_APP_ID=%1").arg(appId)
+ << QStringLiteral("SteamAppId=%1").arg(appId)
+ << QStringLiteral("SteamGameId=%1").arg(appId);
+ }
containerCmd << protonScript << protonArgs;
slrArgs << "--";
@@ -836,64 +824,11 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
if (m_steamAppId != 0) {
const QString appId = QString::number(m_steamAppId);
+ env.insert("STEAM_COMPAT_APP_ID", appId);
env.insert("SteamAppId", appId);
env.insert("SteamGameId", appId);
}
- // Steam overlay injection. Requires (a) Steam client running (handled
- // above), (b) the game owned on the running Steam account, (c) the env
- // triplet SteamAppId/SteamGameId/SteamOverlayGameId pointing at the real
- // Steam app id so the overlay's IPC handshake matches an installed app,
- // (d) gameoverlayrenderer.so preloaded for legacy GL/X11 hooks, and (e)
- // the Steam Vulkan overlay layer enabled for DXVK-rendered games (most
- // modern titles via Proton — Bethesda games included). We don't add any
- // wrapper process (no reaper) so Steam's "in-game" indicator may stay
- // blank, but the overlay itself still attaches because the .so/layer
- // hook the running process directly.
- if (m_useSteamOverlay && m_steamAppId != 0 && !steamPath.isEmpty()) {
- const QString appId = QString::number(m_steamAppId);
- env.insert("SteamOverlayGameId", appId);
-
- const QString preload32 =
- steamPath + "/ubuntu12_32/gameoverlayrenderer.so";
- const QString preload64 =
- steamPath + "/ubuntu12_64/gameoverlayrenderer.so";
-
- QStringList preloads;
- if (QFileInfo::exists(preload32)) preloads << preload32;
- if (QFileInfo::exists(preload64)) preloads << preload64;
-
- if (preloads.isEmpty()) {
- MOBase::log::warn(
- "Steam overlay enabled but gameoverlayrenderer.so not found under "
- "'{}/ubuntu12_{{32,64}}' — skipping overlay env",
- steamPath);
- } else {
- const QString existing = env.value("LD_PRELOAD");
- const QString joined = preloads.join(":");
- env.insert("LD_PRELOAD",
- existing.isEmpty() ? joined : existing + ":" + joined);
-
- // Force-enable the Vulkan overlay implicit layer. Steam ships
- // VkLayer_VALVE_steam_overlay as an *implicit* Vulkan layer that is
- // normally auto-loaded for Steam-launched processes; pressure-vessel
- // and some loader configurations skip it unless explicitly enabled.
- env.insert("ENABLE_VK_LAYER_VALVE_steam_overlay_1", "1");
- // Also clear the disable-flag in case a Proton wrapper script set it.
- env.remove("DISABLE_VK_LAYER_VALVE_steam_overlay_1");
-
- MOBase::log::info(
- "Steam overlay enabled (appid={}, LD_PRELOAD+={}, "
- "ENABLE_VK_LAYER_VALVE_steam_overlay_1=1)",
- appId, joined);
- }
- } else if (m_useSteamOverlay && m_steamAppId == 0) {
- MOBase::log::warn(
- "Steam overlay requested but no Steam App ID is set for this "
- "executable — overlay needs an appid for Steam to recognise the "
- "game; skipping");
- }
-
// When Steam DRM is disabled (e.g. GOG games), set UMU_ID so that
// Proton-GE skips the built-in steam.exe bridge. Without this, Proton
// tries to initialise the Steam client which causes an assertion failure
diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h
index f0ce09b..7ea41eb 100644
--- a/src/src/protonlauncher.h
+++ b/src/src/protonlauncher.h
@@ -22,7 +22,6 @@ public:
ProtonLauncher& setSteamAppId(uint32_t id);
ProtonLauncher& setWrapper(const QString& wrapperCmd);
ProtonLauncher& setSteamDrm(bool useSteamDrm);
- ProtonLauncher& setSteamOverlay(bool useSteamOverlay);
ProtonLauncher& setUseSLR(bool useSLR);
ProtonLauncher& setStoreVariant(const QString& variant);
ProtonLauncher& addEnvVar(const QString& key, const QString& value);
@@ -57,7 +56,6 @@ private:
uint32_t m_steamAppId{0};
QStringList m_wrapperCommands;
bool m_useSteamDrm{true};
- bool m_useSteamOverlay = false;
bool m_useSLR = true;
QString m_storeVariant; // "GOG", "Epic", or empty for Steam
QMap<QString, QString> m_envVars;
diff --git a/src/src/settings.cpp b/src/src/settings.cpp
index 7f8a447..df4275d 100644
--- a/src/src/settings.cpp
+++ b/src/src/settings.cpp
@@ -2120,6 +2120,28 @@ void InterfaceSettings::setStyleName(const QString& name)
set(m_Settings, "Settings", "style", name);
}
+int InterfaceSettings::qssFontSize() const
+{
+ const int size = get<int>(m_Settings, "Settings", "qss_font_size", 0);
+ if (size < 0) {
+ return 0;
+ }
+ if (size > 48) {
+ return 48;
+ }
+ return size;
+}
+
+void InterfaceSettings::setQssFontSize(int size)
+{
+ if (size < 0) {
+ size = 0;
+ } else if (size > 48) {
+ size = 48;
+ }
+ set(m_Settings, "Settings", "qss_font_size", size);
+}
+
bool InterfaceSettings::collapsibleSeparators(Qt::SortOrder order) const
{
return get<bool>(m_Settings, "Settings",
diff --git a/src/src/settings.h b/src/src/settings.h
index 9c4023e..68a668e 100644
--- a/src/src/settings.h
+++ b/src/src/settings.h
@@ -590,6 +590,11 @@ public:
std::optional<QString> styleName() const;
void setStyleName(const QString& name);
+ // QSS font size override in pixels; 0 means use the stylesheet defaults
+ //
+ int qssFontSize() const;
+ void setQssFontSize(int size);
+
// whether to use collapsible separators when possible
//
bool collapsibleSeparators(Qt::SortOrder order) const;
diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui
index fe57b49..833a23e 100644
--- a/src/src/settingsdialog.ui
+++ b/src/src/settingsdialog.ui
@@ -441,6 +441,32 @@
</widget>
</item>
<item>
+ <widget class="QLabel" name="qssFontSizeLabel">
+ <property name="text">
+ <string>Font size</string>
+ </property>
+ <property name="buddy">
+ <cstring>qssFontSizeSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="qssFontSizeSpinBox">
+ <property name="toolTip">
+ <string>Overrides font-size values in QSS themes. Default uses the theme value.</string>
+ </property>
+ <property name="specialValueText">
+ <string>Default</string>
+ </property>
+ <property name="suffix">
+ <string> px</string>
+ </property>
+ <property name="maximum">
+ <number>48</number>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -1843,16 +1869,6 @@ If you disable this feature, MO will only display official DLCs this way. Please
</property>
</widget>
</item>
- <item row="5" column="0" colspan="4">
- <widget class="QCheckBox" name="fuseIoUringCheckBox">
- <property name="toolTip">
- <string>Enable the kernel FUSE io_uring transport for new VFS mounts. Requires administrator authentication and a VFS remount; Fluorine falls back automatically when unsupported.</string>
- </property>
- <property name="text">
- <string>Enable FUSE io_uring</string>
- </property>
- </widget>
- </item>
<item row="6" column="0">
<widget class="QLabel" name="label_64">
<property name="text">
diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp
index fdb8cbd..f4815c2 100644
--- a/src/src/settingsdialogproton.cpp
+++ b/src/src/settingsdialogproton.cpp
@@ -11,7 +11,6 @@
#include "steamdetection.h"
#include "slrmanager.h"
#include <atomic>
-#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QDateTime>
@@ -32,115 +31,11 @@
#include <QSettings>
#include <QScopeGuard>
#include <QStandardPaths>
-#include <QSignalBlocker>
#include <QVBoxLayout>
namespace
{
std::atomic<ProtonSettingsTab*> g_activeInstallTab = nullptr;
-
-constexpr const char* kFuseIoUringParameter =
- "/sys/module/fuse/parameters/enable_uring";
-
-enum class FuseIoUringState
-{
- Unsupported,
- Disabled,
- Enabled,
- Unknown
-};
-
-FuseIoUringState readFuseIoUringState()
-{
- QFile parameter(QString::fromUtf8(kFuseIoUringParameter));
- if (!parameter.exists()) {
- return FuseIoUringState::Unsupported;
- }
-
- if (!parameter.open(QIODevice::ReadOnly | QIODevice::Text)) {
- return FuseIoUringState::Unknown;
- }
-
- const QByteArray value = parameter.readAll().trimmed().toUpper();
- if (value == "Y" || value == "1") {
- return FuseIoUringState::Enabled;
- }
- if (value == "N" || value == "0") {
- return FuseIoUringState::Disabled;
- }
-
- return FuseIoUringState::Unknown;
-}
-
-QString findPrivilegeHelper(QStringList* arguments)
-{
- const QString command =
- QStringLiteral("printf Y > %1").arg(QString::fromUtf8(kFuseIoUringParameter));
-
- const QString pkexec = QStandardPaths::findExecutable(QStringLiteral("pkexec"));
- if (!pkexec.isEmpty()) {
- *arguments = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), command};
- return pkexec;
- }
-
- const QString sudo = QStandardPaths::findExecutable(QStringLiteral("sudo"));
- if (!sudo.isEmpty()) {
- const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
- if (!env.value(QStringLiteral("SUDO_ASKPASS")).isEmpty()) {
- *arguments = {QStringLiteral("-A"), QStringLiteral("/bin/sh"),
- QStringLiteral("-c"), command};
- return sudo;
- }
-
- const QFileInfo stdinInfo(QStringLiteral("/proc/self/fd/0"));
- const QString stdinTarget = stdinInfo.symLinkTarget();
- if (stdinInfo.exists() &&
- (stdinTarget.startsWith(QStringLiteral("/dev/pts/")) ||
- stdinTarget == QStringLiteral("/dev/tty"))) {
- *arguments = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), command};
- return sudo;
- }
- }
-
- return {};
-}
-
-QString enableFuseIoUring()
-{
- QStringList arguments;
- const QString helper = findPrivilegeHelper(&arguments);
- if (helper.isEmpty()) {
- return QObject::tr("Install pkexec/polkit, or configure sudo askpass, then try "
- "again.");
- }
-
- QProcess process;
- process.start(helper, arguments);
- if (!process.waitForStarted(5000)) {
- return QObject::tr("Failed to start %1.").arg(helper);
- }
-
- if (!process.waitForFinished(120000)) {
- process.kill();
- process.waitForFinished(3000);
- return QObject::tr("Timed out waiting for administrator authentication.");
- }
-
- if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
- const QString stderrText = QString::fromLocal8Bit(process.readAllStandardError())
- .trimmed();
- if (!stderrText.isEmpty()) {
- return stderrText;
- }
- return QObject::tr("Administrator authentication was cancelled or failed.");
- }
-
- if (readFuseIoUringState() != FuseIoUringState::Enabled) {
- return QObject::tr("The kernel did not accept the FUSE io_uring setting.");
- }
-
- return {};
-}
}
ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d)
@@ -203,8 +98,6 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d)
&ProtonSettingsTab::onBrowsePrefixLocation);
QObject::connect(ui->downloadSLRButton, &QPushButton::clicked, this,
&ProtonSettingsTab::onDownloadSLR);
- QObject::connect(ui->fuseIoUringCheckBox, &QCheckBox::clicked, this,
- &ProtonSettingsTab::onFuseIoUringToggled);
QObject::connect(&m_installWatcher, &QFutureWatcher<InstallResult>::finished, this,
&ProtonSettingsTab::onInstallFinished);
@@ -291,8 +184,6 @@ void ProtonSettingsTab::refreshState()
ui->openPrefixFolderButton->setEnabled(!m_busy && active);
ui->winetricksButton->setEnabled(!m_busy && active);
ui->protonVersionCombo->setEnabled(!m_busy);
-
- refreshFuseIoUringState();
}
void ProtonSettingsTab::setBusy(bool busy)
@@ -442,74 +333,6 @@ void ProtonSettingsTab::onDownloadSLR()
progress->show();
}
-void ProtonSettingsTab::refreshFuseIoUringState()
-{
- QSignalBlocker blocker(ui->fuseIoUringCheckBox);
- Q_UNUSED(blocker);
-
- const FuseIoUringState state = readFuseIoUringState();
- ui->fuseIoUringCheckBox->setVisible(true);
-
- switch (state) {
- case FuseIoUringState::Enabled:
- ui->fuseIoUringCheckBox->setChecked(true);
- ui->fuseIoUringCheckBox->setEnabled(false);
- ui->fuseIoUringCheckBox->setToolTip(
- tr("FUSE io_uring is enabled in the kernel. It applies to new VFS "
- "mounts after relaunch/remount."));
- break;
- case FuseIoUringState::Disabled:
- ui->fuseIoUringCheckBox->setChecked(false);
- ui->fuseIoUringCheckBox->setEnabled(!m_busy);
- ui->fuseIoUringCheckBox->setToolTip(
- tr("Enable the kernel FUSE io_uring transport for new VFS mounts. "
- "Requires administrator authentication and a VFS remount; Fluorine "
- "falls back automatically when unsupported."));
- break;
- case FuseIoUringState::Unsupported:
- ui->fuseIoUringCheckBox->setChecked(false);
- ui->fuseIoUringCheckBox->setEnabled(false);
- ui->fuseIoUringCheckBox->setToolTip(
- tr("This kernel does not expose %1. FUSE io_uring requires kernel "
- "support for CONFIG_FUSE_IO_URING.")
- .arg(QString::fromUtf8(kFuseIoUringParameter)));
- break;
- case FuseIoUringState::Unknown:
- ui->fuseIoUringCheckBox->setChecked(false);
- ui->fuseIoUringCheckBox->setEnabled(!m_busy);
- ui->fuseIoUringCheckBox->setToolTip(
- tr("Fluorine could not read the kernel FUSE io_uring setting. Click "
- "to try enabling it with administrator authentication."));
- break;
- }
-}
-
-void ProtonSettingsTab::onFuseIoUringToggled(bool checked)
-{
- if (!checked) {
- refreshFuseIoUringState();
- return;
- }
-
- MOBase::log::info("[VFS] user requested enabling FUSE io_uring via settings UI");
-
- const QString error = enableFuseIoUring();
- if (!error.isEmpty()) {
- MOBase::log::warn("[VFS] failed to enable FUSE io_uring: {}", error);
- QMessageBox::warning(parentWidget(), tr("FUSE io_uring"),
- tr("Could not enable FUSE io_uring:\n%1").arg(error));
- refreshFuseIoUringState();
- return;
- }
-
- MOBase::log::info("[VFS] kernel FUSE io_uring parameter is now enabled");
- QMessageBox::information(
- parentWidget(), tr("FUSE io_uring"),
- tr("FUSE io_uring is enabled. Restart the game/VFS mount before testing; "
- "existing mounts keep their current transport."));
- refreshFuseIoUringState();
-}
-
void ProtonSettingsTab::onBrowsePrefixLocation()
{
const QString dir = QFileDialog::getExistingDirectory(
diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h
index 321e5a2..5eee3bd 100644
--- a/src/src/settingsdialogproton.h
+++ b/src/src/settingsdialogproton.h
@@ -33,11 +33,9 @@ private:
void onWinetricks();
void onBrowsePrefixLocation();
void onDownloadSLR();
- void onFuseIoUringToggled(bool checked);
static QString ensureWinetricks();
static QString findProtonWine(const QString& protonPath);
- void refreshFuseIoUringState();
void runPrefixSetupDialog(uint32_t appId, const QString& prefixPath,
const QString& protonName, const QString& protonPath);
diff --git a/src/src/settingsdialogtheme.cpp b/src/src/settingsdialogtheme.cpp
index a88396b..205789a 100644
--- a/src/src/settingsdialogtheme.cpp
+++ b/src/src/settingsdialogtheme.cpp
@@ -14,9 +14,10 @@ using namespace MOBase;
ThemeSettingsTab::ThemeSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d)
{
- // style
- addStyles();
- selectStyle();
+ // style
+ addStyles();
+ selectStyle();
+ selectQssFontSize();
// colors
ui->colorTable->load(s);
@@ -33,14 +34,23 @@ ThemeSettingsTab::ThemeSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab
void ThemeSettingsTab::update()
{
// style
- const QString oldStyle = settings().interface().styleName().value_or("");
- const QString newStyle =
- ui->styleBox->itemData(ui->styleBox->currentIndex()).toString();
-
- if (oldStyle != newStyle) {
- settings().interface().setStyleName(newStyle);
- emit settings().styleChanged(newStyle);
- }
+ const QString oldStyle = settings().interface().styleName().value_or("");
+ const QString newStyle =
+ ui->styleBox->itemData(ui->styleBox->currentIndex()).toString();
+ const int oldQssFontSize = settings().interface().qssFontSize();
+ const int newQssFontSize = ui->qssFontSizeSpinBox->value();
+
+ if (oldStyle != newStyle) {
+ settings().interface().setStyleName(newStyle);
+ }
+
+ if (oldQssFontSize != newQssFontSize) {
+ settings().interface().setQssFontSize(newQssFontSize);
+ }
+
+ if (oldStyle != newStyle || oldQssFontSize != newQssFontSize) {
+ emit settings().styleChanged(newStyle);
+ }
// colors
ui->colorTable->commitColors();
@@ -90,13 +100,18 @@ void ThemeSettingsTab::selectStyle()
const int currentID =
ui->styleBox->findData(settings().interface().styleName().value_or(""));
- if (currentID != -1) {
- ui->styleBox->setCurrentIndex(currentID);
- }
-}
-
-void ThemeSettingsTab::onExploreStyles()
-{
+ if (currentID != -1) {
+ ui->styleBox->setCurrentIndex(currentID);
+ }
+}
+
+void ThemeSettingsTab::selectQssFontSize()
+{
+ ui->qssFontSizeSpinBox->setValue(settings().interface().qssFontSize());
+}
+
+void ThemeSettingsTab::onExploreStyles()
+{
// Open the instance's stylesheets directory (where custom themes from
// modlists live), or the user data dir as fallback.
QString ssPath;
diff --git a/src/src/settingsdialogtheme.h b/src/src/settingsdialogtheme.h
index 0d60f5c..2e22b0b 100644
--- a/src/src/settingsdialogtheme.h
+++ b/src/src/settingsdialogtheme.h
@@ -13,10 +13,11 @@ public:
void update() override;
-private:
- void addStyles();
- void selectStyle();
- static void onExploreStyles();
-};
+private:
+ void addStyles();
+ void selectStyle();
+ void selectQssFontSize();
+ static void onExploreStyles();
+};
#endif // SETTINGSDIALOGGENERAL_H
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp
index 701304b..5deaa1f 100644
--- a/src/src/spawn.cpp
+++ b/src/src/spawn.cpp
@@ -22,7 +22,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "env.h"
#include "envmodule.h"
#include "fluorineconfig.h"
-#include "knowngames.h"
#include "protonlauncher.h"
#include "settings.h"
#include "shared/appconfig.h"
@@ -193,38 +192,6 @@ uint32_t parseSteamAppId(const QString& steamAppId)
return (ok ? n : 0u);
}
-static uint32_t steamAppIdFromFluorineConfig()
-{
- if (auto cfg = FluorineConfig::load(); cfg.has_value()) {
- return cfg->app_id;
- }
-
- return 0;
-}
-
-static uint32_t steamAppIdFromKnownGame(const QString& gameName)
-{
- if (const auto* knownGame = findKnownGameByTitle(gameName);
- knownGame != nullptr && knownGame->steam_app_id != nullptr) {
- bool ok = false;
- const auto n = QString::fromLatin1(knownGame->steam_app_id).toUInt(&ok);
- return (ok ? n : 0u);
- }
-
- return 0;
-}
-
-static bool steamOverlayEnabledForCurrentInstance()
-{
- const Settings* instanceForLaunch = Settings::maybeInstance();
- if (!instanceForLaunch) {
- return false;
- }
-
- const QSettings instanceIni(instanceForLaunch->filename(), QSettings::IniFormat);
- return instanceIni.value("fluorine/steam_overlay", false).toBool();
-}
-
QString firstExistingSetting(const QSettings& settings, const QStringList& keys)
{
for (const QString& key : keys) {
@@ -440,31 +407,6 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
logSpawning(sp, bin + " " + sp.arguments);
uint32_t steamAppId = parseSteamAppId(sp.steamAppID);
- const bool resolveOverlaySteamAppId =
- sp.useProton && steamOverlayEnabledForCurrentInstance();
- if (resolveOverlaySteamAppId && steamAppId == 0) {
- const Settings* instanceForLaunch = Settings::maybeInstance();
- if (instanceForLaunch) {
- const auto gameName = instanceForLaunch->game().name();
- if (gameName && !gameName->trimmed().isEmpty()) {
- steamAppId = steamAppIdFromKnownGame(*gameName);
- if (steamAppId != 0) {
- MOBase::log::debug("spawn: using known-game steam app id '{}' "
- "for '{}'",
- steamAppId, *gameName);
- }
- }
- }
- }
- if (resolveOverlaySteamAppId && steamAppId == 0) {
- steamAppId = steamAppIdFromFluorineConfig();
- if (steamAppId != 0) {
- MOBase::log::debug("spawn: using Fluorine config steam app id '{}' "
- "for Proton launch",
- steamAppId);
- }
- }
-
ProtonLauncher launcher;
launcher.setBinary(bin)
.setArguments(argList)
@@ -476,17 +418,14 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
// Read per-instance settings from the instance INI (not the global QSettings).
const Settings* instanceForLaunch = Settings::maybeInstance();
bool useSteamDrm = true;
- bool useSteamOverlay = false;
QString storeVariant;
if (instanceForLaunch) {
const QSettings instanceIni(instanceForLaunch->filename(), QSettings::IniFormat);
useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool();
- useSteamOverlay = instanceIni.value("fluorine/steam_overlay", false).toBool();
storeVariant = instanceIni.value("game_edition").toString().trimmed();
}
launcher.setSteamDrm(useSteamDrm)
- .setSteamOverlay(useSteamOverlay)
.setStoreVariant(storeVariant)
.setUseSLR(true);
diff --git a/src/src/steamdetection.cpp b/src/src/steamdetection.cpp
index 512b1fa..c1c3f7f 100644
--- a/src/src/steamdetection.cpp
+++ b/src/src/steamdetection.cpp
@@ -4,6 +4,7 @@
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
+#include <QSet>
#include <QStandardPaths>
#include <uibase/log.h>
@@ -69,10 +70,22 @@ QStringList findSteamLibraryPaths()
auto addLibrary = [&](const QString& path) {
const QString cleanPath = QDir::cleanPath(path);
- if (cleanPath.isEmpty() || libraries.contains(cleanPath))
+ if (cleanPath.isEmpty()) {
return;
- if (QFileInfo::exists(QDir(cleanPath).filePath(QStringLiteral("steamapps"))))
- libraries.append(cleanPath);
+ }
+
+ const QFileInfo info(cleanPath);
+ const QString canonicalPath = info.canonicalFilePath();
+ const QString libraryPath =
+ canonicalPath.isEmpty() ? info.absoluteFilePath() : canonicalPath;
+ const QString normalizedLibraryPath = QDir::cleanPath(libraryPath);
+
+ if (normalizedLibraryPath.isEmpty() || libraries.contains(normalizedLibraryPath)) {
+ return;
+ }
+ if (QFileInfo::exists(QDir(normalizedLibraryPath).filePath(QStringLiteral("steamapps")))) {
+ libraries.append(normalizedLibraryPath);
+ }
};
addLibrary(steamPath);
@@ -225,6 +238,36 @@ QVector<SteamProtonInfo> findSteamProtons()
}),
protons.end());
+ QSet<QString> seenPaths;
+ QSet<QString> seenNames;
+ QVector<SteamProtonInfo> uniqueProtons;
+ uniqueProtons.reserve(protons.size());
+
+ for (SteamProtonInfo& proton : protons) {
+ const QFileInfo pathInfo(proton.path);
+ const QString canonicalPath = pathInfo.canonicalFilePath();
+ const QString pathKey =
+ QDir::cleanPath(canonicalPath.isEmpty() ? proton.path : canonicalPath);
+ const QString nameKey = proton.name.trimmed().toCaseFolded();
+
+ if ((!pathKey.isEmpty() && seenPaths.contains(pathKey)) ||
+ (!nameKey.isEmpty() && seenNames.contains(nameKey))) {
+ MOBase::log::debug("Skipping duplicate Proton '{}' at '{}'",
+ proton.name, proton.path);
+ continue;
+ }
+
+ if (!pathKey.isEmpty()) {
+ seenPaths.insert(pathKey);
+ }
+ if (!nameKey.isEmpty()) {
+ seenNames.insert(nameKey);
+ }
+ uniqueProtons.append(std::move(proton));
+ }
+
+ protons = std::move(uniqueProtons);
+
// Sort: Experimental first, then by name descending (newest first).
std::sort(protons.begin(), protons.end(),
[](const SteamProtonInfo& a, const SteamProtonInfo& b) {
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 7b15d04..9a65017 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -93,57 +93,40 @@ struct OpTimer
OpTimer& operator=(const OpTimer&) = delete;
};
-bool fuseHasFeature(const struct fuse_conn_info* conn, uint64_t flag)
+bool fuseHasFeature(const struct fuse_conn_info* conn, uint32_t flag)
{
if (conn == nullptr) {
return false;
}
-#ifdef FUSE_CAP_OVER_IO_URING
- return (conn->capable_ext & flag) != 0 ||
- (flag <= UINT32_MAX && (conn->capable & static_cast<uint32_t>(flag)) != 0);
-#else
- return (conn->capable & static_cast<uint32_t>(flag)) != 0;
-#endif
+ return (conn->capable & flag) != 0;
}
-bool fuseWantsFeature(const struct fuse_conn_info* conn, uint64_t flag)
+bool fuseWantsFeature(const struct fuse_conn_info* conn, uint32_t flag)
{
if (conn == nullptr) {
return false;
}
-#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
- return fuse_get_feature_flag(const_cast<struct fuse_conn_info*>(conn), flag);
-#else
- return (conn->want & static_cast<uint32_t>(flag)) != 0;
-#endif
+ return (conn->want & flag) != 0;
}
-bool fuseRequestFeature(struct fuse_conn_info* conn, uint64_t flag)
+bool fuseRequestFeature(struct fuse_conn_info* conn, uint32_t flag)
{
if (conn == nullptr) {
return false;
}
-#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
- return fuse_set_feature_flag(conn, flag);
-#else
- if ((conn->capable & static_cast<uint32_t>(flag)) == 0) {
+ if ((conn->capable & flag) == 0) {
return false;
}
- conn->want |= static_cast<uint32_t>(flag);
+ conn->want |= flag;
return true;
-#endif
}
-void fuseDropFeature(struct fuse_conn_info* conn, uint64_t flag)
+void fuseDropFeature(struct fuse_conn_info* conn, uint32_t flag)
{
if (conn == nullptr) {
return;
}
-#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
- fuse_unset_feature_flag(conn, flag);
-#else
- conn->want &= ~static_cast<uint32_t>(flag);
-#endif
+ conn->want &= ~flag;
}
void maybeLogCounters(Mo2FsContext* ctx)
@@ -222,18 +205,13 @@ void maybeLogCounters(Mo2FsContext* ctx)
static_cast<unsigned long long>(
ctx->retained_ro_fd_evictions.load(std::memory_order_relaxed)));
std::fprintf(stderr,
- "[VFS] io bytes_read=%llu bytes_written=%llu cow_writes=%llu "
- "transport_uring=%llu transport_legacy=%llu\n",
+ "[VFS] io bytes_read=%llu bytes_written=%llu cow_writes=%llu\n",
static_cast<unsigned long long>(
ctx->read_bytes.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(
ctx->write_bytes.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(
- ctx->cow_write_count.load(std::memory_order_relaxed)),
- static_cast<unsigned long long>(
- ctx->uring_request_count.load(std::memory_order_relaxed)),
- static_cast<unsigned long long>(
- ctx->legacy_request_count.load(std::memory_order_relaxed)));
+ ctx->cow_write_count.load(std::memory_order_relaxed)));
{
size_t lookupSize = 0;
size_t attrSize = 0;
@@ -1331,15 +1309,6 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
fuseRequestFeature(conn, FUSE_CAP_EXPIRE_ONLY);
}
- bool uringCapable = false;
- bool uringWanted = false;
-#ifdef FUSE_CAP_OVER_IO_URING
- uringCapable = fuseHasFeature(conn, FUSE_CAP_OVER_IO_URING);
- if (uringCapable) {
- uringWanted = fuseRequestFeature(conn, FUSE_CAP_OVER_IO_URING);
- }
-#endif
-
// Maximize async I/O slots (default is 12). The kernel will still only
// dispatch as many as there are actual concurrent requests, so higher
// values just raise the ceiling without wasting memory.
@@ -1366,8 +1335,7 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
std::fprintf(stderr,
"[VFS] init: auto_inval=%d explicit_inval=%d readdirplus=%d "
- "no_opendir=%d uring_capable=%d uring_wanted=%d "
- "max_bg=%u max_readahead=%u\n",
+ "no_opendir=%d max_bg=%u max_readahead=%u\n",
fuseWantsFeature(conn, FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0,
fuseWantsFeature(conn, FUSE_CAP_EXPLICIT_INVAL_DATA) ? 1 : 0,
fuseWantsFeature(conn, FUSE_CAP_READDIRPLUS) ? 1 : 0,
@@ -1376,19 +1344,13 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
#else
0,
#endif
- uringCapable ? 1 : 0,
- uringWanted ? 1 : 0,
conn->max_background, conn->max_readahead);
std::fprintf(stderr,
"[VFS] init_caps: libfuse=%s headers=%d.%d proto=%u.%u "
- "capable=0x%08x capable_ext=0x%016llx want=0x%08x "
- "want_ext=0x%016llx max_write=%u max_read=%u\n",
+ "capable=0x%08x want=0x%08x max_write=%u max_read=%u\n",
fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION,
conn->proto_major, conn->proto_minor, conn->capable,
- static_cast<unsigned long long>(conn->capable_ext),
- conn->want,
- static_cast<unsigned long long>(conn->want_ext),
- conn->max_write, conn->max_read);
+ conn->want, conn->max_write, conn->max_read);
}
void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h
index 111b1b9..f8d5330 100644
--- a/src/src/vfs/mo2filesystem.h
+++ b/src/src/vfs/mo2filesystem.h
@@ -153,8 +153,6 @@ struct Mo2FsContext
std::atomic<uint64_t> read_bytes{0};
std::atomic<uint64_t> write_bytes{0};
std::atomic<uint64_t> cow_write_count{0};
- std::atomic<uint64_t> uring_request_count{0};
- std::atomic<uint64_t> legacy_request_count{0};
// CPU snapshot from previous stats tick (microseconds, from getrusage).
// Used to compute per-tick CPU delta so we can distinguish disk-bound vs
// CPU-bound slowness in the VFS layer.