1use alloc::boxed::Box;
7use alloc::rc::Rc;
8use core::any::TypeId;
9
10use crate::schedule::RunContext;
11use crate::state::State;
12use crate::time::ChangeTick;
13use crate::world::World;
14
15type Predicate = Rc<dyn Fn(&World) -> bool>;
16
17#[derive(Clone, Copy)]
18struct StateProbe {
19 type_id: TypeId,
20 transition_tick: fn(&World) -> Option<ChangeTick>,
21 pending: fn(&World) -> bool,
22}
23
24impl StateProbe {
25 fn of<S: Eq + 'static>() -> Self {
26 Self {
27 type_id: TypeId::of::<State<S>>(),
28 transition_tick: |world| {
29 world
30 .resource::<State<S>>()
31 .ok()
32 .flatten()
33 .and_then(State::transition_tick)
34 },
35 pending: |world| {
36 world
37 .resource::<State<S>>()
38 .ok()
39 .flatten()
40 .is_some_and(|state| state.pending().is_some())
41 },
42 }
43 }
44}
45
46#[derive(Clone)]
48pub struct Condition(ConditionKind);
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum ConditionError {
53 ZeroPeriod,
55 PeriodNotPowerOfTwo { period: u64 },
57 PhaseOutOfRange { period: u64, phase: u64 },
59}
60
61#[derive(Clone)]
62enum ConditionKind {
63 Always,
64 Never,
65 ResourceExists(TypeId),
66 ResourceAdded(TypeId),
67 ResourceChanged(TypeId),
68 StateChanged(StateProbe),
69 StatePending(StateProbe),
70 FixedStepMod { mask: u64, phase: u64 },
71 And(Box<ConditionKind>, Box<ConditionKind>),
72 Or(Box<ConditionKind>, Box<ConditionKind>),
73 Predicate(Predicate),
74}
75
76impl Condition {
77 pub const fn always() -> Self {
79 Self(ConditionKind::Always)
80 }
81
82 pub const fn never() -> Self {
84 Self(ConditionKind::Never)
85 }
86
87 pub fn resource_exists<R: 'static>() -> Self {
89 Self(ConditionKind::ResourceExists(TypeId::of::<R>()))
90 }
91
92 pub fn resource_added<R: 'static>() -> Self {
94 Self(ConditionKind::ResourceAdded(TypeId::of::<R>()))
95 }
96
97 pub fn resource_changed<R: 'static>() -> Self {
99 Self(ConditionKind::ResourceChanged(TypeId::of::<R>()))
100 }
101
102 pub fn state_changed<S: Eq + 'static>() -> Self {
104 Self(ConditionKind::StateChanged(StateProbe::of::<S>()))
105 }
106
107 pub fn state_pending<S: Eq + 'static>() -> Self {
109 Self(ConditionKind::StatePending(StateProbe::of::<S>()))
110 }
111
112 pub fn fixed_step_mod(period: u64, phase: u64) -> Result<Self, ConditionError> {
117 if period == 0 {
118 return Err(ConditionError::ZeroPeriod);
119 }
120 if !period.is_power_of_two() {
121 return Err(ConditionError::PeriodNotPowerOfTwo { period });
122 }
123 if phase >= period {
124 return Err(ConditionError::PhaseOutOfRange { period, phase });
125 }
126 Ok(Self(ConditionKind::FixedStepMod {
127 mask: period - 1,
128 phase,
129 }))
130 }
131
132 pub fn from_world<F>(predicate: F) -> Self
134 where
135 F: Fn(&World) -> bool + 'static,
136 {
137 Self::predicate(Rc::new(predicate))
138 }
139
140 pub fn in_state<S: Eq + 'static>(value: S) -> Self {
142 let expected = value;
143 Self::from_world(move |world| {
144 world
145 .state_current::<S>()
146 .ok()
147 .flatten()
148 .is_some_and(|current| *current == expected)
149 })
150 }
151
152 pub fn and(self, other: Self) -> Self {
154 Self(ConditionKind::And(Box::new(self.0), Box::new(other.0)))
155 }
156
157 pub fn or(self, other: Self) -> Self {
159 Self(ConditionKind::Or(Box::new(self.0), Box::new(other.0)))
160 }
161
162 fn predicate(predicate: Predicate) -> Self {
163 Self(ConditionKind::Predicate(predicate))
164 }
165
166 pub(crate) fn evaluate(
167 &self,
168 world: &World,
169 system_index: usize,
170 context: &RunContext,
171 ) -> bool {
172 evaluate_kind(&self.0, world, system_index, context)
173 }
174
175 pub(crate) fn evaluate_for_set(
176 &self,
177 world: &World,
178 set_index: usize,
179 context: &RunContext,
180 ) -> bool {
181 evaluate_kind_for_set(&self.0, world, set_index, context)
182 }
183
184 pub(crate) fn advance_cursors(
185 &self,
186 world: &World,
187 system_index: usize,
188 context: &mut RunContext,
189 ) {
190 advance_kind_cursors(&self.0, world, system_index, context);
191 }
192
193 pub(crate) fn advance_set_cursors(
194 &self,
195 world: &World,
196 set_index: usize,
197 context: &mut RunContext,
198 ) {
199 advance_kind_set_cursors(&self.0, world, set_index, context);
200 }
201}
202
203fn evaluate_kind(
204 kind: &ConditionKind,
205 world: &World,
206 system_index: usize,
207 context: &RunContext,
208) -> bool {
209 match kind {
210 ConditionKind::Always => true,
211 ConditionKind::Never => false,
212 ConditionKind::ResourceExists(type_id) => world.resource_present(*type_id),
213 ConditionKind::ResourceAdded(type_id) => resource_tick_advanced(
214 world.resource_added_tick_for(*type_id),
215 context.resource_added_cursor(system_index, *type_id),
216 ),
217 ConditionKind::ResourceChanged(type_id) => resource_tick_advanced(
218 world.resource_changed_tick_for(*type_id),
219 context.resource_changed_cursor(system_index, *type_id),
220 ),
221 ConditionKind::StateChanged(probe) => state_tick_advanced(
222 (probe.transition_tick)(world),
223 context.state_transition_cursor(system_index, probe.type_id),
224 ),
225 ConditionKind::StatePending(probe) => (probe.pending)(world),
226 ConditionKind::FixedStepMod { mask, phase } => context
227 .fixed_step
228 .is_some_and(|step| step.index & mask == *phase),
229 ConditionKind::And(left, right) => {
230 evaluate_kind(left, world, system_index, context)
231 && evaluate_kind(right, world, system_index, context)
232 }
233 ConditionKind::Or(left, right) => {
234 evaluate_kind(left, world, system_index, context)
235 || evaluate_kind(right, world, system_index, context)
236 }
237 ConditionKind::Predicate(predicate) => predicate(world),
238 }
239}
240
241fn evaluate_kind_for_set(
242 kind: &ConditionKind,
243 world: &World,
244 set_index: usize,
245 context: &RunContext,
246) -> bool {
247 match kind {
248 ConditionKind::Always => true,
249 ConditionKind::Never => false,
250 ConditionKind::ResourceExists(type_id) => world.resource_present(*type_id),
251 ConditionKind::ResourceAdded(type_id) => resource_tick_advanced(
252 world.resource_added_tick_for(*type_id),
253 context.resource_added_cursor_for_set(set_index, *type_id),
254 ),
255 ConditionKind::ResourceChanged(type_id) => resource_tick_advanced(
256 world.resource_changed_tick_for(*type_id),
257 context.resource_changed_cursor_for_set(set_index, *type_id),
258 ),
259 ConditionKind::StateChanged(probe) => state_tick_advanced(
260 (probe.transition_tick)(world),
261 context.state_transition_cursor_for_set(set_index, probe.type_id),
262 ),
263 ConditionKind::StatePending(probe) => (probe.pending)(world),
264 ConditionKind::FixedStepMod { mask, phase } => context
265 .fixed_step
266 .is_some_and(|step| step.index & mask == *phase),
267 ConditionKind::And(left, right) => {
268 evaluate_kind_for_set(left, world, set_index, context)
269 && evaluate_kind_for_set(right, world, set_index, context)
270 }
271 ConditionKind::Or(left, right) => {
272 evaluate_kind_for_set(left, world, set_index, context)
273 || evaluate_kind_for_set(right, world, set_index, context)
274 }
275 ConditionKind::Predicate(predicate) => predicate(world),
276 }
277}
278
279fn advance_kind_cursors(
280 kind: &ConditionKind,
281 world: &World,
282 system_index: usize,
283 context: &mut RunContext,
284) {
285 match kind {
286 ConditionKind::ResourceAdded(type_id) => {
287 if let Some(tick) = world.resource_added_tick_for(*type_id) {
288 context.set_resource_added_cursor(system_index, *type_id, tick);
289 }
290 }
291 ConditionKind::ResourceChanged(type_id) => {
292 if let Some(tick) = world.resource_changed_tick_for(*type_id) {
293 context.set_resource_changed_cursor(system_index, *type_id, tick);
294 }
295 }
296 ConditionKind::StateChanged(probe) => {
297 if let Some(tick) = (probe.transition_tick)(world) {
298 context.set_state_transition_cursor(system_index, probe.type_id, tick);
299 }
300 }
301 ConditionKind::And(left, right) | ConditionKind::Or(left, right) => {
302 advance_kind_cursors(left, world, system_index, context);
303 advance_kind_cursors(right, world, system_index, context);
304 }
305 _ => {}
306 }
307}
308
309fn advance_kind_set_cursors(
310 kind: &ConditionKind,
311 world: &World,
312 set_index: usize,
313 context: &mut RunContext,
314) {
315 match kind {
316 ConditionKind::ResourceAdded(type_id) => {
317 if let Some(tick) = world.resource_added_tick_for(*type_id) {
318 context.set_resource_added_cursor_for_set(set_index, *type_id, tick);
319 }
320 }
321 ConditionKind::ResourceChanged(type_id) => {
322 if let Some(tick) = world.resource_changed_tick_for(*type_id) {
323 context.set_resource_changed_cursor_for_set(set_index, *type_id, tick);
324 }
325 }
326 ConditionKind::StateChanged(probe) => {
327 if let Some(tick) = (probe.transition_tick)(world) {
328 context.set_state_transition_cursor_for_set(set_index, probe.type_id, tick);
329 }
330 }
331 ConditionKind::And(left, right) | ConditionKind::Or(left, right) => {
332 advance_kind_set_cursors(left, world, set_index, context);
333 advance_kind_set_cursors(right, world, set_index, context);
334 }
335 _ => {}
336 }
337}
338
339impl core::fmt::Debug for Condition {
340 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341 f.write_str("Condition")
342 }
343}
344
345impl core::fmt::Display for ConditionError {
346 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
347 match self {
348 Self::ZeroPeriod => f.write_str("fixed-step cadence period must be nonzero"),
349 Self::PeriodNotPowerOfTwo { period } => {
350 write!(
351 f,
352 "fixed-step cadence period {period} is not a power of two"
353 )
354 }
355 Self::PhaseOutOfRange { period, phase } => write!(
356 f,
357 "fixed-step cadence phase {phase} is outside period {period}"
358 ),
359 }
360 }
361}
362
363#[cfg(feature = "std")]
364impl std::error::Error for ConditionError {}
365
366fn resource_tick_advanced(current: Option<ChangeTick>, cursor: ChangeTick) -> bool {
367 current.is_some_and(|tick| tick > cursor)
368}
369
370fn state_tick_advanced(current: Option<ChangeTick>, cursor: ChangeTick) -> bool {
371 current.is_some_and(|tick| tick > cursor)
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::schedule::RunContext;
378 use crate::time::ChangeTick;
379 use crate::world::WorldBuilder;
380 use alloc::string::ToString;
381
382 #[derive(Clone)]
383 struct Score(#[allow(dead_code)] i32);
384
385 #[test]
386 fn fixed_step_mod_validates_binary_cadence() {
387 assert_eq!(
388 Condition::fixed_step_mod(0, 0).err(),
389 Some(ConditionError::ZeroPeriod)
390 );
391 assert_eq!(
392 Condition::fixed_step_mod(3, 0).err(),
393 Some(ConditionError::PeriodNotPowerOfTwo { period: 3 })
394 );
395 assert_eq!(
396 Condition::fixed_step_mod(4, 4).err(),
397 Some(ConditionError::PhaseOutOfRange {
398 period: 4,
399 phase: 4,
400 })
401 );
402 assert_eq!(
403 ConditionError::ZeroPeriod.to_string(),
404 "fixed-step cadence period must be nonzero"
405 );
406 assert!(ConditionError::PeriodNotPowerOfTwo { period: 3 }
407 .to_string()
408 .contains('3'));
409 assert!(ConditionError::PhaseOutOfRange {
410 period: 4,
411 phase: 4
412 }
413 .to_string()
414 .contains('4'));
415 }
416
417 #[test]
418 fn fixed_step_mod_uses_zero_based_mask_and_is_false_outside_fixed_update() {
419 let world = WorldBuilder::new().build().expect("world");
420 let condition = Condition::fixed_step_mod(4, 0).expect("condition");
421 let mut context = RunContext::new();
422 assert!(!condition.evaluate(&world, 0, &context));
423
424 for index in 0..8 {
425 context.fixed_step = Some(crate::time::FixedStep {
426 index,
427 delta: core::time::Duration::from_millis(16),
428 steps: 1,
429 });
430 assert_eq!(condition.evaluate(&world, 0, &context), index % 4 == 0);
431 assert_eq!(
432 condition.evaluate_for_set(&world, 0, &context),
433 index % 4 == 0
434 );
435 }
436 }
437
438 #[test]
439 fn resource_added_and_changed_advance_system_cursors() {
440 let mut builder = WorldBuilder::new();
441 builder.register_resource::<Score>();
442 let mut world = builder.build().expect("build");
443 let mut context = RunContext::new();
444
445 assert!(!Condition::resource_added::<Score>().evaluate(&world, 0, &context));
446 world.insert_resource(Score(1)).expect("insert");
447 assert!(Condition::resource_added::<Score>().evaluate(&world, 0, &context));
448 Condition::resource_added::<Score>().advance_cursors(&world, 0, &mut context);
449 assert!(!Condition::resource_added::<Score>().evaluate(&world, 0, &context));
450
451 world.insert_resource(Score(2)).expect("replace");
452 assert!(Condition::resource_changed::<Score>().evaluate(&world, 1, &context));
453 Condition::resource_changed::<Score>().advance_cursors(&world, 1, &mut context);
454 assert!(!Condition::resource_changed::<Score>().evaluate(&world, 1, &context));
455 }
456
457 #[test]
458 fn resource_added_evaluate_for_set_advances_set_cursors() {
459 let mut builder = WorldBuilder::new();
460 builder.register_resource::<Score>();
461 let mut world = builder.build().expect("build");
462 let mut context = RunContext::with_set_capacity(1);
463
464 world.insert_resource(Score(1)).expect("insert");
465 let condition = Condition::resource_added::<Score>();
466 assert!(condition.evaluate_for_set(&world, 0, &context));
467 condition.advance_set_cursors(&world, 0, &mut context);
468 assert!(!condition.evaluate_for_set(&world, 0, &context));
469 }
470
471 #[test]
472 fn and_or_combinators_delegate_cursor_advance() {
473 let mut builder = WorldBuilder::new();
474 builder.register_resource::<Score>();
475 let mut world = builder.build().expect("build");
476 let mut context = RunContext::new();
477 world.insert_resource(Score(1)).expect("insert");
478
479 let condition =
480 Condition::resource_added::<Score>().and(Condition::resource_changed::<Score>());
481 assert!(condition.evaluate(&world, 0, &context));
482 condition.advance_cursors(&world, 0, &mut context);
483
484 world.insert_resource(Score(2)).expect("change");
485 let or_condition = Condition::never().or(Condition::resource_changed::<Score>());
486 assert!(or_condition.evaluate(&world, 1, &context));
487 or_condition.advance_set_cursors(&world, 1, &mut context);
488 let tick = world
489 .resource_changed_tick_for(core::any::TypeId::of::<Score>())
490 .expect("tick");
491 assert_eq!(
492 context.resource_changed_cursor_for_set(1, core::any::TypeId::of::<Score>()),
493 tick
494 );
495 }
496
497 #[test]
498 fn state_changed_condition_tracks_transitions() {
499 use crate::state::State;
500
501 let mut builder = WorldBuilder::new();
502 builder.register_state::<u8>();
503 let mut world = builder.build().expect("build");
504 let mut context = RunContext::new();
505
506 world.insert_resource(State::new(1u8)).expect("state");
507 world
508 .resource_mut::<State<u8>>()
509 .expect("mut")
510 .expect("present")
511 .request(2)
512 .expect("request");
513 let tick = world.issue_change_tick_for_state().expect("tick");
514 world
515 .resource_mut::<State<u8>>()
516 .expect("mut")
517 .expect("present")
518 .apply_pending(tick);
519
520 let condition = Condition::state_changed::<u8>();
521 assert!(condition.evaluate(&world, 0, &context));
522 condition.advance_cursors(&world, 0, &mut context);
523 assert!(!condition.evaluate(&world, 0, &context));
524 }
525
526 #[test]
527 fn resource_tick_advanced_helper() {
528 let tick = ChangeTick::from_raw(5);
529 assert!(!resource_tick_advanced(Some(ChangeTick::from_raw(4)), tick));
530 assert!(resource_tick_advanced(Some(ChangeTick::from_raw(6)), tick));
531 assert!(!resource_tick_advanced(None, tick));
532 assert!(!state_tick_advanced(Some(ChangeTick::from_raw(4)), tick));
533 assert!(state_tick_advanced(Some(ChangeTick::from_raw(6)), tick));
534 }
535
536 #[test]
537 fn evaluate_for_set_covers_exists_changed_state_and_combinators() {
538 use crate::state::State;
539
540 let mut builder = WorldBuilder::new();
541 builder.register_resource::<Score>();
542 builder.register_state::<u8>();
543 let mut world = builder.build().expect("build");
544 let mut context = RunContext::with_set_capacity(2);
545
546 assert!(!Condition::resource_exists::<Score>().evaluate_for_set(&world, 0, &context));
547 assert!(!Condition::never().evaluate_for_set(&world, 0, &context));
548 assert!(Condition::always().evaluate_for_set(&world, 0, &context));
549
550 world.insert_resource(Score(1)).expect("insert");
551 assert!(Condition::resource_exists::<Score>().evaluate_for_set(&world, 0, &context));
552
553 world.insert_resource(Score(2)).expect("change");
554 let changed = Condition::resource_changed::<Score>();
555 assert!(changed.evaluate_for_set(&world, 0, &context));
556 changed.advance_set_cursors(&world, 0, &mut context);
557 assert!(!changed.evaluate_for_set(&world, 0, &context));
558
559 world.insert_resource(State::new(1u8)).expect("state");
560 world
561 .resource_mut::<State<u8>>()
562 .expect("mut")
563 .expect("present")
564 .request(2)
565 .expect("request");
566 let tick = world.issue_change_tick_for_state().expect("tick");
567 world
568 .resource_mut::<State<u8>>()
569 .expect("mut")
570 .expect("present")
571 .apply_pending(tick);
572 let state_changed = Condition::state_changed::<u8>();
573 assert!(state_changed.evaluate_for_set(&world, 1, &context));
574 state_changed.advance_set_cursors(&world, 1, &mut context);
575 assert!(!state_changed.evaluate_for_set(&world, 1, &context));
576
577 let and = Condition::always().and(Condition::resource_exists::<Score>());
578 assert!(and.evaluate_for_set(&world, 0, &context));
579 let or = Condition::never().or(Condition::resource_exists::<Score>());
580 assert!(or.evaluate_for_set(&world, 0, &context));
581
582 let predicate = Condition::in_state(2u8);
583 assert!(predicate.evaluate_for_set(&world, 0, &context));
584
585 let and = Condition::resource_added::<Score>().and(Condition::resource_changed::<Score>());
586 and.advance_cursors(&world, 0, &mut RunContext::new());
587 and.advance_set_cursors(&world, 0, &mut context);
588 }
589
590 #[test]
591 fn advancing_absent_temporal_values_leaves_cursors_at_zero() {
592 use crate::state::State;
593 use core::any::TypeId;
594
595 let mut builder = WorldBuilder::new();
596 builder.register_resource::<Score>();
597 builder.register_state::<u8>();
598 let world = builder.build().expect("build");
599 let mut context = RunContext::with_set_capacity(1);
600
601 let score = TypeId::of::<Score>();
602 let state = TypeId::of::<State<u8>>();
603 for condition in [
604 Condition::resource_added::<Score>(),
605 Condition::resource_changed::<Score>(),
606 Condition::state_changed::<u8>(),
607 ] {
608 condition.advance_cursors(&world, 0, &mut context);
609 condition.advance_set_cursors(&world, 0, &mut context);
610 }
611
612 assert_eq!(context.resource_added_cursor(0, score), ChangeTick::ZERO);
613 assert_eq!(context.resource_changed_cursor(0, score), ChangeTick::ZERO);
614 assert_eq!(context.state_transition_cursor(0, state), ChangeTick::ZERO);
615 assert_eq!(
616 context.resource_added_cursor_for_set(0, score),
617 ChangeTick::ZERO
618 );
619 assert_eq!(
620 context.resource_changed_cursor_for_set(0, score),
621 ChangeTick::ZERO
622 );
623 assert_eq!(
624 context.state_transition_cursor_for_set(0, state),
625 ChangeTick::ZERO
626 );
627 }
628}