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
15 KiB
"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:
{ "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
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.- Which of the two sites fired (
LoginStateConnectvsLoginStateLoadIspAccountInfo).LoginStateConnectis 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
0x147190e80and dumpr9(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 eventosdk/error/%demitted at0x147190f24.
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:
- Return real, non-empty
fetchClientConfigmaps forOSDK_CORE,OSDK_CLIENT,OSDK_NUCLEUS,OSDK_WEBOFFER,OSDK_ABUSE_REPORTING,OSDK_TICKER(note:OSDK_TICKER, per the in-binary list at0x143962c40— earlier notes recordedOSDK_XMS_ABUSE_REPORTINGas the 6th; verify against the live capture).OSDK_NUCLEUSmust carry auth/account URLs pointed at our stub. - That should let
LoginStateConnectreach sub-state 3 and advance toLoginStateLoadIspAccountInfo, which then needs the account country code to come back as a real 2-letter code (not0, not"ZZ") from the'cnnc'service. - Only then does
LoginStatePCLoginrun — and that is where LSXGetAuthCodewill finally be requested, followed by BlazeAuthentication::login(1/0x0A).