chore: add pedantic workspace lints (#90)

Add [workspace.lints.rust] and wire each member crate up with
[lints] workspace = true:

  unsafe_code = "deny"        (forbid would break the Android JNI build)
  single_use_lifetimes = "warn"
  trivial_casts = "warn"
  unused_lifetimes = "warn"
  unused_qualifications = "warn"
  variant_size_differences = "warn"
  unexpected_cfgs = "warn"

unsafe_code is "deny" rather than the issue's "forbid" so the three
Android JNI FFI modules (android_keystore, android_clipboard, safe_area)
can opt back in with a scoped #![allow(unsafe_code)] — forbid cannot be
locally overridden. Pure crates carry no unsafe and stay clean.

Clean up the warnings the new lints surface:
- 150ish unused_qualifications removed via `cargo fix` (purely syntactic
  redundant-path-prefix removals).
- table_plugin: the TABLE_COLOUR import was #[cfg(test)]-gated while the
  camera clear-colour used the fully-qualified path; unqualifying it left
  a non-test build with no import. Made the import unconditional instead.
- assets/sources: the `as &[u8]` casts in embed_*_svg! coerce each
  fixed-size &[u8; N] to a uniform slice so the tuples fit the
  &[(&str, &[u8])] arrays — load-bearing, so scoped #[allow(trivial_casts)].

Workspace clippy -D warnings and the full test suite pass. Android build
not compiled here (needs the NDK; built separately per CLAUDE.md §15) —
the deny + scoped-allow keeps the JNI unsafe blocks legal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-12 13:05:28 -07:00
parent 9bbb57134f
commit ceb9c950a1
48 changed files with 191 additions and 130 deletions
+3
View File
@@ -52,3 +52,6 @@ web-sys = { version = "0.3", features = ["Storage", "Window"] }
async-trait = { workspace = true }
tempfile = { workspace = true }
solitaire_core = { workspace = true, features = ["test-support"] }
[lints]
workspace = true
+7 -7
View File
@@ -116,7 +116,7 @@ impl Plugin for AchievementPlugin {
// achievements-scroll system also runs cleanly under
// `MinimalPlugins` in tests.
.add_message::<MouseWheel>()
.add_message::<bevy::input::touch::TouchInput>()
.add_message::<TouchInput>()
// Run after GameMutation (so GameWonEvent is available), after
// StatsUpdate (so stats reflect this win), and after ProgressUpdate
// (so daily_challenge_streak is up to date for daily_devotee).
@@ -671,7 +671,7 @@ mod tests {
.add_plugins(AchievementPlugin::headless());
// StatsPlugin's UI toggle system reads ButtonInput<KeyCode>; under
// MinimalPlugins it isn't auto-registered.
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
app.init_resource::<ButtonInput<KeyCode>>();
app.update();
app
}
@@ -819,7 +819,7 @@ mod tests {
app.world_mut()
.resource_mut::<GameStateResource>()
.0
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
.set_test_draw_mode(DrawStockConfig::DrawThree);
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -868,7 +868,7 @@ mod tests {
app.world_mut()
.resource_mut::<GameStateResource>()
.0
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
.set_test_draw_mode(DrawStockConfig::DrawThree);
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -912,7 +912,7 @@ mod tests {
// Put the active game in Zen mode. evaluate_on_win reads
// GameStateResource.mode directly to populate last_win_is_zen.
app.world_mut().resource_mut::<GameStateResource>().0.mode =
solitaire_core::game_state::GameMode::Zen;
GameMode::Zen;
app.world_mut().write_message(GameWonEvent {
score: 0,
@@ -946,7 +946,7 @@ mod tests {
// Default GameMode is Classic; assert and rely on it.
assert_eq!(
app.world().resource::<GameStateResource>().0.mode,
solitaire_core::game_state::GameMode::Classic
GameMode::Classic
);
app.world_mut().write_message(GameWonEvent {
@@ -1250,7 +1250,7 @@ mod tests {
.add_plugins(crate::progress_plugin::ProgressPlugin::headless())
.add_plugins(crate::settings_plugin::SettingsPlugin::headless())
.add_plugins(AchievementPlugin::headless());
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
app.init_resource::<ButtonInput<KeyCode>>();
app.update();
app
}
@@ -1,3 +1,8 @@
// JNI FFI requires `unsafe` to reconstruct `JavaVM` / `JObject` handles from
// raw pointers handed over by the Android runtime. Scoped to this module so
// the rest of the workspace stays `deny(unsafe_code)`.
#![allow(unsafe_code)]
/// Android clipboard bridge via JNI.
///
/// Writes text to the system clipboard by calling into `ClipboardManager`
+1 -1
View File
@@ -354,7 +354,7 @@ fn handle_win_cascade(
end: target.truncate(),
elapsed: 0.0,
duration,
curve: crate::card_animation::MotionCurve::Expressive,
curve: MotionCurve::Expressive,
delay: i as f32 * step,
start_z: start.z,
end_z: target.z,
+6
View File
@@ -115,6 +115,10 @@ macro_rules! embed_classic_svg {
}
/// Every Dark-theme SVG file bundled into the binary.
// The `as &[u8]` in `embed_dark_svg!` coerces each fixed-size
// `&[u8; N]` (N varies per file) to a uniform `&[u8]` so the tuples fit
// this array type. The cast is load-bearing, not trivial.
#[allow(trivial_casts)]
const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
embed_dark_svg!("back.svg"),
embed_dark_svg!("clubs_ace.svg"),
@@ -172,6 +176,8 @@ const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
];
/// Every Classic-theme SVG file bundled into the binary.
// See `DARK_THEME_SVGS`: the `as &[u8]` cast is load-bearing.
#[allow(trivial_casts)]
const CLASSIC_THEME_SVGS: &[(&str, &[u8])] = &[
embed_classic_svg!("back.svg"),
embed_classic_svg!("clubs_ace.svg"),
+2 -2
View File
@@ -192,7 +192,7 @@ fn shared_fontdb() -> Arc<fontdb::Database> {
fn bundled_font_resolver() -> usvg::FontResolver<'static> {
use usvg::FontResolver;
usvg::FontResolver {
FontResolver {
select_font: Box::new(|_font, db| db.faces().next().map(|face| face.id)),
select_fallback: FontResolver::default_fallback_selector(),
}
@@ -282,7 +282,7 @@ mod tests {
/// tightens.
#[test]
fn settings_satisfies_loader_bounds() {
fn assert_loader_settings<T: Default + serde::Serialize + serde::de::DeserializeOwned>() {}
fn assert_loader_settings<T: Default + Serialize + serde::de::DeserializeOwned>() {}
assert_loader_settings::<SvgLoaderSettings>();
}
}
+1 -1
View File
@@ -177,7 +177,7 @@ mod tests {
.add_plugins(GamePlugin)
.add_plugins(TablePlugin)
.add_plugins(AutoCompletePlugin);
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
app.init_resource::<ButtonInput<KeyCode>>();
app.update();
app
}
+1 -1
View File
@@ -36,7 +36,7 @@ pub struct AvatarFetchEvent {
pub url: String,
}
impl bevy::prelude::Message for AvatarFetchEvent {}
impl Message for AvatarFetchEvent {}
/// In-flight avatar download task. Returns the raw image bytes on success,
/// or `None` on any network / decode error.
@@ -73,7 +73,7 @@ pub struct HoverState {
#[derive(Debug, Clone)]
pub enum BufferedInput {
Move {
from: crate::events::MoveRequestEvent,
from: MoveRequestEvent,
},
Draw,
Undo,
+29 -29
View File
@@ -483,7 +483,7 @@ impl Plugin for CardPlugin {
update_stock_empty_indicator.after(GameMutation),
update_stock_count_badge
.after(GameMutation)
.run_if(resource_changed::<crate::GameStateResource>),
.run_if(resource_changed::<GameStateResource>),
collect_resize_events.after(LayoutSystem::UpdateOnResize),
snap_cards_on_window_resize.after(collect_resize_events),
),
@@ -2431,7 +2431,7 @@ fn update_tableau_fan_frac(
.into_iter()
.map(|tableau| {
game.0
.pile(solitaire_core::KlondikePile::Tableau(tableau))
.pile(KlondikePile::Tableau(tableau))
.into_iter()
.filter(|(_, face_up)| *face_up)
.count()
@@ -2566,7 +2566,7 @@ mod tests {
#[test]
fn card_positions_includes_all_52_cards_at_game_start() {
// At game start waste is empty, so all 52 cards are across stock + tableau.
let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let g = GameState::new(42, DrawStockConfig::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
assert_eq!(positions.len(), 52);
@@ -2580,7 +2580,7 @@ mod tests {
for _ in 0..3 {
let _ = g.draw();
}
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
g.waste_cards().iter().map(|c| c.0.clone()).collect();
assert_eq!(waste_ids.len(), 3);
@@ -2624,7 +2624,7 @@ mod tests {
"need at least 3 waste cards for this test"
);
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
waste_pile.iter().map(|c| c.0.clone()).collect();
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
@@ -2677,7 +2677,7 @@ mod tests {
let count = waste_pile.len();
assert!(count >= 2, "need at least 2 waste cards");
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
waste_pile.iter().map(|c| c.0.clone()).collect();
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
@@ -2715,7 +2715,7 @@ mod tests {
for _ in 0..3 {
let _ = g.draw();
}
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
g.waste_cards().iter().map(|c| c.0.clone()).collect();
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
@@ -2740,7 +2740,7 @@ mod tests {
#[test]
fn card_positions_tableau_cards_are_fanned_downward() {
let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let g = GameState::new(42, DrawStockConfig::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
@@ -3083,7 +3083,7 @@ mod tests {
#[test]
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let g = GameState::new(42, DrawStockConfig::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
@@ -3153,7 +3153,7 @@ mod tests {
fn fire_window_resize(app: &mut App, width: f32, height: f32) {
// Any Entity will do — the snap system reads only width/height.
let window = bevy::ecs::entity::Entity::from_raw_u32(0)
let window = Entity::from_raw_u32(0)
.expect("Entity::from_raw_u32(0) is a valid placeholder");
app.world_mut().write_message(WindowResized {
window,
@@ -3171,9 +3171,9 @@ mod tests {
// entity IDs must remain alive.
let mut app = app();
let labels_before: std::collections::HashSet<bevy::prelude::Entity> = app
let labels_before: HashSet<Entity> = app
.world_mut()
.query_filtered::<bevy::prelude::Entity, With<CardLabel>>()
.query_filtered::<Entity, With<CardLabel>>()
.iter(app.world())
.collect();
assert!(
@@ -3184,9 +3184,9 @@ mod tests {
fire_window_resize(&mut app, 1024.0, 768.0);
advance_past_resize_throttle(&mut app);
let labels_after: std::collections::HashSet<bevy::prelude::Entity> = app
let labels_after: HashSet<Entity> = app
.world_mut()
.query_filtered::<bevy::prelude::Entity, With<CardLabel>>()
.query_filtered::<Entity, With<CardLabel>>()
.iter(app.world())
.collect();
@@ -3336,9 +3336,9 @@ mod tests {
// Each shadow's parent must be a CardEntity, so the child relation
// is wired correctly.
let cards: HashSet<bevy::prelude::Entity> = app
let cards: HashSet<Entity> = app
.world_mut()
.query_filtered::<bevy::prelude::Entity, With<CardEntity>>()
.query_filtered::<Entity, With<CardEntity>>()
.iter(app.world())
.collect();
let mut q = app
@@ -3423,7 +3423,7 @@ mod tests {
let card_entity = {
let mut q = app
.world_mut()
.query::<(bevy::prelude::Entity, &CardEntity)>();
.query::<(Entity, &CardEntity)>();
q.iter(app.world())
.find(|(_, c)| c.card == *card)
.map(|(e, _)| e)
@@ -3533,7 +3533,7 @@ mod tests {
#[test]
fn stock_card_count_helper_reads_zero_for_empty_stock() {
let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
let g = GameState::new(42, DrawStockConfig::DrawOne);
let mut g_empty_stock = g.clone();
g_empty_stock.set_test_stock_cards(Vec::new());
assert_eq!(stock_card_count(&g_empty_stock), 0);
@@ -3553,9 +3553,9 @@ mod tests {
// Allocate five different strong handles by passing each a
// distinct dummy `Image`. We never render these; we only
// compare ids.
let mut images = bevy::asset::Assets::<bevy::image::Image>::default();
let backs: [Handle<bevy::image::Image>; 5] =
std::array::from_fn(|_| images.add(bevy::image::Image::default()));
let mut images = Assets::<Image>::default();
let backs: [Handle<Image>; 5] =
std::array::from_fn(|_| images.add(Image::default()));
CardImageSet {
faces: std::array::from_fn(|_| std::array::from_fn(|_| Handle::default())),
backs,
@@ -3569,8 +3569,8 @@ mod tests {
// card must render with the theme's back regardless of which
// legacy back the player picked in Settings.
let mut set = image_set_with_distinct_back_handles();
let mut images = bevy::asset::Assets::<bevy::image::Image>::default();
let theme_back: Handle<bevy::image::Image> = images.add(bevy::image::Image::default());
let mut images = Assets::<Image>::default();
let theme_back: Handle<Image> = images.add(Image::default());
set.theme_back = Some(theme_back.clone());
let face_down = make_card(Suit::Spades, Rank::Ace);
@@ -3634,8 +3634,8 @@ mod tests {
use std::collections::HashMap;
let mut set = image_set_with_distinct_back_handles();
let mut images = bevy::asset::Assets::<bevy::image::Image>::default();
let theme_back: Handle<bevy::image::Image> = images.add(bevy::image::Image::default());
let mut images = Assets::<Image>::default();
let theme_back: Handle<Image> = images.add(Image::default());
let theme = CardTheme {
meta: ThemeMeta {
@@ -3645,7 +3645,7 @@ mod tests {
version: "0".into(),
card_aspect: (2, 3),
},
faces: HashMap::<CardKey, Handle<bevy::image::Image>>::new(),
faces: HashMap::<CardKey, Handle<Image>>::new(),
back: theme_back.clone(),
};
@@ -3813,7 +3813,7 @@ mod tests {
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
g.waste_cards().iter().map(|c| c.0.clone()).collect();
let mut waste_zs: Vec<f32> = positions
@@ -3863,7 +3863,7 @@ mod tests {
let stock_x = layout.pile_positions[&KlondikePile::Stock].x;
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
g.waste_cards().iter().map(|c| c.0.clone()).collect();
let mut waste_positions: Vec<_> = card_positions(&g, &layout)
@@ -3893,7 +3893,7 @@ mod tests {
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
let waste_ids: std::collections::HashSet<Card> =
let waste_ids: HashSet<Card> =
g.waste_cards().iter().map(|c| c.0.clone()).collect();
let mut waste_zs: Vec<f32> = positions
+1 -1
View File
@@ -80,7 +80,7 @@ impl Plugin for CursorPlugin {
Update,
(
update_cursor_icon,
update_drop_highlights.run_if(resource_changed::<crate::resources::DragState>),
update_drop_highlights.run_if(resource_changed::<DragState>),
update_drop_target_overlays,
),
);
+1 -1
View File
@@ -639,7 +639,7 @@ fn lerp_color(from: Color, to: Color, t: f32) -> Color {
fn pile_cards(
game: &solitaire_core::game_state::GameState,
pile: &KlondikePile,
) -> Vec<(solitaire_core::Card, bool)> {
) -> Vec<(Card, bool)> {
match pile {
KlondikePile::Stock => game.waste_cards(),
_ => game.pile(*pile),
+6 -6
View File
@@ -201,7 +201,7 @@ impl Plugin for GamePlugin {
.add_message::<StateChangedEvent>()
.add_message::<crate::events::MoveRejectedEvent>()
.add_message::<GameWonEvent>()
.add_message::<crate::events::CardFlippedEvent>()
.add_message::<CardFlippedEvent>()
.add_message::<crate::events::AchievementUnlockedEvent>()
.add_message::<FoundationCompletedEvent>()
.add_message::<InfoToastEvent>()
@@ -530,7 +530,7 @@ fn handle_new_game(
// hides that information and reads naturally as "dealt from the
// deck." Skipped when LayoutResource isn't present (headless tests).
if let Some(layout) = layout.as_ref()
&& let Some(stock) = layout.0.pile_positions.get(&solitaire_core::KlondikePile::Stock)
&& let Some(stock) = layout.0.pile_positions.get(&KlondikePile::Stock)
{
for mut tx in &mut card_transforms {
tx.translation.x = stock.x;
@@ -868,7 +868,7 @@ fn handle_move(
mut game: ResMut<GameStateResource>,
mut changed: MessageWriter<StateChangedEvent>,
mut won: MessageWriter<GameWonEvent>,
mut flipped: MessageWriter<crate::events::CardFlippedEvent>,
mut flipped: MessageWriter<CardFlippedEvent>,
mut foundation_done: MessageWriter<FoundationCompletedEvent>,
mut recording: ResMut<RecordingReplay>,
path: Option<Res<GameStatePath>>,
@@ -909,7 +909,7 @@ fn handle_move(
.last()
.is_some_and(|c| c.0 == fcard && c.1)
{
flipped.write(crate::events::CardFlippedEvent(fcard));
flipped.write(CardFlippedEvent(fcard));
}
// If this move landed on a foundation pile and that pile is
// now complete (Ace → King, 13 cards), fire the per-suit
@@ -1530,7 +1530,7 @@ mod tests {
// Persistence tests
// -----------------------------------------------------------------------
fn tmp_gs_path(name: &str) -> std::path::PathBuf {
fn tmp_gs_path(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("engine_test_gs_{name}.json"))
}
@@ -1684,7 +1684,7 @@ mod tests {
let events = app
.world()
.resource::<Messages<crate::events::CardFlippedEvent>>();
.resource::<Messages<CardFlippedEvent>>();
let mut cursor = events.get_cursor();
let fired: Vec<_> = cursor.read(events).collect();
assert!(
+1 -1
View File
@@ -51,7 +51,7 @@ impl Plugin for HelpPlugin {
// plugin under `DefaultPlugins`; register them explicitly so
// scroll systems run cleanly under `MinimalPlugins` in tests.
.add_message::<MouseWheel>()
.add_message::<bevy::input::touch::TouchInput>()
.add_message::<TouchInput>()
.add_systems(
Update,
(
+1 -1
View File
@@ -1878,7 +1878,7 @@ mod tests {
let states: Vec<(HomeMode, bool)> = app
.world_mut()
.query::<(&HomeModeCard, bevy::ecs::query::Has<Disabled>)>()
.query::<(&HomeModeCard, Has<Disabled>)>()
.iter(app.world())
.map(|(c, d)| (c.0, d))
.collect();
+2 -2
View File
@@ -2428,8 +2428,8 @@ mod tests {
app.init_resource::<HintSolverConfig>();
app.init_resource::<crate::pending_hint::PendingHintTask>();
app.init_resource::<ButtonInput<KeyCode>>();
app.insert_resource(crate::layout::LayoutResource(
crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
app.insert_resource(LayoutResource(
compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
));
app.insert_resource(GameStateResource(GameState::new(42, DrawStockConfig::DrawOne)));
app.add_systems(Update, handle_keyboard_hint);
+1 -1
View File
@@ -745,7 +745,7 @@ mod tests {
);
// The HUD band top clearance (distance from window top to card top)
// must match as well — this is the quantity directly visible in Bug 2.
let card_top = |layout: &super::Layout| {
let card_top = |layout: &Layout| {
layout.pile_positions[&KlondikePile::Stock].y + layout.card_size.y / 2.0
};
assert!(
+2 -2
View File
@@ -862,7 +862,7 @@ fn handle_display_name_confirm(
.leaderboard_display_name
.clone()
.unwrap_or_else(|| {
if let solitaire_data::settings::SyncBackend::SolitaireServer {
if let SyncBackend::SolitaireServer {
ref username,
..
} = settings.0.sync_backend
@@ -1091,7 +1091,7 @@ mod tests {
.add_plugins(crate::achievement_plugin::AchievementPlugin::headless())
.add_plugins(SyncPlugin::new(NoOpProvider))
.add_plugins(LeaderboardPlugin);
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
app.init_resource::<ButtonInput<KeyCode>>();
app.update();
app
}
+4 -4
View File
@@ -2312,8 +2312,8 @@ fn format_suit_glyph_all_suits() {
fn format_foundations_row_empty_board() {
let game = solitaire_core::game_state::GameState::new_with_mode(
42,
solitaire_core::DrawStockConfig::DrawOne,
solitaire_core::game_state::GameMode::Classic,
DrawStockConfig::DrawOne,
GameMode::Classic,
);
assert_eq!(format_foundations_row(&game), "F: -- -- -- --");
}
@@ -2324,8 +2324,8 @@ fn format_foundations_row_empty_board() {
fn format_stock_waste_row_initial_state() {
let game = solitaire_core::game_state::GameState::new_with_mode(
42,
solitaire_core::DrawStockConfig::DrawOne,
solitaire_core::game_state::GameMode::Classic,
DrawStockConfig::DrawOne,
GameMode::Classic,
);
let text = format_stock_waste_row(&game);
assert!(
+5
View File
@@ -1,5 +1,10 @@
//! Safe-area insets.
//!
// JNI FFI (Android only) requires `unsafe` to reconstruct `JavaVM` /
// `JObject` handles from raw pointers handed over by the runtime. Scoped to
// this module so the rest of the workspace stays `deny(unsafe_code)`.
#![allow(unsafe_code)]
//!
//! Reports the OS-reserved regions around the playable surface (status
//! bar at the top, gesture / navigation bar at the bottom on Android,
//! display cutouts, etc.) so UI anchored to a screen edge can avoid
+5 -5
View File
@@ -163,7 +163,7 @@ impl Plugin for SelectionPlugin {
update_selection_highlight.after(GameMutation).run_if(
resource_changed::<SelectionState>
.or(resource_changed::<KeyboardDragState>)
.or(resource_changed::<crate::GameStateResource>),
.or(resource_changed::<GameStateResource>),
),
),
);
@@ -534,7 +534,7 @@ fn handle_selection_keys(
/// destination after a lift. Players who want a different column simply
/// press the right-arrow key once or twice.
pub(crate) fn legal_destinations_for(
_bottom: &solitaire_core::Card,
_bottom: &Card,
source: &KlondikePile,
game: &GameState,
stack_count: usize,
@@ -579,7 +579,7 @@ pub(crate) fn legal_destinations_for(
/// Walks backwards from the last element and stops at the first face-down card
/// (or when the slice is exhausted). Returns at least `1` when the top card is
/// face-up; returns `0` for an empty slice or when the top card is face-down.
fn face_up_run_len(cards: &[(solitaire_core::Card, bool)]) -> usize {
fn face_up_run_len(cards: &[(Card, bool)]) -> usize {
let mut count = 0;
for (_, face_up) in cards.iter().rev() {
if *face_up {
@@ -598,8 +598,8 @@ fn face_up_run_len(cards: &[(solitaire_core::Card, bool)]) -> usize {
/// handler can attempt a foundation move first and fall through to a
/// multi-card stack move rather than accepting a single-card tableau move.
fn try_foundation_dest(
card: &solitaire_core::Card,
game: &solitaire_core::game_state::GameState,
card: &Card,
game: &GameState,
) -> Option<KlondikePile> {
let source = game.pile_containing_card(card.clone())?;
for foundation in [
+8 -8
View File
@@ -379,8 +379,8 @@ impl Plugin for SettingsPlugin {
.add_message::<DeleteAccountRequestEvent>()
.add_message::<ToggleSettingsRequestEvent>()
.add_message::<InfoToastEvent>()
.add_message::<bevy::input::mouse::MouseWheel>()
.add_message::<bevy::input::touch::TouchInput>()
.add_message::<MouseWheel>()
.add_message::<TouchInput>()
// `WindowResized` / `WindowMoved` are real Bevy window events
// and emitted by the windowing backend under `DefaultPlugins`,
// but we register them explicitly here so the geometry watcher
@@ -2662,7 +2662,7 @@ fn handle_scan_themes(
let themes_dir = user_theme_dir();
let zips: Vec<std::path::PathBuf> = match std::fs::read_dir(&themes_dir) {
let zips: Vec<PathBuf> = match std::fs::read_dir(&themes_dir) {
Ok(entries) => entries
.flatten()
.map(|e| e.path())
@@ -3019,7 +3019,7 @@ mod tests {
unit: MouseScrollUnit::Line,
x: 0.0,
y: -3.0,
window: bevy::ecs::entity::Entity::PLACEHOLDER,
window: Entity::PLACEHOLDER,
});
app.update();
// ScrollPosition must remain at 0.0 — panel was closed.
@@ -3052,7 +3052,7 @@ mod tests {
unit: MouseScrollUnit::Line,
x: 0.0,
y: -2.0,
window: bevy::ecs::entity::Entity::PLACEHOLDER,
window: Entity::PLACEHOLDER,
});
app.update();
let offset = app
@@ -3364,7 +3364,7 @@ mod tests {
fn fire_resize(app: &mut App, width: f32, height: f32) {
app.world_mut().write_message(WindowResized {
window: bevy::ecs::entity::Entity::PLACEHOLDER,
window: Entity::PLACEHOLDER,
width,
height,
});
@@ -3372,7 +3372,7 @@ mod tests {
fn fire_move(app: &mut App, x: i32, y: i32) {
app.world_mut().write_message(WindowMoved {
window: bevy::ecs::entity::Entity::PLACEHOLDER,
window: Entity::PLACEHOLDER,
position: IVec2::new(x, y),
});
}
@@ -3494,7 +3494,7 @@ mod tests {
unit: MouseScrollUnit::Line,
x: 0.0,
y: 5.0,
window: bevy::ecs::entity::Entity::PLACEHOLDER,
window: Entity::PLACEHOLDER,
});
app.update();
let offset = app
+2 -2
View File
@@ -1010,8 +1010,8 @@ mod tests {
use solitaire_data::Settings;
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugins(bevy::asset::AssetPlugin::default())
.init_asset::<bevy::image::Image>()
.add_plugins(AssetPlugin::default())
.init_asset::<Image>()
.add_plugins(SplashPlugin);
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<ButtonInput<MouseButton>>();
+9 -9
View File
@@ -203,7 +203,7 @@ impl Plugin for StatsPlugin {
// `DefaultPlugins`; register it explicitly so the stats-scroll
// system also runs cleanly under `MinimalPlugins` in tests.
.add_message::<MouseWheel>()
.add_message::<bevy::input::touch::TouchInput>()
.add_message::<TouchInput>()
// record_abandoned must read `move_count` BEFORE handle_new_game
// clobbers it with a fresh game. These are NOT in StatsUpdate because
// StatsUpdate (as a set) is ordered after GameMutation by external
@@ -464,7 +464,7 @@ fn repaint_replay_selector_detail(
/// Pure helper: render the detail line for the selected replay. Returns
/// `"{duration} win on {date}"` plus a `" \u{2022} Shareable"` badge
/// when a share URL is present. Empty when the history slice is empty.
pub fn replay_selector_detail(replays: &[solitaire_data::Replay], index: usize) -> String {
pub fn replay_selector_detail(replays: &[Replay], index: usize) -> String {
let Some(r) = replays.get(index.min(replays.len().saturating_sub(1))) else {
return String::new();
};
@@ -1325,7 +1325,7 @@ mod tests {
fn draw_three_win_increments_draw_three_wins_only() {
let mut app = headless_app();
app.world_mut()
.resource_mut::<crate::resources::GameStateResource>()
.resource_mut::<GameStateResource>()
.0
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
@@ -1371,7 +1371,7 @@ mod tests {
let mut app = headless_app();
app.world_mut()
.resource_mut::<crate::resources::GameStateResource>()
.resource_mut::<GameStateResource>()
.0
.set_test_move_count(3);
@@ -1501,7 +1501,7 @@ mod tests {
fn zen_win_event_updates_zen_best_score_only() {
let mut app = headless_app();
app.world_mut()
.resource_mut::<crate::resources::GameStateResource>()
.resource_mut::<GameStateResource>()
.0
.mode = solitaire_core::game_state::GameMode::Zen;
@@ -1697,7 +1697,7 @@ mod tests {
stats.0.win_streak_current = 3;
}
app.world_mut()
.resource_mut::<crate::resources::GameStateResource>()
.resource_mut::<GameStateResource>()
.0
.set_test_move_count(1);
@@ -1723,7 +1723,7 @@ mod tests {
stats.0.win_streak_current = 1;
}
app.world_mut()
.resource_mut::<crate::resources::GameStateResource>()
.resource_mut::<GameStateResource>()
.0
.set_test_move_count(1);
@@ -1948,9 +1948,9 @@ mod tests {
///
/// Uses a fixed seed, DrawOne mode, Classic game, 2026-05-08 date.
/// `time_seconds` and `share_url` are the only varying fields across tests.
fn make_test_replay(time_seconds: u64, share_url: Option<String>) -> solitaire_data::Replay {
fn make_test_replay(time_seconds: u64, share_url: Option<String>) -> Replay {
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 8).expect("valid date");
let mut r = solitaire_data::Replay::new(
let mut r = Replay::new(
1,
solitaire_core::DrawStockConfig::DrawOne,
solitaire_core::game_state::GameMode::Classic,
+4 -4
View File
@@ -496,7 +496,7 @@ mod tests {
.add_plugins(crate::achievement_plugin::AchievementPlugin::headless())
.add_plugins(SyncPlugin::new(provider));
// MinimalPlugins does not register keyboard input.
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
app.init_resource::<ButtonInput<KeyCode>>();
app.update();
app
}
@@ -629,8 +629,8 @@ mod tests {
replays: vec![initial],
};
save_replay_history_to(&path, &history).expect("seed history on disk");
app.insert_resource(crate::stats_plugin::ReplayHistoryResource(history));
app.insert_resource(crate::stats_plugin::LatestReplayPath(Some(path.clone())));
app.insert_resource(ReplayHistoryResource(history));
app.insert_resource(LatestReplayPath(Some(path.clone())));
// Pre-resolved task carrying the URL the production path would
// get back from the server.
@@ -659,7 +659,7 @@ mod tests {
// In-memory contract: replays[0].share_url is now Some(url).
let live = app
.world()
.resource::<crate::stats_plugin::ReplayHistoryResource>();
.resource::<ReplayHistoryResource>();
assert_eq!(
live.0.replays.first().and_then(|r| r.share_url.clone()),
Some(url.clone()),
+4 -6
View File
@@ -11,9 +11,7 @@ use solitaire_core::Suit;
use crate::events::{HintVisualEvent, StateChangedEvent};
use crate::hud_plugin::HudVisibility;
#[cfg(test)]
use crate::layout::TABLE_COLOUR;
use crate::layout::{Layout, LayoutResource, LayoutSystem, compute_layout};
use crate::layout::{Layout, LayoutResource, LayoutSystem, TABLE_COLOUR, compute_layout};
use crate::resources::GameStateResource;
use crate::safe_area::SafeAreaInsets;
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
@@ -177,9 +175,9 @@ fn setup_table(
Camera2d,
Camera {
clear_color: ClearColorConfig::Custom(Color::srgb(
crate::layout::TABLE_COLOUR[0],
crate::layout::TABLE_COLOUR[1],
crate::layout::TABLE_COLOUR[2],
TABLE_COLOUR[0],
TABLE_COLOUR[1],
TABLE_COLOUR[2],
)),
..default()
},
+6 -6
View File
@@ -236,7 +236,7 @@ pub fn import_theme_into(zip_path: &Path, target_root: &Path) -> Result<ThemeId,
/// Sums every entry's declared uncompressed size and rejects archives
/// that overflow [`MAX_ARCHIVE_BYTES`]. Iterates the central
/// directory only — does not actually decompress anything.
fn enforce_archive_size_limit<R: io::Read + io::Seek>(
fn enforce_archive_size_limit<R: Read + io::Seek>(
archive: &mut zip::ZipArchive<R>,
) -> Result<(), ImportError> {
let mut total: u64 = 0;
@@ -257,7 +257,7 @@ fn enforce_archive_size_limit<R: io::Read + io::Seek>(
/// (after normalisation) escapes its root. Catches `..`, absolute
/// paths, drive prefixes on Windows, and the awkward case where
/// `enclosed_name` returns `None` because the entry is suspicious.
fn enforce_zip_slip_safe<R: io::Read + io::Seek>(
fn enforce_zip_slip_safe<R: Read + io::Seek>(
archive: &mut zip::ZipArchive<R>,
) -> Result<(), ImportError> {
for i in 0..archive.len() {
@@ -291,7 +291,7 @@ fn is_safe_relative_path(p: &Path) -> bool {
/// `theme.ron` entry at its root.
/// - [`ImportError::ManifestParse`] when the bytes don't form valid
/// RON for `ThemeManifest`.
fn read_manifest<R: io::Read + io::Seek>(
fn read_manifest<R: Read + io::Seek>(
archive: &mut zip::ZipArchive<R>,
) -> Result<ThemeManifest, ImportError> {
// We can't use `?` directly across `by_name` because a missing
@@ -318,7 +318,7 @@ fn read_manifest<R: io::Read + io::Seek>(
///
/// Returns [`ImportError::MissingFile`] when the archive has no entry
/// matching the path.
fn read_archive_entry<R: io::Read + io::Seek>(
fn read_archive_entry<R: Read + io::Seek>(
archive: &mut zip::ZipArchive<R>,
path: &Path,
) -> Result<Vec<u8>, ImportError> {
@@ -351,7 +351,7 @@ fn archive_key(path: &Path) -> String {
/// parent directories as needed. The destination path is rebuilt from
/// the safe components we already vetted in
/// [`enforce_zip_slip_safe`], not from the raw entry name.
fn write_archive_entry<R: io::Read + io::Seek>(
fn write_archive_entry<R: Read + io::Seek>(
archive: &mut zip::ZipArchive<R>,
name: &str,
staging: &Path,
@@ -384,7 +384,7 @@ fn write_archive_entry<R: io::Read + io::Seek>(
/// Variant of [`write_archive_entry`] keyed by `Path` for the
/// manifest-declared face/back paths.
fn write_archive_entry_pathbuf<R: io::Read + io::Seek>(
fn write_archive_entry_pathbuf<R: Read + io::Seek>(
archive: &mut zip::ZipArchive<R>,
path: &Path,
staging: &Path,
+1 -1
View File
@@ -484,7 +484,7 @@ mod tests {
let mut image_set = empty_card_image_set();
// Snapshot the legacy back ids so we can prove they don't
// change when a theme is applied.
let legacy_ids_before: [bevy::asset::AssetId<bevy::image::Image>; 5] =
let legacy_ids_before: [AssetId<Image>; 5] =
std::array::from_fn(|i| image_set.backs[i].id());
let theme = empty_theme();
assert!(image_set.theme_back.is_none(), "theme_back starts empty");
+1 -1
View File
@@ -521,7 +521,7 @@ mod tests {
// the session timer or the running win count.
// -----------------------------------------------------------------------
fn tmp_ta_path(name: &str) -> std::path::PathBuf {
fn tmp_ta_path(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("engine_test_ta_{name}.json"))
}
+1 -1
View File
@@ -1324,7 +1324,7 @@ mod tests {
let row = world.spawn((FocusRow, Node::default())).id();
world.entity_mut(scrim).add_child(row);
let make_swatch = |w: &mut World, marker: fn(&mut bevy::ecs::world::EntityWorldMut)| {
let make_swatch = |w: &mut World, marker: fn(&mut EntityWorldMut)| {
let mut e = w.spawn((
Button,
Node::default(),
+3 -3
View File
@@ -982,9 +982,9 @@ mod tests {
outline_width: 0.0,
outline_offset: 0.0,
unrounded_size: card_size,
border: bevy::sprite::BorderRect::default(),
border_radius: bevy::ui::ResolvedBorderRadius::default(),
padding: bevy::sprite::BorderRect::default(),
border: BorderRect::default(),
border_radius: ResolvedBorderRadius::default(),
padding: BorderRect::default(),
inverse_scale_factor: 1.0,
};
// `is_empty` guard inside Bevy treats zero-size
+7 -7
View File
@@ -249,12 +249,12 @@ pub const BORDER_SUBTLE_HC: Color = Color::srgba(0.627, 0.627, 0.627, 1.0);
pub struct HighContrastBorder {
/// Border colour to use when high-contrast mode is *off* — the
/// site's normal idle / active-state colour.
pub default_color: bevy::prelude::Color,
pub default_color: Color,
}
impl HighContrastBorder {
/// Convenience constructor — `HighContrastBorder::with_default(BORDER_SUBTLE)`.
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
pub const fn with_default(default_color: Color) -> Self {
Self { default_color }
}
}
@@ -282,18 +282,18 @@ impl HighContrastBorder {
pub struct HighContrastBackground {
/// Background colour to use when high-contrast mode is *off* —
/// the site's normal idle / active-state colour.
pub default_color: bevy::prelude::Color,
pub default_color: Color,
/// Background colour to use when high-contrast mode is *on*.
/// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
///
/// [`with_default`]: HighContrastBackground::with_default
pub hc_color: bevy::prelude::Color,
pub hc_color: Color,
}
impl HighContrastBackground {
/// Convenience constructor — HC colour defaults to
/// [`BORDER_SUBTLE_HC`].
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
pub const fn with_default(default_color: Color) -> Self {
Self {
default_color,
hc_color: BORDER_SUBTLE_HC,
@@ -305,8 +305,8 @@ impl HighContrastBackground {
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
/// than to a neutral gray.
pub const fn with_hc(
default_color: bevy::prelude::Color,
hc_color: bevy::prelude::Color,
default_color: Color,
hc_color: Color,
) -> Self {
Self {
default_color,
+1 -1
View File
@@ -551,7 +551,7 @@ fn spawn_win_summary_after_delay(
// speed the duration is zero anyway, suppressing the shake.
let speed = settings
.as_ref()
.map_or(solitaire_data::AnimSpeed::Normal, |s| s.0.animation_speed);
.map_or(AnimSpeed::Normal, |s| s.0.animation_speed);
let scaled = scaled_duration(SHAKE_DURATION_SECS, speed);
shake.remaining = scaled;
shake.total = scaled;