From 1d6a6fffcc99a21a6377cf70f8aeddd0857546ef Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 19 Aug 2026 01:27:22 +0000 Subject: [PATCH] fifa17 store: make reward packs (SBC/draft/season/...) openable SBC completion and the other Core reward services grant packs with symbolic definition_ids ("silver_pack", "gold_pack", ...). The FIFA 17 pack system keys entirely on numeric catalogue ids, and entitlement_pack_ids / handle_pack_open resolved definition_ids with definition_id.parse::(), so every symbolic reward pack was silently dropped from the openable My Packs list. The unopened count (recoveredPacks, = entitlement count) still counted them, so the client showed "you have N packs" but had no tile to open -> "no pack available". Fix (adapter-layer, Core stays game-neutral): add owned-only reward pack catalogue entries 71-75 (bronze/silver/gold/rare_gold/icon) and a resolver owned_pack_id_for_definition() that maps both numeric owned ids and the symbolic reward names to their numeric owned-only pack. entitlement_pack_ids and the pack-open entitlement selection now use it, so reward packs render as openable My Packs tiles and redeem their entitlement for free (consume-once, no debit). Server-verified on staging: 3 Core reward entitlements now render 3 openable mypacks tiles matching recoveredPacks=3. Tests: adapter resolver + rendering, host symbolic-reward open flow; full adapter + host suites green. --- .../src/fut/store_catalog.rs | 78 +++++++++++++++++++ openfut-utas-host/src/economy_store.rs | 36 ++++++++- openfut-utas-host/src/lib.rs | 13 +++- 3 files changed, 119 insertions(+), 8 deletions(-) diff --git a/openfut-adapter-fifa17/src/fut/store_catalog.rs b/openfut-adapter-fifa17/src/fut/store_catalog.rs index 16d9eac..7693c13 100644 --- a/openfut-adapter-fifa17/src/fut/store_catalog.rs +++ b/openfut-adapter-fifa17/src/fut/store_catalog.rs @@ -92,6 +92,21 @@ pub const PACK_CATALOG: &[PackDef] = &[ PackDef { id: 70, name: "Reward Gold Pack", price: 0, n_bronze: 0, n_silver: 0, n_gold: 11, rares: 11, category: "gold", special_chance: 1.0, owned_only: true }, + PackDef { id: 71, name: "Bronze Pack", price: 0, + n_bronze: 10, n_silver: 2, n_gold: 0, rares: 1, category: "bronze", + special_chance: 0.01, owned_only: true }, + PackDef { id: 72, name: "Silver Pack", price: 0, + n_bronze: 1, n_silver: 11, n_gold: 0, rares: 1, category: "silver", + special_chance: 0.02, owned_only: true }, + PackDef { id: 73, name: "Gold Pack", price: 0, + n_bronze: 0, n_silver: 2, n_gold: 10, rares: 1, category: "gold", + special_chance: 0.05, owned_only: true }, + PackDef { id: 74, name: "Rare Gold Pack", price: 0, + n_bronze: 0, n_silver: 2, n_gold: 10, rares: 3, category: "gold", + special_chance: 0.10, owned_only: true }, + PackDef { id: 75, name: "Icon Pack", price: 0, + n_bronze: 0, n_silver: 0, n_gold: 12, rares: 12, category: "gold", + special_chance: 1.0, owned_only: true }, ]; /// Look up a catalogue pack by id (the 65534 sentinel is never present). @@ -99,6 +114,29 @@ pub fn pack_by_id(id: u64) -> Option<&'static PackDef> { PACK_CATALOG.iter().find(|p| p.id == id) } +/// Resolve a Core entitlement's opaque `definition_id` to the FIFA 17 numeric +/// owned-only pack it renders and opens as. Accepts a numeric id (imported +/// entitlements, e.g. `"70"`) or one of the symbolic reward-pack names Core's +/// reward services grant (SBC, draft, season, check-in, FUT Champions). Only +/// owned-only packs qualify, so an unknown or non-reward entitlement resolves to +/// `None` and is simply not shown as an openable pack rather than faked. +pub fn owned_pack_id_for_definition(definition_id: &str) -> Option { + if let Ok(numeric) = definition_id.parse::() { + return pack_by_id(numeric) + .filter(|pack| pack.owned_only) + .map(|pack| pack.id); + } + let id = match definition_id { + "bronze_pack" => 71, + "silver_pack" => 72, + "gold_pack" => 73, + "rare_gold_pack" => 74, + "icon_pack" => 75, + _ => return None, + }; + Some(id) +} + /// The FIFA 17 StoreFront category token for a pack tile (`displayGroup.value`): /// one of the six hard-coded tokens the client resolves. Each catalogue pack /// carries its own token; owned/reward packs take `mypacks` instead (see @@ -276,6 +314,46 @@ mod tests { assert!(PACK_CATALOG.iter().all(|p| p.id != SENTINEL_PACK_ID)); } + #[test] + fn reward_pack_definitions_resolve_to_openable_owned_packs() { + // Core reward services grant symbolic pack names; each must resolve to an + // owned-only catalogue pack so it renders as an openable My Packs tile. + for (def, want) in [ + ("bronze_pack", 71), + ("silver_pack", 72), + ("gold_pack", 73), + ("rare_gold_pack", 74), + ("icon_pack", 75), + ] { + let id = owned_pack_id_for_definition(def).expect("reward def resolves"); + assert_eq!(id, want); + assert!( + pack_by_id(id).unwrap().owned_only, + "a reward pack must be owned-only" + ); + } + // Imported numeric owned-pack ids resolve to themselves. + assert_eq!(owned_pack_id_for_definition("70"), Some(70)); + // Purchasable (non-owned) numeric ids and unknown names never resolve. + assert_eq!(owned_pack_id_for_definition("5"), None); + assert_eq!(owned_pack_id_for_definition("mystery_pack"), None); + } + + #[test] + fn symbolic_reward_pack_renders_as_openable_my_packs_tile() { + // A granted silver reward pack (resolved to id 72) must appear as an owned + // My Packs tile, not the non-openable sentinel shim. + let got = build_purchasegroup(&[72], StoreMode::Sentinel); + let packs = got["purchase"].as_array().unwrap(); + let reward = packs + .iter() + .find(|p| p["id"] == 72) + .expect("reward pack tile present"); + assert_eq!(reward["displayGroup"]["value"], "mypacks"); + assert!(reward["unopened"].as_bool().unwrap()); + assert!(packs.iter().all(|p| p["id"] != SENTINEL_PACK_ID)); + } + #[test] fn clean_v1_empty_emits_no_mypacks_group() { let got = build_purchasegroup(&[], StoreMode::CleanV1); diff --git a/openfut-utas-host/src/economy_store.rs b/openfut-utas-host/src/economy_store.rs index 4912555..5344c51 100644 --- a/openfut-utas-host/src/economy_store.rs +++ b/openfut-utas-host/src/economy_store.rs @@ -31,7 +31,9 @@ use openfut_adapter_fifa17::fut::pack_content::{ generate_pack_contents, GeneratedCandidate, GeneratedCard, }; use openfut_adapter_fifa17::fut::squad::SquadWireResolver; -use openfut_adapter_fifa17::fut::store_catalog::{pack_by_id, PackDef}; +use openfut_adapter_fifa17::fut::store_catalog::{ + owned_pack_id_for_definition, pack_by_id, PackDef, +}; use crate::{ error_response, json_response, json_status, CoreAccess, CoreEconomy, CoreError, @@ -273,11 +275,12 @@ pub fn handle_pack_open(body: &[u8], deps: &StoreDeps<'_>, rng: &mut impl Rng) - Ok(e) => e, Err(_) => return error_response(503, "core_unavailable"), }; - // The unopened pack instance is an entitlement whose definition id is the - // pack id. Absent → already consumed / never granted: honest empty reveal. + // The unopened pack instance is an entitlement whose definition id resolves + // to this owned-only pack id (a numeric id or a symbolic reward-pack name). + // Absent → already consumed / never granted: honest empty reveal. let ent = match ents .into_iter() - .find(|e| e.definition_id.parse::().ok() == Some(pid)) + .find(|e| owned_pack_id_for_definition(&e.definition_id) == Some(pid)) { Some(e) => e, None => return json_response(&json!({ "itemData": [] })), @@ -890,6 +893,31 @@ mod tests { assert_eq!(redeemed[0].1.len(), 11); // pack 70 count } + #[test] + fn open_symbolic_silver_reward_redeems_entitlement_without_debit() { + // A Core reward grant ("silver_pack") resolves to owned-only pack 72 and + // opens for free by consuming its entitlement — the SBC reward-pack fix. + let econ = RecEcon::with_entitlements(4600, &["silver_pack"]); + let pool = pool(); + let assets = FakeAssets::for_pool(&pool); + let ent = Fifa17Entities::default(); + let deps = store_deps(&econ, &assets, &ent, &pool); + let resp = handle_pack_open(&body(json!({ "packId": 72 })), &deps, &mut rng(12)); + assert_eq!(resp.status, 200); + let b: Value = serde_json::from_slice(&resp.body).unwrap(); + assert_eq!(b["packId"], 72); + assert_eq!(b["purchasePackType"], "SILVER"); + assert_eq!(econ.coins(), 4600, "a reward pack opens for free"); + assert!( + econ.entitlements.lock().is_empty(), + "the reward entitlement is consumed once" + ); + let redeemed = econ.redeemed.lock(); + assert_eq!(redeemed.len(), 1); + assert_eq!(redeemed[0].0, "e0"); + assert_eq!(redeemed[0].1.len(), 12); // 1 bronze + 11 silver + } + #[test] fn open_owned_70_twice_is_consume_once() { let econ = RecEcon::with_entitlements(4600, &["70"]); diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index 70932fe..edc0f3e 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -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, }; @@ -2216,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 { ents.iter() - .filter_map(|e| e.definition_id.parse::().ok()) + .filter_map(|e| owned_pack_id_for_definition(&e.definition_id)) .collect() }