RETRACTION FIRST. The previous commit added itemType to club items on the theory
that it gated ingestion, because player/staff sent it and were resident while
kit/badge/stadium omitted it and were not. Relaunched the client with itemType on
all three: ?type=kit answered total=2 emitted=2, and still no cardtype-7 record.
The correlation was an artefact of the control. Measured read-only over
/proc/PID/mem with full coverage (3605 MiB, nothing skipped): the "resident"
players and staff were all SQUAD members, which arrive via userMassInfo. Players
that appear in /club?type=player but NOT in userMassInfo are not resident either -
0 records for 6 of 6 sampled, 5 with no byte match at all, out of 1966 served.
Residency tracks the ROUTE. /club?type= responses never enter the persistent card
collection, and no value of itemType changes that. itemType is kept as wire
fidelity (every real EA item carries it) and relabelled; its doc no longer claims
to fix anything. The diagnostic KIT_PROBE is removed - it could only have tested
shape hypotheses that this result makes moot.
RECOVERED from the unpacked CardsDLL, no archive extraction, no instrumentation:
Packed kit id, both directions present and agreeing:
id = (teamid << 14) | (year ? (year-1800) << 5 : 0) | kittype
so a kit is addressed by the triple (teamid, year, kittype).
FUN_180033770 answers ONLY for team 130000 - 0x1800d8ab0 is literally
`mov $0x1fbd0,%eax ; ret`. Every other team id falls through to the engine's
catalogue kits, which are the lockable ones.
sub_180033430 writes the tile: NAME = "HOME_SIDE"/"AWAY_SIDE", TYPE = the
localised Kit_type_0 / Kit_type_1 / Kit_type_historical, and LOCKED (always
value 0, never 1). If the queried triple matches NEITHER active triple it
writes NOTHING - which is exactly why one tile rendered "undefined". A missing
write, not a bad string. There is no Kit_type_2.
FUN_1800d73d0 selector 2/3 does `setne dil ; add $0x65,%edi` then compares
itemState: active home = 101, active away = 102, derived arithmetically and
independent of the enum table. year at +0xba is movzbl - a byte INDEX.
Above all of it: FUT_GET_MATCH_KITS_DP (0x7565) handler FUN_1800be6a0 gates on
`cmpb $0x1,0x152(%r14)` and returns early otherwise. KITS_AVAILABLE IS
ctx+0x152. Constructor zeroes it; the only setter is case index 6 (message
0x757a) of the jump table at 0x1800c00d4. Live value is 0, so no kit list is
ever built. 0x757a has no name in CardsDLL and that is bounded, not sloppy: the
registration run ends at 0x7575 with the epilogue immediately after, and 70
other ids resolve from the same table as the positive control.
Tables (audit_fifa17_kits.py, full-table counts): category 2/3/5 -> engine kit
type 0/1/2 with 0 counterexamples against 54/166/145 discriminating keys; the id
band is NOT home/away (band 63 holds 740 home AND 88 third).
Vault: "Kit Selector Data Path.md". cargo test 429 passed 0 failed across the two
crates; clippy -D warnings clean; fmt clean.
openfut-utas-host
The FIFA 17 UTAS migration boundary. It accepts the client-visible HTTP surface, serves migrated routes from Rust/Core plus host-owned durable stores, and proxies only the unclassified tail to the Python behavioral oracle.
FIFA 17 ──HTTP──▶ openfut-utas-host
├── migrated route ──▶ Rust adapter / Core / host stores
└── unclassified tail ──▶ Python UTAS oracle
src/lib.rs::classify is the route-level source of truth. The current Rust surface
includes club/squad/user reads, club rename, auth/session/client data, Store/economy,
packs, owned-item moves, market/trade-pile, and the observed hub support routes.
Safety model
- Classification happens exactly once before execution. There is no "try Rust then Python"; a mutation cannot be double-applied.
- A route classified to Rust never falls back to Python on a Core/store/projection failure. Each handler uses its captured fail-closed or honest-empty wire contract.
PUT …/clubandPUT|POST …/user/clubatomically update the shared account JSON. Every input returns the required zero-atom200 {}response; rejection and persistence failures remain visible in logs.- Numeric
GET …/squad/<n>returns the one Core-backed current squad, matching the Python oracle's single-current-squad behavior. - Python remains the behavioral oracle and rollback backend for routes not yet classified to Rust. New economy behavior belongs in Rust/Core, never Python.
Configuration (env)
| Var | Required | Default | Meaning |
|---|---|---|---|
OPENFUT_UTAS_HOST_ADDR |
yes | — | client-visible host listen address |
OPENFUT_UTAS_PYTHON_URL |
yes | — | Python oracle base for the unclassified tail; must differ from this host |
OPENFUT_FIFA17_CATALOG |
yes | — | FIFA 17 definition identity catalog |
OPENFUT_IDENTITY_STORE |
yes | — | persistent owned-instance ↔ wire-id store |
OPENFUT_PERSONA_ID |
yes | — | non-zero FIFA persona id shared by LSX/Blaze/POW/UTAS |
OPENFUT_MARKET_DB |
yes | — | durable host-owned transfer-market SQLite DB |
OPENFUT_PILE_DB |
yes | — | durable host-owned item-pile SQLite DB |
OPENFUT_CORE_URL |
no | http://127.0.0.1:8080 |
OpenFUT Core base |
OPENFUT_FIFA17_TABLES_DIR |
no | fifa17-recon/data/tables |
FIFA entity tables |
OPENFUT_CLIENTDATA_DB |
no | identity-store sibling clientdata.json |
durable opaque client-data JSON |
OPENFUT_ACCOUNT_PATH |
no | FUT_ACCOUNT_PATH, then identity-store sibling active_account.json |
shared FIFA account/club JSON |
Startup fails if required identity or durable economy state cannot be opened. No placeholder production identity source is substituted.
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 versionedOPENFUT_FIFA17_CATALOG. An unmapped definition is dropped and counted, never faked. - Instance identity (
id) — a stable, persistent, reversible wire integer from the genericopenfut-identitystore under the FIFA 17 wire-id policy (monotonic from100_000_001). The same owned instance keeps its id across restart and reverses exactly; two copies of one definition share aresourceIdbut get distinctids. The namespace is globally monotonic within(fifa17, owned-item)— no per-account column is needed because Core owned-instance ids are globally-unique UUIDs.
Production has a frozen post-P1 baseline and a hot Python rollback. A source change passing local tests is not deployment approval. Build verification, staging, host restart, and live-client promotion remain operator-gated; the current state and promotion evidence live in the OpenFUT Obsidian vault.
Logs are safe by construction: no auth/session/device/token material — only owner, route, filter summary, counts, status.