//! `bevy_fairy/time`: pause the game clock and set its speed.
//!
//! Gregory, *Game Engine Architecture* (3rd ed., §10.4–10.6), lists a pause and a time scale among
//! the features that make in-engine debugging useful: an agent can freeze a frame, then inspect it.
//! This drives `Time<Virtual>`, the clock that gameplay and animation read, so pausing it freezes
//! the simulation but not the renderer or BRP itself.
//!
//! The command writes a [`ClockRequest`] and does not fight the host for `Time<Virtual>`. A system in
//! `First`, ahead of bevy_time's own clock system, applies the request every frame, so a host that
//! writes `Time<Virtual>` later in the frame (an `unpause`, a `set_relative_speed`) cannot undo it.
//! A host that must combine the request with its own holds and slow motion turns that system off with
//! `DebuggerPlugin::host_owns_clock` and reads [`ClockRequest`] itself. Either way the reply says
//! whether the request held, measured on the frame after it was made (issue #10).
use bevy::prelude::*;
use bevy::time::TimeSystems;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::plugin::commands::{add_builtin_command, DebugCommandInfo};
/// Params of `bevy_fairy/time`. Both fields are optional; an empty call only reports the clock.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TimeParams {
pub pause: Option<bool>,
pub speed: Option<f32>,
}
/// What the agent asked of the game clock. `bevy_fairy/time` writes it; it stays until the agent
/// changes it (a pause survives a new game or a phase change). A host that owns the clock reads it:
/// OR `pause` into its own hold and multiply `speed` into its own factor.
#[derive(Resource, Clone, Copy, Debug, PartialEq)]
pub struct ClockRequest {
/// Hold `Time<Virtual>` paused.
pub pause: bool,
/// The speed multiplier asked for; `1.0` is real time.
pub speed: f32,
}
impl Default for ClockRequest {
fn default() -> Self {
Self { pause: false, speed: 1.0 }
}
}
/// What the clock did on the last frame, and under which request.
#[derive(Clone, Copy)]
struct Observation {
request: ClockRequest,
delta: std::time::Duration,
/// The speed the clock advanced at: `request.speed`, or less when `clamped`.
effective_speed: f32,
/// The speed `Time<Virtual>` held when it advanced, whoever set it.
relative_speed: f32,
/// Whether `Time<Virtual>` was paused on that frame, whoever paused it.
paused: bool,
/// The engine cut the frame to `max_delta`, so `effective_speed` is below `relative_speed`.
clamped: bool,
}
/// The largest `speed` the command accepts. `Time<Virtual>` multiplies a frame's raw delta by the
/// speed with `Duration::mul_f64`, which panics when the product does not fit a `Duration`
/// (about 1.8e19 s), and the multiplication runs before the `max_delta` clamp: a speed of 1e30
/// crashed the game on the next frame (issue #12). The engine caps each frame at `max_delta`
/// (250 ms by default), so any speed above `max_delta / frame time`, about 15 at 60 fps, gives
/// the same clock; 1000 is far above that and far below the overflow.
const MAX_SPEED: f32 = 1000.0;
/// Private bookkeeping: whether the host owns the clock, what this crate has overridden (so a
/// cleared request is undone exactly once), and the last observation.
#[derive(Resource, Default)]
pub(crate) struct ClockState {
pub(crate) host_owned: bool,
paused_by_us: bool,
speed_by_us: bool,
observed: Option<Observation>,
}
/// Registers `bevy_fairy/time` as a recorded built-in command.
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; it does not fight a host's own pause or slow motion once the request is back to not paused at 1.0. Returns the clock state, `held` (did the clock match the request on the last frame: paused when asked to be, running when not, at the asked speed; a pause that leaves speed at 1.0 is judged on the pause alone; false when something else holds the clock, null with `unavailable` until a frame has run), `clamped_by_max_delta` (true when the engine cut the last frame to the clock's max_delta, 250 ms by default, so `effective_speed` is below `speed` though nothing overrode the request) and `host_owned`.",
)
.params("pause: bool, optional; speed: f32 in (0, 1000], optional (1.0 is real time)")
.example(json!({"pause": true})),
handle_time,
);
}
/// Applies the request to the clock, and undoes what this crate set once the request is cleared.
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>) {
let relative_speed = clock.relative_speed();
state.observed = Some(Observation {
request: *request,
delta: clock.delta(),
effective_speed: clock.effective_speed(),
relative_speed,
paused: clock.is_paused(),
// The engine cuts a frame to exactly `max_delta` and reports the speed that gives; an
// unclamped frame advances at the speed it was given.
clamped: !clock.is_paused() && clock.delta() == clock.max_delta() && clock.effective_speed() < relative_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 || speed > MAX_SPEED {
return Err(format!(
"speed must be > 0 and at most {MAX_SPEED}, got {speed}. Time<Virtual> overflows past that, and the engine caps a frame at its max_delta anyway. Example: {{\"speed\": 4.0}}"
));
}
}
if let Some(speed) = params.speed {
request.speed = speed;
}
if let Some(pause) = params.pause {
request.pause = pause;
}
// Apply now so the reply reads the new clock; the host-owned mode never writes the clock.
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 near = |speed: f32| (speed - request.speed).abs() <= 1e-3 * request.speed;
// `held` is whether the clock matched the request on that frame: paused when it was
// asked to be, running when it was not, at the speed asked for. "Not paused, 1.0" is a
// request like any other (B-129): it used to be skipped, so a host holding its own pause
// or running its own slow motion read as `held: true` beside `paused: true`.
let pause_held = if request.pause { seen.delta.is_zero() } else { !seen.paused };
// A paused clock has no effective speed to read. A request of 1.0 asks for no speed (the
// plugin never writes 1.0 over a host's own), so a pause beside a host's slow motion is
// held; any other speed is compared with the one the clock holds for when it resumes.
// A frame the engine clamped advanced slower than the speed in force, so the request
// held when the speed in force is the request's.
let speed_held = if seen.paused {
request.speed == 1.0 || near(seen.relative_speed)
} else {
near(seen.effective_speed) || (seen.clamped && near(seen.relative_speed))
};
reply["held"] = json!(pause_held && speed_held);
reply["clamped_by_max_delta"] = json!(seen.clamped);
}
None => {
reply["held"] = Value::Null;
reply["clamped_by_max_delta"] = 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)
}