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:
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solitaire_core::game_state::{GameState, GAME_STATE_SCHEMA_VERSION};
|
||||
use solitaire_core::game_state::{GAME_STATE_SCHEMA_VERSION, GameState};
|
||||
|
||||
use crate::stats::StatsSnapshot;
|
||||
|
||||
@@ -57,9 +57,8 @@ pub fn load_stats() -> StatsSnapshot {
|
||||
/// Save stats to the platform default path. Returns an error if the platform
|
||||
/// data dir is unavailable or the write fails.
|
||||
pub fn save_stats(stats: &StatsSnapshot) -> io::Result<()> {
|
||||
let path = stats_file_path().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::NotFound, "platform data dir unavailable")
|
||||
})?;
|
||||
let path = stats_file_path()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "platform data dir unavailable"))?;
|
||||
save_stats_to(&path, stats)
|
||||
}
|
||||
|
||||
@@ -89,11 +88,7 @@ pub fn load_game_state_from(path: &Path) -> Option<GameState> {
|
||||
if gs.schema_version != GAME_STATE_SCHEMA_VERSION {
|
||||
return None;
|
||||
}
|
||||
if gs.is_won {
|
||||
None
|
||||
} else {
|
||||
Some(gs)
|
||||
}
|
||||
if gs.is_won { None } else { Some(gs) }
|
||||
}
|
||||
|
||||
/// Save an in-progress `GameState` atomically. Skips the write if `gs.is_won`
|
||||
@@ -180,7 +175,10 @@ pub struct TimeAttackSession {
|
||||
/// Returns the platform-specific path to `time_attack_session.json`, or
|
||||
/// `None` if `crate::data_dir()` is unavailable.
|
||||
pub fn time_attack_session_path() -> Option<PathBuf> {
|
||||
crate::data_dir().map(|d| d.join(crate::APP_DIR_NAME).join(TIME_ATTACK_SESSION_FILE_NAME))
|
||||
crate::data_dir().map(|d| {
|
||||
d.join(crate::APP_DIR_NAME)
|
||||
.join(TIME_ATTACK_SESSION_FILE_NAME)
|
||||
})
|
||||
}
|
||||
|
||||
/// Save a Time Attack session atomically. Mirrors `save_game_state_to`'s
|
||||
@@ -422,7 +420,10 @@ mod tests {
|
||||
let mut gs = GameState::new(99, DrawMode::DrawOne);
|
||||
gs.is_won = true;
|
||||
save_game_state_to(&path, &gs).expect("save should be no-op, not error");
|
||||
assert!(!path.exists(), "should not have written a file for a won game");
|
||||
assert!(
|
||||
!path.exists(),
|
||||
"should not have written a file for a won game"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -556,7 +557,10 @@ mod tests {
|
||||
loaded.remaining_secs,
|
||||
);
|
||||
assert_eq!(loaded.wins, 3, "wins must round-trip");
|
||||
assert_eq!(loaded.saved_at_unix_secs, saved_at, "timestamp must round-trip");
|
||||
assert_eq!(
|
||||
loaded.saved_at_unix_secs, saved_at,
|
||||
"timestamp must round-trip"
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user