# FIFA 17 OriginSDK — what makes the game consider the user LOGGED IN (LSX events)

Clean-room RE. Sources: `/mnt/games/FIFA 17/FIFA17.exe` (fully symboled; `.text` is
runtime-decrypted, so all code analysis was done on live `/proc/PID/mem` dumps taken from the
running game — `live_code.bin` @0x140001000, `live_high.bin` @0x144ed3000, both in this
scratchpad), plus our own LSX captures. No leaked material used.

Helper tools written for this pass (scratchpad): `dumplive.py`, `disasm_helper.py`, `fndis.py`
(disassemble + auto-annotate rip-relative string refs), `fnstart.py`, `callers.py`, `xref2.py`,
`bulkxref.py`, `symmap.py` (turns the binary's own `Class::Method` log strings into a symbol map).

---

## VERDICT

**The prime hypothesis is CORRECT. The OriginSDK has a distinct "user is logged in" state that is
delivered ONLY as a server-PUSHED LSX `<Login>` event. Our request-only responder never sends it,
so FIFA's Origin manager has had `m_isLoggedIn = false` for the entire session — independently of
`GetInternetConnectedState connected="1"`.**

Add this **pushed** message to `lsx_responder.py` (encrypted like any other post-handshake
message: `hex(AES128-ECB(session_key, pkcs7(xml)))` + `\0`):

```xml
<LSX><Event sender="LOGIN_EVENT"><Login IsLoggedIn="true"/></Event></LSX>
```

`sender="LOGIN_EVENT"` is **mandatory and exact** (proof below). `IsLoggedIn` is parsed as a
bool by literal comparison against `"false"` — anything that is not the exact string `false`
means true, so `"true"` or `"1"` both work; `"false"` / missing attribute means logged out.

Strongly recommended companion (FIFA also subscribes to this one, and it is likewise
push-only — never requestable):

```xml
<LSX><Event sender="ONLINE_STATUS_EVENT"><OnlineStatusEvent isOnline="true"/></Event></LSX>
```

Optional, keeps the profile consistent:

```xml
<LSX><Event sender="PROFILE_EVENT"><ProfileEvent Changed="0" UserId="33068179"/></Event></LSX>
```

**When to send:** any time after the client's first post-handshake request (the event handler
array is built inside `Origin::OriginSDK::Initialize`, which completes before the client issues
`GetConfig`). Practical recipe: push `<Login>` right after we answer the first `GetProfile`
(id=3), and push it again after answering `GetInternetConnectedState` (id=16/18) and after
`GetGameInfo UPTODATE`. Re-sending is harmless — the handler is idempotent (it just sets a
flag and broadcasts).

---

## Evidence chain

### 1. The SDK's LSX event surface (28 push-only messages)

`Origin::OriginSDK::RegisterEventCallback` (`0x14710e710`) indexes a fixed array of
pre-constructed handlers at `sdk + 0x278 + 8*eventEnum` (29 slots — `lea ebp,[rdx+0x1d]` in the
unregister-all path).

