#!/bin/bash
# Build the CRIU claim image matrix for the d3d9 command-stream seats.
#
# One generic checkpoint per (engine profile, game family, seat) holds a
# fully booted engine — with the w3cs d3d9 proxy loaded — blocked at the
# replay-open gate. lab-game-session.sh restores these in ~150 ms per
# viewer claim, so any same-family replay starts after only its map load.
#
# Each image is fingerprinted over everything baked into it: the proxy
# DLL, the per-seat game environment file, and the engine files of the
# profile view. A rebuild happens only when the fingerprint changes, so
# this script is safe to run on every deploy. Building a seat's images
# stops that seat (one wine stack per prefix); seats are built serially
# and restarted immediately after.
#
# Run as root on the CPU worker:
#   w3cs-criu-build-images.sh [seat ...]     (default: seats 1 2 3 4 5)
set -euo pipefail

[ "${EUID:-$(id -u)}" -eq 0 ] || { echo "run as root" >&2; exit 1; }

CLASSIC_ROOT=/opt/war3-runtime/classic
CLASSIC_131_ROOT=/opt/war3-runtime/classic-131
CRIU_ROOT=$CLASSIC_ROOT/w3cs-criu
IMAGES=$CRIU_ROOT/images
LAB=/home/ubuntu/w3cs-lab
PROFILE_ROOT=$LAB/engine-profiles
BUILDER=/usr/local/sbin/war3-build-classic-layer-base
PROXY_DLL=$LAB/d3d9.dll
PROXY_DLL64=$LAB/d3d9-x64.dll
PREFIX_PREPARER=/usr/local/sbin/war3-prepare-classic-builder-prefix
BUILD_PROFILE_FILTER=${W3CS_BUILD_PROFILE_FILTER:-}
ENABLE_131=${W3CS_ENABLE_131:-0}
# Video seats (the H.264 lane that replaced the AWS fleet): their images
# hold an engine that RASTERIZES through WineD3D on llvmpipe, with the
# proxy in clock-only mode. Same views, same gate, same placeholders; only
# the baked game environment (and so the fingerprint) differs per lane.
# The deploy records the seat list so the daily timer builds the same
# matrix; W3CS_VIDEO_SEATS overrides it.
VIDEO_SEATS_FILE=/etc/w3cs/video-seats
VIDEO_SEATS=${W3CS_VIDEO_SEATS-$(cat "$VIDEO_SEATS_FILE" 2>/dev/null || echo 5)}
for video_seat in $VIDEO_SEATS; do
  [[ "$video_seat" =~ ^[1-9]$ ]] || {
    echo "W3CS_VIDEO_SEATS must list seat numbers 1..9" >&2; exit 2; }
done
seat_lane() {
  local candidate
  for candidate in $VIDEO_SEATS; do
    [ "$candidate" = "$1" ] && { echo video; return; }
  done
  echo command
}
case "$ENABLE_131" in
  0|1) ;;
  *) echo "W3CS_ENABLE_131 must be 0 or 1" >&2; exit 2 ;;
