1use core::{num::NonZero, time::Duration};
7
8use bevy::{prelude::*};
9use cranium_bevy_plugin::CraniumPlugin;
10
11#[derive(Resource)]
12struct AutoRunHeartbeatTimeout(core::time::Duration);
13
14impl Default for AutoRunHeartbeatTimeout {
15 fn default() -> Self {
16 Self(core::time::Duration::from_mins(5))
17 }
18}
19
20#[derive(Resource)]
21struct AutoRunHeartbeatWrapPeriod(core::time::Duration);
22
23impl Default for AutoRunHeartbeatWrapPeriod {
24 fn default() -> Self {
25 Self(core::time::Duration::from_hours(6))
26 }
27}
28
29
30#[derive(Default, Resource)]
31struct AutoRunHeartbeatTracker {
32 last_tick: core::time::Duration
33}
34
35
36#[derive(Event)]
38struct AutoRunHeartbeat;
39
40pub fn _heartbeat(
44 mut commands: Commands
45) {
46 commands.trigger(AutoRunHeartbeat);
47}
48
49fn update_heartbeat(
50 _trigger: On<AutoRunHeartbeat>,
51 timer: Res<Time<Real>>,
52 mut heartbeat_tracker: ResMut<AutoRunHeartbeatTracker>
53) {
54 let now = timer.elapsed_wrapped();
55 heartbeat_tracker.last_tick = now;
56}
57
58fn setup_wrap_period(
59 wrap_period: Res<AutoRunHeartbeatWrapPeriod>,
60 mut timer: ResMut<Time<Real>>,
61) {
62 let period = wrap_period.0;
63 timer.set_wrap_period(period);
64}
65
66fn check_heartbeat_system(
68 heartbeat_tracker: Res<AutoRunHeartbeatTracker>,
69 heartbeat_timeout: Res<AutoRunHeartbeatTimeout>,
70 timer: Res<Time<Real>>,
71 mut app_exit: MessageWriter<AppExit>,
72) {
73 let timeout = heartbeat_timeout.0;
74 let last_tick = heartbeat_tracker.last_tick;
75
76 let now = timer.elapsed_wrapped();
77 let now = match now < last_tick {
78 false => now,
86 true => {
87 now + timer.wrap_period()
88 },
89 };
90
91 let delta = now - last_tick;
92
93 if delta > timeout {
94 bevy::log::error!(
95 "Cranium received no heartbeat in more than {:?}s (delta:{:?}s, last update time: {:?}s), quitting!",
96 timeout.as_secs(), delta.as_secs(), last_tick.as_secs()
97 );
98 app_exit.write(AppExit::Error(NonZero::new(1u8).unwrap()));
99 };
100}
101
102pub fn create_app() -> App {
103 let mut app = App::new();
104 app.add_plugins(CraniumPlugin);
105
106 #[cfg(feature = "logging")]
107 app.add_plugins(
108 bevy::log::LogPlugin {
109 level: bevy::log::Level::DEBUG,
110 custom_layer: |_| None,
111 filter: "wgpu=error,bevy_render=info,bevy_ecs=info".to_string(),
112 fmt_layer: |_| None,
113 }
114 );
115
116 app
117}
118
119pub fn _tick_world(app: &mut App) -> &mut App {
120 app.update();
121 app
122}
123
124struct AutoRunPlugin;
125
126impl Plugin for AutoRunPlugin {
127 fn build(&self, app: &mut App) {
128 let timeout_seconds = option_env!("CORTEX_AUTORUN_HEARTBEAT_TIMEOUT_SECONDS")
129 .map(|s| s.trim().parse::<u64>().ok()).flatten()
130 .unwrap_or(60*5) ;
132
133 let period_seconds = option_env!("CORTEX_AUTORUN_PERIOD_SECONDS")
134 .map(|s| s.trim().parse::<u64>().ok()).flatten()
135 .unwrap_or(60*60*6) ;
137
138 app
139 .init_resource::<AutoRunHeartbeatTracker>()
140 .insert_resource(AutoRunHeartbeatTimeout(Duration::from_secs(timeout_seconds)))
141 .insert_resource(AutoRunHeartbeatWrapPeriod(Duration::from_secs(period_seconds)))
142 .add_systems(Startup, setup_wrap_period)
143 .add_systems(Last, check_heartbeat_system)
144 .add_observer(update_heartbeat)
145 ;
146 }
147}
148
149pub fn configure_for_autorun(mut app: App) -> App {
150 let run_rate = option_env!("CORTEX_AUTORUN_RATE_MILISECONDS")
151 .map(|s| s.trim().parse::<u64>().ok()).flatten()
152 .unwrap_or(200) ;
154
155 app.add_plugins((
156 MinimalPlugins.set(bevy::app::ScheduleRunnerPlugin::run_loop(core::time::Duration::from_millis(run_rate))),
157 AutoRunPlugin,
158 ));
159 app
160}
161
162pub fn autorun(mut app: App) {
163 app
164 .run();
165}
166
167pub fn create_and_autorun() {
168 let app = configure_for_autorun(create_app());
169 #[cfg(feature = "logging")]
170 bevy::log::info!("Created a Cranium Server app, running...");
171 autorun(app);
172}