# FIFA 17 SBC client-hook implementation plan

## Outcome

Implement an opt-in, fail-closed hook that repairs the native response-to-deserializer
dispatch for `GET /ut/game/fifa17/sbs/sets`. The hook must reuse the genuine response
object and SAX reader from the real HTTP 200 transaction, run synchronously on the native
transaction thread, and preserve the game's allocator, object ownership, callbacks, and
index rebuilds.

This plan supersedes the intervention direction in `plan-2026-08-07-sbc-hook.md` and
`sbc-hook-dll-spec.md` wherever those documents claim the client never issues `/sbs/sets`
or recommend constructing a synthetic reader. The fresh 10:20:20 exchange proves the
request is issued and receives populated JSON. The reconciliation report is authoritative.

## Proven anchors

All addresses are static VAs in `CardsDLL_Win64_retail.dll`, image base `0x180000000`.
Runtime addresses are `CardsDLL base + (static VA - 0x180000000)`.

| Purpose | Address / identity |
|---|---|
| Category request constructor | `0x18017a7c0`, request vtable `0x18022e5c0`, tag `0x753c` |
| `/sets` URI builder | `0x18017a980` |
| Typed response factory | `0x18017aa10`, response vtable `0x18022e5b0` |
| Typed category deserializer | `0x18017b2b0`, `rcx=response`, `rdx=genuine reader` |
| Generic completion | `0x18016cca0`, exact-200 check at `0x18016cdd0` |
| FUT root | `A = *0x1802e6398`, expected vtable `0x18021c2a0` |
| SBC gate cache | `B=A+0x1f9d8`; ready byte `B+0x28` |
| Category store | `M=*(A+0x20a68)`; count `WORD[M+0x50]` |
| Renderer count read | `0x1800b5eda` |

Entering `0x18017b2b0` necessarily invokes the `A+0x20a68` lazy getter before JSON-key
parsing. The fresh transaction left that pointer null, proving that the typed category
deserializer was not entered.

## Architecture decision

Use the existing `openfut-hook` Rust `cdylib` and FIFA 17 feature boundary. Retain its
deferred CardsDLL discovery, RVA calculation, guarded reads, default-off environment
gates, and logging. Replace the stale Tier-1 idea of constructing a reader with this flow:

```text
real /sbs/sets HTTP 200
  -> native generic completion and typed-response factory
  -> observe the real response object and real reader/body cursor
  -> at the proven skipped dispatch boundary, call the original typed method once
  -> native parser populates M and rebuilds its indices
  -> resume the native callback/completion chain
  -> validate M; use native gate state if available
  -> only if necessary, arm B+0x28 while B+0x08 remains zero
```

Do not intercept at the socket layer, fabricate a SAX reader, retain response/reader
pointers beyond their synchronous lifetime, hand-build EASTL category/set records, or
write `B+0x08`/`B+0x20`.

## State and feature gates

Use independent flags; no stronger stage should be implied by a weaker one:

- `OPENFUT_SBC_HOOK=1`: resolve and fingerprint only.
- `OPENFUT_SBC_TRACE=1`: install passive probes and structured logging.
- `OPENFUT_SBC_DISPATCH=1`: enable the one-shot native dispatch repair.
- `OPENFUT_SBC_COMMIT=1`: permit gate/refresh action after validated parse success.
- Keep `OPENFUT_SBC_ARM_ONLY=1` solely as a separate negative-control experiment.

Represent runtime progress with an atomic state machine:

```text
Disabled -> Resolved -> Intercepted -> Parsed -> Validated -> Committed
                                        \-> Failed
```

Add a recursion-depth guard and a transaction one-shot keyed by request/response identity.
Any fingerprint, pointer, status, class, thread, reader, or postcondition mismatch moves to
`Failed` and resumes native execution without a write.

## Milestones

### M0 — reconcile and freeze the baseline

