#!/bin/bash
set -eu
ROOTFS_URL=https://github.com/Droid-Deck/DroidDeck-Components/releases/download/fex-rootfs-ubuntu-24.04-2026-08-11/Ubuntu_24_04.sqsh
ROOTFS_SHA256=2854b06d3ff1b8f6e526135bfb6dd5b7b30ab3ab73e79ae933a3d9fed959a178
CDN=https://client-update.fastly.steamstatic.com
# Must match the -clientbeta droiddeck-session starts this client with (steam_channel).
X64_CHANNEL=steamdeck_publicbeta
MANIFEST=steam_client_${X64_CHANNEL}_steamrt64

REAL_HOME=${HOME:-/root}
X64_HOME=${BL_X64_HOME:-$REAL_HOME/.droiddeck-x64}
ROOTFS=$X64_HOME/rootfs
STEAM_X64=$X64_HOME/.local/share/Steam
ARM_STEAM=${XDG_DATA_HOME:-$REAL_HOME/.local/share}/Steam
LOG=$X64_HOME/steam-x64.log
mkdir -p "$X64_HOME"
step() { echo "== STEP $(date -u +%H:%M:%S) $*" | tee -a "$LOG"; }
. "${BASH_SOURCE[0]%/*}/droiddeck-fex-update"

x64_default_dir() {
  local d
  while IFS= read -r d; do
    [ -f "$d/.droiddeck-arm64-proton" ] && { echo "$d"; return; }
  done < <(ls -d "$STEAM_X64"/compatibilitytools.d/DroidDeck-Proton* 2>/dev/null | sort -V -r
           ls -d "$STEAM_X64"/compatibilitytools.d/GE-Proton*-aarch64 2>/dev/null | sort -V -r)
  echo "$STEAM_X64/compatibilitytools.d/droiddeck-proton-arm64"
}
x64_default_tool() {
  sed -n 's/^[[:space:]]*"\([^"]*\)"[[:space:]]*$/\1/p' "$(x64_default_dir)/compatibilitytool.vdf" 2>/dev/null |
    grep -v -x -e compatibilitytools -e compat_tools | head -n 1
}

x64_client_parts() {
  awk -F'"' '
    /^\t"/ && NF >= 3 && $4 == "" { block = $2 }
    $2 == "file" { file[block] = $4 }
    $2 == "sha2" { sha[block] = $4 }
    END { for (b in file) if (b ~ /_all$/ || b ~ /_steamrt64$/) print b, file[b], sha[b] }
  ' "$1" | sort
}

x64_sync_client() {
  local chan cur version work total i block file sum old
  chan=$(cat "$STEAM_X64/package/beta" 2>/dev/null || echo "$X64_CHANNEL")
  old=$STEAM_X64/package/steam_client_${chan}_steamrt64.manifest
  cur=$(awk -F'"' '$2 == "version" { print $4; exit }' "$old" 2>/dev/null || true)
  work=$(mktemp -d)
  if ! curl -fsSL "$CDN/steam_client_${chan}_steamrt64" -o "$work/manifest"; then
    rm -rf "$work"; return 0
  fi
  version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
  if [ -z "$version" ] || [ "$version" = "$cur" ]; then
    rm -rf "$work"; return 0
  fi
  x64_client_parts "$work/manifest" > "$work/parts"
  if [ -f "$old" ]; then
    x64_client_parts "$old" | awk '{ print $3 }' > "$work/have"
    grep -vwF -f "$work/have" "$work/parts" > "$work/todo" || true
  else
    cp "$work/parts" "$work/todo"
  fi
  total=$(grep -c . "$work/todo" || true)
  step "updating the x86-64 Steam client to $version (was ${cur:-none}, $total part(s))"
  i=0
  while read -r block file sum; do
    i=$((i + 1))
    step "downloading Steam: $block ($i/$total)"
    if ! curl -fsSL "$CDN/$file" -o "$work/$file" || ! echo "$sum  $work/$file" | sha256sum -c --quiet; then
      step "could not update the x86-64 client; it updates itself"
      rm -rf "$work"; return 0
    fi
    unzip -qo "$work/$file" -d "$STEAM_X64"
    rm -f "$work/$file"
  done < "$work/todo"
  cp -f "$work/manifest" "$old"
  echo "$version" > "$STEAM_X64/package/droiddeck-installed"
  chmod +x "$STEAM_X64/steam.sh" "$STEAM_X64/steamrt64/steam" "$STEAM_X64/ubuntu12_32/steam" 2>/dev/null || true
  rm -rf "$work"
}

