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
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:
@@ -14,15 +14,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool, Task};
|
||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use solitaire_data::{
|
||||
save_achievements_to, save_progress_to, save_replay_history_to, save_stats_to,
|
||||
AchievementRecord, PlayerProgress, Replay, StatsSnapshot, SyncError, SyncProvider,
|
||||
save_achievements_to, save_progress_to, save_replay_history_to, save_stats_to,
|
||||
};
|
||||
use solitaire_sync::{merge, SyncPayload, SyncResponse};
|
||||
use solitaire_sync::{SyncPayload, SyncResponse, merge};
|
||||
|
||||
use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath};
|
||||
use crate::events::{
|
||||
@@ -32,7 +32,9 @@ use crate::events::{
|
||||
use crate::game_plugin::RecordingReplay;
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath};
|
||||
use crate::resources::{GameStateResource, SyncStatus, SyncStatusResource, TokioRuntimeResource};
|
||||
use crate::stats_plugin::{LatestReplayPath, ReplayHistoryResource, StatsResource, StatsStoragePath};
|
||||
use crate::stats_plugin::{
|
||||
LatestReplayPath, ReplayHistoryResource, StatsResource, StatsStoragePath,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public resources
|
||||
@@ -148,9 +150,7 @@ fn start_pull(
|
||||
) {
|
||||
let provider = provider.0.clone();
|
||||
let rt = rt.0.clone();
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
rt.block_on(provider.pull())
|
||||
});
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move { rt.block_on(provider.pull()) });
|
||||
task_res.0 = Some(task);
|
||||
status.0 = SyncStatus::Syncing;
|
||||
}
|
||||
@@ -173,9 +173,7 @@ fn handle_manual_sync_request(
|
||||
}
|
||||
let provider = provider.0.clone();
|
||||
let rt = rt.0.clone();
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
rt.block_on(provider.pull())
|
||||
});
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move { rt.block_on(provider.pull()) });
|
||||
task_res.0 = Some(task);
|
||||
status.0 = SyncStatus::Syncing;
|
||||
}
|
||||
@@ -219,17 +217,20 @@ fn poll_pull_result(
|
||||
|
||||
// Persist merged state atomically.
|
||||
if let Some(p) = &stats_path.0
|
||||
&& let Err(e) = save_stats_to(p, &merged.stats) {
|
||||
warn!("sync: failed to persist stats: {e}");
|
||||
}
|
||||
&& let Err(e) = save_stats_to(p, &merged.stats)
|
||||
{
|
||||
warn!("sync: failed to persist stats: {e}");
|
||||
}
|
||||
if let Some(p) = &achievements_path.0
|
||||
&& let Err(e) = save_achievements_to(p, &merged.achievements) {
|
||||
warn!("sync: failed to persist achievements: {e}");
|
||||
}
|
||||
&& let Err(e) = save_achievements_to(p, &merged.achievements)
|
||||
{
|
||||
warn!("sync: failed to persist achievements: {e}");
|
||||
}
|
||||
if let Some(p) = &progress_path.0
|
||||
&& let Err(e) = save_progress_to(p, &merged.progress) {
|
||||
warn!("sync: failed to persist progress: {e}");
|
||||
}
|
||||
&& let Err(e) = save_progress_to(p, &merged.progress)
|
||||
{
|
||||
warn!("sync: failed to persist progress: {e}");
|
||||
}
|
||||
|
||||
// Update in-world resources.
|
||||
let now = Utc::now();
|
||||
@@ -342,9 +343,8 @@ fn push_replay_on_win(
|
||||
);
|
||||
let provider = provider.0.clone();
|
||||
let rt = rt.0.clone();
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
rt.block_on(provider.push_replay(&replay))
|
||||
});
|
||||
let task = AsyncComputeTaskPool::get()
|
||||
.spawn(async move { rt.block_on(provider.push_replay(&replay)) });
|
||||
// If a previous upload is still in flight, drop it — the most
|
||||
// recent win is the one whose share link the player will care
|
||||
// about. Bevy's `Task` Drop cancels cooperatively.
|
||||
@@ -520,10 +520,7 @@ mod tests {
|
||||
// Status is either Syncing (task still running) or LastSynced (resolved).
|
||||
let status = &app.world().resource::<SyncStatusResource>().0;
|
||||
assert!(
|
||||
matches!(
|
||||
status,
|
||||
SyncStatus::Syncing | SyncStatus::LastSynced(_)
|
||||
),
|
||||
matches!(status, SyncStatus::Syncing | SyncStatus::LastSynced(_)),
|
||||
"status should be Syncing or LastSynced, got {status:?}"
|
||||
);
|
||||
}
|
||||
@@ -539,8 +536,7 @@ mod tests {
|
||||
// mirrors the auto-save flake fix and turns this test from
|
||||
// "pass on a fast machine" into "pass on any machine that
|
||||
// makes meaningful progress".
|
||||
let deadline =
|
||||
std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
loop {
|
||||
app.update();
|
||||
if matches!(
|
||||
@@ -565,8 +561,7 @@ mod tests {
|
||||
fn pull_failure_fires_warning_toast() {
|
||||
use bevy::ecs::message::Messages;
|
||||
let mut app = headless_app_with(FailingProvider);
|
||||
let deadline =
|
||||
std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
loop {
|
||||
app.update();
|
||||
if matches!(
|
||||
@@ -590,17 +585,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn build_payload_sets_nil_user_id() {
|
||||
let payload = build_payload(
|
||||
&StatsSnapshot::default(),
|
||||
&[],
|
||||
&PlayerProgress::default(),
|
||||
);
|
||||
let payload = build_payload(&StatsSnapshot::default(), &[], &PlayerProgress::default());
|
||||
assert_eq!(payload.user_id, Uuid::nil());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_payload_clones_stats() {
|
||||
let stats = StatsSnapshot { games_played: 42, ..Default::default() };
|
||||
let stats = StatsSnapshot {
|
||||
games_played: 42,
|
||||
..Default::default()
|
||||
};
|
||||
let payload = build_payload(&stats, &[], &PlayerProgress::default());
|
||||
assert_eq!(payload.stats.games_played, 42);
|
||||
}
|
||||
@@ -615,12 +609,11 @@ mod tests {
|
||||
fn upload_result_writes_share_url_into_replay_and_persists() {
|
||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||
use solitaire_data::{
|
||||
load_replay_history_from, save_replay_history_to, Replay, ReplayHistory,
|
||||
Replay, ReplayHistory, load_replay_history_from, save_replay_history_to,
|
||||
};
|
||||
|
||||
let mut app = headless_app_with(NoOpProvider);
|
||||
let path = std::env::temp_dir()
|
||||
.join("solitaire_test_replay_share_url_persist.json");
|
||||
let path = std::env::temp_dir().join("solitaire_test_replay_share_url_persist.json");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
// Seed the in-memory history with a single replay carrying no
|
||||
@@ -649,9 +642,7 @@ mod tests {
|
||||
let url = url.clone();
|
||||
async move { Ok::<String, SyncError>(url) }
|
||||
});
|
||||
app.world_mut()
|
||||
.resource_mut::<PendingReplayUpload>()
|
||||
.0 = Some(task);
|
||||
app.world_mut().resource_mut::<PendingReplayUpload>().0 = Some(task);
|
||||
|
||||
// Pump frames until the polling system observes the task as
|
||||
// ready and clears `PendingReplayUpload`.
|
||||
|
||||
Reference in New Issue
Block a user