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.
This commit is contained in:
@@ -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/<mode>` — 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}");
|
||||
|
||||
Reference in New Issue
Block a user