openfut-utas-host: the first live UTAS host. Serves GET /ut/game/<title>/club from OpenFUT Core via the FIFA17 adapter and reverse-proxies every other UTAS route verbatim to the Python oracle. Plaintext HTTP/1.1 keep-alive (no TLS); route classification before execution; a Core error on /club degrades to an empty page and never falls back to Python. CoreAccess is a host-owned boundary (the adapter stays transport-agnostic). openfut-adapter-fifa17::fut: owned_query (wire parse + FIFA id->name mapping, unknown id = hard error), entities (id<->name from committed tables), and club_response (FIFA _item shaping; drops items lacking a real FIFA asset id, never fabricates one). openfut-core submodule advanced to the reconciled trunk (6acae54 = 8c8a4116 multi-game + eab522a replace_squad/SquadRules + the /club semantic query). 11 host tests + adapter fut tests; 10/10 host mutations killed. rare=SP UNKNOWN. Retail rendering of Core inventory still blocked on the Core-card->asset-id identity decision (next phase).
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/<title>/club→ Rust; everything else → Python. No shared path, no "try Rust then Python". - A Core failure on
/clubdegrades to a valid empty{"itemData":[]}and logs an error — it never falls back to Python (which could double-apply a mutation on other routes)./clubis 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_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 |
OPENFUT_FIFA17_ASSET_MAP |
no | — | JSON { "<core_card_id>": <fifa_asset_id> } (see the blocker) |
KNOWN BLOCKER — retail rendering of Core inventory
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). OpenFUT Core's catalogue is synthetic
string-id cards (card_pl_001) with no FIFA asset id, and no committed
card→asset mapping exists. So:
- Without
OPENFUT_FIFA17_ASSET_MAP,/clubreturns{"itemData":[]}— honest, never faked. The shaper drops any item lacking a real asset id. - Making Core inventory actually render in retail requires a Core-card→FIFA-asset
identity decision (seed Core from FIFA assets, or a real mapping table). This is
the "who owns FUT state" question and is the prerequisite for a rendering
retail
/club. Filtering, pagination, entity mapping, transport and fallback are all done and tested independently of it.
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):
cargo test -p openfut-utas-host -p openfut-adapter-fifa17green;clippy -D warningsclean;fmt --checkclean.- Built binary identity == HEAD (
scripts/verify-build-identity.sh); no dirty tree. - Python UTAS directly reachable; the Rust host directly probeable; no stale NAT/switch rules; FIFA fully closed.
Bring-up:
- Move Python UTAS to an alternate port (
FUT_PORT=8199in the container/openfut-fut.sh); it keeps serving there. - 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_ASSET_MAP=<map.json> openfut-utas-host - 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
/clubrequest in the window. - Python positive for other UTAS: unimplemented routes still reach Python.
- Core positive: Core logs the
/collectionquery 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.