Compare commits

...
Author SHA1 Message Date
The412Banner ed2160c137 VAC diagnostics: an attempt to connect is not a connection
Left 4 Dead 2 logs "Connecting to", retries, and refuses a VAC server with
a "Connection Blocked: Please remove plug-ins" dialog that never reaches
the console. Counting the attempt as reaching a server would have reported
an insecure game as connected. Only "Connected to" counts now; an attempt
alone is reported as not connected, naming the dialog to look for.
2026-10-02 12:37:37 -04:00
The412Banner c9fb19b0cd 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.
2026-10-02 12:30:34 -04:00
The412Banner 9a6558d8b3 VAC diagnostics: game console + vac.txt in the session logs
A Source-engine game says whether it started VAC-secure only in its own
console, which reaches a file only with -condebug. With the new Setup
toggle on (off by default), the launch wrapper adds -condebug to Source
and Source 2 titles and notes each launch; at the end of the session each
console.log written during it is copied, scrubbed, into vac/ and vac.txt
quotes the game's secure/insecure lines, any VAC lines in the client's
logs and which client builds are on disk. No connection to a server is
reported as inconclusive rather than guessed.
2026-10-02 11:17:56 -04:00
11 changed files with 314 additions and 7 deletions

No files matched your search

@@ -279,6 +279,7 @@ class MainActivity : ComponentActivity() {
private var emulatorList by mutableStateOf<List<Library.Emulator>>(emptyList())
private var runningLabel by mutableStateOf<String?>(null)
private var logsEnabled by mutableStateOf(true)
private var vacDiagnostics by mutableStateOf(false)
private var showRoms by mutableStateOf(false)
private var homeAppSelected by mutableStateOf(false)
private var homeScreenEnabled by mutableStateOf(false)
@@ -363,7 +364,7 @@ class MainActivity : ComponentActivity() {
busy = busy, stage = stage, percent = percent,
desktopInstalled = desktopInstalled,
offlineAccount = offlineAccount, offline = offline,
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled,
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, vacDiagnostics = vacDiagnostics,
steamGames = steamGames, emulators = emulatorList, running = runningLabel,
frameGen = FrameGen.mode(this),
lossless = lossless,
@@ -461,6 +462,10 @@ class MainActivity : ComponentActivity() {
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
logsEnabled = SessionPrefs.logsEnabled(this)
},
onVacDiagnostics = {
SessionPrefs.setVacDiagnostics(this, !SessionPrefs.vacDiagnostics(this))
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
},
onShareLogs = {
Thread({
val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull()
@@ -1192,6 +1197,7 @@ class MainActivity : ComponentActivity() {
frameGenLabel = FrameGen.label(this)
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
logsEnabled = SessionPrefs.logsEnabled(this)
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
runningLabel = if (SessionState.running) when (SessionState.mode) {
SessionService.MODE_DESKTOP -> "Desktop"
SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program)
@@ -45,6 +45,7 @@ object SessionArtifacts {
}
LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context))
copySteamLogs(context, dir)
VacDiagnostics.collect(context, dir)
// A session the system killed leaves its trace here and nowhere else.
SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log"))
scrubFolder(dir)
@@ -145,6 +146,7 @@ object SessionArtifacts {
)
if (newest) {
copySteamLogs(context, dir)
VacDiagnostics.collect(context, dir)
}
SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log"))
SessionEvents.record("session.artifacts_recovered", mapOf("newest" to newest), dir)
@@ -424,6 +424,18 @@ object SessionPrefs {
prefs(context).edit().putBoolean("logs", on).apply()
}
/**
* VAC diagnostics (see [VacDiagnostics]): Source-engine games launched with -condebug and their
* console plus the client's VAC lines summarised into vac.txt. Off by default: it changes a
* game's command line.
*/
fun vacDiagnostics(context: Context): Boolean =
prefs(context).getBoolean("vac_diagnostics", false)
fun setVacDiagnostics(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("vac_diagnostics", on).apply()
}
// ── Per-mode display ────────────────────────────────────────────────────────────────────
/**
@@ -267,6 +267,7 @@ class SessionService : Service() {
val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() }
killStragglers()
SessionFiles.stage(this, root)
VacDiagnostics.arm(this)
val sessionDir = openSessionFolder()
val sessionLog = File(sessionDir, "session.log")
@@ -0,0 +1,194 @@
package com.droiddeck.launcher.session
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.core.LogRedactor
import com.droiddeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.io.RandomAccessFile
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* VAC diagnostics: what a session can show about whether a game ran VAC-secure, from the files
* Valve's own software writes - nothing is inferred from our side of the launch.
*
* The verdict is the game's to give. A Source-engine game decides secure or insecure once, as it
* starts, and says so only in its own console ("You are in insecure mode. You must restart before
* you can connect to secure servers."). That console reaches a file only when the game runs with
* -condebug, as console.log in the game's mod folder. With this on, the launch wrapper
* (bannerlator-proton, bl_vacdiag) adds -condebug to Source titles and notes each launch; at the end
* of the session this copies each console.log written during it into vac/, scrubbed, and writes
* vac.txt: the game's own secure/insecure lines, any VAC lines in the client's logs, and which
* client files are present.
*
* The SteamLite lesson applies: its "INSECURE - VAC may kick" warning was our own parser reading the
* wrong log while the client was secure all along. So vac.txt quotes the lines it found and says
* "inconclusive" when the game never reached a server, rather than guessing either way.
*/
object VacDiagnostics {
private const val TAG = "VacDiagnostics"
/** In the guest's home; the launch wrapper checks for it. Holds the session's start, epoch seconds. */
private const val MARKER = "root/.droiddeck-vac-diagnostics"
/** One line per game launch the wrapper saw: epoch, app id, -condebug added or not, the exe. */
private const val LAUNCHES = "root/.droiddeck-vac-launches"
/** The console log grows across launches; the end of it is the session that just ran. */
private const val CONSOLE_TAIL_BYTES = 4L * 1024 * 1024
private const val MAX_QUOTED = 40
private val INSECURE = Regex("insecure mode", RegexOption.IGNORE_CASE)
// "Connecting to" is only an attempt: L4D2 logs it, retries, and then refuses a VAC server with a
// dialog ("Connection Blocked: Please remove plug-ins before attempting to connect to a VAC
// secured server") that never reaches the console. Only "Connected to" means the game got in.
private val CONNECTED = Regex("^Connected to", RegexOption.IGNORE_CASE)
private val ATTEMPTED = Regex("^Connecting to", RegexOption.IGNORE_CASE)
private val NOTABLE = Regex("\\bVAC\\b|secure|Connected to|Connecting to|Disconnect|challenge|game coordinator", RegexOption.IGNORE_CASE)
private val STEAM_VAC = Regex("\\bVAC\\b|anti-?cheat", RegexOption.IGNORE_CASE)
fun enabled(context: Context): Boolean = SessionPrefs.vacDiagnostics(context)
/**
* At session start, after the scripts are staged: arms the wrapper for this session (or disarms
* it) and starts a fresh launch list, so the summary names only games this session ran.
*/
fun arm(context: Context) {
val root = LinuxRuntime.rootDir(context)
val marker = File(root, MARKER)
try {
File(root, LAUNCHES).delete()
if (enabled(context)) marker.writeText("${System.currentTimeMillis() / 1000}\n")
else marker.delete()
} catch (e: Exception) {
Log.w(TAG, "could not arm VAC diagnostics", e)
}
}
/** At the end of a session, into its folder. Does nothing unless the session was armed. */
fun collect(context: Context, dir: File) {
val root = LinuxRuntime.rootDir(context)
val marker = File(root, MARKER)
if (!marker.isFile) return
try {
val since = (marker.readText().trim().toLongOrNull() ?: 0L) * 1000 - 5_000
val out = File(dir, "vac").apply { mkdirs() }
val consoles = consoleLogs(context).filter { it.lastModified() >= since }
val report = StringBuilder()
report.appendLine("VAC diagnostics - ${now()}")
report.appendLine("Read from the game's own console (-condebug) and the Steam client's logs.")
report.appendLine()
report.appendLine("== Client")
clientFiles(root).forEach { report.appendLine(" $it") }
report.appendLine()
report.appendLine("== Game launches this session (from the launch wrapper)")
val launches = File(root, LAUNCHES).takeIf { it.isFile }?.readLines().orEmpty().filter { it.isNotBlank() }
if (launches.isEmpty()) report.appendLine(" none - no Windows game was launched through the client")
launches.forEach { report.appendLine(" " + LogRedactor.redact(it)) }
report.appendLine()
report.appendLine("== Game consoles")
if (consoles.isEmpty()) {
report.appendLine(" No console.log was written this session. Only Source-engine games write one,")
report.appendLine(" and only when started with -condebug (this setting adds it).")
}
consoles.forEach { log ->
val name = consoleName(log)
val lines = tail(log)
File(out, "$name-console.log").bufferedWriter().use { w ->
lines.forEach { w.write(LogRedactor.redact(it)); w.newLine() }
}
report.appendLine("-- $name (vac/$name-console.log)")
report.appendLine(" Verdict: " + verdict(lines))
lines.filter { NOTABLE.containsMatchIn(it) }.takeLast(MAX_QUOTED)
.forEach { report.appendLine(" | " + LogRedactor.redact(it.trim())) }
report.appendLine()
}
report.appendLine("== VAC in the Steam client's logs")
val steamHits = File(dir, "steam").listFiles { f -> f.isFile }?.sortedBy { it.name }.orEmpty()
.flatMap { f -> runCatching { f.readLines() }.getOrDefault(emptyList()).filter { STEAM_VAC.containsMatchIn(it) }.map { "${f.name}: ${it.trim()}" } }
if (steamHits.isEmpty()) report.appendLine(" none - the client logged no VAC activity this session")
steamHits.takeLast(MAX_QUOTED).forEach { report.appendLine(" $it") }
File(dir, "vac.txt").writeText(report.toString())
Log.i(TAG, "VAC diagnostics: ${consoles.size} console log(s), ${steamHits.size} client VAC line(s), into $dir")
} catch (e: Exception) {
Log.w(TAG, "collecting VAC diagnostics", e)
}
}
private fun verdict(lines: List<String>): String = when {
lines.any { INSECURE.containsMatchIn(it) } ->
"INSECURE - the game started in insecure mode and cannot join VAC-secured servers until it restarts."
lines.any { CONNECTED.containsMatchIn(it) } ->
"No insecure notice, and the game connected to a server. Confirm with the server's own info that it is VAC-secured."
lines.any { ATTEMPTED.containsMatchIn(it) } ->
"NOT CONNECTED - the game tried a server and never got in. If the screen said \"Connection Blocked: " +
"Please remove plug-ins ... VAC secured server\" (Left 4 Dead 2's wording), the game is insecure; " +
"that dialog never reaches the console."
else ->
"Inconclusive - the game never connected to a server. Join a VAC-secured server, then check again."
}
/** Every console.log in a Steam library: <game>/<mod>/console.log (Source) and <game>/game/<mod>/console.log (Source 2). */
private fun consoleLogs(context: Context): List<File> {
val found = LinkedHashMap<String, File>()
for (library in libraryRoots(context)) {
val games = File(library, "steamapps/common").listFiles { f -> f.isDirectory } ?: continue
for (game in games) {
val mods = (game.listFiles { f -> f.isDirectory }.orEmpty().toList() +
File(game, "game").listFiles { f -> f.isDirectory }.orEmpty().toList())
for (mod in mods) {
val log = File(mod, "console.log")
if (log.isFile) found[runCatching { log.canonicalPath }.getOrDefault(log.path)] = log
}
}
}
return found.values.toList()
}
private fun libraryRoots(context: Context): List<File> {
val root = LinuxRuntime.rootDir(context)
val steam = File(root, "root/.local/share/Steam")
val roots = mutableListOf(steam)
GameStorage.effective(context)?.let { roots.add(File(it.path)) }
runCatching {
Regex("\"path\"\\s+\"([^\"]+)\"").findAll(File(steam, "steamapps/libraryfolders.vdf").readText()).forEach {
roots.add(File(root, it.groupValues[1].removePrefix("/")))
}
}
return roots.filter { it.isDirectory }.distinctBy { runCatching { it.canonicalPath }.getOrDefault(it.path) }
}
/** "Team Fortress 2-tf": the game folder and the mod, safe as a file name. */
private fun consoleName(log: File): String {
val mod = log.parentFile
val game = mod?.parentFile?.let { if (it.name == "game") it.parentFile else it }
return "${game?.name ?: "game"}-${mod?.name ?: "mod"}".replace(Regex("[^A-Za-z0-9._ +-]+"), "_")
}
private fun tail(file: File): List<String> = try {
RandomAccessFile(file, "r").use { raf ->
val start = (raf.length() - CONSOLE_TAIL_BYTES).coerceAtLeast(0)
raf.seek(start)
val bytes = ByteArray((raf.length() - start).toInt())
raf.readFully(bytes)
String(bytes, Charsets.UTF_8).lines().let { if (start > 0) it.drop(1) else it }
}
} catch (e: Exception) {
emptyList()
}
/** Which Steam client builds are on disk - the arm64 one has no VAC module of its own to run. */
private fun clientFiles(root: File): List<String> {
val steam = File(root, "root/.local/share/Steam")
return listOf(
"linuxarm64/steamclient.so" to "arm64 Linux client",
"linux64/steamclient.so" to "x86-64 Linux client",
"ubuntu12_64/steamclient.so" to "x86-64 Linux client (ubuntu12_64)",
).map { (path, label) -> "$label: ${if (File(steam, path).isFile) "present" else "absent"} ($path)" }
}
private fun now(): String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
}
@@ -91,6 +91,7 @@ class FrontEndState(
val frameGenLabel: String,
val romsDir: String?,
val logsEnabled: Boolean,
val vacDiagnostics: Boolean = false,
val steamGames: List<Library.SteamGame>,
val emulators: List<Library.Emulator>,
val running: String?,
@@ -152,6 +153,7 @@ class FrontEndActions(
val onFiles: () -> Unit,
val onBrowseFiles: (File) -> Unit = {},
val onLogs: () -> Unit,
val onVacDiagnostics: () -> Unit = {},
val onShareLogs: () -> Unit = {},
val onClearLogs: () -> Unit = {},
val onOffline: () -> Unit,
@@ -289,6 +289,7 @@ internal fun SetupPanel(
}
}
ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() }
ToggleRow(host, "vacdiag", stringResource(R.string.setup_vac_diag), stringResource(R.string.setup_vac_diag_hint), s.vacDiagnostics) { a.onVacDiagnostics() }
ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs)
ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs)
ToggleRow(
+2
View File
@@ -483,6 +483,8 @@
<string name="setup_session">Sesión</string>
<string name="setup_logs">Registros de sesión</string>
<string name="setup_logs_hint">Se guardan tras cada sesión</string>
<string name="setup_vac_diag">Diagnóstico de VAC</string>
<string name="setup_vac_diag_hint">Los juegos Source registran su consola (-condebug); vac.txt en los registros dice si es seguro o no</string>
<string name="setup_latest_logs">Últimos registros de sesión</string>
<string name="setup_saved_logs">Registros de sesión guardados</string>
<string name="setup_saved_logs_hint">Conserva los %1$d más recientes</string>
+2
View File
@@ -508,6 +508,8 @@
<string name="setup_session">Session</string>
<string name="setup_logs">Session logs</string>
<string name="setup_logs_hint">Saved after each session</string>
<string name="setup_vac_diag">VAC diagnostics</string>
<string name="setup_vac_diag_hint">Source games log their console (-condebug); vac.txt in the session logs says secure or insecure</string>
<string name="setup_latest_logs">Latest session logs</string>
<string name="setup_saved_logs">Saved session logs</string>
<string name="setup_saved_logs_hint">Keeps the newest %1$d</string>
@@ -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
@@ -189,6 +189,37 @@ bl_idtech3() {
}
"""
# VAC diagnostics (the app's Setup page): a Source-engine game says whether it started VAC-secure
# only in its own console, and writes that console to <mod>/console.log only with -condebug. Armed
# per session by the app (~/.droiddeck-vac-diagnostics); each launch is noted for its summary.
# Nothing else about the launch changes - this only asks the game to keep its own log.
BL_VACDIAG_SETUP = """
bl_vacdiag() {
[ -f "${HOME:-/root}/.droiddeck-vac-diagnostics" ] || return 0
[ "${verb:-}" = waitforexitandrun ] || return 0
local exe= a dir added=no app=${SteamAppId:-${STEAM_COMPAT_APP_ID:-0}}
for a in "$@"; do case $a in *.exe|*.EXE) exe=$a ;; esac; done
[ -n "$exe" ] || return 0
dir=$(dirname "$exe")
# Source: <game>/<mod>/gameinfo.txt beside the exe. Source 2: <game>/game/bin/win64/<exe> with
# <game>/game/<mod>/gameinfo.gi.
if ls "$dir"/*/gameinfo.txt >/dev/null 2>&1 || ls "$dir"/../../*/gameinfo.gi >/dev/null 2>&1; then
case " $* " in
*" -condebug "*) echo "bannerlator-proton: vac-diag: app $app already has -condebug" >&2 ;;
*) BL_EXTRA_ARGS+=(-condebug); added=yes
echo "bannerlator-proton: vac-diag: app $app is a Source title; launched with -condebug" >&2 ;;
esac
else
added=not-source
echo "bannerlator-proton: vac-diag: app $app is not a Source title; it keeps no console log to read" >&2
fi
case " $* " in
*" -insecure "*) echo "bannerlator-proton: vac-diag: app $app has -insecure in its launch options - it can never be VAC-secure this way" >&2 ;;
esac
printf '%s\tapp %s\tcondebug %s\t%s\n' "$(date +%s)" "$app" "$added" "${exe##*/}" >> "${HOME:-/root}/.droiddeck-vac-launches"
}
"""
BL_DIRECTAUDIO_SETUP = """
bl_directaudio() {
base=$1
@@ -271,6 +302,7 @@ if [ -n "$depot" ]; then
bl_directaudio "$depot"
bl_game_cpus
bl_idtech3 "$@"
bl_vacdiag "$@"
bl_libpath
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$depot/proton" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
fi
@@ -282,7 +314,7 @@ exit 1
# something else.
LAUNCHER_SH = LAUNCHER_SH.replace(
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
+ BL_VACDIAG_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_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
@@ -318,6 +350,7 @@ bl_components "$here"
bl_directaudio "$here"
bl_game_cpus
bl_idtech3 "$@"
bl_vacdiag "$@"
bl_libpath
# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be
# started from its own tree.
@@ -503,7 +536,7 @@ def adopt_extras(tools):
body = (EXTRA_WRAPPER_SH % entry_point).replace(
'here=$(dirname "$0")',
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
+ BL_VACDIAG_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
write_changed(wrapper, body, 0o755)
write_changed(manifest, EXTRA_MANIFEST_VDF)
write_changed(compat_vdf, EXTRA_COMPAT_TOOL_VDF % (name, entry))
@@ -546,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}
@@ -555,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:
@@ -579,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):
@@ -633,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__":