aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-23 21:22:43 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-23 21:25:13 -0500
commit12ce612e00af99990d364b1b74aca39e81b4d56f (patch)
tree6ea4643827ddb60d70d5b9ed4e322e089563c0c5
parent5891a5feabdd768a3e649ff6087fac9c42b129cc (diff)
Fix native Stardew launch paths
-rw-r--r--libs/basic_games/games/game_stardewvalley.py106
-rw-r--r--src/src/moapplication.cpp6
-rw-r--r--src/src/organizercore.cpp4
-rw-r--r--src/src/protonlauncher.cpp19
-rw-r--r--src/src/spawn.cpp2
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);