Merge branch 'main' into fix/steam-client-download

This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-10-11 15:53:38 -04:00
commit 11bd604dfd
10 files changed
+841 -43

No files matched your search

@@ -255,8 +255,9 @@ EVALUATOR_SCRIPT = re.compile(r"(?:^|[\\/])evaluatorscript_[0-9]+\.vdf$", re.IGN
def mark_install_scripts(command, env):
"""The client is about to run a game's install scripts (iscriptevaluator.exe): mark every process
in them as already run first, so no redistributable installer starts - on first launch the
shared ones may still be downloading, and on ARM64 Proton the VC++ ones stop with an error."""
in them but a Vulkan runtime installer as already run first, so no redistributable installer
starts - on first launch the shared ones may still be downloading, and on ARM64 Proton the VC++
ones stop with an error."""
if not any(arg.lower().replace("\\", "/").endswith("iscriptevaluator.exe") for arg in command):
return
root = env.get("STEAM_COMPAT_CLIENT_INSTALL_PATH") or str(Path.home() / ".local/share/Steam")
@@ -0,0 +1,230 @@
#!/usr/bin/python3
"""Holds a game launcher's hidden web interface on one slow core while something else has focus.
Rockstar's launcher keeps drawing its Chromium interface (SocialClubHelper.exe: a renderer, and a
GPU process compositing through DXVK) for as long as it runs, hidden behind the game too, and at a
higher priority than the game's (Wine passes Chromium's on: the GPU process ran at nice -10). With
Red Dead Redemption 2 in front on the RedMagic that was 1.3 to 2.5 cores under FEX, on a phone
already held back by its temperature, for a page nobody sees. Held, riding at the throttle limit
went from 46.5 to 48.7 fps, 1% lows from 27.5 to 32.6. So while the focused window belongs to
anything but the launcher, every helper thread runs on one core of the slowest cluster at nice 19;
when the launcher's own window has focus again (signing in, the game closed) each thread gets its
own mask and priority back. Stopping the helpers instead would not hold: proot, the tracer of every
guest process, resumes a stopped tracee.
Focus comes from droiddeck-agent's focus.json (the path is the argument), which the agent rewrites
whenever gamescope's focus changes. droiddeck-session starts this for Steam sessions;
BL_LAUNCHER_CORES=0 leaves the launchers alone."""
import json
import os
import signal
import sys
import time
from pathlib import Path
# Image paths (lower case, backslashes), matched at the end: the helpers held back, and the
# programs whose window having focus means the user is looking at the launcher.
HELPERS = ("\\rockstar games\\social club\\socialclubhelper.exe",)
FRONTS = HELPERS + ("\\rockstar games\\launcher\\launcher.exe",)
NICE = 19
POLL = 2.0
# How often a held launcher is looked over again: for a helper the unheld Launcher.exe started
# since, and for threads the program has moved back.
RESCAN = 10.0
STEAM_CLIENT = 769
def log(message):
print("droiddeck-launcher-cores: " + message, flush=True)
def image(proc, pid):
"""A process's program as Wine names it (argv[0]), lower case with backslashes, or None."""
try:
with open(proc / str(pid) / "cmdline", "rb") as stream:
first = stream.read(4096).split(b"\0", 1)[0]
except OSError:
return None
return first.decode("utf-8", "replace").lower().replace("/", "\\")
def is_one_of(path, suffixes):
return path is not None and path.endswith(suffixes)
def tasks(proc, pid):
try:
return [int(tid) for tid in os.listdir(proc / str(pid) / "task") if tid.isdigit()]
except OSError:
return []
class Cores:
"""The kernel calls, apart so a test can stand in for them."""
def own(self):
return os.sched_getaffinity(0)
def get(self, tid):
return os.sched_getaffinity(tid), os.getpriority(os.PRIO_PROCESS, tid)
def set(self, tid, cpus, nice):
try:
os.sched_setaffinity(tid, cpus)
except OSError:
# A mask outside the app's cpuset now (Android shrinks it in the background) is
# EINVAL; the session's own is always allowed.
os.sched_setaffinity(tid, self.own())
os.setpriority(os.PRIO_PROCESS, tid, nice)
class Holder:
def __init__(self, proc=Path("/proc"), cpufreq=Path("/sys/devices/system/cpu"), cores=None):
self.proc = proc
self.cpufreq = cpufreq
self.cores = cores or Cores()
self.held = {} # pid -> {tid: (cpus, nice) before the hold}
self.core = None
def slow_core(self):
"""The last core of the slowest cluster the session has - not of the game's cores, which
the launcher inherits when BL_GAME_CPUS is set: the core the game misses least."""
def top(cpu):
try:
return int((self.cpufreq / f"cpu{cpu}/cpufreq/cpuinfo_max_freq").read_text())
except (OSError, ValueError):
return 0
cpus = self.cores.own()
slowest = min(top(cpu) for cpu in cpus)
return max(cpu for cpu in cpus if top(cpu) == slowest)
def helpers(self):
for name in os.listdir(self.proc):
if name.isdigit() and is_one_of(image(self.proc, name), HELPERS):
yield int(name)
def hold(self):
"""Every helper thread onto the slow core at NICE: new ones, and held ones the program has
moved since (Chromium sets its threads' priorities, and Wine passes that on)."""
if self.core is None:
self.core = self.slow_core()
hold = ({self.core}, NICE)
for pid in self.helpers():
before = self.held.setdefault(pid, {})
for tid in tasks(self.proc, pid):
try:
now = self.cores.get(tid)
if now != hold:
self.cores.set(tid, *hold)
except OSError:
continue # the thread ended
if tid not in before:
# A thread that already has the hold inherited it from a held parent: it gets
# what its process had, or the helpers' first.
before[tid] = now if now != hold else before.get(pid) or self.first() or now
for pid in [pid for pid in self.held if not (self.proc / str(pid)).exists()]:
del self.held[pid]
def first(self):
for before in self.held.values():
for state in before.values():
if state != ({self.core}, NICE):
return state
return None
def release(self):
"""Every held thread back to its own mask and priority; one started during the hold gets
what its process had."""
for pid, before in self.held.items():
default = before.get(pid) or self.first()
for tid in tasks(self.proc, pid):
state = before.get(tid, default)
if state is None:
continue
try:
self.cores.set(tid, *state)
except OSError:
pass
self.held.clear()
def focused(snapshot):
"""(app id, pid) of the window gamescope has focused, from droiddeck-agent's snapshot."""
window = snapshot.get("focusedWindow")
for entry in snapshot.get("focusableWindows") or []:
if entry.get("window") == window:
return snapshot.get("focusedApp"), entry.get("pid")
return snapshot.get("focusedApp"), None
class Governor:
def __init__(self, focus_path, holder=None, clock=time.monotonic):
self.focus_path = Path(focus_path)
self.holder = holder or Holder()
self.clock = clock
self.seen = None
self.app = self.pid = None
self.scanned = None
self.reported = None
def read_focus(self):
try:
stamp = self.focus_path.stat().st_mtime_ns
except OSError:
return False
if stamp == self.seen:
return False
try:
self.app, self.pid = focused(json.loads(self.focus_path.read_text()))
except (OSError, ValueError, AttributeError):
return False
self.seen = stamp
return True
def tick(self):
changed = self.read_focus()
if self.pid is None:
return
launcher_in_front = is_one_of(image(self.holder.proc, self.pid), FRONTS)
if launcher_in_front:
if self.holder.held:
self.holder.release()
self.report("released: the launcher has focus")
return
# Nothing to look for until a game has had focus: launchers only run for one, and a launcher
# left running after its game stays held through the Steam client's own focus.
if self.app in (None, 0, STEAM_CLIENT) and not self.holder.held:
return
now = self.clock()
if changed or self.scanned is None or now - self.scanned >= RESCAN:
self.scanned = now
self.holder.hold()
if self.holder.held:
self.report(f"{len(self.holder.held)} SocialClubHelper process(es) on core {self.holder.core} "
f"at nice {NICE} while the launcher is hidden")
def report(self, message):
if message != self.reported:
self.reported = message
log(message)
def stop(self, *_):
self.holder.release()
sys.exit(0)
def main(argv):
if len(argv) != 1:
print("usage: droiddeck-launcher-cores <agent focus.json>", file=sys.stderr)
return 2
governor = Governor(argv[0])
signal.signal(signal.SIGTERM, governor.stop)
signal.signal(signal.SIGHUP, governor.stop)
signal.signal(signal.SIGINT, governor.stop)
while True:
governor.tick()
time.sleep(POLL)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
@@ -17,6 +17,11 @@
# 2015-2022 runtime is recorded as installed the way Microsoft's installers look for it: on ARM64
# Proton the x64 bundle otherwise stops with "Microsoft Visual C++ 2015-2022 Redistributable (arm64)
# is required", while Proton's own msvcp140/vcruntime140 already serve the game.
#
# A Vulkan runtime installer is the one left to run. Proton has no stand-in for it: for a game whose
# Steam compat config says nativevulkanloader (Red Dead Redemption 2) it loads vulkan-1.dll native
# only, and Khronos' loader is in the prefix only once that installer has run - without it the game
# exits with STATUS_DLL_NOT_FOUND before it opens a window. The installer finishes in a second.
set -u
steam_root=${1:?steam root}
LIBRARIES=${DROIDDECK_LIBRARIES-/mnt/droiddeck /mnt/droiddeck-sd}
@@ -25,14 +30,15 @@ redist="$steam_root/steamapps/common/Steamworks Shared/_CommonRedist"
# Every process an install script would run, as the registry key and value the client writes once
# it has run it. Names sit one level inside "Run Process", the key they share a level further in.
read_scripts() { xargs -0 -r awk '
# "read_scripts vulkan" lists only the Vulkan runtime installers, which the plain call leaves out.
read_scripts() { xargs -0 -r awk -v want="${1:-redists}" '
# A block whose HasRunKey is set is one process; its name is the key the block opened under, at
# whatever depth - an install script nests it under "Run Process", the client'"'"'s evaluator script
# for a game wraps the same blocks once more.
FNR == 1 { depth = 0; pending = "" }
/^[[:space:]]*\{/ { depth++; name[depth] = pending; key[depth] = ""; pending = ""; next }
/^[[:space:]]*\{/ { depth++; name[depth] = pending; key[depth] = ""; vulkan[depth] = 0; pending = ""; next }
/^[[:space:]]*\}/ {
if (depth > 0 && key[depth] != "" && name[depth] != "")
if (depth > 0 && key[depth] != "" && name[depth] != "" && vulkan[depth] == (want == "vulkan"))
printf "%s\t%s\n", key[depth], name[depth]
if (depth > 0) depth--
next
@@ -40,6 +46,12 @@ read_scripts() { xargs -0 -r awk '
{
n = split($0, part, "\"")
if (n >= 5 && tolower(part[2]) == "hasrunkey") { key[depth] = part[4]; next }
if (n >= 5 && tolower(part[2]) ~ /^process [0-9]+$/) {
program = tolower(part[4])
sub(/.*[\\\/]/, "", program)
if (program ~ /vulkan/) vulkan[depth] = 1
next
}
if (n == 3) pending = part[2]
}
'; }
@@ -84,13 +96,17 @@ if [ "$(head -n 1 "$cache" 2>/dev/null)" != "$sig" ]; then
done <<<"$libraries" | sort -u
} >"$cache.tmp" && mv -f "$cache.tmp" "$cache"
fi
game_pairs=$(tail -n +2 "$cache" 2>/dev/null | tr '\n' '\0' | read_scripts)
# DROIDDECK_SEED_SCRIPTS: more scripts, one path a line - droiddeck-game-env passes the client's
# evaluator script for the game about to run its installers, read before the client looks.
if [ -n "${DROIDDECK_SEED_SCRIPTS:-}" ]; then
game_pairs=$(printf '%s\n%s\n' "$game_pairs" "$(printf '%s\n' "$DROIDDECK_SEED_SCRIPTS" | tr '\n' '\0' | read_scripts)")
fi
game_scripts() { { tail -n +2 "$cache" 2>/dev/null; printf '%s\n' "${DROIDDECK_SEED_SCRIPTS:-}"; } | sed '/^$/d' | tr '\n' '\0'; }
game_pairs=$(game_scripts | read_scripts)
[ -n "$(printf '%s' "$game_pairs" | tr -d '[:space:]')" ] && pairs=$(printf '%s\n%s\n' "$pairs" "$game_pairs" | sort -u | sed '/^$/d')
# A Vulkan runtime installer marked as run by an earlier seeding still has its marker: those come out
# again below, so the client runs the installer on the game's next launch.
declare -A vulkan=()
while IFS= read -r line; do
[[ $line == *$'\t'* ]] && vulkan[$line]=1
done < <(game_scripts | read_scripts vulkan)
# The VC++ 2015-2022 runtime as its installers check for it, for each architecture and both registry
# views. The version is the newest the scripts above know, so any 2015-2022 bundle finds itself
@@ -138,6 +154,38 @@ block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" '
for line; do [[ $line == vcrt$'\t'* ]] && vcrt "$now"; done
}
# Takes the given markers out of a registry file again, in both views, keeping its date.
unseed() {
local reg=$1 out
shift
out=$(mktemp "${reg%/*}/.system.reg.XXXXXX") || return 1
if printf '%s\n' "$@" | awk -F'\t' '
NR == FNR {
path = $1
sub(/^HKEY_LOCAL_MACHINE\\\\/, "", path)
if (path == $1)
next
path = tolower(path)
drop["[" path "]" SUBSEP tolower($2)] = 1
if (substr(path, 1, 10) == "software\\\\")
drop["[software\\\\wow6432node\\\\" substr(path, 11) "]" SUBSEP tolower($2)] = 1
next
}
/^\[/ { section = tolower(substr($0, 1, index($0, "]"))) }
/^"/ {
value = substr($0, 2)
value = tolower(substr(value, 1, index(value, "\"=") - 1))
if ((section SUBSEP value) in drop)
next
}
{ print }
' - "$reg" >"$out" && touch -r "$reg" "$out" && mv -f "$out" "$reg"; then
return 0
fi
rm -f "$out"
return 1
}
# Proton builds a new prefix from its template, and the client runs the install scripts in that
# prefix moments later, so the template carries the markers too. Its date is kept: Proton reads a
# changed one as a new template and upgrades every prefix made from the old.
@@ -158,6 +206,16 @@ for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/syste
if [ -f "$seeded" ]; then
while IFS= read -r line; do [[ $line == *$'\t'* ]] && had[$line]=1; done <"$seeded"
fi
stale=() kept=()
for line in "${!had[@]}"; do
if [ -n "${vulkan[$line]:-}" ]; then stale+=("$line"); else kept+=("$line"); fi
done
if [ ${#stale[@]} -gt 0 ] && unseed "$reg" "${stale[@]}"; then
had=()
for line in "${kept[@]}"; do had[$line]=1; done
printf '%s\n' "${kept[@]}" >"$seeded"
echo "== took Vulkan runtime markers out of $reg"
fi
missing=()
for line in "${want[@]}"; do [ -n "${had[$line]:-}" ] || missing+=("$line"); done
[ ${#missing[@]} -eq 0 ] && continue
@@ -50,6 +50,37 @@ prepare_locale() {
}
[ -z "${BL_INSIDE:-}" ] && prepare_locale
# What earlier sessions left in /tmp. The guest's /tmp is a directory in the rootfs, never emptied
# by a boot, and Android ends sessions by killing them: each left its mangoapp config, the client's
# temporary directories and shared-memory sockets, Chromium's singleton directory (the web
# helper's, an Electron app's), a session bus socket per run and a run's start stamp - 1,400
# entries on a device after a few weeks. Each kind is swept while nothing that could own one runs,
# with one find for the lot.
clear_stale_tmp() {
local running names=() n
running=$(pgrep -af 'steamwebhelper|steamrtarm64/steam|--type=|droiddeck-appimage-run|dbus-run-session|dbus-daemon.*--session|dbus-launch|kwin|plasmashell' 2>/dev/null)
case $running in
*steamwebhelper* | *steamrtarm64/steam*) ;;
*) names+=(-o -name 'mangohud.????????' -o -name '.com.valvesoftware.Steam.??????'
-o -name 'steam_chrome_shmem_uid*' -o \( -type d -empty -name 'steam??????' \)) ;;
esac
case $running in *--type=*) ;; *) names+=(-o \( -type d -name 'scoped_dir??????' \)) ;; esac
case $running in *droiddeck-appimage-run*) ;; *) names+=(-o -name 'droiddeck-appimage.??????') ;; esac
case $running in
*dbus-run-session* | *dbus-daemon*--session* | *dbus-launch*) ;;
*) names+=(-o \( -type s -name 'dbus-??????????' \)) ;;
esac
case $running in
*kwin* | *plasmashell*) ;;
*) names+=(-o \( -type d -empty -name 'plasma-csd-generator.??????' \)) ;;
esac
[ ${#names[@]} -gt 0 ] || return 0
n=$(find /tmp -mindepth 1 -maxdepth 1 \( "${names[@]:1}" \) -print -exec rm -rf {} + 2>/dev/null | wc -l)
[ "$n" -gt 0 ] && echo "== /tmp: cleared $n entries earlier sessions left"
return 0
}
[ -z "${BL_INSIDE:-}" ] && clear_stale_tmp
# Until the runtime is stable, Steam's own logs are copied beside the session log at exit so a
# failed launch can be handed over as one folder.
collect_steam_logs() {
@@ -271,7 +302,8 @@ if [ -z "${BL_INSIDE:-}" ]; then
# BL_MANGOAPP=0 turns it off (one community build found Valve's mangoapp crashing on Turnip).
if [ "$mode" = steam ] && [ "${BL_MANGOAPP:-1}" = 1 ] \
&& [ "${BL_STEAM_UI:-}" != desktop ] && command -v mangoapp >/dev/null 2>&1; then
MANGOHUD_CONFIGFILE=$(mktemp /tmp/mangohud.XXXXXXXX)
# One fixed file, rewritten each session: a mktemp name per session was never removed.
MANGOHUD_CONFIGFILE=/tmp/droiddeck-mangohud.conf
echo no_display > "$MANGOHUD_CONFIGFILE"
export MANGOHUD_CONFIGFILE STEAM_USE_MANGOAPP=1 STEAM_MANGOAPP_PRESETS_SUPPORTED=1 STEAM_MANGOAPP_HORIZONTAL_SUPPORTED=1
args+=(--mangoapp)
@@ -436,6 +468,13 @@ case $mode in
done
) &
fi
# Rockstar's launcher keeps drawing its hidden web interface for as long as a game of theirs
# runs; droiddeck-launcher-cores holds it on one slow core while it is out of sight, from
# gamescope's focus as the agent publishes it. BL_LAUNCHER_CORES=0 leaves launchers alone.
if [ "${BL_LAUNCHER_CORES:-1}" != 0 ] && [ -n "${BL_LAUNCH_DIR:-}" ] && [ -z "${BL_DESKTOP:-}" ] \
&& [ "${BL_STEAM_UI:-}" != desktop ] && [ -x /usr/local/bin/droiddeck-launcher-cores ]; then
/usr/local/bin/droiddeck-launcher-cores "$BL_LAUNCH_DIR/agent/focus.json" 2>&1 &
fi
step "registering the app's game library with Steam"
droiddeck-steam-library "$steam_root" || true
@@ -1,10 +1,16 @@
#!/usr/bin/python3
"""Initialize Steam's own Gamescope display settings once; subsequent choices belong to Steam.
"""Initialize Steam's own display settings once; subsequent choices belong to Steam.
The UI scale, and native resolution: the client treats our gamescope as an external display and,
unless "Enable Native Resolution on External Displays" is on, draws itself at 1920x1080 on any
larger output - a 2340x1080 session came up as 16:9 with bars. The session's size is already the
user's Resolution choice, so the client should use all of it."""
user's Resolution choice, so the client should use all of it.
And the screensaver over a running game. The client's defaults (steamui.so's settings descriptor)
show screensavers while a game runs, after 300 s on AC power with no button pressed - and a stick or
trigger held is no press. Red Dead Redemption 2's cutscenes run longer than that: the slideshow
covered the game, and the button that woke it reached the game as well (B asked to quit to the
desktop). Customization > Screensaver > "Show screensavers when playing a game" still turns it on."""
import math
import os
from pathlib import Path
@@ -15,6 +21,8 @@ import tempfile
DISPLAY = "External: gamescope|||Windowed"
NATIVE_RESOLUTION = "EnableSteamNativeExternalResolution"
# screensaver_while_ingame: kept in config.vdf like the Gamescope settings, under this path.
SCREENSAVER_IN_GAME = ("Customization", "Screensaver", "WhileInGame")
TOKEN = re.compile(r'\s+|//[^\n]*|"(?:\\.|[^"\\])*"|[{}]')
@@ -108,25 +116,43 @@ def seed_text(text, scale):
return text[:at] + insertion + text[at:], True
def seed_native_resolution(text):
def seed_value(text, path, value):
"""InstallConfigStore/<path> = value, unless the client already has a value there (the
user's), adding only the blocks that are missing."""
root = parse(text).get('InstallConfigStore')
if not isinstance(root, Block):
raise ValueError("missing InstallConfigStore")
gamescope = root.get('Gamescope')
if gamescope is not None and not isinstance(gamescope, Block):
raise ValueError("invalid Gamescope settings")
if gamescope is not None and gamescope.get(NATIVE_RESOLUTION) is not None:
*sections, name = path
block, depth = root, 1
while sections:
child = block.get(sections[0])
if child is None:
break
if not isinstance(child, Block):
raise ValueError(f"invalid {sections[0]} settings")
block, depth = child, depth + 1
sections.pop(0)
if not sections and block.get(name) is not None:
return text, False
entry = f'"{NATIVE_RESOLUTION}" "1"'
if gamescope is not None:
at, indent = gamescope.end, 2
else:
entry = '"Gamescope"\n{\n' + entry + '\n}'
at, indent = root.end, 1
insertion = '\n' + '\n'.join('\t' * indent + line for line in entry.splitlines()) + '\n'
lines = [(len(sections), f'"{name}"\t\t"{value}"')]
for level, section in reversed(list(enumerate(sections))):
lines = [(level, f'"{section}"'), (level, '{')] + lines + [(level, '}')]
insertion = ''.join('\t' * (depth + level) + line + '\n' for level, line in lines)
# Before the line the block closes on, when it has one of its own; else beside the brace.
at = text.rfind('\n', 0, block.end) + 1
if text[at:block.end].strip():
at, insertion = block.end, '\n' + insertion
return text[:at] + insertion + text[at:], True
def seed_native_resolution(text):
return seed_value(text, ('Gamescope', NATIVE_RESOLUTION), '1')
def seed_no_screensaver_in_game(text):
return seed_value(text, SCREENSAVER_IN_GAME, '0')
def atomic_write(path, text):
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix='.' + path.name, dir=path.parent)
@@ -140,31 +166,36 @@ def atomic_write(path, text):
def initialize(steam_root, width, height):
scale = initial_scale(width, height)
return seed_once(steam_root, 'droiddeck-ui-scale-initialized', lambda text: seed_text(text, scale),
f'== steam: initial handheld UI scale {scale:.3f}')
def seed_once(steam_root, marker_name, seed, message):
"""Applies seed to config.vdf the first time only: the marker keeps a later choice - including
a return to the client's own default, which the client may store by dropping the key - the
user's."""
config = Path(steam_root) / 'config/config.vdf'
marker = config.with_name('droiddeck-ui-scale-initialized')
marker = config.with_name(marker_name)
if marker.exists():
return False
text = config.read_text() if config.exists() else '"InstallConfigStore"\n{\n}\n'
updated, changed = seed_text(text, initial_scale(width, height))
updated, changed = seed(text)
if changed:
atomic_write(config, updated)
print(f'== steam: initial handheld UI scale {initial_scale(width, height):.3f}')
print(message)
atomic_write(marker, '1\n')
return changed
def initialize_native_resolution(steam_root):
config = Path(steam_root) / 'config/config.vdf'
marker = config.with_name('droiddeck-native-resolution-initialized')
if marker.exists():
return False
text = config.read_text() if config.exists() else '"InstallConfigStore"\n{\n}\n'
updated, changed = seed_native_resolution(text)
if changed:
atomic_write(config, updated)
print('== steam: native resolution on the session display')
atomic_write(marker, '1\n')
return changed
return seed_once(steam_root, 'droiddeck-native-resolution-initialized', seed_native_resolution,
'== steam: native resolution on the session display')
def initialize_screensaver(steam_root):
return seed_once(steam_root, 'droiddeck-screensaver-initialized', seed_no_screensaver_in_game,
'== steam: no screensaver over a running game')
if __name__ == '__main__':
@@ -174,9 +205,10 @@ if __name__ == '__main__':
except (OSError, ValueError, RecursionError) as error:
print('== steam: left UI scale unchanged: ' + str(error), file=sys.stderr)
status = 1
try:
initialize_native_resolution(sys.argv[1])
except (OSError, ValueError, RecursionError) as error:
print('== steam: left native resolution unchanged: ' + str(error), file=sys.stderr)
status = 1
for what, step in (('native resolution', initialize_native_resolution), ('screensaver', initialize_screensaver)):
try:
step(sys.argv[1])
except (OSError, ValueError, RecursionError) as error:
print(f'== steam: left {what} unchanged: {error}', file=sys.stderr)
status = 1
sys.exit(status)