#!/bin/bash
# panel-native Conan Exiles entrypoint.
#
# Contract:
#   /game       — SteamCMD-installed Windows build of Conan Exiles Dedicated
#                 Server (populated by the panel's SteamCMD updater sidecar).
#                 If missing, exit with EX_CONFIG and a helpful message.
#   /game-saves — ConanSandbox/Saved/ tree (world data) + Config/ INIs.
#
# Stage 1 runs as root: chown volumes (SteamCMD wrote as root), nuke stale
# X lock, symlink /Logs + ConanSandbox/Saved into the save volume, then drop
# to the `panel` user via setpriv. Wine refuses to run as root.
#
# Stage 2 (panel user): stamp config_values env vars into [ServerSettings]
# of ServerSettings.ini, then exec the dedicated server under Wine + Xvfb.

set -euo pipefail

log() { printf '[panel-conan] %s\n' "$*"; }

# ----- stage 1: root-only bootstrap -----
if [[ "$(id -u)" = "0" ]]; then
  chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true

  # UE4/UE5 under Wine writes to Z:\Logs (= /Logs). Pre-create as symlink
  # into save volume so logs survive container recreate + show up in Files
  # tab.
  mkdir -p /game-saves/ConanLogs
  chown -R panel:panel /game-saves/ConanLogs
  if [[ ! -e /Logs || -L /Logs ]]; then
    ln -sfn /game-saves/ConanLogs /Logs
  fi

  log "dropping privileges to panel (UID 1000)"
  exec setpriv --reuid=panel --regid=panel --clear-groups \
       --inh-caps=-all --bounding-set=-all \
       env \
         HOME=/home/panel \
         WINEPREFIX=/home/panel/.wine \
         WINEDEBUG=-all \
         STEAM_APP_ID="${STEAM_APP_ID:-440900}" \
         SteamAppId="${STEAM_APP_ID:-440900}" \
         SERVER_NAME="${SERVER_NAME:-panel Conan Exiles}" \
         SERVER_PASSWORD="${SERVER_PASSWORD:-}" \
         ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" \
         RCON_PASSWORD="${RCON_PASSWORD:-panel_rcon}" \
         MAX_PLAYERS="${MAX_PLAYERS:-40}" \
         SERVER_REGION="${SERVER_REGION:-1}" \
         SERVER_COMMUNITY="${SERVER_COMMUNITY:-0}" \
         MAX_NUDITY="${MAX_NUDITY:-0}" \
         SERVER_MAP="${SERVER_MAP:-/Game/Maps/ConanSandbox/ConanSandbox}" \
         CUSTOM_MAP="${CUSTOM_MAP:-}" \
         ENABLE_BATTLEYE="${ENABLE_BATTLEYE:-False}" \
         ENABLE_VAC="${ENABLE_VAC:-True}" \
         PVP_ENABLED="${PVP_ENABLED:-}" \
         AVATARS_ENABLED="${AVATARS_ENABLED:-}" \
         CONTAINERS_IGNORE_OWNERSHIP="${CONTAINERS_IGNORE_OWNERSHIP:-}" \
         CAN_DAMAGE_PLAYER_OWNED_STRUCTURES="${CAN_DAMAGE_PLAYER_OWNED_STRUCTURES:-}" \
         DYNAMIC_BUILDING_DAMAGE="${DYNAMIC_BUILDING_DAMAGE:-}" \
         FRIENDLY_FIRE_DAMAGE="${FRIENDLY_FIRE_DAMAGE:-}" \
         PLAYER_XP_RATE="${PLAYER_XP_RATE:-}" \
         PLAYER_XP_KILL="${PLAYER_XP_KILL:-}" \
         PLAYER_XP_HARVEST="${PLAYER_XP_HARVEST:-}" \
         PLAYER_XP_CRAFT="${PLAYER_XP_CRAFT:-}" \
         PLAYER_XP_TIME="${PLAYER_XP_TIME:-}" \
         PLAYER_DAMAGE_MULTIPLIER="${PLAYER_DAMAGE_MULTIPLIER:-}" \
         PLAYER_DAMAGE_TAKEN="${PLAYER_DAMAGE_TAKEN:-}" \
         PLAYER_HEALTH_REGEN="${PLAYER_HEALTH_REGEN:-}" \
         PLAYER_STAMINA_COST="${PLAYER_STAMINA_COST:-}" \
         PLAYER_SPRINT_SPEED="${PLAYER_SPRINT_SPEED:-}" \
         PLAYER_MOVEMENT_SPEED="${PLAYER_MOVEMENT_SPEED:-}" \
         PLAYER_ENCUMBRANCE="${PLAYER_ENCUMBRANCE:-}" \
         NPC_DAMAGE_MULTIPLIER="${NPC_DAMAGE_MULTIPLIER:-}" \
         NPC_DAMAGE_TAKEN="${NPC_DAMAGE_TAKEN:-}" \
         NPC_HEALTH_MULTIPLIER="${NPC_HEALTH_MULTIPLIER:-}" \
         NPC_RESPAWN_MULTIPLIER="${NPC_RESPAWN_MULTIPLIER:-}" \
         NPC_MAX_SPAWN_CAP="${NPC_MAX_SPAWN_CAP:-}" \
         STRUCTURE_DAMAGE_MULTIPLIER="${STRUCTURE_DAMAGE_MULTIPLIER:-}" \
         STRUCTURE_HEALTH_MULTIPLIER="${STRUCTURE_HEALTH_MULTIPLIER:-}" \
         LANDCLAIM_RADIUS_MULTIPLIER="${LANDCLAIM_RADIUS_MULTIPLIER:-}" \
         AVATAR_DOME_DURATION="${AVATAR_DOME_DURATION:-}" \
         AVATAR_DOME_DAMAGE="${AVATAR_DOME_DAMAGE:-}" \
         GAME_PORT="${GAME_PORT:-7777}" \
         PINGER_PORT="${PINGER_PORT:-7778}" \
         QUERY_PORT="${QUERY_PORT:-27015}" \
         RCON_PORT="${RCON_PORT:-25575}" \
         PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
       /entrypoint.sh "$@"
