4b9d008be2
Conservative cleanup pass — applied only the high-signal pedantic lints whose fixes either remove genuine waste or read more naturally, skipping anything stylistic that would bloat the diff. - map_unwrap_or: 29 .map(...).unwrap_or(...) sites collapsed to .map_or / .is_some_and / .map_or_else equivalents - uninlined_format_args: 7 production format!/write!/println! sites rewritten to the inline-argument style; assert! sites in test code intentionally untouched - match_same_arms: 2 redundant arms collapsed where the bodies were identical and the merger didn't obscure intent Public API is unchanged. No dependencies added or removed. The pedantic warning count dropped from 840 to 807 (-33). Out-of-scope findings — needless_pass_by_value on Bevy Res params, false-positive explicit_iter_loop on Bevy Query iterators, items_after_statements inside test mods, and the "ask before changing" merge logic in solitaire_sync — were intentionally deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
6.9 KiB
Rust
158 lines
6.9 KiB
Rust
use std::fs::OpenOptions;
|
||
use std::io::Write;
|
||
use std::time::{SystemTime, UNIX_EPOCH};
|
||
|
||
use bevy::prelude::*;
|
||
use bevy::window::{MonitorSelection, PresentMode, WindowPosition};
|
||
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings};
|
||
use solitaire_engine::{
|
||
AchievementPlugin, AnimationPlugin, AudioPlugin, AutoCompletePlugin, CardAnimationPlugin,
|
||
CardPlugin, ChallengePlugin, CursorPlugin, DailyChallengePlugin, FeedbackAnimPlugin,
|
||
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
||
OnboardingPlugin, PausePlugin, ProfilePlugin, ProgressPlugin, SelectionPlugin, SettingsPlugin,
|
||
SplashPlugin, StatsPlugin, SyncPlugin, TablePlugin, TimeAttackPlugin, UiFocusPlugin,
|
||
UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
||
};
|
||
|
||
fn main() {
|
||
// Install a panic hook that writes a crash log next to the save files
|
||
// before re-running the default hook (so stderr still gets the message
|
||
// and any debugger attached still sees the panic).
|
||
install_crash_log_hook();
|
||
|
||
// Initialise the platform keyring store before any token operations.
|
||
// On Linux this uses the Secret Service (GNOME Keyring / KWallet); on
|
||
// macOS it uses the Keychain; on Windows it uses the Credential store.
|
||
// If the platform has no OS keyring (e.g. a headless CI box), keyring
|
||
// operations will fail gracefully with TokenError::KeychainUnavailable.
|
||
if let Err(e) = keyring::use_native_store(true) {
|
||
eprintln!(
|
||
"warn: could not initialise OS keyring ({e}); \
|
||
server sync login will be unavailable"
|
||
);
|
||
}
|
||
|
||
// Load settings before building the app so we can construct the right
|
||
// sync provider. Falls back to defaults if no settings file exists yet.
|
||
let settings: Settings = settings_file_path()
|
||
.map(|p| load_settings_from(&p))
|
||
.unwrap_or_default();
|
||
let sync_provider = provider_for_backend(&settings.sync_backend);
|
||
|
||
// Restore the previous window geometry if the player has one saved.
|
||
// Otherwise open at the platform default (1280×800, centred on the
|
||
// primary monitor). The window_geometry field is None on first run
|
||
// and after upgrading from a build that didn't persist geometry.
|
||
let (window_resolution, window_position) = match settings.window_geometry {
|
||
Some(geom) => (
|
||
(geom.width, geom.height).into(),
|
||
WindowPosition::At(IVec2::new(geom.x, geom.y)),
|
||
),
|
||
None => (
|
||
(1280u32, 800u32).into(),
|
||
WindowPosition::Centered(MonitorSelection::Primary),
|
||
),
|
||
};
|
||
|
||
App::new()
|
||
.add_plugins(
|
||
DefaultPlugins
|
||
.set(WindowPlugin {
|
||
primary_window: Some(Window {
|
||
title: "Solitaire Quest".into(),
|
||
// X11/Wayland WM_CLASS so taskbar managers group
|
||
// multiple windows of this app correctly.
|
||
name: Some("solitaire-quest".into()),
|
||
resolution: window_resolution,
|
||
position: window_position,
|
||
// AutoNoVsync prefers Mailbox (triple-buffered) and
|
||
// falls back to Immediate, eliminating the vsync stall
|
||
// that AutoVsync produces during continuous window
|
||
// resize on X11 / Wayland. The game's frame budget is
|
||
// small enough that a few stray dropped frames from
|
||
// disabling vsync are imperceptible.
|
||
present_mode: PresentMode::AutoNoVsync,
|
||
resize_constraints: bevy::window::WindowResizeConstraints {
|
||
min_width: 800.0,
|
||
min_height: 600.0,
|
||
..default()
|
||
},
|
||
..default()
|
||
}),
|
||
..default()
|
||
})
|
||
// The `assets/` directory lives at the workspace root, but
|
||
// Bevy resolves `AssetPlugin::file_path` relative to the
|
||
// binary package's `CARGO_MANIFEST_DIR` (`solitaire_app/`).
|
||
// Point one level up so `cargo run -p solitaire_app` finds
|
||
// card faces, backs, backgrounds, and the UI font.
|
||
.set(bevy::asset::AssetPlugin {
|
||
file_path: "../assets".to_string(),
|
||
..default()
|
||
}),
|
||
)
|
||
.add_plugins(FontPlugin)
|
||
.add_plugins(GamePlugin)
|
||
.add_plugins(TablePlugin)
|
||
.add_plugins(CardPlugin)
|
||
.add_plugins(CursorPlugin)
|
||
.add_plugins(InputPlugin)
|
||
.add_plugins(SelectionPlugin)
|
||
.add_plugins(AnimationPlugin)
|
||
.add_plugins(FeedbackAnimPlugin)
|
||
.add_plugins(CardAnimationPlugin)
|
||
.add_plugins(AutoCompletePlugin)
|
||
.add_plugins(StatsPlugin::default())
|
||
.add_plugins(ProgressPlugin::default())
|
||
.add_plugins(AchievementPlugin::default())
|
||
.add_plugins(DailyChallengePlugin)
|
||
.add_plugins(WeeklyGoalsPlugin)
|
||
.add_plugins(ChallengePlugin)
|
||
.add_plugins(TimeAttackPlugin)
|
||
.add_plugins(HudPlugin)
|
||
.add_plugins(HelpPlugin)
|
||
.add_plugins(HomePlugin)
|
||
.add_plugins(ProfilePlugin)
|
||
.add_plugins(PausePlugin)
|
||
.add_plugins(SettingsPlugin::default())
|
||
.add_plugins(AudioPlugin)
|
||
.add_plugins(OnboardingPlugin)
|
||
.add_plugins(SyncPlugin::new(sync_provider))
|
||
.add_plugins(LeaderboardPlugin)
|
||
.add_plugins(WinSummaryPlugin)
|
||
.add_plugins(UiModalPlugin)
|
||
.add_plugins(UiFocusPlugin)
|
||
.add_plugins(UiTooltipPlugin)
|
||
.add_plugins(SplashPlugin)
|
||
.run();
|
||
}
|
||
|
||
/// Wraps the default panic hook with one that also appends a crash log
|
||
/// to `<data_dir>/crash.log` (next to `settings.json`). The default hook
|
||
/// still runs afterwards, so stderr output and debugger integration are
|
||
/// unchanged. If the data directory is unavailable, the wrapper silently
|
||
/// falls through — the default hook handles output either way.
|
||
fn install_crash_log_hook() {
|
||
let crash_log_path = settings_file_path().and_then(|p| {
|
||
p.parent()
|
||
.map(|parent| parent.join("crash.log"))
|
||
});
|
||
let default_hook = std::panic::take_hook();
|
||
std::panic::set_hook(Box::new(move |info| {
|
||
if let Some(path) = crash_log_path.as_ref()
|
||
&& let Ok(mut file) = OpenOptions::new()
|
||
.create(true)
|
||
.append(true)
|
||
.open(path)
|
||
{
|
||
// Plain unix-seconds timestamp keeps the format trivially
|
||
// parseable and avoids pulling in chrono just for this.
|
||
let secs = SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.map_or(0, |d| d.as_secs());
|
||
let _ = writeln!(file, "----- t={secs} -----\n{info}\n");
|
||
}
|
||
default_hook(info);
|
||
}));
|
||
}
|