fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
Build and Deploy / build-and-push (push) Successful in 3m54s

- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
       queries already in .sqlx cache; EXISTS variant would require sqlx prepare

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funman300
2026-05-28 13:07:22 -07:00
parent 8cb4c9808e
commit 6e407a3ea7
104 changed files with 6356 additions and 3092 deletions
+17 -14
View File
@@ -9,7 +9,7 @@ use std::path::PathBuf;
use bevy::prelude::*;
use solitaire_data::{
load_progress_from, progress_file_path, save_progress_to, xp_for_win, PlayerProgress,
PlayerProgress, load_progress_from, progress_file_path, save_progress_to, xp_for_win,
};
use crate::events::{GameWonEvent, XpAwardedEvent};
@@ -74,9 +74,7 @@ impl Plugin for ProgressPlugin {
.add_message::<GameWonEvent>()
.add_systems(
Update,
award_xp_on_win
.after(GameMutation)
.in_set(ProgressUpdate),
award_xp_on_win.after(GameMutation).in_set(ProgressUpdate),
);
}
}
@@ -102,9 +100,10 @@ fn award_xp_on_win(
});
}
if let Some(target) = &path.0
&& let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress: {e}");
}
&& let Err(e) = save_progress_to(target, &progress.0)
{
warn!("failed to save progress: {e}");
}
}
}
@@ -183,7 +182,10 @@ mod tests {
fn crossing_500_xp_fires_levelup_event() {
let mut app = headless_app();
// Pre-load 480 XP so a 75-XP win pushes us over the 500 boundary.
app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 480;
app.world_mut()
.resource_mut::<ProgressResource>()
.0
.total_xp = 480;
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -233,7 +235,10 @@ mod tests {
#[test]
fn levelup_event_total_xp_matches_progress_resource() {
let mut app = headless_app();
app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 480;
app.world_mut()
.resource_mut::<ProgressResource>()
.0
.total_xp = 480;
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -255,13 +260,11 @@ mod tests {
// score=0 in the event (Zen keeps score at 0), time=300 (no speed bonus),
// undo_count=0 so no-undo bonus applies: expected 50+25=75.
let mut app = headless_app();
app.world_mut()
.resource_mut::<GameStateResource>()
.0
.mode = solitaire_core::game_state::GameMode::Zen;
app.world_mut().resource_mut::<GameStateResource>().0.mode =
solitaire_core::game_state::GameMode::Zen;
app.world_mut().write_message(GameWonEvent {
score: 0, // Zen mode keeps score at 0
score: 0, // Zen mode keeps score at 0
time_seconds: 300,
});
app.update();