fi

GAME_DIR=/game
SAVE_DIR=/game-saves

# --- edition-aware launch mode ---
# Enhanced (UE5) is the native-Linux depot — direct exec, no Wine.
# Legacy (UE4) is Windows-only — Wine + Xvfb.
# Drives binary path, fallback find pattern, config subdir, prefix bootstrap,
# SIGTERM trap, and the launch line further below.
case "${EDITION:-enhanced}" in
  legacy)
    LAUNCH_MODE=wine
    CANDIDATES=("${GAME_DIR}/ConanSandbox/Binaries/Win64/ConanSandboxServer-Win64-Shipping.exe")
    CONFIG_SUBDIR=WindowsServer
    ;;
  *)
    LAUNCH_MODE=native
    CANDIDATES=("${GAME_DIR}/ConanSandbox/Binaries/Linux/ConanSandboxServer-Linux-Shipping")
    CONFIG_SUBDIR=LinuxServer
    ;;
esac
BIN_BASENAME=$(basename "${CANDIDATES[0]}")
log "edition: ${EDITION:-enhanced}  launch mode: ${LAUNCH_MODE}  config subdir: ${CONFIG_SUBDIR}"

# --- locate the server binary ---
# Enhanced UE5 Linux depot ships the binary at
#   /game/ConanSandbox/Binaries/Linux/ConanSandboxServer-Linux-Shipping (ELF).
# Legacy UE4 Windows depot ships
#   /game/ConanSandbox/Binaries/Win64/ConanSandboxServer-Win64-Shipping.exe (PE).
# Mod loading happens via ServerSettings.ini's ServerModList= key (populated
# by the panel's Mods tab) AND ConanSandbox/Mods/modlist.txt — Conan reads both.
DEDI_EXE=""
for c in "${CANDIDATES[@]}"; do
  if [[ -f "$c" ]]; then
    DEDI_EXE="$c"
    break
  fi
done
if [[ -z "${DEDI_EXE}" ]]; then
  found=$(find "${GAME_DIR}" -maxdepth 6 -iname "${BIN_BASENAME}" -type f 2>/dev/null | head -1 || true)
  if [[ -n "${found}" ]]; then
    DEDI_EXE="${found}"
  fi
