use super::*;
use bevy::ecs::error::{BevyError, ErrorContext, Severity, match_severity};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
static TRIPPED: AtomicBool = AtomicBool::new(false);
static SEEN: Mutex<Vec<String>> = Mutex::new(Vec::new());
pub(super) fn keep_going(error: BevyError, ctx: ErrorContext) {
if error.severity() != Severity::Panic {
match_severity(error, ctx);
return;
}
TRIPPED.store(true, Ordering::Relaxed);
let line = format!("{ctx}: {error}");
let mut seen = SEEN
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !seen.contains(&line) {
error!("caught a panic and carried on: {line}");
seen.push(line);
}
}
pub(super) fn install(app: &mut App) {
if cfg!(not(debug_assertions)) {
app.insert_resource(bevy::ecs::error::FallbackErrorHandler(keep_going));
}
}
const BENCH_AGAIN: u32 = 60;
#[derive(bevy::ecs::system::SystemParam)]
pub(super) struct Wreckage<'w, 's> {
online: Option<ResMut<'w, net::Online>>,
sim: Option<ResMut<'w, Sim>>,
editor: Option<ResMut<'w, editor::EditorState>>,
commands: Commands<'w, 's>,
sprites: session::BoardSprites<'w, 's>,
}
pub(super) fn leave_a_broken_round(
screen: Res<State<Screen>>,
settings: Res<settings::GameSettings>,
mut notice: ResMut<RoundNotice>,
mut next_screen: ResMut<NextState<Screen>>,
mut since_bench: Local<Option<u32>>,
wreckage: Wreckage,
) {
if let Some(frames) = since_bench.as_mut() {
*frames = frames.saturating_add(1);
}
if !TRIPPED.swap(false, Ordering::Relaxed) {
return;
}
let Wreckage {
online,
sim,
editor,
mut commands,
sprites,
} = wreckage;
let tr = settings.tr();
match screen.get() {
Screen::Editor if since_bench.is_some_and(|frames| frames <= BENCH_AGAIN) => {
*since_bench = None;
notice.0 = tr.round_failed.to_string();
next_screen.set(Screen::Menu);
}
Screen::Editor => {
if let (Some(mut sim), Some(mut editor)) = (sim, editor) {
editor::recover(&mut sim, &mut editor, &mut commands, &sprites, tr);
}
*since_bench = Some(0);
}
Screen::Puzzle | Screen::Versus | Screen::Interlude => {
notice.0 = tr.round_failed.to_string();
let mut lobby = false;
if let Some(mut online) = online
&& let Some(session) = online.0.as_mut()
{
lobby = session.home.from_lobby;
session.next_round = false;
if *screen.get() == Screen::Interlude {
online.0 = None;
}
}
next_screen.set(match lobby {
true => Screen::Lobby,
false => Screen::Menu,
});
}
Screen::Menu
| Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Language
| Screen::NewVersion => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn a_system_with_a_bug() {
panic!("a system with a bug (expected by this test)");
}
fn a_bug_that_happens_once(mut done: Local<bool>) {
if !std::mem::replace(&mut *done, true) {
panic!("a system with a bug once (expected by this test)");
}
}
fn broken(screen: Screen) -> App {
broken_by(screen, a_system_with_a_bug)
}
fn broken_by<M>(
screen: Screen,
bug: impl IntoScheduleConfigs<bevy::ecs::system::ScheduleSystem, M>,
) -> App {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.insert_state(screen);
app.insert_resource(settings::GameSettings::default());
app.init_resource::<RoundNotice>();
app.insert_resource(bevy::ecs::error::FallbackErrorHandler(keep_going));
app.add_systems(Update, (bug, leave_a_broken_round).chain());
app
}
fn online_from(from_lobby: bool) -> net::Online {
let mut session = net::OnlineSession::new(
crate::transport::UdpTransport::host(0).expect("game socket"),
crate::sim::Lockstep::new(0, vec![0, 1], 0),
2,
crate::transport::MatchTerms::default(),
);
session.home.from_lobby = from_lobby;
net::Online(Some(session))
}
#[test]
fn a_panic_ends_the_round_and_not_the_game() {
let mut app = broken(Screen::Versus);
app.update();
app.update();
assert_eq!(*app.world().resource::<State<Screen>>().get(), Screen::Menu);
assert_eq!(
app.world().resource::<RoundNotice>().0,
i18n::EN.round_failed
);
for (from_lobby, home) in [(true, Screen::Lobby), (false, Screen::Menu)] {
let mut app = broken(Screen::Versus);
app.insert_resource(online_from(from_lobby));
app.update();
app.update();
assert_eq!(*app.world().resource::<State<Screen>>().get(), home);
let online = app.world().resource::<net::Online>();
assert!(
online.0.as_ref().is_none_or(|session| !session.next_round),
"nothing carries the session on to another round"
);
}
let mut app = broken_by(Screen::Editor, a_bug_that_happens_once);
let built = Board::new(9, 7, 5);
let mut playing = Board::new(12, 9, 5);
playing.set_wrap(true);
app.insert_resource(Sim(playing));
let mut state = editor::EditorState::default();
state.mode = editor::Mode::testing(built.clone());
app.insert_resource(state);
app.update();
app.update();
assert_eq!(
*app.world().resource::<State<Screen>>().get(),
Screen::Editor
);
let editor = app.world().resource::<editor::EditorState>();
assert!(!editor.is_testing(), "the playtest is over");
assert_eq!(editor.feedback, i18n::EN.ed_failed);
assert_eq!(
app.world().resource::<Sim>().0.state_hash(),
built.state_hash(),
"the level being built is back"
);
let mut app = broken(Screen::Editor);
app.insert_resource(Sim(Board::new(9, 7, 5)));
app.insert_resource(editor::EditorState::default());
for _ in 0..4 {
app.update();
}
assert_eq!(*app.world().resource::<State<Screen>>().get(), Screen::Menu);
}
}