# openfut-adapter-fifa17 The FIFA 17 game adapter. Everything true of *FIFA 17 specifically* lives here, so that neither OpenFUT Core nor the generic protocol crates have to know about it. ``` openfut-protocol-blaze generic Blaze: Fire2 framing, Heat2/TDF codec ▲ openfut-adapter-fifa17 THIS: command tables, response bodies, dispatch order ▲ OpenFUT Core game-independent FUT domain (not yet wired) ``` ## Status | Surface | Port | State | |---|---|---| | **Blaze / Fire2 RPC** | 42130 | **Implemented**, byte-for-byte parity-tested | | Redirector (HTTPS + XML) | 42127 | Python only | | Nucleus OAuth stub | 42131 | Python only | | LSX / Origin | 4216 | Python only | | Roster XML | 8081 | Python only | | UTAS / RS4 | 8099 | Python only | | POW / EASFC | 8094 / 8080 | Python only | **Nothing here is wired into the running backend.** The crate answers frames; it opens no socket, terminates no TLS and owns no runtime. The Python backend remains the live service and the behavioural oracle. ## What the adapter owns, and what it must not Owns: component/command/notification IDs, response body shapes, dispatch ordering, session identity, the `fetchClientConfig` tables. Must not own: FUT domain state. Blaze is an auth/session/config protocol — no coins, packs, clubs or squads appear on this wire — so `Session` holds a session key, a locale, a service name, an auth code and a flag, and that is all. When UTAS is migrated that boundary will need active defending; here it comes free. ## Parity ```bash ./check-parity.sh # oracle freshness + byte-for-byte replay ./check-parity.sh --regen # after an intentional oracle change ``` `fixtures/blaze_transactions.jsonl` holds 49 request→response(s) transactions produced by calling the real `blaze_responder_v3b.dispatch()`. They replay in order against a shared session per connection, so ordering-dependent behaviour is exercised rather than assumed: preAuth captures the locale that later `ALOC` fields echo, and login sets the auth code `getAuthToken` returns afterwards. Comparison is byte-for-byte including frame count and order — a missing post-login notification or a reply where the oracle stays silent fails here. The suite was **mutation-tested**: swapping two post-login notifications, flipping one enum deep inside `AccountInfo`, and hardcoding an address in `utas_base()`/`nucleus_base()` were each verified to turn it red. The third initially did *not*, because the config templating had made those helpers dead code; the table now templates on URL-level tokens so they are the single place a URL shape is defined. ## Three behaviours that are easy to get wrong * **Login answers with four frames, in order**: reply, then `UserAuthenticated`, `UserSessionExtendedDataUpdate`, `UserAdded`. * **An unimplemented RPC still gets an empty reply.** Silence makes the client wait for a timeout; an empty reply lets every field fall back to a client-side default and the boot continues. * **Non-request message types get nothing at all.** No error replies are emitted. `msgType` 3 exists, but the error-code placement is UNRESOLVED — three clean-room sources disagree between `header[14:16]`, a metadata `ERRC`, and a payload `CNTX`/`ERRC` — so emitting one would be a guess on the wire. ## The client config table `fixtures/client_config.json` carries 227–243 rows per CFID, generated from the Python oracle and templated on `{utas_base}`, `{nucleus_base}`, `{pow_content_url}`, `{advertise}`, `{bind}`, `{pow_host}`. It is reverse-engineered *data*, not logic, and deriving it mechanically removes a class of transcription typo no reviewer could catch. The generator does not take its own templating on trust: it substitutes real addresses back in and diffs against the oracle for every section before writing the file. The table must be *complete*, not representative. The client resolves a per-call key (`FUT_RS4_URL_`) before a per-module one, and any unresolved call falls back to a real, dead EA host — that is what produced "there has been an error connecting to FIFA 17 Ultimate Team" mid-session when only the boot subset was served. ## Known defect reproduced deliberately `nucleusConnect` and `nucleusConnectTrusted` are built from the **bind** address, not the advertised one. On the live split deployment that means the backend tells a client on another machine to reach Nucleus at `http://0.0.0.0:42131`, which it cannot. Verified against the running container, not inferred. This is reproduced exactly, because it is what the only proven-working configuration does and changing it would break parity. It also implies the Nucleus stub is not actually reached in the current remote flow. Fixing it is a separate change that needs live validation — see the vault. ## Configuration Nothing is hardcoded. `AdapterConfig` carries `Identity` (persona, ids, email, namespace, entitlement group, …) and `Endpoints` (advertise, bind, POW hosts, telemetry/ticker/QoS ports). `Default` gives the project's synthetic offline identity on loopback; a remote deployment must override `advertise`. Bind and advertise are deliberately distinct: an advertised URL must carry the address the *client* can reach, which on a two-machine deployment is not the address the server binds.