# The client rewrites config.vdf from memory as it exits: the mapping is written only while no x86 client runs.
x64_map_direct() {
  local mode=write
  [ "${1:-}" = --live ] && mode=live
  [ -f "$STEAM_X64/config/config.vdf" ] || return 0
  if [ "$mode" = write ] && pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then return 0; fi
  python3 - "$mode" "$STEAM_X64/config/config.vdf" "$STEAM_X64/appcache/appinfo.vdf" "$X64_HOME/direct-auto.json" \
    "$X64_HOME/direct-sniper" "$STEAM_X64/steamapps:/mnt/droiddeck-sd/x86-64/steamapps:$X64_HOME/shared/arm64/steamapps" "$(x64_default_tool)" \
    "$REAL_HOME/.bl-compat-pending" ${BL_X64_DIRECT_APPS:-550} \
    <<'PY' || { [ "$mode" = live ] || step "could not map titles to DroidDeck direct"; }
# Steam does not swap an installed build when the tool changes: a title installed for Windows stays on Proton.
import json, os, re, struct, sys
mode, path, appinfo, auto_file, sniper_file, steamapps, proton, pending = sys.argv[1:9]
extra = sys.argv[9:]
WINDOWS_DEPOTS = {}
KNOWN = set()


def read_kv(buf, pos, strings):
    out = {}
    while True:
        t = buf[pos]
        pos += 1
        if t == 0x08:
            return out, pos
        if strings is not None:
            key = strings[struct.unpack_from("<I", buf, pos)[0]]
            pos += 4
        else:
            end = buf.index(b"\0", pos)
            key = buf[pos:end].decode("utf-8", "replace")
            pos = end + 1
        if t == 0x00:
            out[key], pos = read_kv(buf, pos, strings)
        elif t == 0x01:
            end = buf.index(b"\0", pos)
            out[key] = buf[pos:end].decode("utf-8", "replace")
            pos = end + 1
        elif t in (0x02, 0x03, 0x04, 0x06):
            pos += 4
        elif t in (0x07, 0x0A):
            pos += 8
        else:
            raise ValueError("type %#x" % t)


def linux_games(file):
    """{appid: runtime} for the games with a Linux build in the client's appinfo cache."""
    try:
        buf = open(file, "rb").read()
    except OSError:
        return {}
    magic = struct.unpack_from("<I", buf, 0)[0]
    strings, pos = None, 8
    if magic == 0x07564429:
        table = struct.unpack_from("<q", buf, 8)[0]
        count, p = struct.unpack_from("<I", buf, table)[0], table + 4
        strings = []
        for _ in range(count):
            end = buf.index(b"\0", p)
            strings.append(buf[p:end].decode("utf-8", "replace"))
            p = end + 1
        pos = 16
    elif magic != 0x07564428:
        return {}
    games = {}
    while True:
        try:
            appid, size = struct.unpack_from("<II", buf, pos)
        except struct.error:
            break
        if appid == 0:
            break
        KNOWN.add(str(appid))
        try:
            info = read_kv(buf, pos + 8 + 60, strings)[0].get("appinfo", {})
        except (ValueError, IndexError, struct.error):
            info = {}
        pos += 8 + size
        common = info.get("common", {})
        if str(common.get("type", "")).lower() != "game" or "linux" not in str(common.get("oslist", "")):
            continue
        runtime = "scout"
        for m in (info.get("config", {}).get("app_mappings") or {}).values():
            if isinstance(m, dict) and m.get("platform") == "linux":
                tool = str(m.get("tool", "")).lower()
                runtime = "sniper" if "sniper" in tool else "soldier" if "soldier" in tool else runtime
        games[str(appid)] = runtime
        WINDOWS_DEPOTS[str(appid)] = {
            d for d, v in (info.get("depots") or {}).items()
            if isinstance(v, dict) and "windows" in str((v.get("config") or {}).get("oslist", ""))
            and "linux" not in str((v.get("config") or {}).get("oslist", ""))
        }
    return games


def manifest(app):
    """The app's appmanifest text from the first of the client's libraries (internal storage, the
    card) holding one, or None."""
    for library in steamapps.split(":"):
        try:
            return open(os.path.join(library, "appmanifest_%s.acf" % app), encoding="utf-8", errors="replace").read()
        except OSError:
            pass
    return None


def installed_apps():
    found = set()
    for library in steamapps.split(":"):
        try:
            found |= {m.group(1) for m in map(re.compile(r"appmanifest_(\d+)\.acf$").match, os.listdir(library)) if m}
        except OSError:
            pass
    return found


def new_linux_build(app):
    """For a title the appinfo cache does not know yet (bought while the client runs): installed as
    itself, not as a Windows build through a compatibility tool, and not one of Valve's runtimes or
    Protons, which the client installs as apps too."""
    text = manifest(app) or ""
    if re.search(r'"platform_override_source"\s*"windows"', text, re.I):
        return False
    folder = re.search(r'"installdir"\s*"([^"]*)"', text)
    return not re.match(r"steam ?linux ?runtime|proton|steamworks shared", folder.group(1) if folder else "", re.I)


def windows_installed(app):
    """Whether the installed build of `app` is its Windows one (a Windows depot among those installed),
    in any of the client's libraries (internal storage, the card)."""
    text = manifest(app)
    if text is None:
        return False
    found = block(text, "InstalledDepots")
    if not found:
        return False
    installed = set(re.findall(r'"(\d+)"\s*\{', text[found[1]:found[2]]))
    return bool(installed & WINDOWS_DEPOTS.get(app, set()))


def block(text, key):
    """(start of the key, index after its opening brace, index of its closing brace) or None."""
    m = re.search(r'"%s"\s*\{' % re.escape(key), text)
    if not m:
        return None
    depth, k = 1, m.end()
    while depth:
        c = text[k]
        depth += c == "{"
        depth -= c == "}"
        k += 1
    return m.start(), m.end(), k - 1


games = linux_games(appinfo)
for app in extra:
    games.setdefault(app, "scout")
s = open(path, encoding="utf-8", errors="surrogateescape").read()
if not block(s, "CompatToolMapping"):
    st = block(s, "Steam")
    if not st:
        sys.exit("no Steam section")
    s = s[:st[1]] + '\n\t\t\t\t"CompatToolMapping"\n\t\t\t\t{\n\t\t\t\t}' + s[st[1]:]


def entries(inner):
    """{app: its entry's span in `inner`, as block() gives it}, read once for the whole mapping."""
    spans, pos = {}, 0
    for m in re.finditer(r'"(\d+)"\s*\{', inner):
        if m.start() < pos:
            continue
        depth, k = 1, m.end()
        while depth and k < len(inner):
            depth += {"{": 1, "}": -1}.get(inner[k], 0)
            k += 1
        spans.setdefault(m.group(1), (m.start(), m.end(), k - 1))
        pos = k
    return spans


def decide(app, inner, old):
    """(the tool `app` goes to, or None when its entry stays; its entry's span `old` in `inner`; the
    tool that entry names, None without one)."""
    on_windows = windows_installed(app)
    name = None
    if old:
        name = re.search(r'"name"\s*"([^"]*)"', inner[old[1]:old[2]])
        name = name.group(1) if name else ""
        automatic = name in ("", "droiddeck-direct", "droiddeck-proton-arm64", "droiddeck-fex") or name.startswith("proton_")
        if not automatic and app not in extra:
            return None, old, name
        if on_windows and (name == proton or not proton):
            return None, old, name
    elif on_windows and not proton:
        return None, old, name
    return (proton if on_windows else "droiddeck-direct"), old, name


if mode == "live":
    _, open_, close = block(s, "CompatToolMapping")
    inner = s[open_:close]
    spans = entries(inner)
    new = []
    for app in sorted(installed_apps(), key=int):
        if app not in games and (app in KNOWN or not new_linux_build(app)):
            continue
        tool, _, name = decide(app, inner, spans.get(app))
        if tool == "droiddeck-direct" and name != tool:
            new.append(app)
    if new:
        open(pending, "a").write("".join("%s\n" % a for a in new))
        print("droiddeck-steam-x64: installed while the client runs, for DroidDeck direct at its restart: %s" % " ".join(new))
    sys.exit(0)
_, open_, close = block(s, "CompatToolMapping")
inner = s[open_:close]
spans = entries(inner)
mapped, windows, added, dropped = [], [], [], []
for app in sorted(games, key=int):
    tool, old, _ = decide(app, inner, spans.get(app))
    if tool is None:
        continue
    if old:
        dropped.append(old)
    added.append('\n\t\t\t\t\t"%s"\n\t\t\t\t\t{\n\t\t\t\t\t\t"name"\t\t"%s"\n'
                 '\t\t\t\t\t\t"config"\t\t""\n\t\t\t\t\t\t"priority"\t\t"250"\n\t\t\t\t\t}' % (app, tool))
    (mapped if tool == "droiddeck-direct" else windows).append(app)
for old in sorted(dropped, reverse=True):
    inner = inner[:old[0]] + inner[old[2] + 1:]
s = s[:open_] + "".join(reversed(added)) + inner + s[close:]
open(path, "w", encoding="utf-8", errors="surrogateescape").write(s)
json.dump(mapped, open(auto_file, "w"))
open(sniper_file, "w").write("".join("%s\n" % a for a in mapped if games[a] != "scout"))
print("droiddeck-steam-x64: %d Linux title(s) on DroidDeck direct (%d on the newer runtime)%s"
      % (len(mapped), sum(games[a] != "scout" for a in mapped),
         "; %d installed as Windows games kept on %s" % (len(windows), proton) if windows else ""))
PY
}

# A resumable download that reports every tenth of its size as a step.
x64_fetch() {
  local url=$1 out=$2 label=$3 total pid rc=0 last=-1 pct
  total=$(curl -fsSIL "$url" 2>/dev/null | tr -d '\r' | awk 'tolower($1) == "content-length:" { v = $2 } END { print v + 0 }')
  curl -fL -sS -C - "$url" -o "$out" &
  pid=$!
  while kill -0 "$pid" 2>/dev/null; do
    sleep 3
    [ "$total" -gt 0 ] || continue
    pct=$(( $(stat -c %s "$out" 2>/dev/null || echo 0) * 10 / total * 10 ))
    if [ "$pct" -gt "$last" ]; then last=$pct; step "$label ($pct%)"; fi
  done
  wait "$pid" || rc=$?
  return "$rc"
}

