# 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.
