diff --git a/solitaire_engine/src/game_plugin/mod.rs b/solitaire_engine/src/game_plugin/mod.rs index 8826002..83baa47 100644 --- a/solitaire_engine/src/game_plugin/mod.rs +++ b/solitaire_engine/src/game_plugin/mod.rs @@ -265,6 +265,7 @@ impl Plugin for GamePlugin { .ambiguous_with(NewGameRequestWriters), ) .chain() + .after(crate::settings_plugin::SettingsMutation) .before(GameMutation), ) .init_resource::() diff --git a/solitaire_engine/src/hud_plugin/mod.rs b/solitaire_engine/src/hud_plugin/mod.rs index 21b5f7a..06c03a4 100644 --- a/solitaire_engine/src/hud_plugin/mod.rs +++ b/solitaire_engine/src/hud_plugin/mod.rs @@ -473,7 +473,13 @@ impl Plugin for HudPlugin { apply_hud_visibility.before(LayoutSystem::UpdateOnResize), ) .add_systems(Update, restore_hud_on_modal) - .add_systems(Update, (update_hud_avatar, handle_avatar_button)) + .add_systems( + Update, + ( + update_hud_avatar.after(crate::settings_plugin::SettingsMutation), + handle_avatar_button, + ), + ) .add_systems(Update, update_won_previously.after(GameMutation)) .add_systems(Update, announce_auto_complete.after(GameMutation)) .add_systems( diff --git a/solitaire_engine/src/schedule_checks.rs b/solitaire_engine/src/schedule_checks.rs index a06b12a..8680681 100644 --- a/solitaire_engine/src/schedule_checks.rs +++ b/solitaire_engine/src/schedule_checks.rs @@ -38,7 +38,7 @@ mod tests { /// ordering edge — add `.before`/`.after` (order matters) or /// `.ambiguous_with` (provably order-independent) at the registration /// site. When triage lowers the real count, lower this constant too. - const AMBIGUITY_BASELINE: usize = 198; + const AMBIGUITY_BASELINE: usize = 171; fn cluster_app() -> App { let mut app = App::new(); diff --git a/solitaire_engine/src/settings_plugin/mod.rs b/solitaire_engine/src/settings_plugin/mod.rs index cee3276..9aae03b 100644 --- a/solitaire_engine/src/settings_plugin/mod.rs +++ b/solitaire_engine/src/settings_plugin/mod.rs @@ -79,6 +79,14 @@ pub struct PendingWindowGeometry { #[derive(Message, Debug, Clone)] pub struct SettingsChangedEvent(pub Settings); +/// System set for the systems that mutate [`SettingsResource`] every frame +/// (hotkeys and window-geometry persistence). Ordered before +/// [`crate::game_plugin::GameMutation`]; readers of settings should sit +/// after this set (directly, or transitively via `.after(GameMutation)`) +/// so they observe the current frame's settings deterministically (#143). +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct SettingsMutation; + /// Marker on the root Settings panel entity. #[derive(Component, Debug)] pub struct SettingsPanel; @@ -372,15 +380,31 @@ impl Plugin for SettingsPlugin { // also runs cleanly under `MinimalPlugins` (tests). .add_message::() .add_message::() + // Settings changes land before game logic runs: the mutator + // chain (volume keys → geometry record → geometry persist) is a + // deterministic spine, and the whole set precedes GameMutation so + // every reader already ordered after GameMutation sees this + // frame's settings transitively (ambiguity burn-down, #143). + .configure_sets( + Update, + SettingsMutation.before(crate::game_plugin::GameMutation), + ) .add_systems( Update, ( handle_volume_keys, + record_window_geometry_changes, + persist_window_geometry_after_debounce, + ) + .chain() + .in_set(SettingsMutation), + ) + .add_systems( + Update, + ( toggle_settings_screen, scroll_settings_panel, crate::ui_modal::touch_scroll_panel::, - record_window_geometry_changes, - persist_window_geometry_after_debounce, ), ); diff --git a/solitaire_engine/src/ui_focus.rs b/solitaire_engine/src/ui_focus.rs index d78e67a..553fc38 100644 --- a/solitaire_engine/src/ui_focus.rs +++ b/solitaire_engine/src/ui_focus.rs @@ -149,7 +149,7 @@ impl Plugin for UiFocusPlugin { clear_hud_focus_on_unhover, handle_focus_keys, update_focus_overlay, - pulse_focus_overlay, + pulse_focus_overlay.after(crate::settings_plugin::SettingsMutation), ) .chain(), ); diff --git a/solitaire_engine/src/ui_modal.rs b/solitaire_engine/src/ui_modal.rs index f268f6a..450c173 100644 --- a/solitaire_engine/src/ui_modal.rs +++ b/solitaire_engine/src/ui_modal.rs @@ -695,7 +695,8 @@ impl Plugin for UiModalPlugin { advance_modal_enter, paint_modal_buttons, ) - .chain(), + .chain() + .after(crate::settings_plugin::SettingsMutation), ); // Click-outside-to-dismiss is independent of the open // animation chain — it reads `just_pressed(Left)` and runs