feat(engine): emit SyncCompleteEvent on pull resolve
ARCHITECTURE.md §5 lists SyncCompleteEvent(Result<SyncResponse, String>) as a cross-system event, but it was never declared or fired. Add the message to events.rs, register it in SyncPlugin, and emit it from poll_pull_result on both the success path (carrying the merged payload + conflicts as SyncResponse) and the failure path (carrying the user-facing error message). UI/persistence systems can now react to sync completion without polling SyncStatusResource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ use bevy::prelude::Message;
|
|||||||
use solitaire_core::game_state::GameMode;
|
use solitaire_core::game_state::GameMode;
|
||||||
use solitaire_core::pile::PileType;
|
use solitaire_core::pile::PileType;
|
||||||
use solitaire_data::AchievementRecord;
|
use solitaire_data::AchievementRecord;
|
||||||
|
use solitaire_sync::SyncResponse;
|
||||||
|
|
||||||
/// Request to move `count` cards from `from` to `to`. Fired by input systems,
|
/// Request to move `count` cards from `from` to `to`. Fired by input systems,
|
||||||
/// consumed by `GamePlugin`.
|
/// consumed by `GamePlugin`.
|
||||||
@@ -78,6 +79,16 @@ pub struct AchievementUnlockedEvent(pub AchievementRecord);
|
|||||||
#[derive(Message, Debug, Clone, Copy, Default)]
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
pub struct ManualSyncRequestEvent;
|
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<SyncResponse, String>);
|
||||||
|
|
||||||
/// Fired by `InputPlugin` when N is pressed while a game is in progress
|
/// Fired by `InputPlugin` when N is pressed while a game is in progress
|
||||||
/// but confirmation has not yet been received. The animation plugin shows
|
/// but confirmation has not yet been received. The animation plugin shows
|
||||||
/// a "Press N again to confirm" toast. A second N press within the
|
/// a "Press N again to confirm" toast. A second N press within the
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ pub use events::{
|
|||||||
AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent,
|
AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent,
|
||||||
ForfeitEvent, GameWonEvent, HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent,
|
ForfeitEvent, GameWonEvent, HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent,
|
||||||
MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent,
|
MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent,
|
||||||
StateChangedEvent, UndoRequestEvent, XpAwardedEvent,
|
StateChangedEvent, SyncCompleteEvent, UndoRequestEvent, XpAwardedEvent,
|
||||||
};
|
};
|
||||||
pub use game_plugin::{ConfirmNewGameScreen, GameMutation, GameOverScreen, GamePlugin, GameStatePath};
|
pub use game_plugin::{ConfirmNewGameScreen, GameMutation, GameOverScreen, GamePlugin, GameStatePath};
|
||||||
pub use help_plugin::{HelpPlugin, HelpScreen};
|
pub use help_plugin::{HelpPlugin, HelpScreen};
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ use solitaire_data::{
|
|||||||
save_achievements_to, save_progress_to, save_stats_to, AchievementRecord, PlayerProgress,
|
save_achievements_to, save_progress_to, save_stats_to, AchievementRecord, PlayerProgress,
|
||||||
StatsSnapshot, SyncError, SyncProvider,
|
StatsSnapshot, SyncError, SyncProvider,
|
||||||
};
|
};
|
||||||
use solitaire_sync::{merge, SyncPayload};
|
use solitaire_sync::{merge, SyncPayload, SyncResponse};
|
||||||
|
|
||||||
use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath};
|
use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath};
|
||||||
use crate::events::ManualSyncRequestEvent;
|
use crate::events::{ManualSyncRequestEvent, SyncCompleteEvent};
|
||||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath};
|
use crate::progress_plugin::{ProgressResource, ProgressStoragePath};
|
||||||
use crate::resources::{SyncStatus, SyncStatusResource};
|
use crate::resources::{SyncStatus, SyncStatusResource};
|
||||||
use crate::stats_plugin::{StatsResource, StatsStoragePath};
|
use crate::stats_plugin::{StatsResource, StatsStoragePath};
|
||||||
@@ -94,6 +94,7 @@ impl Plugin for SyncPlugin {
|
|||||||
.init_resource::<PullTaskResult>()
|
.init_resource::<PullTaskResult>()
|
||||||
.init_resource::<PullTask>()
|
.init_resource::<PullTask>()
|
||||||
.add_message::<ManualSyncRequestEvent>()
|
.add_message::<ManualSyncRequestEvent>()
|
||||||
|
.add_message::<SyncCompleteEvent>()
|
||||||
.add_systems(Startup, start_pull)
|
.add_systems(Startup, start_pull)
|
||||||
.add_systems(Update, (poll_pull_result, handle_manual_sync_request))
|
.add_systems(Update, (poll_pull_result, handle_manual_sync_request))
|
||||||
.add_systems(Last, push_on_exit);
|
.add_systems(Last, push_on_exit);
|
||||||
@@ -161,6 +162,7 @@ fn poll_pull_result(
|
|||||||
achievements_path: Res<AchievementsStoragePath>,
|
achievements_path: Res<AchievementsStoragePath>,
|
||||||
mut progress: ResMut<ProgressResource>,
|
mut progress: ResMut<ProgressResource>,
|
||||||
progress_path: Res<ProgressStoragePath>,
|
progress_path: Res<ProgressStoragePath>,
|
||||||
|
mut complete_writer: MessageWriter<SyncCompleteEvent>,
|
||||||
) {
|
) {
|
||||||
let Some(task) = task_res.0.as_mut() else {
|
let Some(task) = task_res.0.as_mut() else {
|
||||||
return;
|
return;
|
||||||
@@ -173,7 +175,7 @@ fn poll_pull_result(
|
|||||||
match result {
|
match result {
|
||||||
Ok(remote) => {
|
Ok(remote) => {
|
||||||
let local = build_payload(&stats.0, &achievements.0, &progress.0);
|
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.
|
// Persist merged state atomically.
|
||||||
if let Some(p) = &stats_path.0
|
if let Some(p) = &stats_path.0
|
||||||
@@ -190,10 +192,17 @@ fn poll_pull_result(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update in-world resources.
|
// Update in-world resources.
|
||||||
stats.0 = merged.stats;
|
let now = Utc::now();
|
||||||
achievements.0 = merged.achievements;
|
stats.0 = merged.stats.clone();
|
||||||
progress.0 = merged.progress;
|
achievements.0 = merged.achievements.clone();
|
||||||
status.0 = SyncStatus::LastSynced(Utc::now());
|
progress.0 = merged.progress.clone();
|
||||||
|
status.0 = SyncStatus::LastSynced(now);
|
||||||
|
|
||||||
|
complete_writer.write(SyncCompleteEvent(Ok(SyncResponse {
|
||||||
|
merged,
|
||||||
|
server_time: now,
|
||||||
|
conflicts,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
Err(SyncError::UnsupportedPlatform) => {
|
Err(SyncError::UnsupportedPlatform) => {
|
||||||
// No backend configured — not an error, just leave status as Idle.
|
// 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::Serialization(_) => format!("Unexpected server response: {e}"),
|
||||||
SyncError::UnsupportedPlatform => unreachable!("handled above"),
|
SyncError::UnsupportedPlatform => unreachable!("handled above"),
|
||||||
};
|
};
|
||||||
status.0 = SyncStatus::Error(msg);
|
status.0 = SyncStatus::Error(msg.clone());
|
||||||
|
complete_writer.write(SyncCompleteEvent(Err(msg)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user