Merge pull request #136 from outis1one/claude/zealous-heisenberg-8ylloi

Claude/zealous heisenberg 8ylloi
This commit is contained in:
Outis
2026-06-25 20:50:01 -04:00
committed by GitHub
5 changed files with 1107 additions and 2 deletions
+37 -2
View File
@@ -66,11 +66,11 @@ 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` |
| `gaming` | `drum-rhythm-game`, `js99er`, `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/<service>/` 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) |
@@ -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,
+30
View File
@@ -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:
+231
View File
@@ -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."
}
+416
View File
@@ -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."
}
+393
View File
@@ -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)."
}