# "Unable to retrieve account information. Please try again." — full trace (FIFA 17)

Clean-room. Sources: live `/proc/PID/mem` of FIFA17.exe (PID 3362053, base `0x140000000`),
cached image dumps `scratchpad/live_code.bin` (`0x140001000..0x144c23000`) +
`live_high.bin` (`0x144ed3000..0x151359000`), and `scratchpad/navregion.bin` (heap nav JSON).
All VAs absolute, stable across runs. New tools: `fx.py` (offline image str/dis/xref/callers),
`memsearch2.py` (ASCII+UTF-16 live search), `ptrscan.py`, `livedis.py`, `imgstr.py`.

---

## 0. Headline

The popup is **not** a login *failure*. It is the OSDK **generic alert** raised on a
**60-second timeout** while the OSDK login state machine waits for a step to complete.
The loc id is **`OSDK_A_R30B`**. The two states that raise it are
**`LoginStateConnect`** (waiting for all six `Util::fetchClientConfig` results) and
**`LoginStateLoadIspAccountInfo`** (waiting for the account country/locale from the OSDK
`'cnnc'` service). Both sit **upstream of `LoginStatePCLogin`**, which is where
`GetAuthCode` + `Authentication::login` live — which is exactly why neither is ever seen.

---

## 1. The string is not in the EXE

Live search (ASCII **and** UTF-16LE) over every readable mapping: 14 hits, **all in
anonymous heap**, zero inside the image range `0x140000000..0x151359000`.
So it is a **localized string loaded from data**, resolved at runtime.

Representative heap copies (ref-counted `[u16 refcnt=1][u16 len=0x39][u16 cap]` header,
MSVC debug heap `cd`/`fe` fill around them):

```
0x0000b7563b00  "Unable to retrieve account information. Please try again."
0x0000b79ad020  same
0x0000bc81ba70  same
0x00000db86f88  UTF-16LE render copy
0x000078f5380   entry in a 64-byte-slot string table (slots 0x78f5280,2c0,300,340,380,3c0,…)
```

## 2. Who owns the string — the popup record

Pointer-scan for holders of `0x78f5380` found an FE parameter record at `0x41abd7c0`,
a run of `{begin,end,cap,alloc}` string triples:

| field | value |
|---|---|
| `0x41abd798` | `"OnlineLoginViewModel"` (20 ch) |
| `0x41abd7c0` | `"ALERT_POPUP"` (11 ch) |
| `0x41abd7e0` | `"Unable to retrieve account information. Please try again."` (0x39 = 57 incl NUL) |

=> the text is the **`ALERT_POPUP` data-provider value of `OnlineLoginViewModel`**.

Confirming strings in the image:

```
0x143b4d6c0  OnlineLoginViewModel
0x143b4d890  UIFrameworkService::UIFDataProvider::PopupDp
0x143b4d8c0  LOGIN_POPUP
0x143b4d8e0  ALERT_POPUP
0x143b4d8f0  processLoginAlert
0x143b4d908  processBootLoginFailure
0x143b4d920  processLoginFailure
```

## 3. Which FE action sets it

`OnlineLoginViewModel` action trampoline **`0x147dc62c0`**
(`this=rcx, actionId=edx, params=r8`):

```
1471dc62dc: lea eax,[rdx-0x27a4]      ; actionId - 10148
1471dc62ea: cmp eax,0x6 ; ja  ->ret   ; only ids 0x27a4..0x27aa
1471dc6310: lea rdx,"StrParam" ; call [params->vt+0x28]   ; HasParam("StrParam")
1471dc6329: lea rdx,"StrParam" ; call [params->vt+0x20]   ; GetString("StrParam", &buf)
1471dc634d: call 0x147e03080          ; handler(this, id, buf)
```

Handler **`0x147e03080`** — one function, 7-way jump table
(`add ebx,[0x146b1d808]` where the dword = `-10148`; table at `0x1429f9148`):

