#!/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:-}" log " slots: ${MAX_PLAYERS:-}" 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