Merge pull request #136 from outis1one/claude/zealous-heisenberg-8ylloi
Claude/zealous heisenberg 8ylloi
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
#!/bin/bash
|
||||
# services/kyber-launcher.sh — Kyber Launcher (native Linux AppImage) for SWBF2 (2017).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Kyber is the community multiplayer client for Star Wars Battlefront II (2017)
|
||||
# after EA shut down official servers in 2022. It went open-source (GPL) in
|
||||
# January 2026. This installs the native Linux AppImage port.
|
||||
#
|
||||
# Repo: https://github.com/simonlinuxcraft/kyber-linuxport-unofficial
|
||||
#
|
||||
# ── Requirements ──────────────────────────────────────────────────────────────
|
||||
# - SWBF2 (Steam AppID 1237950) installed via Steam on this machine
|
||||
# - glibc 2.38+ (Ubuntu/Mint 24.04+, Fedora 38+, SteamOS 3.7+)
|
||||
# Linux Mint: Mint 21.x uses Ubuntu 22.04 base (glibc 2.35 — too old).
|
||||
# Mint 22.x uses Ubuntu 24.04 base (glibc 2.39 — works).
|
||||
# - A real discrete GPU with Vulkan drivers (NVIDIA or AMD recommended)
|
||||
# Intel Iris Xe (integrated) is not supported by the Kyber Linux port.
|
||||
# - EA account (free) that owns SWBF2
|
||||
#
|
||||
# ── How to play ───────────────────────────────────────────────────────────────
|
||||
# 1. Open Steam (must be running; do NOT click Play on SWBF2)
|
||||
# 2. Launch Kyber (kyber command or app menu)
|
||||
# 3. Join a server (HOME) or host one (HOST)
|
||||
# 4. Kyber/Maxima launches SWBF2 via its own bundled GE-Proton (~1-3 min)
|
||||
# 5. If the SWBF2 window appears but won't focus: press Alt+Tab or click it
|
||||
#
|
||||
# ── bwrap / unprivileged user namespaces ──────────────────────────────────────
|
||||
# Ubuntu/Mint 24.04 restricts these by default. This installer fixes it
|
||||
# automatically when run with sudo (required for setup.sh anyway).
|
||||
# Without the fix: bwrap: setting up uid map: Permission denied
|
||||
|
||||
# ── 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; }
|
||||
|
||||
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}'"
|
||||
}
|
||||
|
||||
ACTUAL_USER="${SUDO_USER:-$USER}"
|
||||
ACTUAL_HOME=$(eval echo "~$ACTUAL_USER")
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
fi
|
||||
|
||||
install_kyber_launcher
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────────
|
||||
register_service kyber-launcher gaming "Kyber Launcher — native Linux AppImage for SWBF2 (2017) multiplayer (requires discrete GPU)"
|
||||
|
||||
# ── Install function ──────────────────────────────────────────────────────────
|
||||
install_kyber_launcher() {
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ Kyber Launcher — SWBF2 (2017) Multiplayer ║"
|
||||
echo "║ Native Linux AppImage (simonlinuxcraft) ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[DRY-RUN] Would check glibc version (needs 2.38+)"
|
||||
echo "[DRY-RUN] Would fix unprivileged user namespaces (bwrap requirement)"
|
||||
echo "[DRY-RUN] Would download latest Kyber AppImage from GitHub"
|
||||
echo "[DRY-RUN] Would install desktop entry and 'kyber' bin symlink"
|
||||
echo "[DRY-RUN] Would warn if no discrete GPU detected"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local KYBER_REPO="simonlinuxcraft/kyber-linuxport-unofficial"
|
||||
local INSTALL_DIR="$ACTUAL_HOME/.local/share/kyber"
|
||||
local APPIMAGE_PATH="$INSTALL_DIR/KyberLinuxPort.AppImage"
|
||||
local DESKTOP_FILE="$ACTUAL_HOME/.local/share/applications/kyber-launcher.desktop"
|
||||
local BIN_LINK="$ACTUAL_HOME/.local/bin/kyber"
|
||||
|
||||
# ── glibc check ───────────────────────────────────────────────────────────
|
||||
local GLIBC GLIBC_MAJOR GLIBC_MINOR
|
||||
GLIBC=$(ldd --version 2>/dev/null | head -1 | grep -oP '\d+\.\d+$' || echo "0.0")
|
||||
GLIBC_MAJOR=$(echo "$GLIBC" | cut -d. -f1)
|
||||
GLIBC_MINOR=$(echo "$GLIBC" | cut -d. -f2)
|
||||
if [[ "$GLIBC_MAJOR" -lt 2 ]] || { [[ "$GLIBC_MAJOR" -eq 2 ]] && [[ "$GLIBC_MINOR" -lt 38 ]]; }; then
|
||||
log_warning "glibc $GLIBC detected — Kyber requires glibc 2.38+."
|
||||
log_warning "Ubuntu/Mint 22.x ships glibc 2.35 (too old). Upgrade to 24.04-based distro."
|
||||
log_warning "Continuing install anyway — the AppImage may not run."
|
||||
echo ""
|
||||
else
|
||||
log_info "glibc $GLIBC — OK"
|
||||
fi
|
||||
|
||||
# ── GPU check (informational) ─────────────────────────────────────────────
|
||||
local HAS_DISCRETE=false
|
||||
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null 2>&1; then
|
||||
log_info "GPU: NVIDIA detected"
|
||||
HAS_DISCRETE=true
|
||||
elif lspci 2>/dev/null | grep -qiE "VGA.*AMD|AMD.*VGA|Radeon"; then
|
||||
log_info "GPU: AMD detected"
|
||||
HAS_DISCRETE=true
|
||||
elif lspci 2>/dev/null | grep -qiE "VGA.*Intel|Intel.*VGA"; then
|
||||
log_warning "GPU: Intel integrated graphics detected."
|
||||
log_warning "The Kyber Linux port requires a real discrete GPU (NVIDIA or AMD)."
|
||||
log_warning "Game levels will likely crash on Intel Iris Xe."
|
||||
log_warning "Consider the Sunshine + Moonlight setup instead: sudo ./setup.sh sunshine"
|
||||
fi
|
||||
|
||||
# ── Fix unprivileged user namespaces (bwrap) ──────────────────────────────
|
||||
log_info "Checking unprivileged user namespace support (required for Kyber)..."
|
||||
local CLONE_VAL APPARMOR_VAL
|
||||
CLONE_VAL=$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || echo "1")
|
||||
APPARMOR_VAL=$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$CLONE_VAL" != "1" ]] || [[ "$APPARMOR_VAL" != "0" ]]; then
|
||||
log_warning "Unprivileged user namespaces restricted — applying fix..."
|
||||
sysctl -w kernel.unprivileged_userns_clone=1
|
||||
sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
cat > /etc/sysctl.d/99-userns.conf << 'SYSCTL'
|
||||
# Required for Kyber AppImage (bubblewrap sandbox)
|
||||
kernel.unprivileged_userns_clone = 1
|
||||
kernel.apparmor_restrict_unprivileged_userns = 0
|
||||
SYSCTL
|
||||
log_success "bwrap fix applied (saved to /etc/sysctl.d/99-userns.conf)."
|
||||
else
|
||||
log_info "Unprivileged user namespaces: OK"
|
||||
fi
|
||||
|
||||
# ── Fetch latest release ───────────────────────────────────────────────────
|
||||
log_info "Fetching latest Kyber Linux release..."
|
||||
local API_URL="https://api.github.com/repos/${KYBER_REPO}/releases/latest"
|
||||
local APPIMAGE_URL VERSION
|
||||
APPIMAGE_URL=$(curl -fsSL "$API_URL" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for a in data.get('assets', []):
|
||||
url = a['browser_download_url']
|
||||
if url.endswith('.AppImage'):
|
||||
print(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"
|
||||
log_error "Download manually, chmod +x, and run it."
|
||||
return 1
|
||||
fi
|
||||
|
||||
VERSION=$(echo "$APPIMAGE_URL" | grep -oP 'v[\d.a-z-]+' | head -1)
|
||||
log_info "Latest: $VERSION"
|
||||
|
||||
# ── Download ───────────────────────────────────────────────────────────────
|
||||
sudo -u "$ACTUAL_USER" mkdir -p "$INSTALL_DIR"
|
||||
|
||||
local CURRENT_VERSION=""
|
||||
[[ -f "$APPIMAGE_PATH.version" ]] && CURRENT_VERSION=$(cat "$APPIMAGE_PATH.version")
|
||||
|
||||
if [[ -f "$APPIMAGE_PATH" ]] && [[ "$CURRENT_VERSION" == "$VERSION" ]]; then
|
||||
log_info "Already up to date ($VERSION) — skipping download."
|
||||
else
|
||||
log_info "Downloading Kyber AppImage ($VERSION)..."
|
||||
sudo -u "$ACTUAL_USER" curl -L --progress-bar -o "$APPIMAGE_PATH" "$APPIMAGE_URL"
|
||||
sudo -u "$ACTUAL_USER" chmod +x "$APPIMAGE_PATH"
|
||||
echo "$VERSION" | sudo -u "$ACTUAL_USER" tee "$APPIMAGE_PATH.version" > /dev/null
|
||||
log_success "Downloaded: $APPIMAGE_PATH"
|
||||
fi
|
||||
|
||||
# ── Desktop entry + bin symlink ────────────────────────────────────────────
|
||||
sudo -u "$ACTUAL_USER" mkdir -p "$(dirname "$DESKTOP_FILE")" "$ACTUAL_HOME/.local/bin"
|
||||
|
||||
sudo -u "$ACTUAL_USER" tee "$DESKTOP_FILE" > /dev/null << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Kyber Launcher
|
||||
Comment=Community multiplayer for Star Wars Battlefront II (2017)
|
||||
Exec=${APPIMAGE_PATH}
|
||||
Icon=kyber
|
||||
Categories=Game;
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
sudo -u "$ACTUAL_USER" ln -sf "$APPIMAGE_PATH" "$BIN_LINK"
|
||||
sudo -u "$ACTUAL_USER" update-desktop-database "$ACTUAL_HOME/.local/share/applications" 2>/dev/null || true
|
||||
|
||||
log_success "Desktop entry and 'kyber' command installed."
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
log_success "Kyber Launcher installed ($VERSION)."
|
||||
echo ""
|
||||
echo " Launch: kyber (terminal) or search 'Kyber Launcher' in app menu"
|
||||
echo " AppImage: $APPIMAGE_PATH"
|
||||
echo ""
|
||||
echo " Every time you play:"
|
||||
echo " 1. Open Steam (keep running; do NOT click Play on SWBF2)"
|
||||
echo " 2. Launch Kyber → join (HOME) or host (HOST)"
|
||||
echo " 3. Kyber launches SWBF2 via its own GE-Proton — wait 1-3 min"
|
||||
echo " 4. If SWBF2 window won't focus: press Alt+Tab or click taskbar"
|
||||
echo ""
|
||||
echo " First run:"
|
||||
echo " 1. Click 'EA Account' → log in at accounts.ea.com"
|
||||
echo " 2. Click 'Skip' on Nexus Mods (optional)"
|
||||
echo " 3. EA login is cached across sessions"
|
||||
echo ""
|
||||
if [[ "$HAS_DISCRETE" == "false" ]]; then
|
||||
echo " NOTE: No discrete GPU detected. If the game crashes when a level loads,"
|
||||
echo " consider streaming via Sunshine from a machine with a real GPU instead."
|
||||
echo " Install Sunshine on the GPU machine: sudo ./setup.sh sunshine"
|
||||
echo ""
|
||||
fi
|
||||
echo " To update Kyber later, re-run: sudo ./setup.sh kyber-launcher"
|
||||
echo ""
|
||||
echo " Troubleshooting — 'Game Not Found':"
|
||||
echo " find ~/.steam/steam/steamapps -name 'starwarsbattlefrontii.exe' 2>/dev/null | head -1 | xargs dirname"
|
||||
echo " Paste that path into Kyber's SET GAME FOLDER dialog."
|
||||
echo ""
|
||||
echo " Troubleshooting — 'Origin Error: language not entitled':"
|
||||
echo " See README.md Gaming section for the registry fix."
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
#!/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/urandom | head -c "${1:-32}"; }
|
||||
|
||||
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
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────────
|
||||
register_service kyber-server gaming "Kyber dedicated server for SWBF2 (2017) — headless, no GPU required"
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
_kyber_find_swbf2() {
|
||||
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
|
||||
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() {
|
||||
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
|
||||
prompt_text "Server password (leave blank for public):" "" SERVER_PASSWORD
|
||||
|
||||
# ── 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 for a public server, or set a password to make it private
|
||||
KYBER_SERVER_PASSWORD=${SERVER_PASSWORD}
|
||||
# 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 '<rotation-json>' | 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 '<rotation-json>' | 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."
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
#!/bin/bash
|
||||
# services/sunshine.sh — Sunshine game streaming host (Moonlight-compatible).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Sunshine streams your desktop or a specific app to any Moonlight client
|
||||
# (phone, TV, another PC, etc.) with hardware-accelerated encoding and low
|
||||
# latency. Ideal for playing games on a GPU machine from a thin client.
|
||||
#
|
||||
# ── Coexistence with Wolf ──────────────────────────────────────────────────
|
||||
# Wolf and Sunshine both use Moonlight protocol ports. This installer offsets
|
||||
# Sunshine to port base 48090 (vs Wolf's default 47984) so both can run
|
||||
# simultaneously. On Moonlight clients, add the host twice — once on default
|
||||
# port (Wolf) and once on port 48090 (Sunshine).
|
||||
#
|
||||
# To switch between them if you prefer not to run both:
|
||||
# sudo systemctl stop wolf && sudo systemctl start sunshine
|
||||
# sudo systemctl stop sunshine && sudo systemctl start wolf
|
||||
#
|
||||
# ── Closed-lid laptop / headless ──────────────────────────────────────────
|
||||
# Sunshine captures a real display (Xorg/Wayland). For closed-lid use:
|
||||
# Option A: Dummy HDMI/DisplayPort plug (~$5) — simplest, most reliable
|
||||
# Option B: Virtual display via xrandr (software only, set up below)
|
||||
#
|
||||
# ── Controller input ──────────────────────────────────────────────────────
|
||||
# Moonlight sends controller input to Sunshine via uinput (virtual gamepad).
|
||||
# Works for most games; requires uinput udev rules (applied by this script).
|
||||
# Keyboard and mouse work reliably for all games including SWBF2 2017.
|
||||
#
|
||||
# ── Kyber / SWBF2 app entry ───────────────────────────────────────────────
|
||||
# This script pre-configures a "Kyber SWBF2" app entry in Sunshine so
|
||||
# Moonlight shows it as a launchable app. The Kyber AppImage must already
|
||||
# be installed (run setup-kyber-linux.sh first).
|
||||
|
||||
# ── 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() { return 0; }
|
||||
|
||||
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}'"
|
||||
}
|
||||
|
||||
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_sunshine
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────
|
||||
register_service sunshine homelab "Sunshine game streaming host for Moonlight clients (coexists with Wolf on offset ports)"
|
||||
|
||||
# ── Install function ──────────────────────────────────────────────────────
|
||||
install_sunshine() {
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ Sunshine — Game Streaming Host ║"
|
||||
echo "║ Stream to Moonlight on any device ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[DRY-RUN] Would download and install Sunshine .deb"
|
||||
echo "[DRY-RUN] Would configure uinput udev rules"
|
||||
echo "[DRY-RUN] Would write Sunshine config (offset ports for Wolf coexistence)"
|
||||
echo "[DRY-RUN] Would add Kyber SWBF2 app entry if Kyber AppImage found"
|
||||
echo "[DRY-RUN] Would optionally set up virtual display for closed-lid use"
|
||||
echo "[DRY-RUN] Would enable and start sunshine systemd service"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Detect Ubuntu version for correct .deb ────────────────────────────
|
||||
local UBUNTU_VER
|
||||
UBUNTU_VER=$(lsb_release -rs 2>/dev/null || echo "24.04")
|
||||
local DEB_DISTRO
|
||||
case "$UBUNTU_VER" in
|
||||
22*) DEB_DISTRO="ubuntu-22.04" ;;
|
||||
24*) DEB_DISTRO="ubuntu-24.04" ;;
|
||||
26*) DEB_DISTRO="ubuntu-26.04" ;;
|
||||
*) DEB_DISTRO="ubuntu-24.04" ;;
|
||||
esac
|
||||
|
||||
# ── Download and install Sunshine ─────────────────────────────────────
|
||||
if command -v sunshine &>/dev/null; then
|
||||
log_info "Sunshine already installed: $(sunshine --version 2>/dev/null || echo 'version unknown')"
|
||||
else
|
||||
log_info "Fetching latest Sunshine release for $DEB_DISTRO..."
|
||||
|
||||
local API_URL="https://api.github.com/repos/LizardByte/Sunshine/releases/latest"
|
||||
local DEB_URL
|
||||
DEB_URL=$(curl -fsSL "$API_URL" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for a in data.get('assets', []):
|
||||
name = a['name']
|
||||
if '${DEB_DISTRO}' in name and name.endswith('.deb') and 'debuginfo' not in name:
|
||||
print(a['browser_download_url'])
|
||||
break
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ -z "$DEB_URL" ]]; then
|
||||
log_error "Could not find Sunshine .deb for $DEB_DISTRO."
|
||||
log_error "Download manually from: https://github.com/LizardByte/Sunshine/releases/latest"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local DEB_FILE="/tmp/sunshine.deb"
|
||||
log_info "Downloading: $DEB_URL"
|
||||
curl -L --progress-bar -o "$DEB_FILE" "$DEB_URL"
|
||||
apt-get install -y "$DEB_FILE" || {
|
||||
log_error "Sunshine install failed."
|
||||
return 1
|
||||
}
|
||||
rm -f "$DEB_FILE"
|
||||
log_success "Sunshine installed."
|
||||
fi
|
||||
|
||||
# ── uinput udev rules (controller/mouse input from Moonlight) ─────────
|
||||
log_info "Configuring uinput for controller and mouse input..."
|
||||
cat > /etc/udev/rules.d/85-sunshine-input.rules << 'EOF'
|
||||
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
|
||||
EOF
|
||||
usermod -aG input "$ACTUAL_USER" 2>/dev/null || true
|
||||
udevadm control --reload-rules && udevadm trigger || true
|
||||
log_success "uinput configured (user added to 'input' group)."
|
||||
|
||||
# ── Sunshine config directory ─────────────────────────────────────────
|
||||
local CONF_DIR="$ACTUAL_HOME/.config/sunshine"
|
||||
mkdir -p "$CONF_DIR"
|
||||
|
||||
# ── Port config (offset from Wolf to allow coexistence) ───────────────
|
||||
log_info "Configuring Sunshine ports (offset from Wolf defaults)..."
|
||||
cat > "$CONF_DIR/sunshine.conf" << 'EOF'
|
||||
# Sunshine configuration
|
||||
# Ports offset from Wolf defaults so both can run simultaneously.
|
||||
# Wolf uses: 47984/47989/48010
|
||||
# Sunshine uses: 48090/48095/48100
|
||||
# On Moonlight clients, add this host manually with port 48090.
|
||||
port = 48090
|
||||
|
||||
# Hardware encoding — auto-detects GPU (NVIDIA NVENC, Intel QSV, AMD VCE)
|
||||
encoder = auto
|
||||
|
||||
# Logging
|
||||
log_path = /tmp/sunshine.log
|
||||
min_log_level = info
|
||||
EOF
|
||||
|
||||
# ── Kyber app entry ───────────────────────────────────────────────────
|
||||
local KYBER_PATH=""
|
||||
for p in \
|
||||
"$ACTUAL_HOME/Applications/KyberLinuxPort-x86_64.AppImage" \
|
||||
"$ACTUAL_HOME/.local/share/kyber/KyberLinuxPort.AppImage" \
|
||||
"$ACTUAL_HOME/Downloads/kyber/KyberLinuxPort.AppImage"
|
||||
do
|
||||
if [[ -f "$p" ]]; then
|
||||
KYBER_PATH="$p"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local APPS_JSON="$CONF_DIR/apps.json"
|
||||
if [[ -n "$KYBER_PATH" ]]; then
|
||||
log_info "Kyber AppImage found at $KYBER_PATH — adding app entry..."
|
||||
cat > "$APPS_JSON" << EOF
|
||||
{
|
||||
"env": {},
|
||||
"apps": [
|
||||
{
|
||||
"name": "Kyber SWBF2",
|
||||
"output": "",
|
||||
"cmd": "${KYBER_PATH}",
|
||||
"exclude-global-prep-cmd": false,
|
||||
"elevated": false,
|
||||
"auto-detach": true,
|
||||
"wait-all": true,
|
||||
"exit-timeout": 5,
|
||||
"image-path": ""
|
||||
},
|
||||
{
|
||||
"name": "Desktop",
|
||||
"output": "",
|
||||
"cmd": "",
|
||||
"exclude-global-prep-cmd": false,
|
||||
"elevated": false,
|
||||
"auto-detach": true,
|
||||
"wait-all": true,
|
||||
"exit-timeout": 5,
|
||||
"image-path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
log_success "Kyber SWBF2 app entry added."
|
||||
else
|
||||
log_warning "Kyber AppImage not found — skipping Kyber app entry."
|
||||
log_warning "Run setup-kyber-linux.sh first, then re-run this installer."
|
||||
cat > "$APPS_JSON" << 'EOF'
|
||||
{
|
||||
"env": {},
|
||||
"apps": [
|
||||
{
|
||||
"name": "Desktop",
|
||||
"output": "",
|
||||
"cmd": "",
|
||||
"exclude-global-prep-cmd": false,
|
||||
"elevated": false,
|
||||
"auto-detach": true,
|
||||
"wait-all": true,
|
||||
"exit-timeout": 5,
|
||||
"image-path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
ensure_docker_dir_ownership "$CONF_DIR"
|
||||
|
||||
# ── Virtual display for closed-lid / headless ─────────────────────────
|
||||
local VIRTUAL_DISPLAY=""
|
||||
prompt_yn "Set up virtual display for closed-lid/headless use? (y/n):" "n" VIRTUAL_DISPLAY
|
||||
if [[ "$VIRTUAL_DISPLAY" =~ ^[Yy]$ ]]; then
|
||||
log_info "Installing virtual display support..."
|
||||
apt-get install -y xserver-xorg-video-dummy x11-xserver-utils 2>/dev/null || true
|
||||
|
||||
cat > /etc/X11/xorg.conf.d/20-sunshine-virtual.conf << 'EOF'
|
||||
# Virtual display for Sunshine streaming (closed-lid / headless)
|
||||
# Provides a 1920x1080 virtual monitor so Sunshine always has a display to capture.
|
||||
Section "Device"
|
||||
Identifier "DummyDevice"
|
||||
Driver "dummy"
|
||||
VideoRam 256000
|
||||
EndSection
|
||||
|
||||
Section "Screen"
|
||||
Identifier "DummyScreen"
|
||||
Device "DummyDevice"
|
||||
DefaultDepth 24
|
||||
SubSection "Display"
|
||||
Depth 24
|
||||
Modes "1920x1080"
|
||||
EndSubSection
|
||||
EndSection
|
||||
|
||||
Section "Monitor"
|
||||
Identifier "DummyMonitor"
|
||||
HorizSync 28.0-80.0
|
||||
VertRefresh 48.0-75.0
|
||||
Modeline "1920x1080" 148.50 1920 2008 2052 2200 1080 1084 1089 1125 +hsync +vsync
|
||||
EndSection
|
||||
EOF
|
||||
log_success "Virtual display configured at 1920x1080."
|
||||
log_warning "A physical dummy HDMI plug is simpler and more reliable."
|
||||
log_warning "If you have one, plug it in and skip this virtual display config."
|
||||
fi
|
||||
|
||||
# ── Systemd service ───────────────────────────────────────────────────
|
||||
log_info "Enabling Sunshine systemd service..."
|
||||
|
||||
# Sunshine installs its own systemd user service
|
||||
sudo -u "$ACTUAL_USER" systemctl --user enable sunshine 2>/dev/null || true
|
||||
|
||||
# ── Wolf coexistence note ─────────────────────────────────────────────
|
||||
if systemctl is-enabled wolf &>/dev/null 2>&1; then
|
||||
log_warning "Wolf is installed on this machine."
|
||||
log_warning "Both can run simultaneously — Sunshine uses ports 48090-48100."
|
||||
log_warning "On Moonlight, add this host twice:"
|
||||
log_warning " Wolf: $(hostname -I | awk '{print $1}') (default port)"
|
||||
log_warning " Sunshine: $(hostname -I | awk '{print $1}'):48090"
|
||||
fi
|
||||
|
||||
# ── README ────────────────────────────────────────────────────────────
|
||||
local DIR="$DOCKER_DIR/sunshine"
|
||||
mkdir -p "$DIR"
|
||||
ensure_docker_dir_ownership "$DIR"
|
||||
|
||||
write_readme "$DIR" << 'MD'
|
||||
# Sunshine — Game Streaming Host
|
||||
|
||||
Streams your desktop or apps to any Moonlight client with hardware-accelerated
|
||||
encoding (NVIDIA NVENC / Intel QSV / AMD VCE).
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
systemctl --user start sunshine # start
|
||||
systemctl --user stop sunshine # stop
|
||||
systemctl --user restart sunshine # restart
|
||||
systemctl --user status sunshine # check status
|
||||
```
|
||||
|
||||
## Web UI
|
||||
Configure apps and settings at: https://localhost:47990
|
||||
(First run prompts you to create a username and password)
|
||||
|
||||
## Moonlight client setup
|
||||
1. Install Moonlight on the client machine:
|
||||
- Ubuntu/Debian: sudo apt install moonlight-qt
|
||||
- Or AppImage from https://moonlight-stream.org
|
||||
2. On first connection, Moonlight shows a PIN — enter it in the Sunshine web UI
|
||||
3. Sunshine is on port 48090 (offset from Wolf). Add the host manually:
|
||||
- In Moonlight: Add Host → enter IP:48090
|
||||
|
||||
## Coexistence with Wolf
|
||||
Sunshine (ports 48090-48100) and Wolf (ports 47984-47989) can run simultaneously.
|
||||
On Moonlight, add two separate host entries — one for each.
|
||||
|
||||
To run only one at a time:
|
||||
```bash
|
||||
# Switch to Sunshine
|
||||
sudo systemctl stop wolf && systemctl --user start sunshine
|
||||
|
||||
# Switch back to Wolf
|
||||
systemctl --user stop sunshine && sudo systemctl start wolf
|
||||
```
|
||||
|
||||
## Closed-lid / headless use
|
||||
A cheap HDMI/DisplayPort dummy plug is the most reliable solution.
|
||||
Plugin it into the GPU output and close the lid — Sunshine streams the
|
||||
virtual monitor the dummy plug creates.
|
||||
|
||||
Alternative: virtual display via xrandr (if xserver-xorg-video-dummy installed):
|
||||
```bash
|
||||
xrandr --addmode VIRTUAL1 1920x1080
|
||||
xrandr --output VIRTUAL1 --mode 1920x1080
|
||||
```
|
||||
|
||||
## Kyber SWBF2
|
||||
The "Kyber SWBF2" app appears in Moonlight if the Kyber AppImage was found
|
||||
at install time. Click it in Moonlight to launch Kyber on the host machine.
|
||||
If not shown, re-run: sudo ./setup.sh sunshine
|
||||
|
||||
## Keyboard and mouse
|
||||
Moonlight passes keyboard and mouse input transparently — works perfectly
|
||||
for all games including SWBF2 2017.
|
||||
|
||||
## Controller input
|
||||
Moonlight injects a virtual gamepad via uinput. Works for most games.
|
||||
SWBF2 2017 is fully playable with keyboard and mouse if controller has issues.
|
||||
MD
|
||||
|
||||
# ── Start now? ────────────────────────────────────────────────────────
|
||||
local START=""
|
||||
prompt_yn "Start Sunshine now? (y/n):" "y" START
|
||||
if [[ "$START" =~ ^[Yy]$ ]]; then
|
||||
sudo -u "$ACTUAL_USER" systemctl --user start sunshine \
|
||||
&& log_success "Sunshine started." \
|
||||
|| log_warning "Start failed — check: systemctl --user status sunshine"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_success "Sunshine installed."
|
||||
echo ""
|
||||
echo " Web UI: https://localhost:47990 (set username/password on first visit)"
|
||||
echo " Moonlight: add this host at $(hostname -I | awk '{print $1}'):48090"
|
||||
echo " Logs: /tmp/sunshine.log"
|
||||
if [[ -n "$KYBER_PATH" ]]; then
|
||||
echo " Kyber app: 'Kyber SWBF2' visible in Moonlight app list"
|
||||
fi
|
||||
echo ""
|
||||
echo "NOTE: Log out and back in (or reboot) for the 'input' group to take effect"
|
||||
echo " (required for controller and mouse input from Moonlight)."
|
||||
}
|
||||
Reference in New Issue
Block a user