diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index 4168a12..9e93fcf 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -4,6 +4,7 @@ use bevy::prelude::Message; use solitaire_core::game_state::GameMode; use solitaire_core::pile::PileType; use solitaire_data::AchievementRecord; +use solitaire_sync::SyncResponse; /// Request to move `count` cards from `from` to `to`. Fired by input systems, /// consumed by `GamePlugin`. @@ -78,6 +79,16 @@ pub struct AchievementUnlockedEvent(pub AchievementRecord); #[derive(Message, Debug, Clone, Copy, Default)] pub struct ManualSyncRequestEvent; +/// Fired by `SyncPlugin` after a pull task resolves and the merged result has +/// been persisted to disk. `Ok(SyncResponse)` carries the merged payload plus +/// any `ConflictReport`s the merge produced. `Err(String)` carries a +/// human-readable failure message (network, auth, serialization, etc.). +/// +/// UI systems listen for this to refresh views without polling +/// `SyncStatusResource`. See [ARCHITECTURE.md §4](../../ARCHITECTURE.md). +#[derive(Message, Debug, Clone)] +pub struct SyncCompleteEvent(pub Result); + /// Fired by `InputPlugin` when N is pressed while a game is in progress /// but confirmation has not yet been received. The animation plugin shows /// a "Press N again to confirm" toast. A second N press within the diff --git a/solitaire_engine/src/lib.rs b/solitaire_engine/src/lib.rs index 79859eb..840aebd 100644 --- a/solitaire_engine/src/lib.rs +++ b/solitaire_engine/src/lib.rs @@ -69,7 +69,7 @@ pub use events::{ AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent, ForfeitEvent, GameWonEvent, HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, - StateChangedEvent, UndoRequestEvent, XpAwardedEvent, + StateChangedEvent, SyncCompleteEvent, UndoRequestEvent, XpAwardedEvent, }; pub use game_plugin::{ConfirmNewGameScreen, GameMutation, GameOverScreen, GamePlugin, GameStatePath}; pub use help_plugin::{HelpPlugin, HelpScreen}; diff --git a/solitaire_engine/src/sync_plugin.rs b/solitaire_engine/src/sync_plugin.rs index ec462eb..8d770b4 100644 --- a/solitaire_engine/src/sync_plugin.rs +++ b/solitaire_engine/src/sync_plugin.rs @@ -22,10 +22,10 @@ use solitaire_data::{ save_achievements_to, save_progress_to, save_stats_to, AchievementRecord, PlayerProgress, StatsSnapshot, SyncError, SyncProvider, }; -use solitaire_sync::{merge, SyncPayload}; +use solitaire_sync::{merge, SyncPayload, SyncResponse}; use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath}; -use crate::events::ManualSyncRequestEvent; +use crate::events::{ManualSyncRequestEvent, SyncCompleteEvent}; use crate::progress_plugin::{ProgressResource, ProgressStoragePath}; use crate::resources::{SyncStatus, SyncStatusResource}; use crate::stats_plugin::{StatsResource, StatsStoragePath}; @@ -94,6 +94,7 @@ impl Plugin for SyncPlugin { .init_resource::() .init_resource::() .add_message::() + .add_message::() .add_systems(Startup, start_pull) .add_systems(Update, (poll_pull_result, handle_manual_sync_request)) .add_systems(Last, push_on_exit); @@ -161,6 +162,7 @@ fn poll_pull_result( achievements_path: Res, mut progress: ResMut, progress_path: Res, + mut complete_writer: MessageWriter, ) { let Some(task) = task_res.0.as_mut() else { return; @@ -173,7 +175,7 @@ fn poll_pull_result( match result { Ok(remote) => { let local = build_payload(&stats.0, &achievements.0, &progress.0); - let (merged, _conflicts) = merge(&local, &remote); + let (merged, conflicts) = merge(&local, &remote); // Persist merged state atomically. if let Some(p) = &stats_path.0 @@ -190,10 +192,17 @@ fn poll_pull_result( } // Update in-world resources. - stats.0 = merged.stats; - achievements.0 = merged.achievements; - progress.0 = merged.progress; - status.0 = SyncStatus::LastSynced(Utc::now()); + let now = Utc::now(); + stats.0 = merged.stats.clone(); + achievements.0 = merged.achievements.clone(); + progress.0 = merged.progress.clone(); + status.0 = SyncStatus::LastSynced(now); + + complete_writer.write(SyncCompleteEvent(Ok(SyncResponse { + merged, + server_time: now, + conflicts, + }))); } Err(SyncError::UnsupportedPlatform) => { // No backend configured — not an error, just leave status as Idle. @@ -207,7 +216,8 @@ fn poll_pull_result( SyncError::Serialization(_) => format!("Unexpected server response: {e}"), SyncError::UnsupportedPlatform => unreachable!("handled above"), }; - status.0 = SyncStatus::Error(msg); + status.0 = SyncStatus::Error(msg.clone()); + complete_writer.write(SyncCompleteEvent(Err(msg))); } } }