diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session index cb6595340..1f99fea5a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session @@ -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 diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat index 2bf51da32..118467857 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat @@ -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__":