# CONNECT_PLAN — Origin-SDK connect + Blaze-login trigger (clean-room synthesis)

Synthesis of four independent reverses (Origin connect writer, connect-wait guard, "service 0x1e8"
dispatcher, Blaze LoginManager), plus my own live re-verification against **pid 13643** (`FIFA17.exe`,
still up at time of writing). Every VA/byte below is from our decrypted dumps, `/proc/13643/mem`,
`captures/lsx/full_origin_init_session.log`, `/tmp/lsx.log` or `/tmp/blaze_responder.log`.

---

## 0. HEADLINE — BRIEF3's two premises were both wrong, and the real cause is one line of our responder

| BRIEF3 said | Truth (live-verified) |
|---|---|
| `0x1470dbfa0` = service/interface lookup; "service `0x1e8` returns NULL → not connected" | `0x1470dbfa0` is the **Origin SDK allocator thunk** (`jmp [0x144b7c778]`, live target `0x1470d9380` = aligned-alloc). `0x1e8` is an **allocation size** (488 = `sizeof(LSXRequest<GetAuthCodeT,…>)`), and the neighbouring `0x1e6/0x1e8/0x1ea` are `__LINE__` args to the logger. **There is no service table and nothing to register.** |
| `0xa2080000` comes from the auth path failing a service lookup | `0xa2080000` is the LSXRequest/EventHandler **"response failed to validate"** default, written at `0x1471192dd`→`[req+0x1e0]`. It means *we replied and the client threw our reply away.* |
| Empty Blaze `fetchClientConfig` leaves `[cfg+0x750]` NULL and blocks login | `[cfg+0x750]` is live **non-NULL** = `0x43c47ff0` (verified: `*[0x144b86bf8]=0x43c46c70`, `+0x360=0x43c47330`, `+0x750=0x43c47ff0`). It is fed by **`Util::preAuth`'s CONF map**, not by `fetchClientConfig`. No `fetchClientConfig` payload can change it. |

**The actual blocker is a one-attribute protocol mismatch in `lsx_responder_v2.py`.**
Every LSX `<Response>` is accepted only if its `sender` attribute **byte-equals the `recipient` the
client put on the matching `<Request>`**. FIFA sends `recipient=""` for everything after `GetConfig`
(because our `GetConfigResponse` is empty, so its 34-entry service-name table is all empty strings),
but we answer `GetProfile`, `GetAuthCode` and `QueryEntitlements` with `sender="EbisuSDK"`.
Those three replies — and only those three — are discarded. `GetProfile` is the *sole* writer of
`OriginSDK+0x3a0/+0x3a8`, so the whole chain (default user → auth code → OSDK login state → Blaze
`Authentication::login`) never starts.

---

## 1. THE CONNECT MECHANISM — end to end

### 1.1 The writer is a synchronous statement, not an event

`0x1470e5ad5` (the `+0x3a0` write) lives inside **`Origin::OriginSDK::Initialize`**, function start
`0x1470e5770` (prologue `push rbp/rsi/rdi/r12/r13/r14/r15; sub rsp,0xf0`, epilogue `0x1470e5caf`).
Its logger passes `# 0x143937c78` = `"Origin::OriginSDK::Initialize"` and
`# 0x143937b70` = `"E:\p4\fifafb\rl\empatch\TnT\Code\External\EA\OriginSDK\src\impl\OriginSDKimpl.cpp"`
(both read live). Sequence inside `Initialize`:

```
0x1470e5959..96   connect loop: call 0x14712ca40(conn=sdk+0x168, …, port)
                  on false -> Sleep(1000); inc ebx; cmp ebx,0x1e; jl   => 30 x 1s   (NOT 15 s)
0x1470e59fb       version gate: cmp eax,0x9b00000; jb -> 0xa0020007    (passes today)
0x1470e5a2f       ecx=0x1a8; call 0x1470dbfa0      <-- ALLOC 424 bytes for the GetConfig request
0x1470e5a46       lea rdx,[rip..] # 0x143937d58 = "EbisuSDK"   <-- HARD-CODED recipient/sender
                  ... SendSync(GetConfig) ...
0x1470e5a96       lea rdx,[rbx+0x80]; call 0x1470e0520          <-- consume GetConfigResponse
                                                                    -> fills serviceNames[0..33]
0x1470e5aa8       call 0x14710df80(sdk)                          <-- register ~19 event handlers,
                                                                    each keyed on serviceNames[i]
0x1470e5ab7..c7   lea r9,[rsp+0x70]; xor edx,edx; mov r8d,0x3a98(=15000ms); mov rcx,rdi
                  call 0x147118d80                               <-- GetProfileSync(idx=0)
0x1470e5ad5       mov rax,[rdi+0x80]; mov [rbp+0x3a0],rax        <-- UserId
0x1470e5ae1       mov rax,[rdi+0x88]; mov [rbp+0x3a8],rax        <-- PersonaId
```

