Make AppExit more specific about exit reason. (#13022)

# Objective

Closes #13017.

## Solution

- Make `AppExit` a enum with a `Success` and `Error` variant.
- Make `App::run()` return a `AppExit` if it ever returns.
- Make app runners return a `AppExit` to signal if they encountered a
error.

---

## Changelog

### Added

- [`App::should_exit`](https://example.org/)
- [`AppExit`](https://docs.rs/bevy/latest/bevy/app/struct.AppExit.html)
to the `bevy` and `bevy_app` preludes,

### Changed

- [`AppExit`](https://docs.rs/bevy/latest/bevy/app/struct.AppExit.html)
is now a enum with 2 variants (`Success` and `Error`).
- The app's [runner
function](https://docs.rs/bevy/latest/bevy/app/struct.App.html#method.set_runner)
now has to return a `AppExit`.
-
[`App::run()`](https://docs.rs/bevy/latest/bevy/app/struct.App.html#method.run)
now also returns the `AppExit` produced by the runner function.


## Migration Guide

- Replace all usages of
[`AppExit`](https://docs.rs/bevy/latest/bevy/app/struct.AppExit.html)
with `AppExit::Success` or `AppExit::Failure`.
- Any custom app runners now need to return a `AppExit`. We suggest you
return a `AppExit::Error` if any `AppExit` raised was a Error. You can
use the new [`App::should_exit`](https://example.org/) method.
- If not exiting from `main` any other way. You should return the
`AppExit` from `App::run()` so the app correctly returns a error code if
anything fails e.g.
```rust
fn main() -> AppExit {
    App::new()
        //Your setup here...
        .run()
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Brezak 2024-04-22 18:48:18 +02:00 committed by GitHub
parent a50223622f
commit de875fdc4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 248 additions and 60 deletions

View File

@ -4,7 +4,7 @@ use crate::{
}; };
pub use bevy_derive::AppLabel; pub use bevy_derive::AppLabel;
use bevy_ecs::{ use bevy_ecs::{
event::event_update_system, event::{event_update_system, ManualEventReader},
intern::Interned, intern::Interned,
prelude::*, prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel}, schedule::{ScheduleBuildSettings, ScheduleLabel},
@ -13,8 +13,14 @@ use bevy_ecs::{
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
use bevy_utils::tracing::info_span; use bevy_utils::tracing::info_span;
use bevy_utils::{tracing::debug, HashMap}; use bevy_utils::{tracing::debug, HashMap};
use std::fmt::Debug; use std::{
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; fmt::Debug,
process::{ExitCode, Termination},
};
use std::{
num::NonZeroU8,
panic::{catch_unwind, resume_unwind, AssertUnwindSafe},
};
use thiserror::Error; use thiserror::Error;
bevy_ecs::define_label!( bevy_ecs::define_label!(
@ -151,7 +157,7 @@ impl App {
/// # Panics /// # Panics
/// ///
/// Panics if not all plugins have been built. /// Panics if not all plugins have been built.
pub fn run(&mut self) { pub fn run(&mut self) -> AppExit {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _bevy_app_run_span = info_span!("bevy_app").entered(); let _bevy_app_run_span = info_span!("bevy_app").entered();
if self.is_building_plugins() { if self.is_building_plugins() {
@ -160,7 +166,7 @@ impl App {
let runner = std::mem::replace(&mut self.runner, Box::new(run_once)); let runner = std::mem::replace(&mut self.runner, Box::new(run_once));
let app = std::mem::replace(self, App::empty()); let app = std::mem::replace(self, App::empty());
(runner)(app); (runner)(app)
} }
/// Sets the function that will be called when the app is run. /// Sets the function that will be called when the app is run.
@ -177,17 +183,20 @@ impl App {
/// ``` /// ```
/// # use bevy_app::prelude::*; /// # use bevy_app::prelude::*;
/// # /// #
/// fn my_runner(mut app: App) { /// fn my_runner(mut app: App) -> AppExit {
/// loop { /// loop {
/// println!("In main loop"); /// println!("In main loop");
/// app.update(); /// app.update();
/// if let Some(exit) = app.should_exit() {
/// return exit;
/// }
/// } /// }
/// } /// }
/// ///
/// App::new() /// App::new()
/// .set_runner(my_runner); /// .set_runner(my_runner);
/// ``` /// ```
pub fn set_runner(&mut self, f: impl FnOnce(App) + 'static) -> &mut Self { pub fn set_runner(&mut self, f: impl FnOnce(App) -> AppExit + 'static) -> &mut Self {
self.runner = Box::new(f); self.runner = Box::new(f);
self self
} }
@ -849,11 +858,44 @@ impl App {
self.main_mut().ignore_ambiguity(schedule, a, b); self.main_mut().ignore_ambiguity(schedule, a, b);
self self
} }
/// Attempts to determine if an [`AppExit`] was raised since the last update.
///
/// Will attempt to return the first [`Error`](AppExit::Error) it encounters.
/// This should be called after every [`update()`](App::update) otherwise you risk
/// dropping possible [`AppExit`] events.
pub fn should_exit(&self) -> Option<AppExit> {
let mut reader = ManualEventReader::default();
self.should_exit_manual(&mut reader)
} }
type RunnerFn = Box<dyn FnOnce(App)>; /// Several app runners in this crate keep their own [`ManualEventReader<AppExit>`].
/// This exists to accommodate them.
pub(crate) fn should_exit_manual(
&self,
reader: &mut ManualEventReader<AppExit>,
) -> Option<AppExit> {
let events = self.world().get_resource::<Events<AppExit>>()?;
fn run_once(mut app: App) { let mut events = reader.read(events);
if events.len() != 0 {
return Some(
events
.find(|exit| exit.is_error())
.cloned()
.unwrap_or(AppExit::Success),
);
}
None
}
}
type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
fn run_once(mut app: App) -> AppExit {
while app.plugins_state() == PluginsState::Adding { while app.plugins_state() == PluginsState::Adding {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
bevy_tasks::tick_global_task_pools_on_main_thread(); bevy_tasks::tick_global_task_pools_on_main_thread();
@ -862,27 +904,99 @@ fn run_once(mut app: App) {
app.cleanup(); app.cleanup();
app.update(); app.update();
let mut exit_code_reader = ManualEventReader::default();
if let Some(app_exit_events) = app.world().get_resource::<Events<AppExit>>() {
if exit_code_reader
.read(app_exit_events)
.any(AppExit::is_error)
{
return AppExit::error();
}
} }
/// An event that indicates the [`App`] should exit. If one or more of these are present at the AppExit::Success
/// end of an update, the [runner](App::set_runner) will end and ([maybe](App::run)) return }
/// control to the caller.
/// An event that indicates the [`App`] should exit. If one or more of these are present at the end of an update,
/// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller.
/// ///
/// This event can be used to detect when an exit is requested. Make sure that systems listening /// This event can be used to detect when an exit is requested. Make sure that systems listening
/// for this event run before the current update ends. /// for this event run before the current update ends.
#[derive(Event, Debug, Clone, Default)] ///
pub struct AppExit; /// # Portability
/// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns
/// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#))
/// we only allow error codes between 1 and [255](u8::MAX).
#[derive(Event, Debug, Clone, Default, PartialEq, Eq)]
pub enum AppExit {
/// [`App`] exited without any problems.
#[default]
Success,
/// The [`App`] experienced an unhandleable error.
/// Holds the exit code we expect our app to return.
Error(NonZeroU8),
}
impl AppExit {
/// Creates a [`AppExit::Error`] with a error code of 1.
#[must_use]
pub const fn error() -> Self {
Self::Error(NonZeroU8::MIN)
}
/// Returns `true` if `self` is a [`AppExit::Success`].
#[must_use]
pub const fn is_success(&self) -> bool {
matches!(self, AppExit::Success)
}
/// Returns `true` if `self` is a [`AppExit::Error`].
#[must_use]
pub const fn is_error(&self) -> bool {
matches!(self, AppExit::Error(_))
}
/// Creates a [`AppExit`] from a code.
///
/// When `code` is 0 a [`AppExit::Success`] is constructed otherwise a
/// [`AppExit::Error`] is constructed.
#[must_use]
pub const fn from_code(code: u8) -> Self {
match NonZeroU8::new(code) {
Some(code) => Self::Error(code),
None => Self::Success,
}
}
}
impl From<u8> for AppExit {
#[must_use]
fn from(value: u8) -> Self {
Self::from_code(value)
}
}
impl Termination for AppExit {
fn report(self) -> std::process::ExitCode {
match self {
AppExit::Success => ExitCode::SUCCESS,
// We leave logging an error to our users
AppExit::Error(value) => ExitCode::from(value.get()),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::marker::PhantomData; use std::{marker::PhantomData, mem};
use bevy_ecs::{ use bevy_ecs::{
schedule::{OnEnter, States}, schedule::{OnEnter, States},
system::Commands, system::Commands,
}; };
use crate::{App, Plugin}; use crate::{App, AppExit, Plugin};
struct PluginA; struct PluginA;
impl Plugin for PluginA { impl Plugin for PluginA {
@ -1089,13 +1203,15 @@ mod tests {
#[derive(Resource)] #[derive(Resource)]
struct MyState {} struct MyState {}
fn my_runner(mut app: App) { fn my_runner(mut app: App) -> AppExit {
let my_state = MyState {}; let my_state = MyState {};
app.world_mut().insert_resource(my_state); app.world_mut().insert_resource(my_state);
for _ in 0..5 { for _ in 0..5 {
app.update(); app.update();
} }
AppExit::Success
} }
fn my_system(_: Res<MyState>) { fn my_system(_: Res<MyState>) {
@ -1108,4 +1224,11 @@ mod tests {
.add_systems(PreUpdate, my_system) .add_systems(PreUpdate, my_system)
.run(); .run();
} }
#[test]
fn app_exit_size() {
// There wont be many of them so the size isn't a issue but
// it's nice they're so small let's keep it that way.
assert_eq!(mem::size_of::<AppExit>(), mem::size_of::<u8>());
}
} }

View File

@ -28,7 +28,7 @@ pub use sub_app::*;
pub mod prelude { pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ pub use crate::{
app::App, app::{App, AppExit},
main_schedule::{ main_schedule::{
First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main, First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main,
PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, StateTransition, PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, StateTransition,

View File

@ -3,7 +3,7 @@ use crate::{
plugin::Plugin, plugin::Plugin,
PluginsState, PluginsState,
}; };
use bevy_ecs::event::{Events, ManualEventReader}; use bevy_ecs::event::ManualEventReader;
use bevy_utils::{Duration, Instant}; use bevy_utils::{Duration, Instant};
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -84,7 +84,15 @@ impl Plugin for ScheduleRunnerPlugin {
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default(); let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
match run_mode { match run_mode {
RunMode::Once => app.update(), RunMode::Once => {
app.update();
if let Some(exit) = app.should_exit_manual(&mut app_exit_event_reader) {
return exit;
}
AppExit::Success
}
RunMode::Loop { wait } => { RunMode::Loop { wait } => {
let mut tick = move |app: &mut App, let mut tick = move |app: &mut App,
wait: Option<Duration>| wait: Option<Duration>|
@ -93,14 +101,9 @@ impl Plugin for ScheduleRunnerPlugin {
app.update(); app.update();
if let Some(app_exit_events) = if let Some(exit) = app.should_exit_manual(&mut app_exit_event_reader) {
app.world_mut().get_resource_mut::<Events<AppExit>>() return Err(exit);
{ };
if let Some(exit) = app_exit_event_reader.read(&app_exit_events).last()
{
return Err(exit.clone());
}
}
let end_time = Instant::now(); let end_time = Instant::now();
@ -116,40 +119,54 @@ impl Plugin for ScheduleRunnerPlugin {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
while let Ok(delay) = tick(&mut app, wait) { loop {
if let Some(delay) = delay { match tick(&mut app, wait) {
std::thread::sleep(delay); Ok(Some(delay)) => std::thread::sleep(delay),
Ok(None) => continue,
Err(exit) => return exit,
} }
} }
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
fn set_timeout(f: &Closure<dyn FnMut()>, dur: Duration) { fn set_timeout(callback: &Closure<dyn FnMut()>, dur: Duration) {
web_sys::window() web_sys::window()
.unwrap() .unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0( .set_timeout_with_callback_and_timeout_and_arguments_0(
f.as_ref().unchecked_ref(), callback.as_ref().unchecked_ref(),
dur.as_millis() as i32, dur.as_millis() as i32,
) )
.expect("Should register `setTimeout`."); .expect("Should register `setTimeout`.");
} }
let asap = Duration::from_millis(1); let asap = Duration::from_millis(1);
let mut rc = Rc::new(app); let exit = Rc::new(RefCell::new(AppExit::Success));
let f = Rc::new(RefCell::new(None)); let closure_exit = exit.clone();
let g = f.clone();
let c = move || { let mut app = Rc::new(app);
let app = Rc::get_mut(&mut rc).unwrap(); let moved_tick_closure = Rc::new(RefCell::new(None));
let base_tick_closure = moved_tick_closure.clone();
let tick_app = move || {
let app = Rc::get_mut(&mut app).unwrap();
let delay = tick(app, wait); let delay = tick(app, wait);
if let Ok(delay) = delay { match delay {
set_timeout(f.borrow().as_ref().unwrap(), delay.unwrap_or(asap)); Ok(delay) => set_timeout(
moved_tick_closure.borrow().as_ref().unwrap(),
delay.unwrap_or(asap),
),
Err(code) => {
closure_exit.replace(code);
}
} }
}; };
*g.borrow_mut() = Some(Closure::wrap(Box::new(c) as Box<dyn FnMut()>)); *base_tick_closure.borrow_mut() =
set_timeout(g.borrow().as_ref().unwrap(), asap); Some(Closure::wrap(Box::new(tick_app) as Box<dyn FnMut()>));
}; set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap);
exit.take()
}
} }
} }
}); });

View File

@ -37,7 +37,7 @@ fn ci_testing_exit_after(
) { ) {
if let Some(exit_after) = ci_testing_config.exit_after { if let Some(exit_after) = ci_testing_config.exit_after {
if *current_frame > exit_after { if *current_frame > exit_after {
app_exit_events.send(AppExit); app_exit_events.send(AppExit::Success);
info!("Exiting after {} frames. Test successful!", exit_after); info!("Exiting after {} frames. Test successful!", exit_after);
} }
} }

View File

@ -193,7 +193,7 @@ fn renderer_extract(app_world: &mut World, _world: &mut World) {
render_channels.send_blocking(render_app); render_channels.send_blocking(render_app);
} else { } else {
// Renderer thread panicked // Renderer thread panicked
world.send_event(AppExit); world.send_event(AppExit::error());
} }
}); });
}); });

View File

@ -13,7 +13,7 @@ use bevy_ecs::prelude::*;
pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Query<&Window>) { pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Query<&Window>) {
if windows.is_empty() { if windows.is_empty() {
bevy_utils::tracing::info!("No windows are open, exiting"); bevy_utils::tracing::info!("No windows are open, exiting");
app_exit_events.send(AppExit); app_exit_events.send(AppExit::Success);
} }
} }
@ -28,7 +28,7 @@ pub fn exit_on_primary_closed(
) { ) {
if windows.is_empty() { if windows.is_empty() {
bevy_utils::tracing::info!("Primary window was closed, exiting"); bevy_utils::tracing::info!("Primary window was closed, exiting");
app_exit_events.send(AppExit); app_exit_events.send(AppExit::Success);
} }
} }

