#!/bin/bash # services/kyber-server.sh — Kyber dedicated server for SWBF2 (2017), headless Docker container. # Part of the modular post-install system (sourced by setup.sh). # # 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 # server via the official Armchair Developers Docker image — no GPU required for the # server itself (GPU passthrough is added automatically if detected, for future use). # # Prerequisites: # - SWBF2 (Steam AppID 1237950) installed via Steam on this machine # - Docker CE + Compose plugin # - EA account that owns SWBF2 # - 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) # 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 # and set KYBER_MAP_ROTATION to a base64-encoded rotation string (use the Kyber client # HOST tab to build a rotation, then base64-encode it). # ── Standalone bootstrap ────────────────────────────────────────────────────── if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then [[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; } _SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" _COMMON="$_SELF_DIR/../lib/common.sh" if [[ -f "$_COMMON" ]]; then source "$_COMMON" else log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; } log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; } log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; } log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; } require_docker() { command -v docker &>/dev/null || { log_error "Docker not found. Install it first:" log_error " curl -fsSL https://get.docker.com | sudo sh" return 1 } docker compose version &>/dev/null || { log_error "Docker Compose plugin missing:" log_error " sudo apt-get install -y docker-compose-plugin" return 1 } } ensure_docker_dir_ownership() { chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true } prompt_text() { local _q="$1" _def="$2" _var="$3" _r [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; } read -r -p " $_q " _r eval "$_var='${_r:-$_def}'" } prompt_yn() { local _q="$1" _def="$2" _var="$3" _r [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; } read -r -p " $_q " _r eval "$_var='${_r:-$_def}'" } generate_password() { tr -dc 'A-Za-z0-9' /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() { local appimage_path="$1" local extract_dir="/tmp/kyber-cli-extract" log_info "Extracting kyber_cli from AppImage..." rm -rf "$extract_dir" mkdir -p "$extract_dir" # Run extraction as the actual user (AppImage needs user context) sudo -u "$ACTUAL_USER" "$appimage_path" --appimage-extract-and-run \ squashfs-root 2>/dev/null || true sudo -u "$ACTUAL_USER" bash -c \ "cd '$extract_dir' && '$appimage_path' --appimage-extract" \ 2>/dev/null || true local cli_bin cli_bin=$(find "$extract_dir/squashfs-root" /tmp/squashfs-root \ -name "kyber_cli" -type f 2>/dev/null | head -1) if [[ -z "$cli_bin" ]]; then # Try running kyber_cli directly if AppImage mounts itself cli_bin=$(find /tmp/.mount_Kyber* -name "kyber_cli" -type f 2>/dev/null | head -1) fi if [[ -z "$cli_bin" ]]; then log_error "Could not extract kyber_cli from AppImage." log_error "Run the Kyber AppImage manually, log in, then find your token in:" log_error " ~/.local/share/maxima/auth.toml" return 1 fi log_info "Found kyber_cli at: $cli_bin" echo "" log_info "A browser will open for EA login. Log in, then return here." echo "" local token token=$(sudo -u "$ACTUAL_USER" "$cli_bin" get_token 2>/dev/null | tr -d '[:space:]') if [[ -z "$token" ]]; then log_error "kyber_cli get_token returned nothing." log_error "Try running manually: $cli_bin get_token" return 1 fi echo "$token" } # ── Install function ─────────────────────────────────────────────────────────── install_kyber_server() { require_docker || return 1 echo "" echo "╔══════════════════════════════════════════════════════╗" echo "║ Kyber Dedicated Server — SWBF2 (2017) ║" echo "║ Headless Docker container, no GPU required ║" echo "╚══════════════════════════════════════════════════════╝" echo "" # ── DRY RUN ─────────────────────────────────────────────────────────────── if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY-RUN] Would auto-detect SWBF2 install path" 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 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/.env (chmod 600)" return 0 fi # ── Detect SWBF2 ────────────────────────────────────────────────────────── log_info "Locating SWBF2 installation..." local SWBF2_PATH SWBF2_PATH=$(_kyber_find_swbf2) if [[ -z "$SWBF2_PATH" ]]; then echo "" echo " ╔══════════════════════════════════════════════════════════════╗" echo " ║ ERROR: Star Wars Battlefront II (2017) not found. ║" echo " ║ ║" echo " ║ Install it first: ║" echo " ║ 1. Install Steam: https://store.steampowered.com ║" echo " ║ 2. In Steam, install SWBF2 (AppID 1237950): ║" echo " ║ steam://install/1237950 ║" echo " ║ 3. Re-run this installer once the download completes. ║" echo " ╚══════════════════════════════════════════════════════════════╝" echo "" return 1 fi log_success "Found SWBF2 at: $SWBF2_PATH" # ── Detect GPU ──────────────────────────────────────────────────────────── local GPU_TYPE GPU_TYPE=$(_kyber_detect_gpu) case "$GPU_TYPE" in nvidia) log_info "GPU detected: NVIDIA (will add GPU passthrough)" ;; intel) log_info "GPU detected: Intel (will add DRI device passthrough)" ;; amd) log_info "GPU detected: AMD (will add DRI device passthrough)" ;; none) log_info "No GPU detected — running CPU-only (fine for dedicated server)" ;; esac # ── Kyber AppImage / CLI ────────────────────────────────────────────────── local KYBER_REPO="simonlinuxcraft/kyber-linuxport-unofficial" local APPIMAGE_DIR="$ACTUAL_HOME/.local/share/kyber" local APPIMAGE_PATH="$APPIMAGE_DIR/KyberLinuxPort.AppImage" if [[ ! -f "$APPIMAGE_PATH" ]]; then log_info "Kyber AppImage not found — downloading latest release..." local APPIMAGE_URL APPIMAGE_URL=$(curl -fsSL \ "https://api.github.com/repos/${KYBER_REPO}/releases/latest" \ | python3 -c " import json, sys data = json.load(sys.stdin) for a in data.get('assets', []): if a['browser_download_url'].endswith('.AppImage'): print(a['browser_download_url']) break " 2>/dev/null) if [[ -z "$APPIMAGE_URL" ]]; then log_error "Could not fetch Kyber AppImage URL from GitHub." log_error "Check: https://github.com/${KYBER_REPO}/releases" return 1 fi sudo -u "$ACTUAL_USER" mkdir -p "$APPIMAGE_DIR" log_info "Downloading: $APPIMAGE_URL" sudo -u "$ACTUAL_USER" curl -L --progress-bar \ -o "$APPIMAGE_PATH" "$APPIMAGE_URL" sudo -u "$ACTUAL_USER" chmod +x "$APPIMAGE_PATH" log_success "Kyber AppImage downloaded." else log_info "Kyber AppImage already present: $APPIMAGE_PATH" fi # ── Get Kyber token ─────────────────────────────────────────────────────── echo "" log_info "Getting your Kyber server token..." log_info "This opens a browser for EA login — log in, then return here." echo "" local KYBER_TOKEN="" KYBER_TOKEN=$(_kyber_get_token "$APPIMAGE_PATH") if [[ -z "$KYBER_TOKEN" ]]; then log_warning "Could not retrieve token automatically." prompt_text "Paste your KYBER_TOKEN manually (or press Enter to skip):" "" KYBER_TOKEN fi if [[ -z "$KYBER_TOKEN" ]]; then log_error "No Kyber token provided. Cannot continue." log_error "Get it manually: run the Kyber AppImage, log in, then check" log_error " ~/.local/share/maxima/auth.toml" return 1 fi log_success "Kyber token obtained." # ── EA credentials ──────────────────────────────────────────────────────── echo "" local EA_EMAIL EA_PASSWORD prompt_text "EA account email:" "" EA_EMAIL if [[ -z "$EA_EMAIL" ]]; then log_error "EA email is required." return 1 fi # Read password without echo local _pw_prompt=" EA account password: " if [[ "${UNATTENDED:-false}" == "true" ]]; then EA_PASSWORD="" else read -r -s -p "$_pw_prompt" EA_PASSWORD echo "" fi if [[ -z "$EA_PASSWORD" ]]; then log_error "EA password is required." return 1 fi # ── Server config ───────────────────────────────────────────────────────── local SERVER_NAME MAX_PLAYERS prompt_text "Server name [Kyber Server]:" "Kyber Server" SERVER_NAME prompt_text "Max players [40]:" "40" MAX_PLAYERS # ── Write files ─────────────────────────────────────────────────────────── local DIR="$DOCKER_DIR/kyber-server" mkdir -p "$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 name: kyber-server services: kyber-server: image: ghcr.io/armchairdevelopers/kyber-server:latest container_name: kyber-swbf2 restart: unless-stopped env_file: .env volumes: - ${SWBF2_PATH}:/mnt/battlefront:ro - ./logs:/logs${GPU_SECTION}${DEVICES_SECTION}${GROUP_ADD_SECTION} EOF # Write .env with restricted permissions cat > "$DIR/.env" << EOF MAXIMA_CREDENTIALS=${EA_EMAIL}:${EA_PASSWORD} KYBER_TOKEN=${KYBER_TOKEN} KYBER_SERVER_NAME=${SERVER_NAME} KYBER_SERVER_MAX_PLAYERS=${MAX_PLAYERS} # Leave blank until you have a base64-encoded map rotation string. # Build one in the Kyber client HOST tab, then base64-encode it: # echo -n '' | base64 KYBER_MAP_ROTATION= EOF chmod 600 "$DIR/.env" ensure_docker_dir_ownership "$DIR" mkdir -p "$DIR/logs" write_readme "$DIR" << 'MD' # Kyber Dedicated Server (SWBF2 2017) Headless community multiplayer server via the Armchair Developers Docker image. No GPU required for the server itself. ## Manage ```bash docker compose up -d # start docker compose down # stop docker compose logs -f # live logs docker compose pull && docker compose up -d # update ``` ## Map rotation Edit `.env` and set `KYBER_MAP_ROTATION` to a base64-encoded rotation string. Build the rotation JSON in the Kyber client (HOST tab), then: ```bash echo -n '' | base64 ``` ## Token refresh If the Kyber token stops working, re-run the installer or: ```bash ~/.local/share/kyber/KyberLinuxPort.AppImage # log in, get new token # update KYBER_TOKEN in .env, then: docker compose up -d ``` MD log_success "Written: $DIR/docker-compose.yml" log_success "Written: $DIR/.env (permissions: 600)" echo "" # ── Offer to start ──────────────────────────────────────────────────────── local START="" prompt_yn "Pull latest image and start the server now? (y/n):" "y" START if [[ "$START" =~ ^[Yy]$ ]]; then cd "$DIR" || return 1 docker compose pull docker compose up -d \ && log_success "Kyber server started. Check logs: docker compose logs -f" \ || log_warning "Start failed — check: docker compose logs" fi echo "" echo "Server directory: $DIR" echo "Edit $DIR/.env to set map rotation or update credentials." echo "Token refresh: re-run this installer or update KYBER_TOKEN in .env manually." }