panel v0.9.1 — open-source game server manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user