aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-17 23:59:40 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-17 23:59:48 -0600
commit480a57cf037a46f176128e6c94aa5616fcf704a3 (patch)
tree35be94099e2669f518c623acdf8fe23ee82e55a8 /libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
parent91e1d7f9f03ab3fd77cee94c1eac2abf89254e50 (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.cpp117
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});
}