| case | target | does |
|---|---|---|
| 0 | `0x147e03134` | sets `LOGIN_POPUP` |
| 1 | `0x147e03579` | sets `LOGIN_POPUP` |
| **2** | **`0x147e03682`** | **sets `ALERT_POPUP` (`0x147e03690`) + button `OSDK_OK` + callback `processLoginAlert`** |
| 3 | `0x147e03ac6` | `processBootLoginFailure` |
| 4 | `0x147e03b3e` | `ALERT_POPUP` + `OSDK_OK` + `processLoginFailure` |
| 5 | `0x147e04015` | popup hide |
| 6 | `0x147e03fe4` | popup show |

Action-id ↔ name registration block (`0x147de8330..0x147de8544`, `mov edx,<id>` +
`lea rdx,<name>` + `call [vt+0x20]`):

| id | name | case |
|---|---|---|
| `0x27a4` | `onlineLoginToEaPopup` | 0 |
| `0x27a5` | `onlineBootLoginToEaPopup` | 1 |
| **`0x27a6`** | **`evt_onlineAlertPopup`** | **2** |
| `0x27a7` | `evt_onlineBootLoginFailurePopup` | 3 |
| `0x27a8` | `evt_onlineLoginFailurePopup` | 4 |
| `0x27a9` | `onlineLoginPopupHide` | 5 |
| `0x27aa` | `onlineLoginPopupShow` | 6 |

**=> the popup is `evt_onlineAlertPopup` (0x27a6), message carried in param `StrParam`.**
It is explicitly *not* `evt_onlineLoginFailurePopup` / `evt_onlineBootLoginFailurePopup`.

## 4. The FE flow (`/online/onlineLoginFlow.nav`)

Recovered verbatim from `navregion.bin` (offset ~31456301); saved to
`scratchpad/onlineLoginFlow_raw.txt`:

```json
{ "name":"onlineLoginFlow"
, "onEnter":[["loadViewModel", ["OnlineLoginViewModel"]]]
, "states":[{ "transitions":[
      { "event":"evt_onlineAlertPopup",            "targets":["onlineAlertLoginPopup"] }
     ,{ "event":"evt_onlineBootLoginFailurePopup", "targets":["onlineFailureLoginPopup"] }
     ,{ "event":"evt_onlineLoginFailurePopup",     "targets":["onlineFailureLoginPopup"] }
     ,{ "event":"evt_online_disconnected",         "targets":["processLoginFailure"] } ...]
  ,"states":[
     { "name":"startLoginWithoutMultiplayerCheck"
      ,"onEnter":[["sendScreenEvent", ["OnlineLogin", "0"]]] }
    ,{ "name":"onlineAlertLoginPopup"
      ,"onEnter":[["sendAction", ["onlineLoginPopupShow"]]]
      ,"onExit" :[["sendAction", ["onlineLoginPopupHide", "ALERT_POPUP"]]]
      ,"states":[{ "name":"onlineAlertLoginIdle"
                  ,"transitions":[{"event":"processLoginAlert","targets":["processLoginAlert"]}]}
                ,{ "name":"processLoginAlert"
                  ,"onEnter":[["sendScreenEvent", ["ProcessLoginAlert"]]] }]} ...]}]}
```

Entry per `nav/root.nav`: `launchFUTFlow` (`/online/origin.nav`) →`OriginIsOnlineTrue`→
`futBlazeLogin` (`/online/onlineLoginFlow.nav`, entry `startLoginWithoutMultiplayerCheck`)
→ `loginSuccess` → `CheckFUTRosters` → `futFlow`.

**We are inside `futBlazeLogin`.** `origin.nav` already emitted `OriginIsOnlineTrue`
(our LSX work) — the wall is one layer deeper.

## 5. The C++ that raises it — OSDK login state machine

EA **Online SDK (OSDK) 8.01.03.00-fifa.01**; source paths embedded, e.g.
`E:/p4/fifafb/rl/empatch/TnT/Code/fifa/gamemodes/extern/OSDK/8.01.03.00-fifa.01/source/common/modulemanager.cpp`.

