wip(fifa17): pre-existing SBC/economy candidate snapshot
Snapshot of the uncommitted economy/SBC candidate work that built the tested
sbc-host on top of e8d1c1d (NOT authored in this session; committed to leave a
clean tree). Covers pack_content, sbc, store_catalog, host economy_store/lib,
purchasegroup fixtures, economy integration/concurrency/differential tests,
and Cargo.lock. Content matches the running staging host binary.
This commit is contained in:
+100
-17
@@ -73,7 +73,9 @@ use openfut_adapter_fifa17::fut::squad_projection::{
|
||||
project_squad, squad_list, user_mass_info_squad, ProjectionSlot, SquadExtInput,
|
||||
SquadProjection, SquadProjectionInput,
|
||||
};
|
||||
use openfut_adapter_fifa17::fut::store_catalog::build_purchasegroup;
|
||||
use openfut_adapter_fifa17::fut::store_catalog::{
|
||||
build_purchasegroup, owned_pack_id_for_definition,
|
||||
};
|
||||
use openfut_adapter_fifa17::fut::store_session::{
|
||||
validate_capability, SessionStore, StoreMode, SENTINEL_PACK_ID,
|
||||
};
|
||||
@@ -663,6 +665,17 @@ pub trait CoreAccess: Send + Sync {
|
||||
Ok(self.query_owned(&[])?.items)
|
||||
}
|
||||
|
||||
/// Every card DEFINITION in Core content (`GET /cards`) — the full card
|
||||
/// universe a pack can draw from, independent of ownership. Same shape as
|
||||
/// owned items (rating/position/nation/league/club/attributes) so
|
||||
/// [`build_content_pool`] treats definitions and owned items uniformly.
|
||||
/// Default: unimplemented (callers fall back to owned inventory).
|
||||
fn all_definitions(&self) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
Err(CoreError::Parse(
|
||||
"Core content enumeration is not implemented".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Read the active squad + its opaque extension for `namespace`.
|
||||
fn read_squad_ext(&self, namespace: &str) -> Result<CoreSquadRead, CoreError>;
|
||||
|
||||
@@ -746,6 +759,22 @@ impl CoreAccess for HttpCoreClient {
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
parse_core_page(&v)
|
||||
}
|
||||
|
||||
fn all_definitions(&self) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
let url = format!("{}/cards", self.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-OpenFUT-Game", &self.game)
|
||||
.send()
|
||||
.map_err(|e| CoreError::Http(e.to_string()))?;
|
||||
let status = resp.status().as_u16();
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(CoreError::Status(status));
|
||||
}
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
parse_core_definitions(&v)
|
||||
}
|
||||
fn read_squad_ext(&self, namespace: &str) -> Result<CoreSquadRead, CoreError> {
|
||||
let url = format!("{}/squad/ext", self.base_url);
|
||||
let resp = self
|
||||
@@ -1361,6 +1390,38 @@ fn core_item_from_json(e: &Value) -> Option<CoreOwnedItem> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse Core's `/cards` response `{ "cards": [CardDefinition...], ... }` into the
|
||||
/// same [`CoreOwnedItem`] shape as owned items (`owned_card_id` empty — a
|
||||
/// definition is not an instance), for full-universe pool building.
|
||||
pub fn parse_core_definitions(v: &Value) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
let arr = v
|
||||
.get("cards")
|
||||
.and_then(|c| c.as_array())
|
||||
.ok_or_else(|| CoreError::Parse("missing `cards` array".into()))?;
|
||||
Ok(arr.iter().filter_map(core_item_from_definition).collect())
|
||||
}
|
||||
|
||||
fn core_item_from_definition(card: &Value) -> Option<CoreOwnedItem> {
|
||||
let attr = |k: &str| card.get(k).and_then(|v| v.as_i64()).unwrap_or(0) as u8;
|
||||
Some(CoreOwnedItem {
|
||||
owned_card_id: String::new(),
|
||||
card_id: card.get("id")?.as_str()?.to_string(),
|
||||
rating: card.get("overall").and_then(|v| v.as_i64()).unwrap_or(0) as u8,
|
||||
position: card.get("position")?.as_str()?.to_string(),
|
||||
nation: card.get("nation")?.as_str()?.to_string(),
|
||||
league: card.get("league")?.as_str()?.to_string(),
|
||||
club: card.get("club")?.as_str()?.to_string(),
|
||||
attributes: [
|
||||
attr("pace"),
|
||||
attr("shooting"),
|
||||
attr("passing"),
|
||||
attr("dribbling"),
|
||||
attr("defending"),
|
||||
attr("physical"),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────── Item identity resolver ───────────────────────
|
||||
|
||||
/// The single production [`ItemIdentityResolver`]: it composes the two distinct
|
||||
@@ -1419,6 +1480,15 @@ impl Fifa17IdentityResolver {
|
||||
.map(|c| c.rareflag)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Non-minting definition identity from the catalog: `Some((rareflag, kind))`
|
||||
/// if the card resolves, else `None`. Used to build the pack pool over ALL
|
||||
/// content definitions WITHOUT allocating a wire id per definition (that would
|
||||
/// pollute the identity store); a real wire id is minted only when a pack draw
|
||||
/// actually mints the card.
|
||||
pub fn definition_identity(&self, card_id: &str) -> Option<(i64, ContentKind)> {
|
||||
self.catalog.lookup(card_id).map(|c| (c.rareflag, c.kind))
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemIdentityResolver for Fifa17IdentityResolver {
|
||||
@@ -2148,11 +2218,14 @@ pub fn handle_credits(econ: &dyn CoreEconomy) -> WireResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map Core entitlements to FIFA unopened pack ids (definition_id parsed as the
|
||||
/// numeric pack id; unparseable entries are skipped, never faked).
|
||||
/// Map Core entitlements to FIFA 17 unopened pack ids. `definition_id` is either
|
||||
/// a numeric owned-only pack id (imported entitlements) or a symbolic reward-pack
|
||||
/// name granted by Core's reward services; both resolve via
|
||||
/// [`owned_pack_id_for_definition`]. Unresolvable entitlements are skipped, never
|
||||
/// faked.
|
||||
fn entitlement_pack_ids(ents: &[EconomyEntitlement]) -> Vec<u64> {
|
||||
ents.iter()
|
||||
.filter_map(|e| e.definition_id.parse::<u64>().ok())
|
||||
.filter_map(|e| owned_pack_id_for_definition(&e.definition_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -2372,30 +2445,40 @@ pub struct EconomyServices {
|
||||
pub sold_experiment: crate::sold_experiment::SoldExperiment,
|
||||
}
|
||||
|
||||
/// Build the pack-content candidate pool from Core's current content, evidenced
|
||||
/// by the owned inventory: every distinct owned card definition that resolves to
|
||||
/// a real FIFA asset id is a candidate (`gold` = rating ≥ 75; `special` from the
|
||||
/// catalog `rareflag > 1`). This is the resolvable FIFA∩Core card universe — the
|
||||
/// cards a pack can award and the shared shaper can render. An empty pool (no
|
||||
/// content, or Core unreachable) is fail-closed by construction: the generator
|
||||
/// returns no cards, so the Store neither mints nor debits.
|
||||
/// Build the pack-content candidate pool. Prefers the FULL card universe (Core
|
||||
/// content, `GET /cards` via [`CoreAccess::all_definitions`]) so a pack can award
|
||||
/// any card in the game, not only cards the profile already owns; falls back to
|
||||
/// owned inventory when content enumeration is unavailable (older Core / tests).
|
||||
/// Each candidate resolves in the FIFA catalog (unmapped or non-player cards are
|
||||
/// dropped, never faked); `gold` = rating ≥ 75, `special` = catalog `rareflag > 1`.
|
||||
/// An empty pool (no content, or Core unreachable) is fail-closed by construction:
|
||||
/// the generator returns no cards, so the Store neither mints nor debits.
|
||||
pub fn build_content_pool(
|
||||
core: &dyn CoreAccess,
|
||||
resolver: &Fifa17IdentityResolver,
|
||||
) -> Vec<GeneratedCandidate> {
|
||||
let owned = match core.all_owned() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
// Full universe first; owned inventory only as a degrade path.
|
||||
let items = match core.all_definitions() {
|
||||
Ok(v) if !v.is_empty() => v,
|
||||
_ => match core.all_owned() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
},
|
||||
};
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut pool = Vec::new();
|
||||
for item in &owned {
|
||||
for item in &items {
|
||||
if !seen.insert(item.card_id.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = resolver.resolve(item) else {
|
||||
// Non-minting catalog lookup: drop unmapped cards and non-player content
|
||||
// (consumables/staff never enter the player-card pack pool).
|
||||
let Some((rareflag, kind)) = resolver.definition_identity(&item.card_id) else {
|
||||
continue;
|
||||
};
|
||||
if kind != ContentKind::Player {
|
||||
continue;
|
||||
}
|
||||
pool.push(GeneratedCandidate {
|
||||
card_id: item.card_id.clone(),
|
||||
rating: item.rating,
|
||||
@@ -2405,7 +2488,7 @@ pub fn build_content_pool(
|
||||
club: item.club.clone(),
|
||||
attributes: item.attributes,
|
||||
gold: item.rating >= 75,
|
||||
special: id.rareflag > 1,
|
||||
special: rareflag > 1,
|
||||
});
|
||||
}
|
||||
pool
|
||||
|
||||
Reference in New Issue
Block a user