#!/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 /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