fi
if [[ -z "${DEDI_EXE}" ]]; then
  log "ERROR: ${BIN_BASENAME} not found under ${GAME_DIR}."
  log "       Run 'Update' on this server in the panel — that launches a"
  log "       SteamCMD sidecar which installs Conan Exiles into this volume"
  log "       for the selected edition (${EDITION:-enhanced})."
  exit 78   # EX_CONFIG
fi
log "found server binary: ${DEDI_EXE}"

# --- persist ConanSandbox/Saved into /game-saves ---
# UE4/UE5 writes world saves + config INIs under <GameRoot>/ConanSandbox/Saved/.
# Move to save volume on first boot + symlink back, so SteamCMD validate
# can't wipe world data. Idempotent on subsequent boots.
mkdir -p "${SAVE_DIR}/ConanSandbox"
if [[ -d "${GAME_DIR}/ConanSandbox/Saved" && ! -L "${GAME_DIR}/ConanSandbox/Saved" ]]; then
  if [[ ! -d "${SAVE_DIR}/ConanSandbox/Saved" ]]; then
    mv "${GAME_DIR}/ConanSandbox/Saved" "${SAVE_DIR}/ConanSandbox/Saved"
  else
    rm -rf "${GAME_DIR}/ConanSandbox/Saved"
  fi
fi
mkdir -p "${SAVE_DIR}/ConanSandbox/Saved/Config/${CONFIG_SUBDIR}"
ln -sfn "${SAVE_DIR}/ConanSandbox/Saved" "${GAME_DIR}/ConanSandbox/Saved"

# First-time native boot after Wine: if WindowsServer/ has config and
# LinuxServer/ does not yet, copy ServerSettings.ini + Engine.ini + Game.ini
# across so operator-tuned values survive the Wine -> native cut-over. Wine
# path is untouched; both dirs coexist.
if [[ "${LAUNCH_MODE}" = native ]]; then
  WIN_CFG_DIR="${SAVE_DIR}/ConanSandbox/Saved/Config/WindowsServer"
  LINUX_CFG_DIR="${SAVE_DIR}/ConanSandbox/Saved/Config/LinuxServer"
  mkdir -p "${LINUX_CFG_DIR}"
  for f in ServerSettings.ini Engine.ini Game.ini; do
    if [[ -f "${WIN_CFG_DIR}/${f}" && ! -f "${LINUX_CFG_DIR}/${f}" ]]; then
      log "migrating ${f} from WindowsServer/ to LinuxServer/ (first native boot)"
      cp -a "${WIN_CFG_DIR}/${f}" "${LINUX_CFG_DIR}/${f}"
    fi
  done
fi

# OSS / Engine.ini: intentionally left alone.
#
# Empirically (see reference_conan_oss_steam_impossible.md): Conan Exiles
# Enhanced (UE5) native Linux dedi cannot be coerced into OSS=Steam without
# breaking the socket bind. AMP's Conan01 instance on figaro runs the same
# build with OSS=Null, no Engine.ini, no steam_appid.txt, and no
# ~/.steam/sdk64/steamclient.so symlink — and we now do the same. Engine
# default is Null, which binds IpNetDriver cleanly. Direct-IP join works;
# Steam server-browser listing is not achievable on this build.
#
# SteamAppId env var is already exported in stage 1 — that's enough for the
# Steamworks Game Server API to identify itself without a steam_appid.txt
# file on disk.

# --- stamp config_values env into ServerSettings.ini ---
# Each non-empty env value is written under [ServerSettings] (replace if the
# key already exists, append if not). Empty values are SKIPPED so operator
# manual edits to ServerSettings.ini via the Files tab survive boot. The
# panel UI's Config tab → Save → recreate-with-env path is the canonical
# way to mutate INI values; direct file edits are for fields the panel
# doesn't expose.
INI="${SAVE_DIR}/ConanSandbox/Saved/Config/${CONFIG_SUBDIR}/ServerSettings.ini"
if [[ ! -f "${INI}" ]]; then
  log "ServerSettings.ini missing — initializing with [ServerSettings] header"
  printf '[ServerSettings]\n' > "${INI}"
