# ARK: Survival Ascended — dedicated server module.
#
# ASA has no native Linux server binary — it runs the Windows build under
# Proton. Rather than bundle our own 500 MB+ Wine/Proton image, this module
# uses `acekorneya/asa_server:latest`, the most actively-maintained
# community image for ASA on Linux. It packages:
#
#   - Proton + Wine runtime
#   - SteamCMD (handles install + auto-update on start)
#   - An entrypoint that reads env vars and assembles the ASA command line
#   - BattlEye / RCON / cluster flags
#
# So: the SteamCMD-style "download game via our sidecar" pattern we use for
# 7DTD doesn't apply here — the image downloads ASA itself on first boot
# via its internal SteamCMD. No external update provider needed, which is
# why this module has NO `update_providers:` section (the `Update` button
# won't do anything useful for ASA; just restart the server to pull game
# patches — the image auto-updates when UPDATE_SERVER=true is set).
#
# Operators who want to own the stack can publish their own image (Proton
# + SteamCMD + ASA) and swap `image:` below. Keep the env vars the same —
# the community entrypoint reads `SESSION_NAME`, `SERVER_ADMIN_PASSWORD`,
# `SERVER_MAP` etc.
#
# First-run flow:
#   1. Create the server in the panel → empty container + volume.
#   2. Click Start → the image's entrypoint runs SteamCMD inside the
#      container and downloads ~18 GB of ASA into /home/pok/arkserver.
#      First boot takes 15–30 minutes depending on bandwidth.
#   3. After the install finishes the server actually starts. Open Console
#      and wait for "Server started on port 7777".
#
# Subsequent starts are quick (<= 2 min) since the files are cached on the
# named volume.

id: ark-sa
name: "ARK: Survival Ascended"
version: 0.3.0
authors:
  - panel contributors

supported_modes:
  - docker

# The acekorneya image handles its own SteamCMD download on first Start
# (UPDATE_SERVER=TRUE env). Opt out of the panel's auto-update-on-create
# sidecar — its install path doesn't match the image's layout and would
# just waste ~20 minutes creating files the server won't use.
auto_update_on_create: false
# But DO auto-start after create — the image's entrypoint runs its own
# SteamCMD on first boot and then launches the server, so Create → Start
# is the one-gesture flow operators expect. (True is the default across
# all modules; declared here so the interaction with auto_update_on_create
# being false is explicit to anyone reading this file.)
auto_start_on_create: true

