# openfut-utas-host The first live FIFA 17 **UTAS migration host**. It fronts the client-visible UTAS port and migrates one route at a time to OpenFUT Core, proxying everything else to the Python UTAS oracle so the rest of FUT keeps working unchanged. ``` FIFA 17 ──HTTP──▶ openfut-utas-host ├── GET …/club ──▶ FIFA17 adapter ──▶ OpenFUT Core (/collection) └── everything else ──▶ Python UTAS oracle (verbatim reverse proxy) ``` ## What it owns / does not own Owns: socket + HTTP/1.1 keep-alive transport, route classification, the Core access client, the Python passthrough, and diagnostics. It owns **no** game domain state — filtering/pagination is Core's; wire parsing/shaping is the adapter's. The adapter never learns how Core is reached (the architecture rule): the host holds the [`CoreAccess`] boundary (`GET {core_url}/collection?…` today). ## Safety model - **Classification happens once, before execution.** Exact `GET /ut/game//club` → Rust; everything else → Python. No shared path, no "try Rust then Python". - A Core failure on `/club` degrades to a valid empty `{"itemData":[]}` and logs an error — it never falls back to Python (which could double-apply a mutation on other routes). `/club` is read-only, but the rule is absolute. - Mutating routes (PUT/POST, `/squad`, `/purchased`, quick-sell, market, auth, SBC, `/club/stats/*`, `/clubUser`) all classify to passthrough and are untouched. ## Configuration (env) | Var | Required | Default | Meaning | |---|---|---|---| | `OPENFUT_UTAS_HOST_ADDR` | yes | — | where this host listens (client-visible UTAS addr) | | `OPENFUT_UTAS_PYTHON_URL` | yes | — | Python UTAS oracle base URL for fallback (must differ from this host) | | `OPENFUT_FIFA17_CATALOG` | yes | — | FIFA 17 card-definition identity catalog (`Fifa17CardCatalog` JSON: card id → asset id) | | `OPENFUT_IDENTITY_STORE` | yes | — | persistent external-identity store file (owned instance → stable wire id) | | `OPENFUT_PERSONA_ID` | yes | — | FIFA persona id stamped on `GET /squad/active` (must match the persona LSX/Blaze/POW/UTAS agree on) | | `OPENFUT_CORE_URL` | no | `http://127.0.0.1:8080` | OpenFUT Core base | | `OPENFUT_FIFA17_TABLES_DIR` | no | `fifa17-recon/data/tables` | `leagues/nations/teams.json` for id⇄name | Startup **fails clearly** if the catalog or identity store cannot be loaded — there is no placeholder fallback (exactly one production identity path). ## Identity model (resolved) FIFA renders an owned card by resolving `resourceId & 0xffffff` against the client's **own local players table**; an invented id renders a **blank generic card** (proven live — `fut_cards.py:11-21`). Two distinct identities, never conflated, are resolved by [`Fifa17IdentityResolver`] (the single production path): - **Definition identity** (`resourceId`/`assetId`) — the card's real FIFA asset id, from the versioned `OPENFUT_FIFA17_CATALOG`. An unmapped definition is **dropped and counted**, never faked. - **Instance identity** (`id`) — a stable, persistent, reversible wire integer from the generic `openfut-identity` store under the FIFA 17 wire-id policy (monotonic from `100_000_001`). The same owned instance keeps its id across restart and reverses exactly; two copies of one definition share a `resourceId` but get distinct `id`s. The namespace is globally monotonic within `(fifa17, owned-item)` — no per-account column is needed because Core owned-instance ids are globally-unique UUIDs. **Remaining prerequisite for a *rendering* retail `/club`:** Core inventory must reference cards that exist in the catalog. The catalog + store + resolver are built and tested; wiring a controlled real FIFA 17 dev-content inventory (the curated per-game dev pack) is the next slice. `rare=SP` ("Special") stays UNSUPPORTED (semantics unproven; parsed, reported, never guessed). ## Retail A/B runbook (first `/club` gate) Change **only** the UTAS routing layer; keep the validated Rust Redirector/Roster and the current Blaze path. Python remains the rollback oracle — do not modify it. Preconditions (mirror the proven blaze/roster switch discipline): 1. `cargo test -p openfut-utas-host -p openfut-adapter-fifa17` green; `clippy -D warnings` clean; `fmt --check` clean. 2. Built binary identity == HEAD (`scripts/verify-build-identity.sh`); no dirty tree. 3. Python UTAS directly reachable; the Rust host directly probeable; no stale NAT/switch rules; FIFA fully closed. Bring-up: 1. Move Python UTAS to an alternate port (`FUT_PORT=8199` in the container/`openfut-fut.sh`); it keeps serving there. 2. Start this host on the client-visible UTAS addr: `OPENFUT_UTAS_HOST_ADDR=<lan>:8099 OPENFUT_UTAS_PYTHON_URL=http://127.0.0.1:8199 OPENFUT_CORE_URL=http://127.0.0.1:8080 OPENFUT_FIFA17_CATALOG=<catalog.json> OPENFUT_IDENTITY_STORE=<store.json> OPENFUT_PERSONA_ID=33068179 openfut-utas-host` 3. Launch FIFA → FUT → **My Squad** player picker and exercise: no-filter, position, nation, league, league+team, Gold+position, then scroll beyond page one. Evidence to capture (all six): - **Switch**: client traffic hits the Rust host. - **Rust positive**: host log `owner=RUST route=club …` for the client IP. - **Python negative for /club**: Python logs no `/club` request in the window. - **Python positive for other UTAS**: unimplemented routes still reach Python. - **Core positive**: Core logs the `/collection` query and returns the expected set. - **Application + pagination**: the UI shows filtered results; later pages differ from page one (no repeated-first-page amplification). Rollback: point the UTAS addr back at Python directly; confirm FUT still usable; then re-enable the host and confirm `/club` again (proves reversibility). Logs are safe by construction: no auth/session/device/token material — only owner, route, filter summary, counts, status.