From 06502bb49b475ab2df98f035b92d897bcef669b6 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 25 Sep 2026 23:54:10 -0400 Subject: [PATCH] Components: choices enforced at every game launch; Steam's tool-0 probes get their own prefix per Proton Steam runs its graphics probe through every registered Proton at once after login, all in compatdata/0. Prefix system32 links point into the Proton that set it up last, and the other Protons' Wines, running outside Proton's pfx.lock, wrote their builtin DLLs through them into that Proton's tree (device: GE's FEX DLLs became Proton Experimental's at session start). - bannerlator-steam-compat: both wrappers (Valve's depot launcher and the third-party bannerlator-proton-wrap) give a tool-0 run STEAM_COMPAT_DATA_PATH=.../compatdata/0-, and before every other launch check the Proton's files against the app's desired.tsv and put back anything that differs (same Proton build only), logging to reapplied.log. - ComponentsManager writes desired.tsv plus unpacked copies of the packages in use into the runtime after every change; the page and the drawer show 're-applied at launch'. --- .../launcher/session/ComponentsManager.kt | 86 +++++++++++++++++- .../droiddeck/launcher/ui/ComponentsPage.kt | 4 +- .../droiddeck/launcher/ui/SessionOverlay.kt | 3 +- .../bannerlator-steam-compatcpython-313.pyc | Bin 0 -> 24508 bytes .../usr/local/bin/bannerlator-steam-compat | 66 +++++++++++++- 5 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 tools/linuxfs/overlay/usr/local/bin/__pycache__/bannerlator-steam-compatcpython-313.pyc diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index 118051424..7004f2d10 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -80,7 +80,11 @@ object ComponentsManager { data class Original(val comp: String, val protonVersion: String, val label: String, val size: Long) data class Package(val file: String, val comp: String, val version: String, val description: String, val size: Long) data class CatalogItem(val file: String, val comp: String, val release: String, val url: String, val size: Long, val digest: String) - data class ProtonView(val proton: Proton, val components: Map, val originals: List, val inUseByGame: Boolean) + data class ProtonView( + val proton: Proton, val components: Map, val originals: List, val inUseByGame: Boolean, + /** When a launch last had to put this Proton's chosen files back (epoch seconds), or 0. */ + val reappliedAt: Long = 0, + ) data class Snapshot(val protons: List, val packages: List) data class Catalog(val items: List, val fetchedAt: Long) @@ -96,6 +100,12 @@ object ComponentsManager { private fun originalsDir(context: Context) = File(dataDir(context), "originals").apply { mkdirs() } private fun stateFile(context: Context) = File(dataDir(context), "state.json") private fun catalogFile(context: Context) = File(dataDir(context), "catalog.json") + /** + * Inside the Linux runtime, read by the Proton launch wrappers (bannerlator-steam-compat's + * bl_components) before every game launch: desired.tsv + an unpacked copy of each package in use. + */ + private const val LAUNCH_DIR = "root/.local/share/bannerlator-components" + private fun launchDir(context: Context) = File(root(context), LAUNCH_DIR) fun safeName(s: String): String = s.replace(Regex("[^A-Za-z0-9._+-]+"), "_").trim('_').ifEmpty { "unnamed" } @@ -112,6 +122,8 @@ object ComponentsManager { val tmp = File(f.parentFile, f.name + ".tmp") tmp.writeText(state.toString(2)) tmp.renameTo(f) + // Every change of choice reaches the launch wrappers at once. + runCatching { syncLaunchState(context, state) }.onFailure { Log.w(TAG, "launch state", it) } } private fun JSONObject.sub(key: String): JSONObject = optJSONObject(key) ?: JSONObject().also { put(key, it) } @@ -364,6 +376,7 @@ object ComponentsManager { for (p in protons(context)) for (comp in COMPONENTS) { runCatching { ensureOriginal(context, p, comp, state) }.onFailure { Log.w(TAG, "original $comp of ${p.name}", it) } } + runCatching { syncLaunchState(context, state) }.onFailure { Log.w(TAG, "launch state", it) } } // ------------------------------------------------------------------ view @@ -371,6 +384,13 @@ object ComponentsManager { fun snapshot(context: Context): Snapshot = synchronized(lock) { migratePlugin(context) val state = loadState(context) + val reapplied = HashMap() + runCatching { + File(launchDir(context), "reapplied.log").forEachLine { line -> + val parts = line.split('\t') + if (parts.size >= 2) parts[0].toLongOrNull()?.let { reapplied[parts[1]] = maxOf(reapplied[parts[1]] ?: 0, it) } + } + } val views = protons(context).map { p -> val comps = COMPONENTS.associateWith { comp -> val detected = if (comp == "fex") fexVersion(p.dir) else readVersionFile(File(p.dir, "${COMP_DIR.getValue(comp)}/version")) @@ -393,11 +413,73 @@ object ComponentsManager { q?.optString("label"), ) } - ProtonView(p, comps, originals(context, p.id), inUse(p)) + ProtonView(p, comps, originals(context, p.id), inUse(p), reapplied[p.dir.name] ?: 0) } Snapshot(views, packages(context)) } + // ------------------------------------------------------------------ launch-time enforcement + + /** + * Writes what each Proton should be using for the launch wrappers: desired.tsv, one line per + * file (tool dir, Proton build, component, file, sha256, source copy), and an unpacked copy of + * every package or older-build original in use to copy from. Rebuilt whole after every change, so + * a restored or deleted choice simply stops being enforced. The Proton's own current-build + * originals are not listed: the wrappers also give Steam's probe runs their own prefix, which is + * what overwrote files in the first place. + */ + private fun syncLaunchState(context: Context, state: JSONObject) { + val dir = launchDir(context) + val store = File(dir, "store").apply { mkdirs() } + val all = protons(context) + val lines = StringBuilder() + val keep = HashSet() + val active = state.sub("active") + for (pid in active.keys()) { + val p = all.firstOrNull { it.id == pid } ?: continue + val comps = active.optJSONObject(pid) ?: continue + for (comp in comps.keys()) { + val a = comps.optJSONObject(comp) ?: continue + if (a.optString("protonVersion") != p.version) continue + val file = a.optString("file") + val (key, wcp) = if (file.startsWith("original ")) { + val v = file.removePrefix("original ") + "original-$pid-${safeName(v)}-$comp" to File(originalsDir(context), "$pid/${safeName(v)}/$comp.wcp") + } else safeName(file.removeSuffix(".wcp")) to File(packagesDir(context), file) + if (!wcp.isFile) continue + val unpacked = File(store, key) + if (!File(unpacked, ".complete").isFile) { + unpacked.deleteRecursively() + readWcp(wcp) { tar -> + while (true) { + val e = tar.nextTarEntry ?: break + val rel = normalize(e.name) + if (e.isDirectory || !rel.startsWith("files/") || rel.split('/').any { it == ".." }) continue + val out = File(unpacked, rel) + out.parentFile?.mkdirs() + FileOutputStream(out).use { tar.copyTo(it) } + } + } + File(unpacked, ".complete").writeText("1\n") + } + keep += key + val files = a.optJSONObject("files") ?: continue + for (rel in files.keys()) { + val src = File(unpacked, rel) + if (!src.isFile) continue + val guestSrc = "/" + src.absolutePath.removePrefix(root(context).absolutePath.trimEnd('/')).trimStart('/') + lines.append(p.dir.name).append('\t').append(p.version).append('\t').append(comp).append('\t') + .append(rel).append('\t').append(files.optString(rel)).append('\t').append(guestSrc).append('\n') + } + } + } + store.listFiles()?.filter { it.isDirectory && it.name !in keep }?.forEach { it.deleteRecursively() } + val list = File(dir, "desired.tsv") + val tmp = File(dir, "desired.tsv.tmp") + tmp.writeText(lines.toString()) + tmp.renameTo(list) + } + // ------------------------------------------------------------------ actions /** Applies a package now, or queues it when a game is running on that Proton. Returns what happened. */ diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt index 6640c7c96..820c9f9eb 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt @@ -147,8 +147,9 @@ fun ComponentsPage( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { Box(Modifier.size(8.dp).background(if (st.queued != null) Color(0xFFFFB86B) else pal.good, CircleShape)) Spacer(Modifier.width(6.dp)) + val fixedAt = if (view.reappliedAt > 0) " · re-applied at launch " + DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(view.reappliedAt * 1000)) else "" Text( - if (st.queued != null) "Next: ${st.queued} · after the game" else "In use: ${st.inUse}", + (if (st.queued != null) "Next: ${st.queued} · after the game" else "In use: ${st.inUse}") + fixedAt, fontSize = 12.5.sp, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false), ) if (st.queued != null) { Spacer(Modifier.width(8.dp)); FocusText("Cancel", pal.signal, onClick = onCancelQueued) } @@ -244,6 +245,7 @@ fun ComponentsPage( Text( "FEX, DXVK and VKD3D-Proton for each Proton the Steam client runs games with.\n\n" + "Tap an installed row to swap it in; it applies the next time a game starts. If a game is running on that Proton, the swap waits until it closes.\n\n" + + "Your choices are also checked right before every game launch: if anything changed a Proton's files (Steam's start-up tests, for one), they are put back first, and the page says \"re-applied at launch\".\n\n" + "Each Proton build's own files are kept as its ORIGINAL, so a Steam update never loses them. Packages come from the Nightlies \"-Linux\" releases (refresh) or Import .wcp.", fontSize = 13.sp, ) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index 8f918fc88..7a4169e70 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -733,7 +733,8 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin val p = view.proton SettingsGroup("Components") { ChoiceRow( - host, "cmp-proton", "Proton", p.version + if (view.inUseByGame) " · in use by the running game" else "", + host, "cmp-proton", "Proton", p.version + (if (view.inUseByGame) " · in use by the running game" else "") + + (if (view.reappliedAt > 0) " · re-applied at launch " + java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT).format(java.util.Date(view.reappliedAt * 1000)) else ""), snap.protons.map { it.proton.id to it.proton.name }, p.id, chipModifier = track("cmp-proton"), onPick = { pick = it }, ) diff --git a/tools/linuxfs/overlay/usr/local/bin/__pycache__/bannerlator-steam-compatcpython-313.pyc b/tools/linuxfs/overlay/usr/local/bin/__pycache__/bannerlator-steam-compatcpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da69fcc7488d97710ad7afbda12d649618391938 GIT binary patch literal 24508 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 literal 0 HcmV?d00001 diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat index 4156846a5..82644dd02 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-compat @@ -70,6 +70,62 @@ TOOL_MANIFEST_VDF = """"manifest" } """ % LAUNCHER + +# Steam runs its own graphics probe through EVERY registered tool at once, ~40 s after logging in, +# all with STEAM_COMPAT_DATA_PATH=.../compatdata/0. A prefix's system32 holds links into the Proton +# that last set it up, and the Wines of the other Protons, still running outside Proton's pfx.lock, +# wrote their own builtin DLLs through those links - into that Proton's tree (seen on device: GE's +# FEX DLLs replaced by Proton Experimental's at session start). So each tool's tool-0 run gets a +# prefix of its own; games, which each have their own compatdata/, are not touched. +BL_PROBE_PREFIX_SETUP = """ +bl_probe_prefix() { + case "${STEAM_COMPAT_DATA_PATH:-}" in + */compatdata/0|*/compatdata/0/) + STEAM_COMPAT_DATA_PATH="${STEAM_COMPAT_DATA_PATH%/}-$1" + mkdir -p "$STEAM_COMPAT_DATA_PATH" + export STEAM_COMPAT_DATA_PATH + echo "bannerlator-proton: Steam's tool-0 run gets its own prefix: $STEAM_COMPAT_DATA_PATH" >&2 ;; + esac +} +""" + +# The Components page's choices, enforced in the one place every game launch passes: the app keeps +# desired.tsv (tool dir, Proton build, component, file, sha256, source copy) and an unpacked copy of +# each package in use. A file that no longer matches - whatever changed it - is put back before +# Proton starts. Only for the Proton build the choice was made for: after a Steam update the build +# differs and the Proton's own new files are left alone until the user chooses again. +BL_COMPONENTS_SETUP = """ +bl_components() { + base=$1 + cm_dir="${HOME:-/root}/.local/share/bannerlator-components" + cm_list="$cm_dir/desired.tsv" + [ -f "$cm_list" ] || return 0 + case "${STEAM_COMPAT_DATA_PATH:-}" in */compatdata/0|*/compatdata/0/|*/compatdata/0-*) return 0 ;; esac + cm_tool=${base##*/} + cm_build=$(awk 'NR == 1 { print $2 }' "$base/version" 2>/dev/null) + cm_fixed=0 + cm_checked=0 + while IFS=$'\\t' read -r t want comp rel sum src; do + [ "$t" = "$cm_tool" ] && [ "$want" = "$cm_build" ] || continue + cm_checked=$((cm_checked + 1)) + f="$base/$rel" + if [ -f "$f" ] && [ "$(sha256sum "$f" | cut -d' ' -f1)" = "$sum" ]; then continue; fi + if [ ! -f "$src" ]; then + echo "bannerlator-proton: components: $comp source missing ($src), left as it is" >&2 + continue + fi + mkdir -p "$(dirname "$f")" + cp -f "$src" "$f.bl-new" && mv -f "$f.bl-new" "$f" || continue + case "$rel" in *.so) chmod 755 "$f" ;; *) chmod 644 "$f" ;; esac + cm_fixed=$((cm_fixed + 1)) + done < "$cm_list" + if [ "$cm_fixed" -gt 0 ]; then + echo "bannerlator-proton: components: re-applied $cm_fixed of $cm_checked file(s) in $cm_tool before this launch" >&2 + printf '%s\\t%s\\t%s\\n' "$(date +%s)" "$cm_tool" "$cm_fixed" >> "$cm_dir/reapplied.log" + fi +} +""" + # Resolves the depot at launch time, so the tool can be registered before the depot exists and # survives the client updating or replacing it. Proton takes its base directory from # dirname(sys.argv[0]) without resolving it, so it is run from the depot itself. @@ -172,9 +228,11 @@ if [ -f "$fake" ]; then *) LD_PRELOAD="$fake${LD_PRELOAD:+:$LD_PRELOAD}"; export LD_PRELOAD ;; esac fi +bl_probe_prefix "bannerlator-proton-arm64" for name in %s; do depot="$steam_root/steamapps/common/$name" if [ -d "$depot/files/bin-arm64" ] && [ -x "$depot/proton" ]; then + bl_components "$depot" bl_directaudio "$depot" bl_game_cpus bl_idtech3 "$@" @@ -188,7 +246,8 @@ exit 1 # and a % format would eat that into a single %, leaving a shell expansion that silently does # something else. LAUNCHER_SH = LAUNCHER_SH.replace( - "verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP, 1) + "verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP + + BL_PROBE_PREFIX_SETUP + BL_COMPONENTS_SETUP, 1) # A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of # that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot @@ -220,6 +279,8 @@ if [ -f "$fake" ]; then *) LD_PRELOAD="$fake${LD_PRELOAD:+:$LD_PRELOAD}"; export LD_PRELOAD ;; esac fi +bl_probe_prefix "${here##*/}" +bl_components "$here" bl_directaudio "$here" bl_game_cpus bl_idtech3 "$@" @@ -384,7 +445,8 @@ def adopt_extras(tools): # ${major%%.*} of its own, and putting it through the format would eat both. body = (EXTRA_WRAPPER_SH % entry_point).replace( 'here=$(dirname "$0")', - 'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP, 1) + 'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP + + BL_PROBE_PREFIX_SETUP + BL_COMPONENTS_SETUP, 1) with open(wrapper, "w", encoding="utf-8") as f: f.write(body) os.chmod(wrapper, 0o755)