So: **the only stimulus for the `+0x3a0` write is a well-formed `<GetProfileResponse>` to the
`<GetProfile index="0">` FIFA already sends at boot (log id=3).** There is nothing else to serve, no
event to push, no "connect" verb we are missing. `+0x3a0`/`+0x3a8` are **u64 scalars** (UserId /
PersonaId), not object pointers — the deserializer `0x147136140` maps `UserId→resp+0x00`,
`PersonaId→+0x08`, `Persona→+0x10`, `AvatarId→+0x30`, `Country→+0x50`, `IsUnderAge→+0x70`,
`IsSubscriber→+0x71`, `GeoCountry→+0x78`, `CommerceCountry→+0x98`, `CommerceCurrency→+0xb8`, and the
response sub-object sits at `request+0x80`.

`0x147118d80` = `Origin::OriginSDK::GetProfileSync` (both exits converge on
`lea rax,[rip..] # 0x143945d00` = `"Origin::OriginSDK::GetProfileSync"`). It is **not** a poll loop:
it builds the request, calls the send-and-wait template `0x1471186f0`, which serializes
`<LSX><Request recipient="X" id="N"><GetProfile index="0" version="3"/></Request></LSX>`, then blocks
on a timed condvar (`0x145e318f0(&req+0x1e8,&req+0x230)`) until the reader thread sets `[req+0x228]=1`.
Success is `[req+0x1e0]==0` (`0x147118851: cmp DWORD PTR [rbx+0x1e0],0x0; sete al`).

### 1.2 The rejection — verified byte-for-byte, live

Response matcher `0x1471189b0` (disassembled from `/proc/13643/mem` this session):

```
1471189c6  call [rax+0x10]                       ; reader ok?           -> je FAIL
1471189d7  call [rax+0x58]                       ; root element name
1471189dd  lea rdx,[rip..] # 0x143938024         ; "LSX"  (4-byte compare loop) -> jne FAIL
147118a14  call [rax+0x18]  rdx=[this+0xd8]      ; child "Response"     -> je FAIL
147118a2a  lea rdx,[rip..] # 0x14355de00 = "id"  ; optional -> atoi -> [this+0x138]
147118a5d  lea rdx,[rip..] # 0x143938028 = "sender"
147118a7d  call [rax+0x70]                       ; GetAttributeValue("sender")
147118a80  test rax,rax ; je 0x147118ad9         ; ATTRIBUTE MUST BE PRESENT
147118a85  lea r8,[rdi+0xf8]                     ; expected sender (std::string, SSO at +0x18)
147118a99..aae  inline byte-wise strcmp          ; MISMATCH -> jne FAIL (0x147118ad9)
147118ab3  lea rdx,[rip..] # 0x143945ce8 = "GetProfileResponse" -> je FAIL
147118acf  tail-jump to deserializer
```

On FAIL, `HandleMessage 0x147119210` does `call 0x1470e2e30` (look for an `<Error>` node), and when
there is none:

```
1471192dd  mov edx,0xc347a20f
1471192e4  lea edx,[rdx-0x213fa20f]     ; = 0xA2080000
1471192ef  mov DWORD PTR [rbx+0x1e0],edx
147119311  call 0x145e30d20             ; signal condvar (so it fails FAST, never times out)
147119316  mov BYTE PTR [rbx+0x228],1
```

The expected string `[req+0xf8]` and the emitted `recipient` are **the same std::string**, taken from
`sdk->serviceNames[facility]` via accessor `0x1470e4870`
(`cmp edx,0x21; movsxd rax,edx; shl rax,5; add rax,[rcx+0x3b0]`), and used symmetrically by the request
ctor `0x147117fe0` (`"Request"@0x1435a43b8` and `"Response"@0x143938198` both get arg2).

Facilities per verb (from the `add rdx,0xNN` after `mov rdx,[sdk+0x3b0]`, `NN/0x20 = idx`):

