1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
|
import os
import struct
import sys
from PyQt6.QtCore import QDir, QFileInfo, qWarning
import mobase
from ..basic_features import BasicLocalSavegames
from ..basic_game import BasicGame
class StardewValleyModDataChecker(mobase.ModDataChecker):
def __init__(self):
super().__init__()
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for e in filetree:
if isinstance(e, mobase.IFileTree) and e.exists(
"manifest.json", mobase.IFileTree.FILE
):
return mobase.ModDataChecker.VALID
return mobase.ModDataChecker.INVALID
def _has_native_linux_install(game_dir: QDir) -> bool:
# Native Linux Stardew ships a `StardewValley` shell-script launcher and
# has no `Stardew Valley.exe`. Use that to distinguish from a Windows
# install dropped under a Wine prefix.
if sys.platform != "linux":
return False
if QFileInfo(game_dir, "Stardew Valley.exe").exists():
return False
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"
Version = "0.1.0a"
GameName = "Stardew Valley"
GameShortName = "stardewvalley"
GameNexusName = "stardewvalley"
GameNexusId = 1303
GameSteamId = 413150
GameGogId = 1453375253
GameBinary = "Stardew Valley.exe"
GameDataPath = "Mods"
GameDocumentsDirectory = "%DOCUMENTS%/StardewValley"
GameSavesDirectory = "%GAME_DOCUMENTS%/Saves"
GameSupportURL = (
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
"Game:-Stardew-Valley"
)
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"
return super().binaryName()
def executables(self):
if self.isNativeLinux():
return [
mobase.ExecutableInfo(
"SMAPI",
QFileInfo(self.gameDirectory(), "StardewModdingAPI"),
),
mobase.ExecutableInfo(
"Stardew Valley",
QFileInfo(self.gameDirectory(), "StardewValley"),
),
]
return [
mobase.ExecutableInfo(
"SMAPI", QFileInfo(self.gameDirectory(), "StardewModdingAPI.exe")
),
mobase.ExecutableInfo(
"Stardew Valley", QFileInfo(self.gameDirectory(), "Stardew Valley.exe")
),
]
|