x64_prepare() {
fex_ready || { step "FEX could not be installed; see $LOG"; exit 1; }
step "FEX ready ($FEX_NOW, $FEX_FROM)"
# The client's opt-in to its SteamRT3 build; without it, it re-executes through steam.sh, whose checks fail under proot.
mkdir -p "$STEAM_X64" && : > "$STEAM_X64/.steam-enable-steamrt64-client"

if [ ! -f "$ROOTFS/.droiddeck-complete" ]; then
  # Resumes a part-done download; a finished one answers 416, which is not a failure here.
  x64_fetch "$ROOTFS_URL" "$X64_HOME/rootfs.sqsh" "downloading FEX's Ubuntu 24.04 x86 rootfs" || {
    rc=$?
    [ "$rc" = 22 ] && [ -s "$X64_HOME/rootfs.sqsh" ] || { step "rootfs download failed (curl $rc)"; exit 1; }
  }
  if ! echo "$ROOTFS_SHA256  $X64_HOME/rootfs.sqsh" | sha256sum -c --quiet >/dev/null 2>&1; then
    rm -f "$X64_HOME/rootfs.sqsh"; step "rootfs download did not match its checksum; try again"; exit 1
  fi
  step "unpacking the rootfs"
  rm -rf "$ROOTFS"
  /usr/local/bin/unsquashfs -no-xattrs -no-progress -ignore-errors -d "$ROOTFS" "$X64_HOME/rootfs.sqsh" >>"$LOG" 2>&1 || true
  [ -x "$ROOTFS/usr/bin/bash" ] || { step "rootfs unpack failed (no usr/bin/bash); see $LOG"; exit 1; }
  rm -f "$X64_HOME/rootfs.sqsh"
  touch "$ROOTFS/.droiddeck-complete"
fi
echo "== x86-64 rootfs: $(sed -n 's|.*/download/\([^/]*\)/.*|\1|p' <<<"$ROOTFS_URL") (sha256 ${ROOTFS_SHA256:0:12})" >&2
# FEX falls back to the guest's own /etc/ld.so.preload (arm64 libraries) when the rootfs has none. The x86
# session shim provides System V IPC, which Android kernels lack and the client's tier0 needs to start.
: > "$ROOTFS/etc/ld.so.preload"
for lib in libblsession libfakeinput; do
  have=0
  # 32-bit libfakeinput crashes SDL2 games (a NULL udev attribute reaches strdup), so only the x86_64 copy
  # is preloaded, by full path: with $LIB, FEX falls through to the session's own i386 copy.
  arches="x86_64 i386"; [ "$lib" = libfakeinput ] && arches=x86_64
  rm -f "$ROOTFS/usr/local/lib/i386-linux-gnu/libfakeinput.so"
  for arch in $arches; do
    [ -f "/usr/local/lib/droiddeck-fex/$arch/$lib.so" ] || continue
    mkdir -p "$ROOTFS/usr/local/lib/$arch-linux-gnu"
    cp -f "/usr/local/lib/droiddeck-fex/$arch/$lib.so" "$ROOTFS/usr/local/lib/$arch-linux-gnu/$lib.so"
    have=1
  done
  if [ "$have" = 1 ] && [ "$lib" = libfakeinput ]; then
    echo "/usr/local/lib/x86_64-linux-gnu/$lib.so" >> "$ROOTFS/etc/ld.so.preload"
  elif [ "$have" = 1 ]; then echo "/usr/local/\$LIB/$lib.so" >> "$ROOTFS/etc/ld.so.preload"
  else step "warning: no x86 build of $lib (/usr/local/lib/droiddeck-fex/x86_64/$lib.so)"; fi
done
rm -f "$ROOTFS/usr/local/lib/libblsession-x86_64.so" "$ROOTFS/usr/local/lib/libfakeinput-x86_64.so"
unconfirmed=$STEAM_X64/package/droiddeck-unconfirmed
if [ -d "${unconfirmed%/*}" ] && ! grep -qx tracked "$unconfirmed" 2>/dev/null; then echo tracked > "$unconfirmed"; fi
x64_repair=
if [ "${BL_X64_REPAIR:-0}" = 1 ]; then
  x64_repair=requested
elif [ -f "$unconfirmed" ] && ! grep -qx repaired "$unconfirmed" && [ "$(grep -cx start "$unconfirmed")" -ge 2 ]; then
  x64_repair="the client was started $(grep -cx start "$unconfirmed") times without showing anything"
fi
if [ -n "$x64_repair" ]; then
  step "repairing the x86-64 Steam client ($x64_repair)"
  rm -f "$STEAM_X64/package/droiddeck-installed" "$STEAM_X64/.crash" "$STEAM_X64/steam.pid" \
    "$STEAM_X64"/package/steam_client_*.installed
fi
if [ ! -x "$STEAM_X64/steamrt64/steam" ] || [ ! -f "$STEAM_X64/package/droiddeck-installed" ]; then
  step "downloading the x86-64 Steam client ($MANIFEST)"
  work=$(mktemp -d)
  curl -fsSL "$CDN/$MANIFEST" -o "$work/manifest"
  version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
  awk -F'"' '
    /^\t"/ && NF >= 3 && $4 == "" { block = $2 }
    $2 == "file" { file[block] = $4 }
    $2 == "sha2" { sha[block] = $4 }
    END { for (b in file) if (b ~ /_all$/ || b ~ /_steamrt64$/) print b, file[b], sha[b] }
  ' "$work/manifest" | sort > "$work/parts"
  [ -s "$work/parts" ] || { step "no client components in $MANIFEST"; exit 1; }
  mkdir -p "$STEAM_X64/package"
  total=$(grep -c . "$work/parts")
  i=0
  while read -r block file sum; do
    i=$((i + 1))
    step "client: $block ($i/$total)"
    curl -fsSL "$CDN/$file" -o "$work/$file"
    echo "$sum  $work/$file" | sha256sum -c --quiet
    unzip -qo "$work/$file" -d "$STEAM_X64"
    rm -f "$work/$file"
  done < "$work/parts"
  cp -f "$work/manifest" "$STEAM_X64/package/$MANIFEST.manifest"
  rm -rf "$work"
  echo "$X64_CHANNEL" > "$STEAM_X64/package/beta"
  echo "$version" > "$STEAM_X64/package/droiddeck-installed"
  chmod +x "$STEAM_X64/steam.sh" "$STEAM_X64/steamrt64/steam" "$STEAM_X64/ubuntu12_32/steam" 2>/dev/null || true
fi
[ -n "$x64_repair" ] && printf 'tracked\nrepaired\n' > "$unconfirmed"
x64_sync_client
scout=$STEAM_X64/ubuntu12_32
if [ -f "$scout/steam-runtime.tar.xz" ] &&
   { [ ! -x "$scout/steam-runtime/run.sh" ] ||
     ! cmp -s "$scout/steam-runtime.tar.xz.checksum" "$scout/steam-runtime/checksum"; }; then
  step "unpacking Valve's scout runtime for native Linux games"
  rm -rf "$scout/steam-runtime.new"
  mkdir -p "$scout/steam-runtime.new"
  if tar -xJf "$scout/steam-runtime.tar.xz" -C "$scout/steam-runtime.new" && [ -x "$scout/steam-runtime.new/steam-runtime/run.sh" ]; then
    rm -rf "$scout/steam-runtime"
    mv "$scout/steam-runtime.new/steam-runtime" "$scout/steam-runtime"
    cp -f "$scout/steam-runtime.tar.xz.checksum" "$scout/steam-runtime/checksum" 2>/dev/null || true
  else
    step "could not unpack the scout runtime; native Linux games will not start"
  fi
  rm -rf "$scout/steam-runtime.new"
fi
[ -L "$STEAM_X64/steamapps" ] && rm -f "$STEAM_X64/steamapps"
mkdir -p "$STEAM_X64/steamapps"
# FEX looks in the rootfs first: an empty dev/dri there hides the session's KGSL node, which x86 programs spin on.
mkdir -p "$ROOTFS/dev/dri"
mkdir -p "$X64_HOME/.steam"
ln -sfn "$STEAM_X64" "$X64_HOME/.steam/root"
ln -sfn "$STEAM_X64" "$X64_HOME/.steam/steam"
ln -sfn "$STEAM_X64/ubuntu12_32" "$X64_HOME/.steam/bin32"
ln -sfn "$STEAM_X64/ubuntu12_64" "$X64_HOME/.steam/bin64"
ln -sfn "$STEAM_X64/linux32" "$X64_HOME/.steam/sdk32"
ln -sfn "$STEAM_X64/linux64" "$X64_HOME/.steam/sdk64"
# Valve's Steam Runtime container needs user namespaces and a session-bus launcher; steamrt-shim stands in for both.
droiddeck-steamrt-shim "$STEAM_X64/steamrt64" "$X64_HOME/steamrt-shim" || rm -rf "$X64_HOME/steamrt-shim"
# Valve's platform libraries ship without soname links (pressure-vessel makes them); ldconfig adds them here.
srt_libs=$X64_HOME/steamrt-libs
rm -rf "$srt_libs"
mkdir -p "$srt_libs"
for d in "$STEAM_X64"/steamrt64/pv-runtime/steam-runtime-steamrt/steamrt3c_platform_*/files/lib/x86_64-linux-gnu \
         "$STEAM_X64"/steamrt64/pv-runtime/steam-runtime-steamrt/steamrt3c_platform_*/files/usr/lib/x86_64-linux-gnu; do
  [ -d "$d" ] || continue
  for f in "$d"/*.so*; do [ -e "$srt_libs/$(basename "$f")" ] || ln -s "$f" "$srt_libs/"; done
done
FEX_ROOTFS=$ROOTFS /usr/local/bin/FEX "$ROOTFS/usr/sbin/ldconfig.real" -n "$srt_libs" >>"$LOG" 2>&1 || step "ldconfig on $srt_libs failed"
# The rootfs's HarfBuzz needs a newer FreeType than some games bundle (Counter-Strike 2): use Valve's runtime one.
sniper_compat=$X64_HOME/sniper-compat
rm -rf "$sniper_compat"
mkdir -p "$sniper_compat"
hb=$(ls "$srt_libs"/libharfbuzz.so.0.* 2>/dev/null | head -n 1)
if [ -n "$hb" ]; then ln -s "$hb" "$sniper_compat/libharfbuzz.so.0"; else step "warning: no HarfBuzz in Valve's runtime for the newer-runtime titles"; fi
# LD_LIBRARY_PATH beats the system folders whatever its order: Valve's older glibc must not shadow the rootfs's.
for f in "$srt_libs"/*; do
  n=$(basename "$f")
  if [ -e "$ROOTFS/usr/lib/x86_64-linux-gnu/$n" ] || [ -e "$ROOTFS/lib/x86_64-linux-gnu/$n" ]; then rm -f "$f"; fi
done
  tool=$STEAM_X64/compatibilitytools.d/droiddeck-direct
  mkdir -p "$tool"
  rm -f "$tool/droiddeck-proton-wrap" "$tool/toolmanifest.vdf.droiddeck-orig"
  cat > "$tool/compatibilitytool.vdf" <<'VDF'
"compatibilitytools"
{
  "compat_tools"
  {
    "droiddeck-direct"
    {
      "install_path" "."
      "display_name" "DroidDeck direct (no container)"
      "from_oslist" "linux"
      "to_oslist" "linux"
    }
  }
}
VDF
  cat > "$tool/toolmanifest.vdf" <<'VDF'
"manifest"
{
  "version" "2"
  "commandline" "/run %verb%"
}
VDF
  printf '%s\n' "$REAL_HOME/.droiddeck-vac-diagnostics" > "$tool/vac-marker"
  cat > "$tool/run" <<'RUN'
#!/bin/sh
verb=$1; shift
rt=$HOME/.local/share/Steam/ubuntu12_32/steam-runtime.old
[ -x "$rt/run.sh" ] || rt=$HOME/.local/share/Steam/ubuntu12_32/steam-runtime
# -condebug only while VAC diagnostics are on (the marker is in the real home, not this client's).
marker=$(cat "${0%/*}/vac-marker" 2>/dev/null)
case "$*" in *hl2.sh*|*hl2_linux*|*srcds*) [ -n "$marker" ] && [ -f "$marker" ] && set -- "$@" -condebug ;; esac
# game-gpu.env is rewritten whenever the launcher runs; game-gpu.local is the user's and is read after it.
gpu=$HOME/game-gpu.env
if [ -f "$gpu" ] && [ "${BL_X64_GAME_SOFTWARE:-0}" != 1 ]; then
  . "$gpu"
  [ -f "$HOME/game-gpu.local" ] && . "$HOME/game-gpu.local"
else
  export mesa_glthread=false
fi
case $verb in
  run|waitforexitandrun)
    # Titles Valve gives a newer runtime (sniper, soldier) need newer libraries than scout's: the rootfs's.
    if grep -qx "${STEAM_COMPAT_APP_ID:-${SteamAppId:-none}}" "$HOME/direct-sniper" 2>/dev/null; then
      [ -e "$HOME/sniper-compat/libharfbuzz.so.0" ] &&
        export LD_PRELOAD="$HOME/sniper-compat/libharfbuzz.so.0${LD_PRELOAD:+:$LD_PRELOAD}"
      case $1 in
        *.sh)
          if grep -q '^[[:space:]]*\. /etc/os-release' "$1" 2>/dev/null; then
            script=$1; shift
            exec bash -c "$(sed 's|^\([[:space:]]*\)\. /etc/os-release|\1VERSION_CODENAME=sniper|' "$script")" "$script" "$@"
          fi ;;
      esac
      exec "$@"
    fi
    exec "$rt/run.sh" "$@" ;;
  *) exec "$@" ;;
esac
RUN
  chmod 755 "$tool/run"
  if [ ! -f "$STEAM_X64/config/config.vdf" ] && ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
    mkdir -p "$STEAM_X64/config"
    printf '"InstallConfigStore"\n{\n\t"Software"\n\t{\n\t\t"Valve"\n\t\t{\n\t\t\t"Steam"\n\t\t\t{\n\t\t\t\t"CompatToolMapping"\n\t\t\t\t{\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n' \
      > "$STEAM_X64/config/config.vdf"
  fi
  x64_share x86-64
  x64_map_direct
  # Each wrapper execs droiddeck-x64-arm-proton through an arm64 binary, which FEX hands straight to the CPU.
  arm_tools=$ARM_STEAM/compatibilitytools.d
  mkdir -p "$X64_HOME/arm64"
  cp -f /usr/bin/env "$X64_HOME/arm64/env"
  for w in "$STEAM_X64"/compatibilitytools.d/*/.droiddeck-arm64-proton; do
    [ -e "$w" ] || continue
    d=${w%/.droiddeck-arm64-proton}
    [ -d "$arm_tools/${d##*/}" ] && [ "${d##*/}" != droiddeck-fex ] || rm -rf "$d"
  done
  for src in "$arm_tools"/*/; do
    src=${src%/}; name=${src##*/}
    [ -f "$src/compatibilitytool.vdf" ] && [ -f "$src/toolmanifest.vdf" ] || continue
    # A droiddeck-fex wrapper made the arm64 client install Valve's FEX when this client probed its tools after logon.
    case $name in droiddeck-direct|droiddeck-fex) continue ;; esac
    dst=$STEAM_X64/compatibilitytools.d/$name
    mkdir -p "$dst"
    rm -f "$dst/droiddeck-proton-wrap" "$dst/toolmanifest.vdf.droiddeck-orig"
    : > "$dst/.droiddeck-arm64-proton"
    python3 - "$src/compatibilitytool.vdf" "$dst/compatibilitytool.vdf" <<'PY' || { rm -rf "$dst"; continue; }
