9.8 KiB
FIFA 17 SBC response reconciliation
Verdict: the live client receives HTTP 200 for GET /ut/game/fifa17/sbs/sets, but the
typed FutSBCLoadCategoryDetailsServerResponse deserializer is not invoked. The evidence
does not identify a server-controlled header, envelope field, or correlation value that
can fix this. The previous 0x180154860 “SBC continuation” diagnosis was based on the wrong
request class and is retracted.
Scope and authority
This pass used only:
- the shipped
CardsDLL_Win64_retail.dllcopied to/tmp/fut/cardsdll.dll; - read-only
/proc/<pid>/memaccess to the running game; - the local OpenFUT request log; and
- existing clean-room notes and scripts in this repository.
No game memory was written, no breakpoint was inserted, and no service or game process was restarted during the measurement.
Fresh live observation
The control run used fresh FIFA process PID 59054. The CardsDLL mapping resolved to
0x6ffffc140000, giving slide 0x6ffe7c140000. Bytes at static control function
0x180180d00 matched the on-disk DLL, proving the mapping/slide before data reads.
At the FUT hub, before opening SBC:
A = *[0x1802e6398] = 0xb78f7c50;M = *(A+0x20a68) = 0;- hub cache byte
*(A+0x1fd70+0x28) = 1(fresh hub response ready); and - SBC cache byte
*(A+0x1f9d8+0x28) = 0.
The user then opened the SBC tile. The real client exchange was:
[10:20:20] GET /ut/game/fifa17/sbs/sets
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
Accept: application/json
Content-Type: application/json
X-UT-SID: OPENFUT-SID-0000000000000001
Accept-Encoding: gzip
-> 200 {"categories":[...]}
The game displayed “There was a problem communicating with the FIFA Ultimate Team servers.”
With that modal still open, the same slide was re-proved and M was still exactly zero.
What M == 0 proves
The typed /sets deserializer is 0x18017b2b0. At 0x18017b309–0x18017b327 it obtains
the FUT root and calls vtable slot +0x9b0, the lazy getter 0x18011b7d0. That getter
allocates and stores A+0x20a68 before the deserializer examines the root object or the
categories key.
Consequently:
- valid JSON would leave
Mnon-null; - malformed or empty JSON reaching this function would also leave
Mnon-null; and M == 0after the completed HTTP transaction means0x18017b2b0was not invoked.
The normal reset of M is 0x180114ee0; its observed use belongs to broad FUT-root
initialization/reset work, not the /sets completion path. There is no evidence that the
deserializer ran and then immediately cleared M during this transaction.
Correct class map
Three classes were conflated in earlier notes:
| Function/class | Proven URI | Role |
|---|---|---|
FutSBCLoadCategoryDetailsServerResponse, request URI builder 0x18017a980, factory 0x18017aa10, response deser 0x18017b2b0 |
/sets under the ut/%s/sbs base |
Initial category/set list; this is the live failing request |
FutSBCSetDataServerResponse, factory 0x18016fca0, deser 0x18016fe90 |
/squadBuildingSets (0x18022bd88) |
Parses reset; not the observed /sbs/sets request |
FutLoadSetTypesServerResponse, deser 0x180154990 |
/challenge/%d/squad (0x1802270e0) |
Parses challengeId, playerRequirements, and squad; later challenge flow |
This corrects two prior claims:
FutSBCSetDataServerResponsedoes not share the literal/setsURI in this binary; its URI string is/squadBuildingSets.0x180154860is not a dedicated completion continuation for the initial category-list request.0x180154830is a generic callback thunk used by multiple request classes, while the nearby0x180154990parser and/challenge/%d/squadURI belong toFutLoadSetTypesServerResponse.
Therefore the earlier hub-versus-0x180154860 comparison contrasted the hub with a later
challenge-squad operation, not with GET /sbs/sets. Its proposed “copy the hub re-arm path”
fix is unsupported for the category-list failure.
What the generic completion code actually checks
The shared request completion routine 0x18016cca0:
- calls request vtable slot
+0x80at0x18016cd32to create the class-selected typed response object; - stores the received status at request offset
+0x48(0x18016cd3d); and - compares it with decimal 200 at
0x18016cdd0.
Exactly 200 takes the success branch to 0x18016d0b9. Non-200 status invokes the error
translation path through request slot +0x60 first. Response construction is selected by
the request vtable; it is not selected by an HTTP response header or a JSON envelope field.
No pre-deserialization branch found in this path reads Content-Type, a request/correlation
ID, the X-UT-SID response header, or a top-level JSON key. The live server already supplies
the one proven transport-level success input: status 200.
Hub comparison
The fresh hub response was consumed successfully and set the hub cache byte to one. After
the subsequent navigation its resting value returned to zero. The SBC cache byte remained
zero. This confirms that cache +0x28 is transient async-result/TTL state; a later resting
zero does not establish which completion branch ran.
The previous report's live snapshot—where both values were zero long after the requests—was therefore insufficient to infer the hub/SBC divergence. The fresh before/after measurement supersedes it.
Server-fixability verdict
Not demonstrated. In particular:
- changing the category JSON cannot make the typed parser start, because the lazy store is allocated before any JSON key is inspected;
- the server already returns the proven success status, 200;
- request-class/response-class selection is client-owned; and
- no header, envelope, or correlation field was found feeding a pre-parser decision.
This does not mathematically prove that no transport variation could ever affect the client. It does prove that the specific server-fix candidates proposed by the killed workflow were speculative and had no reading instruction behind them.
Exact remaining unknown and next measurement
The unresolved boundary is between:
ProtoHttp completion with status 200
-> class-selected response object creation
-> delivery of response bytes/SAX cursor
-> response vtable +0x08 (`0x18017b2b0`)
The next useful experiment is transient tracing of calls—not another resting-state scan. Instrument, in a disposable/local diagnostic build or a non-mutating tracing facility:
- request factory
0x18017aa10; - typed deserializer
0x18017b2b0; - generic completion entry
0x18016cca0and its status at0x18016cdd0; and - the generic response-body/SAX dispatch site that calls response vtable slot
+0x08.
Record whether the factory is called, whether it returns an object with vtable
0x18022e5b0, and whether a body/SAX object is delivered. That separates three remaining
client-side possibilities: wrong request instance despite the URI, typed object created but
body not attached, or body attached but virtual deserialization dispatch skipped.
Until that transient trace exists, the defensible implementation direction remains the
client-side hook described in docs/sbc-hook-dll-spec.md, but its rationale must be stated
as “native category deserializer is not reached,” not the retracted 0x180154860
hub-rearm theory.
2026-08-07 passive-trace result: deserialization is proven
The first gated passive client trace supersedes the final inference above. During exactly one SBC navigation, with every mutation feature disabled, the hook recorded:
SBC_TRACE: factory entry=1 exit=1 tid=652 this=0xb80cd910 result=0x7a99178;
deser entry=1 exit=1 tid=652 this=0x7a99178 reader=0x7fcff7f8 result=true
The matching UTAS request occurred at 11:09:01: GET /ut/game/fifa17/sbs/sets returned
HTTP 200 with one category and two sets. No degraded hook state was reported, and FIFA
remained alive until the operator closed it after the single permitted attempt.
This proves all of the following for the observed request:
- the category response factory is called exactly once and returns a non-null object;
- the native category deserializer is called exactly once on that same object;
- the body reader is non-null;
- deserialization returns success (
true); and - both calls return normally on the same native thread.
Therefore the earlier M == 0 resting snapshot did not prove that 0x18017b2b0 was
skipped. The failure boundary is now strictly after successful native deserialization.
The next measurement must trace the response object's post-deserializer completion,
ownership handoff, and publication into the SBC UI/cache collection. Repeating the factory
or deserializer trace will not add useful information.
2026-08-07 post-deserializer handoff trace
A second one-shot run combined the factory/deserializer probes with atomic replacements of
the category request vtable slots +0x90 (completion callback dispatch) and +0x88
(response ownership transfer). All four calls completed on native thread 656:
request = 0xb80cdfe0
factory response = 0x7c94808
deserializer this = 0x7c94808, result=true
+0x90 callback argument = 0x7c94808
+0x88 owner-slot address = 0xbc51f7e8
The matching GET /ut/game/fifa17/sbs/sets at 11:24:00 returned HTTP 200, and the same
communication modal appeared. Both callback probes reported entry=1 exit=1; no degraded
hook state or process failure occurred.
This proves that the parsed response reaches the category request's completion dispatcher
and that its ownership-transfer routine also returns normally. The remaining failure
boundary begins at the receiving owner object's vtable +0x18 consumer invoked from
0x1801631e0, or later collection/cache/UI validation. Network transport, response
construction, native parsing, callback dispatch, and request-side ownership handoff are no
longer candidate root causes.