kyber-server: fix Wine crash + map rotation format
Three root-cause fixes found by reading the Docker image source:
1. Wine/Proton page fault: Docker's default seccomp profile blocks
system calls that Wine Proton GE requires. Fix: security_opt:
seccomp=unconfined + shm_size: 256m (Xvfb needs /dev/shm for
MIT-SHM extension; 64 MB default is too small).
Added network_mode: host for game traffic (dynamic UDP ports).
2. KYBER_MAP_ROTATION exit 64: the Kyber CLI decodes base64 and
parses newline-separated "MODE;MAP_PATH" lines, not JSON objects.
Our JSON [{map:...,mode:...}] format split on semicolons into one
field → ExitCode.usage (64). Fixed builder to emit MODE;MAP_PATH\n
lines and updated .env comment + README example.
3. GPU passthrough removed: Proton GE includes DXVK which crashes
headlessly when a GPU is passed through (no Vulkan display). The
server needs no GPU; removing passthrough is the correct fix.
Also: install libgamemode0:i386 on the host (Wine/Proton dep),
add alphanumeric-password warning (special chars → INVALID_PASSWORD).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014be1aK9G8CY2msho5LjxR4
This commit is contained in:
+46
-77
@@ -4,21 +4,25 @@
|
|||||||
#
|
#
|
||||||
# Kyber is a community multiplayer replacement for Star Wars Battlefront II (2017)
|
# Kyber is a community multiplayer replacement for Star Wars Battlefront II (2017)
|
||||||
# after EA shut down official servers in 2022. This installs the headless dedicated
|
# after EA shut down official servers in 2022. This installs the headless dedicated
|
||||||
# server via the official Armchair Developers Docker image — no GPU required for the
|
# server via the official Armchair Developers Docker image.
|
||||||
# server itself (GPU passthrough is added automatically if detected, for future use).
|
#
|
||||||
|
# The container runs Wine Proton GE + Xvfb headlessly. Three requirements:
|
||||||
|
# - seccomp=unconfined (Docker default blocks Wine/Proton system calls → page fault)
|
||||||
|
# - shm_size ≥ 256m (Xvfb needs /dev/shm for MIT-SHM extension)
|
||||||
|
# - network_mode: host (game traffic uses dynamic UDP ports)
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# - SWBF2 (Steam AppID 1237950) installed via Steam on this machine
|
# - SWBF2 (Steam AppID 1237950) installed via Steam on this machine
|
||||||
# - Docker CE + Compose plugin
|
# - Docker CE + Compose plugin
|
||||||
# - EA account that owns SWBF2
|
# - EA account that owns SWBF2 (alphanumeric password — special chars break auth)
|
||||||
# - Internet access to pull the Docker image and authenticate with Kyber
|
# - Internet access to pull the Docker image and authenticate with Kyber
|
||||||
#
|
#
|
||||||
# The Kyber token is obtained via kyber_cli (extracted from the Linux port AppImage)
|
# The Kyber token is obtained via kyber_cli (extracted from the Linux port AppImage)
|
||||||
# which opens a browser for EA OAuth. One-time step — the token never expires.
|
# which opens a browser for EA OAuth. One-time step — the token never expires.
|
||||||
#
|
#
|
||||||
# Map rotation is left blank by default; edit ~/docker/kyber-server/.env after install
|
# Map rotation format: KYBER_MAP_ROTATION = base64 of newline-separated lines,
|
||||||
# and set KYBER_MAP_ROTATION to a base64-encoded rotation string (use the Kyber client
|
# each line: MODE;MAP_PATH e.g. GalacticAssault;Levels/MP/Geonosis_01/Geonosis_01
|
||||||
# HOST tab to build a rotation, then base64-encode it).
|
# The interactive builder below produces the correct format automatically.
|
||||||
# ── Registration ──────────────────────────────────────────────────────────────
|
# ── Registration ──────────────────────────────────────────────────────────────
|
||||||
command -v register_service &>/dev/null && \
|
command -v register_service &>/dev/null && \
|
||||||
register_service kyber-server gaming "Kyber dedicated server for SWBF2 (2017) — headless, no GPU required"
|
register_service kyber-server gaming "Kyber dedicated server for SWBF2 (2017) — headless, no GPU required"
|
||||||
@@ -67,21 +71,6 @@ _kyber_find_swbf2() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
_kyber_detect_gpu() {
|
|
||||||
# Returns: nvidia | intel | amd | none
|
|
||||||
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null 2>&1; then
|
|
||||||
echo "nvidia"
|
|
||||||
elif [[ -e /dev/dri/renderD128 ]]; then
|
|
||||||
if lspci 2>/dev/null | grep -qi "Intel.*VGA\|VGA.*Intel"; then
|
|
||||||
echo "intel"
|
|
||||||
else
|
|
||||||
echo "amd"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "none"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_kyber_get_token() {
|
_kyber_get_token() {
|
||||||
local auth_toml="$ACTUAL_HOME/.local/share/maxima/auth.toml"
|
local auth_toml="$ACTUAL_HOME/.local/share/maxima/auth.toml"
|
||||||
|
|
||||||
@@ -115,10 +104,10 @@ install_kyber_server() {
|
|||||||
# ── DRY RUN ───────────────────────────────────────────────────────────────
|
# ── DRY RUN ───────────────────────────────────────────────────────────────
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
echo "[DRY-RUN] Would auto-detect SWBF2 install path"
|
echo "[DRY-RUN] Would auto-detect SWBF2 install path"
|
||||||
|
echo "[DRY-RUN] Would install libgamemode0:i386 if missing"
|
||||||
echo "[DRY-RUN] Would download Kyber AppImage and extract kyber_cli"
|
echo "[DRY-RUN] Would download Kyber AppImage and extract kyber_cli"
|
||||||
echo "[DRY-RUN] Would run kyber_cli get_token (opens browser)"
|
echo "[DRY-RUN] Would run kyber_cli get_token (opens browser)"
|
||||||
echo "[DRY-RUN] Would prompt for EA credentials and server name"
|
echo "[DRY-RUN] Would prompt for EA credentials and server name"
|
||||||
echo "[DRY-RUN] Would detect GPU and add passthrough if found"
|
|
||||||
echo "[DRY-RUN] Would write ~/docker/kyber-server/docker-compose.yml"
|
echo "[DRY-RUN] Would write ~/docker/kyber-server/docker-compose.yml"
|
||||||
echo "[DRY-RUN] Would write ~/docker/kyber-server/.env (chmod 600)"
|
echo "[DRY-RUN] Would write ~/docker/kyber-server/.env (chmod 600)"
|
||||||
return 0
|
return 0
|
||||||
@@ -177,15 +166,16 @@ install_kyber_server() {
|
|||||||
fi
|
fi
|
||||||
log_success "Found SWBF2 at: $SWBF2_PATH"
|
log_success "Found SWBF2 at: $SWBF2_PATH"
|
||||||
|
|
||||||
# ── Detect GPU ────────────────────────────────────────────────────────────
|
# ── libgamemode0:i386 ─────────────────────────────────────────────────────
|
||||||
local GPU_TYPE
|
# Wine Proton inside the container links against this; without it on the host
|
||||||
GPU_TYPE=$(_kyber_detect_gpu)
|
# the bind-mount of /proc may surface warnings. Install once on the host.
|
||||||
case "$GPU_TYPE" in
|
if ! dpkg -l libgamemode0:i386 &>/dev/null 2>&1; then
|
||||||
nvidia) log_info "GPU detected: NVIDIA (will add GPU passthrough)" ;;
|
log_info "Installing libgamemode0:i386 (Wine/Proton dependency)..."
|
||||||
intel) log_info "GPU detected: Intel (will add DRI device passthrough)" ;;
|
dpkg --add-architecture i386 2>/dev/null || true
|
||||||
amd) log_info "GPU detected: AMD (will add DRI device passthrough)" ;;
|
apt-get update -qq 2>/dev/null || true
|
||||||
none) log_info "No GPU detected — running CPU-only (fine for dedicated server)" ;;
|
apt-get install -y libgamemode0:i386 2>/dev/null \
|
||||||
esac
|
|| log_warning "libgamemode0:i386 install failed — server may still work"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Kyber AppImage / CLI ──────────────────────────────────────────────────
|
# ── Kyber AppImage / CLI ──────────────────────────────────────────────────
|
||||||
local KYBER_REPO="simonlinuxcraft/kyber-linuxport-unofficial"
|
local KYBER_REPO="simonlinuxcraft/kyber-linuxport-unofficial"
|
||||||
@@ -283,6 +273,9 @@ for a in data.get('assets', []):
|
|||||||
|
|
||||||
# ── EA credentials ────────────────────────────────────────────────────────
|
# ── EA credentials ────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
|
echo " ⚠ EA password must contain only letters and numbers."
|
||||||
|
echo " Special characters (!, @, #, \$, %, ^, &, *) cause auth failures."
|
||||||
|
echo ""
|
||||||
local EA_EMAIL EA_PASSWORD
|
local EA_EMAIL EA_PASSWORD
|
||||||
prompt_text "EA account email:" "" EA_EMAIL
|
prompt_text "EA account email:" "" EA_EMAIL
|
||||||
if [[ -z "$EA_EMAIL" ]]; then
|
if [[ -z "$EA_EMAIL" ]]; then
|
||||||
@@ -311,14 +304,11 @@ for a in data.get('assets', []):
|
|||||||
prompt_text "Server password (leave blank for public):" "" SERVER_PASSWORD
|
prompt_text "Server password (leave blank for public):" "" SERVER_PASSWORD
|
||||||
|
|
||||||
# ── Map rotation ──────────────────────────────────────────────────────────
|
# ── Map rotation ──────────────────────────────────────────────────────────
|
||||||
# Known map IDs decoded from maprotation.hive (the binary the Kyber client writes).
|
# Format: base64 of "MODE;MAP_PATH\n" lines (parsed by Kyber CLI).
|
||||||
# Format: JSON array → base64 → KYBER_MAP_ROTATION env var.
|
|
||||||
local MAP_ROTATION_B64=""
|
local MAP_ROTATION_B64=""
|
||||||
local _pick_maps=""
|
local _pick_maps=""
|
||||||
prompt_yn "Configure map rotation now? (y/n):" "y" _pick_maps
|
prompt_yn "Configure map rotation now? (y/n):" "y" _pick_maps
|
||||||
if [[ "$_pick_maps" =~ ^[Yy]$ ]]; then
|
if [[ "$_pick_maps" =~ ^[Yy]$ ]]; then
|
||||||
echo ""
|
|
||||||
echo " Available maps (Space = toggle, Enter when done):"
|
|
||||||
echo ""
|
echo ""
|
||||||
# Map definitions: "Display Name|level_path|mode"
|
# Map definitions: "Display Name|level_path|mode"
|
||||||
local -a MAP_DEFS=(
|
local -a MAP_DEFS=(
|
||||||
@@ -368,25 +358,24 @@ for a in data.get('assets', []):
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build JSON array
|
# Build rotation: one "MODE;MAP_PATH" line per entry (newline-separated).
|
||||||
local _json="["
|
# The CLI base64-decodes this and splits on newlines, then on ';'.
|
||||||
local _first=1
|
local _lines=""
|
||||||
|
local _count=0
|
||||||
for i in "${!MAP_DEFS[@]}"; do
|
for i in "${!MAP_DEFS[@]}"; do
|
||||||
[[ "${SELECTED_FLAGS[$i]}" == "0" ]] && continue
|
[[ "${SELECTED_FLAGS[$i]}" == "0" ]] && continue
|
||||||
IFS='|' read -r _name _level _mode <<< "${MAP_DEFS[$i]}"
|
IFS='|' read -r _name _level _mode <<< "${MAP_DEFS[$i]}"
|
||||||
[[ "$_first" == "0" ]] && _json+=","
|
_lines+="${_mode};${_level}"$'\n'
|
||||||
_json+="{\"map\":\"${_level}\",\"mode\":\"${_mode}\"}"
|
(( _count++ )) || true
|
||||||
_first=0
|
|
||||||
SELECTED_MAPS+=("$_name")
|
SELECTED_MAPS+=("$_name")
|
||||||
done
|
done
|
||||||
_json+="]"
|
|
||||||
|
|
||||||
if [[ "$_first" == "1" ]]; then
|
if [[ $_count -eq 0 ]]; then
|
||||||
log_warning "No maps selected — server will use its default rotation."
|
log_warning "No maps selected — server will use its default rotation."
|
||||||
else
|
else
|
||||||
MAP_ROTATION_B64=$(echo -n "$_json" | base64 -w 0)
|
MAP_ROTATION_B64=$(printf '%s' "$_lines" | base64 -w 0)
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Map rotation configured (${#SELECTED_MAPS[@]} maps):"
|
log_success "Map rotation configured ($_count maps):"
|
||||||
for m in "${SELECTED_MAPS[@]}"; do
|
for m in "${SELECTED_MAPS[@]}"; do
|
||||||
echo " • $m"
|
echo " • $m"
|
||||||
done
|
done
|
||||||
@@ -399,33 +388,6 @@ for a in data.get('assets', []):
|
|||||||
mkdir -p "$DIR"
|
mkdir -p "$DIR"
|
||||||
ensure_docker_dir_ownership "$DIR"
|
ensure_docker_dir_ownership "$DIR"
|
||||||
|
|
||||||
# Build GPU section for docker-compose
|
|
||||||
local GPU_SECTION=""
|
|
||||||
local DEVICES_SECTION=""
|
|
||||||
local GROUP_ADD_SECTION=""
|
|
||||||
|
|
||||||
case "$GPU_TYPE" in
|
|
||||||
nvidia)
|
|
||||||
GPU_SECTION="
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: all
|
|
||||||
capabilities: [gpu]"
|
|
||||||
;;
|
|
||||||
intel|amd)
|
|
||||||
DEVICES_SECTION="
|
|
||||||
devices:
|
|
||||||
- /dev/dri/renderD128:/dev/dri/renderD128"
|
|
||||||
GROUP_ADD_SECTION="
|
|
||||||
group_add:
|
|
||||||
- video
|
|
||||||
- render"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
cat > "$DIR/docker-compose.yml" << EOF
|
cat > "$DIR/docker-compose.yml" << EOF
|
||||||
name: kyber-server
|
name: kyber-server
|
||||||
services:
|
services:
|
||||||
@@ -433,11 +395,17 @@ services:
|
|||||||
image: ghcr.io/armchairdevelopers/kyber-server:latest
|
image: ghcr.io/armchairdevelopers/kyber-server:latest
|
||||||
container_name: kyber-swbf2
|
container_name: kyber-swbf2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# host networking: game traffic uses dynamic UDP ports
|
||||||
|
network_mode: host
|
||||||
|
# seccomp=unconfined: Wine/Proton needs system calls blocked by Docker default
|
||||||
|
# shm_size: Xvfb requires /dev/shm (MIT-SHM extension) — 64 MB default is too small
|
||||||
|
security_opt:
|
||||||
|
- seccomp=unconfined
|
||||||
|
shm_size: '256m'
|
||||||
env_file: .env
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- ${SWBF2_PATH}:/mnt/battlefront
|
- ${SWBF2_PATH}:/mnt/battlefront
|
||||||
- ./logs:/logs
|
- ./logs:/logs
|
||||||
${GPU_SECTION}${DEVICES_SECTION}${GROUP_ADD_SECTION}
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Write .env with restricted permissions
|
# Write .env with restricted permissions
|
||||||
@@ -450,9 +418,9 @@ KYBER_SERVER_NAME='${SERVER_NAME}'
|
|||||||
KYBER_SERVER_MAX_PLAYERS=${MAX_PLAYERS}
|
KYBER_SERVER_MAX_PLAYERS=${MAX_PLAYERS}
|
||||||
# Leave blank for a public server, or set a password to make it private
|
# Leave blank for a public server, or set a password to make it private
|
||||||
KYBER_SERVER_PASSWORD='${SERVER_PASSWORD}'
|
KYBER_SERVER_PASSWORD='${SERVER_PASSWORD}'
|
||||||
# Map rotation: base64-encoded JSON array of {"map":"...","mode":"..."} objects.
|
# Map rotation: base64 of newline-separated "MODE;MAP_PATH" lines.
|
||||||
# Re-run the installer to rebuild, or encode manually:
|
# Re-run the installer to rebuild, or encode manually:
|
||||||
# echo -n '[{"map":"Levels/MP/Geonosis_01/Geonosis_01","mode":"GalacticAssault"}]' | base64 -w0
|
# printf 'GalacticAssault;Levels/MP/Geonosis_01/Geonosis_01\nGalacticAssault;Levels/MP/Hoth_01/Hoth_01\n' | base64 -w0
|
||||||
KYBER_MAP_ROTATION=${MAP_ROTATION_B64}
|
KYBER_MAP_ROTATION=${MAP_ROTATION_B64}
|
||||||
EOF
|
EOF
|
||||||
chmod 600 "$DIR/.env"
|
chmod 600 "$DIR/.env"
|
||||||
@@ -476,10 +444,11 @@ docker compose pull && docker compose up -d # update
|
|||||||
|
|
||||||
## Map rotation
|
## Map rotation
|
||||||
Edit `.env` and set `KYBER_MAP_ROTATION` to a base64-encoded rotation string.
|
Edit `.env` and set `KYBER_MAP_ROTATION` to a base64-encoded rotation string.
|
||||||
Build the rotation JSON in the Kyber client (HOST tab), then:
|
Format: one `MODE;MAP_PATH` per line, e.g.:
|
||||||
```bash
|
```bash
|
||||||
echo -n '<rotation-json>' | base64
|
printf 'GalacticAssault;Levels/MP/Geonosis_01/Geonosis_01\nGalacticAssault;Levels/MP/Hoth_01/Hoth_01\n' | base64 -w0
|
||||||
```
|
```
|
||||||
|
Paste the output as `KYBER_MAP_ROTATION=<value>` in `.env`, then restart.
|
||||||
|
|
||||||
## Token refresh
|
## Token refresh
|
||||||
If the Kyber token stops working, re-run the installer or:
|
If the Kyber token stops working, re-run the installer or:
|
||||||
|
|||||||
Reference in New Issue
Block a user