#!/bin/bash # setup-kyber-wolf.sh — Set up the Kyber Launcher for SWBF2 (2017) inside a # Wolf / Games-on-Whales Docker container. # # Kyber is a community multiplayer client for Star Wars Battlefront II (2017, # AppID 1237950) after EA shut down the official servers. It runs as a Windows # app (Flutter/Rust) and uses an EA OAuth PKCE login flow. # # ── Why this is complicated ──────────────────────────────────────────────── # # Kyber's login flow: # 1. Kyber generates an OAuth code_verifier/code_challenge. # 2. It opens a browser pointing at accounts.ea.com with the PKCE params. # 3. The user logs in to EA. # 4. EA redirects to: qrc:/html/login_successful.html?code= # 5. Kyber's embedded WebView2 (Edge) intercepts the qrc:// URI and extracts # the authorization code. WebView2 then exchanges it for a token. # # The qrc:// scheme is a Qt internal resource protocol — it cannot be opened # by any real browser. Only Kyber's own embedded WebView2 can catch it. # This means every "redirect the browser to localhost" workaround will fail: # the final redirect from EA will always be to qrc://, which no external # browser can handle. # # The ONLY path to a working login is getting Kyber's embedded WebView2 # (msedgewebview2.exe) to initialize successfully under Proton/Wine. # # ── Why WebView2 fails in Wolf ───────────────────────────────────────────── # # Wolf containers run Proton inside Docker. Proton Experimental wraps Wine # in pressure-vessel (bwrap — bubblewrap sandbox). Inside bwrap, creating new # user namespaces is forbidden (CLONE_NEWUSER returns EPERM). msedgewebview2.exe # requires its own sandbox via --no-sandbox or equivalent flags, which under # Wine leads to CreateUserProcess failures. This causes WebView2 to fail to # initialize, and Kyber falls back to `cmd /c start `, which also fails # because bwrap blocks Wine's ShellExecute from exec'ing Linux binaries. # # ── What this script does ────────────────────────────────────────────────── # # 1. Adds Kyber Launcher as a non-Steam shortcut in the Wolf container with # the correct CompatToolMapping so it uses Proton Experimental. # 2. Installs WebView2 bootstrapper inside Kyber's Wine prefix and attempts # a WebView2 installation (may not fully work — see note above). # 3. Installs a fake cmd.exe into Kyber's prefix. When WebView2 fails and # Kyber falls back to `cmd /c start `, the fake cmd.exe writes the # OAuth URL to /tmp/kyber_oauth_url.txt and returns exit 0 instead of # crashing. This keeps Kyber's localhost:13021 listener alive. # 4. Installs Firefox (non-snap) inside the Wolf container for manual login. # 5. Creates a kyber-fresh Firefox profile with the prefs required to make # Firefox's networking work inside Docker (network.process.enabled: false). # 6. Writes a url-watcher.sh helper that monitors kyber_oauth_url.txt and # opens Firefox to the EA login page — this is the manual fallback flow. # # ── Limitations of the manual flow ──────────────────────────────────────── # # Even with the fake cmd.exe + Firefox, login cannot complete because: # - EA redirects the browser to qrc:/html/login_successful.html?code=... # - Firefox cannot handle the qrc:// scheme (it is Qt-internal) # - Firefox shows "The address wasn't understood" on the final redirect # - Kyber's localhost:13021 receives no callback and stays at "Fetching data..." # # The REAL fix is making WebView2 work. Two known approaches: # # A. Run Kyber OUTSIDE Wolf (native Linux Steam, not in the Docker container). # Without nested bwrap, WebView2 may initialize — Proton on bare metal has # more namespace capability than Proton inside Docker. # → See scripts/setup-kyber-linux.sh (if it exists) or run Kyber as a # non-Steam shortcut in your local Steam with Proton Experimental. # # B. Patch msedgewebview2.exe flags inside the container. # msedgewebview2.exe --no-sandbox disables the sandbox that requires # CLONE_NEWUSER. Getting Kyber to pass --no-sandbox to its WebView2 host # process may require a wrapper PE or a Wine registry override. # # ── Prerequisites ────────────────────────────────────────────────────────── # a. SWBF2 (AppID 1237950) is installed in the Wolf container — follow # setup-swbf2-wolf.sh first. # b. Wolf container is running and you can reach Steam in Moonlight. # c. Download Kyber Launcher from https://kyber.gg — save the .exe installer # to ~/Downloads/KyberLauncher.exe on the Docker host before running. # d. mingw-w64 must be installed on the Docker host (for fake cmd.exe): # sudo apt-get install -y mingw-w64 # # ── Usage ────────────────────────────────────────────────────────────────── # chmod +x setup-kyber-wolf.sh # ./setup-kyber-wolf.sh # # After running, to attempt Kyber login manually (partial flow): # ./url-watcher.sh # Then in Moonlight, launch Kyber and click "Login". set -euo pipefail KYBER_INSTALLER="${1:-$HOME/Downloads/KyberLauncher.exe}" KYBER_COMPAT_ID="kyber" # arbitrary prefix name for the Wine prefix KYBER_APPID="9900000001" # non-Steam shortcut appid we will assign echo "=== Kyber Launcher Setup for Wolf / Games-on-Whales ===" echo "" # ── Locate WolfSteam container ───────────────────────────────────────────── WOLF_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i WolfSteam | head -1) if [ -z "$WOLF_CONTAINER" ]; then echo "ERROR: WolfSteam container is not running." echo " Start Wolf and open Steam in Moonlight first." exit 1 fi echo "Container: $WOLF_CONTAINER" # ── Locate the Wolf session home on the host ─────────────────────────────── RETRO_HOME=$(docker inspect "$WOLF_CONTAINER" \ --format '{{range .Mounts}}{{if eq .Destination "/home/retro"}}{{.Source}}{{end}}{{end}}' \ 2>/dev/null) if [ -z "$RETRO_HOME" ] || [ ! -d "$RETRO_HOME" ]; then echo "ERROR: Could not locate /home/retro mount on the host." echo " Ensure Wolf has created a session (open Steam in Moonlight at least once)." exit 1 fi STEAM_DATA="$RETRO_HOME/.steam/steam" echo "Session home: $RETRO_HOME" # ── Verify Kyber installer ───────────────────────────────────────────────── if [ ! -f "$KYBER_INSTALLER" ]; then echo "" echo "ERROR: Kyber installer not found at: $KYBER_INSTALLER" echo "" echo "Download KyberLauncher.exe from https://kyber.gg and save it, then:" echo " $0 /path/to/KyberLauncher.exe" exit 1 fi echo "Kyber installer: $KYBER_INSTALLER" # ── Verify mingw-w64 ─────────────────────────────────────────────────────── if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then echo "mingw-w64 not found — installing..." sudo apt-get install -y mingw-w64 fi echo "" echo "[1/6] Creating Kyber Wine prefix and installing Kyber Launcher..." # ── Create kyber compatdata prefix via Proton Experimental ──────────────── KYBER_PFX="$STEAM_DATA/steamapps/compatdata/$KYBER_COMPAT_ID" mkdir -p "$KYBER_PFX" # Copy the installer into the container and run it with Proton docker cp "$KYBER_INSTALLER" "$WOLF_CONTAINER:/tmp/KyberLauncher.exe" # Find Proton Experimental path inside the container PROTON_PATH=$(docker exec "$WOLF_CONTAINER" \ bash -c "find /home/retro/.steam/steam/steamapps/common -maxdepth 2 -name 'proton' 2>/dev/null \ | grep -i 'Proton Experimental' | head -1" 2>/dev/null || true) if [ -z "$PROTON_PATH" ]; then PROTON_PATH=$(docker exec "$WOLF_CONTAINER" \ bash -c "find /home/retro/.steam/steam/steamapps/common -maxdepth 2 -name 'proton' 2>/dev/null \ | head -1" 2>/dev/null || true) fi if [ -z "$PROTON_PATH" ]; then echo "ERROR: Proton not found in the container." echo " Install Proton Experimental from Steam → Settings → Compatibility." exit 1 fi echo " Proton: $PROTON_PATH" # Run the Kyber installer silently docker exec -u 1000 \ -e STEAM_COMPAT_DATA_PATH="/home/retro/.steam/steam/steamapps/compatdata/$KYBER_COMPAT_ID" \ -e STEAM_COMPAT_CLIENT_INSTALL_PATH="/home/retro/.steam/steam" \ -e PROTON_NO_ESYNC=1 \ "$WOLF_CONTAINER" \ "$PROTON_PATH" run /tmp/KyberLauncher.exe /S 2>/dev/null || true # Give the installer time to complete sleep 5 KYBER_EXE_PATH=$(docker exec "$WOLF_CONTAINER" \ bash -c "find /home/retro/.steam/steam/steamapps/compatdata/$KYBER_COMPAT_ID/pfx \ -name 'Kyber.exe' 2>/dev/null | head -1" 2>/dev/null || true) if [ -z "$KYBER_EXE_PATH" ]; then echo "" echo "WARNING: Kyber.exe not found after installation." echo " You may need to install Kyber manually in Moonlight using a Windows:" echo " wine /tmp/KyberLauncher.exe" echo " or run the installer directly in a Windows Steam Proton session." echo "" echo " Assuming default path and continuing..." KYBER_EXE_WIN='C:\Program Files\Kyber\Kyber.exe' else # Convert Linux path to Windows path KYBER_EXE_WIN=$(echo "$KYBER_EXE_PATH" \ | sed "s|.*/compatdata/$KYBER_COMPAT_ID/pfx/drive_c/||" \ | sed 's|/|\\|g' \ | sed 's|^|C:\\|') echo " Kyber.exe: $KYBER_EXE_WIN" fi echo "" echo "[2/6] Adding Kyber as a non-Steam shortcut..." # ── Shortcuts.vdf ────────────────────────────────────────────────────────── # Non-Steam shortcuts live in userdata//config/shortcuts.vdf. # Format is binary VDF (not text). We write a minimal binary that Steam accepts. STEAM_UID=$(ls "$STEAM_DATA/userdata/" 2>/dev/null | head -1) if [ -z "$STEAM_UID" ]; then echo "WARNING: No Steam userdata found. Sign in to Steam in Moonlight first." echo " After signing in, re-run this script to register the shortcut." echo " Skipping shortcut registration." else SHORTCUTS_DIR="$STEAM_DATA/userdata/$STEAM_UID/config" SHORTCUTS_FILE="$SHORTCUTS_DIR/shortcuts.vdf" mkdir -p "$SHORTCUTS_DIR" # Generate a minimal shortcuts.vdf if it doesn't exist or back it up if [ -f "$SHORTCUTS_FILE" ]; then cp "$SHORTCUTS_FILE" "$SHORTCUTS_FILE.bak" echo " Backed up existing shortcuts.vdf" fi # Write shortcuts.vdf using Python (binary VDF format) python3 - "$SHORTCUTS_FILE" "$KYBER_COMPAT_ID" "$KYBER_EXE_WIN" << 'PYEOF' import sys, struct, os def vdf_string(key, value): return b'\x01' + key.encode() + b'\x00' + value.encode() + b'\x00' def vdf_int(key, value): return b'\x02' + key.encode() + b'\x00' + struct.pack('`. Inside Proton's bwrap # sandbox, Wine's real cmd.exe tries to exec Linux binaries via ShellExecute, # which bwrap forbids → STATUS_ACCESS_VIOLATION (0xC0000005) → Kyber shows # error and aborts the login flow. # # Solution: replace cmd.exe with a minimal Windows PE that: # 1. Finds the https:// argument (the EA OAuth URL). # 2. Writes it to Z:\tmp\kyber_oauth_url.txt (which is /tmp/ from Linux). # 3. Returns exit code 0 so Kyber does not see a failure. # # Kyber then shows "Fetching data..." and its localhost:13021 listener stays # alive. A separate watcher script reads the URL file and opens Firefox. # (See [5/6] for the watcher script and the WebView2 limitation note.) FAKE_CMD_C="/tmp/fake_cmd_$$.c" FAKE_CMD_EXE="/tmp/fake_cmd_$$.exe" cat > "$FAKE_CMD_C" << 'EOF' #include #include #include int main(int argc, char **argv) { int i; for (i = 1; i < argc; i++) { if (strncmp(argv[i], "https://", 8) == 0) { FILE *f = fopen("Z:\\tmp\\kyber_oauth_url.txt", "w"); if (f) { fputs(argv[i], f); fclose(f); } break; } } return 0; } EOF x86_64-w64-mingw32-gcc -o "$FAKE_CMD_EXE" "$FAKE_CMD_C" -mwindows 2>/dev/null \ || x86_64-w64-mingw32-gcc -o "$FAKE_CMD_EXE" "$FAKE_CMD_C" rm -f "$FAKE_CMD_C" KYBER_SYS32="$KYBER_PFX/pfx/drive_c/windows/system32" if [ -d "$KYBER_SYS32" ]; then # Back up real cmd.exe once if [ ! -f "$KYBER_SYS32/cmd.exe.bak" ] && [ -f "$KYBER_SYS32/cmd.exe" ]; then cp "$KYBER_SYS32/cmd.exe" "$KYBER_SYS32/cmd.exe.bak" echo " Backed up real cmd.exe → cmd.exe.bak" fi cp "$FAKE_CMD_EXE" "$KYBER_SYS32/cmd.exe" chown 1000:1000 "$KYBER_SYS32/cmd.exe" echo " Fake cmd.exe installed in Kyber prefix." else echo " WARNING: Kyber prefix system32 not found at $KYBER_SYS32" echo " Install Kyber first (run the installer in Wine), then re-run this script." fi rm -f "$FAKE_CMD_EXE" echo "" echo "[5/6] Installing Firefox and creating kyber-fresh profile..." # ── Install Firefox (non-snap) if not already present ───────────────────── # Firefox's socket process (which handles HTTPS in a separate process) calls # clone(CLONE_NEWUSER) which Docker blocks (EPERM). This causes all HTTPS # requests to fail. The fix is user_pref("network.process.enabled", false) # which forces networking into the main Firefox process. # # Do NOT use the snap version (can't pass prefs) or apt Firefox < 115 on # Ubuntu 22.04 (different pref structure). Use the Mozilla tarball directly. FIREFOX_TARBALL_URL="https://download.mozilla.org/?product=firefox-latest-ssl&os=linux64&lang=en-US" FIREFOX_DEST="/home/retro/firefox" FF_INSTALLED=$(docker exec "$WOLF_CONTAINER" \ bash -c "[ -x '$FIREFOX_DEST/firefox' ] && echo yes || echo no" 2>/dev/null) if [ "$FF_INSTALLED" = "yes" ]; then echo " Firefox already installed in container." else echo " Downloading Firefox (Mozilla tarball)..." FF_TMP="/tmp/firefox_latest_$$.tar.bz2" if ! curl -L --progress-bar -o "$FF_TMP" "$FIREFOX_TARBALL_URL"; then echo "ERROR: Firefox download failed. Check your internet connection." rm -f "$FF_TMP" exit 1 fi echo " Installing Firefox into container..." docker cp "$FF_TMP" "$WOLF_CONTAINER:/tmp/firefox_latest.tar.bz2" docker exec -u 1000 "$WOLF_CONTAINER" \ bash -c "mkdir -p $FIREFOX_DEST && \ tar -xjf /tmp/firefox_latest.tar.bz2 --strip-components=1 \ -C $FIREFOX_DEST && \ rm /tmp/firefox_latest.tar.bz2" rm -f "$FF_TMP" echo " Firefox installed at $FIREFOX_DEST" fi # ── Create kyber-fresh Firefox profile ──────────────────────────────────── # A completely fresh profile is required for each OAuth attempt. Stale EA # session cookies cause EA to redirect differently, breaking the flow. # This profile is deleted and recreated by url-watcher.sh before each login. PROFILE_DIR="/home/retro/.mozilla/firefox/kyber-fresh" docker exec -u 1000 "$WOLF_CONTAINER" \ bash -c "mkdir -p '$PROFILE_DIR'" docker exec -u 1000 "$WOLF_CONTAINER" \ bash -c "cat > '$PROFILE_DIR/user.js'" << 'JSEOF' // Required for Firefox to make network connections inside Docker. // Docker blocks clone(CLONE_NEWUSER). Firefox's socket process uses this // to create its own namespace — it crashes silently, killing all HTTPS. // Forcing networking to the main process avoids the crash. user_pref("network.process.enabled", false); // Reduce sandbox level to avoid other CLONE_NEWUSER failures. user_pref("security.sandbox.content.level", 0); // Do not restore a previous session — always start at a blank page. user_pref("browser.sessionstore.resume_from_crash", false); user_pref("browser.startup.page", 0); // NOTE: Do NOT add network.dns.disableIPv6 or network.trr.mode overrides. // Those break DNS resolution in the main process after network.process.enabled // is false. The defaults work correctly. JSEOF echo " Firefox kyber-fresh profile created." echo "" echo "[6/6] Writing url-watcher.sh helper script..." # ── URL watcher script ───────────────────────────────────────────────────── # This is the manual login helper. Run it from the Docker host terminal. # It watches /tmp/kyber_oauth_url.txt (written by fake cmd.exe when Kyber # tries to open the EA login URL) and opens that URL in Firefox. # # IMPORTANT LIMITATION: This only gets Firefox to the EA login page. # After the user completes EA login, EA redirects the browser to: # qrc:/html/login_successful.html?code= # Firefox cannot handle qrc:// (it is a Qt-internal resource scheme). # Firefox shows "The address wasn't understood" — this is correct behavior. # # The auth code is only delivered to Kyber's embedded WebView2 via the qrc:// # intercept. Without a working WebView2, the login flow cannot complete. # See the header comments for the real fix (run Kyber outside Wolf, or patch # msedgewebview2.exe --no-sandbox flags). WATCHER_SCRIPT="$(dirname "$0")/kyber-url-watcher.sh" cat > "$WATCHER_SCRIPT" << WATCHEOF #!/bin/bash # kyber-url-watcher.sh — Manual Kyber OAuth helper. # # Run this from the Docker host terminal BEFORE clicking Login in Kyber. # It waits for fake cmd.exe to write the EA OAuth URL, then opens Firefox. # # IMPORTANT: This can get you to the EA login page but cannot complete login. # The final EA redirect goes to qrc:// which only Kyber's WebView2 can catch. # See setup-kyber-wolf.sh comments for details and the real fix. # # Usage: ./kyber-url-watcher.sh WOLF_CONTAINER=\$(docker ps --format '{{.Names}}' | grep -i WolfSteam | head -1) if [ -z "\$WOLF_CONTAINER" ]; then echo "ERROR: WolfSteam container is not running." exit 1 fi echo "Waiting for Kyber to open the EA login URL..." echo "(Click Login in Kyber now)" echo "" docker exec -u 1000 \\ -e DISPLAY=:0 \\ -e WAYLAND_DISPLAY=wayland-2 \\ -e XDG_RUNTIME_DIR=/run/user/wolf \\ "\$WOLF_CONTAINER" bash -c ' # Always start with a completely fresh profile — stale EA cookies cause # different redirect behavior that breaks the flow. rm -rf /home/retro/.mozilla/firefox/kyber-fresh mkdir -p /home/retro/.mozilla/firefox/kyber-fresh cat > /home/retro/.mozilla/firefox/kyber-fresh/user.js << '"'"'JSEOF'"'"' user_pref("network.process.enabled", false); user_pref("security.sandbox.content.level", 0); user_pref("browser.sessionstore.resume_from_crash", false); user_pref("browser.startup.page", 0); JSEOF rm -f /tmp/kyber_oauth_url.txt echo "Watching for /tmp/kyber_oauth_url.txt ..." while [ ! -s /tmp/kyber_oauth_url.txt ]; do sleep 0.1; done # Strip any Windows carriage returns the C fopen() might have added URL=\$(tr -d "\r\n" < /tmp/kyber_oauth_url.txt) echo "Got URL: \$URL" echo "" echo "Opening Firefox..." echo "(After EA login Firefox will show '\''The address wasn'\''t understood'\'' — this" echo " is expected. The qrc:// redirect can only be caught by Kyber'\''s WebView2.)" /home/retro/firefox/firefox \\ --profile /home/retro/.mozilla/firefox/kyber-fresh "\$URL" ' WATCHEOF chmod +x "$WATCHER_SCRIPT" echo " Written: $WATCHER_SCRIPT" echo "" echo "=== Kyber Setup Complete ===" echo "" echo "── How to attempt login (manual fallback flow) ──────────────────────" echo "" echo " Terminal 1 (before clicking Login in Kyber):" echo " $(dirname "$0")/kyber-url-watcher.sh" echo "" echo " Then in Moonlight: launch Kyber → click Login" echo " Firefox opens the EA login page — complete the login." echo "" echo "── Known limitation ─────────────────────────────────────────────────" echo "" echo " After EA login, EA redirects to qrc:/html/login_successful.html?code=..." echo " Firefox cannot open qrc:// (Qt-internal scheme). It shows:" echo " 'The address wasn't understood'" echo " This is the expected behavior. The auth code is only deliverable to" echo " Kyber's embedded WebView2 — which cannot initialize inside Docker" echo " due to bubblewrap (bwrap) blocking user namespace creation." echo "" echo "── Real fix: run Kyber outside Wolf ─────────────────────────────────" echo "" echo " Since SWBF2 already runs with Proton Experimental on native Linux Steam," echo " add Kyber Launcher as a non-Steam shortcut in your LOCAL Steam and use" echo " Proton Experimental. Outside Docker, Proton runs without the bwrap" echo " restrictions that block WebView2 — login is likely to work." echo "" echo " Steps:" echo " 1. In Steam → Library → + (bottom left) → Add a Non-Steam Game" echo " 2. Browse to KyberLauncher.exe (or the installed Kyber.exe)" echo " 3. Right-click → Properties → Compatibility → Proton Experimental" echo " 4. Launch — WebView2 should initialize and the login flow completes" echo "" echo "── To restore real cmd.exe (undo fake cmd.exe) ──────────────────────" echo "" CMD_BAK="$KYBER_SYS32/cmd.exe.bak" echo " cp '$CMD_BAK' '${CMD_BAK%.bak}'" echo ""