fi
if ! grep -qE '^\[ServerSettings\]' "${INI}"; then
  log "ServerSettings.ini lacked [ServerSettings] header — prepending"
  printf '[ServerSettings]\n' > "${INI}.new"
  cat "${INI}" >> "${INI}.new"
  mv "${INI}.new" "${INI}"
fi

# set_ini_kv KEY VALUE: write/replace KEY=VALUE under [ServerSettings].
# Skip on empty value. Idempotent. Tolerates `&` and `\` in values via the
# safe_value escape (sed RHS replacement metachars).
set_ini_kv() {
  local key="$1" value="$2"
  if [[ -z "${value}" ]]; then return 0; fi
  local safe_value
  safe_value=$(printf '%s' "${value}" | sed -e 's/[\\&|]/\\&/g')
  if grep -qE "^${key}=" "${INI}"; then
    sed -i "s|^${key}=.*|${key}=${safe_value}|" "${INI}"
  else
    sed -i "/^\[ServerSettings\]/a ${key}=${safe_value}" "${INI}"
  fi
}

# Resolve the {{CUSTOM_MAP}} sentinel: if Map is set to "Custom" via the UI
# the panel passes SERVER_MAP="{{CUSTOM_MAP}}" or "{{CustomMap}}" — swap in
# the operator's CUSTOM_MAP value (or fall back to Exiled Lands if blank).
case "${SERVER_MAP}" in
  "{{CUSTOM_MAP}}"|"{{CustomMap}}")
    if [[ -n "${CUSTOM_MAP}" ]]; then
      log "Map set to Custom; resolving to ${CUSTOM_MAP}"
      SERVER_MAP="${CUSTOM_MAP}"
    else
      log "WARNING: Map set to Custom but CUSTOM_MAP is empty; falling back to Exiled Lands"
      SERVER_MAP="/Game/Maps/ConanSandbox/ConanSandbox"
    fi
    ;;
esac

log "stamping operator config_values into ${INI}"

# Identity
set_ini_kv "ServerName"            "${SERVER_NAME}"
set_ini_kv "ServerPassword"        "${SERVER_PASSWORD}"
set_ini_kv "AdminPassword"         "${ADMIN_PASSWORD}"
set_ini_kv "MaxPlayers"            "${MAX_PLAYERS}"
set_ini_kv "serverRegion"          "${SERVER_REGION}"
set_ini_kv "ServerCommunity"       "${SERVER_COMMUNITY}"
set_ini_kv "MaxNudity"             "${MAX_NUDITY}"

# Anti-cheat
set_ini_kv "IsBattlEyeEnabled"     "${ENABLE_BATTLEYE}"
set_ini_kv "IsVACEnabled"          "${ENABLE_VAC}"

# PvP / world
set_ini_kv "PVPEnabled"                       "${PVP_ENABLED}"
set_ini_kv "AvatarsEnabled"                   "${AVATARS_ENABLED}"
set_ini_kv "ContainersIgnoreOwnership"        "${CONTAINERS_IGNORE_OWNERSHIP}"
set_ini_kv "CanDamagePlayerOwnedStructures"   "${CAN_DAMAGE_PLAYER_OWNED_STRUCTURES}"
set_ini_kv "DynamicBuildingDamage"            "${DYNAMIC_BUILDING_DAMAGE}"
set_ini_kv "FriendlyFireDamageMultiplier"     "${FRIENDLY_FIRE_DAMAGE}"

# XP rates
set_ini_kv "PlayerXPRateMultiplier"     "${PLAYER_XP_RATE}"
set_ini_kv "PlayerXPKillMultiplier"     "${PLAYER_XP_KILL}"
set_ini_kv "PlayerXPHarvestMultiplier"  "${PLAYER_XP_HARVEST}"
set_ini_kv "PlayerXPCraftMultiplier"    "${PLAYER_XP_CRAFT}"
set_ini_kv "PlayerXPTimeMultiplier"     "${PLAYER_XP_TIME}"

