Confirmed live: install_frigate()'s fresh-install path overwrote a working, hand-crafted docker-compose.yml (Frigate + mosquitto + frigate-notify) with zero backup, because that file's shape didn't match what frigate.sh's own "existing install" detection knew how to recognize. Every service's own detection is a judgment call about what counts as "already installed" and can miss a real setup built outside this repo's conventions. lib/common.sh gains backup_if_exists(FILE) — copies FILE to FILE.bak.<timestamp> if it exists, no-ops otherwise (including DRY_RUN). Applied before every service's own `cat > docker-compose.yml`/`cat > .env` write across all 60 services that do one (115 call sites), plus a matching standalone-mode stub added to every service's own bootstrap block, same convention already used for port_in_use/find_free_port. This doesn't replace a service's own update/fresh-reinstall detection — it's the safety net underneath it, so a wrong detection costs a .bak file to restore from instead of the original silently disappearing. Also fixes the actual gap that surfaced this: services/frigate.sh's Authelia offer only checked for Authelia installed locally on Frigate's own box, which is never true for a dedicated NVR box with no local Caddy either (the common shape — Caddy lives elsewhere, snippet-generation mode already handles that). Now offers Authelia protection unconditionally and, when Authelia isn't local, asks whether it lives on the same machine as Caddy (still "import authelia", since that's local to wherever Caddy ends up) or on a genuinely separate third machine (the explicit header-pinned forward_auth form, per CLAUDE.md's "forward_auth to a remote Authelia" note, needed because a bare authelia:9091 shortcut only works one hop).
579 lines
25 KiB
Bash
579 lines
25 KiB
Bash
#!/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.
|
|
#
|
|
# 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:
|
|
# - SWBF2 (Steam AppID 1237950) installed via Steam on this machine
|
|
# - Docker CE + Compose plugin
|
|
# - EA account that owns SWBF2 (alphanumeric password — special chars break auth)
|
|
# - 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 format: KYBER_MAP_ROTATION = base64 of newline-separated lines,
|
|
# each line: MODE;MAP_PATH e.g. GalacticAssault;Levels/MP/Geonosis_01/Geonosis_01
|
|
# The interactive builder below produces the correct format automatically.
|
|
# ── Registration ──────────────────────────────────────────────────────────────
|
|
command -v register_service &>/dev/null && \
|
|
register_service kyber-server gaming "Kyber dedicated server for SWBF2 (2017) — headless, no GPU required"
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
_kyber_find_swbf2() {
|
|
# Check standard Steam paths first
|
|
local candidates=(
|
|
"$ACTUAL_HOME/.local/share/Steam/steamapps/common/STAR WARS Battlefront II"
|
|
"$ACTUAL_HOME/.steam/steam/steamapps/common/STAR WARS Battlefront II"
|
|
"/home/$ACTUAL_USER/.local/share/Steam/steamapps/common/STAR WARS Battlefront II"
|
|
)
|
|
for p in "${candidates[@]}"; do
|
|
if [[ -f "$p/starwarsbattlefrontii.exe" ]]; then
|
|
echo "$p"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# Search Steam library folders on mounted drives (libraryfolders.vdf lists them)
|
|
local vdf="$ACTUAL_HOME/.local/share/Steam/steamapps/libraryfolders.vdf"
|
|
if [[ -f "$vdf" ]]; then
|
|
while IFS= read -r line; do
|
|
local libpath
|
|
libpath=$(echo "$line" | grep -oP '"path"\s+"\K[^"]+')
|
|
[[ -z "$libpath" ]] && continue
|
|
local candidate="$libpath/steamapps/common/STAR WARS Battlefront II"
|
|
if [[ -f "$candidate/starwarsbattlefrontii.exe" ]]; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
done < "$vdf"
|
|
fi
|
|
|
|
# Last resort: find across /home and common mount points
|
|
local found
|
|
found=$(find "$ACTUAL_HOME" /mnt /media /run/media 2>/dev/null \
|
|
-name "starwarsbattlefrontii.exe" -not -path "*/wolf-state/*" \
|
|
-print -quit 2>/dev/null)
|
|
if [[ -n "$found" ]]; then
|
|
echo "$(dirname "$found")"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
_kyber_get_token() {
|
|
local auth_toml="$ACTUAL_HOME/.local/share/maxima/auth.toml"
|
|
|
|
# Best path: read token from auth.toml written by Kyber GUI login
|
|
if [[ -f "$auth_toml" ]]; then
|
|
local token
|
|
token=$(grep -oP '(?<=token\s=\s")[^"]+' "$auth_toml" 2>/dev/null \
|
|
|| grep -oP "(?<=token = ')[^']+" "$auth_toml" 2>/dev/null \
|
|
|| grep -oP 'token\s*=\s*\K\S+' "$auth_toml" 2>/dev/null | tr -d '"'"'" )
|
|
if [[ -n "$token" ]]; then
|
|
echo "$token"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Fallback: nothing found
|
|
return 1
|
|
}
|
|
|
|
# ── 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 install libgamemode0:i386 if missing"
|
|
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 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
|
|
log_warning "SWBF2 not found in standard Steam paths."
|
|
echo ""
|
|
echo " The dedicated server mounts the game files read-only — no Steam or"
|
|
echo " EA authentication needed at runtime. You can rsync the game folder"
|
|
echo " from another machine that has SWBF2 installed via Steam."
|
|
echo ""
|
|
|
|
local _do_rsync=""
|
|
prompt_yn "Rsync SWBF2 files from a remote machine now? (y/n):" "y" _do_rsync
|
|
if [[ "$_do_rsync" =~ ^[Yy]$ ]]; then
|
|
local _remote_host _remote_user _remote_path _local_dest
|
|
prompt_text "Remote host (IP or hostname):" "" _remote_host
|
|
prompt_text "Remote username:" "$ACTUAL_USER" _remote_user
|
|
# Default Steam path on Linux
|
|
local _default_remote_path="/home/${_remote_user}/.local/share/Steam/steamapps/common/STAR WARS Battlefront II"
|
|
prompt_text "Remote SWBF2 path [${_default_remote_path}]:" "$_default_remote_path" _remote_path
|
|
|
|
_local_dest="$DOCKER_DIR/kyber-server/swbf2"
|
|
mkdir -p "$_local_dest"
|
|
ensure_docker_dir_ownership "$_local_dest"
|
|
|
|
log_info "Rsyncing from ${_remote_user}@${_remote_host}:${_remote_path}/ ..."
|
|
log_info "Destination: $_local_dest"
|
|
echo ""
|
|
rsync -av --progress \
|
|
"${_remote_user}@${_remote_host}:${_remote_path}/" \
|
|
"$_local_dest/" \
|
|
|| { log_error "rsync failed — check SSH access and remote path."; return 1; }
|
|
|
|
SWBF2_PATH="$_local_dest"
|
|
else
|
|
echo ""
|
|
prompt_text "Enter the full local path to the SWBF2 folder (or leave blank to abort):" "" SWBF2_PATH
|
|
if [[ -z "$SWBF2_PATH" ]]; then
|
|
log_error "No path provided — cannot continue without game files."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -f "$SWBF2_PATH/starwarsbattlefrontii.exe" ]]; then
|
|
log_error "starwarsbattlefrontii.exe not found in: $SWBF2_PATH"
|
|
log_error "Make sure you pointed to the root of the SWBF2 install folder."
|
|
return 1
|
|
fi
|
|
fi
|
|
log_success "Found SWBF2 at: $SWBF2_PATH"
|
|
|
|
# ── libgamemode0:i386 ─────────────────────────────────────────────────────
|
|
# Wine Proton inside the container links against this; without it on the host
|
|
# the bind-mount of /proc may surface warnings. Install once on the host.
|
|
if ! dpkg -l libgamemode0:i386 &>/dev/null 2>&1; then
|
|
log_info "Installing libgamemode0:i386 (Wine/Proton dependency)..."
|
|
dpkg --add-architecture i386 2>/dev/null || true
|
|
apt-get update -qq 2>/dev/null || true
|
|
apt-get install -y libgamemode0:i386 2>/dev/null \
|
|
|| log_warning "libgamemode0:i386 install failed — server may still work"
|
|
fi
|
|
|
|
# ── 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
|
|
|
|
# ── Fresh start option (clear cached EA auth) ────────────────────────────
|
|
local MAXIMA_DIR="$ACTUAL_HOME/.local/share/maxima"
|
|
if [[ -d "$MAXIMA_DIR" ]]; then
|
|
local _fresh=""
|
|
prompt_yn "Existing EA login found. Start fresh (clear cached auth)? (y/n):" "n" _fresh
|
|
if [[ "$_fresh" =~ ^[Yy]$ ]]; then
|
|
sudo -u "$ACTUAL_USER" rm -rf "$MAXIMA_DIR"
|
|
log_success "Cleared Maxima auth cache — EA login will be prompted fresh."
|
|
fi
|
|
fi
|
|
|
|
# ── Get Kyber token ───────────────────────────────────────────────────────
|
|
echo ""
|
|
log_info "Looking for Kyber token in ~/.local/share/maxima/auth.toml..."
|
|
|
|
local KYBER_TOKEN=""
|
|
KYBER_TOKEN=$(_kyber_get_token)
|
|
|
|
if [[ -z "$KYBER_TOKEN" ]]; then
|
|
echo ""
|
|
log_warning "No EA token found — need to log in via Kyber AppImage."
|
|
log_info "Launching Kyber as $ACTUAL_USER — click 'EA Account' and log in, then close Kyber."
|
|
echo ""
|
|
read -r -p " Press Enter to launch Kyber now..." _
|
|
|
|
# Launch AppImage as the actual user in the background
|
|
sudo -u "$ACTUAL_USER" DISPLAY="${DISPLAY:-:0}" \
|
|
XAUTHORITY="$ACTUAL_HOME/.Xauthority" \
|
|
"$APPIMAGE_PATH" &
|
|
local APPIMAGE_PID=$!
|
|
|
|
echo ""
|
|
log_info "Kyber is running (PID $APPIMAGE_PID)."
|
|
log_info "Click 'EA Account', log in via the browser, then close Kyber."
|
|
echo ""
|
|
read -r -p " Press Enter once you have logged in and closed Kyber..." _
|
|
|
|
# Give Maxima a moment to flush auth.toml
|
|
sleep 2
|
|
|
|
# Kill AppImage if still running
|
|
kill "$APPIMAGE_PID" 2>/dev/null || true
|
|
wait "$APPIMAGE_PID" 2>/dev/null || true
|
|
|
|
KYBER_TOKEN=$(_kyber_get_token)
|
|
fi
|
|
|
|
if [[ -z "$KYBER_TOKEN" ]]; then
|
|
echo ""
|
|
log_warning "Still no token found in auth.toml."
|
|
prompt_text "Paste your token manually (or press Enter to abort):" "" KYBER_TOKEN
|
|
fi
|
|
|
|
if [[ -z "$KYBER_TOKEN" ]]; then
|
|
log_error "No Kyber token provided — cannot continue."
|
|
return 1
|
|
fi
|
|
log_success "Kyber token obtained."
|
|
|
|
# ── EA credentials ────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo " ⚠ EA password must contain only letters and numbers."
|
|
echo " Special characters (!, @, #, \$, %, ^, &, *) cause auth failures."
|
|
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
|
|
prompt_text "Server password (leave blank for public):" "" SERVER_PASSWORD
|
|
|
|
# ── Map rotation ──────────────────────────────────────────────────────────
|
|
# Format: base64 of "MODE;MAP_PATH\n" lines (parsed by Kyber CLI).
|
|
local MAP_ROTATION_B64=""
|
|
local _pick_maps=""
|
|
prompt_yn "Configure map rotation now? (y/n):" "y" _pick_maps
|
|
if [[ "$_pick_maps" =~ ^[Yy]$ ]]; then
|
|
echo ""
|
|
# Map definitions: "Display Name|level_path|mode"
|
|
local -a MAP_DEFS=(
|
|
"Geonosis — Galactic Assault|Levels/MP/Geonosis_01/Geonosis_01|GalacticAssault"
|
|
"Kamino — Galactic Assault (ocean platform)|Levels/MP/Kamino_01/Kamino_01|GalacticAssault"
|
|
"Endor — Galactic Assault|Levels/MP/Endor_01/Endor_01|GalacticAssault"
|
|
"Tatooine — Galactic Assault|Levels/MP/Tatooine_01/Tatooine_01|GalacticAssault"
|
|
"Death Star II — Galactic Assault|Levels/MP/DeathStar02_01/DeathStar02_01|GalacticAssault"
|
|
"Starkiller Base — Galactic Assault|Levels/MP/StarKiller_01/StarKiller_01|GalacticAssault"
|
|
"Crait — Galactic Assault|Levels/MP/Crait_01/Crait_02|GalacticAssault"
|
|
"Hoth — Galactic Assault|Levels/MP/Hoth_01/Hoth_01|GalacticAssault"
|
|
"Fondor — Starfighter Assault (space)|Levels/Space/SB_Fondor_01/SB_Fondor_01|StarfighterAssault"
|
|
"Geonosis — Planetary Battles|Levels/MP/Geonosis_01/Geonosis_01|PlanetaryBattles"
|
|
"Kamino — Planetary Battles|Levels/MP/Kamino_01/Kamino_01|PlanetaryBattles"
|
|
"Endor — Planetary Battles|Levels/MP/Endor_01/Endor_01|PlanetaryBattles"
|
|
)
|
|
|
|
local -a SELECTED_MAPS=()
|
|
local -a SELECTED_FLAGS=()
|
|
for i in "${!MAP_DEFS[@]}"; do
|
|
SELECTED_FLAGS[$i]=0
|
|
done
|
|
|
|
# Simple numbered selection (no ncurses needed)
|
|
echo " Enter map numbers separated by spaces (e.g. 1 2 3 4 5):"
|
|
echo ""
|
|
for i in "${!MAP_DEFS[@]}"; do
|
|
local _label
|
|
_label="${MAP_DEFS[$i]%%|*}"
|
|
printf " %2d) %s\n" "$((i+1))" "$_label"
|
|
done
|
|
echo ""
|
|
local _map_choices=""
|
|
read -r -p " Map numbers (Enter for all): " _map_choices
|
|
|
|
if [[ -z "$_map_choices" ]]; then
|
|
# Select all
|
|
for i in "${!MAP_DEFS[@]}"; do
|
|
SELECTED_FLAGS[$i]=1
|
|
done
|
|
else
|
|
for n in $_map_choices; do
|
|
local idx=$((n-1))
|
|
if [[ $idx -ge 0 ]] && [[ $idx -lt ${#MAP_DEFS[@]} ]]; then
|
|
SELECTED_FLAGS[$idx]=1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Build rotation: one "MODE;MAP_PATH" line per entry (newline-separated).
|
|
# The CLI base64-decodes this and splits on newlines, then on ';'.
|
|
local _lines=""
|
|
local _count=0
|
|
for i in "${!MAP_DEFS[@]}"; do
|
|
[[ "${SELECTED_FLAGS[$i]}" == "0" ]] && continue
|
|
IFS='|' read -r _name _level _mode <<< "${MAP_DEFS[$i]}"
|
|
_lines+="${_mode};${_level}"$'\n'
|
|
(( _count++ )) || true
|
|
SELECTED_MAPS+=("$_name")
|
|
done
|
|
|
|
if [[ $_count -eq 0 ]]; then
|
|
log_warning "No maps selected — server will use its default rotation."
|
|
else
|
|
MAP_ROTATION_B64=$(printf '%s' "$_lines" | base64 -w 0)
|
|
echo ""
|
|
log_success "Map rotation configured ($_count maps):"
|
|
for m in "${SELECTED_MAPS[@]}"; do
|
|
echo " • $m"
|
|
done
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# ── Write files ───────────────────────────────────────────────────────────
|
|
local DIR="$DOCKER_DIR/kyber-server"
|
|
mkdir -p "$DIR"
|
|
ensure_docker_dir_ownership "$DIR"
|
|
|
|
backup_if_exists "$DIR/docker-compose.yml"
|
|
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
|
|
# 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
|
|
volumes:
|
|
- ${SWBF2_PATH}:/mnt/battlefront
|
|
- ./logs:/logs
|
|
EOF
|
|
|
|
# Write .env with restricted permissions
|
|
# Values are single-quoted so special characters ($, !, &, etc.) are safe.
|
|
# Exception: single quotes inside a value would still break — avoid them.
|
|
backup_if_exists "$DIR/.env"
|
|
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 for a public server, or set a password to make it private
|
|
KYBER_SERVER_PASSWORD='${SERVER_PASSWORD}'
|
|
# Map rotation: base64 of newline-separated "MODE;MAP_PATH" lines.
|
|
# Re-run the installer to rebuild, or encode manually:
|
|
# printf 'GalacticAssault;Levels/MP/Geonosis_01/Geonosis_01\nGalacticAssault;Levels/MP/Hoth_01/Hoth_01\n' | base64 -w0
|
|
KYBER_MAP_ROTATION=${MAP_ROTATION_B64}
|
|
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.
|
|
Format: one `MODE;MAP_PATH` per line, e.g.:
|
|
```bash
|
|
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
|
|
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."
|
|
|
|
# ── Offer to set up Kyber launcher if not already installed ──────────────
|
|
local BIN_LINK="$ACTUAL_HOME/.local/bin/kyber"
|
|
local DESKTOP_FILE="$ACTUAL_HOME/.local/share/applications/kyber-launcher.desktop"
|
|
if [[ ! -L "$BIN_LINK" ]] && [[ ! -f "$DESKTOP_FILE" ]]; then
|
|
echo ""
|
|
local _setup_launcher=""
|
|
prompt_yn "Set up Kyber launcher (desktop entry + 'kyber' command)? (y/n):" "y" _setup_launcher
|
|
if [[ "$_setup_launcher" =~ ^[Yy]$ ]]; then
|
|
if command -v install_kyber_launcher &>/dev/null; then
|
|
install_kyber_launcher
|
|
else
|
|
# Sourced standalone — call the other service file if present
|
|
local _launcher_sh
|
|
_launcher_sh="$(dirname "${BASH_SOURCE[0]}")/kyber-launcher.sh"
|
|
if [[ -f "$_launcher_sh" ]]; then
|
|
source "$_launcher_sh"
|
|
install_kyber_launcher
|
|
else
|
|
log_warning "kyber-launcher.sh not found — run: sudo ./setup.sh kyber-launcher"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ── 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/urandom | head -c "${1:-32}"; }
|
|
|
|
write_readme() {
|
|
local _dir="$1"; shift
|
|
[[ "$DRY_RUN" == "true" ]] && return 0
|
|
mkdir -p "$_dir"
|
|
cat > "$_dir/README.md"
|
|
}
|
|
backup_if_exists() {
|
|
local _file="$1"
|
|
[ -f "$_file" ] || return 0
|
|
cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null
|
|
}
|
|
|
|
ACTUAL_USER="${SUDO_USER:-$USER}"
|
|
ACTUAL_HOME=$(eval echo "~$ACTUAL_USER")
|
|
DOCKER_DIR="$ACTUAL_HOME/docker"
|
|
DRY_RUN="${DRY_RUN:-false}"
|
|
UNATTENDED="${UNATTENDED:-false}"
|
|
fi
|
|
|
|
install_kyber_server
|
|
exit $?
|
|
fi
|