#!/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
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
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"
  "native-1140|tft|war3.exe|78b48840982b638fe49c9b06a8e5b70fdb2664aa|114"
  "native-1120|roc|war3.exe|1271ea1feb3b939a60e3ec44c9b7a2b9c3a55bdb|114"
  "native-1100|roc|war3.exe|9e310763b046d34df875d456ac3894e5f446650b|114"
  "native-1110|roc|war3.exe|2c0132d4b01a54f2cd62051ef9f8d010194442b5|114"
)

# 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) echo 'd3d9=n,b;d3d8,dxgi,d3d11=b' ;;
    *) echo 'd3d9=n,b;d3d8=n,b;dxgi,d3d11=b' ;;
  esac
}

assemble_view() {
  local profile=$1 exe=$2
  local view=$CRIU_ROOT/$profile/game
  local base overlay
  rm -rf "$view"
  mkdir -p "$view"
  case "$profile" in
    native-1285) base="$CLASSIC_ROOT/war3-1285/Warcraft III" ;;
    native-1140) base="$CLASSIC_ROOT/war3-1140/Warcraft III" ;;
    native-1220|native-124ab|native-124cde|native-1120|native-1110|native-1100)
      base="$CLASSIC_ROOT/war3rk/Warcraft III" ;;
    *) echo "unsupported claim profile: $profile" >&2; return 1 ;;
  esac
  cp -as "$base/." "$view/"
  case "$profile" in
    native-1220|native-124ab|native-124cde|native-1120|native-1110|native-1100)
      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
      ;;
  esac
  rm -f "$view/d3d9.dll"
  cp "$PROXY_DLL" "$view/$(dirname "$exe")/d3d9.dll" 2>/dev/null || \
    cp "$PROXY_DLL" "$view/d3d9.dll"
  # Extended zoom-out (ceiling 3000, 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.28.5 is not covered yet: its ladder loads
  # from a shared SSE constant pool that needs proper disassembly first.
  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
      ;;
  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.
    python3 /usr/local/libexec/war3/w3cs-camera-zoom-patch.py \
      "$zoom_engine" "$view/game.dll" || {
      echo "zoom patch failed for $profile; discarding the view" >&2
      rm -rf "$view"
      return 1
    }
  fi
  chown -R ubuntu:ubuntu "$CRIU_ROOT/$profile"
}

write_seat_env() {
  local seat=$1
  local conf=/etc/w3cs/criu-game-env-seat$seat.conf
  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.
  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=1070
W3_D3D9_DISPLAY_WIDTH=1376
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=768
W3_D3D9_SOURCE_WIDTH=1024
W3_D3D9_STREAM_CREATE=1
W3_D3D9_STREAM_FILE='Z:\\tmp\\w3cs-command-stream-seat$seat.bin'
W3CS_BLOBREF=1
W3CS_OCTA=1
W3CS_QUANT=1
W3CS_TEXDELTA=1
EOF
  chmod 644 "$conf"
}

fingerprint() {
  local profile=$1 family=$2 seat=$3 gate=${4:-default}
  local view=$CRIU_ROOT/$profile/game
  {
    sha256sum "$PROXY_DLL" /etc/w3cs/criu-game-env-seat$seat.conf
    # 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.
    find "$view" -maxdepth 1 \( -type f -o -type l \) \
      \( -name '*.exe' -o -name '*.dll' -o -name 'War3Patch.mpq' \) \
      -print0 | sort -z | xargs -0 sha256sum
    echo "d3d=$(profile_d3d_overrides "$profile")"
    # 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.
    dpkg-query -W -f 'wine=${Version}\n' wine-staging-amd64 2>/dev/null || true
    # 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"
    echo "base=w3cs-$profile-$family-open2-v1"
  } | sha256sum | cut -d' ' -f1
}

build_seat() {
  local seat=$1
  local entry profile family exe placeholder gate want fp img meta out
  local stopped=0
  for entry in "${MATRIX[@]}"; do
    IFS='|' read -r profile family exe placeholder gate <<< "$entry"
    [ -f "$LAB/replays/$placeholder.w3g" ] || {
      echo "SKIP $profile/$family seat$seat: placeholder missing"; continue;
    }
    img=$IMAGES/${profile}__${family}/seat$seat
    meta=$img/meta.json
    fp=$(fingerprint "$profile" "$family" "$seat" "$gate")
    if [ -f "$meta" ] && [ -f "$img/generic/inventory.img" ] && \
        grep -q "\"fingerprint\": \"$fp\"" "$meta"; then
      echo "OK   $profile/$family seat$seat (unchanged)"
      continue
    fi
    if [ "$stopped" = 0 ]; then
      systemctl stop "w3cs-live@$seat"
      sleep 3
      stopped=1
    fi
    out=$CRIU_ROOT/build-tmp
    rm -rf "$out"
    local internal_res=()
    # 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
      )
    fi
    if ! env \
      "${gate_env[@]}" \
      W3_CLASSIC_GAME="$CRIU_ROOT/$profile/game" \
      W3_CLASSIC_GAME_EXE="$exe" \
      W3_CLASSIC_SLOT_PREFIX="$CLASSIC_ROOT/prefix-seat$seat" \
      W3_CLASSIC_GAME_ENV_FILE="/etc/w3cs/criu-game-env-seat$seat.conf" \
      W3_CLASSIC_D3D_OVERRIDES="$(profile_d3d_overrides "$profile")" \
      W3_CLASSIC_AUDIO_MODE=embedded \
      W3_CLASSIC_XVFB_RES=1376x1070 \
      W3_CLASSIC_RENDER_FPS=40 \
      W3_CLASSIC_GAME_MODE="$family" \
      "${internal_res[@]}" \
      W3_CLASSIC_LAYER_BASE_ID="w3cs-$profile-$family-open2-v1" \
      "$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/w3cs-$profile-$family-open2-v1/slot$seat/generic" "$img/"
    cp -a "$out/w3cs-$profile-$family-open2-v1/slot$seat/war3.json" "$img/"
    printf '{"fingerprint": "%s", "baseId": "w3cs-%s-%s-open2-v1", "builtAt": "%s"}\n' \
      "$fp" "$profile" "$family" "$(date -u +%FT%TZ)" > "$meta"
    rm -rf "$out"
    echo "BUILT $profile/$family seat$seat"
  done
  if [ "$stopped" = 1 ]; then
    systemctl start "w3cs-live@$seat"
  fi
}

[ -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"
  [ -d "$CRIU_ROOT/$profile/game" ] || assemble_view "$profile" "$exe"
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"
  view=$CRIU_ROOT/$profile/game
  [ -d "$view" ] || continue
  if ! cmp -s "$PROXY_DLL" "$view/d3d9.dll" 2>/dev/null; then
    assemble_view "$profile" "$exe"
  fi
done

for seat in "${SEATS[@]}"; do
  write_seat_env "$seat"
  build_seat "$seat"
done
echo "claim image matrix complete"
