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,97 @@
|
||||
# panel-native Windrose runtime image.
|
||||
#
|
||||
# Windrose's dedicated server is a Windows-only UE5 build, so we bring along
|
||||
# just enough Wine + Xvfb to host it headlessly. SteamCMD is NOT installed
|
||||
# here — the panel's generic updater sidecar uses the official steamcmd
|
||||
# image and writes into our /game volume.
|
||||
#
|
||||
# Expected runtime layout inside the container:
|
||||
# /game — SteamCMD-installed Windows build (panel volume)
|
||||
# /game/WindroseServer.exe — dedicated server binary (preferred path)
|
||||
# /game/R5/Binaries/Win64/WindroseServer.exe — fallback path (UE5 convention)
|
||||
# /game-saves — ServerDescription.json, R5/Saved save tree
|
||||
# /entrypoint.sh — boot Xvfb, launch exe under Wine
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
TZ=Etc/UTC \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
WINEDEBUG=-all \
|
||||
WINEPREFIX=/home/panel/.wine \
|
||||
DISPLAY=:99
|
||||
|
||||
# Wine: install winehq-stable from dl.winehq.org rather than Debian's
|
||||
# wine 8.0~repack. Reason: Windrose's UE5 game uses gRPC for its R5 coop
|
||||
# session. gRPC's Windows event engine asserts on partial-transfer IOCP
|
||||
# completions ("ASSERTION FAILED: result.bytes_transferred ==
|
||||
# buffer_->Length() at windows_endpoint.cc:355"). Wine 8.0's IOCP can
|
||||
# return partial completions for WSARecv that the assertion rejects, so
|
||||
# the server *crashes* the moment a player session reaches the gRPC
|
||||
# transport layer (~12 seconds after a join). Wine 9+ on winehq's stable
|
||||
# repo has the IOCP fix; matches what CubeCoders' AMP wine-stable image
|
||||
# ships. cabextract + winbind are commonly needed by Wine's installer
|
||||
# helpers even though we don't call winetricks directly.
|
||||
RUN dpkg --add-architecture i386 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
locales \
|
||||
tini \
|
||||
curl \
|
||||
gnupg \
|
||||
xvfb \
|
||||
xauth \
|
||||
winbind \
|
||||
cabextract \
|
||||
procps \
|
||||
&& mkdir -pm755 /etc/apt/keyrings \
|
||||
&& curl -fsSL https://dl.winehq.org/wine-builds/winehq.key \
|
||||
| gpg --dearmor -o /etc/apt/keyrings/winehq-archive.key \
|
||||
&& curl -fsSL https://dl.winehq.org/wine-builds/debian/dists/bookworm/winehq-bookworm.sources \
|
||||
-o /etc/apt/sources.list.d/winehq-bookworm.sources \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends winehq-stable \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
|
||||
&& locale-gen \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -g 1000 panel \
|
||||
&& useradd -u 1000 -g 1000 -m -s /bin/bash panel \
|
||||
&& mkdir -p /game /game-saves \
|
||||
&& chown -R panel:panel /game /game-saves /home/panel
|
||||
|
||||
# Pre-warm the Wine prefix at /home/panel/.wine during the image build.
|
||||
#
|
||||
# Why: Wine 11's first-boot wineboot pops `appwiz.cpl install_mono` to
|
||||
# install Wine Mono. In a headless xvfb container that step blocks
|
||||
# indefinitely, and any interrupt (SIGINT, container restart) leaves the
|
||||
# prefix half-built — kernel32.dll never finishes registering, so the
|
||||
# next launch dies with "could not load kernel32.dll, status c0000135"
|
||||
# in a tight crash-loop. UE5 dedicated servers don't need .NET, so we
|
||||
# skip Mono entirely via WINEDLLOVERRIDES=mscoree= and the Gecko HTML
|
||||
# engine via mshtml=. Result: first boot is fast and idempotent;
|
||||
# entrypoint can skip wineboot since system.reg is already present.
|
||||
USER panel
|
||||
ENV HOME=/home/panel WINEPREFIX=/home/panel/.wine WINEDEBUG=-all
|
||||
# wineboot --init under xvfb-run hangs on Wine 11 because xvfb-run kills
|
||||
# its X server the moment wineboot's main thread returns, but wineboot
|
||||
# spawns explorer.exe / services.exe in the background that keep trying
|
||||
# to reach :99 and emit "X connection broken" errors that kill the build.
|
||||
# Run Xvfb directly, give it a moment, point DISPLAY at it, init the
|
||||
# prefix, then `wineserver -w` to flush state and explicitly stop Xvfb.
|
||||
RUN Xvfb :99 -screen 0 1024x768x24 -nolisten tcp & XVFB_PID=$! \
|
||||
&& sleep 2 \
|
||||
&& DISPLAY=:99 WINEDLLOVERRIDES="mscoree=;mshtml=" wineboot --init \
|
||||
&& wineserver -w \
|
||||
&& kill "$XVFB_PID" 2>/dev/null || true
|
||||
USER root
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 7777/udp 27015/udp
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Windrose — panel-native module
|
||||
|
||||
Panel-native Windrose dedicated server running under Wine + Xvfb.
|
||||
|
||||
- **Image:** `panel-windrose:latest` — built from `./Dockerfile` (Debian 12 + Wine + Xvfb + tini).
|
||||
- **Game files:** downloaded by the panel's SteamCMD updater sidecar (app 4129620) into the `panel-<id>-game` named volume. Windrose ships only as a Windows build, so the updater uses `+@sSteamCmdForcePlatformType windows` (declared in `module.yaml` via `platform: windows`).
|
||||
- **Saves:** `panel-<id>-saves` volume mounted at `/game-saves`; the entrypoint moves `R5/Saved/` from the game install into the save volume on first boot and symlinks it back, so SteamCMD `validate` can't clobber world data.
|
||||
- **Config:** `ServerDescription.json` lives in the save volume (authoritative) and is symlinked back to the game install path the binary expects. Edit via the Files tab — JSON parsed in-browser; no dedicated Config-tab form yet.
|
||||
- **RCON:** none. Windrose has no remote admin protocol; the admin console is in-process only. The Console tab streams the UE log.
|
||||
|
||||
## Ports
|
||||
|
||||
- **7777/udp** — Direct-connect game port (docs refer to this as `DirectConnectionServerPort`).
|
||||
- **27015/udp** — Steam query (UE5 convention).
|
||||
|
||||
Most Windrose peer connectivity runs through Steam NAT punch-through, but the direct port must be reachable for LAN or public-IP clients.
|
||||
|
||||
## Updating
|
||||
|
||||
Click **Update** in the panel. The SteamCMD sidecar re-validates app 4129620 against the `/game` volume. First install is ~900 MB compressed.
|
||||
|
||||
## Known gotchas
|
||||
|
||||
- Server binary path isn't documented — the entrypoint probes `/game/WindroseServer.exe`, `/game/R5/Binaries/Win64/WindroseServer.exe`, and `/game/R5/Binaries/WinGDK/WindroseServer.exe`, then falls back to `find` if none match. Check logs on first boot.
|
||||
- No RCON means the Players tab will stay empty; start/stop state comes from log-line ready-regex + container state.
|
||||
@@ -0,0 +1,293 @@
|
||||
#!/bin/bash
|
||||
# panel-native Windrose entrypoint.
|
||||
#
|
||||
# Contract:
|
||||
# /game — SteamCMD-installed Windows build of Windrose (volume,
|
||||
# populated by the panel's SteamCMD updater sidecar).
|
||||
# If missing, exit with EX_CONFIG and a helpful message.
|
||||
# /game-saves — ServerDescription.json + R5/Saved/ tree (volume).
|
||||
#
|
||||
# Runs as root for its opening stage (fix ownership after the SteamCMD
|
||||
# sidecar wrote as root, nuke stale X lock, symlink /Logs + R5/Saved into
|
||||
# the save volume), then drops to the `panel` user for Wine + Xvfb + the
|
||||
# game. Wine refuses to run as root, so the drop is load-bearing.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[panel-windrose] %s\n' "$*"; }
|
||||
|
||||
# ----- stage 1: root-only bootstrap -----
|
||||
if [[ "$(id -u)" = "0" ]]; then
|
||||
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
|
||||
|
||||
# Remove Debian/Ubuntu's "127.0.1.1 <hostname>" mapping from the
|
||||
# container's /etc/hosts (docker manages a per-container copy at
|
||||
# /var/lib/docker/containers/<id>/hosts, so this does NOT touch the
|
||||
# host file).
|
||||
#
|
||||
# Why: Windrose's UE5 / Wine ICE stack does gethostname() +
|
||||
# gethostbyname(), gets 127.0.1.1, then enumerates it as a host
|
||||
# ICE candidate. When a player joins via cloud P2P the server
|
||||
# tries to send STUN from 127.0.1.1 to the peer's relay, the
|
||||
# Linux kernel rejects with EACCES (loopback can't route
|
||||
# externally), Windrose retries a few times then crashes the
|
||||
# *server* process. Removing the mapping makes gethostbyname
|
||||
# fall through, ICE skips the loopback candidate, and joins work.
|
||||
# Note: /etc/hosts is a docker bind mount of the per-container
|
||||
# /var/lib/docker/containers/<id>/hosts; `sed -i` fails with
|
||||
# "Device or resource busy" on rename, so we filter then truncate-
|
||||
# write in place via grep|tee.
|
||||
#
|
||||
# Two-stage fix:
|
||||
# 1) Strip any "127.0.1.1 <hostname>" line.
|
||||
# 2) Pin "127.0.0.1 <hostname>" so NSS-files wins before DNS —
|
||||
# otherwise systemd-resolved on the host (which the container's
|
||||
# resolv.conf points at via 127.0.0.53 under network_mode=host)
|
||||
# synthesizes <hostname>=>127.0.1.1 from thin air, undoing
|
||||
# step 1. With <hostname>=>127.0.0.1 in /etc/hosts, ICE's
|
||||
# loopback filter actually catches it and the candidate is
|
||||
# dropped instead of becoming an EACCES STUN.
|
||||
if [[ -w /etc/hosts ]]; then
|
||||
HOSTNAME_VAL="$(hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo localhost)"
|
||||
grep -vE '^127\.0\.1\.[0-9]+[[:space:]]' /etc/hosts > /tmp/hosts.new || true
|
||||
if [[ -s /tmp/hosts.new ]]; then
|
||||
cat /tmp/hosts.new > /etc/hosts
|
||||
fi
|
||||
if ! grep -qE "^127\\.0\\.0\\.1[[:space:]].*\\b${HOSTNAME_VAL}\\b" /etc/hosts; then
|
||||
printf '127.0.0.1 %s\n' "${HOSTNAME_VAL}" >> /etc/hosts
|
||||
fi
|
||||
rm -f /tmp/hosts.new
|
||||
fi
|
||||
|
||||
# UE5 under Wine writes to Z:\Logs (= /Logs) and Z:\<Project>\Saved etc.
|
||||
# Pre-create /Logs as a symlink into the save volume so logs survive
|
||||
# container recreate and show up in the Files tab.
|
||||
mkdir -p /game-saves/WindroseLogs
|
||||
chown -R panel:panel /game-saves/WindroseLogs
|
||||
if [[ ! -e /Logs || -L /Logs ]]; then
|
||||
ln -sfn /game-saves/WindroseLogs /Logs
|
||||
fi
|
||||
|
||||
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 \
|
||||
SERVER_NAME="${SERVER_NAME:-panel Windrose}" \
|
||||
DIRECT_PORT="${DIRECT_PORT:-7777}" \
|
||||
USE_DIRECT_CONNECTION="${USE_DIRECT_CONNECTION:-true}" \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
/entrypoint.sh "$@"
|
||||
fi
|
||||
|
||||
GAME_DIR=/game
|
||||
SAVE_DIR=/game-saves
|
||||
|
||||
# --- locate the server binary ---
|
||||
# Windrose's StartServerForeground.bat launches the shipping binary directly:
|
||||
# start /abovenormal R5\Binaries\Win64\WindroseServer-Win64-Shipping.exe -log
|
||||
# The WindroseServer.exe at the game root is a ~260KB launcher stub, not the
|
||||
# real server. Prefer the shipping binary; fall back to any match if Windrose
|
||||
# changes their layout.
|
||||
CANDIDATES=(
|
||||
"${GAME_DIR}/R5/Binaries/Win64/WindroseServer-Win64-Shipping.exe"
|
||||
"${GAME_DIR}/R5/Binaries/WinGDK/WindroseServer-WinGDK-Shipping.exe"
|
||||
"${GAME_DIR}/R5/Binaries/Win64/WindroseServer.exe"
|
||||
)
|
||||
DEDI_EXE=""
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
if [[ -f "$c" ]]; then
|
||||
DEDI_EXE="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "${DEDI_EXE}" ]]; then
|
||||
# Last-ditch: find any *-Shipping.exe (or WindroseServer.exe in a Binaries dir).
|
||||
found=$(find "${GAME_DIR}" -maxdepth 6 -iname "WindroseServer*.exe" -type f 2>/dev/null | grep -i "Shipping\|Binaries" | head -1 || true)
|
||||
if [[ -n "${found}" ]]; then
|
||||
DEDI_EXE="${found}"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${DEDI_EXE}" ]]; then
|
||||
log "ERROR: WindroseServer-Win64-Shipping.exe not found under ${GAME_DIR}/R5/Binaries/."
|
||||
log " Run 'Update' on this server in the panel — that launches a"
|
||||
log " SteamCMD sidecar which installs Windrose (Windows build) into"
|
||||
log " this volume via +@sSteamCmdForcePlatformType windows."
|
||||
exit 78 # EX_CONFIG
|
||||
fi
|
||||
log "found server binary: ${DEDI_EXE}"
|
||||
|
||||
# --- persist Saves into /game-saves so SteamCMD validate can't clobber ---
|
||||
# UE5 writes to <GameRoot>/R5/Saved/. Move the directory on first boot then
|
||||
# symlink. Rebooting finds the symlink already in place; idempotent.
|
||||
mkdir -p "${SAVE_DIR}/R5"
|
||||
if [[ -d "${GAME_DIR}/R5/Saved" && ! -L "${GAME_DIR}/R5/Saved" ]]; then
|
||||
if [[ ! -d "${SAVE_DIR}/R5/Saved" ]]; then
|
||||
mv "${GAME_DIR}/R5/Saved" "${SAVE_DIR}/R5/Saved"
|
||||
else
|
||||
rm -rf "${GAME_DIR}/R5/Saved"
|
||||
fi
|
||||
fi
|
||||
mkdir -p "${SAVE_DIR}/R5/Saved"
|
||||
mkdir -p "${GAME_DIR}/R5"
|
||||
ln -sfn "${SAVE_DIR}/R5/Saved" "${GAME_DIR}/R5/Saved"
|
||||
|
||||
# --- ServerDescription.json: seed → migrate → symlink ---
|
||||
# Windrose 0.10.x generates ServerDescription.json at /game/R5/ on first
|
||||
# boot, which races our "migrate to save volume" step and leaves the file
|
||||
# unmigrated until the next restart (panel's Config tab reads from
|
||||
# /game-saves/R5/). Fix: seed a minimal stub in the save volume BEFORE
|
||||
# the game starts. The game opens the existing file, fills in empty
|
||||
# InviteCode / PersistentServerId / WorldIslandId on first save, and panel
|
||||
# reads a populated config immediately — no first-boot restart dance.
|
||||
# Template mirrors cytech-services AMP (windroseserverdescription.json).
|
||||
mkdir -p "${GAME_DIR}/R5" "${SAVE_DIR}/R5"
|
||||
DESC_GAME="${GAME_DIR}/R5/ServerDescription.json"
|
||||
DESC_SAVE="${SAVE_DIR}/R5/ServerDescription.json"
|
||||
|
||||
if [[ -f "${DESC_GAME}" && ! -L "${DESC_GAME}" ]]; then
|
||||
# Legacy path: a prior boot wrote the file at /game/R5/ without migration.
|
||||
if [[ ! -s "${DESC_SAVE}" ]]; then
|
||||
log "migrating existing ServerDescription.json into save volume"
|
||||
mv "${DESC_GAME}" "${DESC_SAVE}"
|
||||
else
|
||||
rm -f "${DESC_GAME}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -s "${DESC_SAVE}" ]]; then
|
||||
log "seeding blank ServerDescription.json (game fills in InviteCode, WorldIslandId, PersistentServerId on first save)"
|
||||
cat > "${DESC_SAVE}" <<'JSON'
|
||||
{
|
||||
"Version": 1,
|
||||
"ServerDescription_Persistent":
|
||||
{
|
||||
"PersistentServerId": "",
|
||||
"InviteCode": "",
|
||||
"IsPasswordProtected": false,
|
||||
"Password": "",
|
||||
"ServerName": "panel Windrose",
|
||||
"Note": "",
|
||||
"WorldIslandId": "",
|
||||
"MaxPlayerCount": 8,
|
||||
"P2pProxyAddress": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
fi
|
||||
|
||||
# Symlink /game/R5/ServerDescription.json → save-volume copy so the game
|
||||
# reads + writes the persistent file regardless of which path it opens.
|
||||
ln -sfn "${DESC_SAVE}" "${DESC_GAME}"
|
||||
|
||||
# --- patch DirectConnectionServerPort + UseDirectConnection from env ---
|
||||
# Windrose reads its listening port from this JSON (no CLI flag honored).
|
||||
# We patch on every boot so a panel-allocated DIRECT_PORT flows through.
|
||||
# UseDirectConnection: true = listen on a public IP, players use IP+port.
|
||||
# false = use windrose.support cloud P2P (NAT-traversal
|
||||
# via TURN). Required when the host is on a LAN with
|
||||
# no port-forward, or for invite-code-only flows.
|
||||
# Default: true (matches the panel's typical "expose host networking + open
|
||||
# the port" deployment). Set USE_DIRECT_CONNECTION=false on a per-instance
|
||||
# basis (panel config_values) to switch to cloud P2P.
|
||||
USE_DIRECT_CONN_LOWER="$(printf '%s' "${USE_DIRECT_CONNECTION:-true}" | tr '[:upper:]' '[:lower:]')"
|
||||
case "${USE_DIRECT_CONN_LOWER}" in
|
||||
false|0|no|off) UDC_VAL="false" ;;
|
||||
*) UDC_VAL="true" ;;
|
||||
esac
|
||||
if [[ -s "${DESC_SAVE}" ]]; then
|
||||
log "patching ServerDescription.json (DirectConnectionServerPort=${DIRECT_PORT}, UseDirectConnection=${UDC_VAL})"
|
||||
if grep -q '"DirectConnectionServerPort"' "${DESC_SAVE}"; then
|
||||
sed -i -E "s|(\"DirectConnectionServerPort\"[[:space:]]*:[[:space:]]*)[0-9]+|\1${DIRECT_PORT}|" "${DESC_SAVE}"
|
||||
else
|
||||
# Insert before the closing brace of ServerDescription_Persistent.
|
||||
sed -i -E "s|(\"P2pProxyAddress\"[[:space:]]*:[[:space:]]*\"[^\"]*\")|\1,\n\t\t\"UseDirectConnection\": ${UDC_VAL},\n\t\t\"DirectConnectionServerPort\": ${DIRECT_PORT}|" "${DESC_SAVE}"
|
||||
fi
|
||||
if grep -q '"UseDirectConnection"' "${DESC_SAVE}"; then
|
||||
sed -i -E "s|(\"UseDirectConnection\"[[:space:]]*:[[:space:]]*)(true\|false)|\1${UDC_VAL}|" "${DESC_SAVE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Belt-and-suspenders watcher — catches the odd case where a future
|
||||
# Windrose build writes to /game/R5/ through the symlink's parent dir
|
||||
# instead of following the link. Exits once things are consistent;
|
||||
# bounded to 30 min to avoid leaking on crash paths.
|
||||
(
|
||||
for _ in $(seq 1 360); do
|
||||
sleep 5
|
||||
if [[ -L "${DESC_GAME}" ]] && [[ -s "${DESC_SAVE}" ]]; then exit 0; fi
|
||||
if [[ -f "${DESC_GAME}" ]] && [[ ! -L "${DESC_GAME}" ]]; then
|
||||
log "watcher: re-migrating ServerDescription.json (game wrote through the symlink parent?)"
|
||||
if [[ ! -s "${DESC_SAVE}" ]]; then
|
||||
mv "${DESC_GAME}" "${DESC_SAVE}"
|
||||
else
|
||||
rm -f "${DESC_GAME}"
|
||||
fi
|
||||
ln -sfn "${DESC_SAVE}" "${DESC_GAME}"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
) &
|
||||
DESC_WATCHER_PID=$!
|
||||
|
||||
# --- Wine prefix bootstrap (first boot, or repair after a partial init). xvfb-run
|
||||
# handles X for the real run; for prefix init we still need a display so
|
||||
# wineboot's installer dialogs auto-dismiss. ---
|
||||
NEED_INIT=0
|
||||
if [[ ! -d "${WINEPREFIX}" || ! -f "${WINEPREFIX}/system.reg" ]]; then
|
||||
NEED_INIT=1
|
||||
fi
|
||||
# A previous boot can leave a *partially* initialized prefix (system.reg
|
||||
# present but kernel32.dll missing) if wineboot was killed early — that
|
||||
# state crashes every subsequent start with "could not load kernel32.dll,
|
||||
# status c0000135" because our existing-prefix check passes. Detect and
|
||||
# repair by nuking the broken prefix and re-initing from scratch.
|
||||
if [[ "${NEED_INIT}" = 0 && ! -f "${WINEPREFIX}/drive_c/windows/system32/kernel32.dll" ]]; then
|
||||
log "wine prefix at ${WINEPREFIX} is missing kernel32.dll — wiping and reinitializing"
|
||||
rm -rf "${WINEPREFIX}"
|
||||
NEED_INIT=1
|
||||
fi
|
||||
if [[ "${NEED_INIT}" = 1 ]]; then
|
||||
log "initializing Wine prefix at ${WINEPREFIX}"
|
||||
xvfb-run -a wineboot --init 2>&1 | sed 's/^/ /' || true
|
||||
wineserver -w || true
|
||||
if [[ ! -f "${WINEPREFIX}/drive_c/windows/system32/kernel32.dll" ]]; then
|
||||
log "WARNING: wineboot finished but kernel32.dll still missing — game start will fail"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- go ---
|
||||
BIN_DIR="$(dirname "${DEDI_EXE}")"
|
||||
cd "${BIN_DIR}"
|
||||
log "starting Windrose dedicated server"
|
||||
log " game dir: ${GAME_DIR}"
|
||||
log " save dir: ${SAVE_DIR}"
|
||||
log " binary: ${DEDI_EXE}"
|
||||
log " bin dir: ${BIN_DIR}"
|
||||
log " port: ${DIRECT_PORT}"
|
||||
|
||||
trap 'log "SIGTERM received; stopping server"; wineserver -k 2>/dev/null || true; kill "${DESC_WATCHER_PID:-}" 2>/dev/null || true; exit 0' TERM INT
|
||||
|
||||
# Launch flags — matched to cytech-services' AMP template (windrose.kvp):
|
||||
# R5 project name (required positional for UE5 servers)
|
||||
# -stdout mirror UE5 log categories to stdout, not just R5.log
|
||||
# -FullStdOutLogOutput disable stdout filtering — the LogGlobalStatus ready
|
||||
# line we grep for is category-filtered otherwise and
|
||||
# only lands in R5.log. This flag is THE reason AMP's
|
||||
# AppReadyRegex ever matches.
|
||||
# -log enable the UE5 log file (R5/Saved/Logs/R5.log) too
|
||||
# -UNATTENDED / -NullRHI standard UE5 headless flags; harmless no-ops on this
|
||||
# build but documented in other UE5 dedicated patterns.
|
||||
# Port + server identity come from ServerDescription.json (we patch the
|
||||
# port above on every boot from $DIRECT_PORT). xvfb-run -a auto-allocates
|
||||
# a free X display — see panel/memory/gotchas.md for why we can't use :99.
|
||||
exec stdbuf -oL -eL xvfb-run -a --server-args="-screen 0 1024x768x24" \
|
||||
wine "${DEDI_EXE}" \
|
||||
R5 \
|
||||
-stdout \
|
||||
-FullStdOutLogOutput \
|
||||
-log \
|
||||
-UNATTENDED \
|
||||
-NullRHI
|
||||
@@ -0,0 +1,118 @@
|
||||
# Windrose dedicated server module.
|
||||
#
|
||||
# Same panel-native Wine pattern as Empyrion / V Rising / Sons of the Forest:
|
||||
# a minimal Debian 12 image we own (panel-windrose:latest, Debian 12 + Wine
|
||||
# + Xvfb + tini), with game files populated by the generic SteamCMD updater
|
||||
# sidecar into a named volume.
|
||||
#
|
||||
# Windrose ships as a Windows-only dedicated server (SteamDB app 4129620:
|
||||
# depot 4129621 is Windows-default, depot 4129622 is a Linux placeholder
|
||||
# with no manifest). The updater is told to pretend SteamCMD is running on
|
||||
# Windows (`platform: windows` → +@sSteamCmdForcePlatformType windows), and
|
||||
# the entrypoint runs WindroseServer.exe under Wine under Xvfb.
|
||||
#
|
||||
# There is NO native RCON protocol in Windrose — admin console is in-game
|
||||
# only, output via StartServerForeground.bat to stdout. We capture that
|
||||
# stream and surface it in the Console tab; Players tab will be empty until
|
||||
# a third-party RCON shim is wired in (deferred).
|
||||
#
|
||||
# Reference: https://playwindrose.com/dedicated-server-guide/
|
||||
|
||||
id: windrose
|
||||
name: "Windrose"
|
||||
version: 0.1.0
|
||||
authors:
|
||||
- panel contributors
|
||||
|
||||
supported_modes:
|
||||
- docker
|
||||
|
||||
runtime:
|
||||
docker:
|
||||
# Built locally from ./Dockerfile. Rebuild with:
|
||||
# docker build -t panel-windrose:latest modules/windrose
|
||||
image: panel-windrose:latest
|
||||
# Windrose's P2P coop gateway opens a long-lived UDP session to its TURN
|
||||
# server (coturn-us.windrose.support:3478). Docker's default bridge NAT
|
||||
# on Windows rebinds UDP source ports mid-session, which breaks ICE
|
||||
# consent checks and disconnects every joining client (observed:
|
||||
# "Check consent was failed for IceControlling. Reach timeout 10000 ms"
|
||||
# ~25s after join, which also crashes the Windrose client).
|
||||
#
|
||||
# Host networking shares the host's network namespace directly — no
|
||||
# NAT, stable 5-tuples, clients can join reliably. On Linux hosts this
|
||||
# is free and recommended (target deployment: Ubuntu). On Windows Docker
|
||||
# Desktop it requires "Enable host networking" in Settings → Resources
|
||||
# → Network (4.29+); if disabled, remove this line to fall back to
|
||||
# bridge mode (accepting that joins will fail intermittently).
|
||||
network_mode: host
|
||||
browseable_root: /game-saves
|
||||
browseable_roots:
|
||||
- name: "Saves & Configs"
|
||||
path: /game-saves
|
||||
hint: "ServerDescription.json, R5/Saved/SaveProfiles/ world data"
|
||||
- name: "Game Files"
|
||||
path: /game
|
||||
hint: "WindroseServer.exe, R5/ engine files, batch scripts"
|
||||
env:
|
||||
SERVER_NAME: "panel Windrose"
|
||||
DIRECT_PORT: "7777"
|
||||
# true — server listens on host IP:DIRECT_PORT for direct-connect joins
|
||||
# (works when host has a public IP or a port-forward).
|
||||
# false — cloud P2P via windrose.support; players join by InviteCode
|
||||
# with NAT-traversal via TURN. Required on a LAN host with no
|
||||
# port-forward.
|
||||
# ServerDescription.json's UseDirectConnection field is patched to this
|
||||
# value on every boot.
|
||||
USE_DIRECT_CONNECTION: "true"
|
||||
volumes:
|
||||
- { type: volume, name: "panel-$INSTANCE_ID-saves", container: "/game-saves" }
|
||||
- { type: volume, name: "panel-$INSTANCE_ID-game", container: "/game" }
|
||||
- { target: "$DATA_PATH/logs", container: "/game-saves/logs" }
|
||||
|
||||
# Windrose relies on its own R5 coop API gateway for matchmaking / NAT
|
||||
# punch-through (r5coopapigateway-*.windrose.support). The only port the
|
||||
# server LISTENS on is the DirectConnectionServerPort (7777 in the shipped
|
||||
# default ServerDescription.json). No Steam query — publishing 27015/udp
|
||||
# collides with the default Palworld/ARK query port on multi-server hosts.
|
||||
ports:
|
||||
- { name: game, proto: udp, default: 7777, required: true, env: DIRECT_PORT }
|
||||
|
||||
resources:
|
||||
min_ram_mb: 4096
|
||||
recommended_ram_mb: 8192
|
||||
# Log-derived player events. Regexes converted from CubeCoders'
|
||||
# AMPTemplates (windrose.kvp) — .NET (?<n>) syntax -> Go (?P<n>).
|
||||
# Sourced-from-template; not yet exercised against a live client.
|
||||
events:
|
||||
join:
|
||||
pattern: 'ServerAccount\. AccountName ''(?P<name>[^'']+)''\. AccountId (?P<id>\S+?)\. PlayerState'
|
||||
kind: join
|
||||
leave:
|
||||
pattern: '::MoveAccountToListOfDisconnected +Account disconnected\. AccountId (?P<id>\S+)$'
|
||||
kind: leave
|
||||
|
||||
|
||||
update_providers:
|
||||
- id: stable
|
||||
kind: steamcmd
|
||||
app_id: "4129620"
|
||||
platform: windows # server is Windows-only — force Windows depot on Linux host
|
||||
install_path: /game
|
||||
# SteamCMD's app_update ... validate call fails with "Missing configuration"
|
||||
# on a fresh empty install dir (same behaviour as Conan Exiles, app 443030).
|
||||
# Omit the validate keyword so first-install succeeds; operators can run
|
||||
# Update again later for an integrity pass once the tree exists.
|
||||
skip_validate: true
|
||||
|
||||
# --- Manifest-driven UI metadata (WI-07) --------------------------
|
||||
# Consumed by the CONTROLLER/dashboard only (the controller loads its
|
||||
# own ./modules copy at startup); the agent ignores these at runtime.
|
||||
# ready_pattern is a JavaScript regex (slash-delimited when it needs
|
||||
# flags) moved verbatim from the dashboard's READY_PATTERNS map.
|
||||
ready_pattern: '/Host server is ready for owner to connect|LogGlobalStatus:\s*UEngine::LoadMap\s+Load\s+map\s+complete\s+\/Game\/Maps\/(?!Lobby)/i'
|
||||
appearance:
|
||||
emoji: "🧭"
|
||||
grad: "linear-gradient(135deg,#1e3a5f,#60a5fa)"
|
||||
art: "/game-art/windrose.jpg"
|
||||
steam: "3041230"
|
||||
Reference in New Issue
Block a user