From a8ceed97a91292d57c8fa5a6485695b802ba913b Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 27 May 2026 17:08:54 -0700 Subject: [PATCH] refactor(engine): migrate gameplay plugins into CoreGamePlugin (closes #45, closes #46) All engine plugin registrations now live in CoreGamePlugin::build(). build_app() is reduced to DefaultPlugins setup + CoreGamePlugin registration. sync_provider is threaded through CoreGamePlugin::new() via Mutex>. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- solitaire_app/src/lib.rs | 196 ++++++++--------------- solitaire_engine/src/core_game_plugin.rs | 100 +++++++++++- 2 files changed, 159 insertions(+), 137 deletions(-) diff --git a/solitaire_app/src/lib.rs b/solitaire_app/src/lib.rs index f86373e..cb3c812 100644 --- a/solitaire_app/src/lib.rs +++ b/solitaire_app/src/lib.rs @@ -18,25 +18,15 @@ use std::io::Write; use std::time::{SystemTime, UNIX_EPOCH}; use bevy::prelude::*; -use bevy::window::{MonitorSelection, PresentMode, WindowPosition}; -#[cfg(target_os = "android")] -use bevy::winit::{UpdateMode, WinitSettings}; #[cfg(not(target_os = "android"))] use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow}; +use bevy::window::{MonitorSelection, PresentMode, WindowPosition}; #[cfg(not(target_os = "android"))] use bevy::winit::WinitWindows; -use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings}; -use solitaire_engine::{ - register_theme_asset_sources, AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin, - AudioPlugin, AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin, - CoreGamePlugin, CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, - FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, - LeaderboardPlugin, OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, - ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin, - SafeAreaInsetsPlugin, SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncPlugin, - SyncProvider, SyncSetupPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, - UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin, -}; +#[cfg(target_os = "android")] +use bevy::winit::{UpdateMode, WinitSettings}; +use solitaire_data::{Settings, load_settings_from, provider_for_backend, settings_file_path}; +use solitaire_engine::{CoreGamePlugin, SyncProvider, register_theme_asset_sources}; fn load_settings() -> Settings { settings_file_path() @@ -87,7 +77,6 @@ fn build_app_with_settings( settings: Settings, sync_provider: Box, ) -> App { - // Restore the previous window geometry if the player has one saved. // Otherwise open at the platform default (1280×800, centred on the // primary monitor) — `apply_smart_default_window_size` will resize @@ -95,7 +84,7 @@ fn build_app_with_settings( // sessions don't end up with a comparatively tiny window. #[cfg(not(target_os = "android"))] let had_saved_geometry = settings.window_geometry.is_some(); - let (window_resolution, window_position) = match settings.window_geometry { + let (window_resolution, window_position) = match settings.window_geometry.as_ref() { Some(geom) => ( (geom.width, geom.height).into(), WindowPosition::At(IVec2::new(geom.x, geom.y)), @@ -111,124 +100,71 @@ fn build_app_with_settings( // The card-theme system's `themes://` asset source must be // registered *before* `DefaultPlugins` builds `AssetPlugin`, // because that plugin freezes the asset-source list at build - // time. The matching `AssetSourcesPlugin` (added below) finishes - // the wiring after `DefaultPlugins` by populating the embedded - // default theme into Bevy's `EmbeddedAssetRegistry`. + // time. The matching `AssetSourcesPlugin` (registered by + // `CoreGamePlugin`) finishes the wiring after `DefaultPlugins` + // by populating the embedded default theme into Bevy's + // `EmbeddedAssetRegistry`. register_theme_asset_sources(&mut app); - app - .add_plugins( - DefaultPlugins - .set(WindowPlugin { - primary_window: Some(Window { - title: "Ferrous Solitaire".into(), - // X11/Wayland WM_CLASS so taskbar managers group - // multiple windows of this app correctly. - name: Some("ferrous-solitaire".into()), - resolution: window_resolution, - position: window_position, - // On Android, AutoVsync caps the GPU at the display - // refresh rate (~60-90 fps). Without it the renderer - // spins as fast as the hardware allows, keeping the - // GPU fully loaded and draining the battery even when - // the game is completely idle. - // - // On desktop (X11 / Wayland) AutoNoVsync prefers - // Mailbox (triple-buffered) and falls back to - // Immediate, eliminating the vsync stall that - // AutoVsync produces during continuous window resize. - // The game's frame budget is small enough that a few - // stray dropped frames from disabling vsync are - // imperceptible on desktop. - #[cfg(target_os = "android")] - present_mode: PresentMode::AutoVsync, - #[cfg(not(target_os = "android"))] - present_mode: PresentMode::AutoNoVsync, - // Android windows always fill the screen; max_width/max_height - // default to 0.0, which panics Bevy's clamp when min > max. - #[cfg(not(target_os = "android"))] - resize_constraints: bevy::window::WindowResizeConstraints { - min_width: 800.0, - min_height: 600.0, - ..default() - }, - ..default() - }), - ..default() - }) - // The `assets/` directory lives at the workspace root, but - // on desktop Bevy resolves `AssetPlugin::file_path` relative - // to the binary package's `CARGO_MANIFEST_DIR` - // (`solitaire_app/`), so `cargo run -p solitaire_app` would - // miss the workspace-root `assets/` without a `../` prefix. - // - // On Android cargo-apk packages the same directory into the - // APK at `assets/` (via `[package.metadata.android].assets` - // in solitaire_app/Cargo.toml). Bevy's `AndroidAssetReader` - // is already rooted there, so any `file_path` other than the - // default makes it walk *out* of the APK's assets root and - // all loads fail silently — which is what produced the - // solid-red card-back fallback in the v0.22.3 screenshot. - .set(bevy::asset::AssetPlugin { + app.add_plugins( + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + title: "Ferrous Solitaire".into(), + // X11/Wayland WM_CLASS so taskbar managers group + // multiple windows of this app correctly. + name: Some("ferrous-solitaire".into()), + resolution: window_resolution, + position: window_position, + // On Android, AutoVsync caps the GPU at the display + // refresh rate (~60-90 fps). Without it the renderer + // spins as fast as the hardware allows, keeping the + // GPU fully loaded and draining the battery even when + // the game is completely idle. + // + // On desktop (X11 / Wayland) AutoNoVsync prefers + // Mailbox (triple-buffered) and falls back to + // Immediate, eliminating the vsync stall that + // AutoVsync produces during continuous window resize. + // The game's frame budget is small enough that a few + // stray dropped frames from disabling vsync are + // imperceptible on desktop. + #[cfg(target_os = "android")] + present_mode: PresentMode::AutoVsync, #[cfg(not(target_os = "android"))] - file_path: "../assets".to_string(), + present_mode: PresentMode::AutoNoVsync, + // Android windows always fill the screen; max_width/max_height + // default to 0.0, which panics Bevy's clamp when min > max. + #[cfg(not(target_os = "android"))] + resize_constraints: bevy::window::WindowResizeConstraints { + min_width: 800.0, + min_height: 600.0, + ..default() + }, ..default() }), - ) - .add_plugins(AssetSourcesPlugin) - .add_plugins(ThemePlugin) - .add_plugins(ThemeRegistryPlugin) - .add_plugins(FontPlugin) - .add_plugins(GamePlugin) - .add_plugins(TablePlugin) - .add_plugins(CardPlugin) - // Cursor-icon feedback is desktop-only; Android has no pointer cursor. - // The drop-target highlight systems (update_drop_highlights, - // update_drop_target_overlays) live in CursorPlugin but ARE useful - // on Android — they've been left running because their Bevy system - // params compile and function on Android; only the CursorIcon insert - // is inert. Gate the whole plugin if the cursor APIs ever cause - // Android linker issues; for now it's harmless to leave it registered. - .add_plugins(CursorPlugin) - .add_plugins(InputPlugin) - .add_plugins(RadialMenuPlugin) - .add_plugins(SelectionPlugin) - .add_plugins(AnimationPlugin) - .add_plugins(FeedbackAnimPlugin) - .add_plugins(CardAnimationPlugin) - .add_plugins(AutoCompletePlugin) - .add_plugins(ReplayPlaybackPlugin) - .add_plugins(ReplayOverlayPlugin) - .add_plugins(StatsPlugin::default()) - .add_plugins(ProgressPlugin::default()) - .add_plugins(AchievementPlugin::default()) - .add_plugins(DailyChallengePlugin) - .add_plugins(WeeklyGoalsPlugin) - .add_plugins(ChallengePlugin) - .add_plugins(PlayBySeedPlugin) - .add_plugins(DifficultyPlugin) - .add_plugins(TimeAttackPlugin) - .add_plugins(SafeAreaInsetsPlugin) - .add_plugins(HudPlugin) - .add_plugins(HelpPlugin) - .add_plugins(HomePlugin::default()) - .add_plugins(AvatarPlugin) - .add_plugins(ProfilePlugin) - .add_plugins(PausePlugin) - .add_plugins(SettingsPlugin::default()) - .add_plugins(AudioPlugin) - .add_plugins(OnboardingPlugin) - .add_plugins(SyncPlugin::new(sync_provider)) - .add_plugins(SyncSetupPlugin) - .add_plugins(AnalyticsPlugin) - .add_plugins(LeaderboardPlugin) - .add_plugins(WinSummaryPlugin) - .add_plugins(UiModalPlugin) - .add_plugins(UiFocusPlugin) - .add_plugins(UiTooltipPlugin) - .add_plugins(SplashPlugin) - .add_plugins(DiagnosticsHudPlugin) - .add_plugins(CoreGamePlugin); + ..default() + }) + // The `assets/` directory lives at the workspace root, but + // on desktop Bevy resolves `AssetPlugin::file_path` relative + // to the binary package's `CARGO_MANIFEST_DIR` + // (`solitaire_app/`), so `cargo run -p solitaire_app` would + // miss the workspace-root `assets/` without a `../` prefix. + // + // On Android cargo-apk packages the same directory into the + // APK at `assets/` (via `[package.metadata.android].assets` + // in solitaire_app/Cargo.toml). Bevy's `AndroidAssetReader` + // is already rooted there, so any `file_path` other than the + // default makes it walk *out* of the APK's assets root and + // all loads fail silently — which is what produced the + // solid-red card-back fallback in the v0.22.3 screenshot. + .set(bevy::asset::AssetPlugin { + #[cfg(not(target_os = "android"))] + file_path: "../assets".to_string(), + ..default() + }), + ) + .add_plugins(CoreGamePlugin::new(sync_provider)); // On Android the default WinitSettings use UpdateMode::Continuous for // the focused window, which means Bevy renders as fast as possible even diff --git a/solitaire_engine/src/core_game_plugin.rs b/solitaire_engine/src/core_game_plugin.rs index 56f1119..3e9ef2d 100644 --- a/solitaire_engine/src/core_game_plugin.rs +++ b/solitaire_engine/src/core_game_plugin.rs @@ -1,15 +1,101 @@ -//! Central plugin that groups all gameplay systems. +//! Central plugin that groups all gameplay plugins. //! //! Register [`CoreGamePlugin`] once in the app instead of the individual -//! plugins. Systems are added here rather than directly in the app entry point. +//! plugins. Plugin registration lives here rather than directly in the app +//! entry point. + +use std::sync::Mutex; use bevy::prelude::*; -/// Groups all Ferrous Solitaire gameplay plugins. -pub struct CoreGamePlugin; +use crate::{ + AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin, AudioPlugin, + AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin, + CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin, + FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin, + OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin, + RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin, SafeAreaInsetsPlugin, + SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncPlugin, SyncProvider, + SyncSetupPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, + UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin, +}; -impl Plugin for CoreGamePlugin { - fn build(&self, _app: &mut App) { - // Gameplay systems will be migrated here in follow-up issues. +/// Groups all Ferrous Solitaire gameplay plugins. +pub struct CoreGamePlugin { + sync_provider: Mutex>>, +} + +impl CoreGamePlugin { + /// Create a new [`CoreGamePlugin`] with the sync provider used by [`SyncPlugin`]. + pub fn new(sync_provider: Box) -> Self { + Self { + sync_provider: Mutex::new(Some(sync_provider)), + } + } +} + +impl Plugin for CoreGamePlugin { + fn build(&self, app: &mut App) { + let mut sync_provider = match self.sync_provider.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let sync_provider = sync_provider + .take() + .expect("CoreGamePlugin::build called twice"); + + app.add_plugins(AssetSourcesPlugin) + .add_plugins(ThemePlugin) + .add_plugins(ThemeRegistryPlugin) + .add_plugins(FontPlugin) + .add_plugins(GamePlugin) + .add_plugins(TablePlugin) + .add_plugins(CardPlugin) + // Cursor-icon feedback is desktop-only; Android has no pointer cursor. + // The drop-target highlight systems (update_drop_highlights, + // update_drop_target_overlays) live in CursorPlugin but ARE useful + // on Android — they've been left running because their Bevy system + // params compile and function on Android; only the CursorIcon insert + // is inert. Gate the whole plugin if the cursor APIs ever cause + // Android linker issues; for now it's harmless to leave it registered. + .add_plugins(CursorPlugin) + .add_plugins(InputPlugin) + .add_plugins(RadialMenuPlugin) + .add_plugins(SelectionPlugin) + .add_plugins(AnimationPlugin) + .add_plugins(FeedbackAnimPlugin) + .add_plugins(CardAnimationPlugin) + .add_plugins(AutoCompletePlugin) + .add_plugins(ReplayPlaybackPlugin) + .add_plugins(ReplayOverlayPlugin) + .add_plugins(StatsPlugin::default()) + .add_plugins(ProgressPlugin::default()) + .add_plugins(AchievementPlugin::default()) + .add_plugins(DailyChallengePlugin) + .add_plugins(WeeklyGoalsPlugin) + .add_plugins(ChallengePlugin) + .add_plugins(PlayBySeedPlugin) + .add_plugins(DifficultyPlugin) + .add_plugins(TimeAttackPlugin) + .add_plugins(SafeAreaInsetsPlugin) + .add_plugins(HudPlugin) + .add_plugins(HelpPlugin) + .add_plugins(HomePlugin::default()) + .add_plugins(AvatarPlugin) + .add_plugins(ProfilePlugin) + .add_plugins(PausePlugin) + .add_plugins(SettingsPlugin::default()) + .add_plugins(AudioPlugin) + .add_plugins(OnboardingPlugin) + .add_plugins(SyncPlugin::new(sync_provider)) + .add_plugins(SyncSetupPlugin) + .add_plugins(AnalyticsPlugin) + .add_plugins(LeaderboardPlugin) + .add_plugins(WinSummaryPlugin) + .add_plugins(UiModalPlugin) + .add_plugins(UiFocusPlugin) + .add_plugins(UiTooltipPlugin) + .add_plugins(SplashPlugin) + .add_plugins(DiagnosticsHudPlugin); } }