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
+89 -22
View File
@@ -355,7 +355,11 @@ mod tests {
ids.sort();
let len = ids.len();
ids.dedup();
assert_eq!(ids.len(), len, "duplicate achievement ID in ALL_ACHIEVEMENTS");
assert_eq!(
ids.len(),
len,
"duplicate achievement ID in ALL_ACHIEVEMENTS"
);
}
#[test]
@@ -422,13 +426,19 @@ mod tests {
for hour in [22u32, 23, 0, 1, 2] {
c.wall_clock_hour = Some(hour);
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"night_owl"), "expected night_owl at hour {hour}");
assert!(
ids.contains(&"night_owl"),
"expected night_owl at hour {hour}"
);
}
// Daytime hours must not trigger.
for hour in [3u32, 7, 12, 20, 21] {
c.wall_clock_hour = Some(hour);
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"night_owl"), "unexpected night_owl at hour {hour}");
assert!(
!ids.contains(&"night_owl"),
"unexpected night_owl at hour {hour}"
);
}
}
@@ -440,13 +450,19 @@ mod tests {
for hour in [5u32, 6] {
c.wall_clock_hour = Some(hour);
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"early_bird"), "expected early_bird at hour {hour}");
assert!(
ids.contains(&"early_bird"),
"expected early_bird at hour {hour}"
);
}
// Outside the window must not trigger.
for hour in [0u32, 3, 4, 7, 12, 23] {
c.wall_clock_hour = Some(hour);
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"early_bird"), "unexpected early_bird at hour {hour}");
assert!(
!ids.contains(&"early_bird"),
"unexpected early_bird at hour {hour}"
);
}
}
@@ -506,7 +522,10 @@ mod tests {
#[test]
fn achievement_by_id_finds_known_and_returns_none_for_unknown() {
assert_eq!(achievement_by_id("first_win").map(|d| d.name), Some("First Win"));
assert_eq!(
achievement_by_id("first_win").map(|d| d.name),
Some("First Win")
);
assert!(achievement_by_id("nonexistent").is_none());
}
@@ -538,7 +557,10 @@ mod tests {
let mut c = ctx_defaults();
c.last_win_time_seconds = 179;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"speed_demon"), "speed_demon should unlock at 179s");
assert!(
ids.contains(&"speed_demon"),
"speed_demon should unlock at 179s"
);
}
#[test]
@@ -546,7 +568,10 @@ mod tests {
let mut c = ctx_defaults();
c.last_win_time_seconds = 181;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"speed_demon"), "speed_demon must not unlock at 181s");
assert!(
!ids.contains(&"speed_demon"),
"speed_demon must not unlock at 181s"
);
}
#[test]
@@ -562,7 +587,10 @@ mod tests {
let mut c = ctx_defaults();
c.last_win_time_seconds = 90;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"lightning"), "lightning must not unlock at exactly 90s");
assert!(
!ids.contains(&"lightning"),
"lightning must not unlock at exactly 90s"
);
}
#[test]
@@ -570,7 +598,10 @@ mod tests {
let mut c = ctx_defaults();
c.last_win_used_undo = false;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"no_undo"), "no_undo should unlock when undo was not used");
assert!(
ids.contains(&"no_undo"),
"no_undo should unlock when undo was not used"
);
}
#[test]
@@ -578,7 +609,10 @@ mod tests {
let mut c = ctx_defaults();
c.last_win_used_undo = true;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"no_undo"), "no_undo must not unlock when undo was used");
assert!(
!ids.contains(&"no_undo"),
"no_undo must not unlock when undo was used"
);
}
#[test]
@@ -586,7 +620,10 @@ mod tests {
let mut c = ctx_defaults();
c.best_single_score = 5_000;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"high_scorer"), "high_scorer should unlock at best_single_score=5000");
assert!(
ids.contains(&"high_scorer"),
"high_scorer should unlock at best_single_score=5000"
);
}
#[test]
@@ -594,7 +631,10 @@ mod tests {
let mut c = ctx_defaults();
c.best_single_score = 4_999;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"high_scorer"), "high_scorer must not unlock at best_single_score=4999");
assert!(
!ids.contains(&"high_scorer"),
"high_scorer must not unlock at best_single_score=4999"
);
}
#[test]
@@ -602,7 +642,10 @@ mod tests {
let mut c = ctx_defaults();
c.win_streak_current = 3;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"on_a_roll"), "on_a_roll should unlock at streak=3");
assert!(
ids.contains(&"on_a_roll"),
"on_a_roll should unlock at streak=3"
);
}
#[test]
@@ -610,7 +653,10 @@ mod tests {
let mut c = ctx_defaults();
c.last_win_recycle_count = 3;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"comeback"), "comeback should unlock at last_win_recycle_count=3");
assert!(
ids.contains(&"comeback"),
"comeback should unlock at last_win_recycle_count=3"
);
}
#[test]
@@ -631,12 +677,18 @@ mod tests {
c.win_streak_current = 9;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"unstoppable"));
assert!(ids.contains(&"on_a_roll"), "streak 9 must still satisfy on_a_roll");
assert!(
ids.contains(&"on_a_roll"),
"streak 9 must still satisfy on_a_roll"
);
c.win_streak_current = 10;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"unstoppable"));
assert!(ids.contains(&"on_a_roll"), "streak 10 must also satisfy on_a_roll");
assert!(
ids.contains(&"on_a_roll"),
"streak 10 must also satisfy on_a_roll"
);
}
#[test]
@@ -657,12 +709,18 @@ mod tests {
c.games_played = 499;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"veteran"));
assert!(ids.contains(&"century"), "499 games must also satisfy century");
assert!(
ids.contains(&"century"),
"499 games must also satisfy century"
);
c.games_played = 500;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"veteran"));
assert!(ids.contains(&"century"), "500 games must also satisfy century");
assert!(
ids.contains(&"century"),
"500 games must also satisfy century"
);
}
#[test]
@@ -727,7 +785,10 @@ mod tests {
assert!(ids.contains(&"first_win"), "first_win should unlock");
assert!(ids.contains(&"on_a_roll"), "on_a_roll should unlock");
assert!(ids.contains(&"no_undo"), "no_undo should unlock");
assert!(ids.len() >= 3, "at least 3 achievements must fire simultaneously");
assert!(
ids.len() >= 3,
"at least 3 achievements must fire simultaneously"
);
}
#[test]
@@ -742,7 +803,10 @@ mod tests {
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"perfectionist"), "perfectionist must unlock");
assert!(ids.contains(&"no_undo"), "no_undo must also unlock when perfectionist does");
assert!(
ids.contains(&"no_undo"),
"no_undo must also unlock when perfectionist does"
);
}
#[test]
@@ -778,6 +842,9 @@ mod tests {
c.last_win_score = 50_000;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"perfectionist"), "score far above threshold must pass");
assert!(
ids.contains(&"perfectionist"),
"score far above threshold must pass"
);
}
}