| Verb | site | facility |
|---|---|---|
| `GetConfig` | `0x1470e5a46` | **literal `"EbisuSDK"`** (not table-driven) |
| `GetProfile` | `0x147118df2` (`lea edx,[r12+1]`) | 1 PROFILE |
| `GetAuthCode` | `0x1470e6882` (`add rdx,0x120`) | 9 UTILITY |
| `GetSetting` / `GetGameInfo` | `0x1470e4903` / `0x1470e3683` (no add) | 0 SDK |
| `QueryUserId` | `0x1470e62b4` | 26 GET_USERID |
| `IsProgressiveInstallationAvailable` / `SetDownloaderUtilization` | `0x147129ddb` / `0x14712b821` | 31 PROGRESSIVE_INSTALLATION *(inferred)* |

### 1.3 Live state of the table (re-verified myself, pid 13643)

```
OriginSDK = *[0x144b7c7a0] = 0x25c98c50
  +0x1b8 = 0x1fc                 (conn+0x50 socket, ALIVE -> transport healthy)
  +0x1e8 = 0xffffffffffffffff    (conn+0x80, a SECOND handle, still unset - see §5)
  +0x218 = 0x0                   (pending-request map size)
  +0x270 = 0x1c                  (28 requests issued == our lsx.log last id 27 + 1)
  +0x3a0 = 0x0                   <-- default user STILL NULL
  +0x3a8 = 0x0
  +0x3b0 = 0x25cb93e0 .. +0x3b8 = 0x25cb9820   -> (0x440)/0x20 = 34 entries
  all 34 std::strings: size == 0  ("all-empty")
```

34 entries allocated proves `Initialize` **did** reach and accept our `GetConfigResponse`
(`0x1470e0520`: `lea rbp,[rcx+0x3b0]; lea edx,[rbx+0x22]; call 0x1470eadd0` resizes to 34) — and all
empty proves it contained **zero `<Service>` children**, because we answer
`<GetConfigResponse Config="false"/>`.

### 1.4 On the wire — the smoking gun

`captures/lsx/full_origin_init_session.log`:

```
id=1  <Request recipient="EALS"     …><ChallengeResponse …>      (plaintext handshake)
id=2  <Request recipient="EbisuSDK" …><GetConfig version="3"/>   -> we reply sender="EbisuSDK"  MATCH  ✔
id=3  <Request recipient=""         …><GetProfile index="0"/>    -> we reply sender="EbisuSDK"  MISMATCH ✘
id=4  <Request recipient=""         …><GetSetting …/>            -> we reply sender=""          MATCH  ✔
id=5  <Request recipient=""         …><GetGameInfo …/>           -> we reply sender=""          MATCH  ✔
… ids 6..14 all recipient=""
```

Independent live proof that an **empty** `sender` attribute really does validate (i.e. `vt[0x70]`
returns a pointer to `""`, not NULL): FIFA's OSDK failure classifier `0x14717d5d0` only reaches its
`cmp eax,0xa2000003` branch **after** `OriginGetGameInfoSync(id=0)` returns rc==0 **and** the buffer
strncmp-equals `"true"@0x14354be6c`; otherwise it would set `TXT_ORIGIN_GAME_VERSION_OUT_OF_DATE`.
Live `[0x43d189d8+0x80] = 0x14395ca10 = "OSDK_INVALID_USER"` and `[+0x260] = 16` — so our
`GetGameInfo(UPTODATE) → GameInfo="true"` reply, sent with `sender=""`, **was accepted**. (It also
pins `GameInfoId` enum 0 == `UPTODATE`.)

### 1.5 Events are broken by the same mechanism

Event validator `0x1471015e0` (per report 3) requires root `"LSX"@0x143938024`, child literal
`"Event"@0x14365d8fc` (len 5), optional `id`, then `call [rax+0x70]` for `"sender"@0x143938028` →
`test rax,rax; je FAIL` → inline strcmp against the per-handler string `[handler+0x10]`.
I disassembled the registrar `0x14710df80` live and confirmed **that string is
`serviceNames[facility]`** — every registration is `mov edx,<idx>; call 0x1470e4870; mov rdx,rax;
call <per-event registrar>`:

