cf961603fe
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.
blaze/ids.rs component/command/notification tables
blaze/config.rs injectable identity + endpoints, nothing hardcoded
blaze/session.rs per-connection state
blaze/client_config.rs the fetchClientConfig tables
blaze/responses.rs 16 Blaze::* response bodies
blaze/dispatch.rs (component, command) -> Vec<Frame>
Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.
98 tests green across both crates; clippy clean.
MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.
The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.
Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.
Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.
Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
5.2 KiB
Markdown
116 lines
5.2 KiB
Markdown
# 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_<CALL>`) 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.
|