#!/usr/bin/env bash
# Hexcavator on a Steam Deck: run once in Desktop Mode (Konsole), with
#   curl -sSL https://www.artifexarcanum.ie/Hexcavator/steamdeck/install.sh | bash
# It installs Google Chrome (Flathub) if it isn't there, lets Chrome see the Deck's controls, makes a launcher
# that opens the game full-screen, and adds "Hexcavator" to the Steam library for Game Mode.
# Nothing is installed system-wide; everything lives in your home folder. Run it again any time to repair.
set -euo pipefail

GAME_URL="https://www.artifexarcanum.ie/Hexcavator/"
APP_NAME="Hexcavator"
CHROME="com.google.Chrome"
HOME_DIR="$HOME/.local/share/hexcavator"
LAUNCHER="$HOME_DIR/launch.sh"
ART="$HOME_DIR/art"

say() { printf '\n\033[1;33m%s\033[0m\n' "$*"; }
fail() { printf '\n\033[1;31m%s\033[0m\n' "$*" >&2; exit 1; }

command -v flatpak >/dev/null || fail "Flatpak isn't here - this installer is for a Steam Deck (SteamOS)."
command -v python3 >/dev/null || fail "Python 3 isn't here - this installer is for a Steam Deck (SteamOS)."

# 1. Google Chrome, from Flathub (just for you, no password needed)
if flatpak info "$CHROME" >/dev/null 2>&1; then
    say "Google Chrome is already installed."
else
    say "Installing Google Chrome from Flathub (this can take a few minutes)..."
    flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
    flatpak install --user -y flathub "$CHROME"
fi

# 2. Let Chrome see the controller: Steam's controller reaches it through /run/udev
say "Letting Chrome see the Deck's controls..."
flatpak override --user --filesystem=/run/udev:ro "$CHROME"

# 3. The launcher: the game full-screen in its own Chrome window (its own profile, so your saves stay put)
mkdir -p "$HOME_DIR"
cat > "$LAUNCHER" <<EOF
#!/usr/bin/env bash
# Hexcavator - made by the Steam Deck installer ($GAME_URL)
exec flatpak run --branch=stable --arch=x86_64 $CHROME \\
    --user-data-dir="\$HOME/.var/app/$CHROME/data/hexcavator" \\
    --kiosk --start-fullscreen --no-first-run --no-default-browser-check --noerrdialogs \\
    --disable-features=TranslateUI --autoplay-policy=no-user-gesture-required \\
    "${GAME_URL}?steamdeck"
EOF
chmod +x "$LAUNCHER"
# the library art (Hexcavator/tools/make_steam_art.py): the tile, the wide tile, the game page's banner and logo, the icon
mkdir -p "$ART"
for f in capsule wide hero logo icon; do
    curl -fsSL -o "$ART/$f.png" "${GAME_URL}steamdeck/art/$f.png" || say "(Couldn't fetch the $f art - the game will still work.)"
done

# 4. Into the Steam library. Steam rewrites its shortcuts when it closes, so it has to be closed for this.
if pgrep -x steam >/dev/null 2>&1; then
    say "Steam needs to close for a moment to add the game. It will open again afterwards."
    reply="y"
    if [ -r /dev/tty ]; then read -r -p "Close Steam now? [Y/n] " reply </dev/tty || reply="y"; fi
    case "$reply" in [nN]*) fail "Stopped: nothing added to Steam. Run the installer again when you're ready." ;; esac
    steam -shutdown >/dev/null 2>&1 || true
    for _ in $(seq 1 30); do pgrep -x steam >/dev/null 2>&1 || break; sleep 1; done
    pgrep -x steam >/dev/null 2>&1 && fail "Steam didn't close. Quit it from its menu, then run the installer again."
    reopen=1
else
    reopen=0
fi

say "Adding $APP_NAME to your Steam library..."
APP_NAME="$APP_NAME" LAUNCHER="$LAUNCHER" HOME_DIR="$HOME_DIR" ART="$ART" python3 - <<'PY'
# Steam's shortcuts.vdf is a small binary key-value file: maps (0x00), strings (0x01), 32-bit ints (0x02), each
# map ended by 0x08. Read it, add (or refresh) our entry, write it back - keeping a backup of the old one.
import glob, os, shutil, struct, zlib