# Player tuning
set_ini_kv "PlayerDamageMultiplier"           "${PLAYER_DAMAGE_MULTIPLIER}"
set_ini_kv "PlayerDamageTakenMultiplier"      "${PLAYER_DAMAGE_TAKEN}"
set_ini_kv "PlayerHealthRegenSpeedScale"      "${PLAYER_HEALTH_REGEN}"
set_ini_kv "PlayerStaminaCostMultiplier"      "${PLAYER_STAMINA_COST}"
set_ini_kv "PlayerSprintSpeedScale"           "${PLAYER_SPRINT_SPEED}"
set_ini_kv "PlayerMovementSpeedScale"         "${PLAYER_MOVEMENT_SPEED}"
set_ini_kv "PlayerEncumbranceMultiplier"      "${PLAYER_ENCUMBRANCE}"

# NPC tuning
set_ini_kv "NPCDamageMultiplier"      "${NPC_DAMAGE_MULTIPLIER}"
set_ini_kv "NPCDamageTakenMultiplier" "${NPC_DAMAGE_TAKEN}"
set_ini_kv "NPCHealthMultiplier"      "${NPC_HEALTH_MULTIPLIER}"
set_ini_kv "NPCRespawnMultiplier"     "${NPC_RESPAWN_MULTIPLIER}"
set_ini_kv "NPCMaxSpawnCapMultiplier" "${NPC_MAX_SPAWN_CAP}"

# Structures / land claim
set_ini_kv "StructureDamageMultiplier"  "${STRUCTURE_DAMAGE_MULTIPLIER}"
set_ini_kv "StructureHealthMultiplier"  "${STRUCTURE_HEALTH_MULTIPLIER}"
set_ini_kv "LandClaimRadiusMultiplier"  "${LANDCLAIM_RADIUS_MULTIPLIER}"
set_ini_kv "AvatarDomeDurationMultiplier" "${AVATAR_DOME_DURATION}"
set_ini_kv "AvatarDomeDamageMultiplier"   "${AVATAR_DOME_DAMAGE}"

# --- Steam OSS fix — INCOMPLETE, currently disabled ---
# Live experiment on conan-1 (2026-05-10) showed:
#   1. Adding DefaultPlatformService=Steam to Engine.ini alone is a no-op:
#      Steam SDK reports "[STEAM] disabled" because the Windows binary can't
#      locate steamclient64.dll (Funcom's dedi-server depot ships these at
#      /game root, not under ConanSandbox/Binaries/Win64/).
#   2. Copying steamclient*.dll + tier0_s*.dll + vstdlib_s*.dll +
#      steamwebrtc*.dll + a steam_appid.txt (440900) into the binary dir
#      makes the SDK initialize successfully:
#        LogOnline: STEAM: [AppId: 440900] Game Server API initialized 1
#        LogOnline: OSS: Created online subsystem instance for: STEAM
#   3. BUT once Steam OSS comes up under Wine, the engine hangs the
#      GameThread within ~80s, never binds the game port (8010 UDP),
#      and LogServerStats reports "NetDriver is null". Pre-fix logs had
#      none of these symptoms (server bound + responded to A2S fine, just
#      couldn't accept real-client joins past encryption handshake).
# Net effect: enabling Steam OSS via Engine.ini + DLL drops trades one
# failure mode (handshake reject) for a worse one (no NetDriver at all).
# Leaving the original Engine.ini untouched until we find the correct
# NetDriverDefinitions / SteamSockets-disable knob that lets Steam OSS
# coexist with a working IpNetDriver under Wine.
#
# See session log 2026-05-10 for full investigation.

# --- Wine prefix bootstrap (first boot only, legacy/Wine path only). xvfb-run
#     gives Wine a guaranteed-fresh DISPLAY without colliding with host Xvfb
#     on :99. Native (enhanced) path skips this entirely. ---
if [[ "${LAUNCH_MODE}" = wine ]]; then
  if [[ ! -d "${WINEPREFIX}" || ! -f "${WINEPREFIX}/system.reg" ]]; then
    log "initializing Wine prefix at ${WINEPREFIX}"
    xvfb-run -a wineboot --init 2>&1 | sed 's/^/  /' || true
    wineserver -w || true
  fi
