feat(utas-host): serve GET /squad/active from Core
Migrate the active-squad READ off the Python oracle to the existing Core-backed projector, completing the squad authority (read + write + /squad/list + userMassInfo overlay) on one projector. - classify: GET /ut/game/<t>/squad/active -> Route::SquadActive. Numeric GET /squad/<n> stays on Python (no Core multi-squad model yet). - handle_squad_active returns the projector object via user_mass_info_squad(v, persona) — byte-identical to userMassInfo.squad; degrades to an empty overlay on stale/missing/Core-error, never falls back to Python. - persona: new REQUIRED OPENFUT_PERSONA_ID (non-zero) on HostConfig, injected not baked, must match LSX/Blaze/POW/UTAS identity. - tests: squad_active parity test; classify updated; README config table + A/B command. fmt + clippy -D warnings + tests (24 host + adapter) green.
This commit is contained in:
@@ -70,6 +70,8 @@ pub enum Route {
|
||||
SquadReplace,
|
||||
/// `GET …/squad/list` — the squad summary, projected from Core.
|
||||
SquadList,
|
||||
/// `GET …/squad/active` — the active squad object, projected from Core.
|
||||
SquadActive,
|
||||
/// `GET …/userMassInfo` — proxied to Python, with only `.squad` overlaid.
|
||||
UserMassInfo,
|
||||
/// Anything else — proxied verbatim to the Python oracle.
|
||||
@@ -78,12 +80,13 @@ pub enum Route {
|
||||
|
||||
/// Classify a request ONCE, before execution. Rust owns exactly:
|
||||
/// * `GET …/club`
|
||||
/// * `PUT …/squad/<n>` (numeric id; `…/squad/active` is NOT numeric → Python)
|
||||
/// * `PUT …/squad/<n>` (numeric id)
|
||||
/// * `GET …/squad/list`
|
||||
/// * `GET …/squad/active` (the active squad, projected from Core)
|
||||
/// * `GET …/userMassInfo` (proxied, `.squad` overlaid)
|
||||
///
|
||||
/// Everything else — `GET …/squad/<n>`, `…/squad/active`, `/clubUser`, auth,
|
||||
/// packs, market, other mutations — falls through to Python. There is no
|
||||
/// Everything else — numeric `GET …/squad/<n>`, `/clubUser`, auth, packs,
|
||||
/// market, other mutations — falls through to Python. There is no
|
||||
/// "try Rust then Python", so a squad mutation can never be double-applied.
|
||||
pub fn classify(method: &str, path: &str) -> Route {
|
||||
let get = method.eq_ignore_ascii_case("GET");
|
||||
@@ -93,6 +96,7 @@ pub fn classify(method: &str, path: &str) -> Route {
|
||||
}
|
||||
match ut_tail(path) {
|
||||
Some("squad/list") if get => Route::SquadList,
|
||||
Some("squad/active") if get => Route::SquadActive,
|
||||
Some("userMassInfo") if get => Route::UserMassInfo,
|
||||
Some(tail) if put && is_numeric_squad_tail(tail) => Route::SquadReplace,
|
||||
_ => Route::Passthrough,
|
||||
@@ -114,9 +118,10 @@ fn is_exact_club_path(path: &str) -> bool {
|
||||
ut_tail(path) == Some("club")
|
||||
}
|
||||
|
||||
/// `squad/<digits>` — the active/full squad save. `squad/active` (non-numeric) is
|
||||
/// deliberately excluded so the multi-squad flow stays with Python until there is
|
||||
/// retail evidence for it.
|
||||
/// `squad/<digits>` — the numeric full-squad target used by `PUT`. The active
|
||||
/// squad READ (`GET …/squad/active`) is routed separately (Core-backed); a
|
||||
/// numeric `GET …/squad/<n>` for a non-active squad stays on Python (there is no
|
||||
/// Core model for multiple squads yet).
|
||||
fn is_numeric_squad_tail(tail: &str) -> bool {
|
||||
match tail.strip_prefix("squad/") {
|
||||
Some(id) => !id.is_empty() && id.bytes().all(|b| b.is_ascii_digit()),
|
||||
@@ -904,6 +909,44 @@ pub fn handle_squad_list(deps: &SquadDeps<'_>) -> (WireResponse, SquadLog) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET …/squad/active` — the active squad projected from Core, returned as the
|
||||
/// top-level squad object (byte-identical to what `userMassInfo.squad` embeds).
|
||||
/// Never served from Python and NEVER projected from a stale extension; on a
|
||||
/// stale/missing extension or a Core error it degrades to an honest empty squad
|
||||
/// (never 401/403, never a Python fallback that could mask split authority).
|
||||
pub fn handle_squad_active(deps: &SquadDeps<'_>, persona_id: i64) -> (WireResponse, SquadLog) {
|
||||
match project_active_squad(deps) {
|
||||
HostProjection::Squad(v) => (
|
||||
json_response(&user_mass_info_squad(v, persona_id)),
|
||||
SquadLog {
|
||||
outcome: "ok",
|
||||
detail: String::new(),
|
||||
},
|
||||
),
|
||||
HostProjection::Stale => (
|
||||
json_response(&empty_squad_overlay(persona_id)),
|
||||
SquadLog {
|
||||
outcome: "stale_integrity",
|
||||
detail: "stale extension not applied".into(),
|
||||
},
|
||||
),
|
||||
HostProjection::Missing => (
|
||||
json_response(&empty_squad_overlay(persona_id)),
|
||||
SquadLog {
|
||||
outcome: "missing_integrity",
|
||||
detail: "no extension stored".into(),
|
||||
},
|
||||
),
|
||||
HostProjection::Error(e) => (
|
||||
json_response(&empty_squad_overlay(persona_id)),
|
||||
SquadLog {
|
||||
outcome: "core_error",
|
||||
detail: e,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// An explicit empty active squad used only when Rust squad authority cannot
|
||||
/// produce a Fresh projection during a userMassInfo overlay. It is NOT Python's
|
||||
/// squad (that would reintroduce split authority) and NOT fabricated extension
|
||||
@@ -1138,6 +1181,10 @@ pub struct Server {
|
||||
/// and the userMassInfo overlay — one wire↔owned identity everywhere.
|
||||
resolver: Arc<Fifa17IdentityResolver>,
|
||||
pass: Arc<PassClient>,
|
||||
/// The launcher-selected FIFA persona id (injected via `OPENFUT_PERSONA_ID`),
|
||||
/// used to stamp `personaId` on the Core-backed `GET /squad/active` object.
|
||||
/// Never baked in — it must match the persona LSX/Blaze/POW/UTAS agree on.
|
||||
persona_id: i64,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
@@ -1147,12 +1194,14 @@ impl Server {
|
||||
entities: Arc<Fifa17Entities>,
|
||||
resolver: Arc<Fifa17IdentityResolver>,
|
||||
pass: Arc<PassClient>,
|
||||
persona_id: i64,
|
||||
) -> Self {
|
||||
Server {
|
||||
core,
|
||||
entities,
|
||||
resolver,
|
||||
pass,
|
||||
persona_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,6 +1225,7 @@ impl Server {
|
||||
entities: Arc::new(entities),
|
||||
resolver,
|
||||
pass: Arc::new(PassClient::new(cfg.python_upstream.clone())),
|
||||
persona_id: cfg.persona_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1232,6 +1282,15 @@ impl Server {
|
||||
);
|
||||
resp
|
||||
}
|
||||
Route::SquadActive => {
|
||||
let deps = self.squad_deps();
|
||||
let (resp, log) = handle_squad_active(&deps, self.persona_id);
|
||||
eprintln!(
|
||||
"utas-host owner=RUST route=squad-active status={} outcome={} detail=[{}]",
|
||||
resp.status, log.outcome, log.detail
|
||||
);
|
||||
resp
|
||||
}
|
||||
Route::UserMassInfo => {
|
||||
let deps = self.squad_deps();
|
||||
let (resp, log) =
|
||||
|
||||
Reference in New Issue
Block a user