```
0x14710df8a  edx=0x0c IGO_EVENT            -> 0x1470eb6a0
0x14710dfac  edx=0x0f INVITE_EVENT         -> 0x1470eb060      (0xe7ffa20f+0x18005e00 = 0x0f)
0x14710dfcd  edx=0x0e LOGIN_EVENT          -> 0x1470eb100      (0xc390050f+0x3c6ffaff = 0x0e)  << ours
0x14710dfee  edx=0x10 PROFILE_EVENT        -> 0x1470eb1a0
0x14710e009  edx=0x11 PRESENCE_EVENT       -> 0x1470eb240
0x14710e024  edx=0x12 FRIENDS_EVENT        -> 0x1470eb2e0
0x14710e03f  edx=0x13 COMMERCE_EVENT       -> 0x1470eb380
0x14710e05a  edx=0x15 DOWNLOAD_EVENT       -> 0x1470eb420
0x14710e075  edx=0x19 BLOCKED_USER_EVENT   -> 0x1470eb4c0
0x14710e090  edx=0x14 CHAT_EVENT           -> 0x1470ebfc0
0x14710e0ab  edx=0x1b ONLINE_STATUS_EVENT  -> 0x1470eb560      << ours
0x14710e0c6  edx=0x1d ACHIEVEMENT_EVENT    -> 0x1470eb600
0x14710e0e1  edx=0x0f INVITE_EVENT (2nd)   -> 0x1470ebf20
… 0x1e BROADCAST_EVENT, 0x20 PROGRESSIVE_INSTALLATION_EVENT, plus repeats of 0x11/0x13
```

**This corrects report 1's open question #5: `LOGIN_EVENT` (14) and `ONLINE_STATUS_EVENT` (27)
handlers ARE registered here** — they exist and are listening. They just expect `sender == ""` right
now, while we push `sender="LOGIN_EVENT"` / `"LOGIN"` / `"ONLINE_STATUS_EVENT"`. All 3219 pushed
`<Event>` frames in `/tmp/lsx.log` have been silently dropped.

### 1.6 The connect story, one paragraph

There is no separate "connect" handshake to serve. `Initialize` connects the socket (already alive,
`[sdk+0x1b8]=0x1fc`), passes the version gate (`[sdk+0x360]="10,4,13,6637"`), does GetConfig (accepted,
table sized to 34 but left empty), registers ~19 event handlers keyed on the now-empty names, and then
calls `GetProfileSync`. **Our `GetProfileResponse` is thrown away for a `sender` mismatch**, so
`+0x3a0`/`+0x3a8` stay 0, `OriginGetDefaultUser (0x1470da6d0)` keeps returning NULL,
`OriginRequestAuthCodeSync (0x1470e67f0)` keeps failing its `test rdx,rdx` / `cmp rdx,[rcx+0x3a0]`
guards with `0xa2000003`, and FIFA's OSDK classifier turns that into `OSDK_INVALID_USER`.

---

## 2. THE BLAZE-LOGIN TRIGGER

### 2.1 `login` is compiled in and is exactly `1/0x0A` — it is simply never invoked

* Stub `0x146e15070` ends `mov r9d,0xa; movzx r8d,WORD PTR [rdi+0x10]; call 0x146df0e80` (generic
  `sendRequest`). Live component id: `WORD[0x7c17170+0x10] = 0x0001` = Authentication. **Our
  responder's `1/0x0A = login` mapping is correct.**
* Its only caller `0x146e12d83` sits in `0x146e12d20`, which appears at **vtable slot +0xd0** of all
  four LoginState vtables (`0x14389f828/…938/…a70/…b98`).
* Live: all four `LoginManagerImpl` (vptr `0x14389f5a0`) are still in **`LoginStateInit`**
  (`stateMachine+0x28 == +0x08 == stateInit`, `+0x30 stateId = 0xFFFFFFFF`). The state machine has
  never been asked to advance.
* `Authentication::logout (1/0x46)` comes from `LoginStateBase::logout` = vt+0x48 (`0x146e15eb0` →
  RPC stub `0x146e109d0`). It is a **normal step** of FIFA's OSDK sequence
  (Connect → LoadConfig → **Logout** → VersionCheck → Login) and matches our wire log exactly
  (preAuth → 6× fetchClientConfig → logout, 37 occurrences in `/tmp/blaze_responder.log`).
  **It is not an error signal and our empty REPLY to it is correct.**

### 2.2 The precondition FIFA checks — it is an Origin check, not a Blaze one

FIFA's own OSDK login state (`0x43d189d8`, vptr `0x14395c180`, Update `0x1471b58e0`, jump table
`0x141e7f55c`):

* case 0 → `mov rcx,[0x144b86bf0]; call [rax+0x60]` — live `vt[0x60] = 0x146f82070 = xor eax,eax; ret`,
  so it always falls to `0x1471b5b42`, sets `TXT_NOT_LOGIN_TO_EBISU@0x1439633e8`, `[state+0x260]=1`,
  and calls the classifier `0x14717d5d0`.
* Classifier `0x14717d5d0`: `OriginGetGameInfoSync(0)` must be rc==0 and `"true"` (it is — see §1.4),
  then reads the Ebisu/Origin manager's last error via `[rsi]->vt[0x80]()` and
  `cmp eax,0xa2000003; jne`. On match → `[state+0x80] = "OSDK_INVALID_USER"@0x14395ca10`,
  `call 0x147190d50(state, 0xe)`.
