feat(fifa17): own FUT hub tile counts in Rust

Migrate GET /hub from the Python proxy to a Rust handler deriving counts from
authoritative state: clubPlayers = owned PLAYER cards in Core (consumables/staff
excluded via catalog kind; may be lower than Python's profile count by the
deferred Legend instances = DIFFERENT-BY-DESIGN), auction/tradePile counts from
the durable market store via the async bridge. Fail-closed 503 on Core error;
market read failure degrades cosmetic counts to 0. Adds classify arm, handler,
ownership + integration tests; reachability tool marks hub migrated.
This commit is contained in:
funman300
2026-08-15 17:18:06 +00:00
parent b30aa352f6
commit 70eb3fc13f
3 changed files with 81 additions and 3 deletions
+53 -1
View File
@@ -117,6 +117,9 @@ pub enum Route {
/// `GET …/club/stats/staff` — Rust-owned static `{}` (production oracle body;
/// FIFA's staff-bonus stat set is deliberately empty).
ClubStatsStaff,
/// `GET …/hub` — the FUT hub tile counts (club players + auction/tradePile),
/// derived from Core inventory + the durable market store (no Python).
Hub,
/// Anything else — proxied verbatim to the Python oracle.
Passthrough,
}
@@ -157,6 +160,7 @@ pub fn classify(method: &str, path: &str) -> Route {
Some("match/reset") if put => Route::MatchReset,
Some(tail) if tail.starts_with("phishing/") => Route::SecurityQuestion,
Some("club/stats/staff") if get => Route::ClubStatsStaff,
Some("hub") if get => Route::Hub,
_ => Route::Passthrough,
}
}
@@ -2273,6 +2277,7 @@ impl Server {
eprintln!("utas-host owner=RUST route=club-stats-staff status=200");
json_status(200, &non_economy::club_stats_staff_body())
}
Route::Hub => self.handle_hub(),
Route::SecurityQuestion => self.handle_security_question(method, target, headers),
Route::Passthrough => {
let resp = match self.pass.forward(method, target, headers, body) {
@@ -2457,6 +2462,52 @@ impl Server {
json_status(status, &body)
}
/// `GET …/hub` — the FUT hub tile counts, owned in Rust (no Python). Derived
/// from authoritative state: `clubPlayers` is the count of owned PLAYER cards
/// in Core (consumables/staff excluded via the catalog kind), and the auction
/// / tradePile counts are the user's active listings in the durable market
/// store. `clubPlayers` may be lower than the Python oracle's profile count by
/// exactly the deferred (unnameable Legend) instances — DIFFERENT-BY-DESIGN,
/// since deferred cards are not owned in Core. Fail-closed on Core error (503);
/// a market-store read failure degrades the cosmetic listing counts to 0.
fn handle_hub(&self) -> WireResponse {
let club_players = match self.core.all_owned() {
Ok(items) => items
.iter()
.filter(|it| self.resolver.kind_of(it) == ContentKind::Player)
.count(),
Err(e) => {
eprintln!("utas-host owner=RUST route=hub status=503 error=core:{e}");
return error_response(503, "core_unavailable");
}
};
let active = match &self.economy {
Some(svc) => {
let market = svc.market.clone();
match svc
.bridge
.block_on(async move { market.query_listings("active").await })
{
Ok(l) => l.len(),
Err(e) => {
eprintln!("utas-host WARN hub market read failed (counts=0): {e}");
0
}
}
}
None => 0,
};
let body = json!({
"clubPlayers": club_players,
"auctionCount": active,
"tradePile": { "count": active, "selling": active, "sold": 0 },
});
eprintln!(
"utas-host owner=RUST route=hub status=200 clubPlayers={club_players} auctionCount={active}"
);
json_status(200, &body)
}
/// Serve forever on `addr` (thread-per-connection, HTTP/1.1 keep-alive).
pub fn serve(&self, addr: &str) -> std::io::Result<()> {
let listener = TcpListener::bind(addr)?;
@@ -3493,13 +3544,14 @@ mod tests {
"/ut/game/fifa17/club/stats/staff",
Route::ClubStatsStaff,
),
("GET", "/ut/game/fifa17/hub", Route::Hub),
];
for (m, p, want) in owned {
assert_eq!(classify(m, p), *want, "OWN: {m} {p}");
}
// Still Python (not yet migrated) / lookalikes / wrong method.
let proxied: &[(&str, &str)] = &[
("GET", "/ut/game/fifa17/hub"),
("GET", "/ut/game/fifa17/club/stats/consumables"),
("GET", "/ut/game/fifa17/club/stats/year"),
("PUT", "/ut/game/fifa17/clientdata/userHubData"),
("POST", "/openfut/account/sync"),