States are classes whose vtable slot 1 is a `GetName()` thunk (`lea rax,<name>; ret`)
in `0x14719b3xx`; vtable slot 4 (`+0x20`) is the per-tick handler; slot 8 (`+0x40`) is
"advance to next state".

| state | vtable | handler (vt+0x20) |
|---|---|---|
| `LoginStateIsp` | `0x14395bc78` | `0x1471b45f0` |
| `LoginStateConnect` | `0x14395be20` | `0x1471b4a40` |
| `LoginStateLoadIspAccountInfo` | `0x14395bd18` | `0x1471b4b40` |
| `LoginStatePCLogin` | `0x14395c180` | `0x1471b58e0` |

Shared alert builder **`0x147190e80`** — `OSDK_RaiseError(this, Notification* out, int code, const char* locId)`:

```
147190e9b: mov byte [rdx],1          ; notif type = error
147190e9e: mov byte [rdx+2],1
147190ea2: mov [rdx+8],r8d           ; numeric code
147190ebd: lea rcx,[rdi+0xc] ; call 0x145e27ff0   ; strcpy(notif+0xc, locId)
147190ee0: mov byte [rdi+0x20b],0
147190f16: lea rdx,"error" ; lea rcx,"osdk" ; call 0x146ec02d0   ; telemetry osdk/error/%d
```

It has **16 call sites**, all inside the login-state cluster `0x1471b4xxx..0x1471b7xxx`
— i.e. it is *the* OSDK "raise login alert with loc id" entry point. The notification
carries the **loc-id string**, which the OSDK→FE bridge resolves to localized text and
delivers as `StrParam` on `evt_onlineAlertPopup`.

Loc ids raised by neighbouring states (for contrast):

```
0x14395cdd8  OSDK_SIGNIN_REQUIRED            (LoginStateIsp, default branch)
0x14395ce48  OSDK_ONLINE_OUTDATED_PATCH      (LoginStateIsp, NetConn 4CC '-upp')
0x14395ce68  OSDK_ONLINE_DISABLED_PARENTAL   (LoginStateIsp, NetConn 4CC '-adu')
0x14395cdc8  OSDK_ERROR
0x14395c6b8  OSDK_A_R30B                     <<< our popup
```

### 5a. `LoginStateConnect` — handler `0x1471b4a40` (STRONGEST CANDIDATE)

Sub-state in `[this+0x20]`, deadline in `[this+0x24]`.

```
; ---- sub-state 0 : kick off ----
1471b4b0d: mov rcx,[rbx+0x28]
1471b4b11: call 0x1471a8970          ; issue all client-config loads
1471b4b16: lea eax,[rdi+0xea60]      ; rdi = now(ms);  0xea60 = 60000 = 60 s
1471b4b1c: mov [rbx+0x20],1          ; -> polling
1471b4b23: mov [rbx+0x24],eax        ; deadline = now + 60 s

; ---- sub-state 1 : poll ----
1471b4ab4: mov rax,[rbx+0x28] ; xor edx,edx
1471b4aba: mov r8d,[rax+0x60]        ; entry count
1471b4abe: test r8d,r8d ; je -> sub-state 3      ; nothing pending => done
1471b4ac3: mov r9d,[rax+0x64]        ; stride
1471b4ac7: mov r10,[rax+0x58]        ; array base
1471b4acb: (loop) mov rcx,[base + idx*stride]
1471b4ad5:        cmp byte [rcx+0x60],0
1471b4ad9:        je  0x1471b4af4                ; <-- this entry NOT ready
1471b4ae2: mov [rbx+0x20],3          ; all ready -> success
1471b4af4: sub edi,[rbx+0x24]        ; now - deadline
1471b4af7: test edi,edi ; jle -> ret ; still inside the 60 s window: keep waiting
1471b4afb: mov [rbx+0x20],2          ; TIMEOUT

; ---- sub-state 2 : alert ----
1471b4a86: lea r9,"OSDK_A_R30B" ; xor r8d,r8d ; mov rdx,r10 ; mov rcx,rbx
1471b4a9b: call 0x147190e80
```