esac
if [ $# -gt 0 ]; then SEATS=("$@"); else SEATS=(1 2 3 4 5); fi

# profile|family|game-exe|placeholder-replay-id|gate
# The placeholder never survives into a claim (the gate swaps the replay
# before the engine reads a byte); it only drives the engine to the gate.
# gate "default" is the resolved-path interposer; "114" is the raw-path
# variant the 1.14 engine needs (its replay open never resolves to the
# suffix the default gate matches).
MATRIX=(
  "native-1285|tft|Warcraft III.exe|a8da8dd599b44ddba9d3534a7ea71f16a225b629|default"
  "native-1285|roc|Warcraft III.exe|93a34e12662905bb19b0b4a31d074d0ca17864e1|default"
  "native-1220|tft|war3.exe|7d38aa6ae2fcde773f027bae238f4e2e09fb46d8|default"
  "native-124cde|roc|war3.exe|patch24proof|default"
  # 1.24 TFT (375 catalog replays split by sub-patch family). native-124ab
  # is the 2009 events (WCG/GC Chengdu, recorded on 1.24a/b); native-124cde
  # is the 2010+ events. Both cold-launch without these rows. Placeholders
  # are catalog replays the deploy materializes (CLAIM_PLACEHOLDER_IDS).
  "native-124ab|tft|war3.exe|9214a5ebcb86ebd4856c79a553c4857ddddf9676|default"
  "native-124cde|tft|war3.exe|00ec4a090941071e363a28d26abc9a37ac6fe31d|default"
  "native-1140|tft|war3.exe|78b48840982b638fe49c9b06a8e5b70fdb2664aa|114"
  "native-1120|roc|war3.exe|1271ea1feb3b939a60e3ec44c9b7a2b9c3a55bdb|114"
  # 1.12 TFT (160 catalog replays): placeholder = a catalog replay the
  # deploy materializes from R2 (CLAIM_PLACEHOLDER_IDS in deploy-cpu-server.sh).
  "native-1120|tft|war3.exe|9091358d41354eb2dba8a70371926d27628226d4|114"
  "native-1100|roc|war3.exe|9e310763b046d34df875d456ac3894e5f446650b|114"
  "native-1110|roc|war3.exe|2c0132d4b01a54f2cd62051ef9f8d010194442b5|114"
)
if [ "$ENABLE_131" = 1 ]; then
  MATRIX+=(
    "native-1311-d3d9|tft|x86_64/Warcraft III.exe|2bbe1464d3fd3b1fabc767626e320cc6e6bf4644|131"
  )
fi

# Pre-1.28 engines call Direct3D 8. The seat prefixes carry DXVK's
# native d3d8.dll in system32, which translates D3D8 onto D3D9 and so
# reaches the w3cs proxy — that is how the cold lanes capture those
# engines. Forcing d3d8 to wine's builtin here (as the first image
# generation did) silently routed the old engines through wined3d and
# their claims streamed nothing ("engine stalled"). 1.28.5 talks D3D9
# directly, so its override string keeps d3d8 builtin and its images
# stay valid.
profile_d3d_overrides() {
  case "$1" in
    native-1285|native-1311-d3d9)
      echo 'd3d9=n,b;d3d8,dxgi,d3d11=b' ;;
    *) echo 'd3d9=n,b;d3d8=n,b;dxgi,d3d11=b' ;;
  esac
}

profile_view() {
  local profile=$1 seat=${2:-}
  if [ "$profile" = native-1311-d3d9 ]; then
    # The 1.31 NGDP client enters gameplay only from its canonical install
    # path. Byte-identical copies and symlink views both stop presenting.
    printf '%s\n' "$CLASSIC_131_ROOT/source/Warcraft III"
  else
    printf '%s\n' "$CRIU_ROOT/$profile/game"
  fi
}

profile_prefix() {
  local profile=$1 seat=$2
  if [ "$profile" = native-1311-d3d9 ]; then
    # Match the proven AWS 1.31 prefix contract. A copied prefix can reach
    # D3D9 and stat the replay map, but the client rejects the map before it
    # opens the archive.
    printf '/run/war3-prefixes/classic131/slot%s\n' "$seat"
  else
    printf '%s\n' "$CLASSIC_ROOT/prefix-seat$seat"
  fi
}

ensure_131_prefix() {
  local seat=$1 prefix
  prefix=$(profile_prefix native-1311-d3d9 "$seat")
  [ -d "$CLASSIC_131_ROOT/prefix64/drive_c" ] || {
    echo "clean 1.31 prefix is missing" >&2
    return 1
  }
  [ -x "$PREFIX_PREPARER" ] || {
    echo "1.31 prefix preparer is missing: $PREFIX_PREPARER" >&2
    return 1
  }
  # The service is stopped before this function runs. Recreate the upper
  # layer every time so a prior replay or diagnostic cannot contaminate the
  # generic checkpoint.
  echo "PREPARE AWS-style 1.31 overlay prefix for seat$seat"
  W3_CLASSIC_PREFIX_LOWER="$CLASSIC_131_ROOT/prefix64" \
  W3_CLASSIC_PREFIX_ROOT=/run/war3-prefixes/classic131 \
  W3_CLASSIC_BUILDER_OVERLAY_ROOT=/run/war3-prefix-overlays/classic131 \
    "$PREFIX_PREPARER" "$seat"
}

