1use web_time::Duration;
20
21use bevy_ecs::prelude::*;
22use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule};
23use bevy_ecs::system::ScheduleSystem;
24
25pub use bevy_ecs;
26
27pub mod hash;
28pub mod rng;
29pub mod state;
30pub mod tape;
31pub mod task;
32pub mod timer;
33
34pub use hash::Hash64;
35pub use rng::{Rng, SimRng, WorldSeed};
36pub use state::{NextState, State, States, in_state};
37pub use tape::{Divergence, Intent, Setup, Tape, VerifyError, WorldHasher};
38pub use task::{Task, TaskPool};
39pub use timer::{StepTimer, TimerMode};
40
41pub struct Sim {
44 pub world: World,
46 schedule: Schedule,
47 state_appliers: Vec<Box<dyn FnMut(&mut World) + Send + Sync>>,
49 accumulator: Duration,
50 pub step: Duration,
54 pub time_scale: f32,
57 pub max_steps: u32,
62}
63
64#[derive(Resource, Clone, Copy, Debug, Default)]
66pub struct SimTime {
67 pub elapsed_secs: f64,
69 pub delta_secs: f32,
71}
72
73impl Sim {
74 pub fn new(step: Duration) -> Self {
75 let mut world = World::new();
76 world.init_resource::<SimTime>();
77 Self {
78 world,
79 schedule: Schedule::default(),
80 state_appliers: Vec::new(),
81 accumulator: Duration::ZERO,
82 step,
83 time_scale: 1.0,
84 max_steps: 8,
85 }
86 }
87
88 pub fn with_default_step() -> Self {
90 Self::new(Duration::from_secs_f64(1.0 / 60.0))
91 }
92
93 pub fn add_system<M>(&mut self, system: impl IntoSystem<(), (), M>) -> &mut Self {
95 self.schedule.add_systems(system);
96 self
97 }
98
99 pub fn add_chained_systems<M>(
103 &mut self,
104 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
105 ) -> &mut Self {
106 self.schedule.add_systems(systems);
107 self
108 }
109
110 pub fn add_state<S: States>(&mut self, initial: S) -> &mut Self {
116 self.world.insert_resource(State::<S>::new(initial));
117 self.world.insert_resource(NextState::<S>::new());
118 state::init_schedules::<S>(&mut self.world);
119 self.state_appliers.push(Box::new(state::apply::<S>));
120 self
121 }
122
123 pub fn on_enter<S, M>(
127 &mut self,
128 state: S,
129 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
130 ) -> &mut Self
131 where
132 S: States,
133 {
134 state::add_enter(&mut self.world, state, systems);
135 self
136 }
137
138 pub fn on_exit<S, M>(
140 &mut self,
141 state: S,
142 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
143 ) -> &mut Self
144 where
145 S: States,
146 {
147 state::add_exit(&mut self.world, state, systems);
148 self
149 }
150
151 pub fn on_transition<S, M>(
153 &mut self,
154 exited: S,
155 entered: S,
156 systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
157 ) -> &mut Self
158 where
159 S: States,
160 {
161 state::add_transition(&mut self.world, exited, entered, systems);
162 self
163 }
164
165 pub fn set_state<S: States>(&mut self, next: S) -> &mut Self {
168 self.world.resource_mut::<NextState<S>>().set(next);
169 self
170 }
171
172 pub fn state<S: States>(&self) -> &S {
174 self.world.resource::<State<S>>().get()
175 }
176
177 pub fn step(&mut self, dt: Duration) -> u32 {
186 self.step_with(dt, |_| {})
187 }
188
189 fn paused(&self) -> bool {
192 self.time_scale.is_finite() && self.time_scale <= 0.0
193 }
194
195 pub fn step_with(&mut self, dt: Duration, mut after_tick: impl FnMut(&mut World)) -> u32 {
196 if self.step.is_zero() {
197 return 0;
198 }
199 if self.paused() {
200 self.accumulator = Duration::ZERO;
203 return 0;
204 }
205 self.accumulator += dt;
206 let mut ran = 0;
207 while self.accumulator >= self.step && ran < self.max_steps.max(1) {
208 self.accumulator -= self.step;
209 self.tick_with(&mut after_tick);
210 ran += 1;
211 }
212 if self.accumulator >= self.step {
213 let step_ns = self.step.as_nanos();
215 if step_ns > 0 {
216 let acc_ns = self.accumulator.as_nanos() % step_ns;
217 self.accumulator = Duration::from_nanos(acc_ns as u64);
218 } else {
219 self.accumulator = Duration::ZERO;
220 }
221 }
222 ran
223 }
224
225 pub fn leftover(&self) -> Duration {
227 self.accumulator
228 }
229
230 pub fn alpha(&self) -> f32 {
236 let step = self.step.as_secs_f64();
237 if step <= 0.0 {
238 return 0.0;
239 }
240 (self.accumulator.as_secs_f64() / step).clamp(0.0, 1.0) as f32
241 }
242
243 pub fn tick(&mut self) {
246 self.tick_with(|_| {});
247 }
248
249 fn tick_with(&mut self, after_tick: impl FnOnce(&mut World)) {
250 if self.paused() {
251 return;
252 }
253 let time_scale = if self.time_scale.is_finite() {
254 self.time_scale
255 } else {
256 1.0
257 };
258 {
259 let mut time = self.world.resource_mut::<SimTime>();
260 time.delta_secs = self.step.as_secs_f32() * time_scale;
261 time.elapsed_secs += self.step.as_secs_f64() * time_scale as f64;
262 }
263 let Sim {
264 state_appliers,
265 world,
266 schedule,
267 ..
268 } = self;
269 for apply in state_appliers.iter_mut() {
270 apply(world);
271 }
272 schedule.run(world);
273 after_tick(world);
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn fixed_steps_accumulate() {
283 let mut sim = Sim::with_default_step();
284 let ran = sim.step(Duration::from_nanos(50_000_001));
285 assert_eq!(ran, 3);
286 let time = sim.world.resource::<SimTime>();
287 assert!((time.elapsed_secs - 3.0 / 60.0).abs() < 1e-9);
288 }
289
290 #[test]
291 fn zero_time_scale_pauses_without_banking_wall_time() {
292 let per_frame = Sim::with_default_step().step(Duration::from_millis(50));
293 assert!(per_frame > 0);
294
295 let mut sim = Sim::with_default_step();
296 sim.step(Duration::from_millis(50));
297 let before = sim.world.resource::<SimTime>().elapsed_secs;
298
299 sim.time_scale = 0.0;
300 assert_eq!(
301 sim.step(Duration::from_secs(5)),
302 0,
303 "paused sim runs no ticks"
304 );
305 sim.tick();
306 assert_eq!(
307 sim.world.resource::<SimTime>().elapsed_secs,
308 before,
309 "paused sim does not advance its clock"
310 );
311
312 sim.time_scale = 1.0;
313 assert_eq!(
314 sim.step(Duration::from_millis(50)),
315 per_frame,
316 "resuming must not fire a catch-up burst"
317 );
318 }
319
320 #[test]
321 fn non_finite_time_scale_runs_at_normal_speed() {
322 let mut sim = Sim::with_default_step();
323 sim.time_scale = f32::NAN;
324 sim.step(Duration::from_millis(51));
325 let time = sim.world.resource::<SimTime>();
326 assert!(
327 (time.delta_secs - 1.0 / 60.0).abs() < 1e-6,
328 "a non-finite scale must not stall the delta, got {}",
329 time.delta_secs
330 );
331 assert!(time.elapsed_secs > 0.0, "the clock still advances");
332 }
333
334 #[test]
335 fn post_tick_runs_after_each_fixed_step() {
336 use std::sync::{Arc, Mutex};
337
338 let log = Arc::new(Mutex::new(Vec::new()));
339 let sim_log = log.clone();
340 let mut sim = Sim::with_default_step();
341 sim.add_system(move || sim_log.lock().unwrap().push("sim"));
342 let post_log = log.clone();
343
344 let ran = sim.step_with(Duration::from_millis(51), move |_| {
345 post_log.lock().unwrap().push("post");
346 });
347
348 assert_eq!(ran, 3);
349 assert_eq!(
350 *log.lock().unwrap(),
351 ["sim", "post", "sim", "post", "sim", "post"]
352 );
353 }
354
355 #[test]
356 fn zero_step_never_ticks() {
357 let mut sim = Sim::new(Duration::ZERO);
358 let ran = sim.step(Duration::from_secs(10));
359 assert_eq!(ran, 0);
360 let time = sim.world.resource::<SimTime>();
361 assert_eq!(time.elapsed_secs, 0.0);
362 }
363
364 #[test]
365 fn spiral_of_death_is_capped() {
366 let mut sim = Sim::with_default_step();
367 sim.max_steps = 4;
368 let ran = sim.step(Duration::from_secs(10));
369 assert_eq!(ran, 4);
370 assert!(
371 sim.leftover() < sim.step,
372 "keeps only fraction, got {:?}",
373 sim.leftover()
374 );
375 }
376
377 #[test]
378 fn alpha_is_interpolation_fraction() {
379 let mut sim = Sim::new(Duration::from_millis(16));
380 sim.step(Duration::from_millis(8));
381 assert!((sim.alpha() - 0.5).abs() < 1e-6, "got {}", sim.alpha());
382 sim.step(Duration::from_millis(8));
383 assert!(sim.alpha() < 1e-6, "step boundary, got {}", sim.alpha());
384 }
385
386 #[test]
387 fn time_scale_changes_simulated_delta_without_changing_wall_accumulation() {
388 let mut sim = Sim::with_default_step();
389 sim.time_scale = 0.25;
390 let ran = sim.step(Duration::from_nanos(16_666_667));
391 let time = sim.world.resource::<SimTime>();
392 assert_eq!(ran, 1);
393 assert!((time.delta_secs - 1.0 / 240.0).abs() < 1e-6);
394 assert!((time.elapsed_secs - 1.0 / 240.0).abs() < 1e-9);
395 }
396
397 #[test]
398 fn chained_systems_run_in_order() {
399 use std::sync::{Arc, Mutex};
400 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
401 let (a_log, b_log, c_log) = (log.clone(), log.clone(), log.clone());
402 let mut sim = Sim::with_default_step();
403 sim.add_chained_systems(
404 (
405 move || a_log.lock().unwrap().push("a"),
406 move || b_log.lock().unwrap().push("b"),
407 move || c_log.lock().unwrap().push("c"),
408 )
409 .chain(),
410 );
411 sim.tick();
412 assert_eq!(*log.lock().unwrap(), vec!["a", "b", "c"]);
413 }
414}