import re, sys
s = open(sys.argv[1], encoding="utf-8", errors="replace").read()
m = re.search(r'"compat_tools"\s*\{\s*"([^"]+)"', s)
if not m:
    sys.exit(1)
# The x86-64 client ignores a tool named arm64 or aarch64 ("for a different target platform linux arm64").
key = re.sub(r"arm64|aarch64", "a64", m.group(1))
d = re.search(r'"display_name"\s*"([^"]*)"', s)
name = d.group(1) if d else key
open(sys.argv[2], "w").write(
    '"compatibilitytools"\n{\n  "compat_tools"\n  {\n    "%s"\n    {\n      "install_path" "."\n'
    '      "display_name" "%s"\n      "from_oslist" "windows"\n      "to_oslist" "linux"\n    }\n  }\n}\n'
    % (key, name))
PY
    cat > "$dst/toolmanifest.vdf" <<'VDF'
"manifest"
{
  "version" "2"
  "commandline" "/run %verb%"
}
VDF
    printf '#!/bin/sh\n# The arm64 client'"'"'s %s, run natively (droiddeck-x64-arm-proton).\nexec "%s" /usr/local/bin/droiddeck-x64-arm-proton "%s" "$@"\n' \
      "$name" "$X64_HOME/arm64/env" "$name" > "$dst/run"
    chmod 755 "$dst/run"
  done
  x64_sync_login
  # Each client keeps its own libraries; written only while no x86 client runs, as it rewrites these on exit.
  if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
    listing_ok=0
    if BL_X64_SHARED=$X64_HOME/shared/arm64 python3 - "$STEAM_X64" "${BL_X64_STEAM_LIBRARIES:-}" "$STEAM_X64/config/libraryfolders.vdf" "$STEAM_X64/steamapps/libraryfolders.vdf" <<'PY'; then
