# FUT loading-screen QUIET STALL — completion condition, fix, and experiment plan Clean-room. Every claim below is backed by a VA + bytes/disasm read from the **live frozen process (pid 66672)** via `/proc/66672/mem`, or by a log/capture line. Nothing from leaked EA source or headers. All disassembly in this document was re-dumped and re-verified by the synthesis pass, not copied from the reverser reports. --- ## 0. Verdict in one paragraph The FUT loading screen is **not** waiting on a Blaze RPC reply, **not** waiting on a server-pushed notification, and **not** waiting on entitlements. It is sitting in the data-defined UI state `CheckFUTRosterUpdateXML`, which fires the native condition `isFUTRosterXMLAvailable` and then leaves **only** on `advance` (roster XML downloaded) or `back` (download failed). Neither event can ever fire, because the roster-update **URL resolves to an empty string** and the fetcher takes a silent early-return that never issues a request and never arms a callback. The URL has exactly three sources; two are ini keys that do not exist on disk, and the third is the Blaze client-config store — which we answer with an **empty map**. So the missing input is a single **config string**: `ROSTERUPDATE_URL`, deliverable through `Util::fetchClientConfig`. --- ## 1. THE COMPLETION CONDITION ### 1.1 The state machine, read live out of FIFA's heap FUT entry is driven by the flow `checkFUTRostersFlow` (`data/ui/nav/checkFUTRostersFlow.nav`, path string live @0x1b8fc38). The full JSON is resident at **0x41abd000-0x41abe400**. Verified live (84 non-image copies of the literal `checkFUTRostersFlow`, 17 of `CheckFUTRosterUpdateXML`; zero copies of either inside the image range 0x140000000-0x151359000 — i.e. these are runtime data, not string constants). ``` LoadFUTDatabase → LoadFUTSquad → CheckFUTRosterUpdateXML → CheckFUTSquadBinFile │ │ │ back ├─ "false" → EnterFUT ← GOAL ▼ └─ "true" → FUTLiveDBPopup → downloadLiveDB FailFUTRosterXMLDownloadPopup │ advance ▼ unloadFUTDatabaseOnFail (dead end — FUT aborts) ``` The blocked state, verbatim from live memory: ```json "name":"CheckFUTRosterUpdateXML" ,"onEnter": [ ["sendAction",["condition", "isFUTRosterXMLAvailable"]] ,["sendScreenEvent", ["ShowLoadingIcon"]] ] , "onExit": [ ["sendScreenEvent", ["HideLoadingIcon"]] ] ,"transitions": [ {"event":"advance" ,"targets":["CheckFUTSquadBinFile"]} ,{"event":"back" ,"targets":["FailFUTRosterXMLDownloadPopup"]} ,{"event":"evt_online_disconnected","targets":["unloadFUTDatabaseOnFail"]} ,{"event":"evt_invite_accepted" ,"targets":["unloadFUTDatabaseOnFail"]} ] ``` **There is no timeout transition.** `ShowLoadingIcon` on entry and `HideLoadingIcon` on exit is exactly the symptom: stadium + "17" + spinner, no error, forever. Two of the four exits (`evt_online_disconnected`, `evt_invite_accepted`) route to `unloadFUTDatabaseOnFail` — they *abort* FUT, they do not advance it. So the only forward exit is `advance`, and the only backward exit is `back`; both are raised by the roster-XML download callbacks (`futRosterXMLDownloadSuccess` @0x143b54400 / `futRosterXMLDownloadFail` @0x143b54420). ### 1.2 Good news downstream — the next state is already satisfied Also read live, and **not previously reported**: the state *after* the block short-circuits straight to `EnterFUT` for a fresh offline account. ```json "name":"CheckFUTSquadBinFile" ,"onEnter":[["sendAction",["condition", "isFUTSquadAvailable"]]] ,"transitions": [ {"event":"true" ,"actions":[["sendScreenEvent",["SetLiveDbDownloadState","4"]]],"targets":["FUTLiveDBPopup"]} ,{"event":"false","actions":[["sendScreenEvent",["EnterFUT",""]]] ,"targets":["advance"]} ] ``` `isFUTSquadAvailable == false` (no squad bin file yet) → **`EnterFUT` fires immediately**. So `CheckFUTRosterUpdateXML` is very likely the **last** gate in this flow. ### 1.3 The exact blocked input — verified disassembly Chain, all re-dumped live this session: **(a)** `isFUTRosterXMLAvailable` handler = **0x147d989d0** (dispatcher string-compares the condition name against `"isFUTRosterXMLAvailable"` @0x143b543b8 — literal confirmed live). Five skip predicates; if all return false: ``` 147d98a57: c6 87 38 01 00 00 01 mov BYTE PTR [rdi+0x138],0x1 ; "downloading" flag 147d98a5e: 48 89 d9 mov rcx,rbx 147d98a61: e8 3a 43 cd ff call 0x147a6cda0 ; start roster-XML fetch ``` **(b)** `0x147a6cda0` — the "issue if not already issued" wrapper: ``` 147a6cda5: 48 8b 59 08 mov rbx,QWORD PTR [rcx+0x8] ; this = *[obj+8] (the downloader) 147a6cda9: 83 bb a0 02 00 00 00 cmp DWORD PTR [rbx+0x2a0],0x0 147a6cdb0: 75 1d jne 0x147a6cdcf ; already issued -> return 147a6cdb6: c6 83 a4 02 00 00 00 mov BYTE PTR [rbx+0x2a4],0x0 147a6cdbd: e8 1e a3 08 00 call 0x147af70e0 147a6cdca: e9 41 a6 00 00 jmp 0x147a77410 ; -> URL resolver, this=rbx ``` **(c)** `0x147a77410` — the URL resolver. **This is where it dies.** Full verified path: ``` 147a7744d: mov DWORD PTR [rbx+0x2a0],0x0 ; clear "request issued" 147a77457: test rdi,rdi / je 0x147a775c5 ; bail #1: no download manager 147a77460: cmp DWORD PTR [rbx+0x2a8],0x5 147a77467: jb 0x147a77476 147a77469: cmp BYTE PTR [rbx+0x2b9],0x1 147a77470: jne 0x147a775c5 ; bail #2: >=5 attempts and flag != 1 ; --- source 1: game-ini key "FUT/ROSTERUPDATE_URL" ------------------------- 147a774b5: lea rdx,[rip+...] # 0x143af1810 ; "FUT/ROSTERUPDATE_URL" 147a774bf: call 0x147763ac0 ; ini->HasKey 147a774c4: test al,al / je 0x147a774f8 ; MISS in our setup 147a774f1: call 0x145e27ff0 ; strncpy(url, val, 0x100) ; --- source 2: game-ini key "ROSTERUPDATE_URL", wrapped "https://%s" ------- 147a774fd: lea rdx,[rip+...] # 0x143af1828 ; "ROSTERUPDATE_URL" 147a77507: call 0x147763ac0 ; ini->HasKey 147a7750e: je 0x147a77541 ; MISS in our setup 147a77524: lea r8, [rip+...] # 0x143af1840 ; "https://%s" 147a77538: call 0x145e24d70 ; sprintf(url, 0x100, "https://%s", val) ; --- source 3: Blaze CLIENT-CONFIG store ---------------------------------- 147a77541: mov rax,QWORD PTR [rsi] 147a77544: lea r9, [rsp+0x30] ; out buffer 147a77549: lea r8, [rip+...] # 0x14354b5f0 ; default = "" 147a77550: lea rdx,[rip+...] # 0x143af1828 ; key = "ROSTERUPDATE_URL" 147a77557: mov rcx,rsi ; cfg object 147a7755a: mov DWORD PTR [rsp+0x20],0x100 ; out size 147a77562: call QWORD PTR [rax+0x30] ; cfg->getString(key, "", out, 0x100) ; --- THE BAIL -------------------------------------------------------------- 147a77565: lea rcx,[rsp+0x30] 147a7756a: call 0x145e27b10 ; strlen(url) 147a77577: test rax,rax 147a7757a: 74 49 je 0x147a775c5 ; <<<<<< EMPTY URL -> SILENT RETURN ; --- the code that is being skipped --------------------------------------- 147a775a5: call QWORD PTR [r10+0x50] ; issue the HTTP download 147a775b2: call 0x147173690 ; attach job handle to [rbx+0x290] 147a775b7: mov DWORD PTR [rbx+0x2a0],0x1 ; "request issued" 147a775c1: mov BYTE PTR [rbx+0x48],0x0 147a775c5: ; bare epilogue ``` `0x147a775c5` is the bare epilogue. Taking it means: **no socket, no callback registered, no success event, no fail event, no timeout, no popup.** Exactly the observed quiet stall. All string literals re-read live from 0x143af1810 / 0x143af1828 / 0x143af1840 and confirmed as `FUT/ROSTERUPDATE_URL`, `ROSTERUPDATE_URL`, `https://%s`. ### 1.4 Why all three sources are empty — verified | Source | Status | Proof | |---|---|---| | ini `FUT/ROSTERUPDATE_URL` | absent | `grep -ral 'ROSTERUPDATE_URL' "/mnt/games/FIFA 17"` matches only FIFA17.exe / FIFA17_Trial.exe — not `default_startup.nsi`, not `Documents/FIFA 17/settings/` | | ini `ROSTERUPDATE_URL` | absent | same grep | | Blaze client-config store | **empty** | FIFA requested `fetchClientConfig(CFID='OSDK_ROSTER')` at RX #22 (`/tmp/blaze_rx/rx_0022_0009_0001.bin` decodes to `{'CFID': (1,'OSDK_ROSTER')}`); `blaze_responder_v3b.py` has **no** `OSDK_ROSTER` key in `CLIENT_CONFIGS` (line 511-524) → `client_config_for()` returns `[]` → EMPTY MAP | **Live confirmation that the store really lacks the key.** Full-address-space scan of pid 66672 (`scratchpad/scan.py`), counting image (0x140000000-0x151359000) vs non-image hits: ``` ROSTERUPDATE_URL image=2 nonimage=0 <-- NOT in the runtime config store FUT_RS4_BASE_URL image=1 nonimage=0 OSDK_ROSTER image=2 nonimage=3 (request-side CFID string @0x43c4a287 etc.) CheckFUTRosterUpdateXML image=0 nonimage=17 (flow is loaded and live) checkFUTRostersFlow image=0 nonimage=84 ``` For contrast, every key we **do** serve is present in the heap store; the live config-store region holds literal `key=value` strings: ``` 0x7b5a710 OSDK_CLUBS_INCOME_SEARCH_MAX=100 (from CFID OSDK_CLIENT) 0x7b5a779 OSDK_CLUBS_MAX_SEARCH_RESULT=50 0x7b5adf0 OSDK_ANTIGRIEFING_MAX_COUNT=0 (from CFID OSDK_CORE) 0x7b5ae84 SV_SERVER_VERSION=0 ``` ### 1.5 RESOLVED: the config store is MERGED, not CFID-scoped This was the single biggest open question across the reports (the reader at 0x147a77550 passes **no CFID**). Resolved by following the accessor live: ``` 0x1471995b0: mov rax,[0x144b86bf8]; ret ; global service registry -> 0x43c46c70 vtable 0x143959168, slot 0x118 -> 0x147199d90 0x147199d90: mov rax,[rcx] mov edx,0xe7ffa20f lea edx,[rdx-0x749c3ba8] ; = 0x73636667 = 4CC 'scfg' call QWORD PTR [rax+0x60] ; services('scfg') mov edx,0xc34f050f lea edx,[rdx-0x63ed98a3] ; = 0x5f616c6c = 4CC '_all' jmp QWORD PTR [r8+0x8] ; scfg->getSection('_all') ``` (FIFA 17 obfuscates immediates as `mov r32,A; lea r32,[r32+B]`; both constants decoded and checked arithmetically.) The object the roster resolver reads from is the config service's **`_all`** section — an all-sections merged view. **Therefore it does not matter which CFID we put `ROSTERUPDATE_URL` in.** Any CFID FIFA fetches before FUT entry will do. (Cross-check: the heap store above contains keys from `OSDK_CORE` *and* `OSDK_CLIENT` — two different CFIDs — and the same `vt[0x118]` accessor shape is used by `LoadRosterConfig` (0x14723bc50) to read `ROSTER_URL`/`ROSTER_VER`/`ROSTER_CSUM`.) ### 1.6 Corroborating live state (all re-verified this pass) * Sockets: exactly two, `127.0.0.1:51238→4216` (LSX, fd 55) and `127.0.0.1:43967→42130` (Blaze, fd 300). **Zero SYNs, zero HTTP, in ~4 h of uptime.** The fetch was never issued. * `grep -c CardsDLL /proc/66672/maps` = **0** — `CardsDLL_Win64_retail.dll` (the FUT DLL, strings `LOAD_FUT_DLL` @0x143aebde8, `CardsDLL` @0x143aebe00) is **not mapped**, i.e. `EnterFUT` never ran. This is the definitive "FUT booted" tripwire. * Blaze link healthy and idle: `/tmp/blaze_responder.log` shows only 20 s PINGs plus the 120 s CensusData re-subscribe, all answered. ### 1.7 What is NOT the block (ruled out, with evidence) **Not an RPC reply.** Heat2 decode is tag-driven, so a 0-byte REPLY decodes as "all members at default" and the SDK completes the job with ERR_OK. Behavioural proof: TX #19, 23, 24, 25, 27, 28, 29, 32 were all 16-byte empty REPLYs, and FIFA issued the *next* RPC immediately after each (RX #20 → #33, all inside one second at 20:47:17), and **never retried any of them**. No Blaze job is pending. **Not a notification.** Of the 22 Blaze components the client links, **14 share the stub `0x145bf3580`** (`lea rax,[0x14354b5f0]; ret`, and `[0x14354b5f0] == 0` → empty string) as their `getNotificationName` — including every FUT/OSDK-flavoured component (Easfc, FifaCups, OSDKSettings, OSDKTournaments, OsdkArena, SponsoredEvents, CoopSeason, VProSPManagement, EaAccess, Mail, GpsContentController) plus Authentication and Util. **There is no FUT/OSDK "data ready" notification in this client.** Independently, nothing in `checkFUTRostersFlow` consumes a Blaze notification — the only exits are the two local HTTP callbacks plus two abort events. **Not entitlements.** They *are* broken (see §2.2) but they are not this gate: an unentitled user takes the `ENTER_FUT2_POPUP_GO_TO_STORE` / `FUT_ENABLE_MENU` path (0x147c86d85-0x147c86df7) and never reaches `checkFUTRostersFlow` at all — yet we are demonstrably *inside* that flow. Also `FifaOnline::NotifyEntitlementsUpdated` (0x146f3f630) is broadcast on the common tail regardless of entitlement count (gated on the message error field, which is 0), so nothing is blocked waiting for it. --- ## 2. THE FIX — ranked and minimal ### FIX 1 (THE GATE) — serve `ROSTERUPDATE_URL` in `Util::fetchClientConfig` **File:** `/home/alex/Documents/OpenFUT/fifa17-recon/tools/blaze_responder_v3b.py` No new TDF work is needed: `fetch_config_response_fields()` already emits the correct `Blaze::Util::FetchConfigResponse{ CONF : map }`, and `client_config_for()` already `sorted()`s the pairs (Heat2 map ordering). The change is **data only**. **(a)** Next to `OSDK_TICKER = []` (line 497) add: ```python # --- FUT LOADING GATE --------------------------------------------------- # CFID FIFA 17 fetches at FUT entry (RX #22, rx_0022_0009_0001.bin). # ROSTERUPDATE_URL is the ONLY remaining source for the FUT roster-XML URL: # the two ini keys FUT/ROSTERUPDATE_URL (@0x143af1810) and ROSTERUPDATE_URL # (@0x143af1828) are absent on disk, so the resolver at 0x147a77410 falls # through to cfg->getString("ROSTERUPDATE_URL", "", out, 0x100) @0x147a77562. # An empty result makes 0x147a77577 test rax,rax / je 0x147a775c5 return # WITHOUT issuing the request and WITHOUT arming a callback, so flow # checkFUTRostersFlow state CheckFUTRosterUpdateXML never gets advance/back # -> the silent loading-screen hang. # The config store is the MERGED '_all' section (services('scfg') # ->getSection('_all'), decoded at 0x147199d90), so any fetched CFID works; # we put it in the CFID FIFA actually asks for at FUT entry. # NOTE: this branch does NOT wrap the value (unlike the ini branch, which # applies "https://%s" @0x143af1840) -> serve an ABSOLUTE url. OSDK_ROSTER = [ ("ROSTERUPDATE_URL", "http://127.0.0.1:8081/fifa17/fut/rosterupdate.xml"), ("ROSTER_URL", "http://127.0.0.1:8081/fifa17/roster/"), # literal @0x143973aa0 ("ROSTER_VER", "0"), # literal @0x143973ab0 ("ROSTER_CSUM", ""), # literal @0x143973ad0 ] ``` **(b)** Register it in `CLIENT_CONFIGS` (line 511-524): ```python "OSDK_ROSTER": OSDK_ROSTER, ``` **(c)** Belt-and-braces (free, and it front-loads the value to *login* time rather than FUT-entry time — useful because `OSDK_CORE` is fetched much earlier): append the same pair to `OSDK_CORE`: ```python ("ROSTERUPDATE_URL", "http://127.0.0.1:8081/fifa17/fut/rosterupdate.xml"), ``` `LoadRosterConfig` (0x14723bc50) builds its CFID with the format `"OSDK_ROSTER%s"` (@0x1439740a0, site 0x14723bc88), so a suffixed variant (`OSDK_ROSTERPC`, `OSDK_ROSTER1`, …) may also be requested. Because the store is merged, serving the key in `OSDK_CORE` covers every such variant — that is the main reason to do (c). **(d)** Stand up a trivial HTTP listener on `127.0.0.1:8081` answering that path. Start with `200 OK` + a minimal XML body. #### Observable success signals, strongest first 1. **A third socket.** `ss -tanp | grep FIFA17` shows a connection to `127.0.0.1:8081`. This alone proves the diagnosis — it is FIFA's **first ever** non-LSX/non-Blaze socket. 2. **`HideLoadingIcon`** fires and the flow leaves `CheckFUTRosterUpdateXML`. 3. **`CardsDLL_Win64_retail.dll` appears in `/proc/$(pgrep -x FIFA17.exe)/maps`.** This is the definitive "FUT booted" signal — `EnterFUT` ran. Per §1.2, with no squad bin file the flow goes `CheckFUTSquadBinFile → (false) → EnterFUT` **immediately** after the advance, so signal 3 should follow signal 1 within seconds. 4. Failing that: the `FUT_SQUAD_DOWNLOAD_FAIL` popup appears. **Still a win** — it proves the state machine moved and tells us the XML *content* is the next problem. (Be aware the fail popup dead-ends into `unloadFUTDatabaseOnFail`, so it aborts FUT; it is a diagnostic, not a path forward.) Live pointer for probing the downloader after the fix — the `this` for the resolver is `*[obj+8]` (from `0x147a6cda5 mov rbx,[rcx+0x8]`), and `[this+0x2a0] == 1` means the request was issued. (My live chase of `obj` via `*[0x144bfb910] → +0x80` reproduced the earlier reverser's failure: it lands on 0x7c17308, a string pool containing `MES_TAB_DP` / `MATCHDAY_SETTING`, so that pointer path is wrong. Use a breakpoint on 0x147a6cda0 instead — see §4.) --- ### FIX 2 (real bug, ship together, but it is NOT the gate) — entitlement group **Live-confirmed broken, re-verified this pass on pid 66672:** ``` base = *[0x1448a3b20] = 0x43dc3e70 (online flag byte[0x1448a3ac3] = 1) entMgr = *[base+0x2b10] = 0x43f15fd0 byte[entMgr+0x88] = 0 <-- "entitlements loaded" flag NOT set qword[entMgr+0x90/0x98/0xa0] = 0 / 0 / 0 <-- record vector begin=end=cap: store EMPTY ``` Cause: the response callback `EntitlementComponent::onListEntitlements` (0x146f27440) keeps an entitlement only if **all three** hold: * `strstr(GNAM, "FIFA17PCBoxContent") != NULL` **OR** `strstr(GNAM, "FIFA16PC") != NULL` (`0x146f27528` / `call 0x145e28fa0`, needles from the const array @0x144334030 = `{0x1438e4d20 "FIFA17PCBoxContent", 0x1438e4d38 "FIFA16PC"}` — both literals re-read live); * `strlen(TAG) != 0` (`0x146f2753e`); * `STAT == 1` (`0x146f2754c cmp DWORD PTR [rbx+0xb0],0x1`) — `EntitlementStatus::ACTIVE`. We send `GNAM = "FIFA17PC"` (`blaze_responder_v3b.py:106`). `strstr("FIFA17PC","FIFA17PCBoxContent")` is NULL (needle longer than haystack) and `strstr("FIFA17PC","FIFA16PC")` is NULL → **zero survivors** → the serialiser emits a zero-length blob → the consumer 0x146f4da20 bails at `cmp QWORD PTR [rdx+0x18],0 / jbe 0x146f4dd9a` and never executes `0x146f4dd8a mov BYTE PTR [rdi+0x88],0x1`. **Change (a)** — line 106: ```python ENTITLEMENT_GROUP = "FIFA17PCBoxContent" # was "FIFA17PC" -> matched NEITHER strstr needle ``` **Change (b)** — parameterise `entitlement_fields` (line 907) and emit a multi-element NLST. **Keep the existing field order** (`DEVI GDAY GNAM ID ISCO PID PJID PRCA PRID STAT STRC TAG TDAY TYPE UCNT VER`) — it is already the ascending-tag order Heat2 requires and is proven on the wire (TX #33.0, 152 payload bytes): ```python def entitlement_fields(now, group=None, tag=None, eid=1): group = group or ENTITLEMENT_GROUP tag = tag or ENTITLEMENT_TAG return OrderedDict([ ("DEVI", (STRING, "")), ("GDAY", (STRING, "2016-09-01T00:00:00Z")), ("GNAM", (STRING, group)), # MUST contain "FIFA17PCBoxContent" or "FIFA16PC" ("ID", (INT, eid)), ("ISCO", (INT, 0)), ("PID", (INT, PERSONA_ID)), ("PJID", (STRING, CONTENT_ID)), ("PRCA", (INT, 2)), ("PRID", (STRING, CONTENT_ID)), ("STAT", (INT, 1)), # MUST be exactly 1 (ACTIVE) ("STRC", (INT, 0)), ("TAG", (STRING, tag)), # MUST be non-empty ("TDAY", (STRING, "")), ("TYPE", (INT, 1)), # entitlementType — NOT checked client-side ("UCNT", (INT, 0)), ("VER", (INT, 1)), ]) def entitlements_response_fields(): now = int(time.time()) return OrderedDict([("NLST", (LIST, (STRUCT, [ entitlement_fields(now, "FIFA17PCBoxContent", "ONLINE_ACCESS", 1), entitlement_fields(now, "FIFA16PC", "ONLINE_ACCESS", 2), ])))]) ``` Hard constraints from the disassembly: * `PRID`/`GNAM`/`TAG` must contain **no `|` and no `/`** — survivors are re-serialised as `"PRID|GNAM|TAG|UCNT/"` (formats `%s|` @0x1438fa858, `%u/` @0x1438fa85c) and re-parsed with `strtok` on those delimiters (0x146f4db93 / 0x146f4dbd3). `CONTENT_ID = "1027460"` is fine. * Keep the list small — the compressor buffer is `0x1000` bytes (`0x146f27681 mov r8d,0x1000`). * Stored records are matched later with fixed-width `strncmp` on the **exact** group string (0x146ee9080, `r8d=0x41`), which is why both literals are sent verbatim. **Deliberately deferred:** the `CC*` DLC tags (`CCALLINONE`, `CCTEAMS1`, `CCTEAMS2`, `CCTOURNAMENTS`, `CCCAREERMODE`, table @0x1444049e8). `0x148198340` calls `HasEntitlement` with `group = NULL` (`xor edx,edx`), which would feed NULL into the `strncpy` at 0x145e27ff0. That path is currently unreachable *only because* `byte[mgr+0x88] == 0`; flipping the flag to 1 makes it live. Ship the two `ONLINE_ACCESS` entries first, confirm no crash, then add the DLC tags. **Success signal** (no restart needed once FIFA re-issues `listEntitlements`): `byte[0x43f15fd0+0x88]` flips `0 → 1`, and `qword[mgr+0x90] != qword[mgr+0x98]` with `(end-begin)/0x118 == 2`. Record layout: `+0x08` groupName, `+0x49` entitlementTag, `+0xca` productId, `+0x10c` useCount. --- ### FIX 3 (DO NOT ship yet) — CensusData notification `CensusData::subscribeToCensusDataUpdates(RSUB=1)` is the one outstanding subscription (re-issued every 120 s: RX #40, 46, 52, 58, 64, 70, 76, 82, 88, 94), we have never pushed `NotifyServerCensusData` (component 0x000A, notify id 0x0001), and the delivery path has a live registered FIFA-side listener (CensusData component obj @0x79f25c0; CensusDataAPI @0x43c70ff0; listener vector holds exactly one entry, 0x7810a58, whose vtable 0x1439742a8 sits beside the strings `CensusData` @0x1439742d0 and `SubscribeToCensusData` @0x143974320). **But** `checkFUTRostersFlow` provably consumes no notification, and the census path is currently *stable* (the 120 s cadence is healthy, not a storm). Changing it now adds a variable to the experiment that fixes the gate. **Hold this until after FIX 1 is evaluated.** If you do ship it later, the encoding is (ascending tag order, `TDFL` **empty** on purpose — its element member is TDF type 0x07 *variable TDF*, which `heat2.py` cannot encode): ```python def notify_server_census_data_fields(): return OrderedDict([ ("CNP", (INT, 30 * 1000000)), ("NTMT", (INT, 90 * 1000000)), ("RTMT", (INT, 300 * 1000000)), ("TDFL", (LIST, (STRUCT, []))), ]) ``` pushed alongside the existing reply via `notification(COMP_CENSUSDATA, NOTIFY_SERVER_CENSUS_DATA, encode_tdf(...))` — both constants already exist in the responder (lines 202, 211). --- ### FIX 4 (DO NOT ship) — non-empty Stats / OSDKSettings / userSettings replies Ruled out as the gate by §1.7 (no pending jobs, no retries, chain continued past every empty reply). Two ideas from the reports are worth *recording* but not shipping now, because each risks perturbing a currently-working login: * `Util::userSettingsLoad` — the real server answers a missing key with an **error** (`UTIL_USS_RECORD_NOT_FOUND`, in the Util error-enum string block @0x143895a40+), whereas we answer OK + `DATA=""`. Note there are **two** keys, `FirstTimeFlag` (RX #30) and **`AchievementCache`** (RX #31, `rx_0031_0009_000a.bin`) — not a duplicate. * Stats config trio (`getStatGroupList` 0x03, `getKeyScopesMap` 0x0f, `getPeriodIds` 0x14) left empty means `STATS_ERR_CONFIG_NOTAVAILABLE` for downstream stat calls. Neither can produce a *silent, timeout-free* hang, which is what we observe. --- ## 3. ORDERING — what ships together | Order | Fix | Ship with FIX 1? | Rationale | |---|---|---|---| | 1 | **FIX 1** — `ROSTERUPDATE_URL` + local HTTP stub | — | The gate. Ships alone or with FIX 2. | | 2 | **FIX 2** — entitlement group | **YES** | Independent code path; live-confirmed broken; zero interaction with the roster flow (its only shared surface is `NotifyEntitlementsUpdated`, which already fires today). Both fixes require the same FIFA relaunch, so bundling costs one experiment instead of two. | | 3 | FIX 3 — CensusData push | **NO** | Unproven; perturbs a currently-stable path; would confound FIX 1's signal. | | 4 | FIX 4 — non-empty Stats/OSDKSettings/userSettings | **NO** | Ruled out as the gate; regression risk on a working login. | **Ship FIX 1 + FIX 2 together in one relaunch.** They are separable in the log: FIX 1's signal is a new socket to :8081, FIX 2's signal is `byte[entMgr+0x88] == 1`. If FIX 2 causes a crash (the `group=NULL` DLC path, §2.2), back out FIX 2 only and re-run FIX 1 alone. Keep FIX 2 to the two `ONLINE_ACCESS` entries on the first run; add the `CC*` DLC tags only after confirming no crash. --- ## 4. WHAT STILL NEEDS A LIVE EXPERIMENT ### 4.1 Restart requirements — important **FIX 1 requires a FIFA relaunch.** `fetchClientConfig(CFID='OSDK_ROSTER')` is issued **once**, during FUT init (RX #22). A responder-only restart cannot re-deliver it — and worse, killing the responder drops the Blaze socket, which raises `evt_online_disconnected`, which transitions `CheckFUTRosterUpdateXML → unloadFUTDatabaseOnFail` and aborts FUT. **FIX 2 may not require a relaunch** — if FIFA re-issues `listEntitlements` on its own, the new reply is consumed immediately and `byte[entMgr+0x88]` flips. But since FIX 1 forces a relaunch anyway, do not spend an experiment on this. **Sequence for the combined run:** 1. Edit `blaze_responder_v3b.py` (FIX 1 a/b/c + FIX 2 a/b). 2. Start the HTTP stub on `127.0.0.1:8081`. 3. Restart the responder. 4. Relaunch FIFA 17, log in, enter FUT. 5. Watch, in order: `ss -tanp | grep FIFA17` for a `:8081` socket → HTTP stub access log → `grep -c CardsDLL /proc/$(pgrep -x FIFA17.exe)/maps` → `/tmp/blaze_responder.log` for any new RPC beyond PING / census re-subscribe. **Preserve the current frozen process (66672) until the new run reproduces the stall or passes it** — it is the only known-good snapshot of the exact wait state. ### 4.2 Free control experiment on the *current* frozen process (do this FIRST) Before relaunching, kill the Blaze responder and watch pid 66672. Per the flow JSON, `evt_online_disconnected` must transition `CheckFUTRosterUpdateXML → unloadFUTDatabaseOnFail` and tear FUT down. If the loading screen visibly reacts, that **proves live** that the flow SM is (a) alive and (b) parked in this state — closing the last inferential gap in §1.1 without any code change. If nothing happens, the state-machine identification is wrong and the whole plan needs re-examination. This is the highest-value/lowest-cost probe available right now, and it costs only a process we were going to relaunch anyway. ### 4.3 Open items that need a breakpoint (not just a memory read) 1. **Resolve the downloader `this`.** The pointer chase `*[0x144bfb910] → +0x80` is wrong (verified: yields 0x7c17308, a string pool). Break on `0x147a6cda0`, read `rcx`, then `this = *[rcx+8]`, then read `[this+0x2a0]` (0 = never issued), `[this+0x2a8]` (attempt counter, compared against 5 at 0x147a77460) and `[this+0x2b9]` (the retry-override flag). This turns "the bail-out branch is taken" from a strong inference into a direct observation. 2. **The XML wire format required for `advance`.** The parser is reachable from `RosterXMLDownloadedSuccess` @0x143af17f0 (site 0x147af03be) / `RosterXMLDownloadedFail` @0x143af16d8 (site 0x147afd0b2); the compared fields are `.dbFUTVer` / `.dbFUTCRC` / `.dbFUTLoc` (0x143af1750 / 0x143af1760 / 0x143af1770). Not yet reversed. **This is the likely next gate** — a 404 produces `back`, which dead-ends at `unloadFUTDatabaseOnFail`, so the XML must actually *succeed*. Cheapest route: serve `200 OK` with a minimal document, capture what the parser rejects, iterate. Aim for an XML that reports "no update available" so the flow advances with no further download. 3. **The five skip predicates** in `isFUTRosterXMLAvailable` (`0x147ad5410(obj)`, then `sess->vt[0x100]` / `[0xd0]` / `[0xe8]` / `[0x108]` at 0x147d98a1d / 2d / 3d / 4d). Any one returning true takes the 0x147d98a6b path, which raises an event and skips the download entirely. One of these is plausibly an "already checked this session / offline" short-circuit — **a zero-network way to advance the flow.** Worth identifying as a fallback if the XML format proves expensive. 4. **`FUT_RS4_BASE_URL`** (@0x1438dbe88; `FutServerCall` builds ` + "ut/game/fifa17/"`, format `"ut/game/%s/"` @0x1438dbe58, game id `"fifa17"` @0x1438dac20). Confirmed live to have **zero** non-image copies — the UTAS layer has no base URL either. This is not the current gate (that layer has not started), but it is the **next** one after `EnterFUT`. Since the store is merged (§1.5), pre-seeding it into `OSDK_CORE` alongside FIX 1(c) is free insurance — the reader sites are 0x146ec98e8 / 0x146f3b2b4. 5. **Which group string FUT passes to `HasEntitlement`.** Recovered literals are `"FIFA16PC"` (formatted from `"FIFA16%s"` @0x143b03708 + `"PC"` @0x1438fe5fc at 0x147bb78b7) and NULL/empty (0x148198340). Sending both group literals is a hedge, not a proof. The script-callable natives (table 0x144311d10..0x144312050; slot 0x144311e80 → `HasEntitlement` thunk 0x146d0f620, slot 0x144311e88 → loaded-flag thunk 0x146d0f630) have data-driven callers not visible in the disassembly. ### 4.4 Closed by this synthesis pass — do not re-investigate * **Config store scoping** — MERGED, via the `'scfg'` → `'_all'` section lookup decoded at 0x147199d90 (§1.5). CFID choice is not load-bearing. * **URL wrapping** — the client-config branch (0x147a77550) does **not** wrap the value; only the ini branch applies `"https://%s"` (@0x143af1840). **Serve an absolute URL.** * **Whether `LoadFUTDatabase` / `LoadFUTSquad` completed** — they must have. Only `CheckFUTRosterUpdateXML` sends `ShowLoadingIcon`, and the spinner is up. * **Whether the flow reaches `EnterFUT` after the advance** — yes, immediately, via `CheckFUTSquadBinFile`'s `"false"` transition (§1.2). No squad bin file means no live-DB popup and no second download.