Emulates FIFA 17's full online + Ultimate Team stack against an offline,
clean-room backend (no EA servers). Proven end-to-end 2026-08-01:
Origin login -> Blaze login -> device-trust -> the FUT hub.
Package:
- tools/openfut-fut.sh one-command orchestrator (start/stop/status/restart)
- tools/root_arm.sh idempotent host arm (sysctls, DNAT, /etc/hosts easw)
- tools/{lsx_responder_v2,blaze_responder_v3b,roster_server,utas_server,autopatch}.py
the 5 servers (Origin LSX :4216, Blaze :42127/42130/42131, roster :8081,
FUT/UTAS :8099) + heat2.py (Fire2/Heat2 TDF codec)
- FUT-RUNBOOK.md runbook + gate-ladder troubleshooting
- docs/, tools/login_dump/*.md the reverse-engineering write-ups
All findings are clean-room, from binaries we own; nothing from any leak.
The wire protocol maps 1:1 to FIFA 23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
22 KiB
Component 0x000A / cmd 0x0005 — CensusData::subscribeToCensusDataUpdates
Status: RPC contract fully resolved (reflection + live disasm + live heap read). Fix is a ~15-line
handler in blaze_responder_v3b.py. Not yet run against the game.
Clean-room: every claim below is from decrypted FIFA17.exe / /proc/66672/mem, /tmp/blaze_rx/,
and /tmp/blaze_responder.log. No EA source or headers.
Independently re-verified for this document (live pid 66672, not just quoted from the reports):
component-id arithmetic at 0x147e639cd, vtable 0x143b72330, both TDF class descriptors + member
tables at 0x144c11d70 / 0x144c11dc0, the reply handler 0x147e57050, the resend functor
0x147e57380, the scheduler zero-delay branch 0x146dbae7c, the live API object at 0x43c70ff0,
and the exact wire bytes through heat2.py.
1. THE RPC CONTRACT
1.1 Component 0x000A = Blaze::CensusData ("CensusDataComponent")
The component constructor at 0x147e63970 materialises the id with FIFA17's usual split-immediate
obfuscation, then installs the component vtable:
147e639cd: b9 0f a2 ff e7 mov ecx,0xe7ffa20f
147e639d2: 8d 89 fb 5d 00 18 lea ecx,[rcx+0x18005dfb] ; 0xe7ffa20f+0x18005dfb = 0x1_0000000A
147e639d8: 66 89 48 10 mov WORD PTR [rax+0x10],cx ; componentId = 0x000A
147e639dc: 48 8d 0d 4d e9 d0 fb lea rcx,[rip+...] ; # 0x143b72330 (component vtable)
Live read of vtable 0x143b72330 and its strings:
| slot | VA | returns |
|---|---|---|
| +0x08 | 0x147e641d0 |
getComponentName → "CensusDataComponent" @0x143b72368 |
| +0x10 | 0x147e64170 |
getCommandName |
| +0x18 | 0x147e645e0 |
getNotificationName |
| +0x20 | 0x147e64250 |
getErrorName |
| +0x30 | 0x147e64710 |
notification dispatcher |
Allocation tag "CENSUSDATAInstance" @0x143b72380. Cross-check from getErrorName @0x147e64250:
cmp edx,0x1000a → CENSUSDATA_ERR_PLAYER_ALREADY_SUBSCRIBED, cmp edx,0x2000a →
CENSUSDATA_ERR_PLAYER_NOT_SUBSCRIBED. Blaze error = (index<<16)|componentId ⇒ componentId 0x000A.
Not an assumption from framework lore — read out of FIFA's own registry.
1.2 Command 0x0005 = subscribeToCensusDataUpdates
getCommandName @0x147e64170 is a movzx ecx,dx + dec/je chain. Full CensusData command table:
| cmd | name |
|---|---|
| 0x0001 | subscribeToCensusData (0x143b723c0) |
| 0x0002 | unsubscribeFromCensusData (0x143b723f8) |
| 0x0003 | getRegionCounts (0x143b723b0) |
| 0x0004 | getLatestCensusData (0x143b72398) |
| 0x0005 | subscribeToCensusDataUpdates (0x143b723d8) |
Notification id 0x0001 = NotifyServerCensusData (0x143b72228, from getNotificationName
@0x147e645e0). FIFA never sends cmd 1–4; it goes straight to cmd 5.
1.3 Request TDF — Blaze::CensusData::SubscribeToCensusDataUpdatesRequest
Class descriptor 0x144c11d70, tdfId 0x2975a3d9, member table 0x144c11bc0, 1 member (live dump):
| member | tag (dword) | wire tag | off | decl type |
|---|---|---|---|---|
resubscribe |
0xcb3d6200 |
cb 3d 62 = RSUB |
+0x10 | 0x0f = bool |
Wire (/tmp/blaze_rx/rx_0017_000a_0005.bin, 21 B = 16 hdr + 5 payload):
cb 3d 62 | 00 | 00 → tag RSUB, Heat2 wire type 0x00 (INT), varint 0.
Every later capture: cb 3d 62 00 **01** → RSUB=1.
Note the wire type byte carries the generic Heat2 scalar type, not the declared C++ type — a
declared bool goes out as Heat2 INT. Same rule applies to our reply's TimeValue members.
1.4 Response TDF — Blaze::CensusData::SubscribeToCensusDataUpdatesResponse
Class descriptor 0x144c11dc0 (header qword 0b000000 a3286174 ⇒ tdfId 0x746128a3), member
table 0x144c11bf0, count 3. Live dump, including the reflection default column:
| member | tag (dword) | wire tag | off | type | default |
|---|---|---|---|---|---|
censusNotificationPeriod |
0x8eec0000 |
8e ec 00 = CNP |
+0x10 | 0x0e TimeValue |
0 |
notificationTimeout |
0xbb4b7400 |
bb 4b 74 = NTMT |
+0x18 | 0x0e TimeValue |
0 |
resubscribeTimeout |
0xcb4b7400 |
cb 4b 74 = RTMT |
+0x20 | 0x0e TimeValue |
0 |
That is the entire response: three int64 time values. No status flag, no subscription id, no list, no count. This categorically rules out "poll until a ready flag appears" — there is nothing in the reply that could carry readiness.
TimeValue is microseconds: TimeValue::parse @0x1479b2d50 builds its internal value as
days*365 then *60 *60 *1000 *1000 (imul rcx,rax,0x16d / imul rdx,rcx,0x3c /
imul rcx,rdx,0x3c / imul rdx,rcx,0x3e8 / imul rax,rdx,0x3e8) = seconds·1e6. Wire cross-check:
Blaze::QosConfigInfo.timeout ('TIME') uses the same typedesc 0x14486d7b8 and its reflection
default is 0x004C4B40 = 5,000,000 — and we already send ("TIME", (INT, 5000000)) in qos_config()
and FIFA accepted it. So TimeValue on the wire = Heat2 INT varint of microseconds.
1.5 Why the empty reply hangs FIFA — the exact mechanism
Our 16-byte empty REPLY is not rejected and is not a decode error. It is a successful
zero-member struct, so the response object is left default-constructed with CNP = NTMT = RTMT = 0.
The reply callback is 0x147e57050. Verified disassembly (live):
147e57055: f7 81 68 01 00 00 ff ff ff f7 test DWORD PTR [rcx+0x168],0xf7ffffff ; job already armed?
147e57062: 0f 85 9f 00 00 00 jne 0x147e57107 ; ...then bail
147e57075: 45 85 c0 test r8d,r8d ; r8d = BlazeError
147e57078: 75 2e jne 0x147e570a8 ; error path
; --- success path (err == 0) ---
147e5707a: 4c 8b 42 18 mov r8,QWORD PTR [rdx+0x18] ; NTMT
147e5707e: 48 8b 42 20 mov rax,QWORD PTR [rdx+0x20] ; RTMT
147e57082: 4c 03 42 10 add r8,QWORD PTR [rdx+0x10] ; += CNP
147e57086: 48 89 81 60 01 00 00 mov QWORD PTR [rcx+0x160],rax ; stash RTMT for the error path
147e5708d: 48 8d 05 ec 02 00 00 lea rax,[rip+0x2ec] ; # 0x147e57380 = resend functor
147e57099: 48 b8 cf f7 53 e3 a5 9b c4 20 movabs rax,0x20c49ba5e353f7cf ; magic /1000
147e570a3: 49 f7 e8 imul r8
; --- error path (err != 0) ---
147e570be: 48 f7 a9 60 01 00 00 imul QWORD PTR [rcx+0x160] ; saved RTMT /1000
; --- common tail ---
147e570ce: 48 c1 fa 07 sar rdx,0x7 ; => delay in MILLISECONDS
147e570ec: 89 54 24 28 mov DWORD PTR [rsp+0x28],edx ; delay arg (32-bit ms)
147e570fa: e8 e1 ee ff ff call 0x147e55fe0 ; alloc FunctorJob + schedule
147e570ff: 8b 08 mov ecx,DWORD PTR [rax]
147e57101: 89 8b 68 01 00 00 mov DWORD PTR [rbx+0x168],ecx ; store armed job id
So: delay_ms = (CNP + NTMT) / 1000 on success; RTMT / 1000 only on the error path.
With an empty reply that is (0 + 0)/1000 = 0.
A zero delay is special-cased into the immediate/ready job list rather than the timed heap —
scheduler 0x146dbae10:
146dbae78: 8b 74 24 58 mov esi,DWORD PTR [rsp+0x58] ; delay ms
146dbae7c: 85 f6 test esi,esi
146dbae7e: 75 14 jne 0x146dbae94 ; nonzero -> now + delay on the timed heap
146dbae80: 49 8d 48 08 lea rcx,[r8+0x8]
146dbae84: 48 8d 57 18 lea rdx,[rdi+0x18] ; zero -> ready list
146dbae88: 40 38 77 48 cmp BYTE PTR [rdi+0x48],sil ; (mid-pump? -> next-idle list [rdi+0x38])
146dbae8e: 48 8d 57 38 lea rdx,[rdi+0x38]
The job that fires is 0x147e57380, which clears the job id, constructs a fresh request and sets
RSUB = 1 before re-sending through the cmd-5 proxy:
147e5739e: 89 b9 68 01 00 00 mov DWORD PTR [rcx+0x168],edi ; clear armed job id
147e573bf: e8 fc bf 00 00 call 0x147e633c0 ; request ctor
147e573c5: c6 84 24 80 00 00 00 01 mov BYTE PTR [rsp+0x80],0x1 ; request+0x10 = RSUB = 1
147e573cd: 48 8d 05 7c fc ff ff lea rax,[rip-0x384] ; # 0x147e57050 = reply cb
Conclusion: this is a self-re-arming subscription-refresh timer with a zero interval, not an error-retry storm. Three independent confirmations:
- Wire evidence — payload histogram over
/tmp/blaze_rx/rx_*_000a_0005.bin:cb3d620000→ 1 (the single initial subscribe),cb3d620001→ all the rest. Only0x147e57380writesRSUB=1, so the timer functor is provably the sole driver. - Cadence —
/tmp/blaze_responder.logper-second RX histogram is a flat 30/s from[17:36:53]to[20:01:15](2 h 24 m and counting; min 25, max 32). That is the game idle-tick rate, i.e. exactly one re-send per BlazeHub pump — mechanically impossible for a socket-speed error retry, and no backoff, no teardown. - Live state — API object
0x43c70ff0(vtable0x143b6d6b0, hub+0x08=0x43c47330, component proxy+0x20=0x079f25c0) read out of/proc/66672/memright now:+0x160(saved RTMT) = 0,+0x168(armed job id) = 0. And a gdb break on0x147e57050showederr = 0x0withresp+0x10 = resp+0x18 = resp+0x20 = 0— the reply is accepted as SUCCESS.
The fix is therefore: make CNP + NTMT ≥ 1000 µs so the job goes on the timed heap.
2. THE FIX
File: /home/alex/Documents/OpenFUT/fifa17-recon/tools/blaze_responder_v3b.py
2.1 Constants — after COMP_USERSESSIONS (line ~201)
COMP_CENSUSDATA = 0x000A # id built at 0x147e639cd-0x147e639d8;
# "CensusDataComponent" @0x143b72368
# ---- CensusData (0x000A) command table, from getCommandName @0x147e64170.
CMD_SUBSCRIBETOCENSUSDATAUPDATES = 0x0005
CENSUSDATA_CMDS = {
0x01: "subscribeToCensusData", 0x02: "unsubscribeFromCensusData",
0x03: "getRegionCounts", 0x04: "getLatestCensusData",
0x05: "subscribeToCensusDataUpdates",
}
NOTIFY_SERVER_CENSUS_DATA = 0x0001 # getNotificationName @0x147e645e0
2.2 Logging names — COMP_NAMES (line 277) and rpc_name() (line 285)
COMP_NAMES = {
...
COMP_CENSUSDATA: "CensusData",
0x000B: "Clubs", # id at 0x147e38b5e/0x147e38b71
0x081C: "SponsoredEvents", # cmd table 0x146f89130
0x08C9: "OSDKSettings", # cmd table 0x14725f9a0
}
and inside rpc_name(), next to the existing COMP_UTIL / COMP_AUTH arms:
elif component == COMP_CENSUSDATA:
cmd = CENSUSDATA_CMDS.get(command, "cmd:0x%04x" % command)
(Log cosmetics only — no behaviour change, but it makes the success signal in §3 readable.)
2.3 Response builder — next to qos_config() / preauth_response_fields()
def subscribe_census_data_updates_response_fields() -> "OrderedDict":
"""Blaze::CensusData::SubscribeToCensusDataUpdatesResponse
classinfo @0x144c11dc0, tdfId 0x746128a3, member table @0x144c11bf0, 3 members,
ALL TDF type 0x0e (TimeValue) -> Heat2 INT varint of MICROSECONDS.
The client's reply callback 0x147e57050 computes
delay_ms = (CNP + NTMT) / 1000 (magic-divide 0x20C49BA5E353F7CF, sar 7)
and arms a FunctorJob (0x147e57380) that re-sends this RPC with RSUB=1.
All three members default to 0 in reflection, so our old EMPTY reply gave
delay_ms = 0, which 0x146dbae7c pushes onto the *ready* job list -> one
re-subscribe per idle tick = the observed 30/s storm.
30s + 90s => the client re-subscribes every 120s instead of every 33ms.
Tags ascend (8eec00 < bb4b74 < cb4b74), which is also correct Heat2 order."""
return OrderedDict([
("CNP", (INT, 30 * 1000000)), # censusNotificationPeriod
("NTMT", (INT, 90 * 1000000)), # notificationTimeout
("RTMT", (INT, 300 * 1000000)), # resubscribeTimeout (error path only)
])
"CNP" encodes to 8e ec 00 — identical to the 4-char "CNP " — verified via
heat2.encode_tag, so the 3-char spelling is safe.
2.4 Dispatch arm — in dispatch(), before the REPLY_EMPTY_TO_UNKNOWN fallback (~line 1166)
# ----------------------------------------------------------- CensusData
if comp == COMP_CENSUSDATA and cmd == CMD_SUBSCRIBETOCENSUSDATAUPDATES:
rsub = find_nested_int(fields, "RSUB") if fields is not None else None
log(" -> subscribeToCensusDataUpdates(RSUB=%s): "
"SubscribeToCensusDataUpdatesResponse{CNP=30s, NTMT=90s, RTMT=300s} "
"-> client re-subscribes in (CNP+NTMT)/1000 = 120000 ms" % rsub)
return [reply_to(hdr, encode_tdf(
subscribe_census_data_updates_response_fields()))]
2.5 Exact bytes on the wire (round-tripped through this repo's heat2.py + reply_to)
Payload — 25 bytes:
8e ec 00 00 80 8e ce 1c CNP tag 8eec00, type 00 (INT), varint 30000000
bb 4b 74 00 80 aa ea 55 NTMT tag bb4b74, type 00 (INT), varint 90000000
cb 4b 74 00 80 8c 8d 9e 02 RTMT tag cb4b74, type 00 (INT), varint 300000000
Full 41-byte REPLY frame (shown for msgNum 0x10, the first census request):
00 00 00 19 00 00 00 0a 00 05 00 00 10 20 00 00
8e ec 00 00 80 8e ce 1c bb 4b 74 00 80 aa ea 55
cb 4b 74 00 80 8c 8d 9e 02
(size 0x19 = 25, component 0x000a, command 0x0005, byte[13] 0x20 = REPLY + userIndex 0 —
produced by the existing reply_to(hdr, payload), no header changes needed.)
2.6 CIDS / component list — do NOT change
COMPONENT_IDS (line 528) feeds only CIDS in preauth_response_fields, and dispatch() never
reads it. CIDS demonstrably does not gate anything: FIFA already sends four components that are
absent from our advertised CIDS [1,4,5,7,9,15,25,28,30722] — 0x000A, 0x000B, 0x081C,
0x08C9. Adding 0x000A would change nothing we can observe, and could only add risk (FIFA might
instantiate more components and issue new unhandled RPCs), which would break the one-variable
discipline that cracked every previous gate. Leave it alone for this experiment.
2.7 Adjacent components 0x000B / 0x08C9 / 0x081C — names only, no handlers
These are each called exactly once, get a 16-byte empty REPLY, and are never retried — so
none of them is currently a hang, and none has a reversed response schema yet. Adding invented reply
bodies would inject a second variable into the experiment. Add the COMP_NAMES entries from §2.2
(pure logging) and stop there. For reference when one of them becomes the next gate — all names read
from FIFA's own command tables:
| comp | cmd | name | cmd-table VA |
|---|---|---|---|
| 0x000B Clubs | 0x0A28 | getClubsComponentSettings |
0x147e40b00 (0x143b69098) |
| 0x000B Clubs | 0x0640 | getInvitations |
0x147e40b00 (0x143b690f0) |
| 0x08C9 OSDKSettings | 0x0001 | fetchSettings |
0x14725f9a0 |
| 0x08C9 OSDKSettings | 0x0002 | fetchSettingsGroups |
0x14725f9a0 |
| 0x081C SponsoredEvents | 0x0003 | getEventsURL |
0x146f89130 |
| 0x0007 Stats | 0x0003 / 0x000F / 0x0014 | getStatGroupList / getKeyScopesMap / getPeriodIds |
0x147e29040 |
| 0x0009 Util | 0x000C | userSettingsLoadAll |
0x146df6dc0 |
| 0x000F Messaging | 0x0002 | fetchMessages (×3) |
0x147e49780 |
2.8 Optional step 2 — only if a residual ~120 s beat is not enough
A real server pushes census content every CNP; we never will, so FIFA will keep re-subscribing
every 120 s. That is the correct steady state, not a bug. If FUT turns out to need actual content:
notification(COMP_CENSUSDATA, NOTIFY_SERVER_CENSUS_DATA, encode_tdf(OrderedDict([
("CNP", (INT, 30 * 1000000)),
("NTMT", (INT, 90 * 1000000)),
("RTMT", (INT, 300 * 1000000)),
("TDFL", (LIST, (STRUCT, []))), # censusDataList -- EMPTY
])))
Blaze::CensusData::NotifyServerCensusData, tdfId 0x5b08e70d, classinfo 0x144c11cd0, member
table 0x144c11ad0, 4 members: TDFL @+0x10 (0xd249ac00, list of NotifyServerCensusDataItem),
CNP @+0x58, NTMT @+0x60, RTMT @+0x68. Keep TDFL empty — its single element member tdf
(0xd2498000, typedesc 0x14486d860) is TDF type 0x07 = variable TDF, a wire format heat2.py
does not implement. Do not attempt it until the variable-TDF encoding is reversed.
3. OBSERVABLE SUCCESS SIGNAL
Primary (log, immediate): the 30/s storm collapses.
grep -oE '^\[[0-9:]+\] RX #[0-9]+ CensusData' /tmp/blaze_responder.log \
| grep -oE '^\[[0-9:]+\]' | uniq -c | tail
Per-second count must go from a flat 30 to 0, with at most one
CensusData::subscribeToCensusDataUpdates roughly every 120 s (RSUB=1). Anything in between
(e.g. a 1/s or 10/s beat) means the reply decoded but with different values than intended — see §4.
Secondary (the real prize): a brand-new RPC on a component other than 0x000A, at a msgNum
higher than the last census msgNum. Right now, from RX #43 onward for 2 h 24 m, the socket carries
nothing but census — not even Util::ping. Any new line here is forward progress:
tail -f /tmp/blaze_responder.log | grep -P 'RX #\d+ ' | grep -v CensusData
Expected candidates: GameManager (0x0004) (never contacted at all so far),
Util::fetchQosConfig (9/0x15), Stats, GameReporting (0x001C), or FUT/UTAS HTTP traffic.
Direct in-process confirmation (proves the fields landed, not just that the spam stopped):
gdb -p 66672
hbreak *0x147e57050
# at the hit:
p/d *(long long*)($rdx+0x10) # CNP -> must be 30000000, was 0
p/d *(long long*)($rdx+0x18) # NTMT -> must be 90000000, was 0
p/d *(long long*)($rdx+0x20) # RTMT -> must be 300000000, was 0
p/x $r8d # BlazeError -> 0
Or without gdb, straight from the live heap (the API object we already located):
python3 -c "
import struct;f=open('/proc/66672/mem','rb',0);f.seek(0x43c70ff0+0x160);b=f.read(12)
print('saved RTMT =',struct.unpack('<Q',b[:8])[0],' armed jobId = 0x%x'%struct.unpack('<I',b[8:])[0])"
Today this prints saved RTMT = 0 armed jobId = 0x0. After the fix it must print
saved RTMT = 300000000 with a stable non-zero jobId persisting between sends (the job now sits
on the timed heap instead of being consumed every idle).
UI signal: the "connecting to FUT" spinner advances / the FUT hub loads.
4. WHAT STILL NEEDS A LIVE EXPERIMENT
-
Does FUT actually gate on census at all? The RPC already returns
err = 0and the response carries no readiness flag, so the fix provably ends the storm but is not proven to unblock FUT. It may simply reveal the next gate. Watch §3's secondary signal: if the storm stops and nothing new is ever sent, the FUT gate is elsewhere and this was noise removal only. Corollary risk: if nothing new appears, also check whetherUtil::pingresumes — it has been sent exactly once (pre-login) in 2 h 24 m, which could mean the census loop was starving the keepalive scheduler, or could just be SDK ping suppression while traffic flows. A permanently missing keepalive is a latent disconnect. -
TimeValue unit sanity at the top end. Microseconds is well-evidenced (
TimeValue::parse*1e6, the/1000→ms divide, and QosConfigInfo's baked default 5,000,000). But the delay is passed to the scheduler as a 32-bit ms int (mov DWORD PTR [rsp+0x28],edx) and no clamp or validation was found in either0x147e57050or0x146dbae10.(CNP+NTMT)= 120e6 µs → 120,000 ms is far inside range, but do not scale these values up by orders of magnitude without re-checking. If FIFA instead re-subscribes on a far different period than 120 s, the observed interval directly reveals the real unit. -
Does the client also watchdog on
NTMT(no notification received)? If a resubscribe beat appears at ~90 s rather than ~120 s, the client is timing out on missing census pushes and we need §2.8'sNotifyServerCensusData. That in turn is blocked on reversing the variable-TDF (type 0x07) wire format for a non-emptyTDFL— an emptyTDFLlist is untested on this client. -
Whether FUT wants census content, not just the ack. There is a whole FIFA-side layer above Blaze CensusData —
FifaOnline::CensusDataHandler@0x1438fb278,OSDK_CensusDataAdaptor@0x143993768,FifaOnline::GetCensusDataRequest/Response@0x1438f7908/0x1438f7930, eventEVENT_CENSUSDATA_UPDATE@0x143984650, plus per-domain payload classes (GameManagerCensusData0x1438bc618,ClubsCensusData0x143b689a0,UserManagerCensusData0x14388c4a0). If any of that blocks on a populated list, the ack alone will not be enough. -
Whether cmds 0x0003 / 0x0004 become the next gate. FIFA has never sent
subscribeToCensusData(cmd 1) — it goes straight to cmd 5. Once cmd 5 succeeds it may follow up withgetRegionCounts(responseBlaze::CensusData::RegionCounts, tdfId0x610ec2da, 1 memberCNOUmap<uint32_t,string>numOfUsersByRegion, classinfo0x144c11d20) orgetLatestCensusData. RegionCounts is cheap to stub if it appears;getLatestCensusDatais not (variable TDFs again). -
Which of the nine empty-replied RPCs bites next is not determinable from this log — none was retried even once, so there is no retry signature to read. A-priori favourites are
OSDKSettings::fetchSettings/fetchSettingsGroups(an OSDK-layer gate FIFA calls exactly twice, matching that component's entire 2-command surface) andClubs::getClubsComponentSettings. Three non-empty but stubbed replies are also live risks once census unblocks:AssociationLists::getListsreturnsLMAP:[]though FIFA explicitly asked forOSDKPreferredPlayerListandOSDKAvoidPlayerList;Util::fetchClientConfigreturns an empty map forOSDK_TICKER/OSDK_ARENA/OSDK_ROSTER;Util::userSettingsLoadreturns emptyDATAforFirstTimeFlagandAchievementCache. -
Restart discipline. The fix changes only the reply to an RPC FIFA re-sends every idle tick, so in principle restarting the responder mid-session is enough — the very next census request would get the new reply. Whether the live client tolerates the dropped TCP connection is untested; a full relaunch is the safe path, and it also gives a clean log to measure the cadence against.