* **Live right now: `[0x43d189d8+0x260] = 16`, `[+0x80] = "OSDK_INVALID_USER"`.**

`0xa2000003` is precisely `OriginRequestAuthCodeSync`'s "NULL/invalid user" return —
`OriginSDK+0x3a0 == 0`. The dword `0xa2000003` is resident at `0x43d320f8`, adjacent to the state heap.

### 2.3 `fetchClientConfig` is a dead end for this gate — do not touch it

* `[cfg+0x750]` is **live non-NULL** (`0x43c47ff0`), holding the Blaze ConnectionManager whose string
  map at `+0x1218` (16 entries, 0x30 bytes each, begin `0x43f56450`) is **byte-for-byte our
  `Util::preAuth` CONF** — `blazeSdkClientId=FIFA17PC`, `blazeServerClientId=FIFA17PC-SERVER`,
  `blazeSdkClientSecret=openfut-secret`, `identityRedirectUri=http://127.0.0.1/login_successful.html`,
  etc. `NUCLEUS_ADDED_URL`/`NUCLEUS_CREATE_URL` (which we only send via
  `fetchClientConfig(CFID=OSDK_NUCLEUS)`) are **absent**, proving the two containers are different.
* The getter the Blaze auth fetchers use is `vt[0x48] = 0x146e1bc50`, a binary search over that same
  `[this+0x1218]` map; the keys it asks for are `"blazeServerClientId"@0x143972690`,
  `"blazeSdkClientId"@0x1439726a8`, `"blazeSdkClientSecret"@0x1439726c0`,
  `"identityRedirectUri"@0x1439726d8` — all present.
* The class that owns `blaze_authfetch1/2` (vtable base **`0x143972480`**, not `0x143972560`) has
  **zero live instances** (full-memory scan for the vptr: 0 hits, while the same scanner finds exactly
  1 hit for the config vtable `0x1438a0850`). That code never runs. Even if it did, it would bail on
  `call 0x1470da6d0` (GetDefaultUser) → `0x1470db3c0` → `0xa2000003`, storing it at `[this+0x950]`.

**Conclusion: the Blaze-login trigger is `OriginSDK+0x3a0 != 0`.** Once GetProfile validates, the
Origin default user exists, `OriginRequestAuthCodeSync` succeeds, the classifier stops writing
`OSDK_INVALID_USER`, the OSDK machine advances past sub-state 16, and LoginState vt+0xd0
(`0x146e12d20`) fires `Authentication::login (1/0x0A)` with no Blaze-side change at all.

---

## 3. CONCRETE SERVER-SIDE CHANGES (ranked, minimal)

### RANK 1 — `lsx_responder_v2.py`: echo the request's `recipient` back as the response's `sender`  ★ do this first

This is the whole fix, and it is correct for both the current empty-table state **and** any future
populated table.

