From 41494bd18f5def6b02b4112a9501c4817dfa324f Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 13 Aug 2026 18:16:47 +0000 Subject: [PATCH] feat(fifa17): port Store pack catalog + purchasegroup wire model (oracle parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds openfut-adapter-fifa17 fut::store_catalog — a pure, faithful Rust port of the Python oracle's PACK_CATALOG + _pack_body + store_catalog assembly at production flag defaults (FUT_STORE_DISPLAYGROUP=1, GROUPID=0, PRICE_PROBE=0): - PackDef + PACK_CATALOG (ids 1/5/6/7/70; economy numbers are OpenFUT PLACEHOLDER, wire shape is oracle-verified; 65534 deliberately absent). - pack_body() (_pack_body port), sentinel_body() (id-65534 compatibility shim), build_purchasegroup(unopened_ids, StoreMode) mirroring store_catalog(3627). - Differential parity: fixtures generated from the Python oracle (tests/fixtures/purchasegroup_{zero_sentinel,zero_clean,pack70}.json); Rust output matches semantically (6 tests). Adapter 140 tests, host 24, fmt/clippy clean, Python A-R oracle green. PURE wire shaping — NOT wired into the live host. Serving purchasegroup from Rust requires an authoritative Rust owner of unopenedPackIds, which is blocked on the economy-authority prerequisite (R3): coins are one shared balance written by many Python-oracle routes (BUY spend, quick-sell credit, SBC/match/objective rewards) persisted to fut_profile.json, so no single coin-touching route can move without a whole-cluster migration. No dual-write introduced; no production deployment. --- openfut-adapter-fifa17/src/fut/mod.rs | 1 + .../src/fut/store_catalog.rs | 268 ++++++++++++++++++ .../tests/fixtures/purchasegroup_pack70.json | 199 +++++++++++++ .../fixtures/purchasegroup_zero_clean.json | 173 +++++++++++ .../fixtures/purchasegroup_zero_sentinel.json | 199 +++++++++++++ 5 files changed, 840 insertions(+) create mode 100644 openfut-adapter-fifa17/src/fut/store_catalog.rs create mode 100644 openfut-adapter-fifa17/tests/fixtures/purchasegroup_pack70.json create mode 100644 openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_clean.json create mode 100644 openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_sentinel.json diff --git a/openfut-adapter-fifa17/src/fut/mod.rs b/openfut-adapter-fifa17/src/fut/mod.rs index abdb3fd..89309b7 100644 --- a/openfut-adapter-fifa17/src/fut/mod.rs +++ b/openfut-adapter-fifa17/src/fut/mod.rs @@ -12,4 +12,5 @@ pub mod owned_query; pub mod squad; pub mod squad_ext; pub mod squad_projection; +pub mod store_catalog; pub mod store_session; diff --git a/openfut-adapter-fifa17/src/fut/store_catalog.rs b/openfut-adapter-fifa17/src/fut/store_catalog.rs new file mode 100644 index 0000000..86fc53d --- /dev/null +++ b/openfut-adapter-fifa17/src/fut/store_catalog.rs @@ -0,0 +1,268 @@ +//! FIFA 17 Store pack catalogue + `/store/purchasegroup` wire shaping. +//! +//! A faithful Rust port of the Python oracle's `PACK_CATALOG` + `_pack_body` + +//! `store_catalog` assembly (`fifa17-recon/tools/{fut_store,utas_server}.py`) at the +//! **production flag defaults** (`FUT_STORE_DISPLAYGROUP=1` on, `FUT_STORE_GROUPID=0` +//! off, `FUT_PRICE_PROBE=0` off). Parity is pinned by differential fixtures generated +//! from the Python oracle (`tests/fixtures/purchasegroup_*.json`). +//! +//! ## Scope / split-brain safety +//! +//! This is **pure wire shaping** — no economy state, no IO. [`build_purchasegroup`] +//! is a function of `(owned unopened pack ids, empty-My-Packs StoreMode)`. It is +//! deliberately **not yet wired** into the live host: serving purchasegroup from Rust +//! requires an authoritative Rust owner of `unopenedPackIds`, and today Python is the +//! single writer of coins + unopened packs (BUY, quick-sell, rewards). Wiring this +//! before that economy authority exists would create a dual-write/split-brain. See +//! the R3 economy-authority prerequisite in the vault (`Rust UTAS Migration`). +//! +//! ## Economy-parameter provenance +//! +//! Prices, counts and odds are the current OpenFUT **PLACEHOLDER** economy, NOT +//! EA-authentic (the overnight audit established the store economy is invented). The +//! wire *shape* is EA-observed/oracle-verified; the *numbers* are placeholders. + +use serde_json::{json, Value}; + +use crate::fut::store_session::{StoreMode, SENTINEL_PACK_ID}; + +/// A FIFA 17 Store pack definition. Wire shape is oracle-verified; the economy +/// numbers (`price`/`count`/`special_chance`) are OpenFUT PLACEHOLDER, not EA-authentic. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PackDef { + pub id: u64, + pub name: &'static str, + pub price: u64, + pub count: u64, + pub gold: bool, + pub special_chance: f64, + /// Reward-only pack (no purchase path): excluded from the normal catalogue, + /// rendered only when owned (in `unopenedPackIds`). + pub owned_only: bool, +} + +/// The current supported FIFA 17 pack catalogue (`fut_store.py:820`). Only observed/ +/// currently-supported ids. The 65534 sentinel is deliberately ABSENT — it is a +/// compatibility shim, never a catalogue pack (never purchasable/openable). +pub const PACK_CATALOG: &[PackDef] = &[ + PackDef { + id: 1, + name: "Bronze Pack", + price: 400, + count: 5, + gold: false, + special_chance: 0.005, + owned_only: false, + }, + PackDef { + id: 5, + name: "Gold Pack", + price: 5000, + count: 7, + gold: true, + special_chance: 0.03, + owned_only: false, + }, + PackDef { + id: 6, + name: "Premium Gold", + price: 15000, + count: 11, + gold: true, + special_chance: 0.08, + owned_only: false, + }, + PackDef { + id: 7, + name: "Special Players Pack", + price: 25000, + count: 11, + gold: true, + special_chance: 1.0, + owned_only: false, + }, + PackDef { + id: 70, + name: "Reward Special Players Pack", + price: 0, + count: 11, + gold: true, + special_chance: 1.0, + owned_only: true, + }, +]; + +/// Look up a catalogue pack by id (the 65534 sentinel is never present). +pub fn pack_by_id(id: u64) -> Option<&'static PackDef> { + PACK_CATALOG.iter().find(|p| p.id == id) +} + +/// The FIFA17 StoreFront category token for a NORMAL pack tile (`utas_server.py:3579`): +/// one of the six hard-coded tokens the client resolves. +fn category(p: &PackDef) -> &'static str { + if p.special_chance >= 1.0 { + "special" + } else if p.gold { + "gold" + } else { + "bronze" + } +} + +/// One `purchase[]` entry — the faithful `_pack_body` port (`utas_server.py:3474`) at +/// production flag defaults. `owned` packs (My Packs / reward / sentinel) drop the +/// purchase fields and take the `mypacks` display group. +pub fn pack_body(p: &PackDef, idx: u64, owned: bool) -> Value { + let mtx = std::cmp::max(1, p.price / 100); + let mut body = json!({ + "assetId": p.id, + "id": p.id, + "packType": if p.gold { "GOLD" } else { "BRONZE" }, + "description": p.name, + "state": "active", + "saleType": "promo", + "limitType": "NONE", + "quantity": 0, + "purchaseLimit": 0, + "purchaseCount": 0, + "isPremium": false, + "sortPriority": idx, + "currencies": [{ "name": "coins", "funds": p.price, "finalFunds": p.price }], + "extPrice": { + "finalPrice": { "amount": mtx, "currency": "mtx" }, + "originalPrice": { "amount": mtx, "currency": "mtx" }, + }, + "packContentInfo": { + "bronzeQuantity": if p.gold { 0 } else { p.count }, + "silverQuantity": 0, + "goldQuantity": if p.gold { p.count } else { 0 }, + "rareQuantity": if p.gold { p.count } else { 0 }, + "itemQuantity": p.count, + }, + "unopened": owned, + }); + let obj = body.as_object_mut().expect("pack body is a JSON object"); + if owned { + // Reward/My-Packs tiles have no purchase path; leaving zero-value coin/mtx + // objects makes the client render the price label as literal "undefined". + obj.remove("currencies"); + obj.remove("extPrice"); + obj.insert( + "displayGroup".into(), + json!({ "value": "mypacks", "priority": idx }), + ); + } else { + obj.insert("displayGroup".into(), json!({ "value": category(p) })); + } + body +} + +/// The synthetic empty-My-Packs sentinel `purchase[]` entry (id 65534): an owned-style +/// body forced to `state:"active"`, `unopened:false`. Compatibility shim ONLY — 65534 +/// is absent from [`PACK_CATALOG`], so it can never be bought/opened/granted. +pub fn sentinel_body(idx: u64) -> Value { + let sentinel = PackDef { + id: SENTINEL_PACK_ID, + name: "", + price: 0, + count: 0, + gold: true, + special_chance: 0.0, + owned_only: true, + }; + let mut body = pack_body(&sentinel, idx, true); + let obj = body + .as_object_mut() + .expect("sentinel body is a JSON object"); + obj.insert("state".into(), json!("active")); + obj.insert("unopened".into(), json!(false)); + body +} + +/// Build the full `/store/purchasegroup` body from the authoritative unopened-pack ids +/// and the frozen empty-My-Packs mode. Pure — mirrors `store_catalog` (`3627`): +/// normal packs (1,5,6,7) first, then any owned packs, then the empty-My-Packs shim +/// (sentinel for [`StoreMode::Sentinel`], nothing for [`StoreMode::CleanV1`]). +pub fn build_purchasegroup(unopened_ids: &[u64], mode: StoreMode) -> Value { + let mut packs: Vec = PACK_CATALOG + .iter() + .filter(|p| !p.owned_only) + .enumerate() + .map(|(i, p)| pack_body(p, i as u64 + 1, false)) + .collect(); + for (i, &pid) in unopened_ids.iter().enumerate() { + if let Some(owned) = pack_by_id(pid) { + packs.push(pack_body(owned, i as u64 + 1, true)); + } + } + if unopened_ids.is_empty() && mode == StoreMode::Sentinel { + packs.push(sentinel_body(1)); + } + json!({ "purchase": packs, "timestamp": 1596326400i64 }) +} + +#[cfg(test)] +mod tests { + //! Differential parity against the Python oracle. The fixtures under + //! `tests/fixtures/purchasegroup_*.json` are generated by calling the oracle's + //! `_pack_body`/`store_catalog` at production flag defaults; Rust must match + //! them semantically (object key order is irrelevant to `serde_json::Value` eq). + use super::*; + + fn parse(s: &str) -> Value { + serde_json::from_str(s).expect("fixture parses") + } + + #[test] + fn purchasegroup_zero_sentinel_matches_oracle() { + let got = build_purchasegroup(&[], StoreMode::Sentinel); + let want = parse(include_str!( + "../../tests/fixtures/purchasegroup_zero_sentinel.json" + )); + assert_eq!(got, want); + } + + #[test] + fn purchasegroup_zero_clean_matches_oracle() { + let got = build_purchasegroup(&[], StoreMode::CleanV1); + let want = parse(include_str!( + "../../tests/fixtures/purchasegroup_zero_clean.json" + )); + assert_eq!(got, want); + } + + #[test] + fn purchasegroup_pack70_matches_oracle() { + // Owned pack present -> no sentinel regardless of mode. + let got = build_purchasegroup(&[70], StoreMode::Sentinel); + let want = parse(include_str!( + "../../tests/fixtures/purchasegroup_pack70.json" + )); + assert_eq!(got, want); + } + + #[test] + fn sentinel_absent_from_catalog() { + assert!(pack_by_id(SENTINEL_PACK_ID).is_none()); + assert!(PACK_CATALOG.iter().all(|p| p.id != SENTINEL_PACK_ID)); + } + + #[test] + fn clean_v1_empty_emits_no_mypacks_group() { + let got = build_purchasegroup(&[], StoreMode::CleanV1); + let ids: Vec = got["purchase"] + .as_array() + .unwrap() + .iter() + .map(|e| e["id"].as_u64().unwrap()) + .collect(); + assert_eq!(ids, vec![1, 5, 6, 7]); + } + + #[test] + fn category_tokens_are_canonical() { + assert_eq!(category(pack_by_id(1).unwrap()), "bronze"); + assert_eq!(category(pack_by_id(5).unwrap()), "gold"); + assert_eq!(category(pack_by_id(7).unwrap()), "special"); + } +} diff --git a/openfut-adapter-fifa17/tests/fixtures/purchasegroup_pack70.json b/openfut-adapter-fifa17/tests/fixtures/purchasegroup_pack70.json new file mode 100644 index 0000000..26288ad --- /dev/null +++ b/openfut-adapter-fifa17/tests/fixtures/purchasegroup_pack70.json @@ -0,0 +1,199 @@ +{ + "purchase": [ + { + "assetId": 1, + "currencies": [ + { + "finalFunds": 400, + "funds": 400, + "name": "coins" + } + ], + "description": "Bronze Pack", + "displayGroup": { + "value": "bronze" + }, + "extPrice": { + "finalPrice": { + "amount": 4, + "currency": "mtx" + }, + "originalPrice": { + "amount": 4, + "currency": "mtx" + } + }, + "id": 1, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 5, + "goldQuantity": 0, + "itemQuantity": 5, + "rareQuantity": 0, + "silverQuantity": 0 + }, + "packType": "BRONZE", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 1, + "state": "active", + "unopened": false + }, + { + "assetId": 5, + "currencies": [ + { + "finalFunds": 5000, + "funds": 5000, + "name": "coins" + } + ], + "description": "Gold Pack", + "displayGroup": { + "value": "gold" + }, + "extPrice": { + "finalPrice": { + "amount": 50, + "currency": "mtx" + }, + "originalPrice": { + "amount": 50, + "currency": "mtx" + } + }, + "id": 5, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 7, + "itemQuantity": 7, + "rareQuantity": 7, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 2, + "state": "active", + "unopened": false + }, + { + "assetId": 6, + "currencies": [ + { + "finalFunds": 15000, + "funds": 15000, + "name": "coins" + } + ], + "description": "Premium Gold", + "displayGroup": { + "value": "gold" + }, + "extPrice": { + "finalPrice": { + "amount": 150, + "currency": "mtx" + }, + "originalPrice": { + "amount": 150, + "currency": "mtx" + } + }, + "id": 6, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 3, + "state": "active", + "unopened": false + }, + { + "assetId": 7, + "currencies": [ + { + "finalFunds": 25000, + "funds": 25000, + "name": "coins" + } + ], + "description": "Special Players Pack", + "displayGroup": { + "value": "special" + }, + "extPrice": { + "finalPrice": { + "amount": 250, + "currency": "mtx" + }, + "originalPrice": { + "amount": 250, + "currency": "mtx" + } + }, + "id": 7, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 4, + "state": "active", + "unopened": false + }, + { + "assetId": 70, + "description": "Reward Special Players Pack", + "displayGroup": { + "priority": 1, + "value": "mypacks" + }, + "id": 70, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 1, + "state": "active", + "unopened": true + } + ], + "timestamp": 1596326400 +} diff --git a/openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_clean.json b/openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_clean.json new file mode 100644 index 0000000..355df79 --- /dev/null +++ b/openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_clean.json @@ -0,0 +1,173 @@ +{ + "purchase": [ + { + "assetId": 1, + "currencies": [ + { + "finalFunds": 400, + "funds": 400, + "name": "coins" + } + ], + "description": "Bronze Pack", + "displayGroup": { + "value": "bronze" + }, + "extPrice": { + "finalPrice": { + "amount": 4, + "currency": "mtx" + }, + "originalPrice": { + "amount": 4, + "currency": "mtx" + } + }, + "id": 1, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 5, + "goldQuantity": 0, + "itemQuantity": 5, + "rareQuantity": 0, + "silverQuantity": 0 + }, + "packType": "BRONZE", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 1, + "state": "active", + "unopened": false + }, + { + "assetId": 5, + "currencies": [ + { + "finalFunds": 5000, + "funds": 5000, + "name": "coins" + } + ], + "description": "Gold Pack", + "displayGroup": { + "value": "gold" + }, + "extPrice": { + "finalPrice": { + "amount": 50, + "currency": "mtx" + }, + "originalPrice": { + "amount": 50, + "currency": "mtx" + } + }, + "id": 5, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 7, + "itemQuantity": 7, + "rareQuantity": 7, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 2, + "state": "active", + "unopened": false + }, + { + "assetId": 6, + "currencies": [ + { + "finalFunds": 15000, + "funds": 15000, + "name": "coins" + } + ], + "description": "Premium Gold", + "displayGroup": { + "value": "gold" + }, + "extPrice": { + "finalPrice": { + "amount": 150, + "currency": "mtx" + }, + "originalPrice": { + "amount": 150, + "currency": "mtx" + } + }, + "id": 6, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 3, + "state": "active", + "unopened": false + }, + { + "assetId": 7, + "currencies": [ + { + "finalFunds": 25000, + "funds": 25000, + "name": "coins" + } + ], + "description": "Special Players Pack", + "displayGroup": { + "value": "special" + }, + "extPrice": { + "finalPrice": { + "amount": 250, + "currency": "mtx" + }, + "originalPrice": { + "amount": 250, + "currency": "mtx" + } + }, + "id": 7, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 4, + "state": "active", + "unopened": false + } + ], + "timestamp": 1596326400 +} diff --git a/openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_sentinel.json b/openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_sentinel.json new file mode 100644 index 0000000..c85cc5a --- /dev/null +++ b/openfut-adapter-fifa17/tests/fixtures/purchasegroup_zero_sentinel.json @@ -0,0 +1,199 @@ +{ + "purchase": [ + { + "assetId": 1, + "currencies": [ + { + "finalFunds": 400, + "funds": 400, + "name": "coins" + } + ], + "description": "Bronze Pack", + "displayGroup": { + "value": "bronze" + }, + "extPrice": { + "finalPrice": { + "amount": 4, + "currency": "mtx" + }, + "originalPrice": { + "amount": 4, + "currency": "mtx" + } + }, + "id": 1, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 5, + "goldQuantity": 0, + "itemQuantity": 5, + "rareQuantity": 0, + "silverQuantity": 0 + }, + "packType": "BRONZE", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 1, + "state": "active", + "unopened": false + }, + { + "assetId": 5, + "currencies": [ + { + "finalFunds": 5000, + "funds": 5000, + "name": "coins" + } + ], + "description": "Gold Pack", + "displayGroup": { + "value": "gold" + }, + "extPrice": { + "finalPrice": { + "amount": 50, + "currency": "mtx" + }, + "originalPrice": { + "amount": 50, + "currency": "mtx" + } + }, + "id": 5, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 7, + "itemQuantity": 7, + "rareQuantity": 7, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 2, + "state": "active", + "unopened": false + }, + { + "assetId": 6, + "currencies": [ + { + "finalFunds": 15000, + "funds": 15000, + "name": "coins" + } + ], + "description": "Premium Gold", + "displayGroup": { + "value": "gold" + }, + "extPrice": { + "finalPrice": { + "amount": 150, + "currency": "mtx" + }, + "originalPrice": { + "amount": 150, + "currency": "mtx" + } + }, + "id": 6, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 3, + "state": "active", + "unopened": false + }, + { + "assetId": 7, + "currencies": [ + { + "finalFunds": 25000, + "funds": 25000, + "name": "coins" + } + ], + "description": "Special Players Pack", + "displayGroup": { + "value": "special" + }, + "extPrice": { + "finalPrice": { + "amount": 250, + "currency": "mtx" + }, + "originalPrice": { + "amount": 250, + "currency": "mtx" + } + }, + "id": 7, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 11, + "itemQuantity": 11, + "rareQuantity": 11, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 4, + "state": "active", + "unopened": false + }, + { + "assetId": 65534, + "description": "", + "displayGroup": { + "priority": 1, + "value": "mypacks" + }, + "id": 65534, + "isPremium": false, + "limitType": "NONE", + "packContentInfo": { + "bronzeQuantity": 0, + "goldQuantity": 0, + "itemQuantity": 0, + "rareQuantity": 0, + "silverQuantity": 0 + }, + "packType": "GOLD", + "purchaseCount": 0, + "purchaseLimit": 0, + "quantity": 0, + "saleType": "promo", + "sortPriority": 1, + "state": "active", + "unopened": false + } + ], + "timestamp": 1596326400 +}