212 lines
9.8 KiB
Markdown
212 lines
9.8 KiB
Markdown
# 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.dll` copied to `/tmp/fut/cardsdll.dll`;
|
||
- read-only `/proc/<pid>/mem` access 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:
|
||
|
||
```text
|
||
[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 `M` non-null;
|
||
- malformed or empty JSON reaching this function would also leave `M` non-null; and
|
||
- `M == 0` after the completed HTTP transaction means `0x18017b2b0` was 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:
|
||
|
||
1. `FutSBCSetDataServerResponse` does **not** share the literal `/sets` URI in this binary;
|
||
its URI string is `/squadBuildingSets`.
|
||
2. `0x180154860` is not a dedicated completion continuation for the initial category-list
|
||
request. `0x180154830` is a generic callback thunk used by multiple request classes, while
|
||
the nearby `0x180154990` parser and `/challenge/%d/squad` URI belong to
|
||
`FutLoadSetTypesServerResponse`.
|
||
|
||
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`:
|
||
|
||
1. calls request vtable slot `+0x80` at `0x18016cd32` to create the class-selected typed
|
||
response object;
|
||
2. stores the received status at request offset `+0x48` (`0x18016cd3d`); and
|
||
3. 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:
|
||
|
||
```text
|
||
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 `0x18016cca0` and its status at `0x18016cdd0`; 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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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.
|