cranium_api/
api.rs

1/* 
2This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. 
3If a copy of the MPL was not distributed with this file, 
4You can obtain one at https://mozilla.org/MPL/2.0/. 
5*/
6use 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/// 
37#[derive(Event)]
38struct AutoRunHeartbeat;
39
40/// Triggers AutoRunHeartbeat events, keeping the AutoRun-ing Cranium instance alive.
41/// This function is expected to be called periodically by the user from downstream code 
42/// as an alternative to driving the whole App themselves.
43pub 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
66/// A System that checks 
67fn 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        // We need to ensure that wrapping doesn't cause issues.
79        //
80        // If the delta would be negative, that means the last heartbeat 
81        // happened in the previous wrap period (or more, we can't tell).
82        // We'll readd it to get a more realistic, positive delta value.
83        //
84        // Note that if someone set timeout >> wrap, the timeout will NEVER happen!
85        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) // 5 mins by default
131        ; 
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) // 6 hours by default
136        ; 
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) // 200ms by default
153    ; 
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}