panel v0.9.1 — open-source game server manager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 23:01:33 -07:00
commit 0a941f3ba6
2161 changed files with 300771 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
#!/bin/bash
# panel-native Valheim entrypoint.
set -euo pipefail
log() { printf '[panel-valheim] %s\n' "$*"; }
# Root-stage: chown (SteamCMD sidecar wrote as root) then drop to panel.
if [[ "$(id -u)" = "0" ]]; then
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
exec setpriv --reuid=panel --regid=panel --clear-groups \
--inh-caps=-all --bounding-set=-all \
env HOME=/game-saves PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
SERVER_NAME="${SERVER_NAME:-panel Valheim}" \
SERVER_WORLD="${SERVER_WORLD:-Dedicated}" \
SERVER_PASSWORD="${SERVER_PASSWORD:-secret12}" \
SERVER_PUBLIC="${SERVER_PUBLIC:-1}" \
/entrypoint.sh "$@"
fi
GAME_DIR=/game
SAVE_DIR=/game-saves
EXE="${GAME_DIR}/valheim_server.x86_64"
CFG="${SAVE_DIR}/server.cfg"
if [[ ! -x "${EXE}" ]]; then
log "ERROR: ${EXE} not found. Run 'Update' in the panel to install Valheim via SteamCMD."
exit 78
fi
# --- settings source of truth: PANEL ENV (config_values) ---
# The panel is the single editing surface (gamehost Settings page writes
# config_values → these env vars on recreate). The env values ALWAYS win.
#
# We REGENERATE /game-saves/server.cfg from the live env on every boot, purely
# as a human-readable reflection of the active settings — it is NOT sourced
# back in. The previous version seeded it once and then SOURCED it on every
# boot, so a stale server.cfg silently overrode every panel edit (a customer's
# renamed server / new password never took effect, and the Settings page showed
# values that disagreed with the running server). Fixed 2026-06-02.
cat > "${CFG}" <<EOF
# AUTO-GENERATED from the panel on every boot — DO NOT EDIT (changes are
# overwritten). Edit settings from the gamehost Settings page instead.
SERVER_NAME="${SERVER_NAME}"
SERVER_WORLD="${SERVER_WORLD}"
SERVER_PASSWORD="${SERVER_PASSWORD}"
SERVER_PUBLIC=${SERVER_PUBLIC}
SERVER_SAVEINTERVAL=${SERVER_SAVEINTERVAL:-1800}
SERVER_PRESET="${SERVER_PRESET:-}"
SERVER_MODIFIERS="${SERVER_MODIFIERS:-}"
EOF
# Valheim writes worlds + adminlist.txt under $HOME/.config/unity3d/IronGate/Valheim.
# HOME is already /game-saves via setpriv; pre-create so perms are right.
mkdir -p "${SAVE_DIR}/.config/unity3d/IronGate/Valheim"
export LD_LIBRARY_PATH="${GAME_DIR}/linux64:${LD_LIBRARY_PATH:-}"
export SteamAppId=892970 # as per Valheim docs (892970, not the dedicated app id)
# Port: bind the panel-allocated host port (host networking → the game listens
# directly on the LAN at this port). The panel injects PORT_GAME via the
# `env:` mapping on the `game` ports entry in module.yaml; fall back to 2456
# only if it's somehow unset (e.g. a hand-run container). Valheim opens
# ${GAME_PORT} for gameplay and ${GAME_PORT}+1 for the Steam server query, so
# the public chain must forward BOTH (panel allocates them contiguously).
GAME_PORT="${PORT_GAME:-2456}"
# --- optional flags: only appended when the corresponding env var is set, so
# an empty value never injects a broken flag. These are surfaced as curated
# fields in the gamehost Settings page.
EXTRA_ARGS=()
# Crossplay is intentionally DISABLED and not honoured. Valheim's -crossplay
# switches the server to PlayFab relay networking and opens NO local inbound
# listening port ("no inbound listening ports in Crossplay" — Iron Gate), so
# players can no longer join by IP:port. That breaks gamehost's entire connect
# model (we hand customers an IP:port). A customer enabling it bricked their
# direct-IP join on 2026-06-02. We never pass -crossplay regardless of any
# SERVER_CROSSPLAY value that might linger in an old config.
if [[ "${SERVER_CROSSPLAY:-0}" != "0" ]]; then
log "NOTE: SERVER_CROSSPLAY is set but crossplay is disabled on this host (it would break direct IP:port joining). Ignoring."
fi
# Save interval in seconds (Valheim default 1800). Only pass if numeric > 0.
if [[ "${SERVER_SAVEINTERVAL:-}" =~ ^[0-9]+$ ]] && [[ "${SERVER_SAVEINTERVAL}" -gt 0 ]]; then
EXTRA_ARGS+=(-saveinterval "${SERVER_SAVEINTERVAL}")
fi
# World preset: one of the vanilla presets (normal/casual/easy/hard/hardcore/
# immersive/hammer). Empty = leave at the world's existing settings.
if [[ -n "${SERVER_PRESET:-}" ]]; then
EXTRA_ARGS+=(-preset "${SERVER_PRESET}")
fi
# Per-key world modifiers: SERVER_MODIFIERS holds space-separated "key value"
# pairs, e.g. "combat hard deathpenalty casual resources more". Each pair
# becomes a "-modifier key value". Operator-facing fields write a clean string.
if [[ -n "${SERVER_MODIFIERS:-}" ]]; then
# shellcheck disable=SC2206
_mods=(${SERVER_MODIFIERS})
i=0
while [[ $i -lt ${#_mods[@]} ]]; do
key="${_mods[$i]}"; val="${_mods[$((i+1))]:-}"
if [[ -n "${key}" && -n "${val}" ]]; then
EXTRA_ARGS+=(-modifier "${key}" "${val}")
fi
i=$((i+2))
done
fi
# --- Mods: BepInEx (Doorstop) loader ---------------------------------------
# Valheim mods load through BepInEx, injected at launch by Unity Doorstop. The
# panel's mod manager installs BepInEx + plugins into /game/BepInEx (the
# Thunderstore/ValheimPlus zips extract their BepInEx/ tree + doorstop_libs +
# libdoorstop into /game). We enable injection ONLY when:
# - mods are turned on (VALHEIM_MODS_ENABLED=1, set by the panel), AND
# - the loader is actually present on disk (BepInEx/core/BepInEx.Preloader.dll)
# Otherwise we launch vanilla (no perf cost, no risk of a half-install bricking
# the server). cwd is already /game below so the relative paths resolve.
BEPINEX_PRELOADER="${GAME_DIR}/BepInEx/core/BepInEx.Preloader.dll"
MODS_ACTIVE=0
if [[ "${VALHEIM_MODS_ENABLED:-0}" != "0" && -f "${BEPINEX_PRELOADER}" ]]; then
MODS_ACTIVE=1
fi
# ValheimPlus config (valheim_plus.cfg) is NOT generated here. gamehost writes
# the full cfg directly into /game/BepInEx/config/valheim_plus.cfg via the file
# API whenever the customer saves V+ settings (it holds all 345 settings in
# config_values as VP_* and renders the cfg from them). Keeping cfg generation
# out of the container env avoids declaring 345 env vars per instance.
cd "${GAME_DIR}"
log "starting Valheim dedicated server"
log " name: ${SERVER_NAME}"
log " world: ${SERVER_WORLD}"
log " public: ${SERVER_PUBLIC}"
log " port: ${GAME_PORT} (+ ${GAME_PORT}+1 for query)"
log " extra: ${EXTRA_ARGS[*]:-(none)}"
if [[ "${MODS_ACTIVE}" = "1" ]]; then
log " mods: BepInEx ENABLED${VALHEIM_PLUS_ENABLED:+ (ValheimPlus)}"
# Doorstop env (from ValheimPlus/BepInEx start_server_bepinex.sh). Paths are
# relative to cwd (=/game).
export DOORSTOP_ENABLED=1
export DOORSTOP_ENABLE=TRUE
export DOORSTOP_TARGET_ASSEMBLY="./BepInEx/core/BepInEx.Preloader.dll"
export DOORSTOP_INVOKE_DLL_PATH="./BepInEx/core/BepInEx.Preloader.dll"
export LD_LIBRARY_PATH="./doorstop_libs:${GAME_DIR}/linux64:${LD_LIBRARY_PATH:-}"
export LD_PRELOAD="libdoorstop_x64.so:${LD_PRELOAD:-}"
else
log " mods: none (vanilla)"
fi
# --- Passwordless via ValheimPlus -----------------------------------------
# disableServerPassword=true in valheim_plus.cfg is NOT enough on its own:
# Valheim still enforces the password if the launch command passes -password,
# and it refuses a passwordless server unless it's also -public 0 (unlisted).
# So when V+ is active AND its cfg has disableServerPassword=true, we OMIT
# -password and force -public 0. (Confirmed: Iron Gate / ServerFlex / multiple
# V+ docker projects all hit exactly this.)
PW_ARGS=(-password "${SERVER_PASSWORD}")
PUBLIC_ARG="${SERVER_PUBLIC}"
VP_CFG="${GAME_DIR}/BepInEx/config/valheim_plus.cfg"
if [[ "${MODS_ACTIVE}" = "1" && "${VALHEIM_PLUS_ENABLED:-0}" != "0" && -f "${VP_CFG}" ]]; then
# Match `disableServerPassword=true` only inside the [Server] section.
if awk '
/^\[Server\]/ {inserver=1; next}
/^\[/ {inserver=0}
inserver && /^[[:space:]]*disableServerPassword[[:space:]]*=[[:space:]]*true[[:space:]]*$/ {found=1}
END {exit found?0:1}
' "${VP_CFG}"; then
log " password: DISABLED via ValheimPlus (omitting -password, forcing -public 0)"
PW_ARGS=()
PUBLIC_ARG="0"
fi
fi
exec stdbuf -oL -eL "${EXE}" \
-name "${SERVER_NAME}" \
-port "${GAME_PORT}" \
-world "${SERVER_WORLD}" \
"${PW_ARGS[@]}" \
-public "${PUBLIC_ARG}" \
-savedir "${SAVE_DIR}" \
"${EXTRA_ARGS[@]}" \
-nographics -batchmode