import json, os, re, sys
keep = {os.path.realpath(sys.argv[1]), os.path.realpath(os.environ["BL_X64_SHARED"])}
# Without the session's list nothing is known about the card: leave the folders as they are.
if not sys.argv[2]:
    sys.exit(3)
try:
    keep |= {os.path.realpath(str(e["path"])) for e in json.load(open(sys.argv[2], encoding="utf-8"))}
except (OSError, ValueError, KeyError, TypeError):
    sys.exit(3)
for path in sys.argv[3:]:
    if not os.path.isfile(path):
        continue
    s = open(path, encoding="utf-8", errors="surrogateescape").read()
    out, removed, i = [], [], 0
    for m in re.finditer(r'^\t"(\d+)"\s*\n\t\{', s, re.M):
        if m.start() < i:
            continue
        depth, k = 1, m.end()
        while depth and k < len(s):
            depth += s[k] == "{"
            depth -= s[k] == "}"
            k += 1
        block = s[m.start():k]
        p = re.search(r'"path"\s*"([^"]*)"', block)
        if p and os.path.realpath(p.group(1)) not in keep:
            out.append(s[i:m.start()])
            i = k + (1 if s[k:k + 1] == "\n" else 0)
            removed.append(p.group(1))
    if not removed:
        continue
    out.append(s[i:])
    open(path, "w", encoding="utf-8", errors="surrogateescape").write("".join(out))
    print("droiddeck-steam-x64: no longer shared with the arm64 client: %s" % ", ".join(removed))
PY
      listing_ok=1
    elif [ $? = 3 ]; then
      step "no library list from the session: the x86-64 client's library folders are left as they are"
    else
      step "could not check the x86-64 client's library folders"
    fi
    if [ "$listing_ok" = 1 ]; then
      BL_STEAM_LIBRARIES=$BL_X64_STEAM_LIBRARIES droiddeck-steam-library "$STEAM_X64" ||
        step "could not add the card to the x86-64 client's library folders"
    fi
  fi
  # default-tool is the default this script set: titles still on it move along; a user's choice is kept.
  default=$(x64_default_dir)
  if [ -f "$default/compatibilitytool.vdf" ] && [ -f "$STEAM_X64/config/config.vdf" ] &&
     ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
    python3 - "$STEAM_X64/config/config.vdf" "$default/compatibilitytool.vdf" "$X64_HOME/default-tool" <<'PY' || step "could not make ARM64 Proton the default"
import os, re, sys
path, vdf, record = sys.argv[1:4]
tool = re.search(r'"compat_tools"\s*\{\s*"([^"]+)"', open(vdf).read()).group(1)
s = open(path, encoding="utf-8", errors="surrogateescape").read()
m = re.search(r'"CompatToolMapping"\s*\{', s)
if not m:
    sys.exit(0)
depth, k = 1, m.end()
while depth:
    depth += s[k] == "{"
    depth -= s[k] == "}"
    k += 1
block = s[m.end():k]
try:
    previous = open(record).read().strip() or None
except OSError:
    previous = None
z = re.search(r'^\s*"0"\s*\{([^{}]*)\}', block, re.M)
if z:
    name = re.search(r'"name"\s*"([^"]*)"', z.group(1))
    name = name.group(1) if name else ""
    set_here = re.search(r'"priority"\s*"75"', z.group(1)) is not None
    if previous is None and set_here and name.endswith("-a64"):
        previous = name
    if "arm64" not in z.group(1) and name != previous:
        sys.exit(0)
    if name == tool and previous == tool:
        sys.exit(0)
    block = block[:z.start()] + block[z.end():]
moved = 0
if previous and previous != tool:
    block, moved = re.subn(r'("name"\s*")%s(")' % re.escape(previous), lambda x: x.group(1) + tool + x.group(2), block)
entry = ('\n\t\t\t\t\t"0"\n\t\t\t\t\t{\n\t\t\t\t\t\t"name"\t\t"%s"\n' % tool +
         '\t\t\t\t\t\t"config"\t\t""\n\t\t\t\t\t\t"priority"\t\t"75"\n\t\t\t\t\t}')
