From 33e91183290b6e9e91aef12e08064fe751eed050 Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 17 Aug 2026 16:10:53 +0000 Subject: [PATCH] feat(host): migrate flag-off UTAS reads (season/tournament/champion/clubUser/user-list) to Rust FUT modes (Seasons/Tournaments/FUT Champions) and the club-identity service are disabled in this emulator, so these GET reads return {} verbatim from the Python oracle. Serve them directly from Rust via a new Route::FeatureOffEmpty + non_economy::feature_off_body() -> {} (byte-identical to the flag-off oracle), reducing the proxied Python surface. - The mutating club rename (user/club) stays on Python (needs a Core write). - Enabling a mode later requires a real Rust handler here, never a Python fallback (no split authority). - classify tests: 5 routes owned + user/club/wrong-method lookalikes stay Passthrough; updated the stale clubUser assertion. - Deployed to prod-host 2026-08-17; verified owner=RUST 200 {} for all five. Docs: PRODUCTION_AUTHORITY_MATRIX + PYTHON_RETIREMENT_PLAN updated. --- docs/PRODUCTION_AUTHORITY_MATRIX.md | 5 +-- docs/PYTHON_RETIREMENT_PLAN.md | 10 +++--- openfut-adapter-fifa17/src/fut/non_economy.rs | 10 ++++++ openfut-utas-host/src/lib.rs | 33 ++++++++++++++++++- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/docs/PRODUCTION_AUTHORITY_MATRIX.md b/docs/PRODUCTION_AUTHORITY_MATRIX.md index e6150f7..cbef3d3 100644 --- a/docs/PRODUCTION_AUTHORITY_MATRIX.md +++ b/docs/PRODUCTION_AUTHORITY_MATRIX.md @@ -53,16 +53,17 @@ Legend: owner R = Rust/Core, P = Python oracle (:8199 proxied via PYTHON_FALLBAC | GET /club/stats/{year,consumables,staff,country,league,team} | R | Core aggregation; context buckets keyed nation/league/team (owned=1982); staff={} | | GET /store, /match/keepalive, /captcha, /tfa, /livemessage, /activeMessage | R | unconditional constant acks (byte-identical to the oracle; StaticAck route) | | GET /watchList (+ PUT/POST/DELETE) | R | empty watch list + authoritative Core credits; add/remove is a no-op ack (oracle persists none) | +| GET /season, /tournament, /champion, /clubUser, /user/list | R | FUT modes + club-identity off → `{}` (FeatureOffEmpty; byte-identical to the flag-off oracle). Migrated + deployed 2026-08-17 | | POST /ut/.../match/end (DestroyMatch) | R | economy reward (coins credited via Core grant_reward) | ### NON-ECONOMY — still Python (PYTHON_FALLBACK) | Method/path | Owner | Reason | |---|---|---| | GET /item/resource, /defid | P | `item_def(rid)` shapes `{itemData:[…]}` from `PLAYER_DEFS` (asset=rid&0xffffff → name/rating/pos/attrs) + consumable defs, placeholder fallback ("Player",75,attrs 70). Faithful Rust needs those def tables in the host. Shape captured: `docs/evidence/route-shapes-2026-08-17/defs_resource.json` | -| GET /clubUser, /user/list, /user/club | P | club identity (off→{}, flag-gated) + rename (mutating); Core has `clubs` but rename needs a Core write | +| POST /user/club (rename) | P | mutating club rename; Core has `clubs` but rename needs a Core write (deferred). Reads `clubUser`/`user/list` are now Rust. | | GET /squad/ (n≠0, non-active) | P | no multi-squad Core model (Rust owns squad/0, squad/active, /squad/list, PUT) | | /squad/mode/draft/* | P | FUT Draft mode | -| GET /season, /tournament, /champion, /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off by default → `{}`); real behavior needs the mode logic ported | +| GET /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off → `{}`/content; `/sbs/sets` ships content); real behavior needs the mode logic ported. `season`/`tournament`/`champion` are now Rust. | | GET /marketdata (+ /marketdata/pricelimits) | P | suggested pricing; freeze-risk (array vs object container); Python-correct, constant band 150..15000 | | POST /ut/.../match (CREATE), /match/ready (READY), /match (PLAY) | P | match handshake legs; no match ever played in-game (see docs/MATCH_LIFECYCLE.md) | diff --git a/docs/PYTHON_RETIREMENT_PLAN.md b/docs/PYTHON_RETIREMENT_PLAN.md index c2dc4a3..322025f 100644 --- a/docs/PYTHON_RETIREMENT_PLAN.md +++ b/docs/PYTHON_RETIREMENT_PLAN.md @@ -8,7 +8,7 @@ rollback-to-python-p2.sh, :p2-rollback image b1b929953f, profile 39bb3e83). ## A. LIVE PRODUCTION REQUIRED (still owns behavior in the live flow) | Component | Role | Rust status | Retire when | |---|---|---|---| -| oracle utas_server.py (:8199) NON-ECONOMY | **remaining**: item-defs (item/resource,defid), user-identity (user,clubUser,user/list,user/club), non-active squad/, draft, watchList, marketdata, mode-gated (season/tournament/champion/leaderboards/sbs, off→{}) | **Most non-economy migrated 2026-08-17** (account/sync, auth, userMassInfo, clientdata, hub, club/stats/*, settings, accountinfo, phishing, match/reset, leaderboards/options, static acks — all Rust). See PRODUCTION_AUTHORITY_MATRIX | remaining tail migrated (needs live captures for the data routes; mode-logic port for the gated ones) OR mode-gated ones kept Python | +| oracle utas_server.py (:8199) NON-ECONOMY | **remaining**: item-defs (item/resource,defid), user/club rename, non-active squad/, draft, marketdata, mode-gated leaderboards + /sbs/* | **Most non-economy migrated 2026-08-17** (account/sync, auth, userMassInfo, user, clientdata, hub, club/stats/*, settings, accountinfo, phishing, match/reset, leaderboards/options, watchList, static acks, and the flag-off empty reads season/tournament/champion/clubUser/user/list → `{}` — all Rust). See PRODUCTION_AUTHORITY_MATRIX | remaining tail migrated (needs live captures for the data routes; mode-logic port for the gated ones) OR mode-gated ones kept Python | | blaze_responder_v3b.py (:42130 Blaze) | FUT Blaze transport | Rust openfut-blaze-host COMPLETE, gate-proven (Gate 10) | container cutover (operator-gated deploy) | | blaze_responder_v3b.py (:42127 redirector TLS) | first-hop TLS redirect | Rust openfut-redirector-host COMPLETE (OpenSSL) | container cutover + cert consistency | | roster_server.py (:8081) | roster-update XML | Rust openfut-roster-host COMPLETE (unit-only) | container cutover | @@ -34,10 +34,10 @@ rollback-to-python-p2.sh, :p2-rollback image b1b929953f, profile 39bb3e83). ## Retirement gating 1. **Mostly DONE (2026-08-17)**: account/sync, ut/auth (Rust SID mint), userMassInfo (full), clientdata, - club/stats/{country,league,team}, and the trivial static acks migrated + deployed. **Remaining on Python**: - item-defs, user-identity (user/clubUser/user-list/user-club), non-active squad/, draft, watchList, - marketdata (no captured wire shape), and mode-gated season/tournament/champion/leaderboards/sbs - (return {} while FUT_MODES/_SBC off). Migrate the data ones once live shapes are captured. + club/stats/{country,league,team}, watchList, static acks, and the flag-off empty reads + (season/tournament/champion/clubUser/user/list → `{}`) migrated + deployed. **Remaining on Python**: + item-defs, user/club rename, non-active squad/, draft, marketdata (freeze-risk), and mode-gated + leaderboards + /sbs/* (need the mode logic ported). Migrate the data ones once live shapes are captured. 2. Deploy Rust blaze/roster/redirector via container cutover (operator-gated) — then those Python responders are class D/E only. 3. POW: build Rust host + reverse bodies (largest blocker) OR keep Python POW as class A indefinitely. diff --git a/openfut-adapter-fifa17/src/fut/non_economy.rs b/openfut-adapter-fifa17/src/fut/non_economy.rs index 337c545..0a9dcdc 100644 --- a/openfut-adapter-fifa17/src/fut/non_economy.rs +++ b/openfut-adapter-fifa17/src/fut/non_economy.rs @@ -46,6 +46,16 @@ pub fn club_stats_staff_body() -> Value { json!({}) } +/// The FUT modes (Seasons / Tournaments / FUT Champions) and the club-identity +/// service are disabled in this emulator, so their read routes (`season`, +/// `tournament`, `champion`, `clubUser`, `user/list`) return an empty object — +/// byte-identical to the Python oracle with `FUT_MODES` / `FUT_CLUB_IDENTITY` +/// off. Enabling a mode later requires a real Rust implementation here, never a +/// Python fallback (which would reintroduce split authority). +pub fn feature_off_body() -> Value { + json!({}) +} + /// The phishing/security-question action, parsed from the URL tail. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SecurityAction { diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index 3cd1fba..bac2f32 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -146,6 +146,13 @@ pub enum Route { /// `GET …/user` — the FUT user profile `{"userInfo": …}`, the same userInfo /// object `userMassInfo` embeds (shared builder). POST /user (create) stays Python. User, + /// `GET …/{season,tournament,champion,clubUser,user/list}` — FUT modes and + /// the club-identity service are disabled in this emulator, so these reads + /// return `{}`, byte-identical to the Python oracle with `FUT_MODES` / + /// `FUT_CLUB_IDENTITY` off. The mutating club-identity route (`user/club` + /// rename) stays on Python. Turning a feature on later means a real Rust + /// handler here — never a Python fallback. + FeatureOffEmpty, /// Anything else — proxied verbatim to the Python oracle. Passthrough, } @@ -206,6 +213,11 @@ pub fn classify(method: &str, path: &str) -> Route { Some("livemessage") => Route::StaticAck, Some("activeMessage") => Route::StaticAck, Some("watchList") => Route::WatchList, + Some("season") if get => Route::FeatureOffEmpty, + Some("tournament") if get => Route::FeatureOffEmpty, + Some("champion") if get => Route::FeatureOffEmpty, + Some("clubUser") if get => Route::FeatureOffEmpty, + Some("user/list") if get => Route::FeatureOffEmpty, _ => Route::Passthrough, } } @@ -2329,6 +2341,7 @@ impl Server { Route::StaticAck => self.handle_static_ack(path), Route::WatchList => self.handle_watchlist(method), Route::User => self.handle_user(), + Route::FeatureOffEmpty => self.handle_feature_off_empty(path), Route::SecurityQuestion => self.handle_security_question(method, target, headers), Route::Passthrough => { let resp = match self.pass.forward(method, target, headers, body) { @@ -2717,6 +2730,16 @@ impl Server { json_status(200, &json!({ "auctionInfo": [], "credits": credits, "total": 0 })) } + /// `GET …/{season,tournament,champion,clubUser,user/list}` — FUT modes and the + /// club-identity service are off, so return `{}` (parity with the flag-off + /// Python oracle). The mutating `user/club` rename is NOT here — it stays on + /// Python until a real Rust club-identity handler exists. + fn handle_feature_off_empty(&self, path: &str) -> WireResponse { + let tail = ut_tail(path).unwrap_or(""); + eprintln!("utas-host owner=RUST route=feature-off-empty tail={tail} status=200"); + json_status(200, &non_economy::feature_off_body()) + } + /// `GET …/club/stats/` — the MY CLUB stat set, computed Core-accurately /// in Rust (no Python). Player tiers + rare from the Core collection, /// staff/consumable families from the catalog kind+subtype, and per-context @@ -3323,9 +3346,10 @@ mod tests { classify("GET", "/ut/game/fifa17/club/stats/country/54"), Route::ClubStats ); + // clubUser is now Rust-owned (`{}` while club identity is off). assert_eq!( classify("GET", "/ut/game/fifa17/clubUser"), - Route::Passthrough + Route::FeatureOffEmpty ); assert_eq!( classify("GET", "/ut/game/fifa17/tradePile"), @@ -3835,6 +3859,11 @@ mod tests { Route::ClientData, ), ("POST", "/openfut/account/sync", Route::AccountSync), + ("GET", "/ut/game/fifa17/season", Route::FeatureOffEmpty), + ("GET", "/ut/game/fifa17/tournament", Route::FeatureOffEmpty), + ("GET", "/ut/game/fifa17/champion", Route::FeatureOffEmpty), + ("GET", "/ut/game/fifa17/clubUser", Route::FeatureOffEmpty), + ("GET", "/ut/game/fifa17/user/list", Route::FeatureOffEmpty), ]; for (m, p, want) in owned { assert_eq!(classify(m, p), *want, "OWN: {m} {p}"); @@ -3845,6 +3874,8 @@ mod tests { ("POST", "/ut/game/fifa17/match/reset"), // match/reset is PUT-only ("GET", "/ut/game/fifa17/match/reset"), ("PUT", "/ut/game/fifa17/user/accountinfo"), + ("POST", "/ut/game/fifa17/season"), // FUT-mode reads are GET-only + ("GET", "/ut/game/fifa17/user/club"), // mutating rename stays Python ]; for (m, p) in proxied { assert_eq!(classify(m, p), Route::Passthrough, "PROXY: {m} {p}");