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::<u64>(), 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.
This commit is contained in:
@@ -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<u64> {
|
||||
if let Ok(numeric) = definition_id.parse::<u64>() {
|
||||
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);
|
||||
|
||||
@@ -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::<u64>().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"]);
|
||||
|
||||
@@ -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<u64> {
|
||||
ents.iter()
|
||||
.filter_map(|e| e.definition_id.parse::<u64>().ok())
|
||||
.filter_map(|e| owned_pack_id_for_definition(&e.definition_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user