1. Mark the reconciliation report as the address/path authority.
2. Record SHA-256, PE timestamp, `SizeOfImage`, and selected section hashes for the shipped
   CardsDLL, FIFA executable, built hook, and deployed proxy DLL.
3. Preserve a known-good launcher and proxy DLL. Do not overwrite a game-directory DLL
   without an exact backup and hashes.
4. Capture a baseline: FUT hub succeeds, `/sbs/sets` returns 200, SBC shows the modal,
   `M==0`, and the category deserializer is not observed.

Exit: the baseline is repeatable and its artifacts identify one binary build exactly.

### M1 — stabilize DLL loading

The existing `version.dll` injection has one historical successful log, but the current
FIFA 17 launcher disables it after later crashes. Resolve this before SBC detours:

1. Port or implement the complete VERSION proxy export surface and forward every export.
2. Build only `--features fifa17` for `x86_64-pc-windows-gnu` into a staging directory.
3. Inspect PE architecture, exports, and imports with the MinGW binutils.
4. Add a FIFA-17-specific launch path using the existing prefix/UMU configuration and
   explicit `WINEDLLOVERRIDES=version=n,b`.
5. Run three cold launches with every SBC mutation/trace flag disabled.

Exit: all three launches reach the FUT hub, VERSION calls forward correctly, and disabling
the override restores the pre-hook baseline.

### M2 — strengthen runtime resolution

Before any detour or byte write, validate:

- exact CardsDLL identity (`SizeOfImage`, PE metadata, and multiple section/function hashes);
- FNV control bytes at `0x180180d00`;
- expected bytes at every proposed patch site;
- `A` and its expected vtable;
- `B` and its expected vtable;
- readable `M` slot and sane cache fields; and
- that runtime VAs lie inside the expected CardsDLL sections.

Use the external read-only `futmem`/probe tooling as an independent oracle. Never cache an
ASLR slide across launches.

Exit: resolve-only mode passes on two launches with different slides and aborts cleanly on
a deliberately mismatched fingerprint fixture.

### M3 — passive transaction tracing

Instrument, without changing return values or state:

1. generic completion `0x18016cca0`;
2. typed response factory `0x18017aa10`;
3. typed category deserializer `0x18017b2b0`; and
4. once found, the common body/SAX virtual-dispatch callsite.

Log a monotonic timestamp, session/build ID, thread ID, recursion depth, status, request
pointer/vtable, response pointer/vtable, reader/body pointer and vtable, and `M`/`B`
before and after. Correlate a request ordinal with `/tmp/utas.log`; do not log SID/auth
values or full response bodies.

Do not use the existing generic four-register probe wrapper for `0x18016cca0`. That routine
has a fifth stack argument. Use a relocated trampoline or a narrowly verified assembly
stub that preserves the full Win64 ABI: nonvolatile GPRs, XMM6-XMM15 if touched, 32-byte
shadow space, 16-byte call alignment, and all stack arguments. The diagnostic
unhook/call/rehook mechanism is also racy and is not acceptable for the final repair.

Exit: one fresh exchange unambiguously identifies whether the factory is skipped, the typed
object exists without a body/reader, or virtual deserialization dispatch is skipped.

### M4 — reverse the exact dispatch contract

Use M3 captures and static analysis to answer all of these before enabling intervention:

- the exact common body-to-response-deserializer callsite;
- the relationship between response vtable `0x18022e5b0` slot `+0x08` and the older
  message-object vtable `0x18022e598` slot `+0x20`;
- which completion argument or object field owns the genuine reader;
- the reader's valid synchronous lifetime;
- whether `0x1800b8c30` executes after a successful forced parse;
- the native transaction/game thread identity; and
- whether the parser can be reached more than once for one response.

Exit: a written call contract identifies the exact hook site, preserved instructions,
original target, arguments, ownership, thread, and resume address.

### M5 — behavior-preserving detour