proxy_target() {
  local profile=$1 seat=${2:-} view
  view=$(profile_view "$profile" "$seat")
  if [ "$profile" = native-1311-d3d9 ]; then
    printf '%s\n' "$view/x86_64/d3d9.dll"
  else
    printf '%s\n' "$view/d3d9.dll"
  fi
}

assemble_view() {
  local profile=$1 exe=$2 seat=${3:-}
  local view
  view=$(profile_view "$profile" "$seat")
  local base overlay
  case "$profile" in
    native-1285) base="$CLASSIC_ROOT/war3-1285/Warcraft III" ;;
    native-1311-d3d9) base="$CLASSIC_131_ROOT/source/Warcraft III" ;;
    native-1140) base="$CLASSIC_ROOT/war3-1140/Warcraft III" ;;
    native-1220|native-124ab|native-124cde|native-1120|native-1110|native-1100|\
    native-1150|native-1160|native-1170|native-1180|native-120e|native-121b|native-1230|native-125b|native-1260)
      base="$CLASSIC_ROOT/war3rk/Warcraft III" ;;
    *) echo "unsupported claim profile: $profile" >&2; return 1 ;;
  esac
  if [ "$profile" = native-1311-d3d9 ]; then
    [ "$view" = "$base" ] && [ -f "$view/x86_64/Warcraft III.exe" ] || {
      echo "canonical 1.31 install is incomplete" >&2
      return 1
    }
  else
    rm -rf "$view"
    mkdir -p "$view"
    cp -as "$base/." "$view/"
  fi
  case "$profile" in
    native-1220|native-124ab|native-124cde|native-1120|native-1110|native-1100|\
    native-1150|native-1160|native-1170|native-1180|native-120e|native-121b|native-1230|native-125b|native-1260)
      overlay="$PROFILE_ROOT/${profile/native-/classic-}/files"
      [ -f "$overlay/war3.exe" ] || {
        echo "engine overlay is not installed: $profile" >&2; return 1;
      }
      # Mirror lab-game-session.sh: the war3rk view keeps its shared MPQs
      # and maps as symlinks while the engine files become private copies.
      # Every target is removed first — cp onto a cp -as symlink writes
      # THROUGH it into the shared install, and a symlinked engine file in
      # the view also bakes the shared path into the CRIU image, which
      # breaks the restore as soon as anything rewrites that shared file.
      rm -f "$view/Game.dll" "$view/war3.exe" "$view/game.dll" \
        "$view/Storm.dll" "$view/War3Patch.mpq"
      local file
      for file in Storm.dll War3Patch.mpq war3.exe game.dll; do
        cp "$overlay/$file" "$view/$file"
      done
      ;;
    native-1311-d3d9)
      # The canonical source is deliberate. The checkpoint and restore must
      # reopen this exact path; a copied or symlinked tree stalls in 1.31.
      :
      ;;
  esac
  local proxy
  proxy=$(proxy_target "$profile" "$seat")
  rm -f "$proxy"
  if [ "$profile" = native-1311-d3d9 ]; then
    cp "$PROXY_DLL64" "$proxy"
  else
    cp "$PROXY_DLL" "$proxy"
  fi
  # Extended zoom-out (ceiling 2250, default view unchanged) on the
  # view's PRIVATE game dll only — the shared installs stay byte-exact.
  # The patcher refuses anything but an exact stock-byte match, and the
  # image fingerprint hashes the patched dll, so images can never be
  # stale or half-patched. 1.31 is still not covered: it ships no game
  # dll at all (NGDP x86/x86_64 layout) and its view IS the canonical
  # licensed install.
  local zoom_engine=
  case "$profile" in
    native-1220) zoom_engine=1.22 ;;
    native-124cde|native-124ab) zoom_engine=1.24cde ;;
    native-1120) zoom_engine=1.12 ;;
    native-1110) zoom_engine=1.11 ;;
    native-1100) zoom_engine=1.10 ;;
    native-1140)
      # The 1140 view symlinks its engine files into the shared install;
      # give it a private game.dll before patching (writing through the
      # cp -as symlink would edit the shared file).
      rm -f "$view/game.dll"
      cp "$base/game.dll" "$view/game.dll"
      zoom_engine=1.14
      ;;
    # native-1285 is deliberately NOT wired here. A private ladder transform
    # applied cleanly and the patched engine booted, checkpointed and ran, but
    # it changed nothing the player could see:
    # tested live on London box 2 on 2026-08-23, the patched constants were
    # the only ones present in the running process and the wheel still
    # stopped at the stock ceiling. The six floats 1.28.5 keeps in that SSE
    # pool are not what bounds its wheel. Wiring it in would only ship a
    # no-op patch and a needless private copy of Game.dll.
  esac
  if [ -n "$zoom_engine" ]; then
    # A view that fails its patch step must not survive: later runs only
    # assemble a view when the directory is missing, so a half-built one
    # (seen 2026-08-21 when an outdated patcher knew no 1.10 ladder) gets
    # baked unpatched into every seat's image on the next run.
    local zoom_binary="$view/game.dll"
    python3 /usr/local/libexec/war3/w3cs-camera-zoom-patch.py \
      "$zoom_engine" "$zoom_binary" || {
      echo "zoom patch failed for $profile; discarding the view" >&2
      rm -rf "$view"
      return 1
    }
  fi
  if [ "$profile" != native-1311-d3d9 ]; then
    chown -R ubuntu:ubuntu "$view"
  fi
}

