use bevy::prelude::*;
use bevy::time::TimeSystems;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::plugin::commands::{add_builtin_command, DebugCommandInfo};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TimeParams {
pub pause: Option<bool>,
pub speed: Option<f32>,
}
#[derive(Resource, Clone, Copy, Debug, PartialEq)]
pub struct ClockRequest {
pub pause: bool,
pub speed: f32,
}
impl Default for ClockRequest {
fn default() -> Self {
Self { pause: false, speed: 1.0 }
}
}
#[derive(Clone, Copy)]
struct Observation {
request: ClockRequest,
delta: std::time::Duration,
effective_speed: f32,
}
#[derive(Resource, Default)]
pub(crate) struct ClockState {
pub(crate) host_owned: bool,
paused_by_us: bool,
speed_by_us: bool,
observed: Option<Observation>,
}
pub(crate) fn register(app: &mut App, host_owned: bool) {
app.init_resource::<ClockRequest>()
.insert_resource(ClockState { host_owned, ..default() })
.add_systems(First, observe_clock.after(TimeSystems));
if !host_owned {
app.add_systems(First, apply_request.before(TimeSystems));
}
add_builtin_command::<TimeParams, _>(
app,
DebugCommandInfo::new(
"bevy_fairy/time",
"Pause or unpause the game clock (Time<Virtual>) and set its speed multiplier. The request persists and is re-applied every frame against a host that writes the clock. Returns the clock state, `held` (did the clock obey on the last frame; null with `unavailable` until a frame has run) and `host_owned`.",
)
.params("pause: bool, optional; speed: f32 > 0, optional (1.0 is real time)")
.example(json!({"pause": true})),
handle_time,
);
}
fn apply(request: &ClockRequest, state: &mut ClockState, clock: &mut Time<Virtual>) {
if request.pause {
clock.pause();
state.paused_by_us = true;
} else if state.paused_by_us {
clock.unpause();
state.paused_by_us = false;
}
if request.speed != 1.0 {
clock.set_relative_speed(request.speed);
state.speed_by_us = true;
} else if state.speed_by_us {
clock.set_relative_speed(1.0);
state.speed_by_us = false;
}
}
fn apply_request(
request: Res<ClockRequest>,
mut state: ResMut<ClockState>,
mut clock: ResMut<Time<Virtual>>,
) {
apply(&request, &mut state, &mut clock);
}
fn observe_clock(request: Res<ClockRequest>, clock: Res<Time<Virtual>>, mut state: ResMut<ClockState>) {
state.observed = Some(Observation {
request: *request,
delta: clock.delta(),
effective_speed: clock.effective_speed(),
});
}
fn handle_time(
In(params): In<TimeParams>,
mut request: ResMut<ClockRequest>,
mut state: ResMut<ClockState>,
mut clock: ResMut<Time<Virtual>>,
) -> Result<Value, String> {
if let Some(speed) = params.speed {
if !speed.is_finite() || speed <= 0.0 {
return Err("speed must be > 0".to_string());
}
}
if let Some(speed) = params.speed {
request.speed = speed;
}
if let Some(pause) = params.pause {
request.pause = pause;
}
if !state.host_owned {
apply(&request, &mut state, &mut clock);
}
let mut reply = json!({
"paused": clock.is_paused(),
"speed": clock.relative_speed(),
"effective_speed": clock.effective_speed(),
"requested": { "pause": request.pause, "speed": request.speed },
"host_owned": state.host_owned,
});
match state.observed.filter(|seen| seen.request == *request) {
Some(seen) => {
let pause_held = !request.pause || seen.delta.is_zero();
let speed_held = request.pause
|| request.speed == 1.0
|| (seen.effective_speed - request.speed).abs() <= 1e-3 * request.speed;
reply["held"] = json!(pause_held && speed_held);
}
None => {
reply["held"] = Value::Null;
reply["unavailable"] = json!(
"no frame has run under this request yet; call bevy_fairy/time with no params to read whether it held"
);
}
}
Ok(reply)
}