Files
OpenFUT/openfut-utas-host
funman300 f9ca901a50 market: stop advertising unlisted pile members as tradeState:"inactive"
RE of the FUT front-end closed the question the Actions-panel investigation left
open, and the answer retracts Q2 rather than completing it.

`tradeState` reaches exactly ONE native branch in CardsDLL — `cmp …,0x4` at
`0x18013e619`, "is it closed?" — and `inactive`(2) and `expired`(3) take the same
edge, producing bit-identical `flagA`/`flagB` (exhaustive 22-site census of
`[reg+0x88]` reads across the PE; confirmed live, both classes read glow=0
inbox=0). The value is then handed to the movie verbatim as the Flash property
`STATE`, and the action gate lives in the APT/ActionScript FUT front-end: the
trade-pile class partitions rows with `getCardsInAuction`/`isInActiveAuction`
(traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`) and only auction rows
reach `PreCheckCardOptions` -> `handleTradeCardAction`. A non-auction row renders
and can never be acted on, which is exactly what the operator saw.

So the rows were never usable. "LIVE-CONFIRMED" established that they RENDER,
which is not the same claim, and I treated it as if it were.

The corpus said this before any of it was built —
`plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client
path treats it specially; do not emit it." The earlier note explaining that the
warning "was written about the PRESENTATION function" was motivated reasoning.
This also fires the corpus's own pre-registered falsifier E3 (:368-373).

Removed: the `inactive` projection from `GET …/tradePile` and `…/trade/status`,
`UnlistedCandidate`, `resolve_unlisted_pile`, `unlisted_record`,
`Server::resolve_trade_pile`, and the two helpers that existed only to feed them
(`MarketStore::blocking_core_items`, `Fifa17IdentityResolver::wire_for_owned_id`).
Unlisted trade-pile membership is now internal state with no wire expression.

Nothing is stranded: `/club` excludes only items with an ACTIVE listing, so an
unlisted pile member stays visible in the club, which is where the client can act
on it. Verified live after deploy — `/tradePile` total 7 -> 1 with zero `inactive`
rows, `/trade/status` resolving only the real auction, coins unchanged at
29,843,976, and all six former rows present in `/club` (1965 items).

Tests: 126 pass, fmt + clippy clean. Two guards replace the three tests that
pinned the old behaviour: `the_trade_pile_advertises_only_real_auctions` and
`trade_status_answers_only_about_real_auctions`.

NOT fixed here, deliberately: `itemData.itemState: "listFS"` is not a FIFA 17
token (0 occurrences in CardsDLL md5 4de3493131d7d2ff7f8b360c5ac9b655, 0 in
4.26 GiB of process memory, decodes to -1; the real value is `forSale` = 5, and
the Python oracle emits `listFS` too — which is why the differential never caught
it). `CARD_OFFERSTATE` is one of three unresolved action-gate candidates and
every actionable row observed carried -1, so that change ships alone with its own
live A/B.
2026-08-17 23:14:35 +00:00
..

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_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 ids. 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.