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:
funman300
2026-08-19 01:27:22 +00:00
parent f56aa613da
commit 1d6a6fffcc
3 changed files with 119 additions and 8 deletions
+32 -4
View File
@@ -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"]);
+9 -4
View File
@@ -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()
}