Files
panel/modules/vein/entrypoint.sh
T
dbledeez a00bd620a1 Panel — public source drop (v0.9.0)
Self-hostable game-server control panel: controller + agent + 26 game
modules. One-line install (prebuilt release, no Go required):

  curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:17:39 -07:00

193 lines
8.0 KiB
Bash

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