s = s[:m.end()] + entry + block + s[k:]
open(path, "w", encoding="utf-8", errors="surrogateescape").write(s)
open(record, "w").write(tool + "\n")
if moved:
    print("droiddeck-steam-x64: default %s -> %s, with the %d titles on it" % (previous, tool, moved))
PY
  fi
  if [ -d "$ARM_STEAM/config/uioverrides" ] && [ ! -e "$STEAM_X64/config/uioverrides" ]; then
    ln -s "$ARM_STEAM/config/uioverrides" "$STEAM_X64/config/uioverrides"
  fi
  if [ -f "$ARM_STEAM/.cef-enable-remote-debugging" ]; then
    : > "$STEAM_X64/.cef-enable-remote-debugging"
  else
    rm -f "$STEAM_X64/.cef-enable-remote-debugging"
  fi
  if [ -f "$ARM_STEAM/config/config.vdf" ] && [ -f "$STEAM_X64/config/config.vdf" ] &&
     ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
    python3 - "$ARM_STEAM/config/config.vdf" "$STEAM_X64/config/config.vdf" <<'PY' || step "could not copy the startup movie choice"
import re, sys
movie = re.search(r'\n([ \t]*)"StartupMovie"\s*\{[^{}]*\}', open(sys.argv[1], encoding="utf-8", errors="replace").read())
if not movie:
    sys.exit(0)
path = sys.argv[2]
s = open(path, encoding="utf-8", errors="surrogateescape").read()
old = re.search(r'\n[ \t]*"StartupMovie"\s*\{[^{}]*\}', s)
if old:
    if old.group(0).split(None, 1)[1] == movie.group(0).split(None, 1)[1]:
        sys.exit(0)
    s = s[:old.start()] + s[old.end():]
steam = re.search(r'"Valve"\s*\{\s*"Steam"\s*\{', s)
if not steam:
    sys.exit(0)
s = s[:steam.end()] + movie.group(0) + s[steam.end():]
open(path, "w", encoding="utf-8", errors="surrogateescape").write(s)
PY
  fi
  # FEX takes its rootfs from FEXServer, and one started without FEX_ROOTFS serves none: stop it while no x86 client runs.
  if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
    FEX_ROOTFS=$ROOTFS /usr/local/bin/FEXServer -k >/dev/null 2>&1 || true
  fi
}

x64_gpu() {
  [ "${BL_X64_SOFTWARE:-0}" != 1 ] && [ -d /usr/local/share/fex-emu/GuestThunks ] && [ -d /usr/local/lib/fex-emu/HostThunks ]
}

x64_webhelper_gpu() {
  x64_gpu && [ "${BL_X64_CLIENT_SOFTWARE:-0}" != 1 ] && [ "${BL_X64_CEF_GPU:-1}" != 0 ]
}

x64_env() {
# droiddeck-x64-arm-proton restores this arm64 environment before Proton runs.
export -p | grep -v -E '^declare -x (PWD|OLDPWD|SHLVL|_)=' > "$X64_HOME/arm-session.env.new" || true
mv -f "$X64_HOME/arm-session.env.new" "$X64_HOME/arm-session.env"
unset LD_LIBRARY_PATH LD_PRELOAD STEAM_RUNTIME STEAM_RUNTIME_PREFER_HOST_LIBRARIES
gpu_vars="GALLIUM_DRIVER MESA_LOADER_DRIVER_OVERRIDE VK_ICD_FILENAMES VK_DRIVER_FILES LIBGL_KOPPER_DRI2 ZINK_DEBUG ZINK_DESCRIPTORS TU_DEBUG MESA_NO_ERROR mesa_glthread"
rm -f "$X64_HOME/game-gpu.env"
if x64_gpu; then
  printf '{"ThunksDB": {"GL": 1, "Vulkan": 1}}\n' > "$X64_HOME/thunks.json"
  {
    echo "export FEX_THUNKCONFIG='$X64_HOME/thunks.json'"
    echo "unset LIBGL_ALWAYS_SOFTWARE LIBGL_DRI3_DISABLE GALLIUM_DRIVER"
    for v in $gpu_vars; do
      eval "val=\${$v-}"; [ -n "$val" ] && printf "export %s='%s'\n" "$v" "$val"
    done
  } > "$X64_HOME/game-gpu.env"
fi
if x64_gpu && [ "${BL_X64_CLIENT_SOFTWARE:-0}" != 1 ]; then
  export FEX_THUNKCONFIG=$X64_HOME/thunks.json
  unset LIBGL_ALWAYS_SOFTWARE LIBGL_DRI3_DISABLE
else
unset FEX_THUNKCONFIG $gpu_vars
export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
# Over DRI3 the X server hands out the GPU node, and Mesa's software path loops on KMS dumb buffers on it.
export LIBGL_DRI3_DISABLE=1
fi
[ "${BL_X64_FP_OFF:-0}" = 1 ] && export PROOT_FP_OFF=1
export FEX_TSOENABLED=${FEX_TSOENABLED:-1} FEX_VECTORTSOENABLED=${FEX_VECTORTSOENABLED:-0} \
  FEX_MEMCPYSETTSOENABLED=${FEX_MEMCPYSETTSOENABLED:-0} FEX_HALFBARRIERTSOENABLED=${FEX_HALFBARRIERTSOENABLED:-0} \
  FEX_X87REDUCEDPRECISION=${FEX_X87REDUCEDPRECISION:-1} FEX_MULTIBLOCK=${FEX_MULTIBLOCK:-1}
export HOME=$X64_HOME
export XDG_DATA_HOME=$X64_HOME/.local/share XDG_CONFIG_HOME=$X64_HOME/.config XDG_CACHE_HOME=$X64_HOME/.cache
export FEX_ROOTFS=$ROOTFS
export STEAM_EXTRA_COMPAT_TOOLS_PATHS=
export STEAM_RUNTIME=1
[ -d "$X64_HOME/steamrt-shim" ] && export STEAM_RUNTIME_STEAMRT=$X64_HOME/steamrt-shim
# The client starts commands via steam-runtime-launch-client; Valve's needs a session-bus service, the shim's runs them.
[ -d "$X64_HOME/steamrt-shim/bin" ] && export PATH=$X64_HOME/steamrt-shim/bin:$PATH
export LD_LIBRARY_PATH=$STEAM_X64/steamrt64:$STEAM_X64/steamrt64/panorama:$X64_HOME/steamrt-libs
# After an update the client re-executes itself through pressure-vessel-wrap (bwrap) unless this guard is set.
export STEAM_STEAMRT_RECURSION_GUARD=1
# proot is a tracer: tier0 takes it for a debugger and breaks into it with int3 on its first failed assertion.
export BL_WEBHELPER_HIDE_TRACER=1
}

x64_manifest_version() {
  local chan
  chan=$(cat "$STEAM_X64/package/beta" 2>/dev/null || echo "$X64_CHANNEL")
  awk -F'"' '$2 == "version" { print $4; exit }' "$STEAM_X64/package/steam_client_${chan}_$1.manifest" 2>/dev/null || echo "?"
}