`0x1471a8970` iterates the same array and for each entry calls `0x1471a89f0`, which:

```
1471a8a20: mov byte [rcx+0x60],0     ; clear the "ready" flag the poll loop reads
1471a8a2c: lea r8,"LoadCfg:%s"       ; 0x143962390
```

**`LoadCfg:%s` is `Util::fetchClientConfig`.** The config-name list sits immediately
after it, and matches the six CFIDs we observe on the wire byte-for-byte:

```
0x143962be8  OSDK_CORE
0x143962bf8  OSDK_CLIENT
0x143962c08  OSDK_NUCLEUS
0x143962c18  OSDK_WEBOFFER
0x143962c28  OSDK_ABUSE_REPORTING
0x143962c40  OSDK_TICKER
```

The entries are `ResourceLoader` objects (`0x143962960..0x143962b60`, states
`LOADED` / `LOADING` / `NOT_FOUND`, `ResourceLoader::ResourceFailure()`).
`[entry+0x60]` is the "loaded OK" flag. **An empty / failed config reply leaves it 0.**

### 5b. `LoginStateLoadIspAccountInfo` — handler `0x1471b4b40` (SECOND CANDIDATE)

Same 4-sub-state shape:

```
; sub-state 0 : GetService('cnnc')->[+0x38]()->[+0xb0]() ; -> sub-state 3
1471b4e9d: mov rcx,[0x144b86bf8] ; mov edx,0x636e6e63 ('cnnc')
1471b4eac: call [rax+0x60]       ; mgr->GetService('cnnc')
1471b4eb5: call [rdx+0x38]
1471b4ebe: call [rdx+0xb0]

; sub-state 1 : poll the same accessor
1471b4e6d: call [rdx+0xb0] -> eax
1471b4e73: test eax,eax ; je  -> timeout check      ; 0     = not resolved
1471b4e77: cmp eax,0x5a5a ; jne -> sub-state 3      ; 'ZZ'  = placeholder
1471b4e7e: sub ebx,[rdi+0x24] ; jle -> ret          ; inside deadline: keep waiting
1471b4e85: mov [rdi+0x20],2                         ; TIMEOUT

; sub-state 2 : alert
1471b4e1f: lea r9,"OSDK_A_R30B" ; call 0x147190e80

; sub-state 3 : SUCCESS — parse the account locale
1471b4b84: mov rsi,[0x144b86bf8] ; mov rdx,[rsi+0xb0]   ; char* country/locale
           ... packs 4 chars, lowercases pair 1 (|0x20), uppercases pair 2 (&0xdf)
1471b4df0: call [rax+0x188] ; mov [rax+0x52c],ebx       ; store packed "enUS"-style u32
1471b4e1c: jmp [this->vt+0x40]                          ; advance to next login state
```

`0x5a5a` is literally **`"ZZ"`** — the unknown-country placeholder. So this state waits
for a **real 2-letter account country code** from the OSDK `'cnnc'` (connection /
Nucleus adaptor) service and alerts `OSDK_A_R30B` if it never arrives.

Operation names registered in the same module manager, right beside `LoadCfg:%s`:

```
0x1439623c0  FetchAccountInfo        <<< semantic match for the popup text
0x1439623d8  UpdateAccountInfo
0x1439623f0  LookupOriginPersona
0x143962408  FetchOriginPersona
0x143962420  CreateOriginPersona
0x143962598  "Operation timed out.  Name = [%s], Handle = [%u]"
```

---

## 6. Why this matches the observed behaviour exactly

