103 lines
3.1 KiB
Rust
103 lines
3.1 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct Squad {
|
|
pub id: String,
|
|
pub club_id: String,
|
|
pub name: String,
|
|
pub formation: String,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
impl Squad {
|
|
pub fn new(
|
|
club_id: impl Into<String>,
|
|
name: impl Into<String>,
|
|
formation: impl Into<String>,
|
|
) -> Self {
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
Self {
|
|
id: Uuid::new_v4().to_string(),
|
|
club_id: club_id.into(),
|
|
name: name.into(),
|
|
formation: formation.into(),
|
|
created_at: now.clone(),
|
|
updated_at: now,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct SquadPlayer {
|
|
pub id: String,
|
|
pub squad_id: String,
|
|
pub owned_card_id: String,
|
|
pub position_index: i64,
|
|
pub is_captain: bool,
|
|
pub is_on_bench: bool,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SaveSquadRequest {
|
|
pub squad_id: Option<String>,
|
|
pub name: Option<String>,
|
|
pub formation: Option<String>,
|
|
pub players: Vec<SquadPlayerInput>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SquadPlayerInput {
|
|
pub owned_card_id: String,
|
|
pub position_index: i64,
|
|
pub is_captain: bool,
|
|
pub is_on_bench: bool,
|
|
}
|
|
|
|
/// A complete squad, as a game client sends it.
|
|
///
|
|
/// # Why replacement rather than edits
|
|
///
|
|
/// Retail FIFA 17 sends the WHOLE squad on every save — roughly 2 KB carrying
|
|
/// every slot, item id and kit number — and a user swapping two players
|
|
/// produced nine changed slots across two saves. Slot deltas therefore do not
|
|
/// describe what the user did, and any attempt to derive `swap_players` or
|
|
/// `move_player` from them would be inventing intent the wire never carried.
|
|
///
|
|
/// So the only honest semantic operation is: *this is the squad now*.
|
|
///
|
|
/// Empty slots are simply absent from `slots`; a client that models an empty
|
|
/// slot as a zero item id must drop it at the adapter boundary rather than
|
|
/// sending a player Core would have to special-case.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SquadReplacement {
|
|
pub name: Option<String>,
|
|
pub formation: Option<String>,
|
|
pub slots: Vec<SlotAssignment>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SlotAssignment {
|
|
pub owned_card_id: String,
|
|
/// Core's slot numbering. The adapter maps the game's numbering onto it.
|
|
pub slot: i64,
|
|
pub is_captain: bool,
|
|
pub is_on_bench: bool,
|
|
}
|
|
|
|
/// Outcome of a replacement.
|
|
///
|
|
/// Carries the server's own evaluation and, separately, any disagreement with
|
|
/// what the client claimed — never a merged value.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SquadReplaced {
|
|
pub squad: Squad,
|
|
pub slots_written: usize,
|
|
pub evaluation: crate::services::squad_rules::SquadEvaluation,
|
|
pub client_disagreements: Vec<crate::services::squad_rules::EvaluationComparison>,
|
|
/// Server-computed deterministic fingerprint of the committed canonical squad
|
|
/// (anchors any opaque game extension against stale projection).
|
|
pub canonical_fingerprint: String,
|
|
}
|