# Connection-gate findings (clean-room) **Status:** observed behaviour only — derived from running the client and reading the repack's own files. No EA source used. Unconfirmed values are marked `TODO/CONFIRM` rather than guessed. ## Components (observed) | Component | Role | |---|---| | `FIFA23.exe` | Game. Statically links EA's **Ebisu SDK** (online) and **DirtySDK/ProtoSSL** (transport). Loads at fixed base `0x140000000` — no ASLR seen across launches. | | `_ProtoSSLSendPacket` @ `FIFA23.exe+0xEFA530` | DirtySDK TLS record assembler — plaintext in, encrypts in place. Already located + hooked. | | `anadius64.dll` | anadius EA-app/Origin **LSX server** emulator (ASLR'd). Provides offline DRM / entitlements / persona so the game *launches*; deliberately keeps it **offline**. | | `FakeEAACLauncher` | Anti-cheat bypass only. Irrelevant to the online gate. | ## Observed online-startup flow 1. Ebisu SDK opens LSX socket(s) to anadius; a **challenge/response handshake** completes — captured XML: `` / `` / ``, `sender="EALS"`, `ContentId 16115019`. 2. River/PIN telemetry `` is emitted. 3. **No further socket traffic.** Clicking Ultimate Team → "connecting to the EA Servers…" → **zero** network attempts (no Blaze DNS, no `connect`, nothing on `send`/`recv`/`WSASend`/`WSARecv`) → falls back to offline. ## Conclusion: the decision is upstream and in-process - The online/offline verdict is delivered through anadius's **in-process detoured Ebisu calls**, not socket messages. (No `GetAuthCode`/`GoOnline`/ entitlement query appears on any socket, sync or async.) - The game therefore **never reaches DirtySDK/ProtoSSL for Blaze** — it aborts before any `ProtoSSLConnect(gosredirector…)`. The gate is an **Ebisu-SDK connection-state / online-session check upstream of the transport.** - `ONLINE_ACCESS` is a `Blaze::Nucleus::EntitlementType` (enum value `1`). anadius's `GoOnline` LSX handler (`anadius64.dll+0x2BB90`) is a success stub; `anadius64.dll+0xB6B1` is a large entitlement/profile builder that *does* reference `ONLINE_ACCESS`. Reverse-engineering that entitlement-status logic is the **wrong fight** (see strategy). ## Strategy (decided) Do **not** make anadius report "online." anadius exists to keep the game offline, and it can't be removed without breaking launch/DRM. Instead: > Let the game run its **real** online flow and answer that flow ourselves via > the hook + brain. Target the Ebisu connection-state check the FUT-entry path > polls; make the game *pass* it so it issues the real `ProtoSSLConnect` to the > Blaze redirector → our redirect + ProtoSSL hook capture the real frames. This deletes the "reverse-engineer anadius's entitlement logic" problem. ## Next RE step (converges with the ProtoSSL hook) 1. Locate and **read-only hook `ProtoSSLConnect`** (same DirtySDK module and technique that found `_ProtoSSLSendPacket`). Confirms the game never initiates the Blaze connection (pins the gate strictly upstream) and gives us the exact symbol that will flip from "never called" to "called" on success. 2. Locate the Ebisu **connection-state getter** the FUT-entry / "connecting" UI polls. Candidate string anchors (observed): `ONLINE_STATUS_EVENT`, `GoOnline` result handling, the "connecting to the EA Servers" UI trigger. 3. Determine the minimal intervention to make that getter report **connected** (out-hook it in our `version.dll`, winning over anadius's detour) so the game proceeds to `ProtoSSLConnect`. - `TODO/CONFIRM`: whether out-hooking the getter is sufficient, or secondary checks (auth-token presence) also gate the attempt. --- ## M1 results (read-only investigation) Method: `protossl-scan` (xref / disasm / callers, app-module-restricted) against the live client, plus the existing `connect`/DNS/LSX hooks. Observed behaviour only — no EA source. ### Point 1 — is `ProtoSSLConnect` reached on the "connecting" abort? **NO — gate is upstream. CONFIRMED.** - The existing `connect` / `GetAddrInfoW` / `getaddrinfo` hooks show the client makes **zero** network attempts during the "connecting to the EA Servers" abort: no DNS for any `gosredirector.*` host, no `connect` to any external address. - The DirtySDK/ProtoSSL transport is therefore never reached — the abort is entirely upstream and in-process. - `ProtoSSLConnect` has no debug string and sits behind the DirtySDK API, so an explicit hook on it isn't needed to prove this; the behavioural evidence is conclusive. `TODO/CONFIRM`: locate `ProtoSSLConnect` by signature later, as a positive M2 trip-wire (it should fire once we flip the gate). ### Surface mapped (clean-room) - **Redirector host table** (`FIFA23.exe` .rdata, pointer table @ `+0x83FC858`): `spring18.gosredirector.{sdev,stest,scert}.ea.com` + production `spring18.gosredirector.ea.com`. - **Redirector request/response config** (same region): `X-BLAZE-ERRORCODE`, **`Authorization:`**, `` — the redirector request carries an **Authorization header (a Nucleus token)**. - **Enum-name tables** (serialization only, NOT decision code): `ONLINE_ACCESS` (`Blaze::Nucleus::EntitlementType` = 1), `ONLINE_STATUS_EVENT`, … These are reflection tables keyed by enum *value*; string-xref of them is a dead end for the decision (the decision compares values, not strings). ### Point 2 — name the connection-state function: **PARTIAL.** - The exact connection-state decision is behind heavy C++ vtable indirection (the redirector config's two code pointers resolved to a virtual-dispatch thunk @ `FIFA23.exe+0x27BD4C0` and a `ret 0` stub @ `+0x4F3CBC0`). The memory-scan toolkit (string / pattern / xref / callers) cannot efficiently navigate this. - **Pinning the exact function needs a static disassembler with decompilation (Ghidra / IDA) on `FIFA23.exe`.** `protossl-scan` remains the bridge to the live process (mapping static addresses to the ASLR'd runtime, confirming hits, installing hooks). - `TODO/CONFIRM`: name the connection-state getter by module+offset via Ghidra. ### Point 3 — gate count: **TWO gates (state + token). Evidence-backed.** - The online-connect path **reads/requires an auth (Nucleus) token**: the redirector request carries an `Authorization:` header, and the SDK surface has `GetAuthCode` / `FakeAuth` / ``. So it is **not** a single connection-state boolean flip — even with the state forced "online", the client needs a valid token to build the redirector request. - **Conclusion for M2: plan for two gates** — (a) the connection-state decision, and (b) supplying an auth token the client accepts. - `TODO/CONFIRM` the exact abort point (no-token vs state-says-offline vs both) — needs Ghidra-level control-flow tracing. ### Dynamic probe result (read-only) — `GoOnline` is NOT the gate A read-only detour on anadius's `GoOnline` handler (`anadius64.dll+0x2BB90`) shows the game **does** call `GoOnline` during the "connecting" attempt (incl. on the Ultimate Team click) and anadius returns success — **yet no Blaze connection follows** (still zero external `CONNECT`/DNS). Therefore the gate is **downstream of `GoOnline`**: the game decides to go online and the request is accepted, then it aborts at the **auth-token step (`GetAuthCode`) and/or while waiting for the "online established" status callback** (`ONLINE_STATUS_EVENT`), and times out into offline. This sharpens the two-gate picture: `GoOnline` passes; the real blocker is the **token / online-status step**. `TODO/CONFIRM` which (token-missing vs status-never-fires) — via a `GetAuthCode` probe and/or Ghidra. ### Recommendation / next Name the downstream gate. Two complementary routes: - **Dynamic:** probe anadius's `GetAuthCode` handler (does it return a token or fail?) — distinguishes token-gate from status-callback. - **Static (Ghidra):** xref the `GoOnline` / `GetAuthCode` / `ONLINE_STATUS_EVENT` strings in `FIFA23.exe` to find the FUT online-flow code that issues `GoOnline` then waits for the token/status, and decompile it. FIFA isn't ASLR'd, so Ghidra addresses (image base `0x140000000`) map 1:1 to our recorded offsets. --- ## M1 COMPLETE — the gate is `GetInternetConnectedState` Found without Ghidra, by reading anadius's LSX **command-registration** function (`anadius64.dll+0x14C0`), which lists every command-name → handler inline. NB the `lea rax,[handler]` is **offset by one** from the `lea rdx,[name]` in that listing (verified empirically: `+0x27060` decompiles to `GetProfile` — it builds a `GetProfileResponse` for persona "fun"). Corrected handler map: | anadius LSX command | handler (anadius64.dll + …) | |---|---| | GetProfile | 0x27060 | | **GetInternetConnectedState** | **0x27790** | | GoOnline | 0x2BB90 | | GetAuthCode | 0x2BBC0 | ### The gate, named: `GetInternetConnectedState` @ `anadius64.dll+0x27790` It serializes an LSX `InternetConnectedState` response with a `connected` attribute whose value is: ``` connected = (byte[+0xCAB1B] != 0 || byte[+0xCAB1A] != 0) ? str(+0xAF530) : str(+0xADE64) ``` Both flag bytes **default to 0**, so it reports the offline value (`+0xADE64`). That is precisely why the client sits at "connecting to the EA Servers" and falls back offline. ### Answers to the three M1 points 1. **ProtoSSLConnect reached on the abort? NO — gate upstream.** Confirmed. 2. **The connection-state function:** `GetInternetConnectedState` @ `anadius64.dll+0x27790`. Decision = the two-flag branch above. 3. **Gate count: ONE actionable gate.** The auth token is already satisfied — `GoOnline` (`+0x2BB90`) is called during the attempt and returns success, and anadius provides a fake auth code; the blocker is purely the connection-state. So M2 = make `GetInternetConnectedState` report **connected** (then the game proceeds with its existing token). ### M2 attempt results — the gate is an async EVENT, not a poll Tried (read/write, EAAC neutralized): - Forced `GetInternetConnectedState` → connected (set flags +0xCAB1A/+0xCAB1B → value `"1"`; strings confirmed: +0xADE64 = `"0"` offline, +0xAF530 = `"1"` connected). - Flipped `GoOnline` to report `"1"` (replicated its builder `+0x25BE0` with the connected string instead of `"0"`). **Neither made the game proceed.** Both handlers fire, no crash — but the game **keeps retrying `GoOnline` every ~7s** and never attempts the Blaze connect. That retry-on-timeout pattern means the FUT-online flow is **event-driven**: the game submits `GoOnline`, gets success, then **waits for an async "online established" event** (ONLINE_STATUS_EVENT-class) that anadius — being offline-only — never pushes (the only `` it ever sends is the Challenge handshake). So flipping poll/return values can't unblock it. **Implication:** getting past "connecting" requires **emulating the EA-app online event sequence** the game waits for (inject the online-status event over LSX, in the format/order EbisuSDK expects), not a single function flip. This is a substantially deeper task (and precedes the Blaze backend emulation). Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline` and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`. ### M2 deeper finding — worker-thread + event architecture (confirmed) A live call-stack capture from inside the GoOnline detour (manual stack scan, bounded by `GetCurrentThreadStackLimits`) found **zero FIFA23.exe frames** and showed `sp` sitting ~2.4 KB below the thread's stack top. So the handler runs at the top of a short stack — i.e. on an **anadius worker thread** (IOCP/threadpool), not FIFA's calling thread. anadius **queues** the GoOnline command and a worker services it. Combined with the retry behaviour, the architecture is now clear and three-way corroborated: **FIFA calls `EbisuSDK::GoOnline` → anadius queues it → returns → FIFA waits for an async "online established" event → anadius (offline-only, no online-event code) never pushes it → timeout/retry.** No handler-response flip can unblock this; the game waits on a *push* anadius never produces. **Conclusion:** crossing this gate requires emulating the EA-app online-event sequence (synthesize + inject the online-status event on the worker→game callback / LSX path, in EbisuSDK's expected format) — a research-grade emulation effort, preceding the Blaze backend. The cheap in-process flips are exhausted. Reaching the FIFA-side flow would need either: (a) locate `EbisuSDK::GoOnline` in FIFA23.exe via anadius's detour table, then `callers` to the online-flow; or (b) find anadius's LSX event-send path and reverse the online-event format. Both are deep. `TODO/CONFIRM`. ### Path A attempt: reach the FIFA-side online-flow (blocked with live toolkit) Goal: find `EbisuSDK::GoOnline` in FIFA23.exe → `callers` → the game's online-flow → read what event it waits on. Every angle our live-memory toolkit offers is blocked: - **String xref:** FIFA23.exe contains no `"GoOnline"` string (typed SDK call, not a string-built command). - **Call-stack from the handler:** GoOnline runs on an anadius worker thread; a bounded stack scan finds zero FIFA frames. - **Detour scan (`jmpscan`):** scanning FIFA23.exe for function-entry `E9` jumps leaving the module yields ~3875 hits — overwhelmingly false positives, because the 505 MB image is mostly embedded *data* (not code), and the real detours don't cleanly cluster. (A .text-section-only scan would help but the chain after — isolate GoOnline → callers → event format → emulate — remains long and each link is gated by SDK abstraction / anadius indirection / worker threads.) **Verdict:** crossing this gate to *playable* FUT is research-grade. It needs an interactive disassembler (IDA/Ghidra GUI, human-driven) to trace the EbisuSDK online-flow, and then a full EA-online + Blaze emulator. The live-memory toolkit (string/xref/disasm/callers/read/jmpscan) has been exhausted for the FIFA side. The clean-room spec (this document) is the finished, valuable artifact. - Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius config / a hidden option (cheapest flip). - Else out-detour `GetInternetConnectedState` in our `version.dll` to force the `+0xAF530` ("connected") path. - Then watch for the client to attempt the real Blaze connect (our `connect` redirect + ProtoSSL hook capture the first plaintext — milestone M3). - `TODO/CONFIRM`: exact text of the offline/connected value strings (`+0xADE64` / `+0xAF530`); whether any secondary check gates the attempt after the state flips. --- ## LSX delivery channel — 2026-06-30 **Question:** does FIFA's port-3216 connection reach the bridge LSX server, or is it serviced inside anadius before the TCP stack (making the bridge irrelevant)? ### Evidence from `openfut_hook.log` (6 consecutive sessions, no bridge running) ``` connect_hook: call 1.0.0.127:3216 sock_type=1 connect_hook: result=0 wsa_err=0 ``` - `1.0.0.127:3216` — FIFA's EbisuSDK connects to `127.0.0.1:3216` (address in the log is little-endian u32; port is big-endian u16 → both decode to `127.0.0.1:3216`). - `sock_type=1` — `SOCK_STREAM` (TCP), not a pipe or named-object. - `result=0 wsa_err=0` — the OS `connect()` syscall returned success. - This line appears exactly once per game session, at early startup (DLL load phase), before the game reaches any menu. No other external `connect()` calls are logged. ### Implication: anadius is NOT in-process-only for LSX Our hook is an **inline detour on the real `ws2_32.dll!connect`** function (not an IAT patch). It fires regardless of who calls `connect()`. Because it fires for the 3216 call and `result=0`, the connection goes through the real TCP stack — it is NOT intercepted inside anadius before the socket layer. In the sessions above, anadius was the answering server (bridge not running); it binds `127.0.0.1:3216` as a real Wine/Proton TCP socket. **Anadius is a true TCP server on port 3216, not an in-process detour.** ### Bridge delivery test (native Linux process) Bridge binary confirmed bindable: `./target/release/openfut-bridge` starts and logs `LSX server listening on 127.0.0.1:3216` immediately. Port 3216 is free before FIFA launches (anadius only binds it when FIFA/anadius64.dll loads). **Pending live test:** start the bridge (native Linux process → real Linux socket on 3216), THEN launch FIFA. When EbisuSDK calls `connect(127.0.0.1:3216)`: - If bridge receives it → bridge logs `DEBUG openfut_bridge::lsx: LSX: connection from 127.0.0.1:xxxxx` - If anadius somehow wins (or game crashes because anadius can't bind) → bridge logs nothing **Run once to confirm:** ```bash cd /home/alex/Documents/OpenFUT/openfut-bridge RUST_LOG=openfut_bridge=debug ./target/release/openfut-bridge 2>&1 | tee /tmp/bridge-lsx-test.log & # then launch FIFA normally, reach any menu, check log: grep "LSX:" /tmp/bridge-lsx-test.log ``` ### Pre-test concern: LSX challenge-response AES key The bridge `lsx.rs` uses a placeholder `AES_KEY = [0,1,2,...,15]`. The real key was RE'd from anadius but not yet substituted. If FIFA validates the `ChallengeAccepted` response the bridge sends, the handshake fails and FIFA may not reach the main menu. This does **not** affect the delivery classification — `LSX: connection from ...` fires at TCP `accept()`, before any protocol. But it may cause the game to stall/crash after the first connection, which would be a separate issue to fix. ### Classification — CONFIRMED 2026-07-02 (supersedes the 2026-06-30 prediction) **CONFIRMED NEGATIVE — a host-side bridge on 3216 does NOT receive FIFA's LSX traffic. Cause: anadius handles LSX *in-process*, inside FIFA23.exe, before the connect reaches the host TCP stack. This is NOT a network/loopback problem — the network namespace is shared and the bridge is reachable.** The 2026-06-30 prediction that this subsection replaced was wrong on both counts (it guessed `"anadius intercepts in-process" = RULED OUT` and `"bridge receives LSX" = EXPECTED`); the live test showed the exact opposite. **Evidence (live test, bridge holding host 127.0.0.1:3216 throughout):** - **Namespace is shared, not isolated.** `readlink /proc//ns/net` equals the host's `/proc/self/ns/net` (both `net:[4026531833]`). Pressure-vessel /umu does not unshare the network — Wine's `127.0.0.1` *is* the host's. - **The bridge is reachable and logs correctly.** A host-side `connect(127.0.0.1:3216)` was accepted, logged `DEBUG openfut_bridge::lsx: LSX: connection from 127.0.0.1:34730`, and returned the 157-byte LSX greeting. So "bridge logged nothing during FIFA" is a real absence, not a logging/filter artefact. - **FIFA's connect succeeds but never reaches the bridge.** `openfut_hook.log` shows 8× `connect_hook: call 1.0.0.127:3216 ... result=0 wsa_err=0` (real, successful connects) while the bridge log stayed at its 8-line startup baseline (zero `LSX: connection from`). FIFA then advanced to "connecting to FUT" — anadius satisfied LSX in-process; the hook log stopped growing once it did. - **Our hook does not fake the 3216 result — it passes it through.** In `openfut-launcher/openfut-hook/src/connect_hook.rs`, `redirect_if_ea` rewrites only 443/10041/42127; port 3216 falls through to the real `ws2_32!connect` and its genuine return is logged. The comment there ("3216 ... handled by the native openfut-bridge LSX server — just let connect() go through") encodes the disproven premise: "going through" reaches anadius in-process, not the host bridge. **Conclusion:** the "native Linux bridge binds 3216 first and wins the race" strategy cannot work as designed — there is no host-stack connect to win; anadius answers inside the process. A host-side LSX server is architecturally off the path. > **OVERTURNED 2026-07-02** — see the next section. This conclusion was correct only > for *port 3216*. anadius keys its in-process interception on port 3216 specifically; > a hook-side redirect of FIFA's connect to a *different* host port (3217) slips past > it and lands on the host bridge over the shared loopback. A host-side LSX server > **does** work once the redirect is in place. The "fix direction" below is now done. **Consequences / open items:** - `TODO/CONFIRM`: the exact interception layer anadius uses (IAT hook on FIFA's `connect` import / Winsock LSP / inline hook on a lower function) is undetermined. Our inline hook on `ws2_32!connect` *does* fire, so FIFA reaches `ws2_32!connect`; yet the real call returns 0 without touching the host socket — naming the layer that absorbs it requires inspecting anadius. - The placeholder `AES_KEY` was **never exercised** — delivery fails upstream of any handshake, so the challenge-response never ran. The earlier "supply the real AES key" follow-up is moot unless/until FIFA is actually routed to our LSX server. - **Fix direction (not yet implemented):** the viable lever is our own hook, not anadius (which is closed, load-bearing, and binds no host port to free). Candidate: have the hook redirect the 3216 connect to a distinct host port the bridge listens on, to slip past anadius's port-specific in-process interception — routing LSX to our bridge on the shared host stack (this revives the real-AES-key requirement above). Scoped to `openfut-hook`, not this repo. --- ## LSX SOLVED via hook redirect + session-seed fix — 2026-07-02 **RESULT: FIFA now completes the LSX handshake AND a sustained encrypted session against our own bridge, live.** The "fix direction" from the previous section was implemented and works. This is the first EA-side gate serviced end-to-end by our code. ### 1. Hook redirect gets FIFA onto the bridge (delivery achieved) Added one arm to `redirect_if_ea` in `openfut-launcher/openfut-hook/src/connect_hook.rs`: FIFA's connect to `127.0.0.1:3216` is rewritten to `127.0.0.1:3217`, where the bridge runs its LSX server (`LSX_ADDR=127.0.0.1:3217`). anadius's in-process interception is keyed on port **3216 specifically**, so the redirected connect slips past it and lands on the host bridge over the shared loopback. - Hook log: `connect_hook: 1.0.0.127:3216 → 127.0.0.1:3217` then `redirect result=0`. - Bridge log: `LSX: connection from 127.0.0.1:…` — **FIFA reached the bridge.** Overturns the "off the path" conclusion above. ### 2. AES key was never a placeholder — it is `000102…0f` The `AES_KEY = [0,1,2,…,15]` in `src/lsx.rs` (and the hook's `lsx.rs`) is the **real, correct** key, confirmed two ways: - **Oracle:** a captured FIFA `ChallengeResponse` gives a known plaintext→ciphertext pair under the real key — our greeting `cacf897a20b6d612ad0c05e011df52bb` (ASCII) → FIFA's `5c52e8ca…`. `AES-ECB-PKCS7([0..15], greeting)` reproduces it byte-for-byte. - **Static:** that same 16-byte value is embedded literally in `anadius64.dll` at offsets `0xa1710`, `0xa1890`, `0xa3c0f`. The earlier "supply the real AES key" TODO is therefore **closed — no key extraction was needed.** The handshake auth was always correct; FIFA accepted our `ChallengeAccepted`. ### 3. The real bug: session-seed derivation (now fixed) After a *correct* handshake, the first encrypted session message still decrypted to garbage. Cause: `compute_seed()` derived the session-key seed by **parsing** the first hex pair of our response (`"0f"` → `0x0F`), when the client uses the **raw ASCII byte values of the first two characters** (`'0','f'` → `0x30,0x66`). Confirmed by capturing the raw session ciphertext (bridge patched to log wire bytes) and brute-forcing all 65,536 u16 seeds offline through `get_lsx_key(seed)`: - Exactly **one** seed, `0x3066` (12390), decrypted the message to valid XML: ``. - `0x3066` = ASCII `'0','f'` = the first two characters of our response hex `0f73…`, **not** the parsed value `0x0F73` (3955) we were using. Fix in `src/lsx.rs` `compute_seed()`: take `hex.as_bytes()[0..2]` directly instead of `u8::from_str_radix` on the hex pairs. (Capture preserved at `captures/lsx-handshake-capture-2026-07-02.log`; brute-forcer in scratchpad.) ### 4. Live confirmation With the redirect deployed and the seed-fixed bridge on 3217, FIFA relaunched and: - Held a **single sustained LSX connection** (`ss` shows `ESTAB FIFA23.exe ↔ openfut-bridge:3217`) instead of the previous connect-once-then-crash. - Drove **20 real EbisuSDK requests** in one conversation: `GetConfig`, `GetProfile` (×5), `GetSetting`, `GetGameInfo` (×8), `SetPresence`, `GetInternetConnectedState`. - **Did not crash** at the LSX phase. ### 5. Remaining gap (next step, not a blocker to this result) `GetGameInfo GameInfoId=…` had no handler in the bridge dispatcher → fell to the generic `` and logged `unknown request type`. FIFA tolerated it (session stayed up), but real `GetGameInfo` responses may be needed to progress toward the next gate (Blaze/online). **Update 2026-07-02 (later):** a `get_game_info` handler was added to `dispatch()` in `src/lsx.rs`. It routes `GetGameInfo GameInfoId=…`, **logs the requested `GameInfoId`** (so the next live run reveals which keys FIFA asks for — we still have no captured real response to model), and replies with a well-formed `` echo. A dispatcher routing test (`dispatch_routes_get_game_info_and_echoes_id`) pins the fragile positional `split('"')` parsing. `TODO/CONFIRM` remains: the real response schema and whether FIFA needs non-empty values — resolve by launching FIFA with this build, reading the logged `GameInfoId`s, and observing how far FIFA advances. ### Provenance note Clean-room intact: the AES key was recovered by validating candidate byte windows from the shipping `anadius64.dll` against the client's own observed output, and the seed rule was recovered by brute-forcing against a live capture — nothing derived from EA source. --- ## LSX fully satisfied — FIFA reaches the Blaze/online gate — 2026-07-02 (later) **RESULT (unconfirmed — read the caveat): FIFA reached the "connecting to EA servers" screen alive, but the launch that did so used a CACHED EA session and did NOT re-hit our bridge (no new `3216` connect, empty bridge log). The last launch that actually ran a fresh LSX session through our bridge still CRASHED. So it is NOT yet established that the fixed bridge carries a fresh FIFA launch past the crash — the crash-clear is confounded by session caching.** The fixes below are believed-correct and unit-tested, but need the clean re-test in the "Confirmation caveat" section before LSX can be called solved. Earlier in the day FIFA crashed ~15–17s into LSX, "right before the loading screen." Root-causing that crash took several live runs and disproved two wrong theories. ### The real root cause: dropped pipelined LSX messages (session-loop framing bug) FIFA batches multiple LSX messages into a single TCP segment — e.g. `SetPresence` immediately followed by a `GetGameInfo GameInfoId="LANGUAGES"` query, two null-terminated ciphertext-hex blocks in one read. The old session loop decrypted the whole read as one blob and dispatched only the **first** message, silently dropping the rest. The dropped request left FIFA waiting on a response that never came; it timed out after ~15s (the mysterious silent gap in every crash log) and then crashed. Fix: `src/lsx.rs` `process_frame()` splits each read on the null terminators and answers **every** sub-message, returning the concatenated null-terminated responses. Regression test: `process_frame_answers_every_pipelined_message`. After the fix, FIFA flowed through the entire LSX sequence in ~1.3s instead of hanging 15s. ### Two theories the live runs DISPROVED first (recorded so we don't repeat them) 1. **"Empty/incorrect INSTALLED_LANGUAGE value crashes it."** Changing the value (`""` → `en_US`) changed nothing — same 3× re-query, same crash. Because… 2. **"Wrong attribute name only."** Switching `Value=""` → `InstalledLanguage="en_US"` also changed nothing *on its own* — because the crash-relevant request (the pipelined `LANGUAGES`) was never being dispatched at all (see root cause). The attribute name *did* matter, but only became observable once pipelining was fixed: FIFA reads a lowercase `value` attribute (case-sensitive), so the response now emits the value under BOTH `value` and the per-key CamelCase name (`InstalledLanguage` / `Languages`). ### anadius.cfg is the response oracle `/mnt/games/FIFA 23/anadius.cfg` is the config anadius's in-process LSX emu reads to build its responses — the clean, correct values to mirror (same provenance basis as the AES-key recovery; it is anadius's own config, not EA source): | Field | Value | |---|---| | `Language` (INSTALLED_LANGUAGE) | `en_US` | | `Languages` (LANGUAGES) | `pt_PT,tr_TR,ko_KR,cs_CZ,zh_CN,zh_HK,da_DK,no_NO,sv_SE,en_US,pt_BR,de_DE,es_ES,fr_FR,it_IT,ja_JP,es_MX,nl_NL,pl_PL,ru_RU,ar_SA` | | `PersonaId` | `1144668899` | | `UserId` | `1000200030000` | | `Username` (Persona) | `fun` | `src/lsx.rs` now returns these real values (GetProfile IDs, GetGameInfo language keys) instead of fabricated ones; fabricated persona/user IDs flow into FIFA's online state and are a likely downstream break. Also added a real `IsProgressiveInstallationAvailable` (`Available="false"`, attribute recovered from `anadius64.dll`). ### Confirmation caveat + how to verify cleanly The launch that first reached the online gate did **not** re-hit our bridge — FIFA presented a cached EA session (no new `3216` connect; empty bridge log) and jumped straight to "connecting to EA servers." So that specific run didn't exercise the final `value`-attribute change. To confirm on a clean boot, clear FIFA's cached EA/Origin session token in the Wine prefix so it re-runs LSX through the bridge, and verify: one sustained LSX session, `INSTALLED_LANGUAGE` queried **once** (not 3×), no crash, and FIFA arriving at "connecting to EA servers". ### GetGameInfo response shape (RE'd from the anadius64.dll builder, 2026-07-02) Populating `GetAllGameInfo` did **not** fix the crash — FIFA still ran the full LSX session (~2.6s, 17 dispatches) then crashed at the same point, ending with the 3× `INSTALLED_LANGUAGE` re-query. So `GetAllGameInfo` was never the trigger. Disassembling the **individual** `GetGameInfo` response builder in `anadius64.dll` (around `0x18002a076`) settled the actual wire shape. It emits, in order: `<` + `GetGameInfoResponse` + ` ` + `GameInfo` + `="` + + `"` + ` />`. The attribute name is the **fixed 8-byte literal `GameInfo`** (string constant at `.rdata` VA `0x1800af818`), *independent of the requested `GameInfoId`* — the value is looked up per key but the attribute is always `GameInfo`. So the correct reply to `GetGameInfo GameInfoId=INSTALLED_LANGUAGE` is: ``` ``` Our bridge had been sending the key as the attribute name (``), an attribute FIFA never reads. That is why FIFA always saw a null installed-language, re-queried `INSTALLED_LANGUAGE` three times, and crashed — and why *every* prior value change (empty → `en_US`, per-key naming) had no effect: FIFA wasn't reading our attribute at all. `src/lsx.rs` `get_game_info()` now emits the fixed `GameInfo=""` attribute. ### ROOT CAUSE of the loading-screen crash: GetConfig missing PROGRESSIVE_INSTALLATION (2026-07-02) FIFA's own crash reports (`FIFA 23/Logs/CrashReport_*.json`) pinned it. **Every** run through our bridge — all 11 today, both the pre-pipelining (`Up Seconds: 17`) and post-pipelining (`Up Seconds: 2`) runs — crashed at the **identical** address: ``` EXCEPTION_ACCESS_VIOLATION, reading 0x18 RIP = FIFA23.exe+0x133dfc5 → mov rax, QWORD PTR [r13+0x18] with r13 = NULL Is Online: False, Blaze State: {} (crash is pre-Blaze, ~2s after LSX) ``` So none of the earlier LSX-response fixes (pipelining, account IDs, GetGameInfo attribute, GetAllGameInfo population) ever touched the crash — it was **invariant**, i.e. caused by a response that had been wrong the *whole time*. Disassembling the faulting function showed r13 is its 3rd argument (a Frostbite ref-counted service *handle*) — a null handle deref, i.e. a **failed service lookup**. That pointed straight at `GetConfig` (request #1, the fabricated service list, never verified). Extracting anadius's real service-registration table from `anadius64.dll` (the `lea rdx=Name / lea r8=Facility` sequence at `0x180026d40`) and diffing it against our `get_config()` showed our list was correct on all 32 entries **except it omitted two services**: ``` Facility="PROGRESSIVE_INSTALLATION" Name="PI" Facility="PROGRESSIVE_INSTALLATION_EVENT" Name="PI" ``` FIFA queries `IsProgressiveInstallationAvailable` in every LSX session; it builds its service registry from `GetConfig` and resolves the `PROGRESSIVE_INSTALLATION` handle from it during online-init. With the service absent, the handle was null → the 0x133dfc5 deref. Fix: `get_config()` now declares both services, and `is_progressive_installation_available()` responds from `sender="PI"` (the service's registry Name). This is the first change that targets the *actual* crash address, so it is the real test of the loading-screen gate. ### M2 unblock lever RECOVERED: the ONLINE_STATUS_EVENT push format (2026-07-02) With the LSX gate cleared, FIFA sits at "connecting to EA Servers", polling GetProfile over LSX. Per the M1/M2 findings this is the async-push wall: FIFA calls GoOnline (handled in-process by anadius/EbisuSDK), gets success, then **waits for an `ONLINE_STATUS_EVENT` push on the LSX socket that never arrives.** Earlier attempts couldn't send it — it was unclear the bridge even owned the LSX socket (anadius intercepted in-process). **That blocker is now gone: our bridge provably owns FIFA's LSX socket** (full session runs on :3217), so we can finally push the event. RE recovered the exact format the client parses (authoritative — from FIFA23.exe's own OriginSDK deserializer, not a guess): - **Event wrapper** (from anadius64.dll's IGOEvent template): `<…/>` - **Element** `OnlineStatusEvent` — confirmed via FIFA23.exe's retained RTTI symbol `Origin::EventHandler::HandleMessage` (source path `OriginSDK/10.6.2.19-fb-fifagame/src/impl/OriginEventClasses`) and the event-name dispatch table. Handler parser at FIFA23.exe+0x274d4ae. - **Attribute** `isOnline` (a single bool) — recovered from the typed deserializer at FIFA23.exe+0x28a4d0: it inits the output byte to 0 (offline), builds an `lsx:` namespace prefix, reads the `isOnline` attribute, and string→bool converts it. Best-effort push (value format `1` vs `true` is `TODO/CONFIRM` against the client — OriginSDK bool attributes elsewhere use `true`/`false`): ```xml ``` **Next implementation step:** give the bridge LSX server an async, unsolicited-push path — encrypt this event with the live session seed and write it on the open socket after the session establishes (or after FIFA's GoOnline settles). Success signal: FIFA stops polling and **dials the Blaze redirector** (gosredirector → :42127 via the hook), which yields the first Fire2 frame and moves us into M3/M4 (fifa-blaze). ### RESULT: OnlineStatusEvent push drives FIFA online — a first — but the auth→Blaze chain is the next wall (2026-07-02) Implemented an unsolicited `OnlineStatusEvent` push in the bridge LSX server (`select!` loop, encrypted with the session seed, re-pushed every 2s). Value `isOnline="true"` (FIFA's bool parser at FIFA23.exe+0x28fb50 string-compares to `false`; `isOnline="1"` was delivered but parsed as not-online — pinned the encoding). **FIFA reacted — the event format is correct and effective:** - Rapid GetProfile polling (~0.5s) stopped the instant the pushes began (13s gap). - FIFA set `SetPresence … Presence="INGAME"` — an *online* action. - Then it went **silent on LSX** (idle, connection alive, no crash). This is the first time FIFA has been driven "online" at the EbisuSDK/LSX level — **anadius never does this** (it is offline DRM and deliberately keeps FIFA offline), so this path is genuinely uncharted, not a regression from a known-good baseline. **But FIFA does NOT proceed to Blaze.** After going online it: - sent **no** `GetAuthCode` (an LSX request the bridge already answers) — so it never started the Nucleus-token exchange; - resolved **no** hostname (`hooked_getaddrinfo` logs every call; the hook log has none) — no gosredirector, no accounts.ea.com; - opened **no** socket beyond LSX (:3216). So a single connectivity event ≠ full online login. RE of FIFA23.exe confirms the remaining chain and its anchors: `GetAuthCode` (12×) → `NucleusAccessToken` / `access_token` / `Bearer` / `Authorization` (redirector header) → `gosredirector` (4×) / `BlazeHub` (3×) → Blaze auth. `OnlineStatusEventT::HandleMessage` (FIFA23.exe+0x274d4ae) only builds the typed event and dispatches it via the SDK callback; the game-side listener that would kick off the auth chain needs more than the connectivity flag (candidate: a login/authenticated-online event or an in-process EbisuSDK login step that our LSX-only redirect doesn't cover). **Strategic note:** this is the M2→M6 research frontier the 2026-06-30 status review priced at months of expert RE, now empirically confirmed. It sits against the 2026-06-30 direction pivot, which made the app-centric FLE/career-mode path the primary plan over full Blaze-backend RE. Decision point before sinking weeks into the Nucleus/Blaze stack. ### Trigger hunt: the missing link is the `Login` event (OriginLoginT) (2026-07-02) Traced why FIFA stalls after going online. The full post-online chain and its FIFA23.exe anchors: ``` OnlineStatusEvent(isOnline=true) ✓ delivered, FIFA set INGAME presence │ [MISSING game-side trigger] GetAuthCode (LSX request we answer) ── never sent │ Nucleus token exchange (accounts.ea.com HTTPS, Bearer/Authorization) ── no DNS │ spring18.gosredirector.ea.com (HTTPS redirector: Authorization + serviceName, env-selected by ENVIRONMENT="production"; headers X-BLAZE-ERRORCODE) │ Blaze server (BlazeHub::LoginManagerImpl, Fire2) ── never reached ``` Login lives inside Blaze (`BlazeHub::mLoginManagers`), which needs a Nucleus token first, which needs `GetAuthCode`, which FIFA never issues. So the block is upstream: the game-side reaction to online doesn't start the auth chain. **Likely cause — no authenticated session.** FIFA's Origin event set (from retained RTTI) includes `Origin::EventHandler` — a `Login` event carrying a full `OriginLoginT` struct (identity/session/token), distinct from `OnlineStatusEvent`'s single bool. Our push set *connectivity* (presence went INGAME) but not an authenticated *session*. anadius never pushes `Login` (offline DRM), so FIFA has a persona (from GetProfile) but no online session token → nothing to drive `GetAuthCode`. **Next experiment:** RE the `LoginT` deserializer (element `Login`, payload `OriginLoginT`) the same way as OnlineStatusEvent, synthesize a `` event push with a fabricated session, and relaunch. Success signal: FIFA issues `GetAuthCode` over LSX and/or resolves `spring18.gosredirector.ea.com` (both logged) — the first move into the Nucleus/Blaze stack. ### Login event pushed — FIFA still won't start the auth chain (2026-07-02) RE'd the `Login` event from FIFA23.exe's `LoginT` deserializer (0x142787a30): fields `UserIndex` (int), `IsLoggedIn` (bool, same true/false parser as isOnline), `LoginReasonCode` (int). Pushed `` before OnlineStatusEvent each cycle. Result: **no change.** FIFA reacts to connectivity (presence INGAME, polling backs off) but issues no `GetAuthCode`, resolves no host (`getaddrinfo` count 0), dials no Blaze. So Login + OnlineStatus (connectivity + authenticated flags) are **not** sufficient to trigger the game-side auth chain. **Boundary reached for black-box LSX emulation.** We can drive every LSX-wire signal FIFA reads, and FIFA visibly reacts, but the decision to start GetAuthCode→Nucleus→ Blaze lives in FIFA's game-side online orchestrator, whose gating state is invisible from the LSX wire (it also makes in-process EbisuSDK calls that go to anadius, not our socket — we can't see those). Guessing more LSX events is low-yield from here. To progress, the next step must convert guessing into observation — **instrument FIFA's in-process online flow** via the hook DLL: detour the EbisuSDK/game functions around `GetAuthCode`, `GoOnline` (anadius64.dll+0x2BB90), and the OnlineStatusEvent consumer, and log what FIFA does after our events (whether it calls GetAuthCode in-process, what it checks, where it returns early). That reveals the exact gate. Alternatively this is the wall the 2026-06-30 direction pivot anticipated — the app-centric FLE/career path avoids the online-backend entirely. Bank: LSX gate cleared; FIFA driven "online" at the EbisuSDK connectivity/login level for the first time (anadius never does); OnlineStatusEvent + Login formats recovered and confirmed effective at the event level. ### In-process instrumentation results (2026-07-02) Built a `probe` feature into openfut-hook (unhook/rehook logging detours, like connect_hook) on four online-flow functions. Deployed + ran with the event push active. Results (C:\openfut_hook.log): - **Our events reach FIFA's code and parse successfully.** `OnlineStatus.deser` (FIFA23.exe+0x28a4d0) and `Login.deser` (+0x287a30) each fired 44× (once per 2s re-push) and returned success (0x1 / valid ptr). The push works at the deepest level — not a delivery problem. - **`GoOnline` (anadius64.dll+0x2bb90) never fires.** The 2026-06-30 review saw FIFA calling GoOnline and *retrying every ~7s*; with our bridge + events FIFA is not in that retry loop at all. Our events moved it out of the retry state — but it does not advance. - **`GetInternetConnectedState` (anadius64.dll+0x27790) never fires** — FIFA asks it over LSX (to our bridge) instead; anadius is fully bypassed for LSX. **Gate localized to the game-side event consumer.** After the deserializer, `OnlineStatusEventT::HandleMessage` delivers the value via a virtual call (`call [rax+0x28]`, FIFA23.exe+0x274d4e2) to a polymorphic game listener. FIFA parses "online + logged in" every cycle but the listener does not act (no GoOnline, no auth, no Blaze). The concrete listener is a runtime vtable target — not statically pinnable. Note vs. the old M2 finding: forcing anadius's in-process state flags to online left FIFA *retrying*; pushing our events *stops* the retry but doesn't advance. Neither path alone flips the game-side online state machine — the real gate is in FIFA's own online orchestrator, reachable only by probing the runtime virtual listener or a deeper RE of that state machine. ### Live listener captured — the event callback is a NO-OP (2026-07-02) Added a behavior-preserving mid-function detour (probe.rs `openfut_listener_stub`, patch at FIFA23.exe+0x274d4d7) to capture the runtime target of the OnlineStatusEvent dispatch (`call [rax+0x28]`). Result: ``` PROBE OnlineStatus.listener: vtable=0x1480b9ac0 (rva 0x80b9ac0) fn=0x142751060 (rva 0x2751060) ``` `FIFA23.exe+0x2751060` is `ret 0x0` — a **no-op**, one of a table of identical `ret 0` stubs (i.e. the default/empty virtual slot; this object does not override the online-status notification). So the immediate event callback does nothing. **Reframes the approach.** `OnlineStatusEventT::HandleMessage` does two things: it stores the parsed bool into a map (state), then calls this (no-op) notify. So our push updates FIFA's *stored* online state but triggers no active reaction — there is no callback to kick off the auth/Blaze chain. The transition must be driven by FIFA **polling** that stored state (consistent with the observed GetProfile / GetInternetConnectedState polling), gated on some combined condition our single online flag doesn't satisfy. Event-pushing alone cannot trigger it; the next dig is the state reader / poll gate (who reads the map HandleMessage writes, and what else it requires), which is deeper FIFA-internal RE. The probe tooling now cleanly captures live virtual-dispatch targets (alignment-safe mid-function detour) — reusable for the next layer. ### Next gate: Blaze/online (M2) "connecting to EA servers" is the Blaze layer. The hook log shows FIFA has made **no** Blaze connect yet (nothing on 443/10041/42127) while spinning, so it is stalled at pre-auth. Per the architecture, Blaze rides ProtoSSL in-process (a different transport than LSX), so serving it is separate, larger work — the M2 effort, now actually reached. --- ## GetAuthCode request format recovered — the gate is job-enqueue, not event-push (2026-07-02) Continuing past the "live listener is a NO-OP" finding, I traced the *other end* of the gate: the LSX request FIFA must send to start the Nucleus→Blaze chain, and what gates it. Static RE of FIFA23.exe (clean-room; the client's own serializers are the oracle). **The request FIFA would send (exact, from its serializer at FIFA23.exe+0x278d2c0):** ``` ``` - Serializer `GetAuthCodeT::Serialize` @ **FIFA23.exe+0x278d2c0**. Element string `GetAuthCode` @ .rdata 0x1480c27d0 (its *only* xref — this is the sole builder). - Field layout in the request struct: `UserId=[rbx+0x00]`, `ClientId=[rbx+0x08]` (string), `Scope=[rbx+0x28]` (string), `AppendAuthSource=[rbx+0x48]` (bool). - Attribute-name strings: `UserId` 0x147ea7bc8, `ClientId` 0x1480c2c98, `Scope` 0x147c79514, `AppendAuthSource` 0x1480c2ca8. - This is rung 1 of M2's auth chain: FIFA asks the SDK (== us, over LSX) for a Nucleus auth code; it then exchanges that code at accounts.ea.com → token → gosredirector → Blaze. We now know the exact request to answer once we can make FIFA send it. **Who sends it — and the actual gate.** The request is wrapped by a **`GetAuthCodeJob`** inside FIFA's EASFC online-job module (a large block ~FIFA23.exe+0x51b0000..0x528xxxx; job/response telemetry tags `GetAuthCodeJob` @ 0x1484630d0, `GetAuthCodeJobResponse` @ 0x148455b98). The job's completion/response handler is at **FIFA23.exe+0x51b66c0** (sets its done-flag `[r15+8]=1`, logs via those tags). The serializer is invoked polymorphically from the LSX client when the job runs — it is **not** called directly, so there is no static call-xref from the trigger; the job is *enqueued* by the EASFC online controller's state machine. **Conclusion (the gate, precisely stated).** The wall is **not** on the LSX wire and **not** in event delivery (our OnlineStatus/Login events parse fine; cf. the no-op-listener finding above). The wall is that FIFA's **EASFC online controller never enqueues `GetAuthCodeJob`**. That controller decides "am I online enough to authenticate?" by *polling* its cached online state (the map `OnlineStatusEventT::HandleMessage` writes), and our single `isOnline=true` flag does not satisfy its combined condition. So: - Event-push alone cannot cross it (confirmed twice now, from both ends). - The true next artifact is the **EASFC controller's online-readiness poll** — the guard, in that 0x51b0000..0x528xxxx module, that must pass before `GetAuthCodeJob` is enqueued. That is a large state-machine RE (hundreds of KB of EASFC job code), i.e. the genuine multi-week M2 frontier the status review priced. **Net for this session:** recovered the exact `GetAuthCode` request format (so the round trip is ready when we cross the gate) and localized the gate to a specific FIFA module and mechanism (EASFC job-enqueue poll, not LSX/events). This closes the "is it the events?" question — it is not. The remaining work is FIFA-internal EASFC state-machine RE, which sits directly against the [[project_direction_pivot]] (FLE career mode is the stated primary plan) as a cost/priority decision, not a technical unknown on the LSX side. --- ## Gate pinned to a concrete object: the Nucleus session context `[mgr+0x778]` (2026-07-02, dig 2) Traced the send path all the way up to FIFA's game-side Nucleus-connect layer and found the exact null-check that stops the auth chain. Clean-room (FIFA's own code + its embedded source-path strings are the oracle). **Send path (confirmed, top to bottom):** - `GetAuthCodeT::Serialize` @ FIFA23.exe+0x278d2c0 → request ``. - Sent by `Origin::OriginSDK::SendXml` (embedded path `E:\packages\OriginSDK\10.6.2.19-fb-fifagame_11164847\src\impl\OriginSDKimpl.cpp`), internal senders FIFA23.exe+0x2732f50 / dispatch +0x2737df0. Statically linked into FIFA23.exe (not a DLL) — anadius does not emulate this layer. - Requested by FIFA's **game-side Nucleus-connect layer**: `nucleusConnectREST` @ FIFA23.exe+0x2861910 and `nucleusConnectTrusted` @ FIFA23.exe+0x5078370 (found via the log-tag strings `nucleusConnectREST` 0x1480db550 / `nucleusConnectTrusted` 0x148441c18). **THE GATE (sharp).** Both connect paths **null-check the same object** — the Nucleus session/context at **`[NucleusManager+0x778]`** — and short-circuit when it is null: - `nucleusConnectREST` (+0x2861910): loads `mgr = (*GetMgr())->vt[0x178]()`, reads `[mgr+0x778]`; **if null → returns 0, builds/sends nothing** (no GetAuthCode). - `nucleusConnectTrusted` (+0x5078370): if `[[this+0x10]+0x778]` is null → returns HRESULT **0x80060000** ("not ready"); its caller state (+0x507d660, part of the online-login state machine — state handlers are vtable slots dispatched via `[rax+0xb8]`) then waits/retries instead of advancing. So the online→auth transition is gated on **the Nucleus session context `+0x778` being instantiated**, and in our setup it never is. This is fully consistent with the earlier no-op-listener finding: pushing `OnlineStatusEvent` updates FIFA's *stored* online bool but does not drive the login state machine to the step that creates the `+0x778` context, so both auth-code paths keep null-checking it and bailing. Event-push cannot create it. **Why static tracing stops here:** `nucleusConnectREST` is a virtual (vtable slot 0x1480db478) and the manager-getter (+0x27d9970) is a ubiquitous singleton (hundreds of callers), so the exact state that *would* call REST — and the predicate that instantiates `+0x778` — are not pinnable by call-xref alone. **Concrete next step (dynamic, cheap, ready to build):** with FIFA at "connecting to EA servers," probe (hook DLL, existing `probe` feature) **nucleusConnectREST @ +0x2861910** and the **`[mgr+0x778]` read** to observe: (a) is REST even called on our path? (b) what does the manager hold at +0x778, and (c) if REST is never called, which online-login state FIFA is parked in (log the state object's vtable). That directly reveals what instantiates `+0x778`. This replaces "months-long unknown" with a single targeted probe against a named object. Still a cost/priority call against [[project_direction_pivot]] (FLE career primary). --- ## DYNAMIC PROBE RESULT — hypothesis overturned: connect flow is never triggered (2026-07-02, run 3) Built a probe (openfut-hook `probe` feature) with entry detours on the game-side connect functions + a guarded sampler that reads the session-context chain directly. Ran it live at "connecting to EA Servers". **Deploy note:** FIFA is launched via **FLE**, which injects `openfut_hook.dll` from the FLE dir — NOT the game-dir `version.dll`; earlier redeploys to version.dll were never loaded (see [[reference_hook_deploy_path]]). **Results (decisive):** - Sampler: `X=[0x14acd02c0]` (OriginSDK singleton), `M=[X+0x360]`, `ctx=[M+0x778]` → **`ctx = 0xba8315a0`, non-null and stable** the entire time. - `nucleusConnectREST` enter=**0**, `nucleusConnectTrusted` enter=**0**, `connectState.tick` (+0x507d660) enter=**0**. - Bridge LSX log: FIFA does its full bootstrap (GetConfig, GetProfile, GetGameInfo, GetSetting, GetInternetConnectedState, IsProgressiveInstallationAvailable, SetPresence, SetDownloaderUtilization) then **goes silent** — only receiving our 2 s event pushes. Over the whole session it **never** sends QueryEntitlements, RequestLicense, or GetAuthCode. **Conclusion — the dig-2 static hypothesis was WRONG.** The Nucleus session context `[M+0x778]` is NOT null; it exists. The connect functions would pass their null-check — but they are **never called**. The blocker is not a missing object; it is that FIFA's EASFC/Nucleus **online-connect flow is never triggered at all**. FIFA completes LSX bootstrap, parks at "connecting", and never advances to the connect state (whose objects are created at init — loop @ +0x4f47920 building the array [ctrl+0x780], count [ctrl+0x298] — but never driven). **This is exactly why we probe:** the static call-graph looked like a null-check gate; the live client showed the gate is upstream — a *trigger*, not a precondition. Event-push (OnlineStatus/ Login) parses fine and even sets FIFA's stored online bool (via the no-op listener's map store), but does not kick the online controller into starting the connect. **Next question: what transition starts the EASFC connect** (idle connect-state → nucleusConnectREST). Candidates to probe next: the connect-state's earlier vtable steps (+0xa8/+0xb0 = +0x507cd60/+0x507cf90) to see if the state is entered-but-stalls vs never-entered, and the online-controller update that would advance it. Still a cost/priority call vs [[project_direction_pivot]]. --- ## RUN 4 — the EASFC connect subsystem is created-but-dormant (2026-07-02) Probed the connect-state lifecycle to settle entered-but-stalled vs never-entered. | probe | enter | reading | |---|---|---| | `connectState.ctor` (+0x5078d20) | **4** | connect states ARE created (4 slots) at boot init | | `ctrl.GetConnState` (+0x4f46570) | **0** | nothing ever reads/iterates the connect-state array | | `connState.m_a8/m_b0/tick_b8/m_c0` | **0** | no vtable method ever invoked on any connect state | | `nucleusConnectREST` (+0x2861910) | 0 | never reached | | `OnlineStatus.deser` (+0x278a4d0) | 33 | control: our pushed events keep arriving/parsing | | sampler `ctx[M+0x778]` | non-null, stable | control: session context exists throughout | **Definitive picture.** The EASFC/Nucleus online-connect subsystem is **constructed at boot (as part of OriginSDK/`X` init — chain: connect-state ctor ← array-init +0x4f47920 ← ctor +0x4f47b70 ← … ← OriginSDK ctor +0x279d5c0) and then left completely dormant**: its connect-state array is built and never touched again — no iteration, no vtable call, no connect attempt. So the block is not inside the connect flow at all; **nothing ever activates the subsystem.** FIFA parks at "connecting to EA Servers" in the earlier EbisuSDK-login layer and never hands off to EASFC. Our OnlineStatus/Login events parse and set FIFA's stored online bool, but the game-side listener is a no-op (see run 2), so no active handler reacts to "online" by starting EASFC. **What this means strategically.** Every object we can see (session context, connect states) exists; the missing piece is the runtime *activation* of the online subsystem — the game's top-level "go online" transition, which anadius (offline DRM) never drives and which our LSX bridge does not reach. Pinning the exact dormancy guard (the EASFC controller's per-frame Update early-returns before reading the array) is the identified next lever, BUT: forcing it only moves FIFA to the *next* wall — dialing the Blaze redirector (gosredirector) and speaking Fire2 — which is the full multi-month M2→M6 Blaze-emulation frontier. This run empirically confirms we are standing exactly at the M2 boundary the status review priced, with the pre-Blaze infrastructure proven present. Decision point vs [[project_direction_pivot]] (FLE career = stated primary plan). --- ## MAPPING SYNTHESIS — the online-activation gate, and a tooling limit (2026-07-02) Goal after run 4: map the exact trigger that would wake the dormant EASFC subsystem. Result: the gate is characterized precisely at the subsystem level, but the *single* activation predicate could not be isolated with byte-level tooling — because the online subsystem is pervasive, not gated by one flag. That pervasiveness is itself the finding. **The complete gate map (proven, top to bottom):** 1. LSX bootstrap — DONE. FIFA reaches "connecting to EA Servers" cleanly; bridge answers GetConfig/GetProfile/GetGameInfo/GetSetting/GetInternetConnectedState/etc. 2. EbisuSDK online report — DONE at the SDK level. Our OnlineStatus/Login event pushes parse (deserializers fire) and set FIFA's stored online bool (HandleMessage map store). BUT the game-side listener is a no-op (dispatch → +0x2751060 = `ret 0`): no active handler reacts. 3. EASFC/Nucleus online-connect subsystem — **DORMANT.** Its connect-state array is built at boot (ctor fires 4x) as part of OriginSDK init, then never touched (GetConnState 0x, no vtable step fires). Session context [M+0x778] exists. nucleusConnectREST/GetAuthCode never fire because nothing iterates/activates the subsystem. 4. GetAuthCode → Nucleus token → Blaze redirector → Fire2 — NEVER REACHED (the M2→M6 frontier). **The missing link = runtime "go online" activation** between (2) and (3): the thing that, on "online", makes FIFA's online controller start iterating/driving the connect states. anadius (offline DRM) never drives it; our LSX bridge reaches the SDK layer (2) but not the game's top-level online-mode activation (3). Attempts to isolate a single activation flag via static byte-scan failed: GetConnState has 61 callers, the connect-state getter family and the online globals resolve to hundreds of sites — the subsystem is woven throughout, so there is likely no one boolean to flip. This is consistent with FIFA being a full online client whose online mode is an entire subsystem activation, not a switch. **Tooling note for whoever continues:** byte-level objdump + offset scans were enough to CHARACTERIZE the gate (this whole document) but are the wrong tool to find the activation predicate inside a 500 MB pervasive subsystem. The right next tool is a decompiler (Ghidra/IDA) to recover FIFA's online-controller Update and its guard as structured code, or continued dynamic probing (the openfut-hook `probe` framework — 8 entry detours + a VirtualQuery-guarded memory sampler — is reusable) to watch the controller Update's guard value live. Either way this is confirmed to be the multi-month M2 frontier; pre-Blaze infrastructure is proven present, and FIFA sits one activation away from the first Blaze dial. Weigh against [[project_direction_pivot]].