game_env_file() {
  local profile=$1 seat=$2
  if [ "$profile" = native-1311-d3d9 ]; then
    printf '/etc/w3cs/criu-game-env-native-1311-d3d9-seat%s.conf\n' "$seat"
  else
    printf '/etc/w3cs/criu-game-env-seat%s.conf\n' "$seat"
  fi
}

write_game_env() {
  local profile=$1 seat=$2 conf display_width display_height source_width source_height
  local lane
  lane=$(seat_lane "$seat")
  conf=$(game_env_file "$profile" "$seat")
  display_width=1376
  display_height=1070
  source_width=1024
  source_height=768
  if [ "$profile" = native-1311-d3d9 ]; then
    # The 1.31.1 client enters and advances at 800x600. At 1024x768 it
    # creates the device, emits the initial command snapshot, then stops
    # presenting. WebGPU scales the 4:3 command surface at the client.
    display_width=800
    display_height=600
    source_width=800
    source_height=600
  fi
  install -d -m 0755 /etc/w3cs
  # Baked into the image: the proxy reads this configuration once at DLL
  # load. Keep the encoder flags in step with w3cs-seat-launch.sh — the
  # image fingerprint covers this file, so changing a flag here (or the
  # proxy DLL) rebuilds every image on the next deploy.
  if [ "$lane" = video ]; then
    # Video lane: the engine renders (no headless bypass), the proxy emits
    # only the clock and draw counts ten times a second, and the relay
    # captures the window. Mirrors the W3CS_LANE=video branch of
    # w3cs-seat-launch.sh.
    cat > "$conf" <<EOF
W3_D3D9_CAPTURE_FILE=/tmp/w3cs-command-stream-seat$seat.bin
W3_D3D9_CAPTURE_FPS=10
W3_D3D9_CLOCK_ONLY=1
W3_D3D9_CONTROL_FILE='Z:\\tmp\\w3cs-game-control-seat$seat'
W3_D3D9_DISPLAY_HEIGHT=$display_height
W3_D3D9_DISPLAY_WIDTH=$display_width
W3_D3D9_HEADLESS_FAST=0
W3_D3D9_MAX_FPS=40
W3_D3D9_NO_RASTER=0
W3_D3D9_RING_FILE='Z:\\dev\\shm\\w3cs-ring-seat$seat'
W3_D3D9_SOURCE_HEIGHT=$source_height
W3_D3D9_SOURCE_WIDTH=$source_width
W3_D3D9_STREAM_CREATE=1
W3_D3D9_STREAM_FILE='Z:\\tmp\\w3cs-command-stream-seat$seat.bin'
W3CS_REPLAY_CLOCK_DIR='Z:\\home\\ubuntu\\w3cs-lab\\clock'
EOF
    chmod 644 "$conf"
    return
  fi
  cat > "$conf" <<EOF
W3_D3D9_CAPTURE_FILE=/tmp/w3cs-command-stream-seat$seat.bin
W3_D3D9_CAPTURE_FPS=40
W3_D3D9_CONTROL_FILE='Z:\\tmp\\w3cs-game-control-seat$seat'
W3_D3D9_DISPLAY_HEIGHT=$display_height
W3_D3D9_DISPLAY_WIDTH=$display_width
W3_D3D9_GEOMETRY_GOP=8
W3_D3D9_HEADLESS_FAST=1
W3_D3D9_MAX_FPS=40
W3_D3D9_NO_RASTER=1
W3_D3D9_RING_FILE='Z:\\dev\\shm\\w3cs-ring-seat$seat'
W3_D3D9_SOURCE_HEIGHT=$source_height
W3_D3D9_SOURCE_WIDTH=$source_width
W3_D3D9_STREAM_CREATE=1
W3_D3D9_STREAM_FILE='Z:\\tmp\\w3cs-command-stream-seat$seat.bin'
W3CS_ADAPTIVE_QUALITY=1
W3CS_BLOBREF=1
W3CS_COMPACT_DRAW=1
W3CS_OCTA=1
W3CS_PACKED_POS=1
W3CS_REPLAY_CLOCK_DIR='Z:\\home\\ubuntu\\w3cs-lab\\clock'
W3CS_QUANT=1
W3CS_QUALITY_LEVEL=8
W3CS_RAIN_QUAD=1
W3CS_TEXDELTA=1
W3CS_TYPED_ATTR=1
EOF
  chmod 644 "$conf"
}

