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<Option<...>>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funman300
2026-05-27 17:08:54 -07:00
parent 86bafdd679
commit a8ceed97a9
2 changed files with 159 additions and 137 deletions
+66 -130
View File
@@ -18,25 +18,15 @@ use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::{MonitorSelection, PresentMode, WindowPosition};
#[cfg(target_os = "android")]
use bevy::winit::{UpdateMode, WinitSettings};
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow}; use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow};
use bevy::window::{MonitorSelection, PresentMode, WindowPosition};
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use bevy::winit::WinitWindows; use bevy::winit::WinitWindows;
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings}; #[cfg(target_os = "android")]
use solitaire_engine::{ use bevy::winit::{UpdateMode, WinitSettings};
register_theme_asset_sources, AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin, use solitaire_data::{Settings, load_settings_from, provider_for_backend, settings_file_path};
AudioPlugin, AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin, use solitaire_engine::{CoreGamePlugin, SyncProvider, register_theme_asset_sources};
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,
};
fn load_settings() -> Settings { fn load_settings() -> Settings {
settings_file_path() settings_file_path()
@@ -87,7 +77,6 @@ fn build_app_with_settings(
settings: Settings, settings: Settings,
sync_provider: Box<dyn SyncProvider + Send + Sync>, sync_provider: Box<dyn SyncProvider + Send + Sync>,
) -> App { ) -> App {
// Restore the previous window geometry if the player has one saved. // Restore the previous window geometry if the player has one saved.
// Otherwise open at the platform default (1280×800, centred on the // Otherwise open at the platform default (1280×800, centred on the
// primary monitor) — `apply_smart_default_window_size` will resize // 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. // sessions don't end up with a comparatively tiny window.
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
let had_saved_geometry = settings.window_geometry.is_some(); 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) => ( Some(geom) => (
(geom.width, geom.height).into(), (geom.width, geom.height).into(),
WindowPosition::At(IVec2::new(geom.x, geom.y)), 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 // The card-theme system's `themes://` asset source must be
// registered *before* `DefaultPlugins` builds `AssetPlugin`, // registered *before* `DefaultPlugins` builds `AssetPlugin`,
// because that plugin freezes the asset-source list at build // because that plugin freezes the asset-source list at build
// time. The matching `AssetSourcesPlugin` (added below) finishes // time. The matching `AssetSourcesPlugin` (registered by
// the wiring after `DefaultPlugins` by populating the embedded // `CoreGamePlugin`) finishes the wiring after `DefaultPlugins`
// default theme into Bevy's `EmbeddedAssetRegistry`. // by populating the embedded default theme into Bevy's
// `EmbeddedAssetRegistry`.
register_theme_asset_sources(&mut app); register_theme_asset_sources(&mut app);
app app.add_plugins(
.add_plugins( DefaultPlugins
DefaultPlugins .set(WindowPlugin {
.set(WindowPlugin { primary_window: Some(Window {
primary_window: Some(Window { title: "Ferrous Solitaire".into(),
title: "Ferrous Solitaire".into(), // X11/Wayland WM_CLASS so taskbar managers group
// X11/Wayland WM_CLASS so taskbar managers group // multiple windows of this app correctly.
// multiple windows of this app correctly. name: Some("ferrous-solitaire".into()),
name: Some("ferrous-solitaire".into()), resolution: window_resolution,
resolution: window_resolution, position: window_position,
position: window_position, // On Android, AutoVsync caps the GPU at the display
// On Android, AutoVsync caps the GPU at the display // refresh rate (~60-90 fps). Without it the renderer
// refresh rate (~60-90 fps). Without it the renderer // spins as fast as the hardware allows, keeping the
// spins as fast as the hardware allows, keeping the // GPU fully loaded and draining the battery even when
// GPU fully loaded and draining the battery even when // the game is completely idle.
// the game is completely idle. //
// // On desktop (X11 / Wayland) AutoNoVsync prefers
// On desktop (X11 / Wayland) AutoNoVsync prefers // Mailbox (triple-buffered) and falls back to
// Mailbox (triple-buffered) and falls back to // Immediate, eliminating the vsync stall that
// Immediate, eliminating the vsync stall that // AutoVsync produces during continuous window resize.
// AutoVsync produces during continuous window resize. // The game's frame budget is small enough that a few
// The game's frame budget is small enough that a few // stray dropped frames from disabling vsync are
// stray dropped frames from disabling vsync are // imperceptible on desktop.
// imperceptible on desktop. #[cfg(target_os = "android")]
#[cfg(target_os = "android")] present_mode: PresentMode::AutoVsync,
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 {
#[cfg(not(target_os = "android"))] #[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() ..default()
}), }),
) ..default()
.add_plugins(AssetSourcesPlugin) })
.add_plugins(ThemePlugin) // The `assets/` directory lives at the workspace root, but
.add_plugins(ThemeRegistryPlugin) // on desktop Bevy resolves `AssetPlugin::file_path` relative
.add_plugins(FontPlugin) // to the binary package's `CARGO_MANIFEST_DIR`
.add_plugins(GamePlugin) // (`solitaire_app/`), so `cargo run -p solitaire_app` would
.add_plugins(TablePlugin) // miss the workspace-root `assets/` without a `../` prefix.
.add_plugins(CardPlugin) //
// Cursor-icon feedback is desktop-only; Android has no pointer cursor. // On Android cargo-apk packages the same directory into the
// The drop-target highlight systems (update_drop_highlights, // APK at `assets/` (via `[package.metadata.android].assets`
// update_drop_target_overlays) live in CursorPlugin but ARE useful // in solitaire_app/Cargo.toml). Bevy's `AndroidAssetReader`
// on Android — they've been left running because their Bevy system // is already rooted there, so any `file_path` other than the
// params compile and function on Android; only the CursorIcon insert // default makes it walk *out* of the APK's assets root and
// is inert. Gate the whole plugin if the cursor APIs ever cause // all loads fail silently — which is what produced the
// Android linker issues; for now it's harmless to leave it registered. // solid-red card-back fallback in the v0.22.3 screenshot.
.add_plugins(CursorPlugin) .set(bevy::asset::AssetPlugin {
.add_plugins(InputPlugin) #[cfg(not(target_os = "android"))]
.add_plugins(RadialMenuPlugin) file_path: "../assets".to_string(),
.add_plugins(SelectionPlugin) ..default()
.add_plugins(AnimationPlugin) }),
.add_plugins(FeedbackAnimPlugin) )
.add_plugins(CardAnimationPlugin) .add_plugins(CoreGamePlugin::new(sync_provider));
.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);
// On Android the default WinitSettings use UpdateMode::Continuous for // On Android the default WinitSettings use UpdateMode::Continuous for
// the focused window, which means Bevy renders as fast as possible even // the focused window, which means Bevy renders as fast as possible even
+93 -7
View File
@@ -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 //! 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::*; use bevy::prelude::*;
/// Groups all Ferrous Solitaire gameplay plugins. use crate::{
pub struct CoreGamePlugin; 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 { /// Groups all Ferrous Solitaire gameplay plugins.
fn build(&self, _app: &mut App) { pub struct CoreGamePlugin {
// Gameplay systems will be migrated here in follow-up issues. sync_provider: Mutex<Option<Box<dyn SyncProvider + Send + Sync>>>,
}
impl CoreGamePlugin {
/// Create a new [`CoreGamePlugin`] with the sync provider used by [`SyncPlugin`].
pub fn new(sync_provider: Box<dyn SyncProvider + Send + Sync>) -> 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);
} }
} }