feat(fifa17): port Store pack catalog + purchasegroup wire model (oracle parity)

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.
This commit is contained in:
funman300
2026-08-13 18:16:47 +00:00
parent 40ebf7c1e7
commit 41494bd18f
5 changed files with 840 additions and 0 deletions
+1
View File
@@ -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;
@@ -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<Value> = 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<u64> = 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");
}
}
@@ -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
}
@@ -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
}
@@ -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
}