diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-17 23:59:40 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-17 23:59:48 -0600 |
| commit | 480a57cf037a46f176128e6c94aa5616fcf704a3 (patch) | |
| tree | 35be94099e2669f518c623acdf8fe23ee82e55a8 /libs/game_bethesda/src/gamebryo/gamegamebryo.cpp | |
| parent | 91e1d7f9f03ab3fd77cee94c1eac2abf89254e50 (diff) | |
Fix Wine prefix deployment, INI handling, and data directory paths
- 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 <noreply@anthropic.com>
Diffstat (limited to 'libs/game_bethesda/src/gamebryo/gamegamebryo.cpp')
| -rw-r--r-- | libs/game_bethesda/src/gamebryo/gamegamebryo.cpp | 117 |
1 files changed, 110 insertions, 7 deletions
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}); } |
