Skip to main content

pebble/ecs/
system_condition.rs

1use crate::ecs::{
2    resources::Resources,
3    system::{IntoSystem, System},
4    system_set::IntoSystemSet,
5};
6
7/// A predicate checked before a system runs. If `should_run` returns
8/// `false`, the wrapped system's body is skipped entirely for that tick —
9/// its `SystemParam`s are never fetched.
10///
11/// Re-checked every tick. This is deliberate: a resource that exists now
12/// is not guaranteed to exist forever (e.g. if something explicitly removes
13/// it later), so conditions should not assume "ready once" means "ready
14/// forever".
15pub trait RunCondition: 'static {
16    fn should_run(world: &hecs::World, resources: &Resources) -> bool;
17}
18
19/// Runs only while resource `T` exists.
20pub struct ResourceExists<T>(std::marker::PhantomData<T>);
21impl<T: 'static + Send + Sync> RunCondition for ResourceExists<T> {
22    fn should_run(world: &hecs::World, resources: &Resources) -> bool {
23        resources.has_resource::<T>(world)
24    }
25}
26
27/// Runs only while both `A` and `B` would run.
28pub struct And<A, B>(std::marker::PhantomData<(A, B)>);
29impl<A: RunCondition, B: RunCondition> RunCondition for And<A, B> {
30    fn should_run(world: &hecs::World, resources: &Resources) -> bool {
31        A::should_run(world, resources) && B::should_run(world, resources)
32    }
33}
34
35/// Runs while either `A` or `B` would run.
36pub struct Or<A, B>(std::marker::PhantomData<(A, B)>);
37impl<A: RunCondition, B: RunCondition> RunCondition for Or<A, B> {
38    fn should_run(world: &hecs::World, resources: &Resources) -> bool {
39        A::should_run(world, resources) || B::should_run(world, resources)
40    }
41}
42
43/// Wraps a [`System`], skipping it for a tick whenever `C::should_run`
44/// returns `false`.
45pub struct Conditional<S, C> {
46    inner: S,
47    _marker: std::marker::PhantomData<C>,
48}
49
50impl<S: System, C: RunCondition> System for Conditional<S, C> {
51    fn run(&mut self, world: &hecs::World, resources: &Resources) {
52        if C::should_run(world, resources) {
53            self.inner.run(world, resources);
54        }
55    }
56
57    // Deliberately does NOT forward `requires()`: wrapping a system in
58    // `.run_if()` means its author has taken over the "is it safe to run"
59    // question themselves (often via `ResourceExists<T>` for the very
60    // resource the body needs) — App shouldn't second-guess that with its
61    // own pre-flight check on top.
62    fn name(&self) -> &'static str {
63        self.inner.name()
64    }
65}
66
67/// Adds [`.run_if`](RunIfExt::run_if) to anything convertible into a
68/// [`System`], gating it on a [`RunCondition`].
69pub trait RunIfExt<Marker>: IntoSystem<Marker> + Sized {
70    fn run_if<C: RunCondition>(self) -> RunIfSystem<Self, Marker, C> {
71        RunIfSystem {
72            inner: self,
73            _marker: std::marker::PhantomData,
74        }
75    }
76}
77
78impl<Marker, T: IntoSystem<Marker>> RunIfExt<Marker> for T {}
79
80pub struct RunIfSystem<T, Marker, C> {
81    inner: T,
82    _marker: std::marker::PhantomData<(Marker, C)>,
83}
84
85impl<T, Marker, C> IntoSystem<Marker> for RunIfSystem<T, Marker, C>
86where
87    T: IntoSystem<Marker>,
88    C: RunCondition,
89{
90    type System = Conditional<T::System, C>;
91
92    fn into_system(self) -> Self::System {
93        Conditional {
94            inner: self.inner.into_system(),
95            _marker: std::marker::PhantomData,
96        }
97    }
98}
99
100/// Adds [`.run_if_fn`](RunIfFnExt::run_if_fn) to anything convertible into a
101/// [`System`], gating it on a plain closure instead of a [`RunCondition`]
102/// type — no struct/impl boilerplate needed for a one-off check:
103///
104/// ```ignore
105/// app.add_system(
106///     SystemStage::Startup,
107///     setup.run_if_fn(|world, resources| {
108///         resources.has_resource::<PBR>(world)
109///             && resources.get_resource::<PBR>(world).cubemap_material_inst != RawAssetHandle::default()
110///     }),
111/// );
112/// ```
113pub trait RunIfFnExt<Marker>: IntoSystem<Marker> + Sized {
114    fn run_if_fn<F>(self, cond: F) -> RunIfFnSystem<Self, Marker, F>
115    where
116        F: Fn(&hecs::World, &Resources) -> bool + 'static,
117    {
118        RunIfFnSystem {
119            inner: self,
120            cond,
121            _marker: std::marker::PhantomData,
122        }
123    }
124}
125
126impl<Marker, T: IntoSystem<Marker>> RunIfFnExt<Marker> for T {}
127
128pub struct RunIfFnSystem<T, Marker, F> {
129    inner: T,
130    cond: F,
131    _marker: std::marker::PhantomData<Marker>,
132}
133
134impl<T, Marker, F> IntoSystem<Marker> for RunIfFnSystem<T, Marker, F>
135where
136    T: IntoSystem<Marker>,
137    F: Fn(&hecs::World, &Resources) -> bool + 'static,
138    Marker: 'static,
139{
140    type System = ConditionalFn<T::System, F>;
141
142    fn into_system(self) -> Self::System {
143        ConditionalFn {
144            inner: self.inner.into_system(),
145            cond: self.cond,
146        }
147    }
148}
149
150/// Wraps a [`System`], skipping it for a tick whenever `cond` returns
151/// `false`. The closure-based counterpart to [`Conditional`] — see
152/// [`RunIfFnExt::run_if_fn`].
153pub struct ConditionalFn<S, F> {
154    inner: S,
155    cond: F,
156}
157
158impl<S: System, F: Fn(&hecs::World, &Resources) -> bool + 'static> System for ConditionalFn<S, F> {
159    fn run(&mut self, world: &hecs::World, resources: &Resources) {
160        if (self.cond)(world, resources) {
161            self.inner.run(world, resources);
162        }
163    }
164
165    // See the matching note on `Conditional::requires` above: no
166    // `requires()` forwarding — the closure has taken over that question.
167    fn name(&self) -> &'static str {
168        self.inner.name()
169    }
170}
171
172/// Adds [`.run_if`](SystemSetRunIfExt::run_if) to an entire
173/// [`IntoSystemSet`] tuple at once, applying the same condition to every
174/// system in it.
175pub trait SystemSetRunIfExt<M>: IntoSystemSet<M> + Sized {
176    fn run_if<C: RunCondition>(self) -> ConditionalSet<Self, M, C> {
177        ConditionalSet {
178            inner: self,
179            _marker: std::marker::PhantomData,
180        }
181    }
182}
183
184impl<M, T: IntoSystemSet<M>> SystemSetRunIfExt<M> for T {}
185
186pub struct ConditionalSet<T, M, C> {
187    inner: T,
188    _marker: std::marker::PhantomData<(M, C)>,
189}
190
191/// Wraps an already-boxed, type-erased [`System`] with a [`RunCondition`].
192/// Used internally by [`ConditionalSet`], since by the time a system set is
193/// flattened into `Vec<Box<dyn System>>`, individual marker types are gone.
194struct BoxedConditional<C> {
195    inner: Box<dyn System>,
196    _marker: std::marker::PhantomData<C>,
197}
198
199impl<C: RunCondition> System for BoxedConditional<C> {
200    fn run(&mut self, world: &hecs::World, resources: &Resources) {
201        if C::should_run(world, resources) {
202            self.inner.run(world, resources);
203        }
204    }
205
206    // See the matching notes on `Conditional` above.
207    fn name(&self) -> &'static str {
208        self.inner.name()
209    }
210}
211
212impl<T, M, C> IntoSystemSet<M> for ConditionalSet<T, M, C>
213where
214    T: IntoSystemSet<M>,
215    C: RunCondition,
216{
217    fn into_system_set(self) -> Vec<Box<dyn System>> {
218        self.inner
219            .into_system_set()
220            .into_iter()
221            .map(|system| {
222                Box::new(BoxedConditional::<C> {
223                    inner: system,
224                    _marker: std::marker::PhantomData,
225                }) as Box<dyn System>
226            })
227            .collect()
228    }
229}