b26200f948
Test / test (pull_request) Failing after 18s
Three items the multi-agent sweep flagged, now removed with user approval (§8 for the solitaire_sync changes): - WinCascadePlugin: never registered; handle_win_cascade in AnimationPlugin is the live win cascade and builds its own targets. Its now-orphaned helpers (win_scatter_targets, cascade_delay, WIN_CASCADE_INTERVAL_SECS) had no callers outside their own tests and go with it. - SyncCompleteEvent: written by the pull-completion system, zero readers — UI reads SyncStatusResource instead. - solitaire_sync::ApiError: unused by client and server; the merge_at crate-root re-export goes too (merge::merge_at stays for the merge module's own use). ARCHITECTURE.md updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
4.0 KiB
Rust
101 lines
4.0 KiB
Rust
//! Shared API types and merge logic for Ferrous Solitaire.
|
|
//!
|
|
//! This crate is the contract between the game client (`solitaire_data`) and
|
|
//! the sync server (`solitaire_server`). Changing any public type here is a
|
|
//! breaking change on both sides — version carefully.
|
|
//!
|
|
//! **No Bevy. No network. No file I/O.** Only `serde`, `uuid`, and `chrono`.
|
|
|
|
pub mod achievements;
|
|
pub mod merge;
|
|
pub mod progress;
|
|
pub mod stats;
|
|
pub mod theme_store;
|
|
|
|
pub use achievements::AchievementRecord;
|
|
pub use merge::merge;
|
|
pub use progress::{PlayerProgress, level_for_xp};
|
|
pub use stats::StatsSnapshot;
|
|
pub use theme_store::{ThemeCatalogEntry, ThemeCatalogResponse};
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sync wire types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Full sync payload sent from the client to the server and returned after
|
|
/// server-side merge. Contains all data needed to reconcile two instances.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SyncPayload {
|
|
/// Identifies the owning player. Must match the authenticated user.
|
|
pub user_id: Uuid,
|
|
/// Cumulative game statistics.
|
|
pub stats: StatsSnapshot,
|
|
/// Per-achievement unlock records.
|
|
pub achievements: Vec<AchievementRecord>,
|
|
/// XP, level, cosmetic unlocks, and daily/weekly progress.
|
|
pub progress: PlayerProgress,
|
|
/// Wall-clock time of the last local modification.
|
|
pub last_modified: DateTime<Utc>,
|
|
}
|
|
|
|
/// Response returned by the sync server after a pull or push operation.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SyncResponse {
|
|
/// The merged payload that the client should save locally.
|
|
pub merged: SyncPayload,
|
|
/// The server's current wall-clock time (useful for clock-skew detection).
|
|
pub server_time: DateTime<Utc>,
|
|
/// Fields where local and remote values differed and could not be merged
|
|
/// deterministically. Returned for display purposes — data is never
|
|
/// silently discarded.
|
|
pub conflicts: Vec<ConflictReport>,
|
|
}
|
|
|
|
/// Describes a single field where local and remote values diverged in a way
|
|
/// that the merge function could not resolve automatically.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ConflictReport {
|
|
/// Dot-separated field path, e.g. `"win_streak_current"`.
|
|
pub field: String,
|
|
/// Human-readable representation of the local value.
|
|
pub local_value: String,
|
|
/// Human-readable representation of the remote value.
|
|
pub remote_value: String,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Daily challenge / leaderboard types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Describes today's daily challenge, returned by `GET /api/daily-challenge`.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ChallengeGoal {
|
|
/// Date this challenge applies to, formatted as `"YYYY-MM-DD"`.
|
|
pub date: String,
|
|
/// Deterministic RNG seed for this date's deal — identical for all players.
|
|
pub seed: u64,
|
|
/// Human-readable description of the goal, e.g. "Win in under 5 minutes".
|
|
pub description: String,
|
|
/// Optional target score required to complete the challenge.
|
|
pub target_score: Option<i32>,
|
|
/// Optional maximum allowed time in seconds to complete the challenge.
|
|
pub max_time_secs: Option<u64>,
|
|
}
|
|
|
|
/// A single row from the server leaderboard, returned by `GET /api/leaderboard`.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct LeaderboardEntry {
|
|
/// Display name chosen by the player at opt-in time.
|
|
pub display_name: String,
|
|
/// The player's best single-game score.
|
|
pub best_score: Option<i32>,
|
|
/// The player's fastest win time in seconds.
|
|
pub best_time_secs: Option<u64>,
|
|
/// When this entry was last recorded.
|
|
pub recorded_at: DateTime<Utc>,
|
|
}
|