Files
DroidDeck/docs/development/ea-ubisoft-stores.md
T
Kurt Himebauch d8ed02f2cf feat: EA store with sign-in, library, native downloads, updates and launch without the EA app (#2259)
* docs: research and plan for EA and Ubisoft stores

* feat(ea): EA store with sign-in, library, native downloads, updates and launch without the EA app

* perf(ea): fetch large archive records as parallel ranges

* fix(ea): register game display names

* fix(ea): license legacy EA Core games and harden the LSX endpoint

* fix(ea): run legacy Origin SDK games with a stand-in Origin process

* fix(ea): trust only the prepared game on LSX and stop stale library syncs

* fix(ea): close LSX sessions on stop and drop clients that never hand-shake

* fix(ea): require every hashed field before licensing or caching a probe

* fix(ea): trust one LSX session per launch

* fix(ea): serialize the library cache write with sign-out and rerun a skipped sync

* fix(ea): hand off library sync requests under one lock

* fix(ea): clear the sign-in before invalidating the library

* fix(ea): serialize sign-out with token refresh and with the library cache

* fix(ea): LSX trusts only verified peers, new launch closes old sessions, exit stops its own game

* fix(ea): LSX hides profile and presence from unverified peers

* fix(ea): LSX handshake has a fixed deadline and untrusted sessions yield their slot

* fix(ea): LSX never registers a client after its listener was stopped

* fix(ea): LSX evicts each untrusted session at most once

* fix(ea): LSX admission and eviction share one lock and keep the cap

* fix(ea): LSX trust promotion excludes eviction

* fix(ea): LSX launch generation guards restarts and drops results after shutdown

* fix(ea): LSX publishes launch state under the admission lock and tags accepts by generation

* fix(ea): LSX restarts rebind its listener and promotion checks the launch generation

* fix(ea): record why EA refuses a game licence in the store log

* chore(ea): log owned offers and the full licence refusal

* chore(ea): log the account's entitlements when no game licence is granted

* chore(ea): log legacy entitlements when no game licence is granted

* chore(ea): log only the refused game's own entitlements

* fix(ea): skip trial launchers flagged 1 and license games whose LSX peer Android cannot name
2026-11-10 14:56:29 -05:00

21 KiB
Raw Blame History

EA app and Ubisoft Connect as stores: research and plan

Research as of 2026-11-07, against origin/main at 914b3588e. Nothing here is implemented yet.

Summary

Both stores can follow the path Battle.net took (#979, then #1602): first the vendor client in its own Proton prefix, then more of it done by DroidDeck itself. Unlike Battle.net, both now have open-source clients that already handle sign-in, library and downloads without the vendor app. So the second step can go further than Battle.net's did.

EA Ubisoft
Sign-in without the client Yes: OAuth JUNO_PC_CLIENT in a WebView Yes: UbiServices session (ticket + remember-me) through Ubisoft's WebAuth page
Owned library without the client Yes: juno GraphQL ownedGameProducts Yes: Demux ownership service (protobuf over TLS), or the GraphQL library
Downloads without the client Yes: a build is one ZIP on EA's CDN, read with HTTP Range per entry (Maxima) Yes: Demux download service → manifest → zstd/deflate slices (Optima, ubi-cli-mvp)
Launch without the client Yes, legitimately: LSX server on 127.0.0.1:3216 plus a real OOA licence from proxy.novafusion.ea.com (Maxima) Only by replacing Ubisoft's DRM DLLs (Optima, Re0xCat loaders). Don't ship that. Modern titles need the real Connect anyway
Cloud saves without the client Yes: cloudsync.juno.ea.com (Maxima) Not researched; Connect handles them
Effectively out of scope Javelin kernel anti-cheat titles (Battlefield, FC, Apex) Kernel AC and online-only titles that misbehave under Proton

Recommendation

  • EA should go fully native in-app: sign-in, library, downloads, launch and cloud saves in Kotlin and Rust, with no EA app in the prefix. The EA app stays as an optional fallback, like the Epic launcher option (EpicClient). The LSX and licence half also fixes Steam's EA titles, which today install the whole EA app (#2036) just to answer link2ea://.
  • Ubisoft should be hybrid: native sign-in, library and downloads in Rust, then register the files with the real Ubisoft Connect and launch through uplay://. That means a Connect prefix managed the way Battle.net's is.
  • Ubisoft's launch step is the biggest open question (see "Unknowns"). If Connect can't be made to adopt natively downloaded files cleanly, start with the Connect-driven download, as #979 did for Battle.net.

What DroidDeck already has, and how much of it each store reuses

Existing piece Where EA Ubisoft
Store enum + StoreBackend (signIn, syncLibrary, loadShelves, install, uninstall) stores/Store.kt, stores/StoresState.kt:355 Reuse as is: new EA entry Reuse as is: new UBISOFT entry
WebView sign-in with redirect capture (StoreLoginActivity.Flow) stores/LoginFlows.kt, StoreLoginActivity.kt Reuse: the redirect is qrc:///html/login_successful.html?code=…, caught in isRedirect Reuse: capture the ubi_v1 ticket from the WebAuth page, then exchange it for a full session
Keystore-backed account store stores/StoreAccounts.kt, CredentialCipher.kt Reuse Reuse
Rust store engine libdroiddeckstores.so (JNI, fetch_core.rs, hand-rolled protobuf in amazon/proto.rs) app/src/main/rust/stores Reuse: ZIP central directory + Range fetch + inflate (flate2 is already a dependency) Reuse fetch_core. New: a TLS socket client for Demux, a protobuf schema, and a zstd decoder (ruzstd is pure Rust; the zstd crate needs cc, which the crate already has)
Download queue: pause, resume, cancel, tiers, Downloads tab stores/download/* Reuse Reuse
Install roots: internal, other storage, Games folder StoreInstallRoot.kt Reuse Reuse
Cloud saves: plan, paths, transport stores/CloudSaves.kt, CloudPlan.kt, CloudSavePaths.kt Reuse with a new EA transport Later, or leave to Connect
Vendor client in its own prefix, a guest Python driver, a headless helper under gamescope, FEX choice, memory policy runtime/BattleNet.kt, tools/linuxfs/overlay/usr/local/bin/droiddeck-battlenet Fallback only Needed: droiddeck-ubisoft modelled on droiddeck-battlenet
"Use the vendor launcher instead" option stores/epic/EpicClient*.kt Pattern for the EA app fallback Pattern for "Open Ubisoft Connect"
Steam install-script seeding that lets EA app and Ubisoft Connect installers run droiddeck-seed-redists (#1449, #2036) Replaced by the LSX stub for Steam EA titles Unchanged
Ubisoft registry handoff for Ubisoft titles bought on Epic open PR #1445 n/a Same registry key (HKLM\SOFTWARE\WOW6432Node\Ubisoft\Launcher\Installs\<id>\InstallDir); merge or share it
Library, Steam shortcuts, art, game page, sidecars frontend/Library.kt, SteamLiveShortcuts.kt, StoreArt.kt, StoreGameSidecar.kt Reuse Reuse
Clean-room process for store modules docs/development/epic-cleanroom.md, amazon-cleanroom.md Use: spec from Maxima, then implement from the spec Use: spec from Optima/ubi-cli-mvp/YoobieRE

Genuinely new engineering:

  1. An LSX server (EA). EA's local SDK protocol is XML over TCP on 3216, with a challenge handshake, GetConfig, GetProfile, GetGameInfo, license and entitlement requests. Add a Wine-side stub registered as the link2ea:// / origin2:// handler with EA app presence keys, plus OOA licence requests. This half is new; the other EA parts are the familiar store pattern.
  2. The EA machine hash. Licences are bound to a machineHash built from WMI/BIOS/disk/CPUID values. They have to match what the game computes inside Wine under FEX on the phone. Nobody has shown that on Android yet, so it has to be measured on device.
  3. A Demux client (Ubisoft). This is a TLS socket to dmx.upc.ubisoft.com speaking protobuf, with patch negotiation, auth, ownership tokens and download-service URL signing. No store we have uses a socket protocol.
  4. A way for Connect to adopt files (Ubisoft). Connect keeps its own DPAPI-encrypted sign-in, tied to the Wine MachineGuid. Our session can't be handed to it (ubi-cli-mvp checked). Connect also has to consider the game installed before uplay://launch works.

Prior art

EA

Project Licence State What it gives us
ArmchairDevelopers/Maxima (Rust, maxima-lib) GPL-3.0 Pre-alpha by its own README; last commit 2026-01; open issues through 2026-11. Backs KYBER The whole stack: OAuth (JUNO_PC_CLIENT, localhost 31033 redirect), juno GraphQL (.gql files for owned games, downloadUrl, availableBuilds, achievements), the ZIP/Range downloader, OOA licences (ooa/mod.rs, machineHash), the LSX server (lsx/), cloud sync, the hardware hash (auth/hardware.rs), Wine registry setup. Unsupported: Battlefield 3/4 (Battlelog), pre-DiP games (Dead Space 2, BFBC2), DLC installs
Maxima #71 2026-11 Games never connect to LSX unless HKLM\SOFTWARE\WOW6432Node\Origin is right, and INSTALLED_LANGUAGE must be answered. Check ours against it
Maxima #49 ARM64: the hash must match what Activation.dll reads from CPUID under Rosetta/Box64/FEX. Directly our problem
unifideck docs/feasibility/ea.md GPL-3.0 2026-07 EA app installer runs unattended (/quiet), login renders on GE-Proton11-1, origin2://game/launch?offerIds=<id>&autoDownload=1 is the install idiom, ~2.5 GB client, aggressive self-update
Lutris services/ea_app.py GPL-3.0 Maintained juno queries, installerdata.xml scan for installed games
BellezaEmporium/galaxy-integration-ead none Maintained Facts only: OAuth params, pc_sign fallback
erri120/GameFinder wiki GPL-3.0 Encrypted IS file format (only for reading EA app installs)
Heroic wontfix #352 No EA support beyond Epic-hosted EA titles

Ubisoft

Project Licence State What it gives us
xXJSONDeruloXx/ubi-cli-mvp (yours, TypeScript) MIT 2026-07 The best-tested data plane: session login with Ubi-Challenge replay, ubi_2fa_v1, refresh; Demux ownership, ownership tokens and signed URLs; manifest decode; slice layouts (slices_v3/<bucket>/<HASH> and flat slices/); zstd and both deflates; integrity and path safety. On the Connect side: connect-install (uplay://install/<id>), connect-seed (let Connect start the official download, pause, exit, then fill the staging area with verified files), remembered-auth migration between prefixes, ubi play via uplay://. Docs: research.md, validation.md, DEMUX_JOURNAL.md, optima-comparison.md
Starkka15/Optima (Rust) GPL-3.0 0.2.0, 2026-08 Rust version of the same data plane (auth.rs, demux.rs, install.rs, proto/*.proto), plus launcherless launch through replacement uplay_r1_loader / ubiorbitapi_r2_loader DLLs, a self-signed certificate that mimics Ubisoft's (imported into the prefix's Root store so Watch_Dogs accepts it), and an EAX shim. Powers GameVault's Ubisoft extension
YoobieRE: ubisoft-demux-node, ubisoft-dmx-decode, ubisoft-manifest-downloader, uplay-install-reverse, product-db-archive mixed or none 2022–2024 The original protocol reversing that both of the above build on
Re0xCat uplay-r1-loader, ubiorbitapi-r2-loader The DRM-DLL replacements (see Risks)
mubaraknumann/unifideck stores/ubisoft/ GPL-3.0 Shipping, active The full Connect-wrapper archetype in Python: three-prefix model (auth / template / per-game, shared MachineGuid), credential capture and fan-out, parsing Connect's binary configurations and ownership catalogs, leveldb spaceId ↔ ubisoftConnectGameId, a manual-install watcher, uplay://launch/<id>/0, session capture after exit. docs/ubisoft-store-spec.md is a good map of what breaks
Heroic Installs Ubisoft Connect automatically for Ubisoft titles from Epic; no Ubisoft store
DroidDeck #1445 (open), #1449, #2286 / #2295 / #2317 / #2292 / #2050 Our own Ubisoft-under-Proton knowledge: the Installs registry key, UplayLaunch.exe being a stub, the bundled UplayInstaller.exe, legacy installer layouts, SD-card staging, helper processes during the exit watch

Licences: Maxima, Optima, unifideck and Lutris are GPL-3.0, which is compatible with DroidDeck (GPL-3.0). ubi-cli-mvp is MIT. Even so, the Epic and Amazon modules were written clean-room, so do the same here: write the spec from these sources, implement from the spec, and credit them in the README the way #1602 does.

Design

EA

StoreLoginActivity (OAuth JUNO_PC_CLIENT) ─► EaAccount (access + refresh token, persona)
EaLibrary ── juno GraphQL ownedGameProducts / legacyOffers ─► CatalogItem(offerId, contentId, slug)
EaInstaller (DownloadQueue job) ── downloadUrl ─► build ZIP ─► Rust: central directory, Range per entry, inflate, CRC
                                                    └─ writes __Installer/installerdata.xml as EA's own layout expects
Launch: droiddeck-ea (guest) ── registers stub for link2ea:// origin2:// + Origin/EA Desktop keys
        app-side EaLsxServer on 3216  ◄── game (OriginSDK / EA SDK)
        before launch: OOA licence (novafusion, machineHash) → ProgramData/Electronic Arts/EA Services/License
        before/after: cloudsync pull / push through CloudSaves
  • Where the LSX server runs. In the Android app. DroidDeck's guest shares the network namespace, so an app-side server on 127.0.0.1:3216 is reachable from Wine. Keeping it in Kotlin or Rust, not a guest daemon, matches how DroidDeck owns other store logic. Check this against proot networking first.
  • The stub. A protocol handler for link2ea:// / origin2:// that hands the request to DroidDeck.
  • Steam's EA titles. Use the same stub and LSX path, keyed by link2ea:// in the launch config, plus entitlement refresh and Steam account linking. #2036's EA app install becomes the fallback.
  • The EA app fallback. Its own prefix (a compatdata id like Battle.net's 2596113170), installed with EAappInstaller.exe /quiet, opened from the store row. That's for titles the LSX path can't start (pre-DiP, Battlelog) and for buying games.

Ubisoft

StoreLoginActivity (Ubisoft WebAuth) ─► ticket ─► full session (expiry, rememberMe); refresh ~3 h
UbisoftLibrary ── Demux ownership ─► products + config YAML (name, exe, uplay id, space id)
UbisoftInstaller (DownloadQueue job) ── Rust Demux: ownership token → download_service urlReq
                                      → manifest (356-byte header, zlib) → slices (zstd/deflate) → files, verified
Register + launch: droiddeck-ubisoft (guest, like droiddeck-battlenet)
        Connect in its own prefix, signed in once in its own window (DPAPI/MachineGuid bound)
        adopt files: Installs\<id>\InstallDir registry (+ whatever state Connect checks)
                     or ubi-cli-mvp's connect-seed: official download starts, pause, seed, let Connect verify
        launch: upc.exe uplay://launch/<id>/0, Connect closed or minimised once the game runs (#979's 20 s rule)
  • One Connect prefix, not one per game. unifideck uses per-game prefixes because Steam on the Deck gives each shortcut its own. DroidDeck already runs Battle.net games inside Battle.net's prefix, which avoids unifideck's whole credential fan-out problem.
  • Two stages, like Battle.net:
    1. Connect in its prefix, install by uplay://install/<id>, and Downloads shows an indeterminate "Installing in Ubisoft Connect" until the folder settles. That is #979's level.
    2. Native downloads in the Rust engine with real progress, pause and resume, then Connect adopts the files. Like #1602, but we own the transfer instead of driving the vendor's downloader. If Connect has a scriptable local API (Battle.net's Agent had one), that's a cheaper middle stage, but none is known.
  • Launcherless DRM shims: don't ship them. See Risks.

Known

  • EA sign-in, library, downloads, licences, LSX and cloud sync all work outside the EA app on Linux (Maxima).
  • The EA app installs unattended and its login renders under GE-Proton 11 on x86 (unifideck).
  • Ubisoft sign-in, refresh, Demux ownership, signed URLs, manifests and slice reconstruction work (ubi-cli-mvp validation.md, Optima). A session ticket lasts about 3 hours and remember-me renews it.
  • Connect's sign-in can't be created from our session. It is DPAPI-encrypted and tied to MachineGuid, but it can be copied between prefixes that share that identity.
  • Connect has repeatedly broken under Proton after its own updates (2023, and a single-source April 2026 report). #1449 and the open Ubisoft PRs show the same churn here.
  • Javelin titles don't run under Proton. EA's March 2026 job listing mentions future Linux/Proton work; nothing has shipped.

Unknowns, in the order to settle them

# Question How to find out Blocks
U1 Does an EA OOA licence fetched with our machineHash validate inside DroidDeck-Proton under FEX on Adreno phones? Which WMI/CPUID values does the game see? Prototype: Maxima's hash code against values read through Wine in our prefix; one DiP single-player title and one Denuvo title. EA launch
U2 Can the app-side LSX server on 127.0.0.1:3216 be reached from proot + Wine, and do games connect with our registry keys (Maxima #71)? Spike with a Steam EA title already in the library EA launch, Steam EA titles
U3 What does EA's CDN serve for a current title: one ZIP, multiple, or delta? How do language selection and installerdata.xml work? Maxima content/ + downloadUrl.gql, one real download through the Rust engine EA install
U4 Does Connect accept a tree we downloaded once the Installs registry key is written, or does it re-verify or re-download? Does connect-seed work under FEX on a phone? Spike with a small owned title (ubi-cli-mvp used Splinter Cell / product 109). #1445's registry work is the start Ubisoft stage 2
U5 Does Ubisoft Connect (Qt + CEF) run acceptably under FEX in our prefix? How much RAM does it hold while a game runs? Install it like Battle.net; measure with the Memory readout from #979 Ubisoft stage 1
U6 Ubisoft's WebAuth page in an Android WebView: captcha, 2FA, Ubi-Challenge Real sign-in on device Ubisoft sign-in
U7 Demux over TLS from Android (certificate pinning, patch-version negotiation drift) Rust spike against dmx.upc.ubisoft.com Ubisoft library and downloads
U8 Ubisoft+ subscription entitlements in Demux ownership Account with Ubisoft+ Library completeness
U9 EA Play subscription entitlements in ownedGameProducts Account with EA Play Library completeness
U10 Ubisoft cloud saves outside Connect Not researched; Connect handles them while it's in the path Later

Plan

Each step is its own PR, tested on device as usual (REDMAGIC tablet, Fold7, Thor).

EA

  1. Spike (no PR): settle U1–U3 with throwaway code against one owned DiP title and one Steam EA title.
  2. Steam EA titles without the EA app: the link2ea stub, app-side LSX server, OOA licence and entitlement refresh. It replaces #2036's EA app install where it works and falls back to it otherwise. This ships value first and proves the hard half.
  3. EA store: sign-in, library, shelves: a Store.EA entry, OAuth flow, juno library, art, a "not installable here" flag for Javelin titles.
  4. EA downloads: a Rust ea module (ZIP/Range/inflate on fetch_core), with Downloads progress, pause, resume, cancel, updates and uninstall.
  5. EA cloud saves through CloudSaves.
  6. EA app fallback prefix and store-row button, for pre-DiP and Battlelog titles.
  7. Clean-room record: docs/development/ea-cleanroom.md.

Ubisoft

  1. Spike (no PR): U4, U5 and U7.
  2. Ubisoft Connect in its prefix (#979 level): droiddeck-ubisoft, the store row, sign-in inside Connect, the installed-games scan (registry Installs keys), Play by uplay://launch, Connect closed once the game runs, install handed to Connect with indeterminate progress. Fold in #1445.
  3. Ubisoft store sign-in and library natively: the WebAuth flow, session refresh, a Rust Demux client, the owned library and art, without opening Connect.
  4. Native Ubisoft downloads: a Rust ubisoft module (manifest, slices, zstd), then hand off to Connect for registration (U4). Updates come from the manifest version.
  5. Clean-room record: docs/development/ubisoft-cleanroom.md.

The two stores share almost nothing beyond the existing store layer, so they can run in parallel. EA step 2 has the best ratio of value to risk and could start first.

Risks

  • DRM circumvention. Optima's launcherless mode replaces Ubisoft's DRM loader DLLs, answers the game's ownership calls itself, and installs a certificate mimicking Ubisoft's so Authenticode checks pass. Gating it on real ownership doesn't change that it bypasses Ubisoft's access control. Keep it out of DroidDeck. EA is different: Maxima requests real licences from EA's licence service and speak EA's documented-by-reversing launcher protocol. Nothing is forged.
  • Terms of service. Both publishers' terms broadly forbid unofficial clients. No bans are known for Linux or alt-launcher users (Maxima, Lutris, unifideck, GameVault). This is the same position as our Epic, GOG and Amazon modules.
  • Vendor churn: juno schema changes (EA already killed the Origin-era integrations once), Demux patch versions, and Connect or EA app updates that break under Proton. Plan for tests on recorded responses, as Epic has.
  • Memory. A resident Connect costs a phone RAM, so reuse #979's close-on-launch policy and zRAM work (#1882). EA's native path avoids the client entirely.
  • FEX hardware fingerprint (EA, U1), if FEX's CPUID differs from what licences are bound to.
  • Kernel anti-cheat: Javelin, and whatever Ubisoft titles use one. Flag them in the catalog instead of letting launches fail.

Sources