Every handler's matcher has the identical shape (e.g. `Origin::EventHandler<lsx::LoginT,unsigned
int>::HandleMessage` @`0x14710c7d0`, matcher @`0x147102800`):

```
root element must be "LSX"
enter element   <Event>                      (name comes from handler, always "Event")
optional attr    id="..."                    (HasAttribute check — may be omitted)
REQUIRED attr    sender="..."                (must be PRESENT and strcmp-equal to handler->sender)
enter element   <Login> / <OnlineStatusEvent> / ...   (hardcoded per handler)
then parse that element's attributes
```

So the wire form for every event is:

```xml
<LSX><Event sender="<SERVICE>"><ElementName attr="..."/></Event></LSX>
```

Full set of inner element names the SDK listens for (extracted from the 28 matchers in
`0x147101000..0x14710a000`):

```
AchievementSets            BlockListUpdated         BroadcastEvent        ChatMessageEvent
ChunkStatus                CoreContentUpdated       CurrentUserPresenceEvent
GameMessageEvent           GetPresenceResponse      GroupEnterEvent       GroupEvent
GroupInviteEvent           GroupLeaveEvent          IGOEvent              IGOUnavailable
Login                      MinimizeRequest          MultiplayerInvite     MultiplayerInvitePending
OnlineStatusEvent          PresenceVisibilityEvent  ProfileEvent          PurchaseEvent
QueryEntitlementsResponse  QueryFriendsResponse     RestoreRequest        UserInvitedEvent
VoipStatusEvent
```

(plus the separate `LSXEvent<lsx::ChallengeT>` = the `<Challenge>` handshake event we already
send.)

### 2. `sender` value = the SDK's service-name enum. Login ⇒ `LOGIN_EVENT`

`0x14710df7f` is the SDK's "create all event handlers" routine. For each handler it does:

```
edx = <service enum>;  rax = GetServiceName(sdk, edx)     ; 0x1470e4870 -> [sdk+0x3b0] + 0x20*edx
call <MakeHandlerN>(sdk, rax /*sender string*/, r8 = sdk+0x278 /*array*/)
```

`GetServiceName`'s runtime table is initialised from the static string-pointer table at
**`0x144341420`**:

```
0=SDK 1=PROFILE 2=PRESENCE 3=FRIENDS 4=COMMERCE 5=RECENTPLAYER 6=IGO 7=MISC 8=LOGIN
9=UTILITY 10=XMPP 11=CHAT 12=IGO_EVENT 13=EALS_EVENTS 14=LOGIN_EVENT 15=INVITE_EVENT
16=PROFILE_EVENT 17=PRESENCE_EVENT 18=FRIENDS_EVENT 19=COMMERCE_EVENT 20=CHAT_EVENT
21=DOWNLOAD_EVENT 22=PERMISSION 23=RESOURCES 24=BLOCKED_USERS 25=BLOCKED_USER_EVENT
26=GET_USERID 27=ONLINE_STATUS_EVENT 28=ACHIEVEMENT 29=ACHIEVEMENT_EVENT 30=BROADCAST_EVENT
31=PROGRESSIVE_INSTALLATION 32=PROGRESSIVE_INSTALLATION_EVENT 33=CONTENT
```

The handler's `sender` string lands at `handler+0x48`, which is `this+0x10` for the
`IEventHandler` sub-object (secondary vtable at object+0x38) — exactly what
`HandleMessage` reads (`mov rbx,[rcx+0x10]`) and strcmp's the `sender` attribute against.

Resolved handler table (service enum → array slot → OriginEventT enum → element):

| OriginEventT | array slot | service enum | `sender` string | element |
|---|---|---|---|---|
| 0 | +0x00 | 0x0c | `IGO_EVENT` | `IGOEvent` |
| 1 | +0x08 | 0x0f | `INVITE_EVENT` | `MultiplayerInvite` |
| **2** | **+0x10** | **0x0e** | **`LOGIN_EVENT`** | **`Login`** |
| 3 | +0x18 | 0x10 | `PROFILE_EVENT` | `ProfileEvent` |
| 4 | +0x20 | 0x11 | `PRESENCE_EVENT` | `GetPresenceResponse` |
| 5 | +0x28 | 0x12 | `FRIENDS_EVENT` | `QueryFriendsResponse` |
| 6 | +0x30 | 0x13 | `COMMERCE_EVENT` | `PurchaseEvent` |
| 7 | +0x38 | 0x15 | `DOWNLOAD_EVENT` | `CoreContentUpdated` |
| 8 | +0x40 | 0x19 | `BLOCKED_USER_EVENT` | `BlockListUpdated` |
| **9** | **+0x48** | **0x1b** | **`ONLINE_STATUS_EVENT`** | **`OnlineStatusEvent`** |
| 10 | +0x50 | 0x1d | `ACHIEVEMENT_EVENT` | `AchievementSets` |
| 11 | +0x58 | 0x0f | `INVITE_EVENT` | `MultiplayerInvitePending` |
| 12 | +0x60 | 0x14 | `CHAT_EVENT` | `ChatMessageEvent` |
| 13 | +0x68 | 0x11 | `PRESENCE_EVENT` | `CurrentUserPresenceEvent` |
| 14 | +0x70 | 0x1e | `BROADCAST_EVENT` | `BroadcastEvent` |
| 15 | +0x78 | 0x11 | `PRESENCE_EVENT` | `PresenceVisibilityEvent` |
| 16 | +0x80 | 0x13 | `COMMERCE_EVENT` | `QueryEntitlementsResponse` |
| 17 | +0x88 | 0x20 | `PROGRESSIVE_INSTALLATION_EVENT` | `ChunkStatus` |
| 18 | +0x90 | 0x0c | `IGO_EVENT` | `IGOUnavailable` |

(13 independent service-enum/element pairings all agree with the table — the mapping is not a guess.)

### 3. `<Login>` carries exactly one attribute: `IsLoggedIn`, parsed as a bool

Deserializer `0x147138640` (reached via `0x14712fd80` from the `Login` matcher) reads exactly one
attribute name, `"IsLoggedIn"` (string @`0x14394e0f0`), then converts with `0x14713ffa0`:

```
0x14713ffc8:  lea rdx, "false"
0x14713ffd2:  call strcmp
0x14713ffda:  setne al          ; value != "false"  =>  true
0x14713ffdd:  mov [rdi], al     ; stored as a byte
```

Same helper is used for `<OnlineStatusEvent isOnline="...">` (`0x147139e6e`) and
`<PresenceVisibilityEvent Visible="...">`. `<ProfileEvent>` carries `Changed` + `UserId`.

### 4. FIFA subscribes to the Login event, and it is the ONLY event that mutates
###    FIFA's Origin login state

FIFA registers 9 Origin event callbacks in a loop at `0x146f33e90`+ (call site `0x146f34068` →
`OriginRegisterEventCallback` @`0x1470db310`), from a constant array:

```
xmm0 @0x143565970 = {0,1,2,3}
xmm1 @0x143902060 = {4,5,6,7}
plus                 9
=> enums {0,1,2,3,4,5,6,7,9}   (all with the same callback 0x146f20c20)
```

Cross-referenced against the table above, FIFA subscribes to exactly:
IGOEvent, MultiplayerInvite, **Login**, ProfileEvent, GetPresenceResponse, QueryFriendsResponse,
PurchaseEvent, CoreContentUpdated, **OnlineStatusEvent**.

`0x146f20c20` forwards to the FIFA Origin-manager dispatcher `0x146f1e060`
(manager singleton via `0x146f28790`, global `[0x1448acf50]`). Its `case 2` is the only case that
writes state:

```
0x146f1e099:  cmp edx, 2                  ; eventEnum == 2 (Login)
0x146f1e09e:  cmp DWORD PTR [r9], 1       ; converted IsLoggedIn == 1 ?
0x146f1e0a2:  lea rbx,[rcx+0x80]          ; listener list for event 2
0x146f1e0ab:  mov BYTE  PTR [rcx+0x13], 1 ; <== OriginMgr.m_isLoggedIn = TRUE
0x146f1e0af:  mov DWORD PTR [rcx+0x14], 0 ; <== clear login error/reason
   (else)
