# "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,` + `lea rdx,` + `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,; 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` 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).