# What x86 GL and Vulkan look like through FEX's thunks, for a session whose menus failed on the GPU.
x64_gpu_probe() {
  local fex=${BL_FEX:-/usr/local/bin/FEX}
  echo "== x86-64 GPU probe (x86 glxinfo and vulkaninfo through FEX):"
  (
    unset LD_LIBRARY_PATH
    export DISABLE_GAMESCOPE_WSI=1 vk_xwayland_wait_ready=true LIBGL_KOPPER_DISABLE=true
    timeout 30 "$fex" /usr/bin/glxinfo -B 2>&1 | grep -E "OpenGL (renderer|core profile version|version) string|direct rendering|rror" | head -5
    timeout 30 "$fex" /usr/bin/vulkaninfo --summary 2>&1 | grep -E "deviceName|driverName|driverInfo|apiVersion|rror" | head -6
  ) | sed 's/^/   /'
}

# Steam's menus come from the web helper; when its GPU process keeps exiting, Chromium gives up on the GPU and
# the menus never appear. Says so once in the session log, with the probe above.
x64_gpu_watch() {
  local log=$STEAM_X64/logs/console-linux.txt from n codes
  from=$(( $(wc -l 2>/dev/null < "$log" || echo 0) + 1 ))
  tail -n0 -F --pid=$$ "$log" 2>/dev/null | grep -q -m1 "GPU process isn't usable" || return 0
  n=$(tail -n +"$from" "$log" | grep -c "GPU process exited unexpectedly" || true)
  codes=$(tail -n +"$from" "$log" | grep -oE "GPU process exited unexpectedly: exit_code=[0-9]+" | sed 's/.*=//' |
    sort | uniq -c | awk '{ printf "%s%s x%s", s, $2, $1; s = ", " }')
  echo "== x86-64: Steam's menu renderer gave up on the GPU after $n GPU process exits (exit code ${codes:-?}); the menus stay blank. See steam/console-linux.txt and steam/webhelper_gpu.txt"
  x64_gpu_probe
}

x64_exec() {
  x64_env
  cd "$STEAM_X64"
  # The client checksums its files at start and reinstalls over edited ones, so the session shim (preload/net.c)
  # swaps the web helper at exec. The arm64 helper only comes up against a client of the same build.
  if [ "${BL_X64_X86_WEBHELPER:-0}" != 1 ] && [ -x /usr/local/bin/droiddeck-x64-arm-webhelper ] &&
     [ -x "$ARM_STEAM/steamrtarm64/steamwebhelper" ]; then
    local chan x86_build arm_build
    chan=$(cat "$STEAM_X64/package/beta" 2>/dev/null || echo "$X64_CHANNEL")
    x86_build=$(awk -F'"' '$2 == "version" { print $4; exit }' "$STEAM_X64/package/steam_client_${chan}_steamrt64.manifest" 2>/dev/null || true)
    arm_build=$(awk -F'"' '$2 == "version" { print $4; exit }' "$ARM_STEAM/package/steam_client_${chan}_linuxarm64.manifest" 2>/dev/null || true)
    if [ -n "$x86_build" ] && [ "$x86_build" = "$arm_build" ]; then
      export BL_X64_ARM_WEBHELPER=/usr/local/bin/droiddeck-x64-arm-webhelper
    else
      echo "== x86-64 client build ${x86_build:-?}, arm64 web helper's ${arm_build:-?}: keeping the x86 web helper" >&2
    fi
  fi
  if [ -z "${BL_X64_ARM_WEBHELPER:-}" ] && x64_webhelper_gpu; then
    # Not --in-process-gpu (the preload's default): a GPU crash there takes the whole helper down.
    export BL_WEBHELPER_ARGS="--ignore-gpu-blocklist --use-gl=angle --use-angle=gl --ozone-platform=x11"
    [ -x /usr/local/bin/droiddeck-x64-webhelper ] && export BL_X64_ARM_WEBHELPER=/usr/local/bin/droiddeck-x64-webhelper
  elif [ -z "${BL_X64_ARM_WEBHELPER:-}" ]; then
    export BL_WEBHELPER_ARGS=stock
    local a args=() soft=0
    for a in "$@"; do
      case $a in
        -cef-force-gpu|-cef-ozone-platform=*|-cef-use-gl=*|-cef-use-angle=*) soft=1 ;;
        *) args+=("$a") ;;
      esac
    done
    [ "$soft" = 1 ] && set -- "${args[@]}" -cef-disable-gpu
  fi
  mkdir -p "$STEAM_X64/logs"
  touch "$STEAM_X64/logs/bootstrap_log.txt"
  ( tail -n0 -F --pid=$$ "$STEAM_X64/logs/bootstrap_log.txt" 2>/dev/null |
      grep --line-buffered -oE 'Downloading update \([0-9,]+ of [0-9,]+ KB\)' | sed -u 's/,//g' >&2 ) &
  echo "== x86-64 client: steamrt64 $(x64_manifest_version steamrt64) (the client may update itself)" >&2
  x64_webhelper_gpu && { x64_gpu_watch >&2 & }
  exec "${BL_FEX:-/usr/local/bin/FEX}" "$STEAM_X64/steamrt64/steam" "$@"
}

x64_share() {
  pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1 && return 0
  pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 && return 0
  [ -d "$STEAM_X64/steamapps" ] && [ -d "$ARM_STEAM/steamapps" ] || return 0
  droiddeck-steam-share "$1" "$ARM_STEAM" "$STEAM_X64" /mnt/droiddeck-sd/x86-64 "$X64_HOME/shared" ||
    step "could not share the games between the clients"
}

