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:
@@ -22,17 +22,17 @@
|
||||
|
||||
use bevy::input::ButtonInput;
|
||||
use bevy::prelude::*;
|
||||
use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool, Task};
|
||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||
use solitaire_core::game_state::DrawMode;
|
||||
use solitaire_core::solver::{try_solve, SolverConfig, SolverResult};
|
||||
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
|
||||
|
||||
use crate::events::{NewGameRequestEvent, StartPlayBySeedRequestEvent};
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::settings_plugin::SettingsResource;
|
||||
use crate::ui_modal::{
|
||||
spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button, spawn_modal_header,
|
||||
ButtonVariant, ScrimDismissible,
|
||||
ButtonVariant, ScrimDismissible, spawn_modal, spawn_modal_actions, spawn_modal_body_text,
|
||||
spawn_modal_button, spawn_modal_header,
|
||||
};
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, BG_ELEVATED_PRESSED, BORDER_SUBTLE, HighContrastBorder, RADIUS_MD,
|
||||
@@ -341,8 +341,7 @@ fn tick_debounce_and_spawn_solver_task(
|
||||
.as_ref()
|
||||
.map_or(DrawMode::DrawOne, |s| s.0.draw_mode);
|
||||
let cfg = SolverConfig::default();
|
||||
let task = AsyncComputeTaskPool::get()
|
||||
.spawn(async move { try_solve(seed, draw_mode, &cfg) });
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move { try_solve(seed, draw_mode, &cfg) });
|
||||
|
||||
pending.seed = Some(seed);
|
||||
pending.handle = Some(task);
|
||||
@@ -407,7 +406,9 @@ fn handle_confirm(
|
||||
}
|
||||
|
||||
let Ok(buf) = buffers.single() else { return };
|
||||
let Ok(seed) = buf.text.parse::<u64>() else { return };
|
||||
let Ok(seed) = buf.text.parse::<u64>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
new_game.write(NewGameRequestEvent {
|
||||
seed: Some(seed),
|
||||
@@ -470,8 +471,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn open_dialog(app: &mut App) {
|
||||
app.world_mut()
|
||||
.write_message(StartPlayBySeedRequestEvent);
|
||||
app.world_mut().write_message(StartPlayBySeedRequestEvent);
|
||||
app.update();
|
||||
}
|
||||
|
||||
@@ -547,7 +547,10 @@ mod tests {
|
||||
|
||||
let msgs = app.world().resource::<Messages<NewGameRequestEvent>>();
|
||||
let mut cursor = msgs.get_cursor();
|
||||
assert!(cursor.read(msgs).next().is_none(), "no NewGameRequestEvent when buffer empty");
|
||||
assert!(
|
||||
cursor.read(msgs).next().is_none(),
|
||||
"no NewGameRequestEvent when buffer empty"
|
||||
);
|
||||
// Dialog should still be open.
|
||||
assert!(dialog_present(&mut app));
|
||||
}
|
||||
@@ -607,7 +610,10 @@ mod tests {
|
||||
}
|
||||
|
||||
let pending = app.world().resource::<PendingVerification>();
|
||||
assert!(pending.handle.is_some(), "solver task should have been spawned after debounce");
|
||||
assert!(
|
||||
pending.handle.is_some(),
|
||||
"solver task should have been spawned after debounce"
|
||||
);
|
||||
assert_eq!(pending.seed, Some(42));
|
||||
}
|
||||
|
||||
@@ -623,11 +629,21 @@ mod tests {
|
||||
for _ in 0..DEBOUNCE_FRAMES {
|
||||
app.update();
|
||||
}
|
||||
assert!(app.world().resource::<PendingVerification>().handle.is_some());
|
||||
assert!(
|
||||
app.world()
|
||||
.resource::<PendingVerification>()
|
||||
.handle
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// New keypress should cancel the in-flight task.
|
||||
press_key(&mut app, KeyCode::Digit3);
|
||||
assert!(app.world().resource::<PendingVerification>().handle.is_none());
|
||||
assert!(
|
||||
app.world()
|
||||
.resource::<PendingVerification>()
|
||||
.handle
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(app.world().resource::<PendingVerification>().seed, None);
|
||||
}
|
||||
|
||||
@@ -649,7 +665,11 @@ mod tests {
|
||||
|
||||
// Poll until the solver task resolves (cap at 15 s wall-clock).
|
||||
let deadline = Instant::now() + std::time::Duration::from_secs(15);
|
||||
while app.world().resource::<PendingVerification>().handle.is_some()
|
||||
while app
|
||||
.world()
|
||||
.resource::<PendingVerification>()
|
||||
.handle
|
||||
.is_some()
|
||||
&& Instant::now() < deadline
|
||||
{
|
||||
app.update();
|
||||
@@ -664,7 +684,13 @@ mod tests {
|
||||
.next()
|
||||
.map(|(t, _)| t.0.clone())
|
||||
.unwrap_or_default();
|
||||
assert_ne!(badge_text, "Verifying\u{2026}", "badge should have resolved to a verdict");
|
||||
assert_ne!(badge_text, "Type a number", "badge should show verdict, not idle state");
|
||||
assert_ne!(
|
||||
badge_text, "Verifying\u{2026}",
|
||||
"badge should have resolved to a verdict"
|
||||
);
|
||||
assert_ne!(
|
||||
badge_text, "Type a number",
|
||||
"badge should show verdict, not idle state"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user