panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 5232609719
2160 changed files with 300415 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
#!/bin/bash
# panel-native Empyrion entrypoint.
#
# Contract:
# /game — SteamCMD-installed Windows build of Empyrion (volume,
# populated by the panel's SteamCMD updater sidecar).
# If missing, exit with EX_CONFIG and a helpful message.
# /game-saves — dedicated.yaml + Saves/ tree (volume).
#
# Runs as root for its opening stage (fix up ownership after the SteamCMD
# sidecar wrote as root, nuke any stale X lock from a previous crash),
# then drops to the `panel` user for Wine + Xvfb + the game. Wine refuses
# to run as root, so the drop is load-bearing.
#
# On every start we verify the binary is there, start Xvfb, boot a minimal
# Wine prefix if needed, then exec EmpyrionDedicated.exe under Wine with
# stdout/stderr line-buffered so docker logs → agent → panel console
# delivers live output.
set -euo pipefail
log() { printf '[panel-empyrion] %s\n' "$*"; }
# ----- stage 1: root-only bootstrap (runs once per container start) -----
if [[ "$(id -u)" = "0" ]]; then
# SteamCMD writes as root in its sidecar; chown once so the panel user
# (UID 1000) can read/write the game dir without fiddling with perms.
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
# Nuke any stale Xvfb state from a previous boot. The container's /tmp
# persists across restarts (same writable layer), so /tmp/.X99-lock plus
# /tmp/.X11-unix/X99 left by a SIGKILL'd Xvfb make the next start die
# with "_XSERVTransMakeAllCOTSServerListeners: server already running".
# Single rm wasn't catching every variant (verified live 2026-04-29);
# nuking the whole dir + re-creating with the right mode is bulletproof.
pkill -9 Xvfb 2>/dev/null || true
rm -rf /tmp/.X11-unix /tmp/.X99-lock 2>/dev/null || true
mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix && chown root:root /tmp/.X11-unix 2>/dev/null || true
# Empyrion writes build-stamped logs to Z:\Logs\<buildnum> (= /Logs/...
# because Wine maps Z:\ to /). Without this symlink panel gets an
# UnauthorizedAccessException on first boot. Pointing it into the save
# volume keeps logs persistent + visible in the Files tab.
mkdir -p /game-saves/EmpyrionLogs
chown -R panel:panel /game-saves/EmpyrionLogs
if [[ ! -e /Logs || -L /Logs ]]; then
ln -sfn /game-saves/EmpyrionLogs /Logs
fi
# Re-exec as `panel` user. setpriv comes with util-linux in debian-slim.
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 \
SRV_PORT="${SRV_PORT:-30000}" \
EAC_PORT="${EAC_PORT:-30003}" \
CSW_PORT="${CSW_PORT:-30004}" \
EAH_API_PORT="${EAH_API_PORT:-30007}" \
SCENARIO="${SCENARIO:-}" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/entrypoint.sh "$@"
fi
GAME_DIR=/game
SAVE_DIR=/game-saves
DEDI_DIR="${GAME_DIR}/DedicatedServer"
DEDI_EXE="${DEDI_DIR}/EmpyrionDedicated.exe"
CONFIG="${SAVE_DIR}/dedicated.yaml"
# Empyrion ships its default dedicated.yaml at the game root.
SHIPPED_CONFIG="${GAME_DIR}/dedicated.yaml"
# --- sanity: Windows build installed? ---
if [[ ! -f "${DEDI_EXE}" ]]; then
log "ERROR: ${DEDI_EXE} not found."
log " Game files haven't been downloaded yet."
log " Run 'Update' on this server in the panel — that launches a"
log " SteamCMD sidecar which installs Empyrion (Windows build) into"
log " this volume via +@sSteamCmdForcePlatformType windows."
exit 78 # EX_CONFIG
fi
# --- dedicated.yaml: copy shipped default on first run ---
if [[ ! -s "${CONFIG}" && -s "${SHIPPED_CONFIG}" ]]; then
log "no panel config found — seeding ${CONFIG} from ${SHIPPED_CONFIG}"
cp -f "${SHIPPED_CONFIG}" "${CONFIG}"
fi
if [[ ! -s "${CONFIG}" ]]; then
log "WARN: no dedicated.yaml found in game install. Empyrion will create"
log " one with defaults on first boot. Edit via the Files tab."
fi
# Symlink so the binary finds the config at its expected relative path.
# Empyrion reads dedicated.yaml from the game root (same dir as the .exe's
# -dedicated argument resolves).
ln -sf "${CONFIG}" "${GAME_DIR}/dedicated.yaml"
# --- patch dedicated.yaml ports from env on every boot ---
# Empyrion reads Srv_Port / EAC_Port / Tel_Port from dedicated.yaml. Without
# this, multiple instances on the same agent all bind 30000-30004 and only
# one is reachable. SRV_PORT comes from the panel allocator via the
# manifest's port→env mapping (`env: SRV_PORT` in module.yaml). yaml is
# whitespace-sensitive but these keys are a flat top-level "Key: NNN" so
# sed is safe.
if [[ -s "${CONFIG}" ]]; then
log "patching dedicated.yaml (Srv_Port=${SRV_PORT}, EAC_Port=${EAC_PORT})"
sed -i -E \
-e "s|^([[:space:]]*Srv_Port:[[:space:]]*)[0-9]+|\1${SRV_PORT}|" \
-e "s|^([[:space:]]*EAC_Port:[[:space:]]*)[0-9]+|\1${EAC_PORT}|" \
"${CONFIG}"
fi
# --- scenario selection ---
# SCENARIO env names a folder under /game/Content/Scenarios/. The empyrion-
# shipped default is "Default Random". Reforged Eden / Reforged Eden 2 /
# Project Eden need to be present in /game/Content/Scenarios/<name>/ first
# (drop via the panel's Files tab, or future workshop-subscribe feature).
# This block keeps dedicated.yaml's CustomScenario in sync with the env.
SCENARIO="${SCENARIO:-}"
if [[ -n "${SCENARIO}" && -s "${CONFIG}" ]]; then
if [[ ! -d "${GAME_DIR}/Content/Scenarios/${SCENARIO}" ]]; then
log "WARN: scenario folder '${SCENARIO}' not found at ${GAME_DIR}/Content/Scenarios/${SCENARIO}"
log " The panel UI Files tab can drop scenario contents there. For"
log " Reforged Eden 2 (Workshop ID 3041847672), unzip into:"
log " /game/Content/Scenarios/Reforged Eden 2/"
log " Booting with Default for now."
else
log "selecting scenario: ${SCENARIO}"
if grep -qE '^[[:space:]]*CustomScenario:' "${CONFIG}"; then
sed -i -E "s|^([[:space:]]*CustomScenario:[[:space:]]*).*|\1${SCENARIO}|" "${CONFIG}"
else
# Insert under GameConfig: at the top, ahead of dedicated game block.
printf '\nGameConfig:\n CustomScenario: %s\n' "${SCENARIO}" >> "${CONFIG}"
fi
fi
fi
# --- stage EPM mod (TCP relay for Eleon Mod API) on first boot ---
# Pre-staged at /opt/panel-epm in the image. Copy into the live game
# Content/Mods/EPM/ if it isn't there yet. This makes the panel-empyrion-
# bridge sidecar work out-of-the-box: every new empyrion instance comes
# with EPM listening on EAH_API_PORT.
EPM_SRC=/opt/panel-epm
EPM_DST="${GAME_DIR}/Content/Mods/EPM"
if [[ -d "${EPM_SRC}" && ! -f "${EPM_DST}/EmpyrionNetworkRelayMod.dll" ]]; then
log "staging EPM mod into ${EPM_DST}"
mkdir -p "${EPM_DST}"
cp -a "${EPM_SRC}/." "${EPM_DST}/"
# If a panel-allocated EAH_API_PORT is set, patch EPM's Settings.yaml
# so its TCP listener binds the right port for this instance.
if [[ -n "${EAH_API_PORT:-}" && -f "${EPM_DST}/Settings.yaml" ]]; then
sed -i -E "s|^GameServerApiPort:[[:space:]]*[0-9]+|GameServerApiPort: ${EAH_API_PORT}|" \
"${EPM_DST}/Settings.yaml"
fi
fi
# Re-patch port even if EPM was already staged, in case panel reallocated.
if [[ -f "${EPM_DST}/Settings.yaml" && -n "${EAH_API_PORT:-}" ]]; then
sed -i -E "s|^GameServerApiPort:[[:space:]]*[0-9]+|GameServerApiPort: ${EAH_API_PORT}|" \
"${EPM_DST}/Settings.yaml"
fi
# --- prep saves dir (Empyrion writes under /game/Saves by default) ---
# Redirect Saves into the save volume via a symlink so steam validate
# can't clobber world data.
mkdir -p "${SAVE_DIR}/Saves"
if [[ -d "${GAME_DIR}/Saves" && ! -L "${GAME_DIR}/Saves" ]]; then
# Steam shipped an empty Saves dir; replace with symlink.
rm -rf "${GAME_DIR}/Saves"
fi
ln -sfn "${SAVE_DIR}/Saves" "${GAME_DIR}/Saves"
# --- Wine + Xvfb via xvfb-run (auto-allocates a free display) ---
#
# We previously hand-started Xvfb on :99. That broke once the host had
# its OWN Xvfb on :99 — the empyrion container uses network_mode: host,
# which shares the network namespace, and X server abstract Unix sockets
# (`@/tmp/.X11-unix/X99`) are namespaced by network ns. Result: container
# Xvfb fails with "_XSERVTransMakeAllCOTSServerListeners: server already
# running" and Wine then dies "Authorization required, but no auth
# protocol specified" → Unity exits "Failed to create batch mode window:
# Success." (verified 2026-04-29).
#
# `xvfb-run -a` finds a free display number, starts Xvfb with proper
# xauth set up, exports DISPLAY to its child, and cleans up on exit.
# Bulletproof against host's existing Xvfb instances.
# --- Wine prefix bootstrap (first boot only). xvfb-run handles the X
# server for the actual run; for prefix init we don't strictly need
# X but wineboot complains less when one is available. ---
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
# --- go ---
cd "${GAME_DIR}"
log "starting Empyrion Dedicated Server"
log " game dir: ${GAME_DIR}"
log " save dir: ${SAVE_DIR}"
log " config: ${CONFIG}"
log " binary: ${DEDI_EXE}"
# Shutdown handler: forward SIGTERM (xvfb-run forwards to wine; wine's
# Unity hook persists state on quit).
trap 'log "SIGTERM received; stopping server"; wineserver -k 2>/dev/null || true; exit 0' TERM INT
# -batchmode / -nographics make Unity skip the renderer; -logFile - pipes
# the Unity log to our stdout so the panel console sees everything.
# `xvfb-run -a` picks a free display, sets up xauth, and exports DISPLAY.
# `--server-args="-screen 0 1024x768x24"` matches our previous Xvfb args.
exec stdbuf -oL -eL xvfb-run -a --server-args="-screen 0 1024x768x24" \
wine "${DEDI_EXE}" \
-batchmode \
-nographics \
-logFile - \
-dedicated dedicated.yaml \
-startDedicated