panel public release

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 01:26:41 -07:00
commit 295eb22826
2165 changed files with 301492 additions and 0 deletions
+399
View File
@@ -0,0 +1,399 @@
#!/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