From 480a57cf037a46f176128e6c94aa5616fcf704a3 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Tue, 17 Feb 2026 23:59:40 -0600 Subject: Fix Wine prefix deployment, INI handling, and data directory paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix data directory path to use ~/.var/app/com.fluorine.manager consistently (was ~/.local/share/fluorine in committed code) - Fix VFS helper path in fuseconnector.cpp to use fluorineDataDir() - Fix localAppFolder() to resolve Wine prefix AppData/Local on Linux instead of returning XDG ~/.local/share - Add localAppName() virtual method for correct AppData folder mapping (Enderal→"enderal", Nehrim→"Oblivion") - Fix mergeTweak() to use direct INI parser instead of QSettings which corrupts backslashes and URL-encodes spaces in keys - Make resolveWineDataDirName() more robust with existence checks and fallback chain (documentsDirectory → gameShortName → gameName) - Add comprehensive debug logging throughout Wine prefix deployment - Fix INI case handling: copy instead of symlink for case-mismatched INIs, ensure both proper-case and lowercase aliases exist - Remove native build script (Flatpak only) Co-Authored-By: Claude Opus 4.6 --- build-native.sh | 82 ----- flatpak/com.fluorine.manager.yml | 2 + flatpak/flatpak-install.sh | 4 +- flatpak/fluorine-manager-wrapper.sh | 10 +- libs/bsatk/src/bsafolder.cpp | 23 ++ libs/game_bethesda/src/gamebryo/gamegamebryo.cpp | 117 ++++++- libs/game_bethesda/src/gamebryo/gamegamebryo.h | 7 + .../src/games/enderal/enderalbsainvalidation.cpp | 2 +- .../src/games/enderal/enderaldataarchives.cpp | 8 +- .../src/games/enderal/gameenderal.cpp | 24 +- libs/game_bethesda/src/games/enderal/gameenderal.h | 1 + .../src/games/fallout3/fallout3bsainvalidation.cpp | 2 +- .../src/games/fallout3/fallout3dataarchives.cpp | 8 +- .../src/games/fallout3/gamefallout3.cpp | 14 +- .../src/games/fallout4/fallout4dataarchives.cpp | 8 +- .../src/games/fallout4/gamefallout4.cpp | 16 +- .../games/fallout4london/fo4londondataarchives.cpp | 8 +- .../src/games/fallout4london/gamefo4london.cpp | 14 +- .../games/fallout4vr/fallout4vrdataarchives.cpp | 8 +- .../src/games/fallout4vr/gamefallout4vr.cpp | 14 +- .../games/falloutnv/falloutnvbsainvalidation.cpp | 2 +- .../src/games/falloutnv/falloutnvdataarchives.cpp | 8 +- .../src/games/falloutnv/gamefalloutnv.cpp | 16 +- .../src/games/morrowind/gamemorrowind.cpp | 2 +- .../src/games/morrowind/morrowinddataarchives.cpp | 8 +- libs/game_bethesda/src/games/nehrim/gamenehrim.cpp | 18 +- libs/game_bethesda/src/games/nehrim/gamenehrim.h | 1 + .../src/games/nehrim/nehrimbsainvalidation.cpp | 2 +- .../src/games/oblivion/gameoblivion.cpp | 12 +- .../src/games/oblivion/oblivionbsainvalidation.cpp | 2 +- .../src/games/oblivion/obliviondataarchives.cpp | 8 +- libs/game_bethesda/src/games/skyrim/gameskyrim.cpp | 14 +- .../src/games/skyrim/skyrimbsainvalidation.cpp | 2 +- .../src/games/skyrim/skyrimdataarchives.cpp | 8 +- .../src/games/skyrimse/gameskyrimse.cpp | 14 +- .../src/games/skyrimse/skyrimsedataarchives.cpp | 8 +- .../src/games/skyrimvr/gameskyrimvr.cpp | 10 +- .../src/games/skyrimvr/skyrimvrdataarchives.cpp | 8 +- .../src/games/ttw/falloutttwbsainvalidation.cpp | 2 +- .../src/games/ttw/falloutttwdataarchives.cpp | 8 +- .../game_bethesda/src/games/ttw/gamefalloutttw.cpp | 16 +- libs/nak/src/paths.rs | 7 +- libs/usvfs/src/shared/directory_tree.cpp | 4 + libs/usvfs/src/shared/directory_tree.h | 21 ++ src/plugins/rootbuilder.py | 24 +- src/src/CMakeLists.txt | 11 + src/src/commandline.cpp | 22 +- src/src/fluorinepaths.cpp | 6 +- src/src/fluorinepaths.h | 8 +- src/src/fuseconnector.cpp | 51 ++- src/src/fuseconnector.h | 1 + src/src/instancemanager.cpp | 15 +- src/src/instancemanagerdialog.cpp | 131 +++++--- src/src/instancemanagerdialog.h | 4 + src/src/instancemanagerdialog.ui | 38 +++ src/src/loglist.cpp | 2 +- src/src/mainwindow.cpp | 15 + src/src/moapplication.cpp | 43 ++- src/src/organizercore.cpp | 64 +++- src/src/process_helper_main.cpp | 359 +++++++++++++++++++++ src/src/processrunner.cpp | 32 +- src/src/processrunner.h | 3 + src/src/profile.cpp | 37 ++- src/src/protonlauncher.cpp | 118 ++++++- src/src/protonlauncher.h | 8 + src/src/settingsdialog.ui | 32 ++ src/src/settingsdialogproton.cpp | 101 ++++-- src/src/settingsdialogproton.h | 1 + src/src/settingsdialogtheme.cpp | 55 +++- src/src/spawn.cpp | 3 +- src/src/spawn.h | 2 + src/src/vfs/vfs_helper_main.cpp | 17 +- src/src/wineprefix.cpp | 77 ++++- 73 files changed, 1460 insertions(+), 393 deletions(-) delete mode 100755 build-native.sh create mode 100644 src/src/process_helper_main.cpp 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" </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(dataArchives.get(), this)); registerFeature(std::make_shared(this)); - registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this, "Enderal.ini")); registerFeature(std::make_shared(this)); registerFeature(std::make_shared(moInfo->gameFeatures())); registerFeature(std::make_shared(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(dataArchives.get(), this)); registerFeature(std::make_shared(this)); - registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this, "Fallout.ini")); registerFeature(std::make_shared(this)); registerFeature( std::make_shared(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(this)); registerFeature(dataArchives); - registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this, "Fallout4Custom.ini")); registerFeature(std::make_shared(this)); registerFeature( std::make_shared(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(this)); - registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this, "Fallout4Custom.ini")); registerFeature(std::make_shared(this)); registerFeature( std::make_shared(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(dataArchives.get(), this)); registerFeature(std::make_shared(this)); - registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this, "Fallout.ini")); registerFeature(std::make_shared(this)); registerFeature( std::make_shared(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(dataArchives.get(), this)); registerFeature(std::make_shared(this)); - registerFeature(std::make_shared(this, "skyrim.ini")); + registerFeature(std::make_shared(this, "Skyrim.ini")); registerFeature(std::make_shared(this)); registerFeature(std::make_shared(m_Organizer->gameFeatures())); registerFeature(std::make_shared(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(dataArchives.get(), this)); registerFeature(std::make_shared(this)); - registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this, "Fallout.ini")); registerFeature(std::make_shared(this)); registerFeature( std::make_shared(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 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 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 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 -/// 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>& 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 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 @@ -815,14 +816,6 @@ std::unique_ptr selectInstance() dlg.activateWindow(); dlg.raise(); - 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 {}; } 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 #include +#include #include #include #include @@ -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(row) >= m_instances.size()) { - return NoSelection; - } - - return static_cast(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(row) >= m_instances.size()) { + return NoSelection; + } + + return static_cast(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 @@ + + + + Prefix + + + + + + + true + + + + + + + Proton + + + + + + + true + + + @@ -338,6 +366,16 @@ + + + + Remove from list + + + Remove this instance from the list without deleting any files. + + + 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 . #include #ifdef _WIN32 #include +#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 . #include "settings.h" #ifndef _WIN32 #include "fluorineconfig.h" +#include "fluorinepaths.h" #include "fuseconnector.h" #include "wineprefix.h" #include @@ -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, @@ -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> 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=, arg= (repeatable), env=KEY=VALUE (repeatable), +// workdir= +// Helper responds: "started " or "error " +// Runtime commands (MO2→helper): "kill" (SIGTERM child tree), "quit" +// Helper reports: "exited " when game process tree exits + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// ── 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 collectDescendants(pid_t root) +{ + std::unordered_set descendants; + + // Build parent→children map + struct ProcEntry { + pid_t pid; + pid_t ppid; + }; + std::vector 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(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(strtol(line.c_str() + 5, nullptr, 10)); + break; + } + } + + if (ppid > 0) { + entries.push_back({pid, ppid}); + } + } + closedir(proc); + + // BFS from root + std::vector 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& 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 args; + std::vector 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(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 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(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::runBinary() #ifdef _WIN32 m_handle.reset(startBinary(parent, m_sp)); #else + m_sp.helperProcessOut = &m_processHelper; m_handle.reset(reinterpret_cast(static_cast(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 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(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 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 " or "error " + 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 +#include #include #include #include #include +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 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 m_envVars; QMap 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 + + + + Show Install Log + + + true + + + false + + + + + + + true + + + false + + + 200 + + + + monospace + 9 + + + + 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(); - - if (!protonName.isEmpty() && !protonPath.isEmpty() && - (cfg->proton_name != protonName || - cfg->proton_path != protonPath)) { - cfg->proton_name = protonName; - cfg->proton_path = protonPath; - cfg->save(); - } + [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()) { + 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::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 #include +#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(); - - ui->styleBox->addItem(iter.fileInfo().completeBaseName(), iter.fileName()); + // 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 + + QSet 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 . #include #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> mods; std::vector> 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(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); + + // 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(QDir(pluginsDir).filePath("loadorder.txt")); + 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>& 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). -- cgit v1.3.1