diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-23 21:22:43 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-23 21:25:13 -0500 |
| commit | 12ce612e00af99990d364b1b74aca39e81b4d56f (patch) | |
| tree | 6ea4643827ddb60d70d5b9ed4e322e089563c0c5 | |
| parent | 5891a5feabdd768a3e649ff6087fac9c42b129cc (diff) | |
Fix native Stardew launch paths
| -rw-r--r-- | libs/basic_games/games/game_stardewvalley.py | 106 | ||||
| -rw-r--r-- | src/src/moapplication.cpp | 6 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 4 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 19 | ||||
| -rw-r--r-- | src/src/spawn.cpp | 2 |
5 files changed, 131 insertions, 6 deletions
diff --git a/libs/basic_games/games/game_stardewvalley.py b/libs/basic_games/games/game_stardewvalley.py index 566a511..e9b6863 100644 --- a/libs/basic_games/games/game_stardewvalley.py +++ b/libs/basic_games/games/game_stardewvalley.py @@ -1,9 +1,12 @@ +import os +import struct import sys -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, qWarning import mobase +from ..basic_features import BasicLocalSavegames from ..basic_game import BasicGame @@ -34,6 +37,94 @@ def _has_native_linux_install(game_dir: QDir) -> bool: return QFileInfo(game_dir, "StardewValley").exists() +def _native_linux_config_dir() -> QDir: + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + candidates = [] + if xdg_config_home: + candidates.append(os.path.join(xdg_config_home, "StardewValley")) + candidates.extend( + [ + os.path.expanduser("~/.config/StardewValley"), + os.path.expanduser( + "~/.var/app/com.valvesoftware.Steam/.config/StardewValley" + ), + os.path.expanduser( + "~/.var/app/com.heroicgameslauncher.hgl/config/StardewValley" + ), + ] + ) + + seen = set() + for candidate in candidates: + if candidate in seen: + continue + seen.add(candidate) + directory = QDir(candidate) + if directory.exists(): + return directory + + return QDir(candidates[0]) + + +def _clear_executable_stack(path: str) -> None: + # Equivalent to `execstack -c`: clear PF_X on the PT_GNU_STACK program + # header. Stardew's Galaxy libraries can ship with this bit set, which + # makes some Linux systems refuse to load them. + PT_GNU_STACK = 0x6474E551 + PF_X = 0x1 + + if not os.path.exists(path): + return + + try: + with open(path, "r+b") as f: + ident = f.read(16) + if len(ident) != 16 or ident[:4] != b"\x7fELF": + return + + elf_class = ident[4] + endian = "<" if ident[5] == 1 else ">" if ident[5] == 2 else None + if endian is None: + return + + if elf_class == 2: + f.seek(32) + phoff = struct.unpack(endian + "Q", f.read(8))[0] + f.seek(54) + phentsize, phnum = struct.unpack(endian + "HH", f.read(4)) + flags_offset = 4 + elif elf_class == 1: + f.seek(28) + phoff = struct.unpack(endian + "I", f.read(4))[0] + f.seek(42) + phentsize, phnum = struct.unpack(endian + "HH", f.read(4)) + flags_offset = 24 + else: + return + + for index in range(phnum): + header_offset = phoff + index * phentsize + f.seek(header_offset) + p_type = struct.unpack(endian + "I", f.read(4))[0] + if p_type != PT_GNU_STACK: + continue + + flags_pos = header_offset + flags_offset + f.seek(flags_pos) + flags = struct.unpack(endian + "I", f.read(4))[0] + if flags & PF_X: + f.seek(flags_pos) + f.write(struct.pack(endian + "I", flags & ~PF_X)) + return + except OSError as err: + qWarning(f"Failed to clear executable stack on '{path}': {err}") + + +def _fix_native_linux_galaxy_libraries(game_dir: QDir) -> None: + for library in ("libGalaxy64.so", "libGalaxyCSharpGlue.so"): + _clear_executable_stack(game_dir.absoluteFilePath(library)) + + class StardewValleyGame(BasicGame): Name = "Stardew Valley Support Plugin" Author = "Syer10" @@ -57,11 +148,24 @@ class StardewValleyGame(BasicGame): def init(self, organizer: mobase.IOrganizer): super().init(organizer) self._register_feature(StardewValleyModDataChecker()) + if self.isNativeLinux(): + _fix_native_linux_galaxy_libraries(self.gameDirectory()) + self._register_feature(BasicLocalSavegames(self)) return True def isNativeLinux(self) -> bool: return _has_native_linux_install(self.gameDirectory()) + def documentsDirectory(self) -> QDir: + if self.isNativeLinux(): + return _native_linux_config_dir() + return super().documentsDirectory() + + def savesDirectory(self) -> QDir: + if self.isNativeLinux(): + return QDir(self.documentsDirectory().absoluteFilePath("Saves")) + return super().savesDirectory() + def binaryName(self) -> str: if self.isNativeLinux(): return "StardewValley" diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index f130881..94e5b02 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -390,9 +390,9 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) FuseConnector::tryCleanupStaleMount(dataDir); } - // Restore any stale INI/save backups left by a previous crash. This ensures - // the documents directory is clean before we do anything else. - { + // Restore any stale INI/save backups left by a previous Wine/Proton crash. + // Native Linux game instances do not use the prefix during launch. + if (!m_instance->gamePlugin()->isNativeLinux()) { auto prefixPath = FluorineConfig::prefixPath(); if (!prefixPath || prefixPath->isEmpty()) { QSettings const instanceSettings(m_settings->filename(), QSettings::IniFormat); diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index c469033..d3f6cc0 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -104,7 +104,9 @@ QStringList toStringList(InputIterator current, InputIterator end) QString resolveWinePrefixPath(const Settings& settings, const IPluginGame* managedGame) { - Q_UNUSED(managedGame); + if (managedGame != nullptr && managedGame->isNativeLinux()) { + return {}; + } if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->prefixExists()) { return cfg->prefix_path.trimmed(); diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index 4e71c55..b4ed15b 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -1062,6 +1062,25 @@ bool ProtonLauncher::launchDirect(qint64& pid) const env.insert(it.key(), it.value()); } + // Native Linux games often ship shared libraries beside the executable. + // QProcess sets the cwd, but the dynamic loader/dlopen will not search it + // unless it is also present in LD_LIBRARY_PATH. + QStringList libraryPaths; + if (!m_workingDir.isEmpty()) { + libraryPaths << QDir::cleanPath(m_workingDir); + } + const QString binaryDir = QFileInfo(m_binary).absolutePath(); + if (!binaryDir.isEmpty() && !libraryPaths.contains(binaryDir)) { + libraryPaths << binaryDir; + } + const QString existingLdLibraryPath = env.value("LD_LIBRARY_PATH"); + if (!existingLdLibraryPath.isEmpty()) { + libraryPaths << existingLdLibraryPath.split(':', Qt::SkipEmptyParts); + } + if (!libraryPaths.isEmpty()) { + env.insert("LD_LIBRARY_PATH", libraryPaths.join(':')); + } + if (m_useTerminal) { wrapInTerminal(program, arguments); } diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index c6f4691..3163792 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -462,7 +462,7 @@ int spawn(const SpawnParameters& sp, pid_t& processId) launcher.setSavesBindMount(sp.saveBindMountSource, sp.saveBindMountTarget); } } else { - MOBase::log::info("Proton disabled for this executable, launching directly"); + MOBase::log::info("Launching executable directly without Proton"); } launcher.setUseTerminal(sp.useTerminal); |
