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:
+15
@@ -18,6 +18,21 @@ version = "0.1.0"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|
||||||
|
# Pedantic correctness lints applied across every member crate via
|
||||||
|
# `[lints] workspace = true`. `unsafe_code` is "deny" rather than "forbid"
|
||||||
|
# so the three Android JNI FFI modules can opt back in with a scoped
|
||||||
|
# `#![allow(unsafe_code)]` — `forbid` cannot be locally overridden, which
|
||||||
|
# would break the Android build. Pure crates (core, sync) carry no `unsafe`
|
||||||
|
# and so remain effectively forbidden in practice.
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unsafe_code = "deny"
|
||||||
|
single_use_lifetimes = "warn"
|
||||||
|
trivial_casts = "warn"
|
||||||
|
unused_lifetimes = "warn"
|
||||||
|
unused_qualifications = "warn"
|
||||||
|
variant_size_differences = "warn"
|
||||||
|
unexpected_cfgs = "warn"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -99,3 +99,6 @@ icon = "@mipmap/ic_launcher"
|
|||||||
# in portrait orientation. Remove (or add a landscape layout) before
|
# in portrait orientation. Remove (or add a landscape layout) before
|
||||||
# enabling auto-rotate.
|
# enabling auto-rotate.
|
||||||
orientation = "portrait"
|
orientation = "portrait"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ fn build_app_with_settings(
|
|||||||
// Android windows always fill the screen; max_width/max_height
|
// Android windows always fill the screen; max_width/max_height
|
||||||
// default to 0.0, which panics Bevy's clamp when min > max.
|
// default to 0.0, which panics Bevy's clamp when min > max.
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
resize_constraints: bevy::window::WindowResizeConstraints {
|
resize_constraints: WindowResizeConstraints {
|
||||||
min_width: 800.0,
|
min_width: 800.0,
|
||||||
min_height: 600.0,
|
min_height: 600.0,
|
||||||
..default()
|
..default()
|
||||||
@@ -166,7 +166,7 @@ fn build_app_with_settings(
|
|||||||
// default makes it walk *out* of the APK's assets root and
|
// default makes it walk *out* of the APK's assets root and
|
||||||
// all loads fail silently — which is what produced the
|
// all loads fail silently — which is what produced the
|
||||||
// solid-red card-back fallback in the v0.22.3 screenshot.
|
// solid-red card-back fallback in the v0.22.3 screenshot.
|
||||||
.set(bevy::asset::AssetPlugin {
|
.set(AssetPlugin {
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
file_path: "../assets".to_string(),
|
file_path: "../assets".to_string(),
|
||||||
..default()
|
..default()
|
||||||
|
|||||||
@@ -30,3 +30,6 @@ path = "src/bin/gen_seeds.rs"
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "gen_difficulty_seeds"
|
name = "gen_difficulty_seeds"
|
||||||
path = "src/bin/gen_difficulty_seeds.rs"
|
path = "src/bin/gen_difficulty_seeds.rs"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ serde = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
klondike = { workspace = true }
|
klondike = { workspace = true }
|
||||||
card_game = { workspace = true }
|
card_game = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -47,3 +47,6 @@ sqlx = { workspace = true }
|
|||||||
jsonwebtoken = { workspace = true }
|
jsonwebtoken = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// JNI FFI requires `unsafe` to reconstruct `JavaVM` / `JByteArray` 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 Keystore token storage via JNI.
|
/// Android Keystore token storage via JNI.
|
||||||
///
|
///
|
||||||
/// Tokens are serialised to JSON, encrypted with AES-256/GCM/NoPadding using a
|
/// Tokens are serialised to JSON, encrypted with AES-256/GCM/NoPadding using a
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ pub trait SyncProvider: Send + Sync {
|
|||||||
/// so backends without a server (e.g. `LocalOnlyProvider`) are
|
/// so backends without a server (e.g. `LocalOnlyProvider`) are
|
||||||
/// silently no-op'd by the engine's push-on-win system, matching
|
/// silently no-op'd by the engine's push-on-win system, matching
|
||||||
/// the same pattern `pull` / `push` follow.
|
/// the same pattern `pull` / `push` follow.
|
||||||
async fn push_replay(&self, _replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
async fn push_replay(&self, _replay: &Replay) -> Result<String, SyncError> {
|
||||||
Err(SyncError::UnsupportedPlatform)
|
Err(SyncError::UnsupportedPlatform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
|||||||
async fn delete_account(&self) -> Result<(), SyncError> {
|
async fn delete_account(&self) -> Result<(), SyncError> {
|
||||||
(**self).delete_account().await
|
(**self).delete_account().await
|
||||||
}
|
}
|
||||||
async fn push_replay(&self, replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
async fn push_replay(&self, replay: &Replay) -> Result<String, SyncError> {
|
||||||
(**self).push_replay(replay).await
|
(**self).push_replay(replay).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,3 +52,6 @@ web-sys = { version = "0.3", features = ["Storage", "Window"] }
|
|||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
solitaire_core = { workspace = true, features = ["test-support"] }
|
solitaire_core = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ impl Plugin for AchievementPlugin {
|
|||||||
// achievements-scroll system also runs cleanly under
|
// achievements-scroll system also runs cleanly under
|
||||||
// `MinimalPlugins` in tests.
|
// `MinimalPlugins` in tests.
|
||||||
.add_message::<MouseWheel>()
|
.add_message::<MouseWheel>()
|
||||||
.add_message::<bevy::input::touch::TouchInput>()
|
.add_message::<TouchInput>()
|
||||||
// Run after GameMutation (so GameWonEvent is available), after
|
// Run after GameMutation (so GameWonEvent is available), after
|
||||||
// StatsUpdate (so stats reflect this win), and after ProgressUpdate
|
// StatsUpdate (so stats reflect this win), and after ProgressUpdate
|
||||||
// (so daily_challenge_streak is up to date for daily_devotee).
|
// (so daily_challenge_streak is up to date for daily_devotee).
|
||||||
@@ -671,7 +671,7 @@ mod tests {
|
|||||||
.add_plugins(AchievementPlugin::headless());
|
.add_plugins(AchievementPlugin::headless());
|
||||||
// StatsPlugin's UI toggle system reads ButtonInput<KeyCode>; under
|
// StatsPlugin's UI toggle system reads ButtonInput<KeyCode>; under
|
||||||
// MinimalPlugins it isn't auto-registered.
|
// MinimalPlugins it isn't auto-registered.
|
||||||
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.update();
|
app.update();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
@@ -819,7 +819,7 @@ mod tests {
|
|||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
|
.set_test_draw_mode(DrawStockConfig::DrawThree);
|
||||||
|
|
||||||
app.world_mut().write_message(GameWonEvent {
|
app.world_mut().write_message(GameWonEvent {
|
||||||
score: 500,
|
score: 500,
|
||||||
@@ -868,7 +868,7 @@ mod tests {
|
|||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
|
.set_test_draw_mode(DrawStockConfig::DrawThree);
|
||||||
|
|
||||||
app.world_mut().write_message(GameWonEvent {
|
app.world_mut().write_message(GameWonEvent {
|
||||||
score: 500,
|
score: 500,
|
||||||
@@ -912,7 +912,7 @@ mod tests {
|
|||||||
// Put the active game in Zen mode. evaluate_on_win reads
|
// Put the active game in Zen mode. evaluate_on_win reads
|
||||||
// GameStateResource.mode directly to populate last_win_is_zen.
|
// GameStateResource.mode directly to populate last_win_is_zen.
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.mode =
|
app.world_mut().resource_mut::<GameStateResource>().0.mode =
|
||||||
solitaire_core::game_state::GameMode::Zen;
|
GameMode::Zen;
|
||||||
|
|
||||||
app.world_mut().write_message(GameWonEvent {
|
app.world_mut().write_message(GameWonEvent {
|
||||||
score: 0,
|
score: 0,
|
||||||
@@ -946,7 +946,7 @@ mod tests {
|
|||||||
// Default GameMode is Classic; assert and rely on it.
|
// Default GameMode is Classic; assert and rely on it.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.world().resource::<GameStateResource>().0.mode,
|
app.world().resource::<GameStateResource>().0.mode,
|
||||||
solitaire_core::game_state::GameMode::Classic
|
GameMode::Classic
|
||||||
);
|
);
|
||||||
|
|
||||||
app.world_mut().write_message(GameWonEvent {
|
app.world_mut().write_message(GameWonEvent {
|
||||||
@@ -1250,7 +1250,7 @@ mod tests {
|
|||||||
.add_plugins(crate::progress_plugin::ProgressPlugin::headless())
|
.add_plugins(crate::progress_plugin::ProgressPlugin::headless())
|
||||||
.add_plugins(crate::settings_plugin::SettingsPlugin::headless())
|
.add_plugins(crate::settings_plugin::SettingsPlugin::headless())
|
||||||
.add_plugins(AchievementPlugin::headless());
|
.add_plugins(AchievementPlugin::headless());
|
||||||
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.update();
|
app.update();
|
||||||
app
|
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.
|
/// Android clipboard bridge via JNI.
|
||||||
///
|
///
|
||||||
/// Writes text to the system clipboard by calling into `ClipboardManager`
|
/// Writes text to the system clipboard by calling into `ClipboardManager`
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ fn handle_win_cascade(
|
|||||||
end: target.truncate(),
|
end: target.truncate(),
|
||||||
elapsed: 0.0,
|
elapsed: 0.0,
|
||||||
duration,
|
duration,
|
||||||
curve: crate::card_animation::MotionCurve::Expressive,
|
curve: MotionCurve::Expressive,
|
||||||
delay: i as f32 * step,
|
delay: i as f32 * step,
|
||||||
start_z: start.z,
|
start_z: start.z,
|
||||||
end_z: target.z,
|
end_z: target.z,
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ macro_rules! embed_classic_svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Every Dark-theme SVG file bundled into the binary.
|
/// 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])] = &[
|
const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||||
embed_dark_svg!("back.svg"),
|
embed_dark_svg!("back.svg"),
|
||||||
embed_dark_svg!("clubs_ace.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.
|
/// 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])] = &[
|
const CLASSIC_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||||
embed_classic_svg!("back.svg"),
|
embed_classic_svg!("back.svg"),
|
||||||
embed_classic_svg!("clubs_ace.svg"),
|
embed_classic_svg!("clubs_ace.svg"),
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ fn shared_fontdb() -> Arc<fontdb::Database> {
|
|||||||
fn bundled_font_resolver() -> usvg::FontResolver<'static> {
|
fn bundled_font_resolver() -> usvg::FontResolver<'static> {
|
||||||
use usvg::FontResolver;
|
use usvg::FontResolver;
|
||||||
|
|
||||||
usvg::FontResolver {
|
FontResolver {
|
||||||
select_font: Box::new(|_font, db| db.faces().next().map(|face| face.id)),
|
select_font: Box::new(|_font, db| db.faces().next().map(|face| face.id)),
|
||||||
select_fallback: FontResolver::default_fallback_selector(),
|
select_fallback: FontResolver::default_fallback_selector(),
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,7 @@ mod tests {
|
|||||||
/// tightens.
|
/// tightens.
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_satisfies_loader_bounds() {
|
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>();
|
assert_loader_settings::<SvgLoaderSettings>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ mod tests {
|
|||||||
.add_plugins(GamePlugin)
|
.add_plugins(GamePlugin)
|
||||||
.add_plugins(TablePlugin)
|
.add_plugins(TablePlugin)
|
||||||
.add_plugins(AutoCompletePlugin);
|
.add_plugins(AutoCompletePlugin);
|
||||||
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.update();
|
app.update();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub struct AvatarFetchEvent {
|
|||||||
pub url: String,
|
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,
|
/// In-flight avatar download task. Returns the raw image bytes on success,
|
||||||
/// or `None` on any network / decode error.
|
/// or `None` on any network / decode error.
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ pub struct HoverState {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BufferedInput {
|
pub enum BufferedInput {
|
||||||
Move {
|
Move {
|
||||||
from: crate::events::MoveRequestEvent,
|
from: MoveRequestEvent,
|
||||||
},
|
},
|
||||||
Draw,
|
Draw,
|
||||||
Undo,
|
Undo,
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ impl Plugin for CardPlugin {
|
|||||||
update_stock_empty_indicator.after(GameMutation),
|
update_stock_empty_indicator.after(GameMutation),
|
||||||
update_stock_count_badge
|
update_stock_count_badge
|
||||||
.after(GameMutation)
|
.after(GameMutation)
|
||||||
.run_if(resource_changed::<crate::GameStateResource>),
|
.run_if(resource_changed::<GameStateResource>),
|
||||||
collect_resize_events.after(LayoutSystem::UpdateOnResize),
|
collect_resize_events.after(LayoutSystem::UpdateOnResize),
|
||||||
snap_cards_on_window_resize.after(collect_resize_events),
|
snap_cards_on_window_resize.after(collect_resize_events),
|
||||||
),
|
),
|
||||||
@@ -2431,7 +2431,7 @@ fn update_tableau_fan_frac(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tableau| {
|
.map(|tableau| {
|
||||||
game.0
|
game.0
|
||||||
.pile(solitaire_core::KlondikePile::Tableau(tableau))
|
.pile(KlondikePile::Tableau(tableau))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(_, face_up)| *face_up)
|
.filter(|(_, face_up)| *face_up)
|
||||||
.count()
|
.count()
|
||||||
@@ -2566,7 +2566,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn card_positions_includes_all_52_cards_at_game_start() {
|
fn card_positions_includes_all_52_cards_at_game_start() {
|
||||||
// At game start waste is empty, so all 52 cards are across stock + tableau.
|
// 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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
assert_eq!(positions.len(), 52);
|
assert_eq!(positions.len(), 52);
|
||||||
@@ -2580,7 +2580,7 @@ mod tests {
|
|||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let _ = g.draw();
|
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();
|
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
assert_eq!(waste_ids.len(), 3);
|
assert_eq!(waste_ids.len(), 3);
|
||||||
|
|
||||||
@@ -2624,7 +2624,7 @@ mod tests {
|
|||||||
"need at least 3 waste cards for this test"
|
"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();
|
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 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();
|
let count = waste_pile.len();
|
||||||
assert!(count >= 2, "need at least 2 waste cards");
|
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();
|
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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
@@ -2715,7 +2715,7 @@ mod tests {
|
|||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let _ = g.draw();
|
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();
|
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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
@@ -2740,7 +2740,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn card_positions_tableau_cards_are_fanned_downward() {
|
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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
@@ -3083,7 +3083,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
|
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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
@@ -3153,7 +3153,7 @@ mod tests {
|
|||||||
|
|
||||||
fn fire_window_resize(app: &mut App, width: f32, height: f32) {
|
fn fire_window_resize(app: &mut App, width: f32, height: f32) {
|
||||||
// Any Entity will do — the snap system reads only width/height.
|
// 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");
|
.expect("Entity::from_raw_u32(0) is a valid placeholder");
|
||||||
app.world_mut().write_message(WindowResized {
|
app.world_mut().write_message(WindowResized {
|
||||||
window,
|
window,
|
||||||
@@ -3171,9 +3171,9 @@ mod tests {
|
|||||||
// entity IDs must remain alive.
|
// entity IDs must remain alive.
|
||||||
let mut app = app();
|
let mut app = app();
|
||||||
|
|
||||||
let labels_before: std::collections::HashSet<bevy::prelude::Entity> = app
|
let labels_before: HashSet<Entity> = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.query_filtered::<bevy::prelude::Entity, With<CardLabel>>()
|
.query_filtered::<Entity, With<CardLabel>>()
|
||||||
.iter(app.world())
|
.iter(app.world())
|
||||||
.collect();
|
.collect();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -3184,9 +3184,9 @@ mod tests {
|
|||||||
fire_window_resize(&mut app, 1024.0, 768.0);
|
fire_window_resize(&mut app, 1024.0, 768.0);
|
||||||
advance_past_resize_throttle(&mut app);
|
advance_past_resize_throttle(&mut app);
|
||||||
|
|
||||||
let labels_after: std::collections::HashSet<bevy::prelude::Entity> = app
|
let labels_after: HashSet<Entity> = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.query_filtered::<bevy::prelude::Entity, With<CardLabel>>()
|
.query_filtered::<Entity, With<CardLabel>>()
|
||||||
.iter(app.world())
|
.iter(app.world())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -3336,9 +3336,9 @@ mod tests {
|
|||||||
|
|
||||||
// Each shadow's parent must be a CardEntity, so the child relation
|
// Each shadow's parent must be a CardEntity, so the child relation
|
||||||
// is wired correctly.
|
// is wired correctly.
|
||||||
let cards: HashSet<bevy::prelude::Entity> = app
|
let cards: HashSet<Entity> = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.query_filtered::<bevy::prelude::Entity, With<CardEntity>>()
|
.query_filtered::<Entity, With<CardEntity>>()
|
||||||
.iter(app.world())
|
.iter(app.world())
|
||||||
.collect();
|
.collect();
|
||||||
let mut q = app
|
let mut q = app
|
||||||
@@ -3423,7 +3423,7 @@ mod tests {
|
|||||||
let card_entity = {
|
let card_entity = {
|
||||||
let mut q = app
|
let mut q = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.query::<(bevy::prelude::Entity, &CardEntity)>();
|
.query::<(Entity, &CardEntity)>();
|
||||||
q.iter(app.world())
|
q.iter(app.world())
|
||||||
.find(|(_, c)| c.card == *card)
|
.find(|(_, c)| c.card == *card)
|
||||||
.map(|(e, _)| e)
|
.map(|(e, _)| e)
|
||||||
@@ -3533,7 +3533,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stock_card_count_helper_reads_zero_for_empty_stock() {
|
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();
|
let mut g_empty_stock = g.clone();
|
||||||
g_empty_stock.set_test_stock_cards(Vec::new());
|
g_empty_stock.set_test_stock_cards(Vec::new());
|
||||||
assert_eq!(stock_card_count(&g_empty_stock), 0);
|
assert_eq!(stock_card_count(&g_empty_stock), 0);
|
||||||
@@ -3553,9 +3553,9 @@ mod tests {
|
|||||||
// Allocate five different strong handles by passing each a
|
// Allocate five different strong handles by passing each a
|
||||||
// distinct dummy `Image`. We never render these; we only
|
// distinct dummy `Image`. We never render these; we only
|
||||||
// compare ids.
|
// compare ids.
|
||||||
let mut images = bevy::asset::Assets::<bevy::image::Image>::default();
|
let mut images = Assets::<Image>::default();
|
||||||
let backs: [Handle<bevy::image::Image>; 5] =
|
let backs: [Handle<Image>; 5] =
|
||||||
std::array::from_fn(|_| images.add(bevy::image::Image::default()));
|
std::array::from_fn(|_| images.add(Image::default()));
|
||||||
CardImageSet {
|
CardImageSet {
|
||||||
faces: std::array::from_fn(|_| std::array::from_fn(|_| Handle::default())),
|
faces: std::array::from_fn(|_| std::array::from_fn(|_| Handle::default())),
|
||||||
backs,
|
backs,
|
||||||
@@ -3569,8 +3569,8 @@ mod tests {
|
|||||||
// card must render with the theme's back regardless of which
|
// card must render with the theme's back regardless of which
|
||||||
// legacy back the player picked in Settings.
|
// legacy back the player picked in Settings.
|
||||||
let mut set = image_set_with_distinct_back_handles();
|
let mut set = image_set_with_distinct_back_handles();
|
||||||
let mut images = bevy::asset::Assets::<bevy::image::Image>::default();
|
let mut images = Assets::<Image>::default();
|
||||||
let theme_back: Handle<bevy::image::Image> = images.add(bevy::image::Image::default());
|
let theme_back: Handle<Image> = images.add(Image::default());
|
||||||
set.theme_back = Some(theme_back.clone());
|
set.theme_back = Some(theme_back.clone());
|
||||||
|
|
||||||
let face_down = make_card(Suit::Spades, Rank::Ace);
|
let face_down = make_card(Suit::Spades, Rank::Ace);
|
||||||
@@ -3634,8 +3634,8 @@ mod tests {
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
let mut set = image_set_with_distinct_back_handles();
|
let mut set = image_set_with_distinct_back_handles();
|
||||||
let mut images = bevy::asset::Assets::<bevy::image::Image>::default();
|
let mut images = Assets::<Image>::default();
|
||||||
let theme_back: Handle<bevy::image::Image> = images.add(bevy::image::Image::default());
|
let theme_back: Handle<Image> = images.add(Image::default());
|
||||||
|
|
||||||
let theme = CardTheme {
|
let theme = CardTheme {
|
||||||
meta: ThemeMeta {
|
meta: ThemeMeta {
|
||||||
@@ -3645,7 +3645,7 @@ mod tests {
|
|||||||
version: "0".into(),
|
version: "0".into(),
|
||||||
card_aspect: (2, 3),
|
card_aspect: (2, 3),
|
||||||
},
|
},
|
||||||
faces: HashMap::<CardKey, Handle<bevy::image::Image>>::new(),
|
faces: HashMap::<CardKey, Handle<Image>>::new(),
|
||||||
back: theme_back.clone(),
|
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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
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();
|
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
|
|
||||||
let mut waste_zs: Vec<f32> = positions
|
let mut waste_zs: Vec<f32> = positions
|
||||||
@@ -3863,7 +3863,7 @@ mod tests {
|
|||||||
|
|
||||||
let stock_x = layout.pile_positions[&KlondikePile::Stock].x;
|
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();
|
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
|
|
||||||
let mut waste_positions: Vec<_> = card_positions(&g, &layout)
|
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 layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
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();
|
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
|
|
||||||
let mut waste_zs: Vec<f32> = positions
|
let mut waste_zs: Vec<f32> = positions
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ impl Plugin for CursorPlugin {
|
|||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
update_cursor_icon,
|
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,
|
update_drop_target_overlays,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ fn lerp_color(from: Color, to: Color, t: f32) -> Color {
|
|||||||
fn pile_cards(
|
fn pile_cards(
|
||||||
game: &solitaire_core::game_state::GameState,
|
game: &solitaire_core::game_state::GameState,
|
||||||
pile: &KlondikePile,
|
pile: &KlondikePile,
|
||||||
) -> Vec<(solitaire_core::Card, bool)> {
|
) -> Vec<(Card, bool)> {
|
||||||
match pile {
|
match pile {
|
||||||
KlondikePile::Stock => game.waste_cards(),
|
KlondikePile::Stock => game.waste_cards(),
|
||||||
_ => game.pile(*pile),
|
_ => game.pile(*pile),
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ impl Plugin for GamePlugin {
|
|||||||
.add_message::<StateChangedEvent>()
|
.add_message::<StateChangedEvent>()
|
||||||
.add_message::<crate::events::MoveRejectedEvent>()
|
.add_message::<crate::events::MoveRejectedEvent>()
|
||||||
.add_message::<GameWonEvent>()
|
.add_message::<GameWonEvent>()
|
||||||
.add_message::<crate::events::CardFlippedEvent>()
|
.add_message::<CardFlippedEvent>()
|
||||||
.add_message::<crate::events::AchievementUnlockedEvent>()
|
.add_message::<crate::events::AchievementUnlockedEvent>()
|
||||||
.add_message::<FoundationCompletedEvent>()
|
.add_message::<FoundationCompletedEvent>()
|
||||||
.add_message::<InfoToastEvent>()
|
.add_message::<InfoToastEvent>()
|
||||||
@@ -530,7 +530,7 @@ fn handle_new_game(
|
|||||||
// hides that information and reads naturally as "dealt from the
|
// hides that information and reads naturally as "dealt from the
|
||||||
// deck." Skipped when LayoutResource isn't present (headless tests).
|
// deck." Skipped when LayoutResource isn't present (headless tests).
|
||||||
if let Some(layout) = layout.as_ref()
|
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 {
|
for mut tx in &mut card_transforms {
|
||||||
tx.translation.x = stock.x;
|
tx.translation.x = stock.x;
|
||||||
@@ -868,7 +868,7 @@ fn handle_move(
|
|||||||
mut game: ResMut<GameStateResource>,
|
mut game: ResMut<GameStateResource>,
|
||||||
mut changed: MessageWriter<StateChangedEvent>,
|
mut changed: MessageWriter<StateChangedEvent>,
|
||||||
mut won: MessageWriter<GameWonEvent>,
|
mut won: MessageWriter<GameWonEvent>,
|
||||||
mut flipped: MessageWriter<crate::events::CardFlippedEvent>,
|
mut flipped: MessageWriter<CardFlippedEvent>,
|
||||||
mut foundation_done: MessageWriter<FoundationCompletedEvent>,
|
mut foundation_done: MessageWriter<FoundationCompletedEvent>,
|
||||||
mut recording: ResMut<RecordingReplay>,
|
mut recording: ResMut<RecordingReplay>,
|
||||||
path: Option<Res<GameStatePath>>,
|
path: Option<Res<GameStatePath>>,
|
||||||
@@ -909,7 +909,7 @@ fn handle_move(
|
|||||||
.last()
|
.last()
|
||||||
.is_some_and(|c| c.0 == fcard && c.1)
|
.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
|
// If this move landed on a foundation pile and that pile is
|
||||||
// now complete (Ace → King, 13 cards), fire the per-suit
|
// now complete (Ace → King, 13 cards), fire the per-suit
|
||||||
@@ -1530,7 +1530,7 @@ mod tests {
|
|||||||
// Persistence 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"))
|
std::env::temp_dir().join(format!("engine_test_gs_{name}.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1684,7 +1684,7 @@ mod tests {
|
|||||||
|
|
||||||
let events = app
|
let events = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<Messages<crate::events::CardFlippedEvent>>();
|
.resource::<Messages<CardFlippedEvent>>();
|
||||||
let mut cursor = events.get_cursor();
|
let mut cursor = events.get_cursor();
|
||||||
let fired: Vec<_> = cursor.read(events).collect();
|
let fired: Vec<_> = cursor.read(events).collect();
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl Plugin for HelpPlugin {
|
|||||||
// plugin under `DefaultPlugins`; register them explicitly so
|
// plugin under `DefaultPlugins`; register them explicitly so
|
||||||
// scroll systems run cleanly under `MinimalPlugins` in tests.
|
// scroll systems run cleanly under `MinimalPlugins` in tests.
|
||||||
.add_message::<MouseWheel>()
|
.add_message::<MouseWheel>()
|
||||||
.add_message::<bevy::input::touch::TouchInput>()
|
.add_message::<TouchInput>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -1878,7 +1878,7 @@ mod tests {
|
|||||||
|
|
||||||
let states: Vec<(HomeMode, bool)> = app
|
let states: Vec<(HomeMode, bool)> = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.query::<(&HomeModeCard, bevy::ecs::query::Has<Disabled>)>()
|
.query::<(&HomeModeCard, Has<Disabled>)>()
|
||||||
.iter(app.world())
|
.iter(app.world())
|
||||||
.map(|(c, d)| (c.0, d))
|
.map(|(c, d)| (c.0, d))
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -2428,8 +2428,8 @@ mod tests {
|
|||||||
app.init_resource::<HintSolverConfig>();
|
app.init_resource::<HintSolverConfig>();
|
||||||
app.init_resource::<crate::pending_hint::PendingHintTask>();
|
app.init_resource::<crate::pending_hint::PendingHintTask>();
|
||||||
app.init_resource::<ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.insert_resource(crate::layout::LayoutResource(
|
app.insert_resource(LayoutResource(
|
||||||
crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
|
compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
|
||||||
));
|
));
|
||||||
app.insert_resource(GameStateResource(GameState::new(42, DrawStockConfig::DrawOne)));
|
app.insert_resource(GameStateResource(GameState::new(42, DrawStockConfig::DrawOne)));
|
||||||
app.add_systems(Update, handle_keyboard_hint);
|
app.add_systems(Update, handle_keyboard_hint);
|
||||||
|
|||||||
@@ -745,7 +745,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
// The HUD band top clearance (distance from window top to card top)
|
// 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.
|
// 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
|
layout.pile_positions[&KlondikePile::Stock].y + layout.card_size.y / 2.0
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -862,7 +862,7 @@ fn handle_display_name_confirm(
|
|||||||
.leaderboard_display_name
|
.leaderboard_display_name
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
if let solitaire_data::settings::SyncBackend::SolitaireServer {
|
if let SyncBackend::SolitaireServer {
|
||||||
ref username,
|
ref username,
|
||||||
..
|
..
|
||||||
} = settings.0.sync_backend
|
} = settings.0.sync_backend
|
||||||
@@ -1091,7 +1091,7 @@ mod tests {
|
|||||||
.add_plugins(crate::achievement_plugin::AchievementPlugin::headless())
|
.add_plugins(crate::achievement_plugin::AchievementPlugin::headless())
|
||||||
.add_plugins(SyncPlugin::new(NoOpProvider))
|
.add_plugins(SyncPlugin::new(NoOpProvider))
|
||||||
.add_plugins(LeaderboardPlugin);
|
.add_plugins(LeaderboardPlugin);
|
||||||
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.update();
|
app.update();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2312,8 +2312,8 @@ fn format_suit_glyph_all_suits() {
|
|||||||
fn format_foundations_row_empty_board() {
|
fn format_foundations_row_empty_board() {
|
||||||
let game = solitaire_core::game_state::GameState::new_with_mode(
|
let game = solitaire_core::game_state::GameState::new_with_mode(
|
||||||
42,
|
42,
|
||||||
solitaire_core::DrawStockConfig::DrawOne,
|
DrawStockConfig::DrawOne,
|
||||||
solitaire_core::game_state::GameMode::Classic,
|
GameMode::Classic,
|
||||||
);
|
);
|
||||||
assert_eq!(format_foundations_row(&game), "F: -- -- -- --");
|
assert_eq!(format_foundations_row(&game), "F: -- -- -- --");
|
||||||
}
|
}
|
||||||
@@ -2324,8 +2324,8 @@ fn format_foundations_row_empty_board() {
|
|||||||
fn format_stock_waste_row_initial_state() {
|
fn format_stock_waste_row_initial_state() {
|
||||||
let game = solitaire_core::game_state::GameState::new_with_mode(
|
let game = solitaire_core::game_state::GameState::new_with_mode(
|
||||||
42,
|
42,
|
||||||
solitaire_core::DrawStockConfig::DrawOne,
|
DrawStockConfig::DrawOne,
|
||||||
solitaire_core::game_state::GameMode::Classic,
|
GameMode::Classic,
|
||||||
);
|
);
|
||||||
let text = format_stock_waste_row(&game);
|
let text = format_stock_waste_row(&game);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
//! Safe-area insets.
|
//! 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
|
//! Reports the OS-reserved regions around the playable surface (status
|
||||||
//! bar at the top, gesture / navigation bar at the bottom on Android,
|
//! bar at the top, gesture / navigation bar at the bottom on Android,
|
||||||
//! display cutouts, etc.) so UI anchored to a screen edge can avoid
|
//! display cutouts, etc.) so UI anchored to a screen edge can avoid
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ impl Plugin for SelectionPlugin {
|
|||||||
update_selection_highlight.after(GameMutation).run_if(
|
update_selection_highlight.after(GameMutation).run_if(
|
||||||
resource_changed::<SelectionState>
|
resource_changed::<SelectionState>
|
||||||
.or(resource_changed::<KeyboardDragState>)
|
.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
|
/// destination after a lift. Players who want a different column simply
|
||||||
/// press the right-arrow key once or twice.
|
/// press the right-arrow key once or twice.
|
||||||
pub(crate) fn legal_destinations_for(
|
pub(crate) fn legal_destinations_for(
|
||||||
_bottom: &solitaire_core::Card,
|
_bottom: &Card,
|
||||||
source: &KlondikePile,
|
source: &KlondikePile,
|
||||||
game: &GameState,
|
game: &GameState,
|
||||||
stack_count: usize,
|
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
|
/// 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
|
/// (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.
|
/// 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;
|
let mut count = 0;
|
||||||
for (_, face_up) in cards.iter().rev() {
|
for (_, face_up) in cards.iter().rev() {
|
||||||
if *face_up {
|
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
|
/// handler can attempt a foundation move first and fall through to a
|
||||||
/// multi-card stack move rather than accepting a single-card tableau move.
|
/// multi-card stack move rather than accepting a single-card tableau move.
|
||||||
fn try_foundation_dest(
|
fn try_foundation_dest(
|
||||||
card: &solitaire_core::Card,
|
card: &Card,
|
||||||
game: &solitaire_core::game_state::GameState,
|
game: &GameState,
|
||||||
) -> Option<KlondikePile> {
|
) -> Option<KlondikePile> {
|
||||||
let source = game.pile_containing_card(card.clone())?;
|
let source = game.pile_containing_card(card.clone())?;
|
||||||
for foundation in [
|
for foundation in [
|
||||||
|
|||||||
@@ -379,8 +379,8 @@ impl Plugin for SettingsPlugin {
|
|||||||
.add_message::<DeleteAccountRequestEvent>()
|
.add_message::<DeleteAccountRequestEvent>()
|
||||||
.add_message::<ToggleSettingsRequestEvent>()
|
.add_message::<ToggleSettingsRequestEvent>()
|
||||||
.add_message::<InfoToastEvent>()
|
.add_message::<InfoToastEvent>()
|
||||||
.add_message::<bevy::input::mouse::MouseWheel>()
|
.add_message::<MouseWheel>()
|
||||||
.add_message::<bevy::input::touch::TouchInput>()
|
.add_message::<TouchInput>()
|
||||||
// `WindowResized` / `WindowMoved` are real Bevy window events
|
// `WindowResized` / `WindowMoved` are real Bevy window events
|
||||||
// and emitted by the windowing backend under `DefaultPlugins`,
|
// and emitted by the windowing backend under `DefaultPlugins`,
|
||||||
// but we register them explicitly here so the geometry watcher
|
// 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 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
|
Ok(entries) => entries
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|e| e.path())
|
.map(|e| e.path())
|
||||||
@@ -3019,7 +3019,7 @@ mod tests {
|
|||||||
unit: MouseScrollUnit::Line,
|
unit: MouseScrollUnit::Line,
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
y: -3.0,
|
y: -3.0,
|
||||||
window: bevy::ecs::entity::Entity::PLACEHOLDER,
|
window: Entity::PLACEHOLDER,
|
||||||
});
|
});
|
||||||
app.update();
|
app.update();
|
||||||
// ScrollPosition must remain at 0.0 — panel was closed.
|
// ScrollPosition must remain at 0.0 — panel was closed.
|
||||||
@@ -3052,7 +3052,7 @@ mod tests {
|
|||||||
unit: MouseScrollUnit::Line,
|
unit: MouseScrollUnit::Line,
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
y: -2.0,
|
y: -2.0,
|
||||||
window: bevy::ecs::entity::Entity::PLACEHOLDER,
|
window: Entity::PLACEHOLDER,
|
||||||
});
|
});
|
||||||
app.update();
|
app.update();
|
||||||
let offset = app
|
let offset = app
|
||||||
@@ -3364,7 +3364,7 @@ mod tests {
|
|||||||
|
|
||||||
fn fire_resize(app: &mut App, width: f32, height: f32) {
|
fn fire_resize(app: &mut App, width: f32, height: f32) {
|
||||||
app.world_mut().write_message(WindowResized {
|
app.world_mut().write_message(WindowResized {
|
||||||
window: bevy::ecs::entity::Entity::PLACEHOLDER,
|
window: Entity::PLACEHOLDER,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
@@ -3372,7 +3372,7 @@ mod tests {
|
|||||||
|
|
||||||
fn fire_move(app: &mut App, x: i32, y: i32) {
|
fn fire_move(app: &mut App, x: i32, y: i32) {
|
||||||
app.world_mut().write_message(WindowMoved {
|
app.world_mut().write_message(WindowMoved {
|
||||||
window: bevy::ecs::entity::Entity::PLACEHOLDER,
|
window: Entity::PLACEHOLDER,
|
||||||
position: IVec2::new(x, y),
|
position: IVec2::new(x, y),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3494,7 +3494,7 @@ mod tests {
|
|||||||
unit: MouseScrollUnit::Line,
|
unit: MouseScrollUnit::Line,
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
y: 5.0,
|
y: 5.0,
|
||||||
window: bevy::ecs::entity::Entity::PLACEHOLDER,
|
window: Entity::PLACEHOLDER,
|
||||||
});
|
});
|
||||||
app.update();
|
app.update();
|
||||||
let offset = app
|
let offset = app
|
||||||
|
|||||||
@@ -1010,8 +1010,8 @@ mod tests {
|
|||||||
use solitaire_data::Settings;
|
use solitaire_data::Settings;
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.add_plugins(MinimalPlugins)
|
app.add_plugins(MinimalPlugins)
|
||||||
.add_plugins(bevy::asset::AssetPlugin::default())
|
.add_plugins(AssetPlugin::default())
|
||||||
.init_asset::<bevy::image::Image>()
|
.init_asset::<Image>()
|
||||||
.add_plugins(SplashPlugin);
|
.add_plugins(SplashPlugin);
|
||||||
app.init_resource::<ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.init_resource::<ButtonInput<MouseButton>>();
|
app.init_resource::<ButtonInput<MouseButton>>();
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ impl Plugin for StatsPlugin {
|
|||||||
// `DefaultPlugins`; register it explicitly so the stats-scroll
|
// `DefaultPlugins`; register it explicitly so the stats-scroll
|
||||||
// system also runs cleanly under `MinimalPlugins` in tests.
|
// system also runs cleanly under `MinimalPlugins` in tests.
|
||||||
.add_message::<MouseWheel>()
|
.add_message::<MouseWheel>()
|
||||||
.add_message::<bevy::input::touch::TouchInput>()
|
.add_message::<TouchInput>()
|
||||||
// record_abandoned must read `move_count` BEFORE handle_new_game
|
// record_abandoned must read `move_count` BEFORE handle_new_game
|
||||||
// clobbers it with a fresh game. These are NOT in StatsUpdate because
|
// clobbers it with a fresh game. These are NOT in StatsUpdate because
|
||||||
// StatsUpdate (as a set) is ordered after GameMutation by external
|
// 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
|
/// Pure helper: render the detail line for the selected replay. Returns
|
||||||
/// `"{duration} win on {date}"` plus a `" \u{2022} Shareable"` badge
|
/// `"{duration} win on {date}"` plus a `" \u{2022} Shareable"` badge
|
||||||
/// when a share URL is present. Empty when the history slice is empty.
|
/// 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 {
|
let Some(r) = replays.get(index.min(replays.len().saturating_sub(1))) else {
|
||||||
return String::new();
|
return String::new();
|
||||||
};
|
};
|
||||||
@@ -1325,7 +1325,7 @@ mod tests {
|
|||||||
fn draw_three_win_increments_draw_three_wins_only() {
|
fn draw_three_win_increments_draw_three_wins_only() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<crate::resources::GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
|
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
|
||||||
|
|
||||||
@@ -1371,7 +1371,7 @@ mod tests {
|
|||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
|
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<crate::resources::GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.set_test_move_count(3);
|
.set_test_move_count(3);
|
||||||
|
|
||||||
@@ -1501,7 +1501,7 @@ mod tests {
|
|||||||
fn zen_win_event_updates_zen_best_score_only() {
|
fn zen_win_event_updates_zen_best_score_only() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<crate::resources::GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.mode = solitaire_core::game_state::GameMode::Zen;
|
.mode = solitaire_core::game_state::GameMode::Zen;
|
||||||
|
|
||||||
@@ -1697,7 +1697,7 @@ mod tests {
|
|||||||
stats.0.win_streak_current = 3;
|
stats.0.win_streak_current = 3;
|
||||||
}
|
}
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<crate::resources::GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.set_test_move_count(1);
|
.set_test_move_count(1);
|
||||||
|
|
||||||
@@ -1723,7 +1723,7 @@ mod tests {
|
|||||||
stats.0.win_streak_current = 1;
|
stats.0.win_streak_current = 1;
|
||||||
}
|
}
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<crate::resources::GameStateResource>()
|
.resource_mut::<GameStateResource>()
|
||||||
.0
|
.0
|
||||||
.set_test_move_count(1);
|
.set_test_move_count(1);
|
||||||
|
|
||||||
@@ -1948,9 +1948,9 @@ mod tests {
|
|||||||
///
|
///
|
||||||
/// Uses a fixed seed, DrawOne mode, Classic game, 2026-05-08 date.
|
/// Uses a fixed seed, DrawOne mode, Classic game, 2026-05-08 date.
|
||||||
/// `time_seconds` and `share_url` are the only varying fields across tests.
|
/// `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 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,
|
1,
|
||||||
solitaire_core::DrawStockConfig::DrawOne,
|
solitaire_core::DrawStockConfig::DrawOne,
|
||||||
solitaire_core::game_state::GameMode::Classic,
|
solitaire_core::game_state::GameMode::Classic,
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ mod tests {
|
|||||||
.add_plugins(crate::achievement_plugin::AchievementPlugin::headless())
|
.add_plugins(crate::achievement_plugin::AchievementPlugin::headless())
|
||||||
.add_plugins(SyncPlugin::new(provider));
|
.add_plugins(SyncPlugin::new(provider));
|
||||||
// MinimalPlugins does not register keyboard input.
|
// MinimalPlugins does not register keyboard input.
|
||||||
app.init_resource::<bevy::input::ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.update();
|
app.update();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
@@ -629,8 +629,8 @@ mod tests {
|
|||||||
replays: vec![initial],
|
replays: vec![initial],
|
||||||
};
|
};
|
||||||
save_replay_history_to(&path, &history).expect("seed history on disk");
|
save_replay_history_to(&path, &history).expect("seed history on disk");
|
||||||
app.insert_resource(crate::stats_plugin::ReplayHistoryResource(history));
|
app.insert_resource(ReplayHistoryResource(history));
|
||||||
app.insert_resource(crate::stats_plugin::LatestReplayPath(Some(path.clone())));
|
app.insert_resource(LatestReplayPath(Some(path.clone())));
|
||||||
|
|
||||||
// Pre-resolved task carrying the URL the production path would
|
// Pre-resolved task carrying the URL the production path would
|
||||||
// get back from the server.
|
// get back from the server.
|
||||||
@@ -659,7 +659,7 @@ mod tests {
|
|||||||
// In-memory contract: replays[0].share_url is now Some(url).
|
// In-memory contract: replays[0].share_url is now Some(url).
|
||||||
let live = app
|
let live = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<crate::stats_plugin::ReplayHistoryResource>();
|
.resource::<ReplayHistoryResource>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
live.0.replays.first().and_then(|r| r.share_url.clone()),
|
live.0.replays.first().and_then(|r| r.share_url.clone()),
|
||||||
Some(url.clone()),
|
Some(url.clone()),
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ use solitaire_core::Suit;
|
|||||||
|
|
||||||
use crate::events::{HintVisualEvent, StateChangedEvent};
|
use crate::events::{HintVisualEvent, StateChangedEvent};
|
||||||
use crate::hud_plugin::HudVisibility;
|
use crate::hud_plugin::HudVisibility;
|
||||||
#[cfg(test)]
|
use crate::layout::{Layout, LayoutResource, LayoutSystem, TABLE_COLOUR, compute_layout};
|
||||||
use crate::layout::TABLE_COLOUR;
|
|
||||||
use crate::layout::{Layout, LayoutResource, LayoutSystem, compute_layout};
|
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
use crate::safe_area::SafeAreaInsets;
|
use crate::safe_area::SafeAreaInsets;
|
||||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||||
@@ -177,9 +175,9 @@ fn setup_table(
|
|||||||
Camera2d,
|
Camera2d,
|
||||||
Camera {
|
Camera {
|
||||||
clear_color: ClearColorConfig::Custom(Color::srgb(
|
clear_color: ClearColorConfig::Custom(Color::srgb(
|
||||||
crate::layout::TABLE_COLOUR[0],
|
TABLE_COLOUR[0],
|
||||||
crate::layout::TABLE_COLOUR[1],
|
TABLE_COLOUR[1],
|
||||||
crate::layout::TABLE_COLOUR[2],
|
TABLE_COLOUR[2],
|
||||||
)),
|
)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
/// Sums every entry's declared uncompressed size and rejects archives
|
||||||
/// that overflow [`MAX_ARCHIVE_BYTES`]. Iterates the central
|
/// that overflow [`MAX_ARCHIVE_BYTES`]. Iterates the central
|
||||||
/// directory only — does not actually decompress anything.
|
/// 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>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
) -> Result<(), ImportError> {
|
) -> Result<(), ImportError> {
|
||||||
let mut total: u64 = 0;
|
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
|
/// (after normalisation) escapes its root. Catches `..`, absolute
|
||||||
/// paths, drive prefixes on Windows, and the awkward case where
|
/// paths, drive prefixes on Windows, and the awkward case where
|
||||||
/// `enclosed_name` returns `None` because the entry is suspicious.
|
/// `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>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
) -> Result<(), ImportError> {
|
) -> Result<(), ImportError> {
|
||||||
for i in 0..archive.len() {
|
for i in 0..archive.len() {
|
||||||
@@ -291,7 +291,7 @@ fn is_safe_relative_path(p: &Path) -> bool {
|
|||||||
/// `theme.ron` entry at its root.
|
/// `theme.ron` entry at its root.
|
||||||
/// - [`ImportError::ManifestParse`] when the bytes don't form valid
|
/// - [`ImportError::ManifestParse`] when the bytes don't form valid
|
||||||
/// RON for `ThemeManifest`.
|
/// RON for `ThemeManifest`.
|
||||||
fn read_manifest<R: io::Read + io::Seek>(
|
fn read_manifest<R: Read + io::Seek>(
|
||||||
archive: &mut zip::ZipArchive<R>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
) -> Result<ThemeManifest, ImportError> {
|
) -> Result<ThemeManifest, ImportError> {
|
||||||
// We can't use `?` directly across `by_name` because a missing
|
// 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
|
/// Returns [`ImportError::MissingFile`] when the archive has no entry
|
||||||
/// matching the path.
|
/// 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>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<Vec<u8>, ImportError> {
|
) -> Result<Vec<u8>, ImportError> {
|
||||||
@@ -351,7 +351,7 @@ fn archive_key(path: &Path) -> String {
|
|||||||
/// parent directories as needed. The destination path is rebuilt from
|
/// parent directories as needed. The destination path is rebuilt from
|
||||||
/// the safe components we already vetted in
|
/// the safe components we already vetted in
|
||||||
/// [`enforce_zip_slip_safe`], not from the raw entry name.
|
/// [`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>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
name: &str,
|
name: &str,
|
||||||
staging: &Path,
|
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
|
/// Variant of [`write_archive_entry`] keyed by `Path` for the
|
||||||
/// manifest-declared face/back paths.
|
/// 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>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
staging: &Path,
|
staging: &Path,
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ mod tests {
|
|||||||
let mut image_set = empty_card_image_set();
|
let mut image_set = empty_card_image_set();
|
||||||
// Snapshot the legacy back ids so we can prove they don't
|
// Snapshot the legacy back ids so we can prove they don't
|
||||||
// change when a theme is applied.
|
// 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());
|
std::array::from_fn(|i| image_set.backs[i].id());
|
||||||
let theme = empty_theme();
|
let theme = empty_theme();
|
||||||
assert!(image_set.theme_back.is_none(), "theme_back starts empty");
|
assert!(image_set.theme_back.is_none(), "theme_back starts empty");
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ mod tests {
|
|||||||
// the session timer or the running win count.
|
// 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"))
|
std::env::temp_dir().join(format!("engine_test_ta_{name}.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1324,7 +1324,7 @@ mod tests {
|
|||||||
let row = world.spawn((FocusRow, Node::default())).id();
|
let row = world.spawn((FocusRow, Node::default())).id();
|
||||||
world.entity_mut(scrim).add_child(row);
|
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((
|
let mut e = w.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node::default(),
|
Node::default(),
|
||||||
|
|||||||
@@ -982,9 +982,9 @@ mod tests {
|
|||||||
outline_width: 0.0,
|
outline_width: 0.0,
|
||||||
outline_offset: 0.0,
|
outline_offset: 0.0,
|
||||||
unrounded_size: card_size,
|
unrounded_size: card_size,
|
||||||
border: bevy::sprite::BorderRect::default(),
|
border: BorderRect::default(),
|
||||||
border_radius: bevy::ui::ResolvedBorderRadius::default(),
|
border_radius: ResolvedBorderRadius::default(),
|
||||||
padding: bevy::sprite::BorderRect::default(),
|
padding: BorderRect::default(),
|
||||||
inverse_scale_factor: 1.0,
|
inverse_scale_factor: 1.0,
|
||||||
};
|
};
|
||||||
// `is_empty` guard inside Bevy treats zero-size
|
// `is_empty` guard inside Bevy treats zero-size
|
||||||
|
|||||||
@@ -249,12 +249,12 @@ pub const BORDER_SUBTLE_HC: Color = Color::srgba(0.627, 0.627, 0.627, 1.0);
|
|||||||
pub struct HighContrastBorder {
|
pub struct HighContrastBorder {
|
||||||
/// Border colour to use when high-contrast mode is *off* — the
|
/// Border colour to use when high-contrast mode is *off* — the
|
||||||
/// site's normal idle / active-state colour.
|
/// site's normal idle / active-state colour.
|
||||||
pub default_color: bevy::prelude::Color,
|
pub default_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HighContrastBorder {
|
impl HighContrastBorder {
|
||||||
/// Convenience constructor — `HighContrastBorder::with_default(BORDER_SUBTLE)`.
|
/// 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 }
|
Self { default_color }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,18 +282,18 @@ impl HighContrastBorder {
|
|||||||
pub struct HighContrastBackground {
|
pub struct HighContrastBackground {
|
||||||
/// Background colour to use when high-contrast mode is *off* —
|
/// Background colour to use when high-contrast mode is *off* —
|
||||||
/// the site's normal idle / active-state colour.
|
/// 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*.
|
/// Background colour to use when high-contrast mode is *on*.
|
||||||
/// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
|
/// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
|
||||||
///
|
///
|
||||||
/// [`with_default`]: HighContrastBackground::with_default
|
/// [`with_default`]: HighContrastBackground::with_default
|
||||||
pub hc_color: bevy::prelude::Color,
|
pub hc_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HighContrastBackground {
|
impl HighContrastBackground {
|
||||||
/// Convenience constructor — HC colour defaults to
|
/// Convenience constructor — HC colour defaults to
|
||||||
/// [`BORDER_SUBTLE_HC`].
|
/// [`BORDER_SUBTLE_HC`].
|
||||||
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
|
pub const fn with_default(default_color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
default_color,
|
default_color,
|
||||||
hc_color: BORDER_SUBTLE_HC,
|
hc_color: BORDER_SUBTLE_HC,
|
||||||
@@ -305,8 +305,8 @@ impl HighContrastBackground {
|
|||||||
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
|
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
|
||||||
/// than to a neutral gray.
|
/// than to a neutral gray.
|
||||||
pub const fn with_hc(
|
pub const fn with_hc(
|
||||||
default_color: bevy::prelude::Color,
|
default_color: Color,
|
||||||
hc_color: bevy::prelude::Color,
|
hc_color: Color,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
default_color,
|
default_color,
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ fn spawn_win_summary_after_delay(
|
|||||||
// speed the duration is zero anyway, suppressing the shake.
|
// speed the duration is zero anyway, suppressing the shake.
|
||||||
let speed = settings
|
let speed = settings
|
||||||
.as_ref()
|
.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);
|
let scaled = scaled_duration(SHAKE_DURATION_SECS, speed);
|
||||||
shake.remaining = scaled;
|
shake.remaining = scaled;
|
||||||
shake.total = scaled;
|
shake.total = scaled;
|
||||||
|
|||||||
@@ -32,3 +32,6 @@ dotenvy = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ struct UserIdKeyExtractor {
|
|||||||
impl KeyExtractor for UserIdKeyExtractor {
|
impl KeyExtractor for UserIdKeyExtractor {
|
||||||
type Key = String;
|
type Key = String;
|
||||||
|
|
||||||
fn extract<T>(&self, req: &axum::http::Request<T>) -> Result<Self::Key, GovernorError> {
|
fn extract<T>(&self, req: &Request<T>) -> Result<Self::Key, GovernorError> {
|
||||||
if let Some(user_id) = self.try_extract_user_id(req.headers()) {
|
if let Some(user_id) = self.try_extract_user_id(req.headers()) {
|
||||||
return Ok(user_id);
|
return Ok(user_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ async fn register_login_push_pull_full_roundtrip() {
|
|||||||
},
|
},
|
||||||
achievements: vec![],
|
achievements: vec![],
|
||||||
progress: PlayerProgress::default(),
|
progress: PlayerProgress::default(),
|
||||||
last_modified: chrono::Utc::now(),
|
last_modified: Utc::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let push_resp = post_authed(
|
let push_resp = post_authed(
|
||||||
@@ -1299,7 +1299,7 @@ async fn expired_access_token_returns_401() {
|
|||||||
exp: usize,
|
exp: usize,
|
||||||
kind: String,
|
kind: String,
|
||||||
}
|
}
|
||||||
let exp = (chrono::Utc::now() - chrono::Duration::hours(2)).timestamp() as usize;
|
let exp = (Utc::now() - chrono::Duration::hours(2)).timestamp() as usize;
|
||||||
let expired_token = encode(
|
let expired_token = encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&ExpiredClaims {
|
&ExpiredClaims {
|
||||||
@@ -1375,7 +1375,7 @@ async fn refresh_with_expired_refresh_token_returns_401() {
|
|||||||
exp: usize,
|
exp: usize,
|
||||||
kind: String,
|
kind: String,
|
||||||
}
|
}
|
||||||
let exp = (chrono::Utc::now() - chrono::Duration::hours(2)).timestamp() as usize;
|
let exp = (Utc::now() - chrono::Duration::hours(2)).timestamp() as usize;
|
||||||
let expired_token = encode(
|
let expired_token = encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&ExpiredRefreshClaims {
|
&ExpiredRefreshClaims {
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ serde = { workspace = true }
|
|||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -755,14 +755,14 @@ mod tests {
|
|||||||
fn progress_total_xp_takes_max() {
|
fn progress_total_xp_takes_max() {
|
||||||
let mut local = default_payload();
|
let mut local = default_payload();
|
||||||
local.progress.total_xp = 1500;
|
local.progress.total_xp = 1500;
|
||||||
local.progress.level = crate::progress::level_for_xp(1500);
|
local.progress.level = level_for_xp(1500);
|
||||||
let mut remote = default_payload();
|
let mut remote = default_payload();
|
||||||
remote.progress.total_xp = 2500;
|
remote.progress.total_xp = 2500;
|
||||||
remote.progress.level = crate::progress::level_for_xp(2500);
|
remote.progress.level = level_for_xp(2500);
|
||||||
|
|
||||||
let (merged, _) = merge(&local, &remote);
|
let (merged, _) = merge(&local, &remote);
|
||||||
assert_eq!(merged.progress.total_xp, 2500);
|
assert_eq!(merged.progress.total_xp, 2500);
|
||||||
assert_eq!(merged.progress.level, crate::progress::level_for_xp(2500));
|
assert_eq!(merged.progress.level, level_for_xp(2500));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -815,7 +815,7 @@ mod tests {
|
|||||||
|
|
||||||
let (merged, _) = merge(&local, &remote);
|
let (merged, _) = merge(&local, &remote);
|
||||||
assert_eq!(merged.progress.total_xp, 5500);
|
assert_eq!(merged.progress.total_xp, 5500);
|
||||||
assert_eq!(merged.progress.level, crate::progress::level_for_xp(5500));
|
assert_eq!(merged.progress.level, level_for_xp(5500));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -1051,11 +1051,11 @@ mod tests {
|
|||||||
// drop the oldest 50 so the cap is preserved.
|
// drop the oldest 50 so the cap is preserved.
|
||||||
let start = nd(2024, 1, 1);
|
let start = nd(2024, 1, 1);
|
||||||
let local_dates: Vec<NaiveDate> = (0..DAILY_CHALLENGE_HISTORY_CAP as i64)
|
let local_dates: Vec<NaiveDate> = (0..DAILY_CHALLENGE_HISTORY_CAP as i64)
|
||||||
.map(|i| start + chrono::Duration::days(i))
|
.map(|i| start + Duration::days(i))
|
||||||
.collect();
|
.collect();
|
||||||
let remote_dates: Vec<NaiveDate> = (DAILY_CHALLENGE_HISTORY_CAP as i64
|
let remote_dates: Vec<NaiveDate> = (DAILY_CHALLENGE_HISTORY_CAP as i64
|
||||||
..DAILY_CHALLENGE_HISTORY_CAP as i64 + 50)
|
..DAILY_CHALLENGE_HISTORY_CAP as i64 + 50)
|
||||||
.map(|i| start + chrono::Duration::days(i))
|
.map(|i| start + Duration::days(i))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut local = default_payload();
|
let mut local = default_payload();
|
||||||
@@ -1073,7 +1073,7 @@ mod tests {
|
|||||||
// is therefore start + 50 days.
|
// is therefore start + 50 days.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
merged.progress.daily_challenge_history.first().copied(),
|
merged.progress.daily_challenge_history.first().copied(),
|
||||||
Some(start + chrono::Duration::days(50))
|
Some(start + Duration::days(50))
|
||||||
);
|
);
|
||||||
// Most recent retained is the last remote date.
|
// Most recent retained is the last remote date.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ web-sys = { version = "0.3", features = ["console"] }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -21,3 +21,6 @@ console_error_panic_hook = "0.1"
|
|||||||
# renderer to WebGL2 compatibility limits, which is wrong for native builds.
|
# renderer to WebGL2 compatibility limits, which is wrong for native builds.
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
bevy = { workspace = true, features = ["webgl2"] }
|
bevy = { workspace = true, features = ["webgl2"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
Reference in New Issue
Block a user