panel — open-source game server manager (public release)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
#!/bin/bash
|
||||
# panel-native VEIN entrypoint.
|
||||
#
|
||||
# Contract:
|
||||
# /game — SteamCMD-installed VEIN Dedicated Server (app 2131400),
|
||||
# populated by the panel's SteamCMD updater. If the server
|
||||
# binary is missing, exit EX_CONFIG (78) with a helpful hint.
|
||||
# /game-saves — Vein/Saved/ tree (savegame + Config/LinuxServer/Game.ini).
|
||||
#
|
||||
# Stage 1 (root): chown volumes (SteamCMD wrote as root), wire the Steam
|
||||
# client SDK (~/.steam/sdk64/steamclient.so) so the Steamworks game-server
|
||||
# API can register on the master server, then drop to the `panel` user.
|
||||
# Stage 2 (panel): stamp config_values env into the CORRECT UE sections of
|
||||
# Game.ini, redirect saves into the volume, then exec the server with the
|
||||
# panel-allocated ports.
|
||||
|
||||
set -euo pipefail
|
||||
log() { printf '[panel-vein] %s\n' "$*"; }
|
||||
|
||||
GAME_DIR=/game
|
||||
SAVE_DIR=/game-saves
|
||||
|
||||
# ----- stage 1: root-only bootstrap -----
|
||||
if [[ "$(id -u)" = "0" ]]; then
|
||||
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
|
||||
|
||||
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 \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
STEAM_APP_ID="${STEAM_APP_ID:-1857950}" \
|
||||
SteamAppId="${STEAM_APP_ID:-1857950}" \
|
||||
GAME_PORT="${GAME_PORT:-7777}" \
|
||||
QUERY_PORT="${QUERY_PORT:-27015}" \
|
||||
RCON_PORT="${RCON_PORT:-7878}" \
|
||||
SERVER_NAME="${SERVER_NAME:-}" \
|
||||
SERVER_DESCRIPTION="${SERVER_DESCRIPTION:-}" \
|
||||
SERVER_PASSWORD="${SERVER_PASSWORD:-}" \
|
||||
MAX_PLAYERS="${MAX_PLAYERS:-}" \
|
||||
SERVER_PUBLIC="${SERVER_PUBLIC:-}" \
|
||||
HEARTBEAT_INTERVAL="${HEARTBEAT_INTERVAL:-}" \
|
||||
ENABLE_VAC="${ENABLE_VAC:-}" \
|
||||
SHOW_SCOREBOARD_BADGES="${SHOW_SCOREBOARD_BADGES:-}" \
|
||||
/entrypoint.sh "$@"
|
||||
fi
|
||||
|
||||
# ----- stage 2: panel user -----
|
||||
EXE="${GAME_DIR}/Vein/Binaries/Linux/VeinServer-Linux-Test"
|
||||
# Fallbacks in case the depot ships a differently-suffixed binary
|
||||
# (Shipping / DebugGame). The smoke test confirms the real name.
|
||||
if [[ ! -x "${EXE}" ]]; then
|
||||
for alt in VeinServer-Linux-Shipping VeinServer-Linux-DebugGame VeinServer-Linux; do
|
||||
if [[ -x "${GAME_DIR}/Vein/Binaries/Linux/${alt}" ]]; then
|
||||
EXE="${GAME_DIR}/Vein/Binaries/Linux/${alt}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ! -x "${EXE}" ]]; then
|
||||
log "ERROR: VEIN server binary not found under ${GAME_DIR}/Vein/Binaries/Linux/."
|
||||
log " Run 'Update' in the panel to install VEIN via SteamCMD (app 2131400)."
|
||||
exit 78
|
||||
fi
|
||||
|
||||
# --- Steam client SDK: the Steamworks game-server API dlopen's steamclient.so
|
||||
# from ~/.steam/sdk64/ (64-bit) and ~/.steam/sdk32/ (32-bit). VEIN's depot does
|
||||
# NOT ship steamclient.so, and the panel's SteamCMD sidecar (which has it) is
|
||||
# torn down after install — so we bake it into the image at /opt/steamsdk and
|
||||
# symlink it here. Without this: "Steam Dedicated Server API failed to
|
||||
# initialize" + endless "Failed to heartbeat" + the ready-regex never fires.
|
||||
mkdir -p "${HOME}/.steam/sdk64" "${HOME}/.steam/sdk32"
|
||||
for pair in "linux64:sdk64" "linux32:sdk32"; do
|
||||
src="/opt/steamsdk/${pair%%:*}/steamclient.so"
|
||||
dst="${HOME}/.steam/${pair##*:}/steamclient.so"
|
||||
if [[ -f "${src}" ]]; then
|
||||
ln -sf "${src}" "${dst}"
|
||||
fi
|
||||
done
|
||||
# Fallback: if the baked SDK is somehow absent, scan known runtime locations.
|
||||
if [[ ! -e "${HOME}/.steam/sdk64/steamclient.so" ]]; then
|
||||
alt="$(find /opt /game "${HOME}/.steam" "${HOME}/.local/share/Steam" -name steamclient.so 2>/dev/null | grep -i 'linux64\|sdk64' | head -1)"
|
||||
[[ -z "${alt}" ]] && alt="$(find /opt /game "${HOME}/.steam" -name steamclient.so 2>/dev/null | head -1)"
|
||||
[[ -n "${alt}" ]] && ln -sf "${alt}" "${HOME}/.steam/sdk64/steamclient.so"
|
||||
fi
|
||||
if [[ -e "${HOME}/.steam/sdk64/steamclient.so" ]]; then
|
||||
log "steamclient.so wired → $(readlink -f "${HOME}/.steam/sdk64/steamclient.so")"
|
||||
else
|
||||
log "WARNING: steamclient.so not found — Steam server registration will fail (server still LAN-joinable)."
|
||||
fi
|
||||
export LD_LIBRARY_PATH="${HOME}/.steam/sdk64:${GAME_DIR}/linux64:${GAME_DIR}/Vein/Binaries/Linux:${LD_LIBRARY_PATH:-}"
|
||||
|
||||
# --- Game.ini: section-aware env → INI stamping -------------------------
|
||||
# VEIN's Game.ini is a multi-section UE config. Each tunable lives under a
|
||||
# SPECIFIC [/Script/...] header — MaxPlayers under [/Script/Engine.GameSession],
|
||||
# ServerName/Password under [/Script/Vein.VeinGameSession], etc. We stamp
|
||||
# only NON-EMPTY env values, in place, into the right section — preserving
|
||||
# operator hand-edits to other keys/sections via the Files tab.
|
||||
CFG_DIR="${SAVE_DIR}/Vein/Saved/Config/LinuxServer"
|
||||
INI="${CFG_DIR}/Game.ini"
|
||||
mkdir -p "${CFG_DIR}"
|
||||
[[ -f "${INI}" ]] || : > "${INI}"
|
||||
|
||||
# set_ini_kv "[Section]" KEY VALUE
|
||||
# Replace KEY=... if it already exists *within* its section; else insert it
|
||||
# immediately after the section header; create the section at EOF if absent.
|
||||
# Skips on empty VALUE. Section-aware: a same-named key in another section
|
||||
# is left untouched. Idempotent. Pure awk — no extra runtime deps.
|
||||
set_ini_kv() {
|
||||
local section="$1" key="$2" value="$3"
|
||||
[[ -z "${value}" ]] && return 0
|
||||
local tmp="${INI}.tmp.$$"
|
||||
awk -v sec="${section}" -v key="${key}" -v val="${value}" '
|
||||
function isheader(l) { return (l ~ /^[ \t]*\[.*\][ \t]*$/) }
|
||||
function trim(l) { gsub(/^[ \t]+|[ \t]+$/, "", l); return l }
|
||||
BEGIN { cur=""; replaced=0; hdr_line=0 }
|
||||
{
|
||||
lines[NR]=$0
|
||||
t=trim($0)
|
||||
if (isheader(t)) cur=t
|
||||
section_of[NR]=cur
|
||||
if (cur==sec && hdr_line==0 && t==sec) hdr_line=NR
|
||||
# replace in-place when key matches inside the target section
|
||||
if (cur==sec && (t ~ ("^" key "[ \t]*=") )) {
|
||||
lines[NR]=key "=" val
|
||||
replaced=1
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (replaced) {
|
||||
for (i=1;i<=NR;i++) print lines[i]
|
||||
} else if (hdr_line>0) {
|
||||
for (i=1;i<=NR;i++) {
|
||||
print lines[i]
|
||||
if (i==hdr_line) print key "=" val
|
||||
}
|
||||
} else {
|
||||
for (i=1;i<=NR;i++) print lines[i]
|
||||
if (NR>0 && trim(lines[NR])!="") print ""
|
||||
print sec
|
||||
print key "=" val
|
||||
}
|
||||
}
|
||||
' "${INI}" > "${tmp}" && mv "${tmp}" "${INI}"
|
||||
}
|
||||
|
||||
GS='[/Script/Engine.GameSession]'
|
||||
VS='[/Script/Vein.VeinGameSession]'
|
||||
OSS='[OnlineSubsystemSteam]'
|
||||
SS='[/Script/Vein.ServerSettings]'
|
||||
|
||||
set_ini_kv "$GS" "MaxPlayers" "${MAX_PLAYERS}"
|
||||
set_ini_kv "$VS" "ServerName" "${SERVER_NAME}"
|
||||
set_ini_kv "$VS" "ServerDescription" "${SERVER_DESCRIPTION}"
|
||||
set_ini_kv "$VS" "Password" "${SERVER_PASSWORD}"
|
||||
set_ini_kv "$VS" "bPublic" "${SERVER_PUBLIC}"
|
||||
set_ini_kv "$VS" "HeartbeatInterval" "${HEARTBEAT_INTERVAL}"
|
||||
set_ini_kv "$OSS" "bVACEnabled" "${ENABLE_VAC}"
|
||||
set_ini_kv "$SS" "GS_ShowScoreboardBadges" "${SHOW_SCOREBOARD_BADGES}"
|
||||
|
||||
# --- Saves: redirect Vein/Saved into the save volume via symlink so world
|
||||
# data + configs survive container recreate and show in the Files tab.
|
||||
mkdir -p "${SAVE_DIR}/Vein/Saved" "${SAVE_DIR}/logs"
|
||||
if [[ -d "${GAME_DIR}/Vein/Saved" && ! -L "${GAME_DIR}/Vein/Saved" ]]; then
|
||||
if [[ -z "$(ls -A "${SAVE_DIR}/Vein/Saved" 2>/dev/null)" ]]; then
|
||||
cp -a "${GAME_DIR}/Vein/Saved/." "${SAVE_DIR}/Vein/Saved/" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "${GAME_DIR}/Vein/Saved"
|
||||
fi
|
||||
mkdir -p "${GAME_DIR}/Vein"
|
||||
ln -sfn "${SAVE_DIR}/Vein/Saved" "${GAME_DIR}/Vein/Saved"
|
||||
|
||||
cd "${GAME_DIR}"
|
||||
log "starting VEIN dedicated server"
|
||||
log " name: ${SERVER_NAME:-<unset, Game.ini default>}"
|
||||
log " slots: ${MAX_PLAYERS:-<unset, Game.ini default>}"
|
||||
log " ports: game=${GAME_PORT} query=${QUERY_PORT}"
|
||||
log " binary: ${EXE}"
|
||||
|
||||
# Launch args mirror the AMP template:
|
||||
# Vein -Port=N -QueryPort=N -UseGameSaveDir -stdout -FullStdOutLogOutput
|
||||
# -stdout + -FullStdOutLogOutput are LOAD-BEARING: they make the server log
|
||||
# to stdout so the panel's Console live-tail (and the ready-regex) work, and
|
||||
# so the stdio admin adapter's command echoes are visible.
|
||||
exec stdbuf -oL -eL "${EXE}" Vein \
|
||||
-Port="${GAME_PORT}" \
|
||||
-QueryPort="${QUERY_PORT}" \
|
||||
-UseGameSaveDir \
|
||||
-stdout \
|
||||
-FullStdOutLogOutput
|
||||
Reference in New Issue
Block a user