feat(fifa17): route SBCs through atomic Rust Core
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod item;
|
||||
pub mod non_economy;
|
||||
pub mod owned_query;
|
||||
pub mod pack_content;
|
||||
pub mod sbc;
|
||||
pub mod squad;
|
||||
pub mod squad_ext;
|
||||
pub mod squad_projection;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
//! FIFA 17 Squad Building Challenge wire shapes.
|
||||
//!
|
||||
//! Numeric category/set/challenge ids and container types mirror the reversed FIFA 17
|
||||
//! `/sbs/*` family. Core ids remain opaque strings and are mapped here, never in Core.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const CATEGORY_ID: i64 = 1;
|
||||
pub const CATEGORY_NAME: &str = "Foundations";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ChallengeIdentity {
|
||||
pub core_id: &'static str,
|
||||
pub set_id: i64,
|
||||
pub challenge_id: i64,
|
||||
pub priority: i64,
|
||||
}
|
||||
|
||||
pub const CHALLENGES: [ChallengeIdentity; 2] = [
|
||||
ChallengeIdentity {
|
||||
core_id: "sbc_bronze_upgrade",
|
||||
set_id: 1,
|
||||
challenge_id: 101,
|
||||
priority: 1,
|
||||
},
|
||||
ChallengeIdentity {
|
||||
core_id: "sbc_hybrid_nations",
|
||||
set_id: 2,
|
||||
challenge_id: 201,
|
||||
priority: 2,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ChallengeView {
|
||||
pub identity: ChallengeIdentity,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub repeatable: bool,
|
||||
pub times_completed: i64,
|
||||
}
|
||||
|
||||
pub fn identity_for_core(core_id: &str) -> Option<ChallengeIdentity> {
|
||||
CHALLENGES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|entry| entry.core_id == core_id)
|
||||
}
|
||||
|
||||
pub fn identity_for_challenge(challenge_id: i64) -> Option<ChallengeIdentity> {
|
||||
CHALLENGES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|entry| entry.challenge_id == challenge_id)
|
||||
}
|
||||
|
||||
/// `GET sbs/sets`: object root; categories, sets and awards are always arrays.
|
||||
pub fn sets_body(challenges: &[ChallengeView]) -> Value {
|
||||
let sets: Vec<Value> = challenges
|
||||
.iter()
|
||||
.map(|challenge| {
|
||||
json!({
|
||||
"setId": challenge.identity.set_id,
|
||||
"categoryId": CATEGORY_ID,
|
||||
"name": challenge.name,
|
||||
"description": challenge.description,
|
||||
"priority": challenge.identity.priority,
|
||||
"challengesCount": 1,
|
||||
"challengesCompletedCount": i64::from(challenge.times_completed > 0),
|
||||
"awards": [],
|
||||
"hidden": false,
|
||||
"endTime": 4_102_444_800_i64
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({
|
||||
"categories": [{
|
||||
"categoryId": CATEGORY_ID,
|
||||
"name": CATEGORY_NAME,
|
||||
"priority": 1,
|
||||
"sets": sets
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
/// `GET sbs/setId/{id}/challenges`: object root with a challenge array.
|
||||
pub fn challenges_body(set_id: i64, challenges: &[ChallengeView]) -> Value {
|
||||
let records: Vec<Value> = challenges
|
||||
.iter()
|
||||
.filter(|challenge| challenge.identity.set_id == set_id)
|
||||
.map(|challenge| {
|
||||
json!({
|
||||
"challengeId": challenge.identity.challenge_id,
|
||||
"setId": challenge.identity.set_id,
|
||||
"categoryId": CATEGORY_ID,
|
||||
"index": 0,
|
||||
"type": "OPEN_CHALLENGE",
|
||||
"name": challenge.name,
|
||||
"description": challenge.description,
|
||||
"challengeImageId": "",
|
||||
"formation": "f442",
|
||||
"endTime": 0,
|
||||
"repeatable": challenge.repeatable,
|
||||
"trophyId": 0,
|
||||
"status": "OPEN",
|
||||
"timesCompleted": challenge.times_completed,
|
||||
"awards": [],
|
||||
"elgReq": []
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({ "challenges": records })
|
||||
}
|
||||
|
||||
/// Empty-body POST starts a challenge. `squad` is object-root on this response class.
|
||||
pub fn start_body(challenge_id: i64) -> Value {
|
||||
json!({
|
||||
"challengeId": challenge_id,
|
||||
"squad": {},
|
||||
"playerRequirements": []
|
||||
})
|
||||
}
|
||||
|
||||
/// GET challenge squad. The reversed response requires array containers.
|
||||
pub fn squad_body(challenge_id: i64, wire_item_ids: &[i64]) -> Value {
|
||||
let squad: Vec<Value> = wire_item_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, id)| {
|
||||
json!({
|
||||
"index": index,
|
||||
"itemData": { "id": id },
|
||||
"kitNumber": 0
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({
|
||||
"id": challenge_id,
|
||||
"squad": squad,
|
||||
"playerRequirements": []
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_body(challenge_id: i64) -> Value {
|
||||
json!({ "id": challenge_id })
|
||||
}
|
||||
|
||||
pub fn submit_body(challenge_id: i64, set_id: i64, credits: i64, unopened_packs: i64) -> Value {
|
||||
json!({
|
||||
"challengeId": challenge_id,
|
||||
"setId": set_id,
|
||||
"credits": credits,
|
||||
"preOrderPacks": 0,
|
||||
"recoveredPacks": unopened_packs,
|
||||
"grantedChallengeAwards": [],
|
||||
"grantedSetAwards": []
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum SbcWireError {
|
||||
Json(String),
|
||||
MissingSquad,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SbcWireError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Json(error) => write!(formatter, "invalid SBC squad JSON: {error}"),
|
||||
Self::MissingSquad => {
|
||||
formatter.write_str("SBC squad body contains no supported squad container")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SbcWireError {}
|
||||
|
||||
/// Extract FIFA wire item ids from a saved/submitted challenge squad.
|
||||
///
|
||||
/// Retail request captures for this body are unavailable. The parser therefore accepts
|
||||
/// only the two already-proven FIFA 17 squad containers: a normal `players` array or the
|
||||
/// SBC `squad` array. Within either, only `itemData.id` is interpreted.
|
||||
pub fn parse_wire_item_ids(body: &[u8]) -> Result<Vec<i64>, SbcWireError> {
|
||||
let root: Value =
|
||||
serde_json::from_slice(body).map_err(|error| SbcWireError::Json(error.to_string()))?;
|
||||
let entries = root
|
||||
.get("players")
|
||||
.and_then(Value::as_array)
|
||||
.or_else(|| root.get("squad").and_then(Value::as_array))
|
||||
.ok_or(SbcWireError::MissingSquad)?;
|
||||
|
||||
let mut ids = Vec::new();
|
||||
for entry in entries {
|
||||
if let Some(id) = entry
|
||||
.get("itemData")
|
||||
.and_then(|item| item.get("id"))
|
||||
.and_then(Value::as_i64)
|
||||
.filter(|id| *id != 0)
|
||||
{
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn challenge() -> ChallengeView {
|
||||
ChallengeView {
|
||||
identity: CHALLENGES[0],
|
||||
name: "Bronze Upgrade".into(),
|
||||
description: "Submit players".into(),
|
||||
repeatable: true,
|
||||
times_completed: 2,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_containers_match_reversed_fifa17_shapes() {
|
||||
let sets = sets_body(&[challenge()]);
|
||||
assert!(sets.is_object());
|
||||
assert!(sets["categories"].is_array());
|
||||
assert!(sets["categories"][0]["sets"].is_array());
|
||||
assert!(sets["categories"][0]["sets"][0]["awards"].is_array());
|
||||
|
||||
let challenges = challenges_body(1, &[challenge()]);
|
||||
assert!(challenges.is_object());
|
||||
assert!(challenges["challenges"].is_array());
|
||||
assert!(challenges["challenges"][0]["awards"].is_array());
|
||||
assert!(challenges["challenges"][0]["elgReq"].is_array());
|
||||
|
||||
assert!(start_body(101)["squad"].is_object());
|
||||
assert!(start_body(101)["playerRequirements"].is_array());
|
||||
assert!(squad_body(101, &[100_000_001])["squad"].is_array());
|
||||
let submit = submit_body(101, 1, 1234, 1);
|
||||
assert!(submit["grantedChallengeAwards"].is_array());
|
||||
assert!(submit["grantedSetAwards"].is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_accepts_only_known_squad_containers_and_item_ids() {
|
||||
let normal = br#"{"players":[{"index":0,"itemData":{"id":100000001}},{"index":1,"itemData":{"id":0}}]}"#;
|
||||
assert_eq!(parse_wire_item_ids(normal).unwrap(), [100_000_001]);
|
||||
let sbc = br#"{"squad":[{"itemData":{"id":100000002}}]}"#;
|
||||
assert_eq!(parse_wire_item_ids(sbc).unwrap(), [100_000_002]);
|
||||
assert_eq!(
|
||||
parse_wire_item_ids(br#"{"challengeId":101}"#),
|
||||
Err(SbcWireError::MissingSquad)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user