layer_base_id() {
  local profile=$1 family=$2
  if [ "$profile" = native-1311-d3d9 ]; then
    printf 'w3cs-%s-%s-open1-v10\n' "$profile" "$family"
  else
    printf 'w3cs-%s-%s-open2-v1\n' "$profile" "$family"
  fi
}

fingerprint() {
  local profile=$1 family=$2 seat=$3 gate=${4:-default} placeholder=${5:-}
  local view env_file
  view=$(profile_view "$profile" "$seat")
  env_file=$(game_env_file "$profile" "$seat")
  local proxy_source=$PROXY_DLL
  [ "$profile" = native-1311-d3d9 ] && proxy_source=$PROXY_DLL64
  {
    sha256sum "$proxy_source" "$env_file"
    # Engine identity: hash the view's engine files, following symlinks —
    # a checkpoint holds these files open by path, so a change to a shared
    # target must invalidate the image or its restore breaks.
    if [ "$profile" = native-1311-d3d9 ]; then
      sha256sum "$view/x86_64/Warcraft III.exe" \
        "$view/x86_64/d3d9.dll" "$view/Data/data/shmem" \
        "$CLASSIC_131_ROOT/prefix64/system.reg" \
        "$CLASSIC_131_ROOT/prefix64/user.reg" \
        "$CLASSIC_131_ROOT/prefix64/userdef.reg"
      echo "prefixMode=aws-overlay-v1"
    else
      find "$view" -maxdepth 1 \( -type f -o -type l \) \
        \( -name '*.exe' -o -name '*.dll' -o -name 'War3Patch.mpq' \) \
        -print0 | sort -z | xargs -0 sha256sum
    fi
    echo "d3d=$(profile_d3d_overrides "$profile")"
    [ "$profile" = native-1311-d3d9 ] && \
      echo "launchContract=aws-audio-v4-openat"
    # Claim images hold /opt/wine-staging files open by path, so a wine
    # upgrade breaks every restore. The wine packages are apt-held to
    # keep upgrades deliberate; folding the version in here makes a
    # deliberate upgrade rebuild the matrix instead of wedging it.
    if [ "$profile" = native-1311-d3d9 ]; then
      /usr/bin/wine --version
    else
      dpkg-query -W -f 'wine=${Version}\n' wine-staging-amd64 2>/dev/null || true
    fi
    # Kept out of the default-gate fingerprint so existing images stay
    # valid; a non-default gate (and its open index) is image identity.
    [ "$gate" = default ] || echo "gate=$gate-open1"
    if [ "$gate" = 131 ]; then
      sha256sum /usr/local/lib/war3-replay-open-gate64.so \
        /usr/local/lib/war3-replay-open-gate-openat32.so \
        /usr/local/lib/war3-shims-aws131/lib/x86_64-linux-gnu/war3-deny-dmabuf.so \
        /usr/local/lib/war3-shims-aws131/lib/i386-linux-gnu/war3-deny-dmabuf.so
    fi
    # A claim also holds the host's shared libraries open by path (libc,
    # libz, libX11, mesa, ...). CRIU refuses to restore a mapping whose
    # file build-ID changed, so an unattended package upgrade silently
    # broke every image on 2026-09-01 (zlib1g). Fold the library set's
    # identity in, so the next builder run (deploy or the daily timer)
    # rebuilds the matrix instead of leaving seats on the cold-launch
    # fallback.
    echo "syslibs=$(system_library_identity)"
    echo "base=$(layer_base_id "$profile" "$family")"
    echo "placeholder=$placeholder"
    # The env file above already differs per lane; naming the lane keeps
    # a command image from ever being mistaken for a video one.
    echo "lane=$(seat_lane "$seat")"
  } | sha256sum | cut -d' ' -f1
}

