6ddd5e9d47
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).
Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
reads identity/rating/face from a resolved record at item+0x10, filled
by a lookup (0x18011cca0) in the FUT item-definition std::map at
CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
(JSON fields routed to the skip handler). Owned items don't auto-trigger
a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
ready; the fetch trigger lives in the packed FIFA17.exe.
New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
341 lines
15 KiB
Markdown
341 lines
15 KiB
Markdown
# "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).
|