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

Claude/zealous heisenberg 8ylloi
This commit is contained in:
Outis
2026-06-22 08:48:43 -04:00
committed by GitHub
2 changed files with 505 additions and 180 deletions
+244 -179
View File
@@ -1,248 +1,311 @@
# Star Wars Battlefront II (2017) — Wolf/Moonlight Setup
**AppID:** 1237950
**Proton:** GE-Proton10-34 (required — stock Proton does not work with EA App)
**Proton:** GE-Proton10-34 (required)
**Platform:** Wolf (Games-on-Whales) — WolfSteam Docker container
---
## Why this is hard
## Prerequisites
SWBF2 (2017) is an EA game. EA mandated their **EA App** launcher for all EA Steam titles.
On launch, SWBF2 calls `ShellExecuteW("link2ea://launchgame/1237950?platform=steam&theme=swbfii")`.
GE-Proton's `steam.exe` intercepts that URL, finds `Link2EA.exe` from the EA Desktop install, and
hands off to the EA App which then launches the actual game.
Without EA Desktop installed in the Wine prefix, the game exits immediately.
- SWBF2 installed via Steam in the WolfSteam session
- GE-Proton10-34 installed (Steam → Settings → Compatibility → Enable Steam Play)
- EA account created at ea.com and linked to your Steam account
- `msitools` on the Docker host: `sudo apt-get install -y msitools`
---
## The bwrap problem (why you can't just docker exec)
## The EA dependency problem
SWBF2 (2017) requires EA's launcher infrastructure — even for single-player offline:
- **Steam** handles payment, library, and game files
- **EA App** handles identity and authentication
First launch requires internet + EA account. After that, EA App caches credentials
locally and subsequent launches work offline — until the token expires or EA's servers
shut down permanently.
**If/when EA shuts down:**
- Community private server projects (like Kyber) may replace EA's infrastructure
- SWBF2 (2005, AppID 6060) is the zero-dependency alternative — no accounts, no
launchers, active community servers, runs perfectly through Proton
**Migrating to another machine:**
```bash
# Back up the Wine prefix — EA credentials travel with it
tar -czf swbf2-prefix-backup.tar.gz \
/home/retro/.steam/steam/steamapps/compatdata/1237950/
```
Tokens last weeks to months before requiring re-login.
---
## Why docker exec can't run Wine/Proton
The WolfSteam container runs Steam as uid 1000 (`retro` inside container).
`docker exec` carries high Linux capabilities (`CapEff ≈ 0xa8ac35fb`).
Steam processes have `CapEff=0`.
When a high-cap shell tries to run bwrap (used by Steam's sniper/pressure-vessel runtime),
bwrap attempts a privileged uid-map path that fails:
Steam processes run with `CapEff=0`. When a high-cap process tries to run bwrap
(Steam's sniper/pressure-vessel sandbox), bwrap tries a privileged uid-map path:
```
setting up uid map: Permission denied
```
This means you **cannot** run Proton or wine64 via `docker exec` — bwrap always fails.
Only Steam itself can properly launch programs through sniper+GE-Proton.
**Only Steam itself can launch programs through sniper+GE-Proton.** All Wine
operations must go through Steam's launch chain — hence the wrapper approach.
---
## Solution: Steam launch wrapper
## How it works
Use SWBF2's **Steam launch options** to intercept the launch, run a helper script,
then pass control back to Steam's real launch chain.
Steam passes a `link2ea://` URL as the "game executable" to GE-Proton. The wrapper
intercepts this, imports registry fixes, then passes the original URL through.
GE-Proton's `steam.exe` handles `link2ea://`, finds `Link2EA.exe` via the registry,
runs it, and EA Desktop authenticates the session before launching the game.
Steam passes all its args to the wrapper as `$@`:
```
steam-launch-wrapper -- reaper SteamLaunch AppId=1237950 -- <sniper> --verb=waitforexitandrun -- <proton> waitforexitandrun <game>
Steamwrapper regedit (registry fixes) → exec original args
→ GE-Proton steam.exe → link2ea:// → Link2EA.exe → EA Desktop → SWBF2
```
The wrapper runs msiexec (or anything else) inside the full sniper+GE-Proton environment
that Steam set up, then `exec "$@"` to hand off to the game.
---
## Step-by-step
### 1. Lock localconfig.vdf against Steam overwriting it
### 1. Trigger Wine prefix creation
Steam uses atomic rename (write temp + rename) when saving config.
`chmod 444` on the file is bypassed. Lock the **directory** instead:
SWBF2 must be launched at least once so Steam creates the Wine prefix and drops
`ea_app.msi` into it. Launch SWBF2 from Moonlight, wait ~10 seconds, then close it
(the "Origin is not installed" error is expected at this point).
Verify the MSI appeared:
```bash
WOLF_CONTAINER=$(docker ps --format '{{.Names}}' | grep Wolf)
docker exec "$WOLF_CONTAINER" ls -lh \
/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/ea_app.msi
```
Expected: ~227 MB file.
### 2. Extract EA Desktop files on the host
The MSI's `JunoConfigureRegistry` .NET custom action fails in Wine (it uses
`SetSecurityDescriptorSddlForm` for registry ACLs — unsupported in Wine). This
causes the entire install to roll back. Extract files on the host instead:
```bash
WOLF_CONTAINER="WolfSteam_XXXXXXXXXXXXXXXX" # docker ps to find exact name
CONFIG_DIR="/home/retro/.steam/steam/userdata/XXXXXXXXX/config"
WOLF_CONTAINER=$(docker ps --format '{{.Names}}' | grep Wolf)
# Find container name
docker ps --format '{{.Names}}' | grep Wolf
docker cp "$WOLF_CONTAINER:/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/ea_app.msi" \
/tmp/ea_app.msi
# Find Steam user ID
docker exec "$WOLF_CONTAINER" ls /home/retro/.steam/steam/userdata/
docker exec "$WOLF_CONTAINER" chmod 555 "$CONFIG_DIR"
sudo apt-get install -y msitools
mkdir -p /tmp/ea_app_extracted
msiextract -C /tmp/ea_app_extracted /tmp/ea_app.msi
```
### 2. Set SWBF2 launch options
### 3. Copy EA Desktop files into the Wine prefix
Edit localconfig.vdf **while Steam is not running** (or kill Steam first):
The MSI installs EA Desktop under a versioned directory with a symlink.
The version (e.g. `14.2.0.3345`) is whatever the extracted MSI contains:
```bash
EA_VERSION=$(ls /tmp/ea_app_extracted/Electronic\ Arts/EA\ Desktop/ | head -1)
# Usually "EA Desktop" — this is the versioned dir, renamed during real install
# Use the version from the failed-install symlink if present:
SYMLINK_TARGET=$(docker exec "$WOLF_CONTAINER" readlink \
"/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop" \
2>/dev/null || echo "14.2.0.3345")
PFXBASE="/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts/EA Desktop"
docker exec "$WOLF_CONTAINER" bash -c "
rm -f '$PFXBASE/EA Desktop'
mkdir -p '$PFXBASE/$SYMLINK_TARGET'
ln -s '$SYMLINK_TARGET' '$PFXBASE/EA Desktop'
"
docker cp "/tmp/ea_app_extracted/Electronic Arts/EA Desktop/EA Desktop/." \
"$WOLF_CONTAINER:$PFXBASE/$SYMLINK_TARGET/"
# Verify
docker exec "$WOLF_CONTAINER" find "$PFXBASE" -name "Link2EA.exe"
```
### 4. Write registry fix files to drive_c
These are imported into the Wine registry on each launch via the wrapper.
Writing to drive_c ensures they survive wineserver restarts (unlike direct
system.reg edits which get overwritten).
```bash
PFXC="$WOLF_CONTAINER:/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c"
# link2ea:// protocol handler — tells Wine how to run Link2EA.exe
cat > /tmp/link2ea_fix.reg << 'EOF'
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\link2ea]
@="URL:link2ea Protocol"
"URL Protocol"=""
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\link2ea\shell\open\command]
@="\"C:\\Program Files\\Electronic Arts\\EA Desktop\\EA Desktop\\Link2EA.exe\" \"%1\""
EOF
# EA Desktop Windows services — EALocalHostSvc provides local IPC that
# Link2EA.exe needs; without it launch fails with RPC_S_SERVER_UNAVAILABLE
cat > /tmp/ea_services.reg << 'EOF'
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EALocalHostSvc]
"Type"=dword:00000010
"Start"=dword:00000002
"ErrorControl"=dword:00000001
"ImagePath"="C:\\Program Files\\Electronic Arts\\EA Desktop\\EA Desktop\\EALocalHostSvc.exe"
"DisplayName"="EA Local Host Service"
"ObjectName"="LocalSystem"
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EABackgroundService]
"Type"=dword:00000010
"Start"=dword:00000002
"ErrorControl"=dword:00000001
"ImagePath"="C:\\Program Files\\Electronic Arts\\EA Desktop\\EA Desktop\\EABackgroundService.exe"
"DisplayName"="EA Background Service"
"ObjectName"="LocalSystem"
EOF
docker cp /tmp/link2ea_fix.reg "$PFXC/link2ea_fix.reg"
docker cp /tmp/ea_services.reg "$PFXC/ea_services.reg"
```
### 5. Lock localconfig.vdf and set launch options
Steam overwrites localconfig.vdf on shutdown via atomic rename (temp file +
rename). `chmod 444` on the file is bypassed. Lock the **directory** instead:
```bash
STEAM_UID=$(docker exec "$WOLF_CONTAINER" ls /home/retro/.steam/steam/userdata/)
CONFIG_DIR="/home/retro/.steam/steam/userdata/$STEAM_UID/config"
CONFIG_FILE="$CONFIG_DIR/localconfig.vdf"
# Kill Steam so it flushes and stops watching
# Stop Steam so it flushes before we edit
docker exec "$WOLF_CONTAINER" pkill -f steam.sh || true
sleep 3
sleep 5
# Edit — find the LaunchOptions line for AppID 1237950
# Add before it if missing:
docker exec "$WOLF_CONTAINER" bash -c "
sed -i '/\"1237950\"/,/}/ s/\"LaunchOptions\"\s*\"[^\"]*\"/\"LaunchOptions\"\t\t\"STEAM_UNIX_SOCKET=\/tmp\/steam.sock \/home\/retro\/ea_install.sh %command%\"/' '$CONFIG_FILE'
"
# Or if LaunchOptions key doesn't exist yet, add it manually.
# Verify:
docker exec "$WOLF_CONTAINER" grep -A5 '"1237950"' "$CONFIG_FILE" | grep LaunchOptions
# Lock directory
docker exec "$WOLF_CONTAINER" chmod 555 "$CONFIG_DIR"
# Add launch options for AppID 1237950
# Find the 1237950 block and insert/replace LaunchOptions
docker exec "$WOLF_CONTAINER" python3 - "$CONFIG_FILE" << 'PYEOF'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
launch_opt = 'STEAM_UNIX_SOCKET=/tmp/steam.sock /home/retro/ea_install.sh %command%'
# Replace existing LaunchOptions in 1237950 block if present
new = re.sub(
r'("1237950".*?"LaunchOptions"\s*)"[^"]*"',
rf'\1"{launch_opt}"',
content, flags=re.DOTALL
)
if new == content:
# LaunchOptions key missing — add it after the "1237950" line
new = re.sub(
r'("1237950"\s*\n\s*\{)',
rf'\1\n\t\t\t\t"LaunchOptions"\t\t"{launch_opt}"',
content
)
with open(path, 'w') as f:
f.write(new)
print("Done")
PYEOF
# Verify
docker exec "$WOLF_CONTAINER" grep -A3 '"1237950"' "$CONFIG_FILE" | grep -i launch
```
### 3. Write the EA install wrapper
The wrapper runs `msiexec /a` (administrative install — no custom actions) to extract
EA Desktop files, then execs the real game:
### 6. Write the wrapper script
```bash
cat > /tmp/ea_install.sh << 'EOF'
#!/bin/bash
echo "=== ea_install.sh called $(date) ===" >> /tmp/ea_install.log
echo "Args: $@" >> /tmp/ea_install.log
"$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" \
msiexec /i "C:\\ea_app.msi" /qn \
TARGETDIR="C:\\Program Files\\Electronic Arts" \
/l*v "C:\\msi_admin.log" \
>> /tmp/ea_install.log 2>&1
echo "MSI exit: $?" >> /tmp/ea_install.log
echo "=== launch $(date) ===" >> /tmp/ea_install.log
# Import registry fixes on every launch (survives wineserver restarts)
"$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" regedit /S "C:\\link2ea_fix.reg"
"$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" regedit /S "C:\\ea_services.reg"
# Pass original args through — Steam's link2ea:// URL goes to GE-Proton → Link2EA.exe
exec "$@"
EOF
chmod +x /tmp/ea_install.sh
docker cp /tmp/ea_install.sh "$WOLF_CONTAINER":/home/retro/ea_install.sh
```
### 4. Get the EA App MSI
### 7. First launch
The MSI ships inside SWBF2's game files. Copy it into the Wine prefix:
1. Reconnect to Moonlight and launch SWBF2
2. **Let Vulkan shaders compile** — do not skip, takes several minutes, only happens once
3. EA App authenticates via your linked Steam/EA account (no manual login if accounts are linked)
4. Game loads
```bash
MSI_SRC="/home/retro/.steam/steam/steamapps/common/STAR WARS Battlefront II/__Installer/Origin/redist/internal/EAappInstaller.exe"
# EAappInstaller.exe is a WiX Burn bootstrapper — the real MSI is buried inside.
# Easier: use the MSI that gets extracted to the Wine prefix during a first launch attempt:
MSI_IN_PREFIX="/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/ea_app.msi"
```
---
The MSI appears in the Wine prefix (`drive_c/ea_app.msi`, ~227 MB) after SWBF2 is launched
once — GE-Proton's protonfixes or steam.exe drops it there.
## Is this fragile? Will it work on a fresh install?
### 5. Extract EA Desktop files on the host (bypassing Wine custom actions)
**Honest assessment:**
The MSI's `JunoConfigureRegistry` custom action fails in Wine (it tries to set registry
ACLs via `SetSecurityDescriptorSddlForm` which Wine doesn't support). This causes
MSI rollback and removes all installed files. Bypass it by extracting on Linux:
| Component | Fresh install safe? | Notes |
|-----------|-------------------|-------|
| EA Desktop files in Wine prefix | ✅ | Extracted from MSI in game files |
| Registry fixes (.reg files) | ✅ | Imported on every launch via wrapper |
| Launch options in localconfig.vdf | ✅ | Set by script |
| Config dir lock (chmod 555) | ✅ | Set by script |
| ea_app.msi in drive_c | ⚠️ | Requires one failed launch first to appear |
| EA account link | ⚠️ | One-time manual step |
| Vulkan shader cache | ⚠️ | Recompiles on fresh prefix — takes minutes |
| EA token in Wine prefix | ⚠️ | Requires login if prefix deleted or token expired |
```bash
# Copy MSI out of container
docker cp "$WOLF_CONTAINER:/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/ea_app.msi" \
/tmp/ea_app.msi
**The catch:** The Wine prefix (`compatdata/1237950/`) is recreated from scratch if deleted.
All file copies and registry entries live inside it. A fresh install needs all steps run again
— but the automation handles that.
# Extract (msitools package)
sudo apt-get install -y msitools
mkdir -p /tmp/ea_app_extracted
msiextract -C /tmp/ea_app_extracted /tmp/ea_app.msi
```
### 6. Copy EA Desktop into the Wine prefix
The MSI installs to `C:\Program Files\Electronic Arts\EA Desktop\` with a versioned
subdirectory (`14.2.0.3345`) and a symlink `EA Desktop -> 14.2.0.3345`.
The normal MSI install creates that symlink via `JunoSetupSymlinkMode`. After a
failed install + rollback, the symlink may remain but the versioned dir is gone.
```bash
PFXBASE="/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts/EA Desktop"
# Remove any broken symlink
docker exec "$WOLF_CONTAINER" bash -c "
[ -L '$PFXBASE/EA Desktop' ] && rm '$PFXBASE/EA Desktop'
mkdir -p '$PFXBASE/14.2.0.3345'
ln -s 14.2.0.3345 '$PFXBASE/EA Desktop'
"
# Copy extracted files into versioned directory
docker cp "/tmp/ea_app_extracted/Electronic Arts/EA Desktop/EA Desktop/." \
"$WOLF_CONTAINER:$PFXBASE/14.2.0.3345/"
# Verify
docker exec "$WOLF_CONTAINER" find "$PFXBASE" -name "Link2EA.exe"
```
Expected output:
```
/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts/EA Desktop/14.2.0.3345/Link2EA.exe
```
### 7. Switch wrapper to pass-through and launch
Once EA Desktop files are in place, update the wrapper to just exec the game:
```bash
cat > /tmp/ea_passthrough.sh << 'EOF'
#!/bin/bash
echo "=== launch $(date) ===" >> /tmp/ea_install.log
exec "$@"
EOF
chmod +x /tmp/ea_passthrough.sh
docker cp /tmp/ea_passthrough.sh "$WOLF_CONTAINER":/home/retro/ea_install.sh
```
Launch SWBF2 in Moonlight. Steam → GE-Proton → SWBF2 → link2ea:// → steam.exe
finds Link2EA.exe → EA App launches → SWBF2 loads.
### 8. EA account login (one-time)
On first launch, EA Desktop will show a login screen. Log in with your EA account.
Credentials are cached; subsequent launches skip login.
### 9. Restore clean state
After EA Desktop is confirmed working, remove the launch wrapper:
```bash
# Restore directory to writable
docker exec "$WOLF_CONTAINER" chmod 755 "$CONFIG_DIR"
# Remove LaunchOptions from localconfig.vdf
# (edit the file while Steam is stopped, remove the LaunchOptions line for 1237950)
```
**The one irreducible manual step:** Linking your EA account to Steam (done once at ea.com,
persists across reinstalls via EA's servers).
---
## Troubleshooting
### "link2ea not found" / game exits in ~6 seconds
EA Desktop files are missing or in wrong path. Check:
### "Origin is not installed" popup
The game binary checks for Origin before calling link2ea://. This happens when:
- The wrapper didn't run (check launch options are set)
- The wrapper ran but regedit failed (check `tail /tmp/ea_install.log`)
### `link2ea://` fails with ret 31 (SE_ERR_NOASSOC)
`link2ea_fix.reg` wasn't imported. Check drive_c has the file and wrapper ran both regedits.
### RPC_S_SERVER_UNAVAILABLE (0x800706ba) in wine log
`ea_services.reg` wasn't imported or EALocalHostSvc failed to register. Check wrapper log.
### Game exits immediately, no popup, no log
Wrapper isn't being called. Config dir may not be chmod 555 or launch options missing.
```bash
docker exec "$WOLF_CONTAINER" find \
"/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts" \
-name "Link2EA.exe"
docker exec "$WOLF_CONTAINER" stat "$CONFIG_DIR" | grep Access
docker exec "$WOLF_CONTAINER" grep -A5 '"1237950"' "$CONFIG_FILE" | grep -i launch
```
### Wrapper never runs (Steam ignores launch options)
Steam wrote config before chmod 555 was applied. Kill Steam, chmod 555 the directory,
then edit localconfig.vdf. Steam reads config fresh on startup.
### INST-14-1603 error in EA App installer
`JunoConfigureRegistry` .NET custom action returns 0 in Wine. This is a Wine limitation
with registry ACL operations. Use the `msiextract` bypass (Step 56) instead of
running the MSI through Wine.
### msiexec DISABLEROLLBACK=1 produces empty log
MSI detected a stale product registration from a previous failed install.
Check: `grep -i C2622085 /home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/system.reg`
If present, delete those registry keys then retry.
### wine64 segfaults outside Steam (exit 139)
GE-Proton10's wine64 requires the sniper runtime. Never call it directly from
`docker exec` — always use Steam's launch chain via the wrapper approach.
### localconfig.vdf gets overwritten on Steam shutdown
Steam uses atomic rename (temp file + rename into place). `chmod 444` on the file
is bypassed by the rename. Solution: `chmod 555` on the **directory** containing
localconfig.vdf — this prevents new files being renamed into the directory.
### Collecting logs after a failure
```bash
docker exec "$WOLF_CONTAINER" bash -c "
tail -20 /tmp/ea_install.log
echo '---'
grep -E '(err:|link2ea|origin|RPC)' /home/retro/steam-1237950.log | tail -20
"
```
---
@@ -251,9 +314,11 @@ localconfig.vdf — this prevents new files being renamed into the directory.
| Path | Description |
|------|-------------|
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/` | SWBF2 Wine prefix |
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/system.reg` | Wine HKLM registry |
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/ea_app.msi` | EA App MSI (~227 MB) |
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts/EA Desktop/14.2.0.3345/` | EA Desktop files |
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop` | Symlink → `14.2.0.3345` |
| `/home/retro/.steam/steam/userdata/<uid>/config/localconfig.vdf` | Steam per-user config (launch options) |
| `/home/retro/ea_install.sh` | Launch wrapper script |
| `/tmp/ea_install.log` | Wrapper log (inside container) |
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/link2ea_fix.reg` | link2ea protocol handler reg |
| `/home/retro/.steam/steam/steamapps/compatdata/1237950/pfx/drive_c/ea_services.reg` | EA service registrations |
| `/home/retro/.steam/steam/steamapps/common/STAR WARS Battlefront II/` | Game files |
| `/home/retro/.steam/steam/userdata/<uid>/config/localconfig.vdf` | Steam launch options |
| `/home/retro/ea_install.sh` | Launch wrapper |
| `/tmp/ea_install.log` | Wrapper log (resets on container restart) |
+261 -1
View File
@@ -2065,6 +2065,265 @@ PYEOF
echo "Expected path: $_dx_log"
fi
;;
setup-swbf2)
# Full automated setup for Star Wars Battlefront II (2017, AppID 1237950)
# on Wolf/Moonlight with GE-Proton. Implements the msiextract bypass for
# the JunoConfigureRegistry Wine incompatibility, installs EA Desktop files
# into the Wine prefix, and wires up a launch wrapper so registry fixes
# survive wineserver restarts.
#
# Prerequisites (do these first, then run this command):
# 1. Wolf is running and Steam is open in Moonlight
# 2. GE-Proton10-34+ is installed (./manage.sh ge-proton)
# 3. SWBF2 (AppID 1237950) is set to use GE-Proton in Steam → Compatibility
# 4. msitools is installed on the host (sudo apt-get install -y msitools)
# 5. Your EA account is linked to your Steam account at ea.com (one-time manual step)
# 6. Launch SWBF2 once from Moonlight, wait ~10s for the "Origin is not installed"
# error, then close it — this triggers Wine prefix creation and drops ea_app.msi
#
# After this command completes: launch SWBF2 from Moonlight. Let Vulkan
# shaders compile on first run (takes several minutes, only once). EA App
# authenticates via your linked Steam/EA account and the game launches.
#
# Usage: ./manage.sh setup-swbf2
_sw_appid="1237950"
_sw_sh=$(_steam_home)
if [ -z "$_sw_sh" ]; then
echo "Steam home not found. Start Wolf (./manage.sh start) and open Steam in Moonlight first."
exit 1
fi
_sw_container=$(docker ps --format '{{.Names}}' | grep -i WolfSteam | head -1)
if [ -z "$_sw_container" ]; then
echo "WolfSteam container is not running. Start Wolf: ./manage.sh start"
exit 1
fi
_sw_pfxc="$_sw_sh/.steam/steam/steamapps/compatdata/$_sw_appid/pfx/drive_c"
_sw_msi="$_sw_pfxc/ea_app.msi"
echo "=== SWBF2 (2017) Wolf/Moonlight Setup ==="
echo ""
# ── Step 1: verify Wine prefix exists ──────────────────────────────────
if [ ! -d "$_sw_pfxc" ]; then
echo "ERROR: Wine prefix not found at $_sw_pfxc"
echo ""
echo "Required setup before running this command:"
echo " 1. In Steam (via Moonlight): right-click SWBF2 → Properties → Compatibility"
echo " → Force GE-Proton10-34 (or later)"
echo " 2. Click Play on SWBF2 — wait ~10 seconds for 'Origin is not installed' error"
echo " 3. Close the error and return here"
exit 1
fi
echo "[1/6] Wine prefix found: OK"
# ── Step 2: check ea_app.msi exists ────────────────────────────────────
if [ ! -f "$_sw_msi" ]; then
echo ""
echo "ERROR: ea_app.msi not found at $_sw_msi"
echo ""
echo "SWBF2 must be launched once so Steam drops ea_app.msi into the Wine prefix."
echo " 1. Open Steam in Moonlight"
echo " 2. Click Play on SWBF2"
echo " 3. Wait ~10 seconds — you will see an 'Origin is not installed' error"
echo " 4. Close the error popup"
echo " 5. Re-run: ./manage.sh setup-swbf2"
echo ""
echo "Expected file size: ~227 MB"
exit 1
fi
_sw_msi_size=$(stat -c%s "$_sw_msi" 2>/dev/null || echo 0)
if [ "$_sw_msi_size" -lt 50000000 ]; then
echo "WARNING: ea_app.msi looks too small ($_sw_msi_size bytes, expected ~227 MB)."
echo "It may still be copying. Try again in a moment."
exit 1
fi
echo "[2/6] ea_app.msi found ($(( _sw_msi_size / 1048576 )) MB): OK"
# ── Step 3: extract MSI on the host (bypasses JunoConfigureRegistry) ──
echo "[3/6] Extracting EA Desktop files from MSI (this takes ~30s)..."
if ! command -v msiextract >/dev/null 2>&1; then
echo "ERROR: msitools not installed. Run: sudo apt-get install -y msitools"
exit 1
fi
_sw_extract_dir="/tmp/ea_app_extracted_$$"
rm -rf "$_sw_extract_dir"
mkdir -p "$_sw_extract_dir"
if ! msiextract -C "$_sw_extract_dir" "$_sw_msi" >/dev/null 2>&1; then
echo "ERROR: msiextract failed. Check that msitools is properly installed."
rm -rf "$_sw_extract_dir"
exit 1
fi
_sw_ea_src=$(find "$_sw_extract_dir" -maxdepth 4 \
-path "*/Electronic Arts/EA Desktop/EA Desktop" -type d 2>/dev/null | head -1)
if [ -z "$_sw_ea_src" ] || [ ! -f "$_sw_ea_src/Link2EA.exe" ]; then
echo "ERROR: Link2EA.exe not found after extraction. MSI structure may have changed."
echo "Expected: <extracted>/Electronic Arts/EA Desktop/EA Desktop/Link2EA.exe"
ls -la "$_sw_extract_dir/Electronic Arts/EA Desktop/" 2>/dev/null || true
rm -rf "$_sw_extract_dir"
exit 1
fi
echo " Extracted to: $_sw_ea_src"
echo " $(ls "$_sw_ea_src" | wc -l) files found including Link2EA.exe"
# ── Step 4: copy EA Desktop files into Wine prefix ─────────────────────
echo "[4/6] Installing EA Desktop files into Wine prefix..."
_sw_ea_dest_base="$_sw_pfxc/Program Files/Electronic Arts/EA Desktop"
# Determine the version directory name from the extracted content.
# The MSI extracts to "EA Desktop" but the real install uses a versioned dir
# with a symlink. Use the same version string if a broken symlink exists from
# a failed install attempt; otherwise default to 14.2.0.3345.
_sw_ea_version=$( \
docker exec "$_sw_container" \
readlink "/home/retro/.steam/steam/steamapps/compatdata/$_sw_appid/pfx/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop" \
2>/dev/null || echo "14.2.0.3345")
_sw_ea_dest="$_sw_ea_dest_base/$_sw_ea_version"
sudo mkdir -p "$_sw_ea_dest"
sudo cp -r "$_sw_ea_src/." "$_sw_ea_dest/"
sudo chown -R 1000:1000 "$_sw_ea_dest_base"
# Remove any broken symlink, then create a clean one
sudo rm -f "$_sw_ea_dest_base/EA Desktop"
docker exec "$_sw_container" bash -c \
"ln -sf '$_sw_ea_version' '/home/retro/.steam/steam/steamapps/compatdata/$_sw_appid/pfx/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop'" \
2>/dev/null || sudo ln -sf "$_sw_ea_version" "$_sw_ea_dest_base/EA Desktop"
if [ ! -f "$_sw_ea_dest/Link2EA.exe" ]; then
echo "ERROR: Copy failed — Link2EA.exe not found at $_sw_ea_dest/Link2EA.exe"
rm -rf "$_sw_extract_dir"
exit 1
fi
echo " EA Desktop installed to: $( echo "$_sw_ea_dest" | sed "s|$_sw_sh||" )"
rm -rf "$_sw_extract_dir"
# ── Step 5: write .reg files and wrapper script into Wine prefix ────────
echo "[5/6] Writing registry fix files and launch wrapper..."
_sw_drive_c="$_sw_pfxc"
# link2ea:// protocol handler — points Wine at Link2EA.exe
sudo tee "$_sw_drive_c/link2ea_fix.reg" > /dev/null << 'REGEOF'
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\link2ea]
@="URL:link2ea Protocol"
"URL Protocol"=""
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\link2ea\shell\open\command]
@="\"C:\\Program Files\\Electronic Arts\\EA Desktop\\EA Desktop\\Link2EA.exe\" \"%1\""
REGEOF
# EA Desktop Windows services — EALocalHostSvc provides local IPC that
# Link2EA.exe requires; without it launch fails with RPC_S_SERVER_UNAVAILABLE
sudo tee "$_sw_drive_c/ea_services.reg" > /dev/null << 'REGEOF'
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EALocalHostSvc]
"Type"=dword:00000010
"Start"=dword:00000002
"ErrorControl"=dword:00000001
"ImagePath"="C:\\Program Files\\Electronic Arts\\EA Desktop\\EA Desktop\\EALocalHostSvc.exe"
"DisplayName"="EA Local Host Service"
"ObjectName"="LocalSystem"
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EABackgroundService]
"Type"=dword:00000010
"Start"=dword:00000002
"ErrorControl"=dword:00000001
"ImagePath"="C:\\Program Files\\Electronic Arts\\EA Desktop\\EA Desktop\\EABackgroundService.exe"
"DisplayName"="EA Background Service"
"ObjectName"="LocalSystem"
REGEOF
sudo chown 1000:1000 "$_sw_drive_c/link2ea_fix.reg" "$_sw_drive_c/ea_services.reg"
# Launch wrapper — runs regedit via Steam's launch chain on every launch
# so registry entries survive wineserver restarts. Direct edits to
# system.reg are overwritten by wineserver when it flushes to disk.
sudo tee /tmp/ea_install_wrapper.sh > /dev/null << 'WRAPEOF'
#!/bin/bash
echo "=== launch $(date) ===" >> /tmp/ea_install.log
"$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" regedit /S "C:\\link2ea_fix.reg"
"$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" regedit /S "C:\\ea_services.reg"
exec "$@"
WRAPEOF
chmod +x /tmp/ea_install_wrapper.sh
docker cp /tmp/ea_install_wrapper.sh "$_sw_container":/home/retro/ea_install.sh
docker exec "$_sw_container" chmod +x /home/retro/ea_install.sh
rm -f /tmp/ea_install_wrapper.sh
echo " .reg files and wrapper script installed."
# ── Step 6: set LaunchOptions in localconfig.vdf and lock config dir ───
echo "[6/6] Setting Steam launch options and locking localconfig.vdf..."
_sw_steam_uid=$(sudo ls "$_sw_sh/.steam/steam/userdata/" 2>/dev/null | head -1)
if [ -z "$_sw_steam_uid" ]; then
echo "WARNING: No Steam userdata found. You may need to have logged into Steam first."
echo " Open Steam in Moonlight, sign in, then re-run this command."
else
_sw_cfg_dir="$_sw_sh/.steam/steam/userdata/$_sw_steam_uid/config"
_sw_cfg_file="$_sw_cfg_dir/localconfig.vdf"
if [ ! -f "$_sw_cfg_file" ]; then
echo "WARNING: localconfig.vdf not found at $_sw_cfg_file"
echo " Open Steam in Moonlight first, then re-run."
else
# Stop Steam so it flushes current state before we edit
docker exec "$_sw_container" pkill -f steam.sh 2>/dev/null || true
sleep 5
# Make sure config dir is writable long enough to edit the file
sudo chmod 755 "$_sw_cfg_dir" 2>/dev/null || true
_sw_launch_opt='STEAM_UNIX_SOCKET=/tmp/steam.sock /home/retro/ea_install.sh %command%'
sudo python3 - "$_sw_cfg_file" "$_sw_launch_opt" << 'PYEOF'
import sys, re
path, launch_opt = sys.argv[1], sys.argv[2]
with open(path, 'r') as f:
content = f.read()
new = re.sub(
r'("1237950".*?"LaunchOptions"\s*)"[^"]*"',
rf'\1"{launch_opt}"',
content, flags=re.DOTALL
)
if new == content:
new = re.sub(
r'("1237950"\s*\n\s*\{)',
rf'\1\n\t\t\t\t"LaunchOptions"\t\t"{launch_opt}"',
content
)
if new == content:
print("WARNING: 1237950 block not found in localconfig.vdf — launch options not set.")
print("In Steam: right-click SWBF2 → Properties → Launch Options, enter:")
print(" STEAM_UNIX_SOCKET=/tmp/steam.sock /home/retro/ea_install.sh %command%")
else:
with open(path, 'w') as f:
f.write(new)
print(" LaunchOptions set for AppID 1237950.")
PYEOF
sudo chown 1000:1000 "$_sw_cfg_file"
# Lock the config directory so Steam can't overwrite localconfig.vdf
# via atomic rename (temp file + rename). chmod 444 on the file is
# bypassed; locking the directory blocks the rename-into.
sudo chmod 555 "$_sw_cfg_dir"
echo " Config directory locked (chmod 555) to preserve launch options."
fi
fi
echo ""
echo "=== Setup complete ==="
echo ""
echo "Next steps:"
echo " 1. In Steam (Moonlight): right-click SWBF2 → Properties → Compatibility"
echo " → Force a specific Steam Play compatibility tool → GE-Proton10-34 (or later)"
echo " 2. Click Play on SWBF2"
echo " 3. Let Vulkan shaders compile — do NOT skip, takes several minutes, only happens once"
echo " 4. EA App will authenticate via your linked Steam/EA account (no manual login needed"
echo " if accounts are linked at ea.com)"
echo " 5. Game should launch"
echo ""
echo "Troubleshooting:"
echo " ./manage.sh diagnose-ea $_sw_appid - check registry state"
echo " docker exec \$container tail /tmp/ea_install.log - check wrapper log"
echo ""
echo "Note: If the config dir was not writable (Steam not stopped cleanly), set launch"
echo "options manually in Steam → SWBF2 → Properties → Launch Options:"
echo " STEAM_UNIX_SOCKET=/tmp/steam.sock /home/retro/ea_install.sh %command%"
;;
fix-perms)
# Wolf creates each app's home dir under WOLF_STATE_DIR/<session-id>/<App>
# and mounts it as /home/retro inside the app container. If anything in
@@ -2099,7 +2358,7 @@ PYEOF
_manage_wolf_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local commands="start stop restart logs status pin update apps reorder
add-web ge-proton games fix-ea-game wait-ea-app
add-web ge-proton games setup-swbf2 fix-ea-game wait-ea-app
install-ea-app diagnose-ea fix-perms install-completion backup"
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
}
@@ -2156,6 +2415,7 @@ COMPEOF
echo " ./manage.sh add-web [name] [url] - Add a URL shortcut tile (Firefox kiosk)"
echo " ./manage.sh ge-proton [version] - Install GE-Proton (latest, or pin a version)"
echo " ./manage.sh games - List games; pick one to apply the EA fix"
echo " ./manage.sh setup-swbf2 - Full SWBF2 2017 setup (EA Desktop + wrapper, see docs/SWBF2-2017-wolf-setup.md)"
echo " ./manage.sh fix-ea-game [appid] - Apply EA fix directly (default: SWBF2 1237950)"
echo " ./manage.sh wait-ea-app [appid] - Watch for EA App to install, then register link2ea:// handler"
echo " ./manage.sh install-ea-app [appid] - Run EA App installer inside container (GUI appears in Moonlight)"