fi

# --- go ---
# CLI-arg fallbacks: env values default to empty strings in module.yaml so
# the entrypoint's INI-stamper leaves operator INI edits alone. But Conan's
# launch CLI requires real values for ServerName, MaxPlayers, ports, and
# RconPassword. Resolve those here so empty config_values still produce a
# launchable server. These fallbacks should match the previous module.yaml
# defaults so behaviour is unchanged for fresh installs.
SERVER_NAME_LAUNCH="${SERVER_NAME:-panel Conan Exiles}"
MAX_PLAYERS_LAUNCH="${MAX_PLAYERS:-40}"
RCON_PASSWORD_LAUNCH="${RCON_PASSWORD:-panel_rcon}"

BIN_DIR="$(dirname "${DEDI_EXE}")"
cd "${BIN_DIR}"
log "starting Conan Exiles dedicated server"
log "  game dir:  ${GAME_DIR}"
log "  save dir:  ${SAVE_DIR}"
log "  binary:    ${DEDI_EXE}"
log "  bin dir:   ${BIN_DIR}"
log "  map:       ${SERVER_MAP}"
log "  max:       ${MAX_PLAYERS_LAUNCH}"
log "  game port: ${GAME_PORT}/tcp+udp"
log "  query:     ${QUERY_PORT}/udp"
log "  rcon:      ${RCON_PORT}/tcp"

if [[ "${LAUNCH_MODE}" = wine ]]; then
  trap 'log "SIGTERM received; stopping server"; wineserver -k || true; exit 0' TERM INT
else
  trap 'log "SIGTERM received; stopping server"; kill -TERM "${SERVER_PID:-0}" 2>/dev/null || true; wait "${SERVER_PID:-0}" 2>/dev/null || true; exit 0' TERM INT
fi

# Launch flags. Conan reads most settings from ServerSettings.ini (which we
# just stamped) — CLI args set the boot-time essentials. Pinger port is
# `Port + 1` per AMP convention; setting only -Port is enough — Conan
# derives pinger from it automatically.
#
# Native (enhanced) — match Funcom's launcher convention: `ConanSandbox`
# first positional, then the map URL. No -log/-nosound/-SteamServerName
# (those are Wine-era flags). ServerName lives in [ServerSettings]
# ServerName= which our INI stamper already handles.
if [[ "${LAUNCH_MODE}" = native ]]; then
  chmod +x "${DEDI_EXE}" || true
  exec stdbuf -oL -eL "${DEDI_EXE}" \
      ConanSandbox \
      "${SERVER_MAP}" \
      -stdout \
      -FullStdOutLogOutput \
      "-ServerName=${SERVER_NAME_LAUNCH}" \
      "-MaxPlayers=${MAX_PLAYERS_LAUNCH}" \
      "-Port=${GAME_PORT}" \
      "-QueryPort=${QUERY_PORT}" \
      -RconEnabled=1 \
      "-RconPort=${RCON_PORT}" \
      "-RconPassword=${RCON_PASSWORD_LAUNCH}"
fi

# Wine (legacy) — xvfb-run -a auto-allocates a free X display (host has its
# own Xvfb on :99 / :100 / :101 — see panel/memory/gotchas.md "network_mode:
# host Xvfb collision"). `ConanSandbox` first positional matches Funcom's
# launcher convention; was missing on the old Wine launch line.
exec stdbuf -oL -eL xvfb-run -a --server-args="-screen 0 1024x768x24" \
    wine "${DEDI_EXE}" \
    ConanSandbox \
    "${SERVER_MAP}" \
    -log \
    "-SteamServerName=${SERVER_NAME_LAUNCH}" \
    "-ServerName=${SERVER_NAME_LAUNCH}" \
    "-MaxPlayers=${MAX_PLAYERS_LAUNCH}" \
    "-Port=${GAME_PORT}" \
    "-QueryPort=${QUERY_PORT}" \
    -RconEnabled=1 \
    "-RconPort=${RCON_PORT}" \
    "-RconPassword=${RCON_PASSWORD_LAUNCH}" \
    -nosound \
    -stdout \
    -FullStdOutLogOutput
