docs: add SWBF2 2017 discovery writeup explaining why the fix works
Long-form document tracing the actual discovery process: the wrong turns, the aha moments, and the reasoning behind each step. Covers: - Why EA is involved in a Steam game at all (link2ea:// architecture) - JunoConfigureRegistry failure and why DISABLEROLLBACK was a dead end - msiextract bypass as the actual solution - Why direct system.reg edits don't survive reboots (wineserver ownership) - The launch wrapper approach for registry persistence - RPC_S_SERVER_UNAVAILABLE and EALocalHostSvc - Wolf-specific bwrap capabilities mismatch - localconfig.vdf atomic rename problem and chmod 555 fix Written for sharing — no identifying information, explains the "why" for each fix so readers can adapt if EA changes their MSI structure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
This commit is contained in:
@@ -0,0 +1,289 @@
|
|||||||
|
# Star Wars Battlefront II (2017) on Linux — How We Got Here
|
||||||
|
|
||||||
|
This document explains *why* the fix works, not just *what* to run. It traces
|
||||||
|
the actual discovery process: the wrong turns, the aha moments, and the
|
||||||
|
reasoning behind each step. If you want to understand what the setup scripts
|
||||||
|
are doing and why, read this first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Does it work out of the box?
|
||||||
|
|
||||||
|
No — not on any Linux Steam setup as of 2025. The game itself runs fine under
|
||||||
|
GE-Proton. The blocker is EA's launcher infrastructure, specifically the step
|
||||||
|
where Steam tries to install the EA App (formerly EA Desktop) into your Wine
|
||||||
|
prefix before the first launch. That installation silently fails, and the game
|
||||||
|
never gets past it.
|
||||||
|
|
||||||
|
This is true whether you're running native Linux Steam, or streaming through
|
||||||
|
Wolf/Games-on-Whales, or any other Proton setup. The root cause is in Wine,
|
||||||
|
not in the streaming layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The architecture: why EA is involved at all
|
||||||
|
|
||||||
|
SWBF2 (2017) is a Steam game that also requires EA's launcher infrastructure.
|
||||||
|
This surprises people because it's a Steam purchase. The dependency exists
|
||||||
|
because EA's backend handles identity and entitlement checks separately from
|
||||||
|
Steam's payment and file delivery.
|
||||||
|
|
||||||
|
The game binary does not launch directly. Instead, Steam passes a `link2ea://`
|
||||||
|
URL as the "executable" to GE-Proton:
|
||||||
|
|
||||||
|
```
|
||||||
|
link2ea://launchgame/1237950?platform=steam&theme=swbfii
|
||||||
|
```
|
||||||
|
|
||||||
|
GE-Proton's internal `steam.exe` intercepts this URL, looks up the `link2ea://`
|
||||||
|
protocol handler in the Wine registry, finds `Link2EA.exe`, runs it, and
|
||||||
|
Link2EA.exe authenticates with EA's servers before handing off to the game.
|
||||||
|
|
||||||
|
If anything in that chain is missing — no protocol handler, no Link2EA.exe, no
|
||||||
|
EA services — the game exits silently and you're back at the Play button.
|
||||||
|
|
||||||
|
The first question was: *why is the protocol handler missing?*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The installer: why it fails
|
||||||
|
|
||||||
|
Steam bundles `ea_app.msi` inside the game files. On first launch it runs this
|
||||||
|
MSI inside Wine to install EA Desktop. The installer runs, appears to do
|
||||||
|
something, and then... nothing. The game exits. No error message.
|
||||||
|
|
||||||
|
### Red herring: the wrong Proton version
|
||||||
|
|
||||||
|
The first instinct was to try different Proton versions. Vanilla Proton, various
|
||||||
|
GE-Proton releases. Some got further than others, but none completed the EA App
|
||||||
|
installation. GE-Proton is required — it contains patches specifically for EA's
|
||||||
|
launcher — but even with it the MSI fails.
|
||||||
|
|
||||||
|
### Finding the actual failure
|
||||||
|
|
||||||
|
The Wine logs showed the MSI starting, running custom actions, and then rolling
|
||||||
|
back. The key line buried in several hundred lines of output:
|
||||||
|
|
||||||
|
```
|
||||||
|
err:msi:HANDLE_CustomType1 ... JunoConfigureRegistry ... returned 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The custom action returned 0 (success) but triggered a rollback anyway. That
|
||||||
|
seems contradictory. The reason: `JunoConfigureRegistry` is a .NET custom
|
||||||
|
action that calls `SetSecurityDescriptorSddlForm` to set ACLs on registry keys.
|
||||||
|
Wine's .NET implementation doesn't support that call. It returns 0 silently
|
||||||
|
(Wine's default for unimplemented functions) but the registry keys end up in
|
||||||
|
an invalid state, which the MSI detects and treats as a failure, rolling back
|
||||||
|
the entire installation.
|
||||||
|
|
||||||
|
The MSI was designed for Windows. On Windows, `SetSecurityDescriptorSddlForm`
|
||||||
|
works. On Wine, it silently no-ops, leaving the installation in a broken state
|
||||||
|
that triggers rollback.
|
||||||
|
|
||||||
|
### DISABLEROLLBACK was a dead end
|
||||||
|
|
||||||
|
The obvious next attempt: run the MSI with `DISABLEROLLBACK=1` so it can't
|
||||||
|
roll back even if something fails. This produced a 3-line log instead of
|
||||||
|
the usual hundreds of lines. The MSI thought the product was already registered
|
||||||
|
from the previous failed attempt and exited immediately without doing anything.
|
||||||
|
The partial state from the first failed run confused it.
|
||||||
|
|
||||||
|
### The actual fix: don't run the installer at all
|
||||||
|
|
||||||
|
MSI files are archives. They contain files, a database of what goes where, and
|
||||||
|
custom actions (executable code). The custom actions are what fail. If you skip
|
||||||
|
the installer entirely and just extract the files directly, you get everything
|
||||||
|
you need without running any of the broken .NET code.
|
||||||
|
|
||||||
|
`msiextract` (from the `msitools` package) extracts an MSI's file payload on
|
||||||
|
Linux without executing any custom actions. Run it on the host (outside Wine),
|
||||||
|
and it produces the full EA Desktop directory — `Link2EA.exe`, `EADesktop.exe`,
|
||||||
|
`EALocalHostSvc.exe`, all of it — without touching Wine at all.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
msiextract -C /tmp/ea_app_extracted /tmp/ea_app.msi
|
||||||
|
```
|
||||||
|
|
||||||
|
This was the first major aha moment. The installer was never necessary. The
|
||||||
|
files were always there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The registry: why direct edits don't work
|
||||||
|
|
||||||
|
With EA Desktop files in place, the next problem: the `link2ea://` protocol
|
||||||
|
handler still wasn't registered in Wine's registry. Without it, GE-Proton's
|
||||||
|
`steam.exe` can't find `Link2EA.exe` and the URL goes nowhere.
|
||||||
|
|
||||||
|
The Wine registry for a prefix lives in plain text files:
|
||||||
|
- `pfx/system.reg` — HKEY_LOCAL_MACHINE
|
||||||
|
- `pfx/user.reg` — HKEY_CURRENT_USER
|
||||||
|
|
||||||
|
The obvious approach: edit `system.reg` directly, add the protocol handler
|
||||||
|
entries, done. This worked — once. On the next launch the entries were gone.
|
||||||
|
|
||||||
|
### Why: wineserver owns the registry
|
||||||
|
|
||||||
|
When Wine (or Proton) starts, `wineserver` loads the registry files into memory.
|
||||||
|
Any changes you make to the files on disk while wineserver is running are
|
||||||
|
ignored — wineserver's in-memory copy is authoritative. When wineserver shuts
|
||||||
|
down, it flushes its in-memory state back to disk, overwriting whatever you
|
||||||
|
wrote.
|
||||||
|
|
||||||
|
So: edit the file while wineserver is stopped → entries appear → Steam launches
|
||||||
|
the game → wineserver starts → your entries are still there → wineserver stops
|
||||||
|
at the end → your entries are overwritten with the original state.
|
||||||
|
|
||||||
|
One launch works. The second launch doesn't.
|
||||||
|
|
||||||
|
### The fix: go through the launch chain
|
||||||
|
|
||||||
|
The correct way to write registry entries that persist is to run `regedit`
|
||||||
|
*through Proton's own wine binary*, which writes into wineserver's live memory.
|
||||||
|
When wineserver then flushes to disk on shutdown, it writes the entries you
|
||||||
|
added — because they came from inside wineserver's own session.
|
||||||
|
|
||||||
|
The Steam launch option `%command%` expands to the full Proton/pressure-vessel
|
||||||
|
launch invocation. A wrapper script can intercept this, run regedit using the
|
||||||
|
same Proton binary before the game starts, then pass control through to the
|
||||||
|
game:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# $1 through ${11} is the Proton launch wrapper
|
||||||
|
# running regedit with the same binary registers into the live wineserver session
|
||||||
|
"$1" "$2" "$3" ... "${11}" regedit /S "C:\\link2ea_fix.reg"
|
||||||
|
"$1" "$2" "$3" ... "${11}" regedit /S "C:\\ea_services.reg"
|
||||||
|
exec "$@" # now launch the actual game
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.reg` files live in `drive_c` (the Wine prefix's C: drive) so they survive
|
||||||
|
prefix wipes and container restarts. On every launch, the wrapper imports them
|
||||||
|
fresh. The entries are always current, always in wineserver's memory.
|
||||||
|
|
||||||
|
This was the second major aha moment. The registry isn't a file to edit — it's
|
||||||
|
a live service to talk to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The missing service: RPC_S_SERVER_UNAVAILABLE
|
||||||
|
|
||||||
|
With the protocol handler registered, `link2ea://` now fired correctly.
|
||||||
|
Link2EA.exe launched. Then it immediately crashed with:
|
||||||
|
|
||||||
|
```
|
||||||
|
RPC_S_SERVER_UNAVAILABLE (0x800706ba)
|
||||||
|
```
|
||||||
|
|
||||||
|
Link2EA.exe uses local RPC (inter-process communication) to talk to
|
||||||
|
`EALocalHostSvc` — a Windows service that runs in the background and provides
|
||||||
|
the local IPC socket Link2EA.exe expects to find. Without that service
|
||||||
|
registered, Link2EA.exe looks for the socket, finds nothing, and exits.
|
||||||
|
|
||||||
|
The fix was the same as the protocol handler: a `.reg` file imported on every
|
||||||
|
launch that registers `EALocalHostSvc` and `EABackgroundService` as Windows
|
||||||
|
services. Wine's service layer is a pale imitation of Windows Services, but it
|
||||||
|
provides enough of the IPC interface that Link2EA.exe is satisfied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Wolf-specific problem: capabilities and bwrap
|
||||||
|
|
||||||
|
*(Skip this section if you're on native Linux Steam — it doesn't apply.)*
|
||||||
|
|
||||||
|
Wolf/Games-on-Whales runs Steam inside a Docker container. The obvious thing to
|
||||||
|
try was running Wine or Proton directly from outside the container using
|
||||||
|
`docker exec`. This failed consistently with:
|
||||||
|
|
||||||
|
```
|
||||||
|
setting up uid map: Permission denied
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason is a Linux capabilities mismatch. Processes launched via `docker exec`
|
||||||
|
inherit elevated capabilities from the Docker daemon (roughly `CapEff ≈ 0xa80c35fb`).
|
||||||
|
Steam processes inside the container run with `CapEff=0` — no elevated
|
||||||
|
capabilities at all.
|
||||||
|
|
||||||
|
When a high-capability process tries to run `bwrap` (the bubblewrap sandbox that
|
||||||
|
Steam's pressure-vessel/sniper runtime uses), bwrap attempts a privileged uid
|
||||||
|
namespace map — and that path requires either no capabilities or full root, not
|
||||||
|
the partial set that `docker exec` provides. It fails.
|
||||||
|
|
||||||
|
The consequence: **only Steam itself can launch programs through the
|
||||||
|
sniper+GE-Proton runtime.** Any attempt to invoke Proton from outside Steam's
|
||||||
|
own process tree hits this wall. Every approach that tried to "just run wine"
|
||||||
|
from a terminal or docker exec hit this problem and was a dead end.
|
||||||
|
|
||||||
|
This is why the wrapper script approach works where direct invocation does not.
|
||||||
|
The wrapper is called *by* Steam as part of its own launch chain, so it inherits
|
||||||
|
Steam's `CapEff=0` context and bwrap succeeds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The localconfig.vdf problem
|
||||||
|
|
||||||
|
Steam stores per-game launch options in
|
||||||
|
`userdata/<uid>/config/localconfig.vdf`. The wrapper needs to be set as the
|
||||||
|
launch option for SWBF2, and that setting needs to survive Steam restarts.
|
||||||
|
|
||||||
|
The naive fix — `chmod 444` on the file — doesn't work. Steam writes
|
||||||
|
`localconfig.vdf` using an atomic rename: it writes to a temporary file in the
|
||||||
|
same directory, then renames the temp file over the original. `chmod 444` on
|
||||||
|
the destination file doesn't prevent a rename-over.
|
||||||
|
|
||||||
|
The fix is `chmod 555` on the *directory*. A rename-into requires write
|
||||||
|
permission on the destination directory. Lock the directory and Steam can't
|
||||||
|
complete the atomic rename — it fails silently and the original file is
|
||||||
|
preserved.
|
||||||
|
|
||||||
|
This is Linux filesystem semantics: file permissions control reads and writes to
|
||||||
|
the file's contents; directory permissions control the ability to create,
|
||||||
|
delete, or rename entries *within* the directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What EA actually requires
|
||||||
|
|
||||||
|
After all of this, a reasonable question: *why does EA need to authenticate at
|
||||||
|
all for a game I bought on Steam?*
|
||||||
|
|
||||||
|
EA's position is that their games require EA account authentication regardless
|
||||||
|
of purchase platform. SWBF2 (2017) is in a middle state: it was originally an
|
||||||
|
Origin-only title, moved to Steam, but EA kept their authentication layer in
|
||||||
|
place. The game phones home to EA's servers on first launch and periodically
|
||||||
|
thereafter. The EA App caches credentials locally, so subsequent launches work
|
||||||
|
offline until the token expires (weeks to months).
|
||||||
|
|
||||||
|
The authentication cannot be bypassed without modifying game binaries — which
|
||||||
|
would be both a TOS violation and unnecessary, since the fix above gets the
|
||||||
|
legitimate authentication flow working correctly. The game still calls home to
|
||||||
|
EA. You still need a valid EA account linked to your Steam account. The scripts
|
||||||
|
here fix the *installation* of the authentication layer, not the authentication
|
||||||
|
itself.
|
||||||
|
|
||||||
|
If EA's servers shut down permanently: the community SWBF2 server project
|
||||||
|
(Kyber) would likely replace the authentication infrastructure, as has happened
|
||||||
|
with other abandoned EA titles. SWBF2 (2005, AppID 6060) has no EA dependency
|
||||||
|
and remains fully playable with no account required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: why each piece is necessary
|
||||||
|
|
||||||
|
| Problem | Root cause | Fix |
|
||||||
|
|---------|-----------|-----|
|
||||||
|
| EA App install rolls back | `JunoConfigureRegistry` .NET call unsupported in Wine | `msiextract` on host, bypass installer entirely |
|
||||||
|
| `link2ea://` URL goes nowhere | Protocol handler not in Wine registry | `link2ea_fix.reg` imported via launch wrapper |
|
||||||
|
| Link2EA.exe crashes immediately | `EALocalHostSvc` IPC service not registered | `ea_services.reg` imported via launch wrapper |
|
||||||
|
| Registry fixes disappear after one launch | wineserver owns the registry; file edits are overwritten on shutdown | Import `.reg` files through Proton's own wine binary inside the launch chain |
|
||||||
|
| Launch wrapper setting gets overwritten | Steam uses atomic rename to write `localconfig.vdf` | `chmod 555` on the config directory blocks the rename |
|
||||||
|
| (Wolf only) Can't run Proton via `docker exec` | Capabilities mismatch breaks bwrap uid namespace mapping | Use Steam's own launch chain; only Steam can invoke the sniper runtime |
|
||||||
|
|
||||||
|
None of these problems are obvious. None of them produce useful error messages
|
||||||
|
by default. Each one looks like "the game just exits" until you find the right
|
||||||
|
log, add the right verbosity flag, or run the right diagnostic. The solutions
|
||||||
|
are individually small — a reg file here, a chmod there — but finding each one
|
||||||
|
required understanding a separate piece of Linux internals, Wine internals, or
|
||||||
|
Docker internals.
|
||||||
|
|
||||||
|
The scripts exist so no one else has to find them again.
|
||||||
Reference in New Issue
Block a user