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
+32 -14
View File
@@ -22,8 +22,8 @@
use std::io::Cursor;
use bevy::prelude::*;
use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
use kira::sound::Region;
use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
use kira::track::{TrackBuilder, TrackHandle};
use kira::{AudioManager, AudioManagerSettings, Decibels, DefaultBackend, Tween, Value};
@@ -178,8 +178,7 @@ fn build_library() -> Option<SoundLibrary> {
let place = decode(include_bytes!("../../assets/audio/card_place.wav"))?;
let invalid = decode(include_bytes!("../../assets/audio/card_invalid.wav"))?;
let fanfare = decode(include_bytes!("../../assets/audio/win_fanfare.wav"))?;
let foundation_complete =
decode(include_bytes!("../../assets/audio/foundation_complete.wav"))?;
let foundation_complete = decode(include_bytes!("../../assets/audio/foundation_complete.wav"))?;
Some(SoundLibrary {
deal,
flip,
@@ -212,8 +211,7 @@ fn start_ambient_loop(
) -> Option<StaticSoundHandle> {
let manager = manager?;
let ambient_bytes: &'static [u8] =
include_bytes!("../../assets/audio/ambient_loop.wav");
let ambient_bytes: &'static [u8] = include_bytes!("../../assets/audio/ambient_loop.wav");
let mut data = match StaticSoundData::from_cursor(Cursor::new(ambient_bytes.to_vec())) {
Ok(d) => d,
Err(e) => {
@@ -280,13 +278,19 @@ impl AudioState {
fn set_sfx_volume(audio: &mut AudioState, volume: f32) {
if let Some(track) = audio.sfx_track.as_mut() {
track.set_volume(amplitude_to_decibels(volume.clamp(0.0, 1.0)), Tween::default());
track.set_volume(
amplitude_to_decibels(volume.clamp(0.0, 1.0)),
Tween::default(),
);
}
}
fn set_music_volume(audio: &mut AudioState, volume: f32) {
if let Some(track) = audio.music_track.as_mut() {
track.set_volume(amplitude_to_decibels(volume.clamp(0.0, 1.0)), Tween::default());
track.set_volume(
amplitude_to_decibels(volume.clamp(0.0, 1.0)),
Tween::default(),
);
}
}
@@ -319,7 +323,10 @@ fn apply_volume_on_change(
let sfx_muted = mute.as_ref().is_some_and(|m| m.sfx_muted);
let music_muted = mute.as_ref().is_some_and(|m| m.music_muted);
set_sfx_volume(&mut audio, if sfx_muted { 0.0 } else { ev.0.sfx_volume });
set_music_volume(&mut audio, if music_muted { 0.0 } else { ev.0.music_volume });
set_music_volume(
&mut audio,
if music_muted { 0.0 } else { ev.0.music_volume },
);
}
}
@@ -374,8 +381,7 @@ fn play_on_draw(
if is_recycle(stock_len) {
let mut data = lib.flip.clone();
data.settings.volume =
Value::Fixed(amplitude_to_decibels(RECYCLE_VOLUME as f32));
data.settings.volume = Value::Fixed(amplitude_to_decibels(RECYCLE_VOLUME as f32));
let result = if let Some(track) = audio.sfx_track.as_mut() {
track.play(data)
} else if let Some(manager) = audio.manager.as_mut() {
@@ -516,7 +522,10 @@ mod tests {
toggle_all(&mut m);
assert!(m.sfx_muted && m.music_muted, "M should mute both channels");
toggle_all(&mut m);
assert!(!m.sfx_muted && !m.music_muted, "second M should unmute both channels");
assert!(
!m.sfx_muted && !m.music_muted,
"second M should unmute both channels"
);
}
#[test]
@@ -537,14 +546,23 @@ mod tests {
assert!(m.music_muted && !m.sfx_muted);
// M should mute sfx (not-all-muted → mute-all).
toggle_all(&mut m);
assert!(m.sfx_muted && m.music_muted, "M unmutes neither — it mutes all when sfx was audible");
assert!(
m.sfx_muted && m.music_muted,
"M unmutes neither — it mutes all when sfx was audible"
);
}
#[test]
fn mute_all_when_both_already_muted_unmutes_both() {
let mut m = MuteState { sfx_muted: true, music_muted: true };
let mut m = MuteState {
sfx_muted: true,
music_muted: true,
};
toggle_all(&mut m);
assert!(!m.sfx_muted && !m.music_muted, "M should unmute both when all were muted");
assert!(
!m.sfx_muted && !m.music_muted,
"M should unmute both when all were muted"
);
}
// -----------------------------------------------------------------------