Files
OpenFUT/fifa17-recon/tools/acct_error_trace.md
T
funman300 6ddd5e9d47 fifa17-recon: offline FUT squad-shell working + full card-system RE
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
2026-08-01 20:24:30 -07:00

341 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# "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).