From 8f9015b2d4bac3664409e6c6cde4ff5bfc6926e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 17:01:30 +0000 Subject: [PATCH 1/5] kyber: document registry fix for Origin language error and GPU crash workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add troubleshooting for 'Origin Error: title installed in language not entitled to play' — Maxima's umu-run reg commands fail silently on some systems; manual wine64 regedit import of Origin locale keys fixes it - Add windowed-mode / HDR fix for SWBF2 crash during level load on integrated GPUs (Intel Iris Xe, DXVK rendering crash) - Document fix in both README.md and script header comments Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs --- README.md | 35 +++++++++++++++++++++++++++++++++++ scripts/setup-kyber-linux.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/README.md b/README.md index 43fc8a2..4a6e0d2 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,31 @@ find ~/.steam/steam/steamapps -name "starwarsbattlefrontii.exe" 2>/dev/null | he ``` Paste that path into the SET GAME FOLDER dialog. +**If Origin Error: "title installed in language not entitled to play":** +Maxima's Wine prefix is missing locale registry keys — its setup commands fail silently on some systems. Fix: +```bash +cat > /tmp/swbf2_fix.reg << 'EOF' +Windows Registry Editor Version 5.00 +[HKEY_LOCAL_MACHINE\Software\Origin Games\1035052] +"locale"="en_US" +"displayname"="STAR WARS Battlefront II" +[HKEY_LOCAL_MACHINE\Software\Wow6432Node\Origin Games\1035052] +"locale"="en_US" +"displayname"="STAR WARS Battlefront II" +[HKEY_LOCAL_MACHINE\Software\Electronic Arts\EA Desktop] +"InstallSuccessful"="true" +[HKEY_LOCAL_MACHINE\Software\Origin] +"InstallSuccessful"="true" +"ClientPath"="C:\\Windows\\System32\\conhost.exe" +[HKEY_CURRENT_USER\Control Panel\International] +"Locale"="00000409" +"LocaleName"="en-US" +"sLanguage"="ENU" +EOF +WINEPREFIX=~/.local/share/maxima/wine/prefix wine64 regedit /tmp/swbf2_fix.reg +``` +Then restart Kyber and try again. + **First run (one-time setup):** 1. Click **EA Account** → log in with your EA credentials in the browser 2. Click **Skip** on Nexus Mods (optional, only needed for mods) @@ -211,6 +236,16 @@ Paste that path into the SET GAME FOLDER dialog. to `/etc/sysctl.d/99-userns.conf` to persist across reboots. Without this fix Kyber fails with: `bwrap: setting up uid map: Permission denied` +**If SWBF2 crashes immediately when a level starts loading:** +Likely a DXVK rendering issue, especially on integrated GPUs (Intel Iris Xe, etc.). +Disable fullscreen and HDR in the game settings file — the game writes this on first run: +```bash +PROFILE=$(find ~/.local/share/maxima -name "ProfileOptions_profile" 2>/dev/null | head -1) +sed -i 's/GstRender.FullscreenEnabled 1/GstRender.FullscreenEnabled 0/' "$PROFILE" +sed -i 's/GstRender.EnableHDR 1/GstRender.EnableHDR 0/' "$PROFILE" +``` +Then restart Kyber and try hosting/joining again. + **What does NOT work:** - Running the Windows `kyber_launcher.exe` under Wine/Proton: EA's auth callback uses the `eadesktop://` URI scheme which has no Linux handler, diff --git a/scripts/setup-kyber-linux.sh b/scripts/setup-kyber-linux.sh index 7759b47..5a9da8d 100755 --- a/scripts/setup-kyber-linux.sh +++ b/scripts/setup-kyber-linux.sh @@ -91,6 +91,36 @@ # kernel.unprivileged_userns_clone = 1 # kernel.apparmor_restrict_unprivileged_userns = 0 # +# ── "Origin Error: title installed in language not entitled" ─────────────── +# +# If Kyber launches SWBF2 but the game shows an Origin Error about language +# entitlement, Maxima's Wine prefix is missing the Origin locale registry keys. +# Maxima writes these via umu-run reg commands on first run, but they can fail +# silently (exit code 1) leaving the keys absent. +# +# Fix: manually import the registry keys into Maxima's Wine prefix. +# Create a file (e.g. /tmp/swbf2_fix.reg) with: +# +# Windows Registry Editor Version 5.00 +# [HKEY_LOCAL_MACHINE\Software\Origin Games\1035052] +# "locale"="en_US" +# "displayname"="STAR WARS Battlefront II" +# [HKEY_LOCAL_MACHINE\Software\Wow6432Node\Origin Games\1035052] +# "locale"="en_US" +# "displayname"="STAR WARS Battlefront II" +# [HKEY_LOCAL_MACHINE\Software\Electronic Arts\EA Desktop] +# "InstallSuccessful"="true" +# [HKEY_LOCAL_MACHINE\Software\Origin] +# "InstallSuccessful"="true" +# "ClientPath"="C:\\Windows\\System32\\conhost.exe" +# [HKEY_CURRENT_USER\Control Panel\International] +# "Locale"="00000409" +# "LocaleName"="en-US" +# "sLanguage"="ENU" +# +# Then import it: +# WINEPREFIX=~/.local/share/maxima/wine/prefix wine64 regedit /tmp/swbf2_fix.reg +# # ── "Game Not Found" dialog ──────────────────────────────────────────────── # # If Kyber shows "Game Not Found" after launching: From e1c3977d8deba21068d10dff013b6849bdccb910 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 23:20:02 +0000 Subject: [PATCH 2/5] gaming: add kyber-server service (headless SWBF2 dedicated server via Docker) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-detects SWBF2 install path; loud error with install instructions if missing - Downloads Kyber AppImage if not present, extracts kyber_cli, runs get_token (opens browser for EA OAuth — one-time step, token never expires) - Prompts for EA credentials (written to .env, chmod 600) and server name - Auto-detects GPU (NVIDIA/Intel/AMD) and adds appropriate passthrough config - Writes docker-compose.yml + .env to ~/docker/kyber-server/ - Map rotation left blank by default with instructions in .env comments Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs --- README.md | 2 +- services/kyber-server.sh | 413 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 services/kyber-server.sh diff --git a/README.md b/README.md index 4a6e0d2..4702bb0 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Update them any time with `sudo ./setup.sh configure`. | `utilities` | `actualbudget`, `ai-gpu`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | -| `gaming` | `drum-rhythm-game`, `js99er`, `minecraft`, `wolf`, `wolf-pair` | +| `gaming` | `drum-rhythm-game`, `js99er`, `kyber-server`, `minecraft`, `wolf`, `wolf-pair` | | `extras` | `kdeconnect`, `silent-send`, `sync-cc` | | `backup` | `backup` — complete recovery: entire `~/docker//` for every service via Kopia (Minecraft: flush+snap, no downtime; others: stop/snap/start for DB consistency); `borg-backup` — same coverage via Borg (chunk dedup, SSH remote repos, Borgmatic/Vorta compatible); `gaming-backup` — frequent game-save snapshots (Minecraft world data, emulator saves, Steam — no downtime, run hourly) | diff --git a/services/kyber-server.sh b/services/kyber-server.sh new file mode 100644 index 0000000..ea1b77e --- /dev/null +++ b/services/kyber-server.sh @@ -0,0 +1,413 @@ +#!/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." +} From 0b4fcef02cd7ac5716b5b3a56d5b40246c6fe9d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 23:23:25 +0000 Subject: [PATCH 3/5] kyber-server: add KYBER_SERVER_PASSWORD prompt and .env entry Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs --- services/kyber-server.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/kyber-server.sh b/services/kyber-server.sh index ea1b77e..9b45be3 100644 --- a/services/kyber-server.sh +++ b/services/kyber-server.sh @@ -299,6 +299,7 @@ for a in data.get('assets', []): 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" @@ -351,6 +352,8 @@ 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 '' | base64 From 5d063598b129f2c0cfed41363e4423ae72c5b3d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 23:56:04 +0000 Subject: [PATCH 4/5] Add Sunshine game streaming host service Installs Sunshine (LizardByte) as a Moonlight-compatible streaming host, offset to port 48090 so it coexists with Wolf. Auto-detects the Kyber AppImage and adds a "Kyber SWBF2" app entry. Optional virtual display for closed-lid/headless use. uinput udev rules for controller/mouse input. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs --- README.md | 2 +- services/sunshine.sh | 393 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 services/sunshine.sh diff --git a/README.md b/README.md index 4702bb0..3d84454 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Update them any time with `sudo ./setup.sh configure`. | Group | Services | |-------|---------| | `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo) | -| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk` | +| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk`, `sunshine` | | `utilities` | `actualbudget`, `ai-gpu`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | diff --git a/services/sunshine.sh b/services/sunshine.sh new file mode 100644 index 0000000..04a59ee --- /dev/null +++ b/services/sunshine.sh @@ -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)." +} From d355de51a278c14f238cf388b8a7f055ca5dbc3d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 00:26:02 +0000 Subject: [PATCH 5/5] Add kyber-launcher service for native Linux AppImage install Installs the Kyber community multiplayer launcher (simonlinuxcraft/kyber- linuxport-unofficial) via the setup.sh wizard. Handles glibc check, bwrap/ userns fix, GitHub release fetch, desktop entry, and bin symlink. Warns on Intel integrated graphics and links to Sunshine as the streaming alternative. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs --- README.md | 2 +- services/kyber-launcher.sh | 231 +++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 services/kyber-launcher.sh diff --git a/README.md b/README.md index 3d84454..1d435df 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Update them any time with `sudo ./setup.sh configure`. | `utilities` | `actualbudget`, `ai-gpu`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | -| `gaming` | `drum-rhythm-game`, `js99er`, `kyber-server`, `minecraft`, `wolf`, `wolf-pair` | +| `gaming` | `drum-rhythm-game`, `js99er`, `kyber-launcher`, `kyber-server`, `minecraft`, `wolf`, `wolf-pair` | | `extras` | `kdeconnect`, `silent-send`, `sync-cc` | | `backup` | `backup` — complete recovery: entire `~/docker//` for every service via Kopia (Minecraft: flush+snap, no downtime; others: stop/snap/start for DB consistency); `borg-backup` — same coverage via Borg (chunk dedup, SSH remote repos, Borgmatic/Vorta compatible); `gaming-backup` — frequent game-save snapshots (Minecraft world data, emulator saves, Steam — no downtime, run hourly) | diff --git a/services/kyber-launcher.sh b/services/kyber-launcher.sh new file mode 100644 index 0000000..6b232fd --- /dev/null +++ b/services/kyber-launcher.sh @@ -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." +}