name, launcher, home, art = (os.environ[k] for k in ("APP_NAME", "LAUNCHER", "HOME_DIR", "ART"))
exe = '"%s"' % launcher

def read_map(b, i):
    out = {}
    while True:
        t = b[i]; i += 1
        if t == 0x08: return out, i
        j = b.index(0, i); key = b[i:j].decode("utf-8", "replace"); i = j + 1
        if t == 0x00: out[key], i = read_map(b, i)
        elif t == 0x01: j = b.index(0, i); out[key] = b[i:j].decode("utf-8", "replace"); i = j + 1
        elif t == 0x02: out[key] = struct.unpack("<i", b[i:i + 4])[0]; i += 4
        else: raise ValueError("unknown field type %d" % t)

def write_map(m):
    b = bytearray()
    for k, v in m.items():
        kb = k.encode() + b"\0"
        if isinstance(v, dict): b += b"\x00" + kb + write_map(v)
        elif isinstance(v, int): b += b"\x02" + kb + struct.pack("<i", v)
        else: b += b"\x01" + kb + str(v).encode() + b"\0"
    return bytes(b) + b"\x08"

# the shortcut's id, as Steam works it out (for its library art)
appid = zlib.crc32((exe + name).encode()) | 0x80000000
signed = appid - (1 << 32)

users = glob.glob(os.path.expanduser("~/.steam/steam/userdata/*/config"))
users = [u for u in users if os.path.basename(os.path.dirname(u)) not in ("0", "anonymous")]
if not users:
    raise SystemExit("No Steam account found - open Steam and sign in once, then run the installer again.")
for cfg in users:
    path = os.path.join(cfg, "shortcuts.vdf")
    data = {"shortcuts": {}}
    if os.path.exists(path):
        raw = open(path, "rb").read()
        if raw:
            shutil.copy2(path, path + ".before-hexcavator")
            data, _ = read_map(raw, 0)
    entries = data.setdefault("shortcuts", {})
    # the same game added before: refreshed in place, not added twice
    keep = [e for e in entries.values() if not (isinstance(e, dict) and e.get("AppName", e.get("appname")) == name)]
    entry = {"appid": signed, "AppName": name, "Exe": exe, "StartDir": '"%s"' % home, "icon": os.path.join(art, "icon.png"),
             "ShortcutPath": "", "LaunchOptions": "", "IsHidden": 0, "AllowDesktopConfig": 1, "AllowOverlay": 1,
             "OpenVR": 0, "Devkit": 0, "DevkitGameID": "", "DevkitOverrideAppID": 0, "LastPlayTime": 0,
             "FlatpakAppID": "", "tags": {}}
    data["shortcuts"] = {str(i): e for i, e in enumerate(keep + [entry])}
    with open(path, "wb") as f: f.write(write_map(data))
    # its art in the library (Steam's grid folder, named by the shortcut's id): the portrait tile, the wide tile,
    # the banner on its page and the logo drawn over it
    grid = os.path.join(cfg, "grid"); os.makedirs(grid, exist_ok=True)
    for src, suffix in (("capsule", "p.png"), ("wide", ".png"), ("hero", "_hero.png"), ("logo", "_logo.png")):
        f = os.path.join(art, src + ".png")
        if os.path.exists(f): shutil.copy2(f, os.path.join(grid, "%d%s" % (appid, suffix)))
    print("  added for Steam account", os.path.basename(os.path.dirname(cfg)))
PY

if [ "$reopen" = 1 ]; then
    say "Opening Steam again..."
    (nohup steam >/dev/null 2>&1 &) || true
fi

say "Done! $APP_NAME is in your Steam library."
cat <<'EOF'
Go back to Game Mode and find it under Library > Non-Steam. The controls:
  left stick            walk (any way: it's a hexapod)
  right stick           look round; where you look is where it digs
  R2 (or A)             dig (hold)
  L2 (or X)             dump
  L1 / R1               turn the body
  Y                     cab view
  View button           lights
  Menu button           pause
In the menus A picks and B goes back. To leave, press the Steam button and choose Exit game.
If the controls don't respond: press the Steam button, open Controller settings for Hexcavator and
choose the "Gamepad" layout.
EOF