runtime:
  docker:
    image: acekorneya/asa_server:latest
    # Host networking -- container shares the host net namespace, so
    # any port the game/mod binds is directly on the LAN. AMP-like
    # model. Multi-instance safety comes from env-driven per-instance
    # ports (panel allocator + env: mapping on each ports entry).
    network_mode: host
    env:
      TZ: "Etc/UTC"
      # acekorneya's entrypoint expects these. They're defaults — operators
      # override per-instance once the config-values layer is wired.
      INSTANCE_NAME: "panel-asa"
      SERVER_MAP: "TheIsland_WP"
      SESSION_NAME: "Panel ARK SA Server"
      SERVER_ADMIN_PASSWORD: "changeme-please"
      SERVER_PASSWORD: ""
      ASA_PORT: "7777"
      # Steam query port. The agent rewrites this to the actually-allocated
      # host port at create time (see resolve.go's port-env mapping) — for
      # the first ARK on an agent this stays 27015; the second gets 27016
      # or whatever the allocator picked. The acekorneya image translates
      # this into the launch arg `?QueryPort=<n>` on its end.
      QUERY_PORT: "27015"
      RCON_PORT: "27020"
      MAX_PLAYERS: "70"
      # Pull any ASA patches from Steam on every start. Cheap when up to
      # date (SteamCMD validate is a few seconds) and means the server is
      # always current.
      UPDATE_SERVER: "TRUE"
      CHECK_FOR_UPDATE_INTERVAL: "24"
      UPDATE_WINDOW_MINIMUM_TIME: "12:00 AM"
      UPDATE_WINDOW_MAXIMUM_TIME: "11:59 PM"
      RESTART_NOTICE_MINUTES: "30"
      ENABLE_MOTD: "FALSE"
      MOTD: "Welcome!"
      MOTD_DURATION: "30"
      MAP_NAME: "TheIsland"
      NOTIFY_ADMIN_COMMANDS_IN_CHAT: "FALSE"
      RANDOM_STARTUP_DELAY: "FALSE"
      # BattlEye under Wine/Proton on Linux containers silently crashes the
      # server ~2-3 minutes after "Server has successfully started!" (no
      # log output between the success line and the process exit — classic
      # UE5 silent death on BE handshake failure). Operators can enable it
      # per-instance once BE for ASA/Linux gets sorted upstream; for now
      # the default is off so servers actually stay up.
      BATTLEEYE: "FALSE"
      RCON_ENABLED: "TRUE"
      DISPLAY_POK_MONITOR_MESSAGE: "FALSE"
      MOD_IDS: ""
      PASSIVE_MODS: ""
      CUSTOM_SERVER_ARGS: ""
      CLUSTER_ID: ""
      # ----- EOS / session-browser stability knobs (see post_start) -----
      # ASA reports the host's auto-detected adapter IP to Epic Online
      # Services when it registers the server session for the in-game
      # browser. Under Docker (host-networking, multiple bridges, IPv6
      # scopes) EOS sometimes picks the wrong adapter on a re-registration
      # and advertises an unreachable IP. The server stays up + RCON works
      # + connect-by-IP works, but it vanishes from the browser. ASA
      # exposes `-PublicIPForEpic=<ipv4>` to override the auto-detect.
      # Leave empty to keep ASA's default behavior; set to the WAN IPv4
      # players reach (the WG-edge VPS for this fleet) to pin EOS to it.
      # Reference: forums.unrealengine.com EOS-DS browser-visibility thread.
      PUBLIC_IP_FOR_EPIC: ""
      # Run as UID/GID 1000 so writes land on the volume as a normal user.
      PUID: "1000"
      PGID: "1000"
    # The acekorneya image runs ARK under Wine — the live install lives at
    # the Wine-virtualized Windows path below. Early module versions mounted
    # /home/pok/arkserver thinking it was a symlink; it isn't — that dir
    # doesn't exist at image build time, so the volume was essentially
    # unused, and saves/configs lived only in the container's ephemeral
    # overlay FS. Fixed by mounting DIRECTLY at the Wine path, matching
    # acekorneya's own docker-compose example. Saves, configs, logs, and
    # admin lists all live under this Saved/ dir.
    browseable_root: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved"
    volumes:
      # Whole ASA install (game binaries + ShooterGame/Saved/{configs,
      # world data, logs}). One volume keeps things simple and mirrors
      # the upstream docker-compose pattern.
      - { type: volume, name: "panel-$INSTANCE_ID-server",
          container: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server" }
      # Cluster transfer dir — may be overridden at instance create time
      # to point at a shared `panel-ark-cluster-<id>` volume for
      # multi-server clustering (see the Cluster button in the ARK modal).
      - { type: volume, name: "panel-$INSTANCE_ID-cluster",
          container: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved/clusters" }

ports:
  # `env:` tells the agent which container env var to set with the
  # allocated host port. The acekorneya entrypoint reads ASA_PORT /
  # QUERY_PORT / RCON_PORT and assembles ARK's launch args from them
  # (?Port=N ?QueryPort=N ?RCONPort=N). Without env-mirror, the second
  # ARK instance on the same agent gets distinct host bindings but
  # ARK still advertises 7777/27015 to Steam, so the browser shows
  # only one of them.
  - { name: game,  proto: udp, default: 7777,  required: true, env: ASA_PORT }
  - { name: raw,   proto: udp, default: 7778,  required: true }
  # Steam server-browser query port. ASA registers itself with the Steam
  # master at this port, and `?QueryPort=<n>` on the ASA launch command
  # tells the engine which port to bind + advertise. Without this entry
  # in the manifest, the allocator can't pick distinct host ports for
  # multiple instances on the same agent — they all clobber 27015 and
  # only one shows up in the Steam server list.
  - { name: query, proto: udp, default: 27015, required: true, env: QUERY_PORT }
  - { name: rcon,  proto: tcp, default: 27020, internal: true, env: RCON_PORT }

# post_start: shell commands executed inside the container by the agent
# after a successful Start. Best-effort — failures log a warning but don't
# fail the Start.
#
# Why this hook exists: ASA writes RCONPort=<n> under [ServerSettings] of
# GameUserSettings.ini at first boot, which is enough for the port to bind
# but the server silently refuses to respond to Source-RCON AUTH packets.
# Adding a duplicate RCONPort line under [SessionSettings] fixes it — a
# real, reproducible ASA quirk (hit with raw Python + the image's bundled
# rcon-cli, not panel-specific). Verified working on princess 2026-04-22.
#
# The hook: (a) waits up to 3 min for the ini to exist (cold first-boot
# SteamCMD install can take 15-30 min but the ini is written well before
# ASA launches), (b) bails out if the [SessionSettings] RCONPort already
# exists (idempotent — subsequent starts are no-ops), (c) injects the
# line, (d) signals the monitor_ark_server.sh watchdog to relaunch ASA so
# the server re-reads the patched config on its next boot cycle. The
# relaunch is the clean way — pkill is safe because the image's
# monitor_ark_server.sh is designed to re-spawn ArkAscendedServer.exe
# whenever it dies.
lifecycle:
  post_start:
    - |
      set -e
      LAUNCH="/usr/games/scripts/launch_ASA.sh"
      INI="/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved/Config/WindowsServer/GameUserSettings.ini"
      WINEPREFIX_DIR="/usr/games/.wine"
      CA_MARKER="$WINEPREFIX_DIR/.panel-amazon-roots-installed-v1"
      changed=0
      # ----- Fix 1: launch_ASA.sh URL is missing ?QueryPort= -----
      # The acekorneya image hard-codes "?Port=${ASA_PORT}" in the launch
      # URL but never appends "?QueryPort=", so every sibling ARK instance
      # defaults to 27015 — only the first to bind wins, the rest have
      # NO query port. Without a query port, EOS / Steam server browser
      # can't advertise the server, so clients can't connect.
      # Idempotent: a second run sees the marker comment and no-ops.
      if [ -f "$LAUNCH" ] && ! grep -q 'QueryPort=\${QUERY_PORT' "$LAUNCH"; then
        echo "[panel-hook] patching $LAUNCH to add ?QueryPort=\${QUERY_PORT}"
        sed -i 's|?Port=${ASA_PORT}|?Port=${ASA_PORT}?QueryPort=${QUERY_PORT}|' "$LAUNCH"
        changed=1
      fi
      # ----- Fix 2: GameUserSettings.ini RCONPort under [SessionSettings] -----
      # ARK ASA's RCON server silently rejects AUTH unless RCONPort also
      # appears under [SessionSettings] (verified on princess 2026-04-22).
      # ASA accepts the launch-URL ?RCONPort=N for binding, but the
      # adapter handshake also reads from this INI key.
      for i in $(seq 1 180); do [ -f "$INI" ] && break; sleep 1; done
      if [ -f "$INI" ] && ! awk '/^\[SessionSettings\]/{f=1; next} /^\[/{f=0} f && /^RCONPort=/{found=1} END{exit !found}' "$INI"; then
        echo "[panel-hook] injecting RCONPort=${RCON_PORT:-27020} under [SessionSettings]"
        awk -v port="${RCON_PORT:-27020}" '/^\[SessionSettings\]/{print; print "RCONPort=" port; next} {print}' "$INI" > "${INI}.new"
        mv "${INI}.new" "$INI"
        changed=1
      fi
      # ----- Fix 3: pin EOS-advertised IP via -PublicIPForEpic ----------------
      # WHY: ASA's EOS session registration auto-detects the host's outbound
      # IP each time it re-registers. Under Docker host-net with multiple
      # interfaces (docker0, br-*, IPv6 link-local) EOS sometimes picks the
      # wrong adapter on a re-register and advertises an unreachable IP to
      # the in-game browser → "server invisible but RCON works" + "restart
      # fixes it" pattern. ASA exposes -PublicIPForEpic=<ipv4> to override.
      # Reference: Epic dev-forum confirmation (multihome → wrong-EOS-IP).
      # Idempotent: marker on the wine binary line means we only inject once.
      if [ -n "$PUBLIC_IP_FOR_EPIC" ] && [ -f "$LAUNCH" ] && ! grep -q -- '-PublicIPForEpic=' "$LAUNCH"; then
        echo "[panel-hook] injecting -PublicIPForEpic=${PUBLIC_IP_FOR_EPIC} into $LAUNCH"
        # Insert the flag right after the ArkAscendedServer.exe binary path.
        # The launch line ends with a backslash continuation, so the next
        # line (the URL + extra args) is unaffected.
        sed -i 's|ArkAscendedServer.exe" \\$|ArkAscendedServer.exe" -PublicIPForEpic='"${PUBLIC_IP_FOR_EPIC}"' \\|' "$LAUNCH"
        changed=1
      fi
      # ----- Fix 4: ensure system CA bundle is up-to-date (idempotent) -------
      # WHY: EOS endpoints (*.epicgames.dev, *.on.epicgames.com) are on AWS
      # CloudFront and chain to Amazon Root CA 1-4 + Starfield Services G2.
      # Whether the EOS SDK under Wine uses Wine's HKLM root store or the
      # statically-linked openssl/curl bundle inside the game, missing CAs
      # in the system bundle have been correlated with hours-later EOS
      # token-refresh failures (session ages out → server vanishes from
      # in-game browser → container restart fixes by replaying cached auth).
      # Reference: acekorneya/Ark-Survival-Ascended-Server#8 (open).
      # The acekorneya image's Debian base lists the Amazon PEMs as enabled
      # in /etc/ca-certificates.conf but may have a stale bundle if
      # update-ca-certificates didn't run during image build. Marker file
      # makes this once-per-prefix-lifetime — the run itself is ~1s if
      # nothing needs rebuilding, so the marker is just for log noise.
      if [ ! -f "$CA_MARKER" ]; then
        echo "[panel-hook] running update-ca-certificates to refresh system CA bundle"
        if update-ca-certificates >/dev/null 2>&1; then
          echo "[panel-hook]   system CA bundle refreshed"
          touch "$CA_MARKER" 2>/dev/null || true
        else
          echo "[panel-hook]   warn: update-ca-certificates failed (non-fatal)"
        fi
      fi
      if [ "$changed" = "1" ]; then
        # Relaunch ASA so it picks up the new launch script + INI. The
        # image's monitor_ark_server.sh spots the missing PID within ~60s
        # and respawns ArkAscendedServer.exe with the fixed config.
        pkill -f ArkAscendedServer.exe 2>/dev/null || true
        echo "[panel-hook] fixups applied; ASA will relaunch via image monitor"
      else
        echo "[panel-hook] all fixups already in place — no-op"
      fi

resources:
  min_ram_mb: 12288
  recommended_ram_mb: 16384

# What gets archived when an operator clicks Back-up-now. Without this
# the agent tarballs the whole 18 GB ASA install (game binaries +
# Proton runtime + saves), which is wasteful — the only files that
# actually need preserving are under ShooterGame/Saved/. Cluster
# transfers live in a sibling clusters/ dir which IS bind-mounted to
# a shared volume on cluster setup, so we exclude that — restoring
# saves shouldn't clobber cluster data the other servers are using.
backup:
  # Paths are RELATIVE to browseable_root (which is already
  # ".../ShooterGame/Saved"), NOT relative to the container root. The
  # earlier "ShooterGame/Saved/SavedArks" form pointed at a doubled path
  # that doesn't exist, so every include filtered out and busybox tar
  # got an empty stdin → exited non-zero → "sidecar exited 1".
  include:
    - "SavedArks"      # world saves (the main thing)
    - "Config"         # GameUserSettings.ini, Game.ini
    - "Logs"           # tribe logs, server logs
  exclude:
    - "clusters"               # shared volume; restoring it would clobber siblings
    - "SavedArks/*/Mods"       # mod cache; rebuilds on next start
    - "Crashes"                # crash dumps — never useful in a restore
  notes: "Saves + configs + tribe logs (~250 MB typical). Game install + Proton runtime are preserved by SteamCMD on next start; no need to back them up."

rcon:
  adapter: docker_exec_rcon
  host_port: rcon
  auth: password
  # Source-of-truth precedence (highest first, per agent's resolveRCONAddress + tracker):
  #   1. config_values["rcon_password"] — explicit override (rare)
  #   2. config_values["SERVER_ADMIN_PASSWORD"] via password_secret —
  #      the env we set at create time. THIS is what actually works
  #      for the acekorneya image.
  #   3. password_from_ini lazy-read at dial time — only effective
  #      AFTER the operator changes the password via the Config tab
  #      and the server has restarted to pick it up.
  #
  # Why both: the acekorneya image takes SERVER_ADMIN_PASSWORD and
  # passes it on the launch command line as `?ServerAdminPassword=...`.
  # ARK SA accepts the launch-arg version BUT does NOT write it back
  # into GameUserSettings.ini's [ServerSettings]ServerAdminPassword
  # — that key stays empty until the operator edits it. So the env
  # value is the working credential. The INI fallback is for after
  # the operator has changed the password via the Config tab (which
  # writes to the INI but doesn't propagate back into the env).
  password_secret: SERVER_ADMIN_PASSWORD
  password_from_ini:
    file: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved/Config/WindowsServer/GameUserSettings.ini"
    section: ServerSettings
    key: ServerAdminPassword
  commands:
    list_players: "ListPlayers"
    say:          "ServerChat {msg}"
    broadcast:    "Broadcast {msg}"
    kick:         "KickPlayer {player}"
    ban:          "BanPlayer {player}"
    save:         "SaveWorld"
    shutdown:     "DoExit"
    give_item:    "GiveItemToPlayer {player} {item} {qty} 1 0"
    set_time:     "SetTimeOfDay {time}"

state_sources:
  - type: rcon
    command: "ListPlayers"
    every: 30s
    parse:
      kind: regex
      pattern: '^(?P<index>\d+)\.\s+(?P<name>.+?),\s+(?P<steamid>\d+)'
      fields:
        players_online: index
  - type: log_tail
    files:
      - "$DATA_PATH/logs/*.log"
      - "/home/pok/arkserver/ShooterGame/Saved/Logs/ShooterGame.log"

events:
  join:
    pattern: "(?P<name>[^ ]+) joined"
    kind: join
  leave:
    pattern: "(?P<name>[^ ]+) left"
    kind: leave
  chat:
    pattern: "Chat: \\[(?P<name>[^\\]]+)\\] (?P<msg>.+)"
    kind: chat

# The panel manifest validator requires at least one update_provider. For
# ARK: SA the *real* update path is the image's built-in SteamCMD loop
# (env UPDATE_SERVER=TRUE). The stub below exists so the manifest loads;
# don't rely on it — the install path won't match the image's expected
# layout so running the panel's SteamCMD sidecar against it is a no-op at
# best. Click "Restart" instead; that triggers the image's update cycle.
update_providers:
  - id: builtin
    kind: steamcmd
    app_id: "2430930"
    install_path: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server"

# --- Manifest-driven UI metadata (WI-07) --------------------------
# Consumed by the CONTROLLER/dashboard only (the controller loads its
# own ./modules copy at startup); the agent ignores these at runtime.
# ready_pattern is a JavaScript regex (slash-delimited when it needs
# flags) moved verbatim from the dashboard's READY_PATTERNS map.
ready_pattern: '/Server has completed startup and is now advertising/i'
appearance:
  emoji: "🦖"
  grad: "linear-gradient(135deg,#3a2e1e,#d97706)"
  art: "/game-art/ark-sa.jpg"
  steam: "2430930"