| observation | explanation |
|---|---|
| `preAuth` → `ping` → **6× `fetchClientConfig`** → then popup | `LoginStateConnect` sub-state 0 issues exactly these 6 `LoadCfg:` requests |
| popup appears after a wait, not instantly | hard-coded **60 000 ms** deadline (`lea eax,[rdi+0xea60]`) |
| we answer all 6 `fetchClientConfig` with **empty** replies | `ResourceLoader` never reaches `LOADED`, `[entry+0x60]` stays 0 → poll never satisfied → timeout |
| popup is the **alert**, not the login-failure popup | `evt_onlineAlertPopup` (0x27a6), not 0x27a7/0x27a8 — the flow never reported a login failure, it timed out earlier |
| **`GetAuthCode` never requested** | `Origin::OriginSDK::RequestAuthCodeSync` / `LSXRequest<lsx::GetAuthCodeT,…>` is driven from `LoginStatePCLogin`, which is **downstream** and never entered |
| **`Authentication::login` (1/0x0A) never sent** | same reason — the state machine aborts before it |
| client sends `Authentication` 1/0x46 then reconnect-loops | the OSDK teardown path after the alert (`LoginStateLogout`) |
| nucleusConnect stub on :42131 gets zero requests | the client gets its Nucleus config **from `fetchClientConfig OSDK_NUCLEUS`**, which we return empty — so it never learns a Nucleus URL to dial |

The last row is the key causal loop: **`OSDK_NUCLEUS` is where the Nucleus/account
endpoints come from.** Returning it empty both (a) fails the `LoginStateConnect` ready
check and (b) guarantees the account-info fetch can never resolve, which is why *both*
`OSDK_A_R30B` sites are armed.

---

## 7. What was NOT proven

1. **`OSDK_A_R30B` → that exact English text** is inferred, not read. The loc DB lives in
   compressed Frostbite superbundles (`Data/Win32/loc/en.toc` + `.sb`, chunked); the
   plain text is not greppable on disk and the game exited before I could read the live
   loc table. Confidence is high (it is the only alert id raised by the two
   "waiting for account data" states, and every other login alert id has a clearly
   different meaning), but it is one unverified link.
2. **Which of the two sites fired** (`LoginStateConnect` vs `LoginStateLoadIspAccountInfo`).
   `LoginStateConnect` is the stronger candidate because the observed Blaze traffic stops
   exactly at its wait condition.

### Cheap runtime confirmations (next live run)

* Read `[state+0x20]` / `[state+0x24]` while the spinner is up — sub-state 2 = timeout hit.
* Breakpoint / patch-log `0x147190e80` and dump `r9` (the loc-id string) — this names the
  alert with zero ambiguity and works for every login alert.
* Grep `/tmp/lsx.log`-style logging for the OSDK telemetry event `osdk` / `error` / `%d`
  emitted at `0x147190f24`.

---

## 8. Actionable conclusion

**Layer implicated: Blaze `Util::fetchClientConfig` (9/0x01) — not LSX, not Nucleus HTTP.**

The Origin/LSX layer is already satisfied (`origin.nav` emitted `OriginIsOnlineTrue`).
The wall is the **first Blaze step after preAuth**: the client demands non-empty client
configs for all six CFIDs and blocks for 60 s, then alerts.

Fix order:
1. Return **real, non-empty** `fetchClientConfig` maps for `OSDK_CORE`, `OSDK_CLIENT`,
   **`OSDK_NUCLEUS`**, `OSDK_WEBOFFER`, `OSDK_ABUSE_REPORTING`, `OSDK_TICKER`
   (note: **`OSDK_TICKER`**, per the in-binary list at `0x143962c40` — earlier notes
   recorded `OSDK_XMS_ABUSE_REPORTING` as the 6th; verify against the live capture).
   `OSDK_NUCLEUS` must carry auth/account URLs pointed at our stub.
2. That should let `LoginStateConnect` reach sub-state 3 and advance to
   `LoginStateLoadIspAccountInfo`, which then needs the account **country code** to come
   back as a real 2-letter code (not `0`, not `"ZZ"`) from the `'cnnc'` service.
3. Only then does `LoginStatePCLogin` run — and that is where **LSX `GetAuthCode`** will
   finally be requested, followed by Blaze `Authentication::login` (1/0x0A).