0x146f1e0b8:  mov BYTE  PTR [rcx+0x13], 0 ; m_isLoggedIn = FALSE
0x146f1e116:  broadcast to listener list
```

All other cases (0,1,3,4,5,6,7,9) only broadcast to their listener list.
`OriginMgr+0x13` is written from exactly two places in the whole image: this Login-event case,
and `0x146f20c80` (the async login/`CheckOnline` completion callback, which also clears +0x18).
**There is no requestable LSX verb that sets it** — it is unreachable without a pushed `<Login>`.

### 5. Why this matters: GetAuthCode is enqueue-driven and has never been enqueued

* FIFA-level auth-code issuer: `FifaOnline::FirstPartyAuthTokenRetriever::DoTick` @`0x146f199b9`
  → `OriginRequestAuthCodeSync` @`0x1470db3c0` → `Origin::OriginSDK::RequestAuthCodeSync`
  @`0x1470e67f0` → LSX `<GetAuthCode>`.
* `DoTick` walks a **2-slot request array** at `retriever+0x8` and does nothing when both slots
  are null (`0x146f199e0: mov rsi,[rbx]; test rsi,rsi; je <exit>`).
* Enqueue path: `RequestAuthCode` @`0x146f5b8ab`, reached via thunk `0x146f57bf0`
  (`mgr = *[0x1448a3b20]` gated by byte `[0x1448a3ac3]`; retriever = `mgr + 0x4e98`).
* **Live read of the stuck game (PID 3362053) confirmed:**
  `[0x1448a3ac3] = 1`, `mgr = 0x43dc3e70`, retriever = `0x43dc8d08`, **both slots = 0x0** —
  no auth-code request has ever been created. This is why `/tmp/openfut_authcode.txt` stays
  empty and `GetAuthCode` never appears in the LSX log. The SDK itself has no login gate inside
  `RequestAuthCodeSync`; the gate is entirely FIFA-side, upstream of the enqueue.

### 6. Corroborating: `LoginStatePCLogin` has an explicit "not logged in to Origin" abort

FIFA's Blaze login state machine (`LoginStateMachineImpl`, `LoginStateBase` subclasses; per-state
`GetName` thunks in `0x14719b360..0x14719b4a7`; states: ShowMaintenance, Isp, LoadIspAccountInfo,
Connect, LoadConfig, Logout, VersionCheck, Login, **PCLogin**, VerifyAccount, UpgradeAccount,
LoginComplete, Unsuspend, CheckUser, RecheckUser, WebOffer).

`LoginStatePCLogin` vtable @`0x14395c188`; its driver is `0x1471b58e0` (a 0x19-case sub-state
machine on `this+0x260`). Its very first sub-state does:

```
0x1471b59a5:  rcx = *[0x144b86bf0]; call [vt+0x60]      ; get the Origin/Ebisu session object
0x1471b59b5:  test rax,rax
0x1471b59b8:  je   0x1471b5b42                          ; NULL -> failure branch
...
0x1471b5b64:  lea rax, "TXT_NOT_LOGIN_TO_EBISU"         ; loc key stored at state+0x80
0x1471b5b72:  mov DWORD PTR [r14+0x260], 1              ; -> error sub-state
```

("Ebisu" is EA's internal codename for Origin; the sibling key
`TXT_ORIGIN_GAME_VERSION_OUT_OF_DATE` is the "title version outdated" gate we already beat via
`GetGameInfo UPTODATE`. Both live at `0x1439633e8` / `0x14395bfc0`-ish in the same loc-key block.)

Also proven live: FIFA's separate "Origin is online" byte `[0x1448a3ac0]` was **1** during the
stuck session, so the *internet/online* gate (fed by `GetInternetConnectedState`, callback
`0x146f1e6ae`, broadcasts `FE::FIFA::OriginOnlineEvent`) is already satisfied. **Online ≠ logged
in.** They are two different flags with two different feeds; we only ever fed the first one.

---

## Wire recipe for `lsx_responder.py`

Same framing as the existing `<Challenge>` event we already push successfully
(NUL-terminated; after the handshake everything is `hex(AES128-ECB(session_key, pkcs7(xml)))`):

```python
def push_event(conn, key, xml):
    conn.sendall(lsx_encrypt(xml, key))

