aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbuild-native.sh82
-rw-r--r--flatpak/com.fluorine.manager.yml2
-rwxr-xr-xflatpak/flatpak-install.sh4
-rwxr-xr-xflatpak/fluorine-manager-wrapper.sh10
-rw-r--r--libs/bsatk/src/bsafolder.cpp23
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.cpp117
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.h7
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.cpp24
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.h1
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/fallout3/gamefallout3.cpp14
-rw-r--r--libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/fallout4/gamefallout4.cpp16
-rw-r--r--libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp14
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp14
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp16
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp2
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/nehrim/gamenehrim.cpp18
-rw-r--r--libs/game_bethesda/src/games/nehrim/gamenehrim.h1
-rw-r--r--libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/oblivion/gameoblivion.cpp12
-rw-r--r--libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.cpp14
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp14
-rw-r--r--libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp10
-rw-r--r--libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp2
-rw-r--r--libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp8
-rw-r--r--libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp16
-rw-r--r--libs/nak/src/paths.rs7
-rw-r--r--libs/usvfs/src/shared/directory_tree.cpp4
-rw-r--r--libs/usvfs/src/shared/directory_tree.h21
-rw-r--r--src/plugins/rootbuilder.py24
-rw-r--r--src/src/CMakeLists.txt11
-rw-r--r--src/src/commandline.cpp22
-rw-r--r--src/src/fluorinepaths.cpp6
-rw-r--r--src/src/fluorinepaths.h8
-rw-r--r--src/src/fuseconnector.cpp51
-rw-r--r--src/src/fuseconnector.h1
-rw-r--r--src/src/instancemanager.cpp15
-rw-r--r--src/src/instancemanagerdialog.cpp131
-rw-r--r--src/src/instancemanagerdialog.h4
-rw-r--r--src/src/instancemanagerdialog.ui38
-rw-r--r--src/src/loglist.cpp2
-rw-r--r--src/src/mainwindow.cpp15
-rw-r--r--src/src/moapplication.cpp43
-rw-r--r--src/src/organizercore.cpp64
-rw-r--r--src/src/process_helper_main.cpp359
-rw-r--r--src/src/processrunner.cpp32
-rw-r--r--src/src/processrunner.h3
-rw-r--r--src/src/profile.cpp37
-rw-r--r--src/src/protonlauncher.cpp118
-rw-r--r--src/src/protonlauncher.h8
-rw-r--r--src/src/settingsdialog.ui32
-rw-r--r--src/src/settingsdialogproton.cpp99
-rw-r--r--src/src/settingsdialogproton.h1
-rw-r--r--src/src/settingsdialogtheme.cpp53
-rw-r--r--src/src/spawn.cpp3
-rw-r--r--src/src/spawn.h2
-rw-r--r--src/src/vfs/vfs_helper_main.cpp17
-rw-r--r--src/src/wineprefix.cpp77
73 files changed, 1458 insertions, 391 deletions
diff --git a/build-native.sh b/build-native.sh
deleted file mode 100755
index 3ebc825..0000000
--- a/build-native.sh
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env bash
-# build-native.sh — Build and install Fluorine Manager natively (non-Flatpak).
-# Uses a container to compile, then installs to ~/.local/share/fluorine/.
-# Override container engine with CONTAINER_ENGINE=docker.
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-IMAGE_NAME="fluorine-builder"
-CONTAINER_ENGINE="${CONTAINER_ENGINE:-podman}"
-INSTALL_DIR="${HOME}/.local/share/fluorine"
-STAGING="${SCRIPT_DIR}/build-container/staging"
-
-# ── Build the container image if it doesn't exist ──
-if ! ${CONTAINER_ENGINE} image exists "${IMAGE_NAME}" 2>/dev/null; then
- echo "Building ${IMAGE_NAME} image (one-time)..."
- ${CONTAINER_ENGINE} build -t "${IMAGE_NAME}" -f "${SCRIPT_DIR}/docker/Dockerfile" "${SCRIPT_DIR}/docker"
-fi
-
-# ── Ensure build dir exists (Podman needs it before mounting) ──
-mkdir -p "${SCRIPT_DIR}/build-container"
-
-# ── Run the build inside the container ──
-echo "Building Fluorine Manager inside container..."
-${CONTAINER_ENGINE} run --rm \
- -v "${SCRIPT_DIR}:/src:Z" \
- -v "${SCRIPT_DIR}/build-container:/src/build:Z" \
- -w /src \
- "${IMAGE_NAME}" \
- bash docker/build-inner.sh
-
-if [ ! -d "${STAGING}" ]; then
- echo "ERROR: Staging directory not found at ${STAGING}"
- exit 1
-fi
-
-# ── Install to ~/.local/share/fluorine/ ──
-echo ""
-echo "Installing to ${INSTALL_DIR}/ ..."
-mkdir -p "${INSTALL_DIR}"
-
-# Remove dangling symlinks left by the Flatpak wrapper (they point to /app/
-# which doesn't exist outside the sandbox) and stale symlinks that conflict
-# with directories from the staging area.
-find "${INSTALL_DIR}" -maxdepth 3 -type l ! -exec test -e {} \; -delete 2>/dev/null || true
-for d in plugins/libs plugins/dlls plugins/data; do
- [ -L "${INSTALL_DIR}/${d}" ] && rm -f "${INSTALL_DIR}/${d}"
-done
-
-# Copy all files, preserving structure. Existing user data (Prefix/, logs/,
-# config/, instances) won't be touched because they're in subdirs that the
-# staging area doesn't contain.
-cp -af "${STAGING}/." "${INSTALL_DIR}/"
-
-# ── Desktop entry ──
-DESKTOP_DIR="${HOME}/.local/share/applications"
-mkdir -p "${DESKTOP_DIR}"
-cat > "${DESKTOP_DIR}/com.fluorine.manager.desktop" <<EOF
-[Desktop Entry]
-Type=Application
-Name=Fluorine Manager
-Comment=Mod Organizer 2 for Linux
-Exec=${INSTALL_DIR}/fluorine-manager
-Icon=com.fluorine.manager
-Terminal=false
-Categories=Game;
-EOF
-
-# Icon
-mkdir -p "${HOME}/.local/share/icons/hicolor/256x256/apps/"
-cp -f "${SCRIPT_DIR}/flatpak/com.fluorine.manager.png" "${HOME}/.local/share/icons/hicolor/256x256/apps/"
-
-# ── Symlink into ~/.local/bin for PATH access ──
-BIN_DIR="${HOME}/.local/bin"
-mkdir -p "${BIN_DIR}"
-ln -sf "${INSTALL_DIR}/fluorine-manager" "${BIN_DIR}/fluorine-manager"
-
-echo ""
-echo "=== Installed ==="
-du -sh "${INSTALL_DIR}"/*/ "${INSTALL_DIR}"/ModOrganizer-core 2>/dev/null | sort -rh
-echo ""
-echo "Fluorine Manager installed to: ${INSTALL_DIR}/"
-echo "Run with: fluorine-manager (or find it in your app launcher)"
diff --git a/flatpak/com.fluorine.manager.yml b/flatpak/com.fluorine.manager.yml
index ec88dab..5ed65dc 100644
--- a/flatpak/com.fluorine.manager.yml
+++ b/flatpak/com.fluorine.manager.yml
@@ -269,6 +269,7 @@ modules:
# VFS helper (standalone, runs on host for Flatpak FUSE support).
# Statically linked against libfuse3 — no runtime deps beyond glibc.
- test -f _build/src/src/mo2-vfs-helper && cp -f _build/src/src/mo2-vfs-helper /app/lib/fluorine/ || true
+ - test -f _build/src/src/mo2-process-helper && cp -f _build/src/src/mo2-process-helper /app/lib/fluorine/ || true
# lootcli.
- test -f _build/libs/lootcli/src/lootcli && cp -f _build/libs/lootcli/src/lootcli /app/lib/fluorine/ || true
@@ -357,6 +358,7 @@ modules:
- find /app/lib/fluorine/lib -name "*.so*" -exec strip --strip-unneeded {} \; 2>/dev/null || true
- test -f /app/lib/fluorine/lootcli && strip --strip-unneeded /app/lib/fluorine/lootcli 2>/dev/null || true
- test -f /app/lib/fluorine/mo2-vfs-helper && strip --strip-unneeded /app/lib/fluorine/mo2-vfs-helper 2>/dev/null || true
+ - test -f /app/lib/fluorine/mo2-process-helper && strip --strip-unneeded /app/lib/fluorine/mo2-process-helper 2>/dev/null || true
# Install wrapper script, desktop file, metainfo, and icon.
- install -Dm755 flatpak/fluorine-manager-wrapper.sh /app/bin/fluorine-manager
diff --git a/flatpak/flatpak-install.sh b/flatpak/flatpak-install.sh
index 53c0645..984d12b 100755
--- a/flatpak/flatpak-install.sh
+++ b/flatpak/flatpak-install.sh
@@ -32,7 +32,7 @@ if [ "${MODE}" = "bundle" ]; then
# ── Build a distributable .flatpak file ──
echo ""
echo "Building Flatpak bundle (this may take a while)..."
- flatpak-builder --repo="${PROJECT_DIR}/flatpak-repo" --force-clean --ccache \
+ flatpak-builder --disable-rofiles-fuse --repo="${PROJECT_DIR}/flatpak-repo" --force-clean --ccache \
"${BUILD_DIR}" "${MANIFEST}"
flatpak build-bundle \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo \
@@ -47,7 +47,7 @@ else
# ── Build and install locally ──
echo ""
echo "Building and installing Flatpak locally (this may take a while)..."
- flatpak-builder --install --user --force-clean --ccache \
+ flatpak-builder --disable-rofiles-fuse --install --user --force-clean --ccache \
"${BUILD_DIR}" "${MANIFEST}"
echo ""
echo "=== Flatpak installed successfully ==="
diff --git a/flatpak/fluorine-manager-wrapper.sh b/flatpak/fluorine-manager-wrapper.sh
index 2e05fcb..eba2b77 100755
--- a/flatpak/fluorine-manager-wrapper.sh
+++ b/flatpak/fluorine-manager-wrapper.sh
@@ -28,7 +28,7 @@ if [ -n "$INSTANCE_DIR" ] && [ -d "$INSTANCE_DIR" ]; then
else
# Global instance: shared overlay at ~/.local/share/fluorine/
# Use $HOME directly to bypass Flatpak's XDG_DATA_HOME remapping.
- USER_DIR="$HOME/.local/share/fluorine"
+ USER_DIR="$HOME/.var/app/com.fluorine.manager"
fi
# ── Create writable overlay with symlinks to bundled files ──
@@ -79,12 +79,16 @@ setup_overlay() {
# VFS helper must be a real binary copy (not a symlink to /app/) because
# it runs on the host via flatpak-spawn --host, where /app/ doesn't exist.
# libfuse3 is statically linked, so no extra .so files needed.
- VFS_HELPER_DIR="$HOME/.local/share/fluorine/bin"
+ VFS_HELPER_DIR="$HOME/.var/app/com.fluorine.manager/bin"
+ mkdir -p "${VFS_HELPER_DIR}"
if [ -e "${BUNDLED}/mo2-vfs-helper" ]; then
- mkdir -p "${VFS_HELPER_DIR}"
cp -f "${BUNDLED}/mo2-vfs-helper" "${VFS_HELPER_DIR}/mo2-vfs-helper"
chmod +x "${VFS_HELPER_DIR}/mo2-vfs-helper"
fi
+ if [ -e "${BUNDLED}/mo2-process-helper" ]; then
+ cp -f "${BUNDLED}/mo2-process-helper" "${VFS_HELPER_DIR}/mo2-process-helper"
+ chmod +x "${VFS_HELPER_DIR}/mo2-process-helper"
+ fi
}
setup_overlay
diff --git a/libs/bsatk/src/bsafolder.cpp b/libs/bsatk/src/bsafolder.cpp
index 21e2611..7cb5f88 100644
--- a/libs/bsatk/src/bsafolder.cpp
+++ b/libs/bsatk/src/bsafolder.cpp
@@ -139,6 +139,19 @@ std::string Folder::getFullPath() const
void Folder::addFolderInt(Folder::Ptr folder)
{
std::filesystem::path path(folder->m_Name);
+ if (path.empty()) {
+ folder->m_Parent = this;
+ if (m_SubFoldersByName.contains("")) {
+ m_SubFoldersByName.at("")->m_Files.insert(m_SubFoldersByName.at("")->m_Files.end(),
+ folder->m_Files.begin(),
+ folder->m_Files.end());
+ return;
+ }
+ m_SubFolders.push_back(folder);
+ m_SubFoldersByName[""] = folder;
+ return;
+ }
+
auto it = path.begin();
std::string firstStr = it->string();
std::filesystem::path remaining;
@@ -174,6 +187,16 @@ void Folder::addFolderInt(Folder::Ptr folder)
Folder::Ptr Folder::addOrFindFolderInt(Folder* folder)
{
std::filesystem::path path(folder->m_Name);
+ if (path.empty()) {
+ if (m_SubFoldersByName.contains("")) {
+ return m_SubFoldersByName.at("");
+ }
+ folder->m_Parent = this;
+ m_SubFolders.push_back(Folder::Ptr(folder));
+ m_SubFoldersByName[""] = m_SubFolders.back();
+ return m_SubFolders.back();
+ }
+
auto it = path.begin();
std::string firstStr = it->string();
std::filesystem::path remaining;
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
index 1bcc2c0..cc561b6 100644
--- a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
@@ -309,6 +309,34 @@ bool GameGamebryo::prepareIni(const QString&)
return true;
}
+#ifndef _WIN32
+// Ensure both the canonical (proper-case) and lowercase versions of an INI file
+// exist in a directory. One will be the real file, the other a symlink.
+// This prevents case-sensitivity issues on Linux where different code paths or
+// the game itself may reference either casing.
+static void ensureCaseAliases(const QDir& dir, const QString& canonicalName,
+ const QString& actualFileName)
+{
+ QString lowerName = canonicalName.toLower();
+
+ auto ensureAlias = [&](const QString& aliasName) {
+ if (aliasName != actualFileName) {
+ QString aliasPath = dir.absoluteFilePath(aliasName);
+ if (!QFileInfo::exists(aliasPath)) {
+ QFile::link(actualFileName, aliasPath);
+ }
+ }
+ };
+
+ // Ensure the canonical (proper-case) name exists
+ ensureAlias(canonicalName);
+ // Ensure the lowercase name exists
+ if (lowerName != canonicalName) {
+ ensureAlias(lowerName);
+ }
+}
+#endif
+
void GameGamebryo::ensureIniFilesExist(const QString& basePath)
{
// Make sure the target directory exists
@@ -347,16 +375,25 @@ void GameGamebryo::ensureIniFilesExist(const QString& basePath)
}
// If a differently-cased version exists with adequate content,
- // replace the stub (if any) with a symlink to the real file.
+ // replace the stub (if any) with a copy of the real file.
if (!caseMismatchPath.isEmpty()) {
QFileInfo altInfo(caseMismatchPath);
if (altInfo.exists() && altInfo.size() > 200) {
if (targetInfo.exists()) {
QFile::remove(targetPath);
}
- QFile::link(altInfo.fileName(), targetPath);
- MOBase::log::info("Created case-matching symlink '{}' -> '{}'",
- iniFile, altInfo.fileName());
+ if (QFile::copy(altInfo.absoluteFilePath(), targetPath)) {
+ QFile::setPermissions(
+ targetPath,
+ QFile::permissions(targetPath) | QFile::WriteUser | QFile::WriteOwner);
+ MOBase::log::info("Copied case-mismatched INI '{}' -> '{}'",
+ altInfo.fileName(), iniFile);
+ } else {
+ MOBase::log::warn("Failed to copy case-mismatched INI '{}' -> '{}'",
+ altInfo.fileName(), iniFile);
+ }
+ // Ensure both proper-case and lowercase aliases exist
+ ensureCaseAliases(baseDir, iniFile, targetInfo.fileName());
continue;
}
}
@@ -364,8 +401,8 @@ void GameGamebryo::ensureIniFilesExist(const QString& basePath)
// The INI doesn't exist or is a stub, and no adequate differently-cased
// version was found. Try to seed from the game's default INI template
- // (e.g., fallout_default.ini, oblivion_default.ini).
- QString baseName = QFileInfo(iniFile).completeBaseName(); // "fallout"
+ // (e.g., Fallout_default.ini, Oblivion_default.ini).
+ QString baseName = QFileInfo(iniFile).completeBaseName();
QString defaultIniName = baseName + "_default.ini";
QString defaultIniPath =
resolveFileCaseInsensitive(gameDirectory().absoluteFilePath(defaultIniName));
@@ -386,6 +423,16 @@ void GameGamebryo::ensureIniFilesExist(const QString& basePath)
defaultIniPath, targetPath);
}
}
+
+#ifndef _WIN32
+ // After creating/finding the INI, ensure both proper-case and lowercase
+ // versions exist so that any code path or the game itself can find it
+ // regardless of which casing it uses.
+ QFileInfo finalInfo(targetPath);
+ if (finalInfo.exists() || finalInfo.isSymLink()) {
+ ensureCaseAliases(baseDir, iniFile, finalInfo.fileName());
+ }
+#endif
}
}
@@ -468,6 +515,36 @@ QString GameGamebryo::localAppFolder()
}
return result;
#else
+ // On Linux, AppData/Local lives inside the Wine prefix.
+ const QString configuredPrefix =
+ QSettings().value("fluorine/prefix_path").toString().trimmed();
+ if (!configuredPrefix.isEmpty()) {
+ const QString appDataLocal =
+ QDir(configuredPrefix).filePath("drive_c/users/steamuser/AppData/Local");
+ if (QDir(appDataLocal).exists() || QDir().mkpath(appDataLocal)) {
+ return appDataLocal;
+ }
+ }
+
+ // Fallback: search Steam Proton prefixes
+ const QString steamRoot =
+ QDir::homePath() + "/.steam/steam/steamapps/compatdata";
+ QDir compatDir(steamRoot);
+ if (compatDir.exists()) {
+ for (const QString& appId :
+ compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
+ const QString appDataLocal = steamRoot + "/" + appId +
+ "/pfx/drive_c/users/steamuser/AppData/Local";
+ if (QDir(appDataLocal).exists()) {
+ return appDataLocal;
+ }
+ }
+ }
+
+ // Last resort: GenericDataLocation (won't work for Wine games but
+ // prevents crashes)
+ MOBase::log::warn("localAppFolder: could not find Wine prefix "
+ "AppData/Local, falling back to XDG data location");
return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
#endif
}
@@ -502,6 +579,32 @@ void GameGamebryo::copyToProfile(QString const& sourcePath,
}
}
}
+
+#ifndef _WIN32
+ // Ensure both proper-case and lowercase versions exist (one as symlink)
+ QFileInfo actualFile(filePath);
+ if (actualFile.exists() || actualFile.isSymLink()) {
+ ensureCaseAliases(destinationDirectory, destinationFileName,
+ actualFile.fileName());
+ }
+#endif
+}
+
+QString GameGamebryo::localAppName() const
+{
+ // Default: derive from the My Games path. If myGamesPath() is
+ // e.g. ".../Documents/My Games/Skyrim Special Edition", we return
+ // "Skyrim Special Edition". This matches the AppData/Local subfolder
+ // for the vast majority of Bethesda games.
+ const QString mgp = myGamesPath();
+ if (!mgp.isEmpty()) {
+ const QString leaf = QDir(mgp).dirName();
+ if (!leaf.isEmpty() && leaf != QStringLiteral(".")) {
+ return leaf;
+ }
+ }
+ // Fallback: gameShortName is used by the base mappings()
+ return gameShortName();
}
MappingType GameGamebryo::mappings() const
@@ -510,7 +613,7 @@ MappingType GameGamebryo::mappings() const
for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) {
result.push_back({m_Organizer->profilePath() + "/" + profileFile,
- localAppFolder() + "/" + gameShortName() + "/" + profileFile,
+ localAppFolder() + "/" + localAppName() + "/" + profileFile,
false});
}
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.h b/libs/game_bethesda/src/gamebryo/gamegamebryo.h
index fa0a13d..78ac1d4 100644
--- a/libs/game_bethesda/src/gamebryo/gamegamebryo.h
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.h
@@ -90,6 +90,13 @@ public: // IPluginFileMapper interface
public: // Other (e.g. for game features)
QString myGamesPath() const;
+ // Returns the folder name used by the game under AppData/Local
+ // (e.g. "FalloutNV", "Skyrim Special Edition"). Defaults to the
+ // My Games subfolder name. Override in games where these differ
+ // (e.g. Enderal uses lowercase "enderal" in AppData but "Enderal"
+ // in My Games).
+ virtual QString localAppName() const;
+
protected:
// Retrieve the saves extension for the game.
virtual QString savegameExtension() const = 0;
diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp
index 5564075..8fb10c6 100644
--- a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp
@@ -2,7 +2,7 @@
EnderalBSAInvalidation::EnderalBSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "enderal.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Enderal.ini", game)
{}
QString EnderalBSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp
index 3ac4ee0..1a7e22c 100644
--- a/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp
+++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp
@@ -16,8 +16,8 @@ QStringList EnderalDataArchives::archives(const MOBase::IProfile* profile) const
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini")
- : localGameDirectory().absoluteFilePath("enderal.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Enderal.ini")
+ : localGameDirectory().absoluteFilePath("Enderal.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -30,8 +30,8 @@ void EnderalDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini")
- : localGameDirectory().absoluteFilePath("enderal.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Enderal.ini")
+ : localGameDirectory().absoluteFilePath("Enderal.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.cpp b/libs/game_bethesda/src/games/enderal/gameenderal.cpp
index fb4e1d7..959d545 100644
--- a/libs/game_bethesda/src/games/enderal/gameenderal.cpp
+++ b/libs/game_bethesda/src/games/enderal/gameenderal.cpp
@@ -51,7 +51,7 @@ bool GameEnderal::init(IOrganizer* moInfo)
registerFeature(dataArchives);
registerFeature(std::make_shared<EnderalBSAInvalidation>(dataArchives.get(), this));
registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
- registerFeature(std::make_shared<EnderalLocalSavegames>(this, "enderal.ini"));
+ registerFeature(std::make_shared<EnderalLocalSavegames>(this, "Enderal.ini"));
registerFeature(std::make_shared<EnderalModDataChecker>(this));
registerFeature(std::make_shared<EnderalModDataContent>(moInfo->gameFeatures()));
registerFeature(std::make_shared<EnderalGamePlugins>(moInfo));
@@ -126,16 +126,16 @@ void GameEnderal::initializeProfile(const QDir& path, ProfileSettings settings)
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/enderal.ini").exists()) {
+ !QFileInfo(myGamesPath() + "/Enderal.ini").exists()) {
// there is no default ini, actually they are going to put them in for us!
- copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini",
- "enderal.ini");
- copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini",
- "enderalprefs.ini");
+ copyToProfile(gameDirectory().absolutePath(), path, "Enderal_default.ini",
+ "Enderal.ini");
+ copyToProfile(gameDirectory().absolutePath(), path, "EnderalPrefs_default.ini",
+ "EnderalPrefs.ini");
} else {
- copyToProfile(myGamesPath(), path, "enderal.ini");
- copyToProfile(myGamesPath(), path, "enderalprefs.ini");
+ copyToProfile(myGamesPath(), path, "Enderal.ini");
+ copyToProfile(myGamesPath(), path, "EnderalPrefs.ini");
}
}
}
@@ -193,6 +193,12 @@ QString GameEnderal::gameShortName() const
return "Enderal";
}
+QString GameEnderal::localAppName() const
+{
+ // Enderal uses lowercase "enderal" in AppData/Local
+ return "enderal";
+}
+
QString GameEnderal::gameNexusName() const
{
return "enderal";
@@ -215,7 +221,7 @@ QStringList GameEnderal::validShortNames() const
QStringList GameEnderal::iniFiles() const
{
- return {"enderal.ini", "enderalprefs.ini"};
+ return {"Enderal.ini", "EnderalPrefs.ini"};
}
QStringList GameEnderal::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.h b/libs/game_bethesda/src/games/enderal/gameenderal.h
index fb0a377..78b8f72 100644
--- a/libs/game_bethesda/src/games/enderal/gameenderal.h
+++ b/libs/game_bethesda/src/games/enderal/gameenderal.h
@@ -30,6 +30,7 @@ public: // IPluginGame interface
virtual QString binaryName() const override;
virtual QString getLauncherName() const override;
virtual QString gameShortName() const override;
+ virtual QString localAppName() const override;
virtual QStringList validShortNames() const override;
virtual QString gameNexusName() const override;
virtual QStringList primarySources() const override;
diff --git a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp
index ce981fb..43c8733 100644
--- a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp
@@ -2,7 +2,7 @@
Fallout3BSAInvalidation::Fallout3BSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Fallout.ini", game)
{}
QString Fallout3BSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp
index ed6627b..167cce5 100644
--- a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp
+++ b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp
@@ -14,8 +14,8 @@ QStringList Fallout3DataArchives::archives(const MOBase::IProfile* profile) cons
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini")
- : localGameDirectory().absoluteFilePath("fallout.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout.ini")
+ : localGameDirectory().absoluteFilePath("Fallout.ini");
result.append(getArchivesFromKey(iniFile, "SArchiveList"));
return result;
@@ -27,7 +27,7 @@ void Fallout3DataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini")
- : localGameDirectory().absoluteFilePath("fallout.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout.ini")
+ : localGameDirectory().absoluteFilePath("Fallout.ini");
setArchivesToKey(iniFile, "SArchiveList", list);
}
diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp
index f2f4562..75e1ea3 100644
--- a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp
+++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp
@@ -40,7 +40,7 @@ bool GameFallout3::init(IOrganizer* moInfo)
registerFeature(dataArchives);
registerFeature(std::make_shared<Fallout3BSAInvalidation>(dataArchives.get(), this));
registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
- registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout.ini"));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Fallout.ini"));
registerFeature(std::make_shared<Fallout3ModDataChecker>(this));
registerFeature(
std::make_shared<Fallout3ModDataContent>(m_Organizer->gameFeatures()));
@@ -167,14 +167,14 @@ void GameFallout3::initializeProfile(const QDir& path, ProfileSettings settings)
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath(), "fallout.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini",
- "fallout.ini");
+ !QFileInfo(myGamesPath(), "Fallout.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Fallout_default.ini",
+ "Fallout.ini");
} else {
- copyToProfile(myGamesPath(), path, "fallout.ini");
+ copyToProfile(myGamesPath(), path, "Fallout.ini");
}
- copyToProfile(myGamesPath(), path, "falloutprefs.ini");
+ copyToProfile(myGamesPath(), path, "FalloutPrefs.ini");
copyToProfile(myGamesPath(), path, "FalloutCustom.ini");
copyToProfile(myGamesPath(), path, "custom.ini");
copyToProfile(myGamesPath(), path, "GECKCustom.ini");
@@ -246,7 +246,7 @@ QString GameFallout3::gameNexusName() const
QStringList GameFallout3::iniFiles() const
{
- return {"fallout.ini", "falloutprefs.ini", "FalloutCustom.ini", "GECKCustom.ini",
+ return {"Fallout.ini", "FalloutPrefs.ini", "FalloutCustom.ini", "GECKCustom.ini",
"GECKPrefs.ini"};
}
diff --git a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp
index c0931ce..035b771 100644
--- a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp
+++ b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp
@@ -22,8 +22,8 @@ QStringList Fallout4DataArchives::archives(const MOBase::IProfile* profile) cons
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini")
- : localGameDirectory().absoluteFilePath("fallout4.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout4.ini")
+ : localGameDirectory().absoluteFilePath("Fallout4.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -36,8 +36,8 @@ void Fallout4DataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini")
- : localGameDirectory().absoluteFilePath("fallout4.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout4.ini")
+ : localGameDirectory().absoluteFilePath("Fallout4.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp
index 8939a3c..02d30cb 100644
--- a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp
+++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp
@@ -43,7 +43,7 @@ bool GameFallout4::init(IOrganizer* moInfo)
registerFeature(std::make_shared<Fallout4ScriptExtender>(this));
registerFeature(dataArchives);
- registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout4custom.ini"));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Fallout4Custom.ini"));
registerFeature(std::make_shared<Fallout4ModDataChecker>(this));
registerFeature(
std::make_shared<Fallout4ModDataContent>(m_Organizer->gameFeatures()));
@@ -139,15 +139,15 @@ void GameFallout4::initializeProfile(const QDir& path, ProfileSettings settings)
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini",
- "fallout4.ini");
+ !QFileInfo(myGamesPath() + "/Fallout4.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Fallout4_default.ini",
+ "Fallout4.ini");
} else {
- copyToProfile(myGamesPath(), path, "fallout4.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4.ini");
}
- copyToProfile(myGamesPath(), path, "fallout4prefs.ini");
- copyToProfile(myGamesPath(), path, "fallout4custom.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4Prefs.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4Custom.ini");
}
}
@@ -226,7 +226,7 @@ QString GameFallout4::gameNexusName() const
QStringList GameFallout4::iniFiles() const
{
- return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"};
+ return {"Fallout4.ini", "Fallout4Prefs.ini", "Fallout4Custom.ini"};
}
QStringList GameFallout4::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp
index e631f75..6d08e86 100644
--- a/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp
+++ b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp
@@ -22,8 +22,8 @@ QStringList Fallout4LondonDataArchives::archives(const MOBase::IProfile* profile
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini")
- : localGameDirectory().absoluteFilePath("fallout4.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout4.ini")
+ : localGameDirectory().absoluteFilePath("Fallout4.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -36,8 +36,8 @@ void Fallout4LondonDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini")
- : localGameDirectory().absoluteFilePath("fallout4.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout4.ini")
+ : localGameDirectory().absoluteFilePath("Fallout4.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp
index 0fa774a..646bf1c 100644
--- a/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp
+++ b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp
@@ -151,15 +151,15 @@ void GameFallout4London::initializeProfile(const QDir& path,
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini",
- "fallout4.ini");
+ !QFileInfo(myGamesPath() + "/Fallout4.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Fallout4_default.ini",
+ "Fallout4.ini");
} else {
- copyToProfile(myGamesPath(), path, "fallout4.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4.ini");
}
- copyToProfile(myGamesPath(), path, "fallout4prefs.ini");
- copyToProfile(myGamesPath(), path, "fallout4custom.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4Prefs.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4Custom.ini");
}
}
@@ -258,7 +258,7 @@ QString GameFallout4London::getLauncherName() const
QStringList GameFallout4London::iniFiles() const
{
- return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"};
+ return {"Fallout4.ini", "Fallout4Prefs.ini", "Fallout4Custom.ini"};
}
QStringList GameFallout4London::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp
index 58a9a09..366724f 100644
--- a/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp
+++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp
@@ -23,8 +23,8 @@ QStringList Fallout4VRDataArchives::archives(const MOBase::IProfile* profile) co
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini")
- : localGameDirectory().absoluteFilePath("fallout4.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout4.ini")
+ : localGameDirectory().absoluteFilePath("Fallout4.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -37,8 +37,8 @@ void Fallout4VRDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini")
- : localGameDirectory().absoluteFilePath("fallout4.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout4.ini")
+ : localGameDirectory().absoluteFilePath("Fallout4.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp
index bd2448b..852bf7d 100644
--- a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp
+++ b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp
@@ -36,7 +36,7 @@ bool GameFallout4VR::init(IOrganizer* moInfo)
}
registerFeature(std::make_shared<Fallout4VRDataArchives>(this));
- registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout4custom.ini"));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Fallout4Custom.ini"));
registerFeature(std::make_shared<Fallout4VRModDataChecker>(this));
registerFeature(
std::make_shared<Fallout4VRModDataContent>(m_Organizer->gameFeatures()));
@@ -112,14 +112,14 @@ void GameFallout4VR::initializeProfile(const QDir& path, ProfileSettings setting
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "fallout4.ini");
+ !QFileInfo(myGamesPath() + "/Fallout4.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Fallout4.ini");
} else {
- copyToProfile(myGamesPath(), path, "fallout4.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4.ini");
}
- copyToProfile(myGamesPath(), path, "fallout4prefs.ini");
- copyToProfile(myGamesPath(), path, "fallout4custom.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4Prefs.ini");
+ copyToProfile(myGamesPath(), path, "Fallout4Custom.ini");
}
}
@@ -175,7 +175,7 @@ QString GameFallout4VR::gameNexusName() const
QStringList GameFallout4VR::iniFiles() const
{
- return {"fallout4.ini", "fallout4custom.ini", "fallout4prefs.ini"};
+ return {"Fallout4.ini", "Fallout4Custom.ini", "Fallout4Prefs.ini"};
}
QStringList GameFallout4VR::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp
index d66591f..2792811 100644
--- a/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp
@@ -2,7 +2,7 @@
FalloutNVBSAInvalidation::FalloutNVBSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Fallout.ini", game)
{}
QString FalloutNVBSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp
index 7314561..008d551 100644
--- a/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp
+++ b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp
@@ -12,8 +12,8 @@ QStringList FalloutNVDataArchives::archives(const MOBase::IProfile* profile) con
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini")
- : localGameDirectory().absoluteFilePath("fallout.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout.ini")
+ : localGameDirectory().absoluteFilePath("Fallout.ini");
result.append(getArchivesFromKey(iniFile, "SArchiveList",
8192)); // NVAC expands the maximum string limit
@@ -26,7 +26,7 @@ void FalloutNVDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini")
- : localGameDirectory().absoluteFilePath("fallout.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout.ini")
+ : localGameDirectory().absoluteFilePath("Fallout.ini");
setArchivesToKey(iniFile, "SArchiveList", list);
}
diff --git a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp
index 1e8e9e9..effc806 100644
--- a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp
+++ b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp
@@ -40,7 +40,7 @@ bool GameFalloutNV::init(IOrganizer* moInfo)
registerFeature(dataArchives);
registerFeature(std::make_shared<FalloutNVBSAInvalidation>(dataArchives.get(), this));
registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
- registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout.ini"));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Fallout.ini"));
registerFeature(std::make_shared<FalloutNVModDataChecker>(this));
registerFeature(
std::make_shared<FalloutNVModDataContent>(m_Organizer->gameFeatures()));
@@ -213,15 +213,15 @@ void GameFalloutNV::initializeProfile(const QDir& path, ProfileSettings settings
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/fallout.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini",
- "fallout.ini");
+ !QFileInfo(myGamesPath() + "/Fallout.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Fallout_default.ini",
+ "Fallout.ini");
} else {
- copyToProfile(myGamesPath(), path, "fallout.ini");
+ copyToProfile(myGamesPath(), path, "Fallout.ini");
}
- copyToProfile(myGamesPath(), path, "falloutprefs.ini");
- copyToProfile(myGamesPath(), path, "falloutcustom.ini");
+ copyToProfile(myGamesPath(), path, "FalloutPrefs.ini");
+ copyToProfile(myGamesPath(), path, "FalloutCustom.ini");
copyToProfile(myGamesPath(), path, "GECKCustom.ini");
copyToProfile(myGamesPath(), path, "GECKPrefs.ini");
}
@@ -282,7 +282,7 @@ QString GameFalloutNV::gameNexusName() const
QStringList GameFalloutNV::iniFiles() const
{
- return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini",
+ return {"Fallout.ini", "FalloutPrefs.ini", "FalloutCustom.ini", "GECKCustom.ini",
"GECKPrefs.ini"};
}
diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp b/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp
index faa3a3b..267d04c 100644
--- a/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp
+++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp
@@ -179,7 +179,7 @@ QString GameMorrowind::gameNexusName() const
QStringList GameMorrowind::iniFiles() const
{
- return {"morrowind.ini"};
+ return {"Morrowind.ini"};
}
QStringList GameMorrowind::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp
index 57a5944..d0b3093 100644
--- a/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp
+++ b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp
@@ -53,8 +53,8 @@ QStringList MorrowindDataArchives::archives(const MOBase::IProfile* profile) con
QString iniFile =
profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("morrowind.ini")
- : gameDirectory().absoluteFilePath("morrowind.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Morrowind.ini")
+ : gameDirectory().absoluteFilePath("Morrowind.ini");
result.append(getArchives(iniFile));
return result;
@@ -65,7 +65,7 @@ void MorrowindDataArchives::writeArchiveList(MOBase::IProfile* profile,
{
QString iniFile =
profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("morrowind.ini")
- : gameDirectory().absoluteFilePath("morrowind.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Morrowind.ini")
+ : gameDirectory().absoluteFilePath("Morrowind.ini");
setArchives(iniFile, before);
}
diff --git a/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp b/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp
index 2e3c8c5..f74448c 100644
--- a/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp
+++ b/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp
@@ -112,14 +112,14 @@ void GameNehrim::initializeProfile(const QDir& path, ProfileSettings settings) c
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/oblivion.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "oblivion_default.ini",
- "oblivion.ini");
+ !QFileInfo(myGamesPath() + "/Oblivion.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Oblivion_default.ini",
+ "Oblivion.ini");
} else {
- copyToProfile(myGamesPath(), path, "oblivion.ini");
+ copyToProfile(myGamesPath(), path, "Oblivion.ini");
}
- copyToProfile(myGamesPath(), path, "oblivionprefs.ini");
+ copyToProfile(myGamesPath(), path, "OblivionPrefs.ini");
}
}
@@ -153,6 +153,12 @@ QString GameNehrim::gameShortName() const
return "Nehrim";
}
+QString GameNehrim::localAppName() const
+{
+ // Nehrim shares Oblivion's AppData/Local folder
+ return "Oblivion";
+}
+
QString GameNehrim::gameNexusName() const
{
return "Nehrim";
@@ -160,7 +166,7 @@ QString GameNehrim::gameNexusName() const
QStringList GameNehrim::iniFiles() const
{
- return {"oblivion.ini", "oblivionprefs.ini"};
+ return {"Oblivion.ini", "OblivionPrefs.ini"};
}
QStringList GameNehrim::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/nehrim/gamenehrim.h b/libs/game_bethesda/src/games/nehrim/gamenehrim.h
index a8af24c..f8f43de 100644
--- a/libs/game_bethesda/src/games/nehrim/gamenehrim.h
+++ b/libs/game_bethesda/src/games/nehrim/gamenehrim.h
@@ -26,6 +26,7 @@ public: // IPluginGame interface
virtual QString steamAPPId() const override;
virtual QStringList primaryPlugins() const override;
virtual QString gameShortName() const override;
+ virtual QString localAppName() const override;
virtual QString gameNexusName() const override;
virtual QStringList iniFiles() const override;
virtual QStringList DLCPlugins() const override;
diff --git a/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp
index ac89d54..4893374 100644
--- a/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp
@@ -2,7 +2,7 @@
NehrimBSAInvalidation::NehrimBSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "oblivion.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Oblivion.ini", game)
{}
QString NehrimBSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp b/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp
index 9f7a174..fc7766a 100644
--- a/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp
+++ b/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp
@@ -113,14 +113,14 @@ void GameOblivion::initializeProfile(const QDir& path, ProfileSettings settings)
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/oblivion.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "oblivion_default.ini",
- "oblivion.ini");
+ !QFileInfo(myGamesPath() + "/Oblivion.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Oblivion_default.ini",
+ "Oblivion.ini");
} else {
- copyToProfile(myGamesPath(), path, "oblivion.ini");
+ copyToProfile(myGamesPath(), path, "Oblivion.ini");
}
- copyToProfile(myGamesPath(), path, "oblivionprefs.ini");
+ copyToProfile(myGamesPath(), path, "OblivionPrefs.ini");
}
}
@@ -171,7 +171,7 @@ QString GameOblivion::gameNexusName() const
QStringList GameOblivion::iniFiles() const
{
- return {"oblivion.ini", "oblivionprefs.ini"};
+ return {"Oblivion.ini", "OblivionPrefs.ini"};
}
QStringList GameOblivion::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp
index 864d37c..9473fa7 100644
--- a/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp
@@ -2,7 +2,7 @@
OblivionBSAInvalidation::OblivionBSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "oblivion.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Oblivion.ini", game)
{}
QString OblivionBSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp
index 47c3dd4..3331cc4 100644
--- a/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp
+++ b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp
@@ -13,8 +13,8 @@ QStringList OblivionDataArchives::archives(const MOBase::IProfile* profile) cons
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini")
- : localGameDirectory().absoluteFilePath("oblivion.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Oblivion.ini")
+ : localGameDirectory().absoluteFilePath("Oblivion.ini");
result.append(getArchivesFromKey(iniFile, "SArchiveList"));
return result;
@@ -26,7 +26,7 @@ void OblivionDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini")
- : localGameDirectory().absoluteFilePath("oblivion.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Oblivion.ini")
+ : localGameDirectory().absoluteFilePath("Oblivion.ini");
setArchivesToKey(iniFile, "SArchiveList", list);
}
diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp
index 4a0053e..b2d95ff 100644
--- a/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp
+++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp
@@ -48,7 +48,7 @@ bool GameSkyrim::init(IOrganizer* moInfo)
registerFeature(dataArchives);
registerFeature(std::make_shared<SkyrimBSAInvalidation>(dataArchives.get(), this));
registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
- registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "skyrim.ini"));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Skyrim.ini"));
registerFeature(std::make_shared<SkyrimModDataChecker>(this));
registerFeature(std::make_shared<SkyrimModDataContent>(m_Organizer->gameFeatures()));
registerFeature(std::make_shared<SkyrimGamePlugins>(moInfo));
@@ -125,14 +125,14 @@ void GameSkyrim::initializeProfile(const QDir& path, ProfileSettings settings) c
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini",
- "skyrim.ini");
+ !QFileInfo(myGamesPath() + "/Skyrim.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Skyrim_default.ini",
+ "Skyrim.ini");
} else {
- copyToProfile(myGamesPath(), path, "skyrim.ini");
+ copyToProfile(myGamesPath(), path, "Skyrim.ini");
}
- copyToProfile(myGamesPath(), path, "skyrimprefs.ini");
+ copyToProfile(myGamesPath(), path, "SkyrimPrefs.ini");
}
}
@@ -187,7 +187,7 @@ QStringList GameSkyrim::validShortNames() const
QStringList GameSkyrim::iniFiles() const
{
- return {"skyrim.ini", "skyrimprefs.ini"};
+ return {"Skyrim.ini", "SkyrimPrefs.ini"};
}
QStringList GameSkyrim::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp
index e056f10..a98a94a 100644
--- a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp
@@ -2,7 +2,7 @@
SkyrimBSAInvalidation::SkyrimBSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "skyrim.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Skyrim.ini", game)
{}
QString SkyrimBSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp
index 6422175..bb18510 100644
--- a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp
+++ b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp
@@ -16,8 +16,8 @@ QStringList SkyrimDataArchives::archives(const MOBase::IProfile* profile) const
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini")
- : localGameDirectory().absoluteFilePath("skyrim.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Skyrim.ini")
+ : localGameDirectory().absoluteFilePath("Skyrim.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -30,8 +30,8 @@ void SkyrimDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini")
- : localGameDirectory().absoluteFilePath("skyrim.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Skyrim.ini")
+ : localGameDirectory().absoluteFilePath("Skyrim.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp
index 5452239..d0492af 100644
--- a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp
+++ b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp
@@ -227,15 +227,15 @@ void GameSkyrimSE::initializeProfile(const QDir& path, ProfileSettings settings)
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini",
- "skyrim.ini");
+ !QFileInfo(myGamesPath() + "/Skyrim.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Skyrim_default.ini",
+ "Skyrim.ini");
} else {
- copyToProfile(myGamesPath(), path, "skyrim.ini");
+ copyToProfile(myGamesPath(), path, "Skyrim.ini");
}
- copyToProfile(myGamesPath(), path, "skyrimprefs.ini");
- copyToProfile(myGamesPath(), path, "skyrimcustom.ini");
+ copyToProfile(myGamesPath(), path, "SkyrimPrefs.ini");
+ copyToProfile(myGamesPath(), path, "SkyrimCustom.ini");
}
}
@@ -298,7 +298,7 @@ QString GameSkyrimSE::gameNexusName() const
QStringList GameSkyrimSE::iniFiles() const
{
- return {"skyrim.ini", "skyrimprefs.ini", "skyrimcustom.ini"};
+ return {"Skyrim.ini", "SkyrimPrefs.ini", "SkyrimCustom.ini"};
}
QStringList GameSkyrimSE::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp
index a4b5869..f3b4c4c 100644
--- a/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp
+++ b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp
@@ -18,8 +18,8 @@ QStringList SkyrimSEDataArchives::archives(const MOBase::IProfile* profile) cons
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini")
- : localGameDirectory().absoluteFilePath("skyrim.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Skyrim.ini")
+ : localGameDirectory().absoluteFilePath("Skyrim.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -32,8 +32,8 @@ void SkyrimSEDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini")
- : localGameDirectory().absoluteFilePath("skyrim.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Skyrim.ini")
+ : localGameDirectory().absoluteFilePath("Skyrim.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp
index 67db6d0..625d846 100644
--- a/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp
+++ b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp
@@ -152,13 +152,13 @@ void GameSkyrimVR::initializeProfile(const QDir& path, ProfileSettings settings)
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/skyrimvr.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "skyrim.ini", "skyrimvr.ini");
+ !QFileInfo(myGamesPath() + "/SkyrimVR.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Skyrim.ini", "SkyrimVR.ini");
} else {
- copyToProfile(myGamesPath(), path, "skyrimvr.ini");
+ copyToProfile(myGamesPath(), path, "SkyrimVR.ini");
}
- copyToProfile(myGamesPath(), path, "skyrimprefs.ini");
+ copyToProfile(myGamesPath(), path, "SkyrimPrefs.ini");
}
}
@@ -224,7 +224,7 @@ QString GameSkyrimVR::gameNexusName() const
QStringList GameSkyrimVR::iniFiles() const
{
- return {"skyrimvr.ini", "skyrimprefs.ini"};
+ return {"SkyrimVR.ini", "SkyrimPrefs.ini"};
}
QStringList GameSkyrimVR::DLCPlugins() const
diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp
index 752aa15..6c032d9 100644
--- a/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp
+++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp
@@ -19,8 +19,8 @@ QStringList SkyrimVRDataArchives::archives(const MOBase::IProfile* profile) cons
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("skyrimvr.ini")
- : localGameDirectory().absoluteFilePath("skyrimvr.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("SkyrimVR.ini")
+ : localGameDirectory().absoluteFilePath("SkyrimVR.ini");
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
@@ -33,8 +33,8 @@ void SkyrimVRDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("skyrimvr.ini")
- : localGameDirectory().absoluteFilePath("skyrimvr.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("SkyrimVR.ini")
+ : localGameDirectory().absoluteFilePath("SkyrimVR.ini");
if (list.length() > 255) {
int splitIdx = list.lastIndexOf(",", 256);
setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
diff --git a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp
index 67a7817..c448a05 100644
--- a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp
+++ b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp
@@ -2,7 +2,7 @@
FalloutTTWBSAInvalidation::FalloutTTWBSAInvalidation(MOBase::DataArchives* dataArchives,
MOBase::IPluginGame const* game)
- : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game)
+ : GamebryoBSAInvalidation(dataArchives, "Fallout.ini", game)
{}
QString FalloutTTWBSAInvalidation::invalidationBSAName() const
diff --git a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp
index 3ffb076..e4a7766 100644
--- a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp
+++ b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp
@@ -12,8 +12,8 @@ QStringList FalloutTTWDataArchives::archives(const MOBase::IProfile* profile) co
QStringList result;
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini")
- : localGameDirectory().absoluteFilePath("fallout.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout.ini")
+ : localGameDirectory().absoluteFilePath("Fallout.ini");
result.append(getArchivesFromKey(iniFile, "SArchiveList"));
return result;
@@ -25,7 +25,7 @@ void FalloutTTWDataArchives::writeArchiveList(MOBase::IProfile* profile,
QString list = before.join(", ");
QString iniFile = profile->localSettingsEnabled()
- ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini")
- : localGameDirectory().absoluteFilePath("fallout.ini");
+ ? QDir(profile->absolutePath()).absoluteFilePath("Fallout.ini")
+ : localGameDirectory().absoluteFilePath("Fallout.ini");
setArchivesToKey(iniFile, "SArchiveList", list);
}
diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp
index 34eb1b2..af1ea31 100644
--- a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp
+++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp
@@ -41,7 +41,7 @@ bool GameFalloutTTW::init(IOrganizer* moInfo)
registerFeature(
std::make_shared<FalloutTTWBSAInvalidation>(dataArchives.get(), this));
registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
- registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "fallout.ini"));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Fallout.ini"));
registerFeature(std::make_shared<FalloutTTWModDataChecker>(this));
registerFeature(
std::make_shared<FalloutTTWModDataContent>(m_Organizer->gameFeatures()));
@@ -222,15 +222,15 @@ void GameFalloutTTW::initializeProfile(const QDir& path, ProfileSettings setting
if (settings.testFlag(IPluginGame::CONFIGURATION)) {
if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
- !QFileInfo(myGamesPath() + "/fallout.ini").exists()) {
- copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini",
- "fallout.ini");
+ !QFileInfo(myGamesPath() + "/Fallout.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Fallout_default.ini",
+ "Fallout.ini");
} else {
- copyToProfile(myGamesPath(), path, "fallout.ini");
+ copyToProfile(myGamesPath(), path, "Fallout.ini");
}
- copyToProfile(myGamesPath(), path, "falloutprefs.ini");
- copyToProfile(myGamesPath(), path, "falloutcustom.ini");
+ copyToProfile(myGamesPath(), path, "FalloutPrefs.ini");
+ copyToProfile(myGamesPath(), path, "FalloutCustom.ini");
copyToProfile(myGamesPath(), path, "GECKCustom.ini");
copyToProfile(myGamesPath(), path, "GECKPrefs.ini");
}
@@ -306,7 +306,7 @@ QString GameFalloutTTW::gameNexusName() const
QStringList GameFalloutTTW::iniFiles() const
{
- return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini",
+ return {"Fallout.ini", "FalloutPrefs.ini", "FalloutCustom.ini", "GECKCustom.ini",
"GECKPrefs.ini"};
}
diff --git a/libs/nak/src/paths.rs b/libs/nak/src/paths.rs
index 4b212b9..e86c284 100644
--- a/libs/nak/src/paths.rs
+++ b/libs/nak/src/paths.rs
@@ -1,12 +1,11 @@
//! Shared data directory for Fluorine Manager.
//!
-//! All data lives under `~/.local/share/fluorine/` — accessible from both
-//! native and Flatpak builds (the Flatpak has `--filesystem=home`).
+//! All data lives under `~/.var/app/com.fluorine.manager/`.
use std::path::PathBuf;
-/// Returns the Fluorine data directory (`~/.local/share/fluorine`).
+/// Returns the Fluorine data directory (`~/.var/app/com.fluorine.manager`).
pub fn data_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
- PathBuf::from(home).join(".local/share/fluorine")
+ PathBuf::from(home).join(".var/app/com.fluorine.manager")
}
diff --git a/libs/usvfs/src/shared/directory_tree.cpp b/libs/usvfs/src/shared/directory_tree.cpp
index 34cc797..33d7ae1 100644
--- a/libs/usvfs/src/shared/directory_tree.cpp
+++ b/libs/usvfs/src/shared/directory_tree.cpp
@@ -34,6 +34,10 @@ fs::path::iterator nextIter(const fs::path::iterator& iter,
void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end)
{
+ if (iter == end) {
+ return;
+ }
+
++iter;
while (iter != end && (iter->wstring() == L"/" || iter->wstring() == L"\\" ||
iter->wstring() == L"."))
diff --git a/libs/usvfs/src/shared/directory_tree.h b/libs/usvfs/src/shared/directory_tree.h
index f82187f..1712071 100644
--- a/libs/usvfs/src/shared/directory_tree.h
+++ b/libs/usvfs/src/shared/directory_tree.h
@@ -327,6 +327,9 @@ public:
NodePtrT findNode(const fs::path& path)
{
fs::path::iterator iter = path.begin();
+ if (iter == path.end()) {
+ return NodePtrT();
+ }
return findNode(path, iter);
}
@@ -338,6 +341,9 @@ public:
const NodePtrT findNode(const fs::path& path) const
{
fs::path::iterator iter = path.begin();
+ if (iter == path.end()) {
+ return NodePtrT();
+ }
return findNode(path, iter);
}
@@ -350,6 +356,9 @@ public:
void visitPath(const fs::path& path, const VisitorFunction& visitor) const
{
fs::path::iterator iter = path.begin();
+ if (iter == path.end()) {
+ return;
+ }
visitPath(path, iter, visitor);
}
@@ -526,6 +535,10 @@ public:
NodePtrT findNode(const fs::path& name, fs::path::iterator& iter)
{
+ if (iter == name.end()) {
+ return NodePtrT();
+ }
+
std::string l = iter->string();
auto subNode = m_Nodes.find(iter->string());
advanceIter(iter, name.end());
@@ -548,6 +561,10 @@ public:
const NodePtrT findNode(const fs::path& name, fs::path::iterator& iter) const
{
+ if (iter == name.end()) {
+ return NodePtrT();
+ }
+
auto subNode = m_Nodes.find(iter->string());
advanceIter(iter, name.end());
@@ -570,6 +587,10 @@ public:
void visitPath(const fs::path& path, fs::path::iterator& iter,
const VisitorFunction& visitor) const
{
+ if (iter == path.end()) {
+ return;
+ }
+
auto subNode = m_Nodes.find(iter->string());
if (subNode != m_Nodes.end()) {
diff --git a/src/plugins/rootbuilder.py b/src/plugins/rootbuilder.py
index fbe082c..dcc6706 100644
--- a/src/plugins/rootbuilder.py
+++ b/src/plugins/rootbuilder.py
@@ -69,8 +69,20 @@ def _host_cp(src: str, dst: str) -> bool:
return False
+def _ensure_readable(path: str):
+ """Ensure a file has owner-read permission (mod archives sometimes strip it)."""
+ try:
+ st = os.stat(path)
+ if not (st.st_mode & 0o400):
+ os.chmod(path, st.st_mode | 0o400)
+ except OSError:
+ pass
+
+
def _reflink_copy(src: str, dst: str):
"""Copy with reflink (CoW) if supported, fallback to regular copy."""
+ _ensure_readable(src)
+ last_err = None
try:
subprocess.run(
["cp", "--reflink=auto", "-f", "--", src, dst],
@@ -78,16 +90,18 @@ def _reflink_copy(src: str, dst: str):
capture_output=True,
)
return
- except (subprocess.CalledProcessError, FileNotFoundError):
- pass
+ except subprocess.CalledProcessError as e:
+ last_err = f"cp failed (exit {e.returncode}): {e.stderr.decode(errors='replace').strip()}"
+ except FileNotFoundError:
+ last_err = "cp command not found"
try:
shutil.copy2(src, dst)
return
- except OSError:
- pass
+ except OSError as e:
+ last_err = f"{e.strerror} (errno {e.errno})"
if _IN_FLATPAK and _host_cp(src, dst):
return
- raise OSError(f"Root Builder: failed to copy {src} -> {dst}")
+ raise OSError(f"Root Builder: failed to copy {src} -> {dst}: {last_err}")
def _ensure_writable(path: str):
diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt
index 699dfae..ab8b6c3 100644
--- a/src/src/CMakeLists.txt
+++ b/src/src/CMakeLists.txt
@@ -52,6 +52,10 @@ list(REMOVE_ITEM ORGANIZER_HEADERS
list(REMOVE_ITEM ORGANIZER_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/vfs/vfs_helper_main.cpp)
+# Process helper has its own main() — exclude from organizer
+list(REMOVE_ITEM ORGANIZER_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/process_helper_main.cpp)
+
# Remove WebEngine-dependent sources when not available
if(NOT Qt6WebEngineWidgets_FOUND)
list(REMOVE_ITEM ORGANIZER_SOURCES
@@ -153,6 +157,13 @@ if(NOT WIN32)
target_compile_definitions(mo2-vfs-helper PRIVATE FUSE_USE_VERSION=35)
target_compile_features(mo2-vfs-helper PRIVATE cxx_std_23)
+ # ── Standalone process helper for Flatpak game launching ──
+ # Keeps the flatpak-spawn proxy alive while monitoring the game process tree.
+ add_executable(mo2-process-helper
+ process_helper_main.cpp)
+ target_link_libraries(mo2-process-helper PRIVATE Threads::Threads)
+ target_compile_features(mo2-process-helper PRIVATE cxx_std_23)
+
option(MO2_BUNDLE_7Z_RUNTIME "Copy a Linux 7z module into organizer/dlls" ON)
option(MO2_STAGE_PYTHON_PLUGIN_PAYLOAD
"Stage shipped Python plugin payload into build plugins/ for Linux runs" ON)
diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp
index 9c1fa85..09aabb8 100644
--- a/src/src/commandline.cpp
+++ b/src/src/commandline.cpp
@@ -984,16 +984,16 @@ std::optional<int> CreatePortableCommand::runEarly()
}
}
- // Create empty profile files
- const QStringList profileFiles = {"modlist.txt", "plugins.txt", "loadorder.txt", "initweaks.ini"};
- for (const auto& f : profileFiles) {
- QFile file(QDir(instanceDir).filePath("profiles/Default/" + f));
- if (!file.open(QIODevice::WriteOnly)) {
- std::cerr << "Error: failed to create file: " << f.toStdString() << "\n";
- return 1;
- }
- file.close();
- }
+ // Create empty profile files
+ const QStringList profileFiles = {"modlist.txt", "plugins.txt", "loadorder.txt", "initweaks.ini"};
+ for (const auto& f : profileFiles) {
+ QFile file(QDir(instanceDir).filePath("profiles/Default/" + f));
+ if (!file.open(QIODevice::WriteOnly)) {
+ std::cerr << "Error: failed to create file: " << f.toStdString() << "\n";
+ return 1;
+ }
+ file.close();
+ }
// Generate ModOrganizer.ini
{
@@ -1067,7 +1067,7 @@ std::optional<int> ListInstancesCommand::runEarly()
const QStringList searchPaths = {
QDir::currentPath(),
QDir::homePath(),
- QDir(QDir::homePath()).filePath(".local/share/fluorine"),
+ QDir(QDir::homePath()).filePath(".var/app/com.fluorine.manager"),
};
bool found = false;
diff --git a/src/src/fluorinepaths.cpp b/src/src/fluorinepaths.cpp
index 815f5aa..98cb7aa 100644
--- a/src/src/fluorinepaths.cpp
+++ b/src/src/fluorinepaths.cpp
@@ -8,13 +8,11 @@
#include <cstdio>
static const QString OldRoot =
- QDir::homePath() + "/.var/app/com.fluorine.manager";
+ QDir::homePath() + "/.local/share/fluorine";
QString fluorineDataDir()
{
- // Use $HOME directly so this resolves the same path in both native
- // and Flatpak builds (the Flatpak has --filesystem=home).
- return QDir::homePath() + "/.local/share/fluorine";
+ return QDir::homePath() + "/.var/app/com.fluorine.manager";
}
void fluorineMigrateDataDir()
diff --git a/src/src/fluorinepaths.h b/src/src/fluorinepaths.h
index a32f6ec..ab7f119 100644
--- a/src/src/fluorinepaths.h
+++ b/src/src/fluorinepaths.h
@@ -3,13 +3,11 @@
#include <QString>
-/// Returns the shared Fluorine data directory: ~/.local/share/fluorine
-/// Uses $HOME directly to bypass Flatpak's XDG_DATA_HOME remapping
-/// (the Flatpak has --filesystem=home).
+/// Returns the Fluorine data directory: ~/.var/app/com.fluorine.manager
QString fluorineDataDir();
-/// One-time migration from the old ~/.var/app/com.fluorine.manager/ path
-/// to ~/.local/share/fluorine/. Call before initLogging().
+/// One-time migration from ~/.local/share/fluorine/ back to
+/// ~/.var/app/com.fluorine.manager/. Call before initLogging().
void fluorineMigrateDataDir();
#endif // FLUORINEPATHS_H
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index a276074..2c5454f 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -1,5 +1,6 @@
#include "fuseconnector.h"
+#include "fluorinepaths.h"
#include "settings.h"
#include "vfs/vfstree.h"
@@ -277,6 +278,9 @@ bool FuseConnector::mount(
std::error_code ec;
fs::create_directories(m_stagingDir, ec);
fs::create_directories(m_overwriteDir, ec);
+ if (!m_customOutputDir.empty()) {
+ fs::create_directories(m_customOutputDir, ec);
+ }
// Scan + cache base game files BEFORE mounting (after mount they're hidden).
// Reuse the cache across mount/unmount cycles since base game files don't
@@ -432,9 +436,8 @@ void FuseConnector::rebuild(
m_lastMods = mods;
if (m_helperProcess) {
- const QString dataDir =
- QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
- const QString configPath = QDir(dataDir).filePath("fluorine/vfs.cfg");
+ const QString dataDir = fluorineDataDir();
+ const QString configPath = QDir(dataDir).filePath("vfs.cfg");
writeVfsConfig(configPath, QString::fromStdString(m_mountPoint),
overwrite_dir, QString::fromStdString(m_gameDir),
data_dir_name, mods);
@@ -471,6 +474,26 @@ void FuseConnector::updateMapping(const MappingType& mapping)
auto mods = buildModsFromMapping(mapping, dataDirPath, overwriteDir);
+ // Check if any mapping has createTarget set — that mod directory should
+ // receive newly created files instead of the overwrite directory.
+ m_customOutputDir.clear();
+ for (const auto& map : mapping) {
+ if (map.createTarget) {
+ log::debug("Found createTarget mapping: source='{}', dest='{}', isDir={}",
+ map.source, map.destination, map.isDirectory);
+ }
+ if (map.createTarget && map.isDirectory) {
+ m_customOutputDir =
+ QDir::cleanPath(QDir::fromNativeSeparators(map.source)).toStdString();
+ log::debug("Custom output directory set to: {}",
+ QString::fromStdString(m_customOutputDir));
+ break;
+ }
+ }
+ if (m_customOutputDir.empty()) {
+ log::debug("No createTarget mapping found, using overwrite dir");
+ }
+
// Deploy non-data-dir mappings as real symlinks and collect file-level
// data-dir mappings for VFS tree injection.
deployExternalMappings(mapping, dataDirPath);
@@ -623,8 +646,17 @@ void FuseConnector::flushStaging()
}
const fs::path staging(m_stagingDir);
- const fs::path overwrite(m_overwriteDir);
+ const fs::path overwrite = m_customOutputDir.empty()
+ ? fs::path(m_overwriteDir)
+ : fs::path(m_customOutputDir);
+
+ log::debug("flushStaging: staging='{}', customOutput='{}', dest='{}'",
+ QString::fromStdString(m_stagingDir),
+ QString::fromStdString(m_customOutputDir),
+ QString::fromStdString(overwrite.string()));
+
if (!fs::exists(staging)) {
+ log::debug("flushStaging: staging dir does not exist, nothing to flush");
return;
}
@@ -757,11 +789,10 @@ bool FuseConnector::mountViaHelper(
const QString& data_dir_name,
const std::vector<std::pair<std::string, std::string>>& mods)
{
- const QString dataDir =
- QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
- const QString configPath = QDir(dataDir).filePath("fluorine/vfs.cfg");
+ const QString dataDir = fluorineDataDir();
+ const QString configPath = QDir(dataDir).filePath("vfs.cfg");
const QString helperBin =
- QDir(dataDir).filePath("fluorine/bin/mo2-vfs-helper");
+ QDir(dataDir).filePath("bin/mo2-vfs-helper");
if (!QFile::exists(helperBin)) {
throw FuseConnectorException(
@@ -824,6 +855,10 @@ void FuseConnector::writeVfsConfig(
out << "data_dir_name=" << data_dir_name << "\n";
out << "overwrite_dir=" << overwrite_dir << "\n";
+ if (!m_customOutputDir.empty()) {
+ out << "output_dir=" << QString::fromStdString(m_customOutputDir) << "\n";
+ }
+
for (const auto& [name, path] : mods) {
out << "mod=" << QString::fromStdString(name) << "|"
<< QString::fromStdString(path) << "\n";
diff --git a/src/src/fuseconnector.h b/src/src/fuseconnector.h
index fd5cb01..cf99d64 100644
--- a/src/src/fuseconnector.h
+++ b/src/src/fuseconnector.h
@@ -67,6 +67,7 @@ private:
std::string m_mountPoint;
std::string m_stagingDir;
std::string m_overwriteDir;
+ std::string m_customOutputDir;
std::string m_gameDir;
std::string m_dataDirName;
std::string m_dataDirPath;
diff --git a/src/src/instancemanager.cpp b/src/src/instancemanager.cpp
index 6620e3c..1269d26 100644
--- a/src/src/instancemanager.cpp
+++ b/src/src/instancemanager.cpp
@@ -802,9 +802,10 @@ std::unique_ptr<Instance> selectInstance()
PluginContainer pc(nullptr);
pc.loadPlugins();
- if (m.hasAnyInstances()) {
- // there is at least one instance available, show the instance manager
- // dialog
+ {
+ // show the instance manager dialog; it has both "Create new instance" and
+ // "Open existing portable" buttons, so it works whether or not instances
+ // already exist
InstanceManagerDialog dlg(pc);
// the dialog normally restarts MO when an instance is selected, but this
@@ -818,14 +819,6 @@ std::unique_ptr<Instance> selectInstance()
if (dlg.exec() != QDialog::Accepted) {
return {};
}
-
- } else {
- // no instances configured, ask the user to create one
- CreateInstanceDialog dlg(pc, nullptr);
-
- if (dlg.exec() != QDialog::Accepted) {
- return {};
- }
}
// return the new instance or the selection
diff --git a/src/src/instancemanagerdialog.cpp b/src/src/instancemanagerdialog.cpp
index d9a4a74..cd4b865 100644
--- a/src/src/instancemanagerdialog.cpp
+++ b/src/src/instancemanagerdialog.cpp
@@ -8,7 +8,9 @@
#include "shared/appconfig.h"
#include "shared/util.h"
#include "ui_instancemanagerdialog.h"
+#include <QFile>
#include <QFileDialog>
+#include <QSettings>
#include <QStandardPaths>
#include <iplugingame.h>
#include <report.h>
@@ -205,6 +207,9 @@ InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* paren
connect(ui->openINI, &QPushButton::clicked, [&] {
openINI();
});
+ connect(ui->removeFromList, &QPushButton::clicked, [&] {
+ removeFromList();
+ });
connect(ui->deleteInstance, &QPushButton::clicked, [&] {
deleteInstance();
});
@@ -522,6 +527,45 @@ void InstanceManagerDialog::openINI()
}
}
+void InstanceManagerDialog::removeFromList()
+{
+ const auto* i = singleSelection();
+ if (!i) {
+ return;
+ }
+
+ auto& m = InstanceManager::singleton();
+ if (i->isActive()) {
+ QMessageBox::information(this, tr("Remove from list"),
+ tr("The active instance cannot be removed."));
+ return;
+ }
+
+ const auto r = QMessageBox::question(
+ this, tr("Remove from list"),
+ tr("Remove \"%1\" from the instance list?\n\n"
+ "No files will be deleted.")
+ .arg(i->displayName()),
+ QMessageBox::Yes | QMessageBox::Cancel);
+
+ if (r != QMessageBox::Yes) {
+ return;
+ }
+
+ if (i->isPortable()) {
+ m.unregisterPortableInstance(i->directory());
+ } else {
+ // for global instances, rename the INI so it's no longer auto-discovered
+ const QString ini = i->iniPath();
+ if (!ini.isEmpty() && QFile::exists(ini)) {
+ QFile::rename(ini, ini + ".disabled");
+ }
+ }
+
+ updateInstances();
+ updateList();
+}
+
void InstanceManagerDialog::deleteInstance()
{
const auto* i = singleSelection();
@@ -538,7 +582,6 @@ void InstanceManagerDialog::deleteInstance()
// creating dialog
- const auto Recycle = QMessageBox::Save;
const auto Delete = QMessageBox::Yes;
const auto Cancel = QMessageBox::Cancel;
@@ -547,10 +590,9 @@ void InstanceManagerDialog::deleteInstance()
MOBase::TaskDialog dlg(this);
dlg.title(tr("Deleting instance"))
- .main(tr("These files and folders will be deleted"))
+ .main(tr("These files and folders will be permanently deleted"))
.content(tr("All checked items will be deleted."))
.icon(QMessageBox::Warning)
- .button({tr("Move to the recycle bin"), Recycle})
.button({tr("Delete permanently"), Delete})
.button({tr("Cancel"), Cancel});
@@ -584,7 +626,7 @@ void InstanceManagerDialog::deleteInstance()
const auto r = dlg.exec();
- if (r != Recycle && r != Delete) {
+ if (r != Delete) {
return;
}
@@ -604,7 +646,7 @@ void InstanceManagerDialog::deleteInstance()
}
// deleting
- if (!doDelete(selected, (r == Recycle))) {
+ if (!doDelete(selected, false)) {
return;
}
@@ -690,37 +732,37 @@ void InstanceManagerDialog::createNew()
select(dlg.creationInfo().instanceName);
}
-std::size_t InstanceManagerDialog::singleSelectionIndex() const
-{
- const auto sel =
- m_filter.mapSelectionToSource(ui->list->selectionModel()->selection());
-
- if (sel.size() != 1) {
- return NoSelection;
- }
-
- const auto indexes = sel.indexes();
- if (indexes.size() != 1 || !indexes[0].isValid()) {
- return NoSelection;
- }
-
- const int row = indexes[0].row();
- if (row < 0 || static_cast<std::size_t>(row) >= m_instances.size()) {
- return NoSelection;
- }
-
- return static_cast<std::size_t>(row);
-}
-
-const Instance* InstanceManagerDialog::singleSelection() const
-{
- const auto i = singleSelectionIndex();
- if (i == NoSelection || i >= m_instances.size()) {
- return nullptr;
- }
-
- return m_instances[i].get();
-}
+std::size_t InstanceManagerDialog::singleSelectionIndex() const
+{
+ const auto sel =
+ m_filter.mapSelectionToSource(ui->list->selectionModel()->selection());
+
+ if (sel.size() != 1) {
+ return NoSelection;
+ }
+
+ const auto indexes = sel.indexes();
+ if (indexes.size() != 1 || !indexes[0].isValid()) {
+ return NoSelection;
+ }
+
+ const int row = indexes[0].row();
+ if (row < 0 || static_cast<std::size_t>(row) >= m_instances.size()) {
+ return NoSelection;
+ }
+
+ return static_cast<std::size_t>(row);
+}
+
+const Instance* InstanceManagerDialog::singleSelection() const
+{
+ const auto i = singleSelectionIndex();
+ if (i == NoSelection || i >= m_instances.size()) {
+ return nullptr;
+ }
+
+ return m_instances[i].get();
+}
void InstanceManagerDialog::fillData(const Instance& ii)
{
@@ -729,6 +771,20 @@ void InstanceManagerDialog::fillData(const Instance& ii)
ui->baseDirectory->setText(ii.baseDirectory());
ui->gameName->setText(ii.gameName());
ui->gameDir->setText(ii.gameDirectory());
+
+ // read prefix info from the instance's INI
+ {
+ const QString ini = ii.iniPath();
+ if (!ini.isEmpty() && QFile::exists(ini)) {
+ QSettings s(ini, QSettings::IniFormat);
+ ui->prefixPath->setText(s.value("Settings/proton_prefix_path").toString());
+ ui->protonVersion->setText(s.value("fluorine/proton_name").toString());
+ } else {
+ ui->prefixPath->clear();
+ ui->protonVersion->clear();
+ }
+ }
+
setButtonsEnabled(true);
const auto& m = InstanceManager::singleton();
@@ -764,6 +820,8 @@ void InstanceManagerDialog::clearData()
ui->baseDirectory->clear();
ui->gameName->clear();
ui->gameDir->clear();
+ ui->prefixPath->clear();
+ ui->protonVersion->clear();
setButtonsEnabled(false);
@@ -779,6 +837,7 @@ void InstanceManagerDialog::setButtonsEnabled(bool b)
ui->exploreGame->setEnabled(b);
ui->convertToPortable->setEnabled(b);
ui->convertToGlobal->setEnabled(b);
+ ui->removeFromList->setEnabled(b);
ui->deleteInstance->setEnabled(b);
ui->switchToInstance->setEnabled(b);
}
diff --git a/src/src/instancemanagerdialog.h b/src/src/instancemanagerdialog.h
index 4df9cdc..c8862fd 100644
--- a/src/src/instancemanagerdialog.h
+++ b/src/src/instancemanagerdialog.h
@@ -68,6 +68,10 @@ public:
//
void openINI();
+ // removes the selected instance from the list without deleting files
+ //
+ void removeFromList();
+
// deletes the selected instance
//
void deleteInstance();
diff --git a/src/src/instancemanagerdialog.ui b/src/src/instancemanagerdialog.ui
index eab814c..2b94a8d 100644
--- a/src/src/instancemanagerdialog.ui
+++ b/src/src/instancemanagerdialog.ui
@@ -286,6 +286,34 @@
</property>
</widget>
</item>
+ <item row="8" column="0">
+ <widget class="QLabel" name="label_prefix">
+ <property name="text">
+ <string>Prefix</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="1">
+ <widget class="QLineEdit" name="prefixPath">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="0">
+ <widget class="QLabel" name="label_proton">
+ <property name="text">
+ <string>Proton</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="1">
+ <widget class="QLineEdit" name="protonVersion">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -339,6 +367,16 @@
</widget>
</item>
<item>
+ <widget class="QPushButton" name="removeFromList">
+ <property name="text">
+ <string>Remove from list</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove this instance from the list without deleting any files.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QPushButton" name="deleteInstance">
<property name="text">
<string>Delete instance...</string>
diff --git a/src/src/loglist.cpp b/src/src/loglist.cpp
index 15299f5..8ed28f1 100644
--- a/src/src/loglist.cpp
+++ b/src/src/loglist.cpp
@@ -406,7 +406,7 @@ bool createAndMakeWritable(const std::wstring& subPath)
bool setLogDirectory(const QString& dir)
{
#ifndef _WIN32
- // On Linux, all logs go to ~/.local/share/fluorine/logs/
+ // On Linux, all logs go to ~/.var/app/com.fluorine.manager/logs/
const QString logDir = fluorineDataDir() + "/logs";
QDir().mkpath(logDir);
const auto logFile = logDir + "/" +
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp
index c0be6fe..425d20c 100644
--- a/src/src/mainwindow.cpp
+++ b/src/src/mainwindow.cpp
@@ -83,6 +83,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <uibase/versioninfo.h>
#ifdef _WIN32
#include <usvfs/usvfs.h>
+#else
+#include "fluorinepaths.h"
#endif
#include "directoryrefresher.h"
@@ -2649,8 +2651,21 @@ void MainWindow::openPluginsFolder()
void MainWindow::openStylesheetsFolder()
{
+#ifndef _WIN32
+ // On Linux, open the instance's stylesheets directory (where custom themes
+ // from modlists live), or the user data dir as fallback.
+ QString ssPath;
+ if (auto ci = InstanceManager::singleton().currentInstance()) {
+ ssPath = ci->directory() + "/" +
+ QString::fromStdWString(AppConfig::stylesheetsPath());
+ } else {
+ ssPath = fluorineDataDir() + "/stylesheets";
+ }
+ QDir().mkpath(ssPath);
+#else
QString ssPath = AppConfig::basePath() + "/" +
ToQString(AppConfig::stylesheetsPath());
+#endif
shell::Explore(ssPath);
}
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp
index b77570e..29e21c4 100644
--- a/src/src/moapplication.cpp
+++ b/src/src/moapplication.cpp
@@ -31,6 +31,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "settings.h"
#ifndef _WIN32
#include "fluorineconfig.h"
+#include "fluorinepaths.h"
#include "fuseconnector.h"
#include "wineprefix.h"
#include <cerrno>
@@ -675,13 +676,43 @@ bool MOApplication::setStyleFile(const QString& styleName)
}
// set new stylesheet or clear it
if (styleName.length() != 0) {
- QString styleSheetName = applicationDirPath() + "/" +
- MOBase::ToQString(AppConfig::stylesheetsPath()) + "/" +
- styleName;
- if (QFile::exists(styleSheetName)) {
- m_styleWatcher.addPath(styleSheetName);
- updateStyle(styleSheetName);
+ // Search for the stylesheet in multiple locations:
+ // 1. applicationDirPath()/stylesheets/ — bundled themes
+ // 2. instance baseDir/stylesheets/ — instance/portable themes (modlists)
+ // 3. fluorineDataDir()/stylesheets/ — user-installed custom themes
+ const QString ssSubdir = MOBase::ToQString(AppConfig::stylesheetsPath());
+ QStringList searchDirs;
+ searchDirs << applicationDirPath() + "/" + ssSubdir;
+#ifndef _WIN32
+ if (m_instance) {
+ // Prefer baseDirectory() (populated after readFromIni), fall back to
+ // directory() which is always set by the constructor.
+ QString base = m_instance->baseDirectory();
+ if (base.isEmpty())
+ base = m_instance->directory();
+ const QString instanceDir = base + "/" + ssSubdir;
+ if (!searchDirs.contains(instanceDir))
+ searchDirs << instanceDir;
+ }
+ const QString userDir = fluorineDataDir() + "/stylesheets";
+ if (!searchDirs.contains(userDir))
+ searchDirs << userDir;
+#endif
+
+ QString resolved;
+ for (const auto& dir : searchDirs) {
+ QString candidate = dir + "/" + styleName;
+ if (QFile::exists(candidate)) {
+ resolved = candidate;
+ break;
+ }
+ }
+
+ if (!resolved.isEmpty()) {
+ m_styleWatcher.addPath(resolved);
+ updateStyle(resolved);
} else {
+ // Could be a built-in Qt style name (e.g. "Fusion")
updateStyle(styleName);
}
} else {
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index e0e32cb..dd1b33b 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -126,14 +126,40 @@ QString resolveWinePrefixPath(const Settings& settings,
QString resolveWineDataDirName(const IPluginGame* managedGame)
{
- if (managedGame != nullptr) {
- const QString docsLeaf = managedGame->documentsDirectory().dirName().trimmed();
- if (!docsLeaf.isEmpty()) {
+ if (managedGame == nullptr) {
+ return {};
+ }
+
+ // Primary: the My Games subfolder name matches the AppData/Local folder
+ // for almost every Bethesda game.
+ const QDir docsDir = managedGame->documentsDirectory();
+ if (docsDir.exists()) {
+ const QString docsLeaf = docsDir.dirName().trimmed();
+ if (!docsLeaf.isEmpty() && docsLeaf != QStringLiteral(".")) {
return docsLeaf;
}
- return managedGame->gameName();
}
- return {};
+
+ // Fallback: gameShortName is used by the base Gamebryo mappings() for
+ // the AppData/Local folder and matches for most games.
+ const QString shortName = managedGame->gameShortName();
+ if (!shortName.isEmpty()) {
+ log::warn("resolveWineDataDirName: documentsDirectory() is empty or "
+ "invalid, falling back to gameShortName '{}'",
+ shortName);
+ return shortName;
+ }
+
+ log::warn("resolveWineDataDirName: both documentsDirectory() and "
+ "gameShortName() are empty, falling back to gameName '{}'",
+ managedGame->gameName());
+ return managedGame->gameName();
+}
+
+QString resolvePrefixGameDocumentsDir(const WinePrefix& prefix,
+ const QString& dataDirName)
+{
+ return QDir(prefix.myGamesPath()).filePath(dataDirName);
}
QString resolveSaveRelativePath(std::shared_ptr<Profile> profile,
@@ -2166,20 +2192,29 @@ bool OrganizerCore::beforeRun(
pluginsFile.close();
if (!plugins.isEmpty()) {
- prefix.deployPlugins(plugins, dataDirName);
- log::debug("Deployed {} plugins to prefix '{}'", plugins.size(),
- prefixPathStr);
+ if (prefix.deployPlugins(plugins, dataDirName)) {
+ log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')",
+ plugins.size(), prefixPathStr, dataDirName);
+ } else {
+ log::error("Failed to deploy {} plugins to prefix '{}' "
+ "(dataDirName='{}')",
+ plugins.size(), prefixPathStr, dataDirName);
+ }
+ } else {
+ log::warn("Profile plugins.txt is empty or contains only comments, "
+ "skipping plugin deployment");
}
}
if (m_CurrentProfile->localSettingsEnabled()) {
+ const QString targetIniBase =
+ resolvePrefixGameDocumentsDir(prefix, dataDirName);
int deployedIniCount = 0;
for (const QString& iniFile : managedGame()->iniFiles()) {
const QString sourceIni =
m_CurrentProfile->absoluteIniFilePath(iniFile);
- const QString targetIni = MOBase::resolveFileCaseInsensitive(
- QFileInfo(managedGame()->documentsDirectory(), iniFile)
- .absoluteFilePath());
+ const QString targetIni =
+ QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName());
log::debug("INI deploy check: source='{}' exists={}, target='{}'",
sourceIni, QFileInfo::exists(sourceIni), targetIni);
if (QFileInfo::exists(sourceIni) &&
@@ -2265,13 +2300,14 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode)
}
if (m_CurrentProfile->localSettingsEnabled()) {
+ const QString targetIniBase =
+ resolvePrefixGameDocumentsDir(prefix, dataDirName);
QList<QPair<QString, QString>> iniMappings;
for (const QString& iniFile : managedGame()->iniFiles()) {
const QString profileIni =
m_CurrentProfile->absoluteIniFilePath(iniFile);
- const QString targetIni = MOBase::resolveFileCaseInsensitive(
- QFileInfo(managedGame()->documentsDirectory(), iniFile)
- .absoluteFilePath());
+ const QString targetIni =
+ QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName());
iniMappings.append({profileIni, targetIni});
log::debug("Sync profile INI '{}' <- '{}'", profileIni, targetIni);
}
diff --git a/src/src/process_helper_main.cpp b/src/src/process_helper_main.cpp
new file mode 100644
index 0000000..185111c
--- /dev/null
+++ b/src/src/process_helper_main.cpp
@@ -0,0 +1,359 @@
+// Standalone process helper for Flatpak game launching.
+// Runs on the host via flatpak-spawn --host, keeping the flatpak-spawn
+// proxy alive for MO2's PID polling while the game process tree is running.
+//
+// Protocol (stdin/stdout, line-oriented):
+// Config phase: MO2 writes key=value lines terminated by a blank line
+// program=<path>, arg=<value> (repeatable), env=KEY=VALUE (repeatable),
+// workdir=<path>
+// Helper responds: "started <pid>" or "error <message>"
+// Runtime commands (MO2→helper): "kill" (SIGTERM child tree), "quit"
+// Helper reports: "exited <code>" when game process tree exits
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+// ── Helpers ──
+
+static void writeResponse(const std::string& msg)
+{
+ std::string line = msg + "\n";
+ ::write(STDOUT_FILENO, line.data(), line.size());
+}
+
+static bool readLine(std::string& out, int timeoutMs)
+{
+ out.clear();
+
+ struct pollfd pfd{};
+ pfd.fd = STDIN_FILENO;
+ pfd.events = POLLIN;
+
+ while (true) {
+ int ret = ::poll(&pfd, 1, timeoutMs);
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ if (ret == 0) {
+ // timeout
+ return false;
+ }
+
+ // Try reading if data is available, even if HUP is also set
+ // (pipe can have buffered data when the writer closes).
+ if (!(pfd.revents & POLLIN)) {
+ // No data available — must be HUP or ERR only
+ return false;
+ }
+
+ char ch = 0;
+ ssize_t n = ::read(STDIN_FILENO, &ch, 1);
+ if (n <= 0) {
+ return false;
+ }
+ if (ch == '\n') {
+ return true;
+ }
+ out.push_back(ch);
+ }
+}
+
+// Collect all descendant PIDs of a given root by scanning /proc.
+static std::unordered_set<pid_t> collectDescendants(pid_t root)
+{
+ std::unordered_set<pid_t> descendants;
+
+ // Build parent→children map
+ struct ProcEntry {
+ pid_t pid;
+ pid_t ppid;
+ };
+ std::vector<ProcEntry> entries;
+
+ DIR* proc = opendir("/proc");
+ if (!proc) {
+ return descendants;
+ }
+
+ struct dirent* entry = nullptr;
+ while ((entry = readdir(proc)) != nullptr) {
+ if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) {
+ continue;
+ }
+
+ char* end = nullptr;
+ long pidLong = strtol(entry->d_name, &end, 10);
+ if (end == entry->d_name || *end != '\0' || pidLong <= 0) {
+ continue;
+ }
+
+ pid_t pid = static_cast<pid_t>(pidLong);
+
+ char statusPath[64];
+ snprintf(statusPath, sizeof(statusPath), "/proc/%ld/status", pidLong);
+
+ std::ifstream status(statusPath);
+ if (!status.is_open()) {
+ continue;
+ }
+
+ std::string line;
+ pid_t ppid = 0;
+ while (std::getline(status, line)) {
+ if (line.rfind("PPid:", 0) == 0) {
+ ppid = static_cast<pid_t>(strtol(line.c_str() + 5, nullptr, 10));
+ break;
+ }
+ }
+
+ if (ppid > 0) {
+ entries.push_back({pid, ppid});
+ }
+ }
+ closedir(proc);
+
+ // BFS from root
+ std::vector<pid_t> queue;
+ queue.push_back(root);
+
+ while (!queue.empty()) {
+ pid_t cur = queue.back();
+ queue.pop_back();
+
+ for (const auto& e : entries) {
+ if (e.ppid == cur && descendants.find(e.pid) == descendants.end()) {
+ descendants.insert(e.pid);
+ queue.push_back(e.pid);
+ }
+ }
+ }
+
+ return descendants;
+}
+
+// Check if any process in the given set is still alive.
+static bool anyAlive(const std::unordered_set<pid_t>& pids)
+{
+ for (pid_t p : pids) {
+ if (::kill(p, 0) == 0 || errno == EPERM) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// ── Main ──
+
+int main()
+{
+ // Make stdout line-buffered for reliable protocol messages.
+ setvbuf(stdout, nullptr, _IOLBF, 0);
+
+ // ── Config phase: read key=value lines until blank line ──
+ std::string program;
+ std::vector<std::string> args;
+ std::vector<std::string> envVars; // "KEY=VALUE"
+ std::string workdir;
+
+ while (true) {
+ std::string line;
+ if (!readLine(line, 30000)) {
+ writeResponse("error stdin closed or timeout during config");
+ return 1;
+ }
+
+ if (line.empty()) {
+ break; // blank line = end of config
+ }
+
+ auto eq = line.find('=');
+ if (eq == std::string::npos) {
+ continue;
+ }
+
+ std::string key = line.substr(0, eq);
+ std::string val = line.substr(eq + 1);
+
+ if (key == "program") {
+ program = val;
+ } else if (key == "arg") {
+ args.push_back(val);
+ } else if (key == "env") {
+ envVars.push_back(val);
+ } else if (key == "workdir") {
+ workdir = val;
+ }
+ }
+
+ if (program.empty()) {
+ writeResponse("error no program specified");
+ return 1;
+ }
+
+ // ── Pipe for exec error reporting ──
+ // Child writes errno to this pipe if execvp fails; parent reads it.
+ int errPipe[2];
+ if (::pipe2(errPipe, O_CLOEXEC) != 0) {
+ writeResponse("error pipe2 failed: " + std::string(strerror(errno)));
+ return 1;
+ }
+
+ // ── Fork ──
+ pid_t child = ::fork();
+ if (child < 0) {
+ writeResponse("error fork failed: " + std::string(strerror(errno)));
+ return 1;
+ }
+
+ if (child == 0) {
+ // ── Child ──
+ ::close(errPipe[0]); // close read end
+
+ // New session so we can kill the whole process group later.
+ ::setsid();
+
+ // Set environment variables.
+ for (const auto& ev : envVars) {
+ ::putenv(const_cast<char*>(ev.c_str()));
+ }
+
+ // Change working directory.
+ if (!workdir.empty()) {
+ if (::chdir(workdir.c_str()) != 0) {
+ int err = errno;
+ (void)::write(errPipe[1], &err, sizeof(err));
+ ::_exit(127);
+ }
+ }
+
+ // Build argv for execvp.
+ std::vector<const char*> argv;
+ argv.push_back(program.c_str());
+ for (const auto& a : args) {
+ argv.push_back(a.c_str());
+ }
+ argv.push_back(nullptr);
+
+ ::execvp(program.c_str(), const_cast<char* const*>(argv.data()));
+
+ // If we get here, exec failed.
+ int err = errno;
+ (void)::write(errPipe[1], &err, sizeof(err));
+ ::_exit(127);
+ }
+
+ // ── Parent ──
+ ::close(errPipe[1]); // close write end
+
+ // Check if exec succeeded (pipe closes on successful exec due to O_CLOEXEC).
+ int execErr = 0;
+ ssize_t n = ::read(errPipe[0], &execErr, sizeof(execErr));
+ ::close(errPipe[0]);
+
+ if (n > 0) {
+ // exec failed in child
+ ::waitpid(child, nullptr, 0);
+ writeResponse("error exec failed: " + std::string(strerror(execErr)));
+ return 1;
+ }
+
+ // Success - report PID.
+ writeResponse("started " + std::to_string(child));
+
+ // ── Monitor loop ──
+ // Wait for the direct child and then monitor descendants (handles Proton
+ // chain: proton → wine → game.exe).
+ bool childExited = false;
+ int childStatus = 0;
+ bool quit = false;
+
+ while (!quit) {
+ // Check for commands on stdin.
+ std::string cmd;
+ // Non-blocking: if readLine returns false due to timeout, that's fine.
+ // If it returns false due to pipe close, MO2 crashed — kill child group.
+ struct pollfd pfd{};
+ pfd.fd = STDIN_FILENO;
+ pfd.events = POLLIN;
+
+ int pollRet = ::poll(&pfd, 1, 200);
+
+ if (pollRet > 0) {
+ if (pfd.revents & (POLLHUP | POLLERR)) {
+ // MO2 crashed or closed pipe — kill child group and exit.
+ ::kill(-child, SIGTERM);
+ break;
+ }
+
+ if (pfd.revents & POLLIN) {
+ if (readLine(cmd, 0)) {
+ if (cmd == "kill") {
+ ::kill(-child, SIGTERM);
+ } else if (cmd == "quit") {
+ quit = true;
+ break;
+ }
+ } else {
+ // read failed = pipe closed
+ ::kill(-child, SIGTERM);
+ break;
+ }
+ }
+ }
+
+ // Check child status.
+ if (!childExited) {
+ int status = 0;
+ pid_t ret = ::waitpid(child, &status, WNOHANG);
+ if (ret == child) {
+ childExited = true;
+ childStatus = status;
+ } else if (ret < 0 && errno != EINTR) {
+ // Child somehow lost
+ childExited = true;
+ childStatus = 0;
+ }
+ }
+
+ if (childExited) {
+ // Check for surviving descendants (e.g., game.exe still running
+ // after the proton wrapper exits).
+ auto desc = collectDescendants(child);
+ if (desc.empty() || !anyAlive(desc)) {
+ // All done.
+ int exitCode = 0;
+ if (WIFEXITED(childStatus)) {
+ exitCode = WEXITSTATUS(childStatus);
+ } else if (WIFSIGNALED(childStatus)) {
+ exitCode = 128 + WTERMSIG(childStatus);
+ }
+ writeResponse("exited " + std::to_string(exitCode));
+ return 0;
+ }
+ // Descendants still alive, keep monitoring.
+ }
+ }
+
+ // If we broke out of the loop, reap child if needed.
+ if (!childExited) {
+ ::waitpid(child, nullptr, 0);
+ }
+
+ writeResponse("exited 0");
+ return 0;
+}
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp
index a6a37ca..ac3c9eb 100644
--- a/src/src/processrunner.cpp
+++ b/src/src/processrunner.cpp
@@ -1175,6 +1175,7 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
#ifdef _WIN32
m_handle.reset(startBinary(parent, m_sp));
#else
+ m_sp.helperProcessOut = &m_processHelper;
m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(startBinary(parent, m_sp))));
#endif
if (m_handle.get() == INVALID_HANDLE_VALUE) {
@@ -1264,7 +1265,12 @@ ProcessRunner::Results ProcessRunner::postRun()
const QFileInfo binary = m_sp.binary;
QPointer<OrganizerCore> core = &m_core;
- std::thread([core, binary, pid]() {
+ QProcess* helper = m_processHelper;
+ m_processHelper = nullptr;
+
+ std::thread([core, binary, pid, helper]() {
+ // For detached processes (including flatpak-spawn helper),
+ // waitpid will fail with ECHILD. Use kill(0) polling instead.
int status = 0;
pid_t waited = -1;
do {
@@ -1278,11 +1284,26 @@ ProcessRunner::Results ProcessRunner::postRun()
} else if (WIFSIGNALED(status)) {
exitCode = static_cast<DWORD>(128 + WTERMSIG(status));
}
+ } else if (errno == ECHILD) {
+ // Detached process — poll with kill(0) until gone.
+ while (::kill(pid, 0) == 0 || errno == EPERM) {
+ usleep(200000); // 200ms
+ }
} else {
MOBase::log::warn("process runner: waitpid failed for pid {}: {}", pid,
errno);
}
+ // Clean up helper QProcess on the main thread.
+ if (helper) {
+ QMetaObject::invokeMethod(
+ QCoreApplication::instance(), [helper]() {
+ helper->waitForFinished(1000);
+ delete helper;
+ },
+ Qt::QueuedConnection);
+ }
+
if (!core) {
return;
}
@@ -1331,6 +1352,15 @@ ProcessRunner::Results ProcessRunner::postRun()
});
}
+#ifndef _WIN32
+ // Clean up the process helper (keeps flatpak-spawn alive during game).
+ if (m_processHelper) {
+ m_processHelper->waitForFinished(1000);
+ delete m_processHelper;
+ m_processHelper = nullptr;
+ }
+#endif
+
if (shouldRefresh(r)) {
QEventLoop loop;
const bool wait = m_waitFlags.testFlag(WaitForRefresh);
diff --git a/src/src/processrunner.h b/src/src/processrunner.h
index 2736c86..e825e98 100644
--- a/src/src/processrunner.h
+++ b/src/src/processrunner.h
@@ -185,6 +185,9 @@ private:
QFileInfo m_shellOpen;
env::HandlePtr m_handle;
DWORD m_exitCode;
+#ifndef _WIN32
+ QProcess* m_processHelper = nullptr;
+#endif
bool shouldRunShell() const;
bool shouldRefresh(Results r) const;
diff --git a/src/src/profile.cpp b/src/src/profile.cpp
index 0060fa1..5489fc9 100644
--- a/src/src/profile.cpp
+++ b/src/src/profile.cpp
@@ -825,20 +825,33 @@ void Profile::mergeTweak(const QString& tweakName, const QString& tweakedIni) co
}
}
#else
- // On Linux, use QSettings to read/merge INI tweaks
- QSettings source(tweakName, QSettings::IniFormat);
- QSettings dest(tweakedIni, QSettings::IniFormat);
-
- for (const QString& group : source.childGroups()) {
- source.beginGroup(group);
- dest.beginGroup(group);
+ // On Linux, parse the tweak INI file line-by-line and merge each
+ // key=value into the destination using WriteRegistryValue (which uses
+ // the safe line-by-line writer that does NOT interpret backslashes as
+ // line continuations or URL-encode spaces in keys).
+ QFile sourceFile(tweakName);
+ if (!sourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ log::warn("mergeTweak: could not open tweak file '{}'", tweakName);
+ return;
+ }
- for (const QString& key : source.childKeys()) {
- dest.setValue(key, source.value(key));
+ QString currentSection;
+ QTextStream stream(&sourceFile);
+ while (!stream.atEnd()) {
+ QString line = stream.readLine().trimmed();
+ if (line.isEmpty() || line.startsWith(';') || line.startsWith('#')) {
+ continue;
+ }
+ if (line.startsWith('[') && line.endsWith(']')) {
+ currentSection = line.mid(1, line.length() - 2).trimmed();
+ continue;
+ }
+ const int eqPos = line.indexOf('=');
+ if (eqPos > 0 && !currentSection.isEmpty()) {
+ const QString key = line.left(eqPos).trimmed();
+ const QString value = line.mid(eqPos + 1).trimmed();
+ MOBase::WriteRegistryValue(currentSection, key, value, tweakedIni);
}
-
- source.endGroup();
- dest.endGroup();
}
#endif
}
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp
index 6497fff..a2aa042 100644
--- a/src/src/protonlauncher.cpp
+++ b/src/src/protonlauncher.cpp
@@ -267,6 +267,12 @@ ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& val
return *this;
}
+ProtonLauncher& ProtonLauncher::setHelperProcessOut(QProcess** out)
+{
+ m_helperProcessOut = out;
+ return *this;
+}
+
std::pair<bool, qint64> ProtonLauncher::launch() const
{
qint64 pid = -1;
@@ -349,9 +355,13 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
}
}
- maybeWrapForFlatpak(program, arguments, env);
-
MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary);
+
+ if (isFlatpak()) {
+ return launchViaProcessHelper(program, arguments, env, m_workingDir, pid);
+ }
+
+ maybeWrapForFlatpak(program, arguments, env);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
@@ -485,8 +495,6 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
}
}
- maybeWrapForFlatpak(program, arguments, env);
-
MOBase::log::info("UMU launch: '{}' '{}' (game id: {}, steam: '{}')", umuRun,
m_binary,
(effectiveSteamAppId == 0
@@ -494,6 +502,12 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
: QStringLiteral("umu-") +
QString::number(effectiveSteamAppId)),
steamPath);
+
+ if (isFlatpak()) {
+ return launchViaProcessHelper(program, arguments, env, m_workingDir, pid);
+ }
+
+ maybeWrapForFlatpak(program, arguments, env);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
@@ -517,11 +531,105 @@ bool ProtonLauncher::launchDirect(qint64& pid) const
env.insert(it.key(), it.value());
}
- maybeWrapForFlatpak(program, arguments, env);
+ if (isFlatpak()) {
+ return launchViaProcessHelper(program, arguments, env, m_workingDir, pid);
+ }
+ maybeWrapForFlatpak(program, arguments, env);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
+bool ProtonLauncher::launchViaProcessHelper(
+ const QString& program, const QStringList& arguments,
+ const QProcessEnvironment& env, const QString& workingDir,
+ qint64& pid) const
+{
+ const QString helperBin = fluorineDataDir() + QStringLiteral("/bin/mo2-process-helper");
+ if (!QFileInfo::exists(helperBin)) {
+ MOBase::log::warn("mo2-process-helper not found at '{}', falling back to "
+ "flatpak-spawn direct launch", helperBin);
+ // Fall back to old direct flatpak-spawn path.
+ QString prog = program;
+ QStringList args = arguments;
+ QProcessEnvironment envCopy = env;
+ maybeWrapForFlatpak(prog, args, envCopy);
+ return startDetachedWithEnv(prog, args, workingDir, envCopy, pid);
+ }
+
+ auto* proc = new QProcess();
+ proc->setProcessChannelMode(QProcess::SeparateChannels);
+ proc->start(QStringLiteral("flatpak-spawn"),
+ {QStringLiteral("--host"), helperBin});
+
+ if (!proc->waitForStarted(5000)) {
+ MOBase::log::error("Failed to start flatpak-spawn for process helper");
+ delete proc;
+ return false;
+ }
+
+ // Write config block to helper's stdin.
+ auto writeLine = [&](const QString& line) {
+ proc->write(line.toUtf8());
+ proc->write("\n");
+ };
+
+ writeLine(QStringLiteral("program=") + program);
+ for (const QString& arg : arguments) {
+ writeLine(QStringLiteral("arg=") + arg);
+ }
+
+ // Send env vars that differ from system environment.
+ const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment();
+ for (const QString& key : env.keys()) {
+ const QString val = env.value(key);
+ if (val != sysEnv.value(key)) {
+ writeLine(QStringLiteral("env=") + key + QStringLiteral("=") + val);
+ }
+ }
+
+ if (!workingDir.isEmpty()) {
+ writeLine(QStringLiteral("workdir=") + workingDir);
+ }
+
+ // Blank line terminates config.
+ proc->write("\n");
+ proc->waitForBytesWritten(2000);
+
+ // Read response: "started <pid>" or "error <message>"
+ if (!proc->waitForReadyRead(10000)) {
+ MOBase::log::error("Process helper did not respond in time");
+ proc->kill();
+ proc->waitForFinished(2000);
+ delete proc;
+ return false;
+ }
+
+ const QString response = QString::fromUtf8(proc->readLine()).trimmed();
+ if (response.startsWith(QStringLiteral("started "))) {
+ MOBase::log::info("Process helper: {}", response);
+ } else {
+ MOBase::log::error("Process helper error: {}", response);
+ proc->kill();
+ proc->waitForFinished(2000);
+ delete proc;
+ return false;
+ }
+
+ // Use the flatpak-spawn PID for kill(pid,0) polling.
+ pid = proc->processId();
+
+ // Store the QProcess so it stays alive (keeping flatpak-spawn alive).
+ if (m_helperProcessOut) {
+ *m_helperProcessOut = proc;
+ } else {
+ // No owner provided — leak intentionally to keep the pipe alive.
+ // The process will clean up when the game exits.
+ MOBase::log::debug("No helper process owner set, helper will self-manage");
+ }
+
+ return true;
+}
+
bool ProtonLauncher::ensureSteamRunning()
{
QProcess pgrep;
diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h
index 95d22d1..d0213f6 100644
--- a/src/src/protonlauncher.h
+++ b/src/src/protonlauncher.h
@@ -2,11 +2,14 @@
#define PROTONLAUNCHER_H
#include <QMap>
+#include <QProcessEnvironment>
#include <QString>
#include <QStringList>
#include <cstdint>
#include <utility>
+class QProcess;
+
class ProtonLauncher
{
public:
@@ -23,6 +26,7 @@ public:
ProtonLauncher& setPreferSystemUmu(bool preferSystemUmu);
ProtonLauncher& setUseSteamRun(bool useSteamRun);
ProtonLauncher& addEnvVar(const QString& key, const QString& value);
+ ProtonLauncher& setHelperProcessOut(QProcess** out);
// Launch dispatch: UMU -> Proton -> Direct
std::pair<bool, qint64> launch() const;
@@ -31,6 +35,9 @@ private:
bool launchWithProton(qint64& pid) const;
bool launchWithUmu(qint64& pid) const;
bool launchDirect(qint64& pid) const;
+ bool launchViaProcessHelper(const QString& program, const QStringList& arguments,
+ const QProcessEnvironment& env,
+ const QString& workingDir, qint64& pid) const;
static bool ensureSteamRunning();
QString m_binary;
@@ -45,6 +52,7 @@ private:
bool m_useSteamRun;
QMap<QString, QString> m_envVars;
QMap<QString, QString> m_wrapperEnvVars;
+ QProcess** m_helperProcessOut = nullptr;
};
#endif // PROTONLAUNCHER_H
diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui
index 7755f6f..9e5c44d 100644
--- a/src/src/settingsdialog.ui
+++ b/src/src/settingsdialog.ui
@@ -1824,6 +1824,38 @@ If you disable this feature, MO will only display official DLCs this way. Please
</property>
</widget>
</item>
+ <item row="6" column="0" colspan="4">
+ <widget class="QPushButton" name="toggleInstallLog">
+ <property name="text">
+ <string>Show Install Log</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0" colspan="4">
+ <widget class="QTextEdit" name="nakInstallLog">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <property name="maximumHeight">
+ <number>200</number>
+ </property>
+ <property name="font">
+ <font>
+ <family>monospace</family>
+ <pointsize>9</pointsize>
+ </font>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp
index f12b9c0..e9d1bec 100644
--- a/src/src/settingsdialogproton.cpp
+++ b/src/src/settingsdialogproton.cpp
@@ -52,23 +52,36 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d)
populateProtons();
QObject::connect(ui->protonVersionCombo, &QComboBox::currentIndexChanged, this,
- [this](int) {
- if (auto cfg = FluorineConfig::load();
- cfg.has_value() && cfg->prefixExists()) {
- const QString protonName =
- ui->protonVersionCombo->currentText().trimmed();
- const QString protonPath = ui->protonVersionCombo
- ->currentData(Qt::UserRole + 1)
- .toString()
- .trimmed();
+ [this](int index) {
+ if (index < 0) {
+ return;
+ }
+
+ auto cfg = FluorineConfig::load();
+ if (!cfg.has_value()) {
+ return;
+ }
+
+ const QString protonName =
+ ui->protonVersionCombo->currentText().trimmed();
+ const QString protonPath = ui->protonVersionCombo
+ ->itemData(index, Qt::UserRole + 1)
+ .toString()
+ .trimmed();
- if (!protonName.isEmpty() && !protonPath.isEmpty() &&
- (cfg->proton_name != protonName ||
- cfg->proton_path != protonPath)) {
- cfg->proton_name = protonName;
- cfg->proton_path = protonPath;
- cfg->save();
- }
+ if (protonName.isEmpty() || protonPath.isEmpty()) {
+ MOBase::log::warn("Proton combo change: name='{}' path='{}' — "
+ "skipping save (empty)", protonName, protonPath);
+ return;
+ }
+
+ if (cfg->proton_name != protonName ||
+ cfg->proton_path != protonPath) {
+ cfg->proton_name = protonName;
+ cfg->proton_path = protonPath;
+ cfg->save();
+ MOBase::log::info("Updated Proton config: {} ({})",
+ protonName, protonPath);
}
});
@@ -90,6 +103,15 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d)
QObject::connect(&m_installWatcher, &QFutureWatcher<InstallResult>::finished, this,
&ProtonSettingsTab::onInstallFinished);
+ // install log viewer
+ ui->nakInstallLog->setVisible(false);
+ QObject::connect(ui->toggleInstallLog, &QPushButton::toggled, this,
+ [this](bool checked) {
+ ui->nakInstallLog->setVisible(checked);
+ ui->toggleInstallLog->setText(
+ checked ? tr("Hide Install Log") : tr("Show Install Log"));
+ });
+
refreshState();
}
@@ -130,6 +152,17 @@ void ProtonSettingsTab::populateProtons()
const int idx = ui->protonVersionCombo->findText(cfg->proton_name);
if (idx >= 0) {
ui->protonVersionCombo->setCurrentIndex(idx);
+ } else if (ui->protonVersionCombo->count() > 0) {
+ // Saved Proton version no longer exists — select first available and
+ // update the config so the stale path doesn't cause umu-run fallback.
+ MOBase::log::warn("Saved Proton '{}' not found, defaulting to '{}'",
+ cfg->proton_name,
+ ui->protonVersionCombo->itemText(0));
+ ui->protonVersionCombo->setCurrentIndex(0);
+ cfg->proton_name = ui->protonVersionCombo->itemText(0).trimmed();
+ cfg->proton_path = ui->protonVersionCombo->itemData(0, Qt::UserRole + 1)
+ .toString().trimmed();
+ cfg->save();
}
}
}
@@ -206,6 +239,9 @@ void ProtonSettingsTab::onCreatePrefix()
setBusy(true);
ui->protonStatusLabel->setText(tr("Creating prefix..."));
+ ui->nakInstallLog->clear();
+ ui->nakInstallLog->setVisible(true);
+ ui->toggleInstallLog->setChecked(true);
startInstallTask(0, pfxPath, protonName, protonPath,
ui->umuCheckBox->isChecked(),
@@ -254,6 +290,9 @@ void ProtonSettingsTab::onRecreatePrefix()
setBusy(true);
ui->protonStatusLabel->setText(tr("Recreating prefix..."));
+ ui->nakInstallLog->clear();
+ ui->nakInstallLog->setVisible(true);
+ ui->toggleInstallLog->setChecked(true);
startInstallTask(cfg->app_id, cfg->prefix_path, cfg->proton_name,
cfg->proton_path, ui->umuCheckBox->isChecked(),
@@ -543,6 +582,9 @@ void ProtonSettingsTab::showGameRegistryDialog()
setBusy(true);
ui->protonStatusLabel->setText(tr("Fixing game registries..."));
+ ui->nakInstallLog->clear();
+ ui->nakInstallLog->setVisible(true);
+ ui->toggleInstallLog->setChecked(true);
g_activeInstallTab.store(this);
@@ -692,10 +734,24 @@ void ProtonSettingsTab::enqueueProgress(float progress)
Qt::QueuedConnection);
}
+void ProtonSettingsTab::appendInstallLog(const QString& message)
+{
+ ui->nakInstallLog->append(message);
+}
+
void ProtonSettingsTab::statusCallback(const char* message)
{
if (auto* tab = g_activeInstallTab.load(); tab != nullptr) {
- tab->enqueueStatus(QString::fromUtf8(message ? message : ""));
+ const QString msg = QString::fromUtf8(message ? message : "");
+ tab->enqueueStatus(msg);
+
+ if (!msg.isEmpty()) {
+ QMetaObject::invokeMethod(tab,
+ [tab, msg] {
+ tab->appendInstallLog(msg);
+ },
+ Qt::QueuedConnection);
+ }
}
}
@@ -704,6 +760,15 @@ void ProtonSettingsTab::logCallback(const char* message)
if (message && *message) {
MOBase::log::info("{}", message);
}
+
+ if (auto* tab = g_activeInstallTab.load(); tab != nullptr && message && *message) {
+ const QString msg = QString::fromUtf8(message);
+ QMetaObject::invokeMethod(tab,
+ [tab, msg] {
+ tab->appendInstallLog(msg);
+ },
+ Qt::QueuedConnection);
+ }
}
void ProtonSettingsTab::progressCallback(float progress)
diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h
index b3499d3..2b864c9 100644
--- a/src/src/settingsdialogproton.h
+++ b/src/src/settingsdialogproton.h
@@ -45,6 +45,7 @@ private:
void enqueueStatus(const QString& message);
void enqueueProgress(float progress);
+ void appendInstallLog(const QString& message);
static void statusCallback(const char* message);
static void logCallback(const char* message);
diff --git a/src/src/settingsdialogtheme.cpp b/src/src/settingsdialogtheme.cpp
index 363ae3b..8d41d35 100644
--- a/src/src/settingsdialogtheme.cpp
+++ b/src/src/settingsdialogtheme.cpp
@@ -1,11 +1,15 @@
#include "settingsdialogtheme.h"
#include "categoriesdialog.h"
#include "colortable.h"
+#include "instancemanager.h"
#include "modlist.h"
#include "shared/appconfig.h"
#include "ui_settingsdialog.h"
#include <questionboxmemory.h>
#include <utility.h>
+#ifndef _WIN32
+#include "fluorinepaths.h"
+#endif
using namespace MOBase;
@@ -52,14 +56,35 @@ void ThemeSettingsTab::addStyles()
ui->styleBox->insertSeparator(ui->styleBox->count());
- QDirIterator iter(QCoreApplication::applicationDirPath() + "/" +
- QString::fromStdWString(AppConfig::stylesheetsPath()),
- QStringList("*.qss"), QDir::Files);
-
- while (iter.hasNext()) {
- iter.next();
+ // Collect .qss files from all stylesheet search directories, deduplicating
+ // by filename so bundled themes aren't listed twice.
+ const QString ssSubdir = QString::fromStdWString(AppConfig::stylesheetsPath());
+ QStringList searchDirs;
+ searchDirs << QCoreApplication::applicationDirPath() + "/" + ssSubdir;
+#ifndef _WIN32
+ if (auto ci = InstanceManager::singleton().currentInstance()) {
+ // currentInstance() returns a bare Instance (readFromIni() not called),
+ // so baseDirectory() is empty. Use directory() which is always set.
+ const QString instanceDir = ci->directory() + "/" + ssSubdir;
+ if (!searchDirs.contains(instanceDir))
+ searchDirs << instanceDir;
+ }
+ const QString userDir = fluorineDataDir() + "/stylesheets";
+ if (!searchDirs.contains(userDir))
+ searchDirs << userDir;
+#endif
- ui->styleBox->addItem(iter.fileInfo().completeBaseName(), iter.fileName());
+ QSet<QString> seen;
+ for (const auto& dir : searchDirs) {
+ QDirIterator iter(dir, QStringList("*.qss"), QDir::Files);
+ while (iter.hasNext()) {
+ iter.next();
+ const QString fileName = iter.fileName();
+ if (seen.contains(fileName))
+ continue;
+ seen.insert(fileName);
+ ui->styleBox->addItem(iter.fileInfo().completeBaseName(), fileName);
+ }
}
}
@@ -75,7 +100,21 @@ void ThemeSettingsTab::selectStyle()
void ThemeSettingsTab::onExploreStyles()
{
+ // On Linux, open the instance's stylesheets directory (where custom themes
+ // from modlists live). Falls back to the user data dir if no instance, or
+ // to the app dir on Windows.
+#ifndef _WIN32
+ QString ssPath;
+ if (auto ci = InstanceManager::singleton().currentInstance()) {
+ ssPath = ci->directory() + "/" +
+ QString::fromStdWString(AppConfig::stylesheetsPath());
+ } else {
+ ssPath = fluorineDataDir() + "/stylesheets";
+ }
+ QDir().mkpath(ssPath);
+#else
QString ssPath = QCoreApplication::applicationDirPath() + "/" +
ToQString(AppConfig::stylesheetsPath());
+#endif
shell::Explore(ssPath);
}
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp
index f68389e..c2f15eb 100644
--- a/src/src/spawn.cpp
+++ b/src/src/spawn.cpp
@@ -643,7 +643,8 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
.setPreferSystemUmu(
QSettings().value("fluorine/prefer_system_umu", false).toBool())
.setUseSteamRun(
- QSettings().value("fluorine/use_steam_run", false).toBool());
+ QSettings().value("fluorine/use_steam_run", false).toBool())
+ .setHelperProcessOut(sp.helperProcessOut);
const QString prefixPath = resolvePrefixPath();
if (prefixPath.isEmpty()) {
diff --git a/src/src/spawn.h b/src/src/spawn.h
index 48aaa05..72c81be 100644
--- a/src/src/spawn.h
+++ b/src/src/spawn.h
@@ -32,6 +32,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <unistd.h>
#endif
+class QProcess;
class Settings;
namespace spawn
@@ -62,6 +63,7 @@ struct SpawnParameters
#else
int stdOut = -1;
int stdErr = -1;
+ QProcess** helperProcessOut = nullptr;
#endif
};
diff --git a/src/src/vfs/vfs_helper_main.cpp b/src/src/vfs/vfs_helper_main.cpp
index 9139d4c..d983574 100644
--- a/src/src/vfs/vfs_helper_main.cpp
+++ b/src/src/vfs/vfs_helper_main.cpp
@@ -31,6 +31,7 @@ struct HelperConfig
std::string game_dir;
std::string data_dir_name;
std::string overwrite_dir;
+ std::string output_dir;
std::vector<std::pair<std::string, std::string>> mods;
std::vector<std::pair<std::string, std::string>> extra_files;
};
@@ -62,6 +63,8 @@ static HelperConfig readConfig(const std::string& path)
cfg.data_dir_name = val;
} else if (key == "overwrite_dir") {
cfg.overwrite_dir = val;
+ } else if (key == "output_dir") {
+ cfg.output_dir = val;
} else if (key == "mod") {
const auto pipe = val.find('|');
if (pipe != std::string::npos) {
@@ -97,10 +100,13 @@ static void tryUnmountStale(const std::string& path)
}
static void flushStaging(const std::string& stagingDir,
- const std::string& overwriteDir)
+ const std::string& overwriteDir,
+ const std::string& outputDir = {})
{
const fs::path staging(stagingDir);
- const fs::path overwrite(overwriteDir);
+ const fs::path overwrite = outputDir.empty()
+ ? fs::path(overwriteDir)
+ : fs::path(outputDir);
if (!fs::exists(staging)) {
return;
}
@@ -193,6 +199,9 @@ int main(int argc, char* argv[])
std::error_code ec;
fs::create_directories(stagingDir, ec);
fs::create_directories(config.overwrite_dir, ec);
+ if (!config.output_dir.empty()) {
+ fs::create_directories(config.output_dir, ec);
+ }
// Scan base game files BEFORE mounting (after mount they're hidden)
auto baseFileCache = scanDataDir(dataDirPath);
@@ -289,7 +298,7 @@ int main(int argc, char* argv[])
config = newConfig;
std::cout << "ok" << std::endl;
} else if (line == "flush") {
- flushStaging(stagingDir, config.overwrite_dir);
+ flushStaging(stagingDir, config.overwrite_dir, config.output_dir);
fs::create_directories(stagingDir, ec);
auto newTree = std::make_shared<VfsTree>(buildDataDirVfs(
@@ -320,7 +329,7 @@ int main(int argc, char* argv[])
fuse_session_destroy(session);
g_session = nullptr;
- flushStaging(stagingDir, config.overwrite_dir);
+ flushStaging(stagingDir, config.overwrite_dir, config.output_dir);
close(backingFd);
std::cout << "ok" << std::endl;
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp
index 64f5aa1..6d6f5e0 100644
--- a/src/src/wineprefix.cpp
+++ b/src/src/wineprefix.cpp
@@ -103,7 +103,9 @@ QStringList findCaseVariants(const QString& path)
WinePrefix::WinePrefix(const QString& prefixPath)
: m_prefixPath(QDir::cleanPath(prefixPath))
-{}
+{
+ MOBase::log::debug("WinePrefix: initialized with path '{}'", m_prefixPath);
+}
bool WinePrefix::isValid() const
{
@@ -133,16 +135,38 @@ QString WinePrefix::appdataLocal() const
bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDir) const
{
if (!isValid()) {
+ MOBase::log::error("deployPlugins: prefix '{}' is not valid (drive_c not found)",
+ m_prefixPath);
return false;
}
const QString pluginsDir = QDir(appdataLocal()).filePath(dataDir);
+ MOBase::log::debug("deployPlugins: target dir='{}', {} plugins to deploy",
+ pluginsDir, plugins.size());
+
if (!QDir().mkpath(pluginsDir)) {
+ MOBase::log::error("deployPlugins: failed to create directory '{}'", pluginsDir);
return false;
}
- QFile pluginsFile(QDir(pluginsDir).filePath("Plugins.txt"));
+ // Remove all case variants of plugins.txt and loadorder.txt before writing.
+ // Linux is case-sensitive, so a stale "plugins.txt" can coexist with
+ // "Plugins.txt" and the game may read the wrong one (e.g. FalloutNV reads
+ // lowercase "plugins.txt").
+ const QString pluginsPath = QDir(pluginsDir).filePath("Plugins.txt");
+ const QString loadOrderPath = QDir(pluginsDir).filePath("loadorder.txt");
+ for (const QString& variant : findCaseVariants(pluginsPath)) {
+ MOBase::log::debug("deployPlugins: removing stale plugins variant '{}'", variant);
+ QFile::remove(variant);
+ }
+ for (const QString& variant : findCaseVariants(loadOrderPath)) {
+ MOBase::log::debug("deployPlugins: removing stale loadorder variant '{}'", variant);
+ QFile::remove(variant);
+ }
+
+ QFile pluginsFile(pluginsPath);
if (!pluginsFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
+ MOBase::log::error("deployPlugins: failed to open '{}' for writing", pluginsPath);
return false;
}
@@ -151,9 +175,23 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi
pluginsStream << plugin << "\r\n";
}
pluginsFile.close();
+ MOBase::log::debug("deployPlugins: wrote {} plugins to '{}'", plugins.size(),
+ pluginsPath);
- QFile loadOrderFile(QDir(pluginsDir).filePath("loadorder.txt"));
+ // Also write lowercase "plugins.txt" for games that expect it (e.g. FalloutNV).
+ const QString pluginsLower = QDir(pluginsDir).filePath("plugins.txt");
+ if (pluginsLower != pluginsPath) {
+ QFile::remove(pluginsLower);
+ if (!QFile::copy(pluginsPath, pluginsLower)) {
+ MOBase::log::warn("deployPlugins: failed to create lowercase copy '{}'",
+ pluginsLower);
+ }
+ }
+
+ QFile loadOrderFile(loadOrderPath);
if (!loadOrderFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
+ MOBase::log::error("deployPlugins: failed to open '{}' for writing",
+ loadOrderPath);
return false;
}
@@ -166,6 +204,7 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi
loadOrderStream << line << "\r\n";
}
+ MOBase::log::debug("deployPlugins: wrote loadorder.txt to '{}'", loadOrderPath);
return true;
}
@@ -175,9 +214,12 @@ bool WinePrefix::deployProfileIni(const QString& sourceIniPath,
{
const QFileInfo iniInfo(sourceIniPath);
if (!iniInfo.exists() || !iniInfo.isFile()) {
+ MOBase::log::warn("deployProfileIni: source '{}' does not exist or is not a file",
+ sourceIniPath);
return false;
}
+ MOBase::log::debug("deployProfileIni: '{}' -> '{}'", sourceIniPath, targetIniPath);
const QString destination = QDir::cleanPath(targetIniPath);
// Back up ALL case-insensitive variants (e.g. both skyrimprefs.ini and
@@ -204,7 +246,22 @@ bool WinePrefix::deployProfileIni(const QString& sourceIniPath,
}
}
- return copyFileWithParents(iniInfo.absoluteFilePath(), destination);
+ if (!copyFileWithParents(iniInfo.absoluteFilePath(), destination)) {
+ return false;
+ }
+
+ // Create a lowercase alias so the game can find the INI regardless of
+ // which casing it uses (e.g. FalloutNV reads "fallout.ini" but we deploy
+ // "Fallout.ini").
+ const QFileInfo destInfo(destination);
+ const QString lowerName = destInfo.fileName().toLower();
+ if (lowerName != destInfo.fileName()) {
+ const QString lowerPath = QDir(destInfo.path()).filePath(lowerName);
+ QFile::remove(lowerPath); // remove stale copy/symlink if any
+ QFile::link(destInfo.fileName(), lowerPath);
+ }
+
+ return true;
}
bool WinePrefix::deployProfileSaves(const QString& profileSaveDir, const QString& gameName,
@@ -212,9 +269,13 @@ bool WinePrefix::deployProfileSaves(const QString& profileSaveDir, const QString
bool clearDestination) const
{
if (!isValid()) {
+ MOBase::log::error("deployProfileSaves: prefix '{}' is not valid", m_prefixPath);
return false;
}
+ MOBase::log::debug("deployProfileSaves: profileSaveDir='{}', gameName='{}', "
+ "saveRelativePath='{}', clearDestination={}",
+ profileSaveDir, gameName, saveRelativePath, clearDestination);
const QString gameRoot = QDir(myGamesPath()).filePath(gameName);
const QString normalizedSavePath =
QString(saveRelativePath).replace('\\', '/').trimmed();
@@ -258,9 +319,13 @@ bool WinePrefix::syncSavesBack(const QString& profileSaveDir, const QString& gam
const QString& saveRelativePath) const
{
if (!isValid()) {
+ MOBase::log::error("syncSavesBack: prefix '{}' is not valid", m_prefixPath);
return false;
}
+ MOBase::log::debug("syncSavesBack: profileSaveDir='{}', gameName='{}', "
+ "saveRelativePath='{}'",
+ profileSaveDir, gameName, saveRelativePath);
const QString gameRoot = QDir(myGamesPath()).filePath(gameName);
const QString normalizedSavePath =
QString(saveRelativePath).replace('\\', '/').trimmed();
@@ -350,10 +415,14 @@ void WinePrefix::restoreStaleBackups() const
bool WinePrefix::syncProfileInisBack(
const QList<QPair<QString, QString>>& iniMappings) const
{
+ MOBase::log::debug("syncProfileInisBack: {} INI mappings to sync back",
+ iniMappings.size());
bool allCopied = true;
for (const auto& mapping : iniMappings) {
const QString profileIniPath = QDir::cleanPath(mapping.first);
const QString prefixIniPath = QDir::cleanPath(mapping.second);
+ MOBase::log::debug("syncProfileInisBack: profile='{}' <- prefix='{}'",
+ profileIniPath, prefixIniPath);
// Find ALL case-insensitive variants of the INI file (e.g. both
// skyrimprefs.ini and SkyrimPrefs.ini may exist on Linux).