549a817bb1
The method had a no-op default, was never overridden in SolitaireServerClient, and was never called by any engine system. Achievements are already synced via the full SyncPayload push, so the method provided no additional value and was a dead maintenance trap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
6.6 KiB
Rust
168 lines
6.6 KiB
Rust
use async_trait::async_trait;
|
|
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
|
use thiserror::Error;
|
|
|
|
/// All errors that can arise during sync operations.
|
|
#[derive(Debug, Error)]
|
|
pub enum SyncError {
|
|
#[error("unsupported platform for this sync backend")]
|
|
UnsupportedPlatform,
|
|
#[error("network error: {0}")]
|
|
Network(String),
|
|
#[error("authentication error: {0}")]
|
|
Auth(String),
|
|
#[error("serialization error: {0}")]
|
|
Serialization(String),
|
|
}
|
|
|
|
/// Every sync backend implements this trait. The SyncPlugin only calls these
|
|
/// methods — it never matches on a backend enum variant.
|
|
#[async_trait]
|
|
pub trait SyncProvider: Send + Sync {
|
|
/// Fetch the remote sync payload. Returns the latest server state for merging.
|
|
async fn pull(&self) -> Result<SyncPayload, SyncError>;
|
|
/// Push the local payload to the backend. Returns the merged server response.
|
|
async fn push(&self, payload: &SyncPayload) -> Result<SyncResponse, SyncError>;
|
|
/// Human-readable name of this backend, used in settings UI and logs.
|
|
fn backend_name(&self) -> &'static str;
|
|
/// Returns true if the user is currently authenticated with this backend.
|
|
fn is_authenticated(&self) -> bool;
|
|
/// Fetch the global leaderboard from this backend. Returns an empty list
|
|
/// for backends that do not support leaderboards (e.g. `LocalOnlyProvider`).
|
|
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
|
Ok(vec![])
|
|
}
|
|
/// Fetch today's daily challenge from the server. Returns `None` for
|
|
/// backends that don't support it, or on any non-fatal network failure.
|
|
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
|
Ok(None)
|
|
}
|
|
/// Opt the authenticated player into the leaderboard with the given
|
|
/// display name. No-op for backends that don't support leaderboards.
|
|
async fn opt_in_leaderboard(&self, _display_name: &str) -> Result<(), SyncError> {
|
|
Ok(())
|
|
}
|
|
/// Remove the authenticated player from the leaderboard.
|
|
/// No-op for backends that don't support leaderboards.
|
|
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
|
|
Ok(())
|
|
}
|
|
/// Permanently delete the authenticated player's account and all server
|
|
/// data. No-op for backends that don't support account management.
|
|
async fn delete_account(&self) -> Result<(), SyncError> {
|
|
Ok(())
|
|
}
|
|
/// Upload a winning replay to the backend. On success, returns the
|
|
/// shareable web URL the player can copy to their clipboard
|
|
/// (`<server>/replays/<id>`). Default returns `UnsupportedPlatform`
|
|
/// so backends without a server (e.g. `LocalOnlyProvider`) are
|
|
/// silently no-op'd by the engine's push-on-win system, matching
|
|
/// the same pattern `pull` / `push` follow.
|
|
async fn push_replay(&self, _replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
|
Err(SyncError::UnsupportedPlatform)
|
|
}
|
|
}
|
|
|
|
/// Blanket impl so `Box<dyn SyncProvider + Send + Sync>` (returned by
|
|
/// `provider_for_backend`) can be passed directly to `SyncPlugin::new`.
|
|
#[async_trait]
|
|
impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
|
async fn pull(&self) -> Result<SyncPayload, SyncError> {
|
|
(**self).pull().await
|
|
}
|
|
async fn push(&self, payload: &SyncPayload) -> Result<SyncResponse, SyncError> {
|
|
(**self).push(payload).await
|
|
}
|
|
fn backend_name(&self) -> &'static str {
|
|
(**self).backend_name()
|
|
}
|
|
fn is_authenticated(&self) -> bool {
|
|
(**self).is_authenticated()
|
|
}
|
|
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
|
(**self).fetch_leaderboard().await
|
|
}
|
|
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
|
(**self).fetch_daily_challenge().await
|
|
}
|
|
async fn opt_in_leaderboard(&self, display_name: &str) -> Result<(), SyncError> {
|
|
(**self).opt_in_leaderboard(display_name).await
|
|
}
|
|
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
|
|
(**self).opt_out_leaderboard().await
|
|
}
|
|
async fn delete_account(&self) -> Result<(), SyncError> {
|
|
(**self).delete_account().await
|
|
}
|
|
async fn push_replay(&self, replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
|
(**self).push_replay(replay).await
|
|
}
|
|
}
|
|
|
|
pub mod stats;
|
|
pub use stats::{StatsExt, StatsSnapshot};
|
|
|
|
pub mod storage;
|
|
pub use storage::{
|
|
cleanup_orphaned_tmp_files, delete_game_state_at, delete_time_attack_session_at,
|
|
game_state_file_path, load_game_state_from, load_stats, load_stats_from,
|
|
load_time_attack_session_from, load_time_attack_session_from_at, save_game_state_to,
|
|
save_stats, save_stats_to, save_time_attack_session_to, stats_file_path,
|
|
time_attack_session_path, time_attack_session_with_now, TimeAttackSession,
|
|
};
|
|
|
|
pub mod achievements;
|
|
pub use achievements::{
|
|
achievements_file_path, load_achievements_from, save_achievements_to, AchievementRecord,
|
|
};
|
|
|
|
pub mod progress;
|
|
pub use progress::{
|
|
daily_seed_for, level_for_xp, load_progress_from, progress_file_path, save_progress_to,
|
|
xp_for_win, PlayerProgress,
|
|
};
|
|
|
|
pub mod weekly;
|
|
pub use weekly::{
|
|
current_iso_week_key, weekly_goal_by_id, WeeklyGoalContext, WeeklyGoalDef, WeeklyGoalKind,
|
|
WEEKLY_GOALS, WEEKLY_GOAL_XP,
|
|
};
|
|
|
|
pub mod challenge;
|
|
pub use challenge::{challenge_count, challenge_seed_for, CHALLENGE_SEEDS};
|
|
|
|
pub mod difficulty_seeds;
|
|
pub use difficulty_seeds::{seeds_for, DifficultySeeds};
|
|
|
|
pub mod settings;
|
|
pub use settings::{
|
|
load_settings_from, save_settings_to, settings_file_path, AnimSpeed, Settings, SyncBackend,
|
|
Theme, WindowGeometry, REPLAY_MOVE_INTERVAL_MAX_SECS, REPLAY_MOVE_INTERVAL_MIN_SECS,
|
|
REPLAY_MOVE_INTERVAL_STEP_SECS, SOLVER_DEAL_RETRY_CAP, TIME_BONUS_MULTIPLIER_MAX,
|
|
TIME_BONUS_MULTIPLIER_MIN, TIME_BONUS_MULTIPLIER_STEP, TOOLTIP_DELAY_MAX_SECS,
|
|
TOOLTIP_DELAY_MIN_SECS, TOOLTIP_DELAY_STEP_SECS,
|
|
};
|
|
|
|
#[cfg(target_os = "android")]
|
|
mod android_keystore;
|
|
|
|
pub mod auth_tokens;
|
|
pub use auth_tokens::{
|
|
delete_tokens, load_access_token, load_refresh_token, store_tokens, TokenError,
|
|
};
|
|
|
|
pub mod sync_client;
|
|
pub use sync_client::{provider_for_backend, LocalOnlyProvider, SolitaireServerClient};
|
|
|
|
pub mod replay;
|
|
#[allow(deprecated)]
|
|
pub use replay::{latest_replay_path, load_latest_replay_from, save_latest_replay_to};
|
|
pub use replay::{
|
|
append_replay_to_history, load_replay_history_from, migrate_legacy_latest_replay,
|
|
replay_history_path, save_replay_history_to, Replay, ReplayHistory, ReplayMove,
|
|
REPLAY_HISTORY_CAP, REPLAY_HISTORY_SCHEMA_VERSION, REPLAY_SCHEMA_VERSION,
|
|
};
|
|
|
|
pub mod platform;
|
|
pub use platform::data_dir;
|