Install the production-form detour at the chosen boundary but initially tail-call the
original path unchanged. Prefer a small audited trampoline abstraction over copying the
repository's unhook/rehook diagnostic pattern.

Exit: exactly one balanced entry/exit is recorded per SBC exchange; HTTP traffic, modal,
M/B state, timing, and unrelated FUT screens remain unchanged.

### M6 — guarded dispatch repair

On the native transaction thread and only while the genuine objects are live:

1. require request vtable `0x18022e5c0`, response vtable `0x18022e5b0`, and status 200;
2. require a readable reader pointer/vtable and recursion depth zero;
3. require that this transaction has not already been parsed;
4. call the original typed method `0x18017b2b0(response, reader)` exactly once;
5. capture its return and the resulting M state; and
6. resume the native completion/callback path.

Never run this from the deferred worker or while the SBC controller is iterating. Do not
attempt in-place memory repair after an exception or partial parse; preserve logs and
relaunch FIFA.

Exit: the deserializer is observed once, returns successfully, and native execution
continues without gate or refresh writes.

### M7 — validate and commit UI state

Before exposing populated data, require:

- `M != 0` and a bounded category count;
- category vector `begin <= end <= capacity`;
- `(end-begin) % 0xf0 == 0` and vector length equals `WORD[M+0x50]`;
- sane, unique category/set identifiers and bounded nested counts;
- all native index-rebuild/finalization calls observed; and
- no duplicate parse or partial state.

First allow the native callback to arm the cache. If it does not, the only fallback is
`BYTE[B+0x28]=1` while `B+0x08==0`; never write `B+0x08` or `B+0x20`. Initially require
the user to close/reopen SBC for refresh. Do not synthesize Scaleform events until the
signature and ownership contract of `0x1801a4a70` are independently proven.

Exit: no modal; displayed categories and set counts match the served response.

### M8 — regression, soak, and rollback proof

1. Open/close SBC ten times; enter every set/challenge and return.
2. Verify a second `/sets` response is idempotent and does not duplicate data.
3. Smoke-test hub, club, store, squads, and normal service traffic.
4. Repeat from two fresh launches with different ASLR slides.
5. Soak 30–60 minutes with navigation and, if supported, repeated FUT enter/exit.
6. Disable all SBC flags and confirm the baseline behavior returns without detours/writes.
7. Disable `WINEDLLOVERRIDES`, restore the exact backed-up proxy if needed, and prove hard
   rollback with FIFA closed.

Exit: zero crashes/freezes, stable counts and memory behavior, no unrelated FUT regression,
and both soft and hard rollback are demonstrated.

## Testing and build checks

Run at minimum:

```text
cargo fmt --check
cargo test --features fifa17
cargo check --release --features fifa17 --target x86_64-pc-windows-gnu
cargo build --release --features fifa17 --target x86_64-pc-windows-gnu
```

Extract pure, host-testable helpers for RVA calculation, fingerprint comparison, state
transitions, bounded vector validation, and structured event formatting. Windows calls,
raw pointer reads, and patching should remain behind small interfaces so guard logic can be
tested without launching FIFA.

## Stop conditions

Stop and roll back on any unknown binary fingerprint, patch-byte mismatch, wrong vtable,
wrong thread, unexpected factory/deserializer count, recursion, invalid vector geometry,
missing finalizer, partial parse, crash/freeze, unrelated FUT regression, or save/profile
change. Preserve hook log, UTAS log, binary hashes, and crash evidence before relaunching.

## Definition of done

- The hook is default-off and endpoint/class-specific.
- Exact binary and patch-site fingerprints are verified before intervention.
- The real category deserializer runs exactly once for each intended HTTP 200 response,
  using the genuine response and reader on their native thread.
- `M` passes structural validation and the populated SBC menu supports drill-down.
- No communication modal appears and non-SBC FUT behavior is unchanged.
- Two fresh ASLR-distinct launches and the soak test pass.
- Unsetting flags restores inert behavior; removing the proxy restores the original launch.
