feat(engine): ManualSyncRequestEvent + Sync Now button in settings
- Added ManualSyncRequestEvent to events.rs (exported from lib.rs). - SyncPlugin now handles ManualSyncRequestEvent: if no pull is in flight, spawns a new AsyncComputeTaskPool task and sets status to Syncing. Ignores duplicate requests while a pull is active. - Settings panel "Sync" section now shows the status text alongside a "Sync Now" button that fires ManualSyncRequestEvent. - Cleaned up stale doc comment in input_plugin.rs (Esc pause note). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,3 +61,10 @@ pub struct CardFlippedEvent(pub u32);
|
|||||||
/// persistence/UI systems that need unlock metadata.
|
/// persistence/UI systems that need unlock metadata.
|
||||||
#[derive(Event, Debug, Clone)]
|
#[derive(Event, Debug, Clone)]
|
||||||
pub struct AchievementUnlockedEvent(pub AchievementRecord);
|
pub struct AchievementUnlockedEvent(pub AchievementRecord);
|
||||||
|
|
||||||
|
/// Request to manually trigger a sync pull from the active backend.
|
||||||
|
///
|
||||||
|
/// Fired by the Settings panel "Sync Now" button. `SyncPlugin` responds by
|
||||||
|
/// starting a new pull task if one is not already in flight.
|
||||||
|
#[derive(Event, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct ManualSyncRequestEvent;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
//! - `U` → `UndoRequestEvent`
|
//! - `U` → `UndoRequestEvent`
|
||||||
//! - `N` → `NewGameRequestEvent { seed: None }`
|
//! - `N` → `NewGameRequestEvent { seed: None }`
|
||||||
//! - `D` → `DrawRequestEvent`
|
//! - `D` → `DrawRequestEvent`
|
||||||
//! - `Esc` → logged as a pause placeholder (no event yet; wired up when the
|
//! - `Esc` → handled by `PausePlugin` (overlay toggle + paused flag)
|
||||||
//! pause screen lands in a later phase)
|
|
||||||
//!
|
//!
|
||||||
//! Mouse:
|
//! Mouse:
|
||||||
//! - Left-click on the stock pile (face-down top) → `DrawRequestEvent`
|
//! - Left-click on the stock pile (face-down top) → `DrawRequestEvent`
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ pub use animation_plugin::{AnimationPlugin, CardAnim};
|
|||||||
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
||||||
pub use card_plugin::{CardEntity, CardLabel, CardPlugin};
|
pub use card_plugin::{CardEntity, CardLabel, CardPlugin};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, MoveRejectedEvent,
|
AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, ManualSyncRequestEvent,
|
||||||
MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
MoveRejectedEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
||||||
};
|
};
|
||||||
pub use game_plugin::{GameMutation, GamePlugin};
|
pub use game_plugin::{GameMutation, GamePlugin};
|
||||||
pub use help_plugin::{HelpPlugin, HelpScreen};
|
pub use help_plugin::{HelpPlugin, HelpScreen};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use bevy::prelude::*;
|
|||||||
use solitaire_core::game_state::DrawMode;
|
use solitaire_core::game_state::DrawMode;
|
||||||
use solitaire_data::{load_settings_from, save_settings_to, settings_file_path, settings::Theme, Settings};
|
use solitaire_data::{load_settings_from, save_settings_to, settings_file_path, settings::Theme, Settings};
|
||||||
|
|
||||||
|
use crate::events::ManualSyncRequestEvent;
|
||||||
use crate::resources::{SyncStatus, SyncStatusResource};
|
use crate::resources::{SyncStatus, SyncStatusResource};
|
||||||
|
|
||||||
/// Volume adjustment step applied by the `[` / `]` hotkeys.
|
/// Volume adjustment step applied by the `[` / `]` hotkeys.
|
||||||
@@ -69,6 +70,7 @@ enum SettingsButton {
|
|||||||
MusicUp,
|
MusicUp,
|
||||||
ToggleDrawMode,
|
ToggleDrawMode,
|
||||||
ToggleTheme,
|
ToggleTheme,
|
||||||
|
SyncNow,
|
||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ impl Plugin for SettingsPlugin {
|
|||||||
.insert_resource(SettingsStoragePath(self.storage_path.clone()))
|
.insert_resource(SettingsStoragePath(self.storage_path.clone()))
|
||||||
.init_resource::<SettingsScreen>()
|
.init_resource::<SettingsScreen>()
|
||||||
.add_event::<SettingsChangedEvent>()
|
.add_event::<SettingsChangedEvent>()
|
||||||
|
.add_event::<ManualSyncRequestEvent>()
|
||||||
.add_systems(Update, (handle_volume_keys, toggle_settings_screen));
|
.add_systems(Update, (handle_volume_keys, toggle_settings_screen));
|
||||||
|
|
||||||
if self.ui_enabled {
|
if self.ui_enabled {
|
||||||
@@ -245,6 +248,7 @@ fn handle_settings_buttons(
|
|||||||
mut screen: ResMut<SettingsScreen>,
|
mut screen: ResMut<SettingsScreen>,
|
||||||
path: Res<SettingsStoragePath>,
|
path: Res<SettingsStoragePath>,
|
||||||
mut changed: EventWriter<SettingsChangedEvent>,
|
mut changed: EventWriter<SettingsChangedEvent>,
|
||||||
|
mut manual_sync: EventWriter<ManualSyncRequestEvent>,
|
||||||
mut sfx_text: Query<&mut Text, (With<SfxVolumeText>, Without<MusicVolumeText>, Without<DrawModeText>, Without<ThemeText>)>,
|
mut sfx_text: Query<&mut Text, (With<SfxVolumeText>, Without<MusicVolumeText>, Without<DrawModeText>, Without<ThemeText>)>,
|
||||||
mut music_text: Query<&mut Text, (With<MusicVolumeText>, Without<SfxVolumeText>, Without<DrawModeText>, Without<ThemeText>)>,
|
mut music_text: Query<&mut Text, (With<MusicVolumeText>, Without<SfxVolumeText>, Without<DrawModeText>, Without<ThemeText>)>,
|
||||||
mut draw_text: Query<&mut Text, (With<DrawModeText>, Without<SfxVolumeText>, Without<MusicVolumeText>, Without<ThemeText>)>,
|
mut draw_text: Query<&mut Text, (With<DrawModeText>, Without<SfxVolumeText>, Without<MusicVolumeText>, Without<ThemeText>)>,
|
||||||
@@ -322,6 +326,9 @@ fn handle_settings_buttons(
|
|||||||
**t = theme_label(&settings.0.theme);
|
**t = theme_label(&settings.0.theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SettingsButton::SyncNow => {
|
||||||
|
manual_sync.send(ManualSyncRequestEvent);
|
||||||
|
}
|
||||||
SettingsButton::Done => {
|
SettingsButton::Done => {
|
||||||
screen.0 = false;
|
screen.0 = false;
|
||||||
}
|
}
|
||||||
@@ -453,12 +460,40 @@ fn spawn_settings_panel(commands: &mut Commands, settings: &Settings, sync_statu
|
|||||||
|
|
||||||
// --- Sync section ---
|
// --- Sync section ---
|
||||||
section_label(card, "Sync");
|
section_label(card, "Sync");
|
||||||
card.spawn((
|
card.spawn(Node {
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
column_gap: Val::Px(10.0),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|row| {
|
||||||
|
row.spawn((
|
||||||
SyncStatusText,
|
SyncStatusText,
|
||||||
Text::new(sync_status.to_string()),
|
Text::new(sync_status.to_string()),
|
||||||
TextFont { font_size: 16.0, ..default() },
|
TextFont { font_size: 16.0, ..default() },
|
||||||
TextColor(Color::srgb(0.65, 0.65, 0.70)),
|
TextColor(Color::srgb(0.65, 0.65, 0.70)),
|
||||||
));
|
));
|
||||||
|
// "Sync Now" button — hidden when SyncPlugin is not installed;
|
||||||
|
// visible because ManualSyncRequestEvent is always registered.
|
||||||
|
row.spawn((
|
||||||
|
SettingsButton::SyncNow,
|
||||||
|
Button,
|
||||||
|
Node {
|
||||||
|
padding: UiRect::axes(Val::Px(10.0), Val::Px(4.0)),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb(0.20, 0.30, 0.45)),
|
||||||
|
BorderRadius::all(Val::Px(4.0)),
|
||||||
|
))
|
||||||
|
.with_children(|b| {
|
||||||
|
b.spawn((
|
||||||
|
Text::new("Sync Now"),
|
||||||
|
TextFont { font_size: 14.0, ..default() },
|
||||||
|
TextColor(Color::WHITE),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Done button
|
// Done button
|
||||||
card.spawn((
|
card.spawn((
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use solitaire_data::{
|
|||||||
use solitaire_sync::{merge, SyncPayload};
|
use solitaire_sync::{merge, SyncPayload};
|
||||||
|
|
||||||
use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath};
|
use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath};
|
||||||
|
use crate::events::ManualSyncRequestEvent;
|
||||||
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};
|
||||||
@@ -92,8 +93,9 @@ impl Plugin for SyncPlugin {
|
|||||||
.init_resource::<SyncStatusResource>()
|
.init_resource::<SyncStatusResource>()
|
||||||
.init_resource::<PullTaskResult>()
|
.init_resource::<PullTaskResult>()
|
||||||
.init_resource::<PullTask>()
|
.init_resource::<PullTask>()
|
||||||
|
.add_event::<ManualSyncRequestEvent>()
|
||||||
.add_systems(Startup, start_pull)
|
.add_systems(Startup, start_pull)
|
||||||
.add_systems(Update, poll_pull_result)
|
.add_systems(Update, (poll_pull_result, handle_manual_sync_request))
|
||||||
.add_systems(Last, push_on_exit);
|
.add_systems(Last, push_on_exit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +118,29 @@ fn start_pull(
|
|||||||
status.0 = SyncStatus::Syncing;
|
status.0 = SyncStatus::Syncing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update system: starts a new pull task when `ManualSyncRequestEvent` is
|
||||||
|
/// received, but only if no pull is already in flight.
|
||||||
|
fn handle_manual_sync_request(
|
||||||
|
mut events: EventReader<ManualSyncRequestEvent>,
|
||||||
|
provider: Res<SyncProviderResource>,
|
||||||
|
mut task_res: ResMut<PullTask>,
|
||||||
|
mut status: ResMut<SyncStatusResource>,
|
||||||
|
) {
|
||||||
|
if events.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
events.clear();
|
||||||
|
if task_res.0.is_some() {
|
||||||
|
return; // Already pulling — ignore.
|
||||||
|
}
|
||||||
|
let provider = provider.0.clone();
|
||||||
|
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||||
|
provider.pull().await.map_err(|e| e.to_string())
|
||||||
|
});
|
||||||
|
task_res.0 = Some(task);
|
||||||
|
status.0 = SyncStatus::Syncing;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update system: polls the pull task without blocking.
|
/// Update system: polls the pull task without blocking.
|
||||||
///
|
///
|
||||||
/// When the task resolves successfully:
|
/// When the task resolves successfully:
|
||||||
|
|||||||
Reference in New Issue
Block a user