feat(engine): fetch daily-challenge seed from server on startup
- Add fetch_daily_challenge() to SyncProvider trait (default: Ok(None)) - SolitaireServerClient calls GET /api/daily-challenge (public endpoint) and returns the ChallengeGoal; non-2xx responses return Ok(None) so callers fall back to the local date-hash seed - DailyChallengePlugin spawns an async task on Startup (only when SyncProviderResource is present) and polls it in Update; on success it overwrites DailyChallengeResource.seed with the server's seed, ensuring all players worldwide get the same deal on a given date Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use solitaire_sync::{LeaderboardEntry, SyncPayload, SyncResponse};
|
||||
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
||||
use thiserror::Error;
|
||||
|
||||
/// All errors that can arise during sync operations.
|
||||
@@ -36,6 +36,11 @@ pub trait SyncProvider: Send + Sync {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Blanket impl so `Box<dyn SyncProvider + Send + Sync>` (returned by
|
||||
@@ -60,6 +65,9 @@ impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub mod stats;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! without matching on [`SyncBackend`] anywhere else in the codebase.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use solitaire_sync::{LeaderboardEntry, SyncPayload, SyncResponse};
|
||||
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
||||
|
||||
use crate::{
|
||||
auth_tokens::{load_access_token, load_refresh_token, store_tokens},
|
||||
@@ -200,6 +200,31 @@ impl SyncProvider for SolitaireServerClient {
|
||||
load_access_token(&self.username).is_ok()
|
||||
}
|
||||
|
||||
/// Fetch today's daily challenge from the server.
|
||||
///
|
||||
/// Does not require authentication — the endpoint is public. Returns `None`
|
||||
/// on any non-success HTTP status so the caller falls back to the local seed.
|
||||
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
||||
let url = format!("{}/api/daily-challenge", self.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| SyncError::Network(e.to_string()))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
let goal: ChallengeGoal = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| SyncError::Serialization(e.to_string()))?;
|
||||
Ok(Some(goal))
|
||||
} else {
|
||||
// Non-fatal — caller will use the locally computed seed instead.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
||||
let token = self.access_token()?;
|
||||
let url = format!("{}/api/leaderboard", self.base_url);
|
||||
|
||||
Reference in New Issue
Block a user