# Steam renews its token every few days and refuses old copies: copy the newest every start, never while a client runs.
x64_sync_login() {
  pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1 && return 0
  pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 && return 0
  python3 - "$ARM_STEAM" "$STEAM_X64" "$REAL_HOME/.steam/registry.vdf" "$X64_HOME/.steam/registry.vdf" <<'PY' || step "could not share the sign-in between the clients"
import os, re, shutil, sys
arm, x64, arm_reg, x64_reg = sys.argv[1:5]
FILES = ("config/loginusers.vdf", "local.vdf")


def read(path):
    try:
        return open(path, encoding="utf-8", errors="surrogateescape").read()
    except OSError:
        return None


def write(path, text):
    open(path, "w", encoding="utf-8", errors="surrogateescape").write(text)


def block(text, name):
    m = re.search(r'"%s"\s*\{' % name, text)
    if not m:
        return None
    depth, k = 1, m.end()
    while depth and k < len(text):
        depth += text[k] == "{"
        depth -= text[k] == "}"
        k += 1
    return m, k


def accounts(root):
    text = read(os.path.join(root, "config/config.vdf")) or ""
    found = block(text, "Accounts")
    if not found:
        return {}
    return dict(re.findall(r'"([^"]+)"\s*\{\s*"SteamID"\s*"(\d+)"', text[found[0].end():found[1]]))


def add_accounts(root, wanted):
    path = os.path.join(root, "config/config.vdf")
    text = read(path)
    if text is None:
        return
    have = accounts(root)
    missing = {name: sid for name, sid in wanted.items() if name not in have}
    if not missing:
        return
    entries = "".join('\n\t\t\t\t\t"%s"\n\t\t\t\t\t{\n\t\t\t\t\t\t"SteamID"\t\t"%s"\n\t\t\t\t\t}' % item for item in missing.items())
    found = block(text, "Accounts")
    if found:
        text = text[:found[0].end()] + entries + text[found[0].end():]
    else:
        steam = re.search(r'"Valve"\s*\{\s*"Steam"\s*\{', text)
        if not steam:
            return
        text = text[:steam.end()] + '\n\t\t\t\t"Accounts"\n\t\t\t\t{' + entries + '\n\t\t\t\t}' + text[steam.end():]
    write(path, text)


def set_keys(src_reg, dst_reg, force):
    st, dt = read(src_reg), read(dst_reg)
    if st is None or dt is None:
        return
    for key in ("AutoLoginUser", "RememberPassword"):
        v = re.search(r'"%s"\s*"([^"]*)"' % key, st)
        if not v:
            continue
        if re.search(r'"%s"\s*"' % key, dt):
            if force:
                dt = re.sub(r'("%s"\s*")[^"]*(")' % key, lambda m: m.group(1) + v.group(1) + m.group(2), dt, count=1)
            continue
        hkcu = re.search(r'"HKCU"\s*\{\s*"Software"\s*\{\s*"Valve"\s*\{\s*"Steam"\s*\{\n', dt)
        if hkcu:
            dt = dt[:hkcu.end()] + '\t\t\t\t\t"%s"\t\t"%s"\n' % (key, v.group(1)) + dt[hkcu.end():]
    write(dst_reg, dt)


def stamp(root):
    if not all(os.path.isfile(os.path.join(root, f)) for f in FILES):
        return None
    # A client that refused its token leaves an empty ConnectCache: newest, but copying it signs the other out.
    text = open(os.path.join(root, "local.vdf"), encoding="utf-8", errors="surrogateescape").read()
    if not re.search(r'"ConnectCache"\s*\{\s*"[^"]+"\s*"[^"]+"', text):
        return None
    return os.path.getmtime(os.path.join(root, "local.vdf"))


a, x = stamp(arm), stamp(x64)
if a is not None or x is not None:
    if x is None or (a is not None and a > x):
        src, dst, sreg, dreg, who = arm, x64, arm_reg, x64_reg, "arm64"
    else:
        src, dst, sreg, dreg, who = x64, arm, x64_reg, arm_reg, "x86-64"
    copy = a is None or x is None or abs(a - x) >= 1
    if copy:
        for f in FILES:
            os.makedirs(os.path.dirname(os.path.join(dst, f)), exist_ok=True)
            # copy2 keeps the time stamp, so both match until one client signs in again.
            shutil.copy2(os.path.join(src, f), os.path.join(dst, f))
        if os.path.isfile(sreg) and not os.path.isfile(dreg):
            os.makedirs(os.path.dirname(dreg), exist_ok=True)
            shutil.copy2(sreg, dreg)
    set_keys(sreg, dreg, copy)
    set_keys(dreg, sreg, False)
    add_accounts(dst, accounts(src))
    add_accounts(src, accounts(dst))
    if copy:
        print("droiddeck-steam-x64: sign-in shared from the %s client (the newer one)" % who)
PY
}

case "${1:-}" in
  --sync-login) x64_sync_login; exit 0 ;;
  --share) x64_share arm64; exit 0 ;;
  --prepare) x64_prepare; exit 0 ;;
  --exec) shift; x64_exec "$@" ;;
  --gpu) x64_gpu; exit $? ;;
  --webhelper-gpu) x64_webhelper_gpu; exit $? ;;
  --default-tool) x64_default_tool; exit 0 ;;
  --map-direct) shift; x64_map_direct "$@"; exit 0 ;;
esac

if pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1; then
  step "shutting down the arm64 client first"
  "$ARM_STEAM/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
  for _ in $(seq 1 30); do pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 || break; sleep 1; done
fi
x64_prepare
step "FEX, rootfs and client ready"
if [ -n "${BL_FEXSERVER_LOG:-}" ]; then
  FEX_ROOTFS=$ROOTFS "$(dirname "${BL_FEX:-/usr/local/bin/FEX}")/FEXServer" -f -p 600 >"$BL_FEXSERVER_LOG" 2>&1 &
  sleep 1
fi
# The client's connection status comes from NetworkManager on the system bus; a desktop has no system bus.
x64_bus= x64_nm= x64_pin=
x64_cleanup() {
  [ -n "$x64_pin" ] && kill "$x64_pin" 2>/dev/null
  [ -n "$x64_nm" ] && kill "$x64_nm" 2>/dev/null
  [ -n "$x64_bus" ] && kill "$x64_bus" 2>/dev/null
  return 0
}
trap x64_cleanup EXIT
if ! pgrep -f 'dbus-daemon --config-file=/usr/local/share/droiddeck/system-bus.conf' >/dev/null 2>&1 \
   && command -v dbus-daemon >/dev/null 2>&1 && command -v droiddeck-netmanager >/dev/null 2>&1; then
  mkdir -p /run/dbus /usr/local/share/droiddeck
  cat > /usr/local/share/droiddeck/system-bus.conf <<'BUSCONF'
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <type>system</type>
  <listen>unix:path=/run/dbus/system_bus_socket</listen>
  <auth>EXTERNAL</auth>
  <policy context="default">
    <allow user="*"/>
    <allow own="*"/>
    <allow send_destination="*"/>
    <allow receive_sender="*"/>
  </policy>
</busconfig>
BUSCONF
  rm -f /run/dbus/system_bus_socket
  dbus-daemon --config-file=/usr/local/share/droiddeck/system-bus.conf --nofork --nopidfile &
  x64_bus=$!
  for _ in $(seq 1 50); do [ -S /run/dbus/system_bus_socket ] && break; sleep 0.1; done
  if [ -S /run/dbus/system_bus_socket ]; then
    droiddeck-netmanager &
    x64_nm=$!
    step "network: system bus up, NetworkManager stand-in running"
  else
    step "network: the system bus did not come up; the client will show no connection"
  fi
fi
# Left by a killed client: .crash opens the update UI on its spinner; stale Singleton* locks can blank the UI.
if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
  for f in "$STEAM_X64/.crash" "$STEAM_X64/steam.pid" "$STEAM_X64/config/htmlcache/SingletonLock" \
           "$STEAM_X64/config/htmlcache/SingletonCookie" "$STEAM_X64/config/htmlcache/SingletonSocket"; do
    { [ -e "$f" ] || [ -L "$f" ]; } && rm -f "$f"
  done
fi
if [ -n "${BL_CLIENT_CPUS:-}" ] && command -v taskset >/dev/null 2>&1; then
  (
    while sleep 5; do
      for pid in $(ps -A -o PID,ARGS | awk '($2 ~ /^(.*\/)?FEX(Interpreter)?$/) && ($3 ~ /steamrt64\/steam$/ || $3 ~ /steamwebhelper$/) {print $1} $2 ~ /steamrtarm64\/steamwebhelper$/ {print $1}'); do
        taskset -a -p -c "$BL_CLIENT_CPUS" "$pid" >/dev/null 2>&1
      done
    done
  ) &
  x64_pin=$!
fi
for attempt in 1 2 3 4 5; do
  # The branch on disk must match -clientbeta before every start, or the client loops on rc=42.
  echo "$X64_CHANNEL" > "$STEAM_X64/package/beta"
  step "starting the x86-64 SteamRT3 client under FEX (attempt $attempt): $*"
  rc=0
  if [ "${BL_X64_X86_WEBHELPER:-0}" != 1 ] || x64_webhelper_gpu; then cef=(-cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan); else cef=(-cef-disable-gpu); fi
  ( x64_exec -clientbeta "$X64_CHANNEL" -no-cef-sandbox "${cef[@]}" "$@" ) >>"$LOG" 2>&1 || rc=$?
  step "client exited rc=$rc"
  [ "$rc" = 42 ] || break
done