**Edits (line numbers as of today's file):**

1. **L418** — widen the request regex so we capture `recipient`:
   ```python
   REQ_RE = re.compile(
       r'<Request[^>]*\brecipient="([^"]*)"[^>]*\bid="(\d+)"[^>]*>\s*<([A-Za-z]+)([^>]*)/?>')
   ```
   (`recipient` precedes `id` in every captured frame — ids 1..14 all have
   `<Request recipient="…" id="N">`. If you prefer robustness, keep the old `REQ_RE` and add a
   separate `RECIP_RE = re.compile(r'<Request[^>]*\brecipient="([^"]*)"')`, defaulting to `""`.)

2. **L475-481** — thread it through:
   ```python
   recip, mid, name, rest = mm.group(1), mm.group(2), mm.group(3), mm.group(4)
   attrs = dict(ATTR_RE.findall(rest))
   reply = build_reply(mid, name, attrs, conn, recip)
   ```

3. **L312 / L316** — make it the default sender:
   ```python
   def resp(mid, body, sender=""):        # unchanged
       return f'<LSX><Response id="{mid}" sender="{sender}"><{body}/></Response></LSX>'

   def build_reply(mid, req_name, attrs, conn, recipient=""):
       ...   # pass sender=recipient at every return site
   ```

4. **DELETE the three `sender="EbisuSDK"` overrides** — **L352** (`GetAuthCode`), **L358**
   (`QueryEntitlements`), **L371** (`GetProfile`). These are the only three rejected verbs.
   **L400** (`GetConfig`) may stay as-is or become `sender=recipient`; with echo it is identical,
   since FIFA really does send `recipient="EbisuSDK"` there (its literal is hard-coded at
   `0x1470e5a46`).

5. **Always emit the attribute, even when empty.** `0x147118a80: test rax,rax; je FAIL` rejects a
   *missing* `sender`; an empty-valued one is fine (proven in §1.4). `resp()` already does this —
   don't "optimise" it away.

6. **L231-232 / L235** — events: with the table empty, the handler strings are empty too, so push
   `sender=""`:
   ```python
   LOGIN_EVENT_SENDERS  = ("", "LOGIN_EVENT", "LOGIN")     # "" first; the others are harmless no-ops
   ONLINE_EVENT_SENDERS = ("", "ONLINE_STATUS_EVENT")
   ```
   Mismatched senders are dropped silently and cost nothing, so keeping the old candidates as a hedge
   is free. Optionally add `id="N"` to `<Event>` (read at `0x14710164c-72` → `handler+0x78`, optional).

**Observables (restart FIFA; boot `GetProfile` is id=3):**

| # | Signal | How to read |
|---|---|---|
| 1 | `Origin Error(a2080000)` disappears for GetProfile/GetAuthCode | `/tmp/lsx.log`, FIFA log |
| 2 | **`OriginSDK+0x3a0` and `+0x3a8` become `0x1f89493` (33068179)** | `SDK=*[0x144b7c7a0]`, read `+0x3a0` — this is *the* pass/fail bit |
| 3 | `[0x43d189d8+0x80]` stops being `"OSDK_INVALID_USER"`, `[+0x260]` leaves 16 | live read (state VA may move across restarts — re-find via vptr `0x14395c180`) |
| 4 | FIFA issues `<GetAuthCode ClientId="FIFA17PC">` **without** our forging anything | `/tmp/lsx.log` |
| 5 | **`Authentication::login (1/0x0A)`** appears | `/tmp/blaze_responder.log` (currently: 37× `logout`, 0× `login`) |

> Note: `[SDK+0x1e8]` (= `conn+0x80`, live `-1`) is **not** "service 0x1e8" and is *not* the success
> signal BRIEF3 asked for — there is no service to become non-null. Use signal #2 instead. Whether
> `+0x1e8` flips as a side effect is a bonus observation (see §5).

---

### RANK 2 — `lsx_responder_v2.py`: make `GetConfigResponse` actually populate the service-name table (EA-faithful; do AFTER rank 1 confirms)

Not required once rank 1 is in (echo is correct for empty names), but it restores the real protocol,
gives events non-empty senders, and de-risks any code path that dislikes empty names.

Consumer chain, all verified: matcher `0x1470e304f` checks child `"GetConfigResponse"@0x143937b30` →
deserializer `0x147135610` builds the child element name as prefix + `"Service"@0x14394dd78` (len 7;
namespaced variant `":Service"@0x14394e1a8` with ns prefix `"lsx"@0x14394def0`) and loops
`0x14713af50` per element → reads `"Name"@0x1435604fc` into `entry+0x00` and `"Facility"@0x14394e198`
via `0x1471400a0`→`0x147140010`, which **linearly strcmps the value against the 34-pointer table at
`0x144341420`** and writes the matched index to `entry+0x20` (init 0). Then `0x1470e0520` assigns
`names[entry.index] = entry.name`, skipping `index > 0x21` and empty names.
Records are 40 bytes (`{std::string name; int32 index;}`), which is why `0x1470e0520` divides by 0x28.

**Facility enum strings — read live from `0x144341420` this session (case-exact, required):**

```
 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
```

Reply (keep the envelope `sender` = echo of `recipient`, i.e. `"EbisuSDK"`):

```xml
<LSX><Response id="{id}" sender="EbisuSDK"><GetConfigResponse>
  <Service Name="SDK"      Facility="SDK"/>
  <Service Name="PROFILE"  Facility="PROFILE"/>
  ...one line per facility, Name == the Facility enum name...
  <Service Name="CONTENT"  Facility="CONTENT"/>
</GetConfigResponse></Response></LSX>
```

Choosing **`Name == Facility`** is deliberate: with rank 1's echo, every response stays correct
automatically, and the pushed events' *original* senders (`"LOGIN_EVENT"`, `"ONLINE_STATUS_EVENT"`)
become correct for the first time. `Name` is free-form as far as anything we found goes (we own both
ends), but see §5.

**Ordering hazard (important):** populating the table while any verb still hard-codes
`sender="EbisuSDK"` — or while events still push `""` — breaks verbs that work today.
**Rank 1 must land first (or in the same commit).** Once echo is in place, rank 2 is a pure superset.

**Observable:** all 34 `std::string`s at `[SDK+0x3b0]` become non-empty (script in §5), and the
`<Event sender="LOGIN_EVENT">` push stops being dropped.

---

### RANK 3 — `blaze_responder_v3b.py`: change nothing that matters now; pre-stage the post-login RPCs

* **DO NOT** enlarge `fetchClientConfig` hoping to fill `[cfg+0x750]` — it is already `0x43c47ff0`
  and is fed by `preAuth`. Keep `blazeSdkClientId` / `blazeServerClientId` / `blazeSdkClientSecret` /
  `identityRedirectUri` **exactly where they are, in `blazesdk_config()` (preAuth CONF)**; moving them
  would break getter `0x146e1bc50`.
* Keep the empty REPLY to `Authentication::logout (1/0x46)` — normal, not a failure.
  **Fix the misleading log line (~L5935/6777/6947 pattern, emitted around `blaze_responder_v3b.py:1091`):**
  it currently blames "layer 1 or blazeSdkClientId". Replace with: *"logout is a normal OSDK step; the
  login gate is `OriginSDK+0x3a0 != 0` (LSX GetProfile), not Blaze config."*
* **Pre-stage Authentication handlers** for the stubs actually compiled into this client (found by
  enumerating all 188 rel32 callers of `sendRequest 0x146df0e80` and reading the preceding
  `mov r9d,imm32`): `0x0A` login, `0x14`, `0x1E`, `0x26`, `0x2D`, `0x2F`, `0x46` logout, `0xF1`,
  `0xF2`, `0xF6`, `0x122`.
  `0x1E` is the one that will stall you next: callers `0x146e15a6e`/`0x146e15cbe` (vtable
  `0x14389fb98` slots +0xe8/+0xe0, discriminated by a bool arg) =
  `getTermsOfServiceContent` / `getPrivacyPolicyContent`; `LoginStateAuthenticated` caches the result
  at `state+0x68/+0x78` (strings `"LoginStateAuthenticated::mTermsOfServiceBuffer"@0x14389fe98`,
  `"…mPrivacypolicyBuffer"@0x14389fec8`) and will hang on an empty body. Serve non-empty text.
* Expect `login`'s `LoginRequest.AUTH` to carry the auth code we hand out at
  `lsx_responder_v2.py:352` (`OPENFUT_AUTHCODE`, default `OPENFUT-000000000000000000000000`).

---

## 4. MEMORY-FORGE SHORTCUT (fallback only — server-side is strictly better)

If rank 1 somehow does not take, you can force the same state. Note `+0x3a0` is a **u64 scalar**
(UserId), not a pointer — ENQUEUE_PLAN's "point it at the SDK object" advice works only because the
guards are `!=0` and `==[sdk+0x3a0]`; the *correct* value is our UserId.

```python
# force_defaultuser.py   (ptrace_scope=0)
import struct
pid = 13643                                   # pgrep -x FIFA17.exe
f   = open(f"/proc/{pid}/mem","r+b")
def q(va): f.seek(va); return struct.unpack('<Q', f.read(8))[0]
sdk = q(0x144b7c7a0)                          # live 0x25c98c50
f.seek(sdk+0x3a0); f.write(struct.pack('<Q', 33068179))   # UserId    0x1f89493
f.seek(sdk+0x3a8); f.write(struct.pack('<Q', 33068179))   # PersonaId 0x1f89493
```

Raw bytes: at `SDK+0x3a0` write `93 94 f8 01 00 00 00 00`, and the same eight bytes at `SDK+0x3a8`.

This alone satisfies `OriginGetDefaultUser (0x1470da6d0)` and both guards in
`OriginRequestAuthCodeSync (0x1470e67f0)` (`test rdx,rdx` / `cmp rdx,[rcx+0x3a0]`). It does **not**
fix the response matcher, so the resulting `<GetAuthCode>` reply would still be rejected with
`0xa2080000` unless rank 1 is also in — i.e. **the forge is only useful as a same-run A/B control**
("does `+0x3a0 != 0` really clear `OSDK_INVALID_USER`?"), not as a shipping path.

Explicitly **not recommended any more**: forging the `FirstPartyAuthCodeFutureImpl` node
(ENQUEUE_PLAN §1/§3a). With the matcher fixed, FIFA enqueues it itself, and the forged-node route
leaves the OSDK state machine untouched — which is why the earlier forge produced a `<GetAuthCode>`
on the wire but never a Blaze `login`.

---

## 5. STILL NEEDS A LIVE EXPERIMENT

1. **The whole of §3 rank 1 needs a FIFA restart.** `GetProfile` has not recurred since log id=18
   (line 1068 of 3385) and `+0x3a0` is still 0 in pid 13643 — the running instance will **not**
   self-heal. `Initialize` runs once.
2. **`Name` semantics in `<Service>`.** Nothing in `0x1470d0000-0x147160000` compares a service *Name*
   against a literal, but the search was not exhaustive. If some path does, `Name == Facility-enum-name`
   would break and real Origin-client Names would be needed. Cheap probe: rank 2 with `Name==Facility`;
   if a verb that worked under rank 1 starts failing, that is the culprit.
3. **Facility index for `GetInternetConnectedState`, `SetDownloaderUtilization`,
   `IsProgressiveInstallationAvailable`** is inferred (9 / 31 / 31). Echo-back makes this moot for
   rank 1; it only matters if you ever hard-code senders again. Confirm by reading the
   `add rdx,0xNN` after `mov rdx,[sdk+0x3b0]` at `0x1470e040a`, `0x14712b821`, `0x147129ddb`.
4. **`[SDK+0x1e8]` (= `conn+0x80`, live `-1`)** — a second connection handle, read by `0x14712cc00`
   and waited on by `0x1470e6b70` (condvar `[sdk+0xf8]`, alt-wake `[sdk+0x240]`, both live 0). Its
   *writer* is untraced; the conn object at `SDK+0x168` has vptr `0x14394c218` with 2 slots
   (`0x14712c9b0`, `0x14712ce00`) — disassemble those. Watch whether it flips after rank 1; if it
   stays `-1` and something blocks on `0x1470e6b70`, that is the next gate.
   Also unidentified: `0x14712cc30(SDK+0x168, &outString)`, called after that wait, whose result is
   inserted into the map at `SDK+0x1f8` (size `[SDK+0x218]`, live 0).
5. **Which of `UserId` / `PersonaId` lands in `+0x3a0` vs `+0x3a8`** is unobservable while we send the
   same number for both. Make them differ once, then read both slots.
6. **Does the fixed chain reach Blaze in one go**, or is there a further OSDK sub-state after 16?
   Watch `[0x43d189d8+0x260]` (re-find via vptr `0x14395c180`) walk off 16 and
   `LoginManagerImpl+0x148 → stateMachine+0x30` leave `0xFFFFFFFF`.
7. **After `login` fires**, `Authentication 0x1E` (ToS / privacy content) is the next likely stall —
   see rank 3.
8. **Post-fix event senders.** Confirm the `LOGIN_EVENT`/`ONLINE_STATUS_EVENT` handlers stop dropping
   frames: under rank 1 push `sender=""`; under rank 2 push `sender="LOGIN_EVENT"`. Note the earlier
   claim that `0xa2080000` in the log means failure is **weak** for events — with 73 EventHandler
   template instantiations, non-matching handlers log it as ordinary fan-out noise. Use
   `OriginMgr(*[0x1448acf50])+0x13` (`m_isLoggedIn`) flipping to 1 *by itself* as the event observable.

### Live verification snippet (run before and after)

```python
#!/usr/bin/env python3
import struct, glob
pid = next(int(d.split('/')[-1]) for d in glob.glob('/proc/[0-9]*')
           if open(d+'/comm').read().strip() == 'FIFA17.exe')
f = open(f"/proc/{pid}/mem", "rb")
def rd(va,n): f.seek(va); return f.read(n)
def q(va):    return struct.unpack('<Q', rd(va,8))[0]
sdk = q(0x144b7c7a0)
print(f"SDK={sdk:#x} +0x3a0={q(sdk+0x3a0):#x} +0x3a8={q(sdk+0x3a8):#x} "
      f"+0x1e8={q(sdk+0x1e8):#x} +0x270={q(sdk+0x270):#x}")
b, e = q(sdk+0x3b0), q(sdk+0x3b8)
for i in range((e-b)//0x20):
    s = rd(b+i*0x20, 0x20)
    n, cap = struct.unpack('<Q', s[0x10:0x18])[0], struct.unpack('<Q', s[0x18:0x20])[0]
    if n:
        print(i, (rd(struct.unpack('<Q', s[:8])[0], n) if cap >= 16 else s[:n]).decode())
```

Baseline recorded today (pid 13643): `+0x3a0=0x0 +0x3a8=0x0 +0x1e8=0xffffffffffffffff +0x270=0x1c`,
all 34 names empty, `[0x43d189d8+0x80]="OSDK_INVALID_USER"`, `[+0x260]=16`,
`/tmp/blaze_responder.log`: 37× `logout`, 0× `login`.