View File

@ -19,6 +19,8 @@ mod winit_config;
pub mod winit_event; pub mod winit_event;
mod winit_windows; mod winit_windows;
use std::sync::{Arc, Mutex};
use approx::relative_eq; use approx::relative_eq;
use bevy_a11y::AccessibilityRequested; use bevy_a11y::AccessibilityRequested;
use bevy_utils::Instant; use bevy_utils::Instant;
@ -263,7 +265,7 @@ type UserEvent = RequestRedraw;
/// ///
/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the /// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the
/// `EventLoop`. /// `EventLoop`.
pub fn winit_runner(mut app: App) { pub fn winit_runner(mut app: App) -> AppExit {
if app.plugins_state() == PluginsState::Ready { if app.plugins_state() == PluginsState::Ready {
app.finish(); app.finish();
app.cleanup(); app.cleanup();
@ -279,6 +281,10 @@ pub fn winit_runner(mut app: App) {
let mut runner_state = WinitAppRunnerState::default(); let mut runner_state = WinitAppRunnerState::default();
// TODO: AppExit is effectively a u8 we could use a AtomicU8 here instead of a mutex.
let mut exit_status = Arc::new(Mutex::new(AppExit::Success));
let handle_exit_status = exit_status.clone();
// prepare structures to access data in the world // prepare structures to access data in the world
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default(); let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default(); let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default();
@ -298,6 +304,8 @@ pub fn winit_runner(mut app: App) {
let mut winit_events = Vec::default(); let mut winit_events = Vec::default();
// set up the event loop // set up the event loop
let event_handler = move |event, event_loop: &EventLoopWindowTarget<UserEvent>| { let event_handler = move |event, event_loop: &EventLoopWindowTarget<UserEvent>| {
let mut exit_status = handle_exit_status.lock().unwrap();
handle_winit_event( handle_winit_event(
&mut app, &mut app,
&mut app_exit_event_reader, &mut app_exit_event_reader,
@ -307,6 +315,7 @@ pub fn winit_runner(mut app: App) {
&mut focused_windows_state, &mut focused_windows_state,
&mut redraw_event_reader, &mut redraw_event_reader,
&mut winit_events, &mut winit_events,
&mut exit_status,
event, event,
event_loop, event_loop,
); );
@ -317,6 +326,12 @@ pub fn winit_runner(mut app: App) {
if let Err(err) = event_loop.run(event_handler) { if let Err(err) = event_loop.run(event_handler) {
error!("winit event loop returned an error: {err}"); error!("winit event loop returned an error: {err}");
} }
// We should be the only ones holding this `Arc` since the event loop exiting cleanly
// should drop the event handler. if this is not the case something funky is happening.
Arc::get_mut(&mut exit_status)
.map(|mutex| mutex.get_mut().unwrap().clone())
.unwrap_or(AppExit::error())
} }
#[allow(clippy::too_many_arguments /* TODO: probs can reduce # of args */)] #[allow(clippy::too_many_arguments /* TODO: probs can reduce # of args */)]
@ -334,6 +349,7 @@ fn handle_winit_event(
focused_windows_state: &mut SystemState<(Res<WinitSettings>, Query<&Window>)>, focused_windows_state: &mut SystemState<(Res<WinitSettings>, Query<&Window>)>,
redraw_event_reader: &mut ManualEventReader<RequestRedraw>, redraw_event_reader: &mut ManualEventReader<RequestRedraw>,
winit_events: &mut Vec<WinitEvent>, winit_events: &mut Vec<WinitEvent>,
exit_status: &mut AppExit,
event: Event<UserEvent>, event: Event<UserEvent>,
event_loop: &EventLoopWindowTarget<UserEvent>, event_loop: &EventLoopWindowTarget<UserEvent>,
) { ) {
@ -350,8 +366,14 @@ fn handle_winit_event(
} }
runner_state.redraw_requested = true; runner_state.redraw_requested = true;
// TODO: Replace with `App::should_exit()`
if let Some(app_exit_events) = app.world().get_resource::<Events<AppExit>>() { if let Some(app_exit_events) = app.world().get_resource::<Events<AppExit>>() {
if app_exit_event_reader.read(app_exit_events).last().is_some() { let mut exit_events = app_exit_event_reader.read(app_exit_events);
if exit_events.len() != 0 {
*exit_status = exit_events
.find(|exit| exit.is_error())
.cloned()
.unwrap_or(AppExit::Success);
event_loop.exit(); event_loop.exit();
return; return;
} }
@ -411,6 +433,7 @@ fn handle_winit_event(
app_exit_event_reader, app_exit_event_reader,
redraw_event_reader, redraw_event_reader,
winit_events, winit_events,
exit_status,
); );
if runner_state.active != ActiveState::Suspended { if runner_state.active != ActiveState::Suspended {
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
@ -638,6 +661,7 @@ fn handle_winit_event(
app_exit_event_reader, app_exit_event_reader,
redraw_event_reader, redraw_event_reader,
winit_events, winit_events,
exit_status,
); );
} }
_ => {} _ => {}
@ -738,6 +762,7 @@ fn run_app_update_if_should(
app_exit_event_reader: &mut ManualEventReader<AppExit>, app_exit_event_reader: &mut ManualEventReader<AppExit>,
redraw_event_reader: &mut ManualEventReader<RequestRedraw>, redraw_event_reader: &mut ManualEventReader<RequestRedraw>,
winit_events: &mut Vec<WinitEvent>, winit_events: &mut Vec<WinitEvent>,
exit_status: &mut AppExit,
) { ) {
runner_state.reset_on_update(); runner_state.reset_on_update();
@ -797,9 +822,16 @@ fn run_app_update_if_should(
} }
} }
// TODO: Replace with `App::should_exit()`
if let Some(app_exit_events) = app.world().get_resource::<Events<AppExit>>() { if let Some(app_exit_events) = app.world().get_resource::<Events<AppExit>>() {
if app_exit_event_reader.read(app_exit_events).last().is_some() { let mut exit_events = app_exit_event_reader.read(app_exit_events);
if exit_events.len() != 0 {
*exit_status = exit_events
.find(|exit| exit.is_error())
.cloned()
.unwrap_or(AppExit::Success);
event_loop.exit(); event_loop.exit();
return;
} }
} }
} }

View File

@ -1,13 +1,13 @@
//! This example demonstrates you can create a custom runner (to update an app manually). It reads //! This example demonstrates you can create a custom runner (to update an app manually). It reads
//! lines from stdin and prints them from within the ecs. //! lines from stdin and prints them from within the ecs.
use bevy::prelude::*; use bevy::{app::AppExit, prelude::*};
use std::io; use std::io;
#[derive(Resource)] #[derive(Resource)]
struct Input(String); struct Input(String);
fn my_runner(mut app: App) { fn my_runner(mut app: App) -> AppExit {
println!("Type stuff into the console"); println!("Type stuff into the console");
for line in io::stdin().lines() { for line in io::stdin().lines() {
{ {
@ -15,17 +15,30 @@ fn my_runner(mut app: App) {
input.0 = line.unwrap(); input.0 = line.unwrap();
} }
app.update(); app.update();
if let Some(exit) = app.should_exit() {
return exit;
} }
} }
AppExit::Success
}
fn print_system(input: Res<Input>) { fn print_system(input: Res<Input>) {
println!("You typed: {}", input.0); println!("You typed: {}", input.0);
} }
fn main() { fn exit_system(input: Res<Input>, mut exit_event: EventWriter<AppExit>) {
if input.0 == "exit" {
exit_event.send(AppExit::Success);
}
}
// AppExit implements `Termination` so we can return it from main.
fn main() -> AppExit {
App::new() App::new()
.insert_resource(Input(String::new())) .insert_resource(Input(String::new()))
.set_runner(my_runner) .set_runner(my_runner)
.add_systems(Update, print_system) .add_systems(Update, (print_system, exit_system))
.run(); .run()
} }

View File

@ -130,10 +130,10 @@ fn game_over_system(
) { ) {
if let Some(ref player) = game_state.winning_player { if let Some(ref player) = game_state.winning_player {
println!("{player} won the game!"); println!("{player} won the game!");
app_exit_events.send(AppExit); app_exit_events.send(AppExit::Success);
} else if game_state.current_round == game_rules.max_rounds { } else if game_state.current_round == game_rules.max_rounds {
println!("Ran out of rounds. Nobody wins!"); println!("Ran out of rounds. Nobody wins!");
app_exit_events.send(AppExit); app_exit_events.send(AppExit::Success);
} }
} }

View File

@ -334,7 +334,7 @@ fn quit(
.distance(cursor_world_pos) .distance(cursor_world_pos)
< BEVY_LOGO_RADIUS < BEVY_LOGO_RADIUS
{ {
app_exit.send(AppExit); app_exit.send(AppExit::Success);
} }
} }

View File

@ -789,7 +789,7 @@ mod menu {
if *interaction == Interaction::Pressed { if *interaction == Interaction::Pressed {
match menu_button_action { match menu_button_action {
MenuButtonAction::Quit => { MenuButtonAction::Quit => {
app_exit_events.send(AppExit); app_exit_events.send(AppExit::Success);
} }
MenuButtonAction::Play => { MenuButtonAction::Play => {
game_state.set(GameState::Game); game_state.set(GameState::Game);

View File

@ -1,5 +1,6 @@
//! An example that illustrates how Time is handled in ECS. //! An example that illustrates how Time is handled in ECS.
use bevy::app::AppExit;
use bevy::prelude::*; use bevy::prelude::*;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
@ -30,7 +31,7 @@ fn help() {
println!(" u: Unpause"); println!(" u: Unpause");
} }
fn runner(mut app: App) { fn runner(mut app: App) -> AppExit {
banner(); banner();
help(); help();
let stdin = io::stdin(); let stdin = io::stdin();
@ -78,6 +79,8 @@ fn runner(mut app: App) {
} }
} }
} }
AppExit::Success
} }
fn print_real_time(time: Res<Time<Real>>) { fn print_real_time(time: Res<Time<Real>>) {