c0a3f68ded
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).
90 lines
5.0 KiB
Markdown
90 lines
5.0 KiB
Markdown
# 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 `/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_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`, `/club` returns `{"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):
|
|
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_ASSET_MAP=<map.json> 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.
|