Restart the client once for a title mapped while it runs

The client reads config.vdf once, as it starts. A title installed during a
session got its per-app ARM64 Proton entry from the compat beat, but the
running client kept its own pick: native for a Linux build, which then sat
on "Downloading content (0%)" waiting for Steam Linux Runtime 2.0 and FEX
it can never run here (Left 4 Dead 2), or proton-stable for a Windows one.

The beat now runs the compat pass with --live, which notes titles it maps
while the client runs. Once each has finished installing and no game is
running, the session restarts the client through the existing restart-once
marker. Once per title and three times per session at most. The mapping is
re-applied before the client starts again, since it writes config.vdf back
from memory as it exits.
This commit is contained in:
The412Banner committed 2026-10-02 12:30:34 -04:00
1 parent 9a6558d8b3
commit c9fb19b0cd
2 files changed
+56 -4

No files matched your search

@@ -498,6 +498,31 @@ case $mode in
done
return 1
}
# A title the compat pass maps while the client runs only takes that mapping at the client's
# next start (see bannerlator-steam-compat): until then the client launches it its own way -
# a Linux build stuck on "Downloading content (0%)" for a Steam Linux Runtime and FEX that
# cannot run here (Left 4 Dead 2, 2026-10-02). So restart the client once for it, through the
# same marker and -shutdown as the ARM64 Proton's restart: only when every new title has
# finished installing (StateFlags 4) and no game is running; another download just resumes.
# Once per title and three times per session at most, so a client that rewrites config.vdf
# can never loop it.
compat_pending=$HOME/.bl-compat-pending
compat_restarted=$HOME/.bl-compat-restarted
rm -f "$compat_pending"
: > "$compat_restarted"
compat_restarts=0
compat_settled() {
local app m flags
while read -r app; do
[ -n "$app" ] || continue
m="$steam_root/steamapps/appmanifest_$app.acf"
[ -f "$m" ] || m="/mnt/droiddeck-sd/steamapps/appmanifest_$app.acf"
[ -f "$m" ] || return 1
flags=$(sed -n 's/^[[:space:]]*"StateFlags"[[:space:]]*"\([0-9]*\)".*/\1/p' "$m" | head -n 1)
[ "$flags" = 4 ] || return 1
done < "$compat_pending"
! pgrep -f 'reaper SteamLaunch' >/dev/null 2>&1
}
for ((_i = 0; _i < 720; _i++)); do
nap 5
beat=$((beat + 1))
@@ -505,7 +530,17 @@ case $mode in
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
if compat_due; then
: > "$compat_stamp"
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
bannerlator-steam-compat "$steam_root" --live >/dev/null 2>&1 || true
fi
if [ -s "$compat_pending" ] && compat_settled; then
_bl_new=$(sort -u "$compat_pending" | grep -vxF -f "$compat_restarted" || true)
rm -f "$compat_pending"
[ -n "$_bl_new" ] && [ "$compat_restarts" -lt 3 ] || continue
compat_restarts=$((compat_restarts + 1))
printf '%s\n' $_bl_new >> "$compat_restarted"
step "newly installed title(s) $(echo $_bl_new) set to the ARM64 Proton: restarting the Steam client once so it takes effect"
: > "$HOME/.bl-restart-once"
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
fi
done
) &
@@ -719,6 +754,9 @@ BUSCONF
# arguments, which were acted on. Not counted against the update cap.
if [ -f "$HOME/.bl-restart-once" ]; then
echo "== steam: restarting once so the new compatibility layer is in the client's lists"
# The client wrote config.vdf back from its own memory as it exited, which can drop the
# per-title entries written while it ran - the very ones this restart is for.
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
set --
continue
fi
@@ -579,7 +579,8 @@ def installed_apps(steam_root):
def register_default(config_path, apps=(), name=None, protect=()):
"""Writes the mapping. `name` defaults to our own tool; the first-run seed passes Valve's."""
"""Writes the mapping. `name` defaults to our own tool; the first-run seed passes Valve's.
Returns the app ids given a per-app entry by this call."""
tool = name or TOOL
# Tool names the user may deliberately have chosen (see adopt_extras); never repointed.
keep = {n.lower() for n in protect}
@@ -588,7 +589,8 @@ def register_default(config_path, apps=(), name=None, protect=()):
tokens = tokenize(original)
steam = find_block(tokens, ["InstallConfigStore", "Software", "Valve", "Steam"])
if steam is None:
return
return []
added = []
mapping = find_block(tokens[steam + 1:], ["CompatToolMapping"])
entry = ['"0"', "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"75"', "}"]
if mapping is None:
@@ -612,10 +614,12 @@ def register_default(config_path, apps=(), name=None, protect=()):
continue
tokens[mapping + 1:mapping + 1] = [
'"%s"' % app, "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"250"', "}"]
added.append(app)
# Nothing to change is the usual case on the session's beat: the client's own file is left alone.
text = serialize(tokens)
if text != serialize(tokenize(original)):
write_changed(config_path, text)
return added
def depot_build(depot):
@@ -666,8 +670,18 @@ def main():
config = os.path.join(steam_root, "config", "config.vdf")
if os.path.isfile(config):
apps = installed_apps(steam_root)
register_default(config, apps, protect=extras)
added = register_default(config, apps, protect=extras)
print("bannerlator-steam-compat: default and %d installed title(s) set to %s" % (len(apps), TOOL))
# The client reads config.vdf once, as it starts, and keeps its own decisions in memory for
# the rest of its run: a title installed while it runs gets the client's own pick (native
# for a Linux build, which then waits for Steam Linux Runtime + FEX it can never run here;
# proton-stable for a Windows one), and the entry written here only counts from the next
# start. Run from the session's beat (--live), the new entries are noted for the session,
# which restarts the client once the install has settled (bannerlator-session).
if added and "--live" in sys.argv[2:]:
with open(os.path.join(os.path.expanduser("~"), ".bl-compat-pending"), "a") as f:
f.write("".join("%s\n" % a for a in added))
print("bannerlator-steam-compat: mapped while the client runs: %s" % " ".join(added))
if __name__ == "__main__":