LOGIN_EVENT  = '<LSX><Event sender="LOGIN_EVENT"><Login IsLoggedIn="true"/></Event></LSX>'
ONLINE_EVENT = '<LSX><Event sender="ONLINE_STATUS_EVENT"><OnlineStatusEvent isOnline="true"/></Event></LSX>'
PROFILE_EVT  = '<LSX><Event sender="PROFILE_EVENT"><ProfileEvent Changed="0" UserId="33068179"/></Event></LSX>'
```

Trigger points (after sending our normal `<Response>`):
1. after answering the first `GetProfile`  → push `LOGIN_EVENT`, then `ONLINE_EVENT`
2. after answering `GetInternetConnectedState` → push `LOGIN_EVENT` again
3. after answering `GetGameInfo UPTODATE` → push `LOGIN_EVENT` + `ONLINE_EVENT` again

Notes / gotchas:
* `sender` must be **exactly** `LOGIN_EVENT` / `ONLINE_STATUS_EVENT`. A wrong or missing `sender`
  makes `HandleMessage` return false and the message is silently dropped (no error, no crash) —
  which is exactly the failure mode to watch for.
* An `id` attribute on `<Event>` is optional.
* Sending an event before `OriginSDK::Initialize` has built the handler array is a silent no-op,
  so never send before the client's first request.
* If the game still doesn't call `GetAuthCode` after this, the next thing to instrument is
  `OriginMgr+0x13` (byte) and `OriginMgr+0x14` (dword) via `/proc/PID/mem`
  (`OriginMgr = *[0x1448acf50]`): +0x13 flipping 0→1 proves the event landed and moves the
  investigation downstream to `LoginStatePCLogin` / the Blaze `Authentication::logout` loop.

## Open / not proven

* I could not statically locate the consumer that *reads* `OriginMgr+0x13` (no matching
  `[reg+0x13]` byte-read in the FIFA online region) — it is presumably an inlined accessor or a
  reaction to the event-2 broadcast. So "flag false ⇒ GetAuthCode never enqueued" is a strong
  inference from (a) the flag being login-specific, (b) `LoginStatePCLogin`'s
  `TXT_NOT_LOGIN_TO_EBISU` abort and (c) the live-confirmed empty auth-code slots — but the exact
  read site is unconfirmed. Instrumenting +0x13 live is the cheap way to close this.
* The one identified event-2 listener list subscriber is the EAStore/DLC subsystem
  (registrar `0x14735077f`, listener `0x147350e30`), not the login flow — consistent with the
  login gate reading the flag rather than listening.
* The Blaze side still ends in `Authentication::logout (1/0x46)` → disconnect →3s ping-reconnect
  loop (`/tmp/blaze_responder.log`). If the Login event does not change that, the second
  candidate is the FIFA `LoginStateMachine` transition out of `LoginStateLogout`, which is a
  separate (Blaze-side) investigation.