system_library_identity() {
  find /usr/lib/x86_64-linux-gnu /usr/lib/i386-linux-gnu /lib64 \
    -maxdepth 1 \( -name 'lib*.so*' -o -name 'ld-linux*' \) -printf '%f %s %T@\n' \
    2>/dev/null | sort | sha256sum | cut -d' ' -f1
}

image_is_current() {
  local img=$1 fp=$2 meta=$1/meta.json
  [ -f "$meta" ] && [ -f "$img/war3.json" ] && \
    [ -f "$img/generic/inventory.img" ] && \
    grep -Fq "\"fingerprint\": \"$fp\"" "$meta"
}

build_seat() {
  (
  local seat=$1
  local entry profile family exe placeholder gate fp img out layer_id index
  local game_dir game_exe prefix status env_file xvfb_res
  local -a current_fingerprints=()
  local stopped=0

  if ! [[ "$seat" =~ ^[1-9][0-9]*$ ]]; then
    echo "invalid seat: $seat" >&2
    return 2
  fi

  restart_stopped_seat() {
    local result=$?
    trap - EXIT
    if [ "$stopped" = 1 ]; then
      if ! systemctl start "w3cs-live@$seat"; then
        echo "failed to restart w3cs-live@$seat" >&2
        [ "$result" -ne 0 ] || result=1
      fi
    fi
    exit "$result"
  }
  trap restart_stopped_seat EXIT

  # Invalidate stale checkpoints before consulting the seat. The proxy reads
  # its environment only when Wine loads the DLL, so a busy seat cannot safely
  # keep an old checkpoint for a later claim. Removing only the claim image
  # makes a deferred or failed rebuild use the existing cold-launch fallback.
  for ((index = 0; index < ${#MATRIX[@]}; ++index)); do
    entry=${MATRIX[$index]}
    IFS='|' read -r profile family exe placeholder gate <<< "$entry"
    if [ -n "$BUILD_PROFILE_FILTER" ] && [ "$profile" != "$BUILD_PROFILE_FILTER" ]; then
      continue
    fi
    fp=$(fingerprint "$profile" "$family" "$seat" "$gate" "$placeholder")
    current_fingerprints[$index]=$fp
    img=$IMAGES/${profile}__${family}/seat$seat
    if { [ -e "$img" ] || [ -L "$img" ]; } && \
        ! image_is_current "$img" "$fp"; then
      echo "DROP $profile/$family seat$seat (stale claim image; cold launch until rebuilt)"
      rm -rf -- "$img"
    fi
  done

  status=$(curl -fsS "http://127.0.0.1:$((8144 + seat))/status" 2>/dev/null || true)
  if [[ "$status" != *'"busy":false'* ]]; then
    echo "DEFER seat$seat: active viewer or relay status unavailable"
    return 0
  fi
  for ((index = 0; index < ${#MATRIX[@]}; ++index)); do
    entry=${MATRIX[$index]}
    IFS='|' read -r profile family exe placeholder gate <<< "$entry"
    if [ -n "$BUILD_PROFILE_FILTER" ] && [ "$profile" != "$BUILD_PROFILE_FILTER" ]; then
      continue
    fi
    [ -f "$LAB/replays/$placeholder.w3g" ] || {
      echo "SKIP $profile/$family seat$seat: placeholder missing"; continue;
    }
    img=$IMAGES/${profile}__${family}/seat$seat
    fp=${current_fingerprints[$index]}
    if image_is_current "$img" "$fp"; then
      echo "OK   $profile/$family seat$seat (unchanged)"
      continue
    fi
    if [ "$stopped" = 0 ]; then
      systemctl stop "w3cs-live@$seat"
      stopped=1
      sleep 3
    fi
    out=$CRIU_ROOT/build-tmp
    rm -rf "$out"
    local internal_res=()
    local profile_env=()
    game_dir=$(profile_view "$profile" "$seat")
    game_exe=$exe
    prefix=$(profile_prefix "$profile" "$seat")
    env_file=$(game_env_file "$profile" "$seat")
    xvfb_res=1376x1070
    # Only 1.28.5 honors the registry resolution stamp; older engines size
    # from the 1376x1070 desktop (outer 1032x802 -> client 1024x768) and
    # would fail the builder's window==internal check.
    [ "$profile" = native-1285 ] && internal_res=(W3_CLASSIC_INTERNAL_RES=1024x768)
    local gate_env=()
    if [ "$gate" = 114 ]; then
      # The 1.14 engine opens its replay exactly ONCE (verified with
      # W3_REPLAY_GATE_DEBUG_PATH: a single libc open of
      # .../Documents/Warcraft III/Replays/slotN.w3g). The default open
      # index of 2 — right for 1.28, which touches the replay twice —
      # left the gate waiting forever and made this lane look ungateable.
      gate_env=(
        W3_CLASSIC_REPLAY_GATE_LIB=/usr/local/lib/war3-replay-open-gate114-64.so
        W3_CLASSIC_REPLAY_GATE_LIB32=/usr/local/lib/war3-replay-open-gate114-32.so
        W3_REPLAY_GATE_OPEN_INDEX=1
      )
    elif [ "$gate" = 131 ]; then
      # The 1.31 x64 client opens the replay once, before its X11 window and
      # Pulse stream are ready. The newer openat variant stops after the
      # replay header has already entered memory, so a restored claim keeps
      # the bootstrap replay instead of the requested one.
      gate_env=(
        W3_CLASSIC_REPLAY_GATE_LIB=/usr/local/lib/war3-replay-open-gate64.so
        W3_CLASSIC_REPLAY_GATE_LIB32=/usr/local/lib/war3-replay-open-gate-openat32.so
        W3_REPLAY_GATE_OPEN_INDEX=1
        W3_REPLAY_GATE_DEBUG_PATH=/tmp/w3cs-gate-debug-seat$seat.log
        W3_CLASSIC_REQUIRE_RESOLVED_GATE=1
      )
      internal_res=(W3_CLASSIC_INTERNAL_RES=800x600)
      xvfb_res=800x600
      ensure_131_prefix "$seat"
      game_dir="$(profile_view "$profile" "$seat")/x86_64"
      game_exe='Warcraft III.exe'
      prefix=$(profile_prefix "$profile" "$seat")
      profile_env=(
        W3_WINE=/usr/bin/wine
        W3_WINESERVER=/usr/bin/wineserver
        W3_SHIM_ROOT=/usr/local/lib/war3-shims-aws131
        W3_CLASSIC_SHIM=/usr/local/lib/war3-shims-aws131/lib/x86_64-linux-gnu/war3-deny-dmabuf.so
        W3_CLASSIC_SHIM32=/usr/local/lib/war3-shims-aws131/lib/i386-linux-gnu/war3-deny-dmabuf.so
        # Keep the validated AWS audio-v4/openat filesystem and Wine contract.
        # The CPU-rendered AWS base may select D3D11. This command-stream lane
        # must explicitly select D3D9 so the native recorder receives calls.
        W3_CLASSIC_WORKING_DIRECTORY="$game_dir"
        W3_CLASSIC_REPLAY_DIRECTORY=Replay
        W3_CLASSIC_WINDOW_MODE=windowed
        W3_CLASSIC_GAME_ARGUMENT_PROFILE=classic131
        W3_CLASSIC_GATE_WAIT_TICKS=12000
        W3_CLASSIC_DEFER_WINDOW_GEOMETRY=1
        W3_CLASSIC_DEFER_AUDIO_PROOF=1
      )
    fi
    layer_id=$(layer_base_id "$profile" "$family")
    [ "$gate" = 131 ] && rm -f "/tmp/w3cs-gate-debug-seat$seat.log"
    # A video seat renders: two llvmpipe workers beside the game thread
    # (the AWS profile's optimum); a command seat's llvmpipe is idle.
    local raster_threads=1
    [ "$(seat_lane "$seat")" = video ] && raster_threads=2
    if ! env \
      "${gate_env[@]}" \
      "${profile_env[@]}" \
      W3_CLASSIC_LLVMPIPE_THREADS="$raster_threads" \
      W3_CLASSIC_GAME="$game_dir" \
      W3_CLASSIC_GAME_EXE="$game_exe" \
      W3_CLASSIC_SLOT_PREFIX="$prefix" \
      W3_CLASSIC_GAME_ENV_FILE="$env_file" \
      W3_CLASSIC_D3D_OVERRIDES="$(profile_d3d_overrides "$profile")" \
      W3_CLASSIC_AUDIO_MODE=embedded \
      W3_CLASSIC_XVFB_RES="$xvfb_res" \
      W3_CLASSIC_RENDER_FPS=40 \
      W3_CLASSIC_GAME_MODE="$family" \
      "${internal_res[@]}" \
      W3_CLASSIC_LAYER_BASE_ID="$layer_id" \
      "$BUILDER" "$seat" "$LAB/replays/$placeholder.w3g" \
      /etc/w3cs/criu-template.json "$out" | tail -1; then
      echo "FAIL $profile/$family seat$seat (seat falls back to cold launch)"
      rm -rf "$out"
      continue
    fi
    rm -rf "$img"
    mkdir -p "$img"
    cp -a "$out/$layer_id/slot$seat/generic" "$img/"
    cp -a "$out/$layer_id/slot$seat/war3.json" "$img/"
    printf '{"fingerprint": "%s", "baseId": "%s", "builtAt": "%s"}\n' \
      "$fp" "$layer_id" "$(date -u +%FT%TZ)" > "$img/meta.json"
    rm -rf "$out"
    echo "BUILT $profile/$family seat$seat"
  done
  )
}

[ -f /etc/w3cs/criu-template.json ] || echo '{}' > /etc/w3cs/criu-template.json

for entry in "${MATRIX[@]}"; do
  IFS='|' read -r profile family exe placeholder gate <<< "$entry"
  if [ "$profile" = native-1311-d3d9 ]; then
    view=$(profile_view "$profile")
    [ -d "$view" ] || assemble_view "$profile" "$exe"
  else
    view=$(profile_view "$profile")
    [ -d "$view" ] || assemble_view "$profile" "$exe"
  fi
done
# The proxy DLL changes on most deploys; refresh every view's copy so the
# fingerprint sees the current build.
for entry in "${MATRIX[@]}"; do
  IFS='|' read -r profile family exe placeholder gate <<< "$entry"
  wanted_proxy=$PROXY_DLL
  [ "$profile" = native-1311-d3d9 ] && wanted_proxy=$PROXY_DLL64
  if [ "$profile" = native-1311-d3d9 ]; then
    view=$(profile_view "$profile")
    [ -d "$view" ] || continue
    if ! cmp -s "$wanted_proxy" "$(proxy_target "$profile")" 2>/dev/null; then
      assemble_view "$profile" "$exe"
    fi
  else
    view=$(profile_view "$profile")
    [ -d "$view" ] || continue
    if ! cmp -s "$wanted_proxy" "$(proxy_target "$profile")" 2>/dev/null; then
      assemble_view "$profile" "$exe"
    fi
  fi
done

for seat in "${SEATS[@]}"; do
  echo "SEAT $seat lane=$(seat_lane "$seat")"
  write_game_env generic "$seat"
  [ "$ENABLE_131" = 1 ] && write_game_env native-1311-d3d9 "$seat"
  build_seat "$seat"
done
echo "claim image matrix complete"
