Skip to main content

nightshade_ecs/
system_param.rs

1//! System-parameter functions over [`DynWorld`]: functions whose
2//! arguments are [`Res`], [`ResMut`], and [`Query`] resolve into runnable
3//! systems the existing [`Schedule`] accepts, with no
4//! `unsafe` and no new executor.
5//!
6//! A system is any function whose parameters are system parameters. Register
7//! it on a [`Schedule`] over a [`DynWorld`] with
8//! [`ScheduleExt::add_system`]:
9//!
10//! ```rust
11//! use nightshade_ecs::Schedule;
12//! use nightshade_ecs::dynamic::DynWorld;
13//! use nightshade_ecs::system_param::{Query, Res, ResMut, ScheduleExt};
14//!
15//! #[derive(Default, Clone, Debug)]
16//! struct Position { x: f32, y: f32 }
17//! #[derive(Default, Clone, Debug)]
18//! struct Velocity { x: f32, y: f32 }
19//!
20//! nightshade_ecs::impl_component!(Position, Velocity);
21//!
22//! struct DeltaTime(f32);
23//! struct Score(u32);
24//!
25//! fn movement(dt: Res<DeltaTime>, mut score: ResMut<Score>, query: Query<(&mut Position, &Velocity)>) {
26//!     query.for_each(|_entity, (position, velocity)| {
27//!         position.x += velocity.x * dt.0;
28//!         position.y += velocity.y * dt.0;
29//!     });
30//!     score.0 += 1;
31//! }
32//!
33//! let mut world = DynWorld::new();
34//! world.insert_resources((DeltaTime(0.5), Score(0)));
35//! world.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 2.0, y: 4.0 }));
36//!
37//! let mut schedule = Schedule::new();
38//! schedule.add_system("movement", movement);
39//! schedule.run(&mut world);
40//!
41//! assert_eq!(world.resource::<Score>().unwrap().0, 1);
42//! ```
43//!
44//! Resource parameters ([`Res`], [`ResMut`]) resolve out of the world's
45//! [`ResourceMap`](crate::dynamic::ResourceMap) through the same take/put
46//! scope [`resources_scope`](crate::dynamic::ResourceHostExt::resources_scope)
47//! uses, so they never alias a query's table borrow. Resource parameters
48//! come first, query parameters after.
49//!
50//! A single query parameter borrows the world directly and pays nothing
51//! beyond the query itself. Several query parameters in one system, such as
52//! `fn(a: Query<&mut Position>, b: Query<&Velocity>)`, share the world
53//! through a cell and each take it only for the span of one
54//! [`for_each`](Query::for_each), so the cost is one borrow check per call
55//! rather than anything per entity. Because `for_each` consumes the query,
56//! two of them run in sequence, never nested. Type-level filters ([`With`],
57//! [`Without`], [`Changed`], [`Added`], [`WithTag`], [`WithoutTag`], and
58//! tuples of them) narrow a query as `Query<(&mut Position,), With<Player>>`.
59//! [`ParamSet`] groups queries behind `p0()`/`p1()` accessors when you would
60//! rather name a set than list the queries.
61//!
62//! Resource-only systems, and systems that end in a `&mut W` host argument,
63//! run over any [`ResourceHost`], not just
64//! [`DynWorld`]. So an engine wrapper that implements `ResourceHost` can
65//! register `fn(Res<A>, ResMut<B>, &mut MyWorld)` on its own
66//! `Schedule<MyWorld>`, pulling resources out of the host's map while the
67//! `&mut MyWorld` stays free for the wrapper's own queries. [`Query`]
68//! parameters resolve against [`DynWorld`], and an unfiltered `Query<Q>` also
69//! resolves against a [`DynEcs`] group through
70//! `query_join`. [`ParamSet`], multiple query parameters, and type-level
71//! query filters are [`DynWorld`] only.
72//!
73//! ```rust
74//! use nightshade_ecs::dynamic::DynWorld;
75//! use nightshade_ecs::system_param::{Query, ScheduleExt};
76//!
77//! #[derive(Default, Clone, Debug)]
78//! struct Position { x: f32, y: f32 }
79//! #[derive(Default, Clone, Debug)]
80//! struct Velocity { x: f32, y: f32 }
81//!
82//! nightshade_ecs::impl_component!(Position, Velocity);
83//!
84//! fn drift(positions: Query<&mut Position>, velocities: Query<&Velocity>) {
85//!     let mut total = (0.0, 0.0);
86//!     velocities.for_each(|_entity, velocity| {
87//!         total.0 += velocity.x;
88//!         total.1 += velocity.y;
89//!     });
90//!     positions.for_each(|_entity, position| {
91//!         position.x += total.0;
92//!         position.y += total.1;
93//!     });
94//! }
95//!
96//! let mut world = DynWorld::new();
97//! world.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 2.0 }));
98//!
99//! let mut schedule = nightshade_ecs::Schedule::new();
100//! schedule.add_system("drift", drift);
101//! schedule.run(&mut world);
102//!
103//! assert_eq!(world.get::<Position>(world.query_ref::<&Position>().single().unwrap().0).unwrap().x, 1.0);
104//! ```
105//!
106//! [`EventReader`] and [`EventWriter`] are extract parameters over the host's
107//! event bus. A writer buffers its sends and flushes them after the system
108//! returns; a reader keeps its own cursor in the runner, so it sees each event
109//! once and coexists with a [`Query`] borrowing the world in the same system.
110//!
111//! ```rust
112//! use nightshade_ecs::Schedule;
113//! use nightshade_ecs::dynamic::DynWorld;
114//! use nightshade_ecs::system_param::{EventReader, EventWriter, ResMut, ScheduleExt};
115//!
116//! #[derive(Clone)]
117//! struct Damaged { amount: u32 }
118//! struct Total(u32);
119//!
120//! fn emit(mut writer: EventWriter<Damaged>) {
121//!     writer.send(Damaged { amount: 3 });
122//!     writer.send(Damaged { amount: 4 });
123//! }
124//! fn accumulate(reader: EventReader<Damaged>, mut total: ResMut<Total>) {
125//!     for event in &reader {
126//!         total.0 += event.amount;
127//!     }
128//! }
129//!
130//! let mut world = DynWorld::new();
131//! world.insert_resource(Total(0));
132//!
133//! let mut schedule = Schedule::new();
134//! schedule.add_system("emit", emit);
135//! schedule.add_system("accumulate", accumulate);
136//! schedule.run(&mut world);
137//!
138//! assert_eq!(world.resource::<Total>().unwrap().0, 7);
139//! ```
140
141use crate::Entity;
142use crate::Schedule;
143use crate::dynamic::{DynEcs, DynJoin, DynQuery, DynWorld, EventBus, QueryTuple, ResourceHost};
144use std::cell::RefCell;
145use std::marker::PhantomData;
146
147/// A shared reference to a resource of type `T`, resolved for a system
148/// parameter. Dereferences to `T`.
149pub struct Res<'world, T> {
150    value: &'world T,
151}
152
153impl<T> std::ops::Deref for Res<'_, T> {
154    type Target = T;
155    fn deref(&self) -> &T {
156        self.value
157    }
158}
159
160/// An exclusive reference to a resource of type `T`, resolved for a system
161/// parameter. Dereferences to `T` and, mutably, writes through to it.
162pub struct ResMut<'world, T> {
163    value: &'world mut T,
164}
165
166impl<T> std::ops::Deref for ResMut<'_, T> {
167    type Target = T;
168    fn deref(&self) -> &T {
169        self.value
170    }
171}
172
173impl<T> std::ops::DerefMut for ResMut<'_, T> {
174    fn deref_mut(&mut self) -> &mut T {
175        self.value
176    }
177}
178
179/// A host that owns an [`EventBus`], so [`EventReader`] and [`EventWriter`]
180/// parameters resolve against it. [`DynWorld`] and [`DynEcs`] both embed one;
181/// a bare [`ResourceHost`] does not, which is why event parameters are
182/// offered over those two containers rather than any host.
183pub trait EventHost {
184    /// The host's event bus, for reading and writing events.
185    fn event_bus_mut(&mut self) -> &mut EventBus;
186
187    /// The same bus, shared, for reading events behind a `&self`, such as the
188    /// frame-settled broadcast [`EventBus::read_frame`](crate::dynamic::EventBus::read_frame).
189    /// Must return the same bus as [`event_bus_mut`](Self::event_bus_mut).
190    fn event_bus(&self) -> &EventBus;
191}
192
193impl EventHost for DynWorld {
194    fn event_bus_mut(&mut self) -> &mut EventBus {
195        &mut self.events
196    }
197
198    fn event_bus(&self) -> &EventBus {
199        &self.events
200    }
201}
202
203impl EventHost for DynEcs {
204    fn event_bus_mut(&mut self) -> &mut EventBus {
205        &mut self.events
206    }
207
208    fn event_bus(&self) -> &EventBus {
209        &self.events
210    }
211}
212
213/// A system parameter resolved by taking data out of the host before the
214/// system runs and writing data back after. [`Res`], [`ResMut`],
215/// [`EventReader`], and [`EventWriter`] are the extract parameters. Each
216/// carries a [`State`](Self::State) kept in the runner between runs (an event
217/// reader's cursor, say), produces an [`Owned`](Self::Owned) value the
218/// parameter borrows for the call, and flushes through [`apply`](Self::apply)
219/// afterward. Extract parameters lead a system's argument list, ahead of a
220/// [`Query`], [`ParamSet`], or trailing `&mut W` host argument. Because the
221/// owned value is lifted out of the host before the run, an extract parameter
222/// never aliases the world a query borrows.
223pub trait ExtractParam<W> {
224    /// Per-system state kept in the runner across runs.
225    type State: Send + 'static;
226    /// The owned value produced for one run; the parameter borrows it.
227    type Owned;
228    /// The parameter value handed to the system for a given borrow.
229    type Item<'item>;
230    /// The initial state for a freshly registered system.
231    fn init() -> Self::State;
232    /// Lifts the owned value out of the host before the run.
233    fn extract(state: &mut Self::State, host: &mut W) -> Self::Owned;
234    /// Wraps the owned value as the parameter value.
235    fn build(owned: &mut Self::Owned) -> Self::Item<'_>;
236    /// Writes back after the run: a resource returns to the map, buffered
237    /// events flush to the bus, a reader does nothing.
238    fn apply(state: &mut Self::State, owned: Self::Owned, host: &mut W);
239}
240
241impl<W: ResourceHost, T: Send + Sync + 'static> ExtractParam<W> for Res<'_, T> {
242    type State = ();
243    type Owned = T;
244    type Item<'item> = Res<'item, T>;
245    fn init() -> Self::State {}
246    fn extract(_state: &mut (), host: &mut W) -> T {
247        host.resource_map_mut().remove::<T>().unwrap_or_else(|| {
248            panic!(
249                "system requires resource {} to be present",
250                std::any::type_name::<T>()
251            )
252        })
253    }
254    fn build(owned: &mut T) -> Res<'_, T> {
255        Res { value: owned }
256    }
257    fn apply(_state: &mut (), owned: T, host: &mut W) {
258        host.resource_map_mut().insert(owned);
259    }
260}
261
262impl<W: ResourceHost, T: Send + Sync + 'static> ExtractParam<W> for ResMut<'_, T> {
263    type State = ();
264    type Owned = T;
265    type Item<'item> = ResMut<'item, T>;
266    fn init() -> Self::State {}
267    fn extract(_state: &mut (), host: &mut W) -> T {
268        host.resource_map_mut().remove::<T>().unwrap_or_else(|| {
269            panic!(
270                "system requires resource {} to be present",
271                std::any::type_name::<T>()
272            )
273        })
274    }
275    fn build(owned: &mut T) -> ResMut<'_, T> {
276        ResMut { value: owned }
277    }
278    fn apply(_state: &mut (), owned: T, host: &mut W) {
279        host.resource_map_mut().insert(owned);
280    }
281}
282
283/// A system parameter that reads events of type `T` from the host's event
284/// bus, delivering each event to this system exactly once across frames. The
285/// reader keeps its own cursor in the runner, so two systems reading the same
286/// event type advance independently and neither disturbs the bus's two-frame
287/// buffer. Events are copied out for the run, so `T` is [`Clone`], and a
288/// [`Query`] in the same system borrows the world freely alongside it.
289pub struct EventReader<'a, T: Clone + Send + Sync + 'static> {
290    events: &'a [T],
291}
292
293impl<T: Clone + Send + Sync + 'static> EventReader<'_, T> {
294    /// Visits the events delivered to this reader this run, oldest first.
295    pub fn iter(&self) -> std::slice::Iter<'_, T> {
296        self.events.iter()
297    }
298
299    /// The number of events delivered to this reader this run.
300    pub fn len(&self) -> usize {
301        self.events.len()
302    }
303
304    /// Whether no events were delivered to this reader this run.
305    pub fn is_empty(&self) -> bool {
306        self.events.is_empty()
307    }
308}
309
310impl<'a, T: Clone + Send + Sync + 'static> IntoIterator for &'a EventReader<'_, T> {
311    type Item = &'a T;
312    type IntoIter = std::slice::Iter<'a, T>;
313    fn into_iter(self) -> Self::IntoIter {
314        self.events.iter()
315    }
316}
317
318impl<W: EventHost, T: Clone + Send + Sync + 'static> ExtractParam<W> for EventReader<'_, T> {
319    type State = u64;
320    type Owned = Vec<T>;
321    type Item<'item> = EventReader<'item, T>;
322    fn init() -> u64 {
323        0
324    }
325    fn extract(state: &mut u64, host: &mut W) -> Vec<T> {
326        host.event_bus_mut().consume::<T>(state).to_vec()
327    }
328    fn build(owned: &mut Vec<T>) -> EventReader<'_, T> {
329        EventReader { events: owned }
330    }
331    fn apply(_state: &mut u64, _owned: Vec<T>, _host: &mut W) {}
332}
333
334/// A system parameter that writes events of type `T` to the host's event bus.
335/// Sends are buffered during the run and flushed to the bus after the system
336/// returns, so a writer and a [`Query`] coexist without contending for the
337/// world. Readers pick the events up through the bus's two-frame buffer.
338pub struct EventWriter<'a, T: Send + Sync + 'static> {
339    pending: &'a mut Vec<T>,
340}
341
342impl<T: Send + Sync + 'static> EventWriter<'_, T> {
343    /// Buffers one event to flush to the bus after the system runs.
344    pub fn send(&mut self, event: T) {
345        self.pending.push(event);
346    }
347
348    /// Buffers a batch of events to flush after the system runs.
349    pub fn send_batch(&mut self, events: impl IntoIterator<Item = T>) {
350        self.pending.extend(events);
351    }
352}
353
354impl<W: EventHost, T: Send + Sync + 'static> ExtractParam<W> for EventWriter<'_, T> {
355    type State = ();
356    type Owned = Vec<T>;
357    type Item<'item> = EventWriter<'item, T>;
358    fn init() -> Self::State {}
359    fn extract(_state: &mut (), _host: &mut W) -> Vec<T> {
360        Vec::new()
361    }
362    fn build(owned: &mut Vec<T>) -> EventWriter<'_, T> {
363        EventWriter { pending: owned }
364    }
365    fn apply(_state: &mut (), owned: Vec<T>, host: &mut W) {
366        for event in owned {
367            host.event_bus_mut().send::<T>(event);
368        }
369    }
370}
371
372/// A type-level query filter: [`With`], [`Without`], [`Changed`], [`Added`],
373/// the unit type for no filter, or a tuple of filters applied in order.
374pub trait QueryFilter {
375    /// Applies this filter to a query builder.
376    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q>;
377}
378
379impl QueryFilter for () {
380    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
381        query
382    }
383}
384
385/// Restricts a query to entities that also carry component `T`, without
386/// fetching it.
387pub struct With<T>(PhantomData<fn() -> T>);
388
389impl<T: Send + Sync + Default + 'static> QueryFilter for With<T> {
390    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
391        query.with::<T>()
392    }
393}
394
395/// Restricts a query to entities that do not carry component `T`.
396pub struct Without<T>(PhantomData<fn() -> T>);
397
398impl<T: Send + Sync + Default + 'static> QueryFilter for Without<T> {
399    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
400        query.without::<T>()
401    }
402}
403
404/// Restricts a query to entities whose component `T` changed since the last
405/// step. `T` must appear in the query tuple.
406pub struct Changed<T>(PhantomData<fn() -> T>);
407
408impl<T: Send + Sync + Default + 'static> QueryFilter for Changed<T> {
409    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
410        query.changed::<T>()
411    }
412}
413
414/// Restricts a query to entities that gained component `T` since the last
415/// step. `T` must appear in the query tuple.
416pub struct Added<T>(PhantomData<fn() -> T>);
417
418impl<T: Send + Sync + Default + 'static> QueryFilter for Added<T> {
419    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
420        query.added::<T>()
421    }
422}
423
424/// Restricts a query to entities carrying the marker tag type `T`.
425pub struct WithTag<T>(PhantomData<fn() -> T>);
426
427impl<T: 'static> QueryFilter for WithTag<T> {
428    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
429        query.with_tag_type::<T>()
430    }
431}
432
433/// Restricts a query to entities not carrying the marker tag type `T`.
434pub struct WithoutTag<T>(PhantomData<fn() -> T>);
435
436impl<T: 'static> QueryFilter for WithoutTag<T> {
437    fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
438        query.without_tag_type::<T>()
439    }
440}
441
442macro_rules! impl_query_filter_tuple {
443    ($($filter:ident),+) => {
444        impl<$($filter: QueryFilter),+> QueryFilter for ($($filter,)+) {
445            fn apply<Q: QueryTuple>(query: DynQuery<'_, Q>) -> DynQuery<'_, Q> {
446                $(let query = $filter::apply(query);)+
447                query
448            }
449        }
450    };
451}
452
453impl_query_filter_tuple!(A);
454impl_query_filter_tuple!(A, B);
455impl_query_filter_tuple!(A, B, C);
456impl_query_filter_tuple!(A, B, C, D);
457
458enum QueryState<'world, Q: QueryTuple> {
459    Eager(DynQuery<'world, Q>),
460    Lazy(&'world RefCell<&'world mut DynWorld>),
461    Join(DynJoin<'world, Q>),
462}
463
464/// A query system parameter over tuple `Q` with type-level filter `F`. The
465/// filter defaults to none. A single query parameter borrows the world
466/// directly; several query parameters in one system share the world through
467/// a cell and take it one [`for_each`](Self::for_each) at a time.
468pub struct Query<'world, Q: QueryTuple, F: QueryFilter = ()> {
469    state: QueryState<'world, Q>,
470    filter: PhantomData<fn() -> F>,
471}
472
473impl<'world, Q: QueryTuple, F: QueryFilter> Query<'world, Q, F> {
474    /// Visits every matching entity with its fetched components.
475    pub fn for_each(self, f: impl for<'item> FnMut(Entity, Q::Item<'item>)) {
476        match self.state {
477            QueryState::Eager(query) => F::apply(query).for_each(f),
478            QueryState::Lazy(cell) => {
479                let mut guard = cell.borrow_mut();
480                let world: &mut DynWorld = &mut guard;
481                F::apply(world.query::<Q>()).for_each(f);
482            }
483            QueryState::Join(join) => join.for_each(f),
484        }
485    }
486
487    /// The parallel form of [`for_each`](Self::for_each), table-granular.
488    #[cfg(not(target_family = "wasm"))]
489    pub fn par_for_each<Fun>(self, f: Fun)
490    where
491        Fun: for<'item> Fn(Entity, Q::Item<'item>) + Send + Sync,
492    {
493        match self.state {
494            QueryState::Eager(query) => F::apply(query).par_for_each(f),
495            QueryState::Lazy(cell) => {
496                let mut guard = cell.borrow_mut();
497                let world: &mut DynWorld = &mut guard;
498                F::apply(world.query::<Q>()).par_for_each(f);
499            }
500            QueryState::Join(join) => join.par_for_each(f),
501        }
502    }
503}
504
505/// A world-borrowing system parameter in the single-query slot, resolved
506/// after the resource parameters are lifted out. Implemented for [`Query`]
507/// and [`ParamSet`].
508pub trait WorldParam {
509    /// The parameter value handed to the system for a given borrow.
510    type Item<'world>;
511    /// Resolves the parameter against the world.
512    fn build(world: &mut DynWorld) -> Self::Item<'_>;
513}
514
515impl<'a, Q: QueryTuple, F: QueryFilter> WorldParam for Query<'a, Q, F> {
516    type Item<'world> = Query<'world, Q, F>;
517    fn build(world: &mut DynWorld) -> Query<'_, Q, F> {
518        Query {
519            state: QueryState::Eager(world.query::<Q>()),
520            filter: PhantomData,
521        }
522    }
523}
524
525/// A query parameter resolved against a [`DynEcs`] group through
526/// [`query_join`](crate::dynamic::DynEcs::query_join), so a system over a
527/// `Schedule<DynEcs>` can take a `Query`. Group queries join across member
528/// worlds under the driver rule, and carry no type-level filter, so only the
529/// unfiltered [`Query<Q>`](Query) is an `EcsParam`. Filter a group query by
530/// taking the group as a `&mut DynEcs` host argument and calling
531/// `query_join` with its builder methods.
532pub trait EcsParam {
533    /// The parameter value handed to the system for a given borrow.
534    type Item<'ecs>;
535    /// Resolves the parameter against the group.
536    fn build_ecs(ecs: &mut DynEcs) -> Self::Item<'_>;
537}
538
539impl<'a, Q: QueryTuple> EcsParam for Query<'a, Q, ()> {
540    type Item<'ecs> = Query<'ecs, Q, ()>;
541    fn build_ecs(ecs: &mut DynEcs) -> Query<'_, Q, ()> {
542        Query {
543            state: QueryState::Join(ecs.query_join::<Q>()),
544            filter: PhantomData,
545        }
546    }
547}
548
549/// A query parameter that shares the world with sibling query parameters
550/// through a cell, so a system can take several [`Query`] arguments. Each
551/// query borrows the world only for the span of one
552/// [`for_each`](Query::for_each), so two queries never iterate at once.
553pub trait MultiQueryParam {
554    /// The parameter value handed to the system for a given borrow.
555    type Item<'world>;
556    /// Resolves the parameter against the shared world cell.
557    fn build_lazy<'world>(cell: &'world RefCell<&'world mut DynWorld>) -> Self::Item<'world>;
558}
559
560impl<'a, Q: QueryTuple, F: QueryFilter> MultiQueryParam for Query<'a, Q, F> {
561    type Item<'world> = Query<'world, Q, F>;
562    fn build_lazy<'world>(cell: &'world RefCell<&'world mut DynWorld>) -> Query<'world, Q, F> {
563        Query {
564            state: QueryState::Lazy(cell),
565            filter: PhantomData,
566        }
567    }
568}
569
570/// A set of conflicting query parameters accessed one at a time. Holds the
571/// world and lends each member query through [`p0`](Self::p0),
572/// [`p1`](Self::p1), and so on, so two mutable queries never live at once.
573pub struct ParamSet<'world, T> {
574    world: &'world mut DynWorld,
575    marker: PhantomData<fn() -> T>,
576}
577
578impl<'a, T: 'static> WorldParam for ParamSet<'a, T>
579where
580    ParamSet<'a, T>: ParamSetAccess,
581{
582    type Item<'world> = ParamSet<'world, T>;
583    fn build(world: &mut DynWorld) -> ParamSet<'_, T> {
584        ParamSet {
585            world,
586            marker: PhantomData,
587        }
588    }
589}
590
591/// Marks the [`ParamSet`] tuples that expose member accessors.
592pub trait ParamSetAccess {}
593
594impl<P0: WorldParam, P1: WorldParam> ParamSetAccess for ParamSet<'_, (P0, P1)> {}
595
596impl<'world, P0: WorldParam, P1: WorldParam> ParamSet<'world, (P0, P1)> {
597    /// Borrows the world for the first member query.
598    pub fn p0(&mut self) -> P0::Item<'_> {
599        P0::build(self.world)
600    }
601
602    /// Borrows the world for the second member query.
603    pub fn p1(&mut self) -> P1::Item<'_> {
604        P1::build(self.world)
605    }
606}
607
608impl<P0: WorldParam, P1: WorldParam, P2: WorldParam> ParamSetAccess for ParamSet<'_, (P0, P1, P2)> {}
609
610impl<'world, P0: WorldParam, P1: WorldParam, P2: WorldParam> ParamSet<'world, (P0, P1, P2)> {
611    /// Borrows the world for the first member query.
612    pub fn p0(&mut self) -> P0::Item<'_> {
613        P0::build(self.world)
614    }
615
616    /// Borrows the world for the second member query.
617    pub fn p1(&mut self) -> P1::Item<'_> {
618        P1::build(self.world)
619    }
620
621    /// Borrows the world for the third member query.
622    pub fn p2(&mut self) -> P2::Item<'_> {
623        P2::build(self.world)
624    }
625}
626
627/// Converts a system-parameter function into a runner the
628/// [`Schedule`] accepts, over a world type `W`. `Marker` is inferred from
629/// the function's parameter types. Resource-only systems (`fn(Res<A>,
630/// ResMut<B>)`) and systems that take the host as a final `&mut W` argument
631/// work over any [`ResourceHost`]; systems with [`Query`]/[`ParamSet`]
632/// parameters resolve against [`DynWorld`] specifically. Register through
633/// [`ScheduleExt::add_system`].
634pub trait IntoSystem<W, Marker>: Sized {
635    /// Wraps the function into a closure that resolves its parameters from
636    /// the world on each call.
637    fn into_runner(self) -> impl FnMut(&mut W) + Send + 'static;
638}
639
640/// Registers system-parameter functions on a [`Schedule<W>`](crate::Schedule),
641/// resolving [`Res`]/[`ResMut`]/[`Query`] parameters per run.
642pub trait ScheduleExt<W> {
643    /// Adds a system-parameter function under `name`, like
644    /// [`Schedule::push`](crate::Schedule::push) for plain systems. Returns
645    /// `&mut Self`, so calls chain in builder style.
646    fn add_system<Marker>(
647        &mut self,
648        name: &'static str,
649        system: impl IntoSystem<W, Marker>,
650    ) -> &mut Self;
651
652    /// Adds a system-parameter function that runs only on the passes where
653    /// `condition` holds, the param-system form of
654    /// [`Schedule::push_if`](crate::Schedule::push_if). The condition reads
655    /// the world each pass; a false skips the system for that pass without
656    /// removing it. This is how a run condition composes with resource,
657    /// query, and event parameters, so gating a system on a state the host
658    /// carries stays declarative: pass `|world| in_state(world, ...)` as the
659    /// condition. The state machinery itself belongs to the host, not here.
660    fn add_system_if<Marker>(
661        &mut self,
662        name: &'static str,
663        condition: impl Fn(&W) -> bool + Send + 'static,
664        system: impl IntoSystem<W, Marker>,
665    ) -> &mut Self;
666
667    /// Adds a tuple of system-parameter functions in one call, each named
668    /// after its function type, so `schedule.add_systems((movement, score))`
669    /// stands in for one [`add_system`](Self::add_system) per system. Reach
670    /// for `add_system` when a system needs an explicit name for later
671    /// [`replace`](crate::Schedule::replace) or removal.
672    fn add_systems<Marker>(&mut self, systems: impl IntoSystems<W, Marker>) -> &mut Self;
673}
674
675impl<W> ScheduleExt<W> for Schedule<W> {
676    fn add_system<Marker>(
677        &mut self,
678        name: &'static str,
679        system: impl IntoSystem<W, Marker>,
680    ) -> &mut Self {
681        self.push(name, system.into_runner())
682    }
683
684    fn add_system_if<Marker>(
685        &mut self,
686        name: &'static str,
687        condition: impl Fn(&W) -> bool + Send + 'static,
688        system: impl IntoSystem<W, Marker>,
689    ) -> &mut Self {
690        self.push_if(name, condition, system.into_runner())
691    }
692
693    fn add_systems<Marker>(&mut self, systems: impl IntoSystems<W, Marker>) -> &mut Self {
694        systems.register(self);
695        self
696    }
697}
698
699fn add_named_system<W, Marker>(schedule: &mut Schedule<W>, system: impl IntoSystem<W, Marker>) {
700    let base = std::any::type_name_of_val(&system);
701    let name = if schedule.contains(base) {
702        let mut suffix = 2;
703        loop {
704            let candidate = format!("{base}#{suffix}");
705            if !schedule.contains(&candidate) {
706                break Box::leak(candidate.into_boxed_str()) as &'static str;
707            }
708            suffix += 1;
709        }
710    } else {
711        base
712    };
713    schedule.push(name, system.into_runner());
714}
715
716/// A tuple of system-parameter functions registered together by
717/// [`ScheduleExt::add_systems`]. Implemented for tuples of up to eight
718/// systems, each of which is an [`IntoSystem`].
719pub trait IntoSystems<W, Marker> {
720    /// Registers each system in the tuple onto `schedule`.
721    fn register(self, schedule: &mut Schedule<W>);
722}
723
724macro_rules! impl_into_systems {
725    ($(($system:ident, $marker:ident)),+) => {
726        impl<W, $($system, $marker,)+> IntoSystems<W, ($($marker,)+)> for ($($system,)+)
727        where
728            $($system: IntoSystem<W, $marker>,)+
729        {
730            #[allow(non_snake_case)]
731            fn register(self, schedule: &mut Schedule<W>) {
732                let ($($system,)+) = self;
733                $(add_named_system(schedule, $system);)+
734            }
735        }
736    };
737}
738
739impl_into_systems!((S0, M0));
740impl_into_systems!((S0, M0), (S1, M1));
741impl_into_systems!((S0, M0), (S1, M1), (S2, M2));
742impl_into_systems!((S0, M0), (S1, M1), (S2, M2), (S3, M3));
743impl_into_systems!((S0, M0), (S1, M1), (S2, M2), (S3, M3), (S4, M4));
744impl_into_systems!((S0, M0), (S1, M1), (S2, M2), (S3, M3), (S4, M4), (S5, M5));
745impl_into_systems!(
746    (S0, M0),
747    (S1, M1),
748    (S2, M2),
749    (S3, M3),
750    (S4, M4),
751    (S5, M5),
752    (S6, M6)
753);
754impl_into_systems!(
755    (S0, M0),
756    (S1, M1),
757    (S2, M2),
758    (S3, M3),
759    (S4, M4),
760    (S5, M5),
761    (S6, M6),
762    (S7, M7)
763);
764
765/// The marker for a system whose parameters are all extract parameters.
766pub struct ExtractSystemMarker<E>(PhantomData<fn() -> E>);
767
768/// The marker for a system with extract parameters and one trailing
769/// world-borrowing parameter.
770pub struct QuerySystemMarker<E, Q>(PhantomData<fn() -> (E, Q)>);
771
772impl<Func, Q> IntoSystem<DynWorld, QuerySystemMarker<(), Q>> for Func
773where
774    Q: WorldParam,
775    Func: FnMut(Q) + for<'world> FnMut(Q::Item<'world>) + Send + 'static,
776{
777    fn into_runner(mut self) -> impl FnMut(&mut DynWorld) + Send + 'static {
778        move |world: &mut DynWorld| {
779            let query = Q::build(world);
780            self(query);
781        }
782    }
783}
784
785/// The marker for a system whose parameters are all extract parameters
786/// followed by a final `&mut W` host argument.
787pub struct HostSystemMarker<E>(PhantomData<fn() -> E>);
788
789macro_rules! impl_extract_system {
790    ($($param:ident $state:ident $owned:ident),+) => {
791        impl<W, Func, $($param,)+> IntoSystem<W, ExtractSystemMarker<($($param,)+)>>
792            for Func
793        where
794            W: 'static,
795            $($param: ExtractParam<W>,)+
796            Func: FnMut($($param,)+)
797                + for<'item> FnMut($($param::Item<'item>,)+)
798                + Send
799                + 'static,
800        {
801            fn into_runner(mut self) -> impl FnMut(&mut W) + Send + 'static {
802                $(let mut $state = <$param as ExtractParam<W>>::init();)+
803                move |host: &mut W| {
804                    $(let mut $owned = <$param as ExtractParam<W>>::extract(&mut $state, host);)+
805                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
806                        self($(<$param as ExtractParam<W>>::build(&mut $owned),)+);
807                    }));
808                    $(<$param as ExtractParam<W>>::apply(&mut $state, $owned, host);)+
809                    if let Err(panic) = result {
810                        std::panic::resume_unwind(panic);
811                    }
812                }
813            }
814        }
815
816        impl<W, Func, $($param,)+> IntoSystem<W, HostSystemMarker<($($param,)+)>>
817            for Func
818        where
819            W: 'static,
820            $($param: ExtractParam<W>,)+
821            Func: FnMut($($param,)+ &mut W)
822                + for<'item> FnMut($($param::Item<'item>,)+ &'item mut W)
823                + Send
824                + 'static,
825        {
826            fn into_runner(mut self) -> impl FnMut(&mut W) + Send + 'static {
827                $(let mut $state = <$param as ExtractParam<W>>::init();)+
828                move |host: &mut W| {
829                    $(let mut $owned = <$param as ExtractParam<W>>::extract(&mut $state, host);)+
830                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
831                        self($(<$param as ExtractParam<W>>::build(&mut $owned),)+ host);
832                    }));
833                    $(<$param as ExtractParam<W>>::apply(&mut $state, $owned, host);)+
834                    if let Err(panic) = result {
835                        std::panic::resume_unwind(panic);
836                    }
837                }
838            }
839        }
840    };
841}
842
843macro_rules! impl_extract_query_system {
844    ($($param:ident $state:ident $owned:ident),+) => {
845        impl<Func, $($param,)+ Q>
846            IntoSystem<DynWorld, QuerySystemMarker<($($param,)+), Q>> for Func
847        where
848            $($param: ExtractParam<DynWorld>,)+
849            Q: WorldParam,
850            Func: FnMut($($param,)+ Q)
851                + for<'item> FnMut($($param::Item<'item>,)+ Q::Item<'item>)
852                + Send
853                + 'static,
854        {
855            fn into_runner(mut self) -> impl FnMut(&mut DynWorld) + Send + 'static {
856                $(let mut $state = <$param as ExtractParam<DynWorld>>::init();)+
857                move |world: &mut DynWorld| {
858                    $(let mut $owned =
859                        <$param as ExtractParam<DynWorld>>::extract(&mut $state, world);)+
860                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
861                        let query = Q::build(world);
862                        self($(<$param as ExtractParam<DynWorld>>::build(&mut $owned),)+ query);
863                    }));
864                    $(<$param as ExtractParam<DynWorld>>::apply(&mut $state, $owned, world);)+
865                    if let Err(panic) = result {
866                        std::panic::resume_unwind(panic);
867                    }
868                }
869            }
870        }
871    };
872}
873
874impl_extract_system!(P0 state0 owned0);
875impl_extract_system!(P0 state0 owned0, P1 state1 owned1);
876impl_extract_system!(P0 state0 owned0, P1 state1 owned1, P2 state2 owned2);
877impl_extract_system!(P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3);
878impl_extract_system!(
879    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4
880);
881impl_extract_system!(
882    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
883    P5 state5 owned5
884);
885impl_extract_system!(
886    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
887    P5 state5 owned5, P6 state6 owned6
888);
889impl_extract_system!(
890    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
891    P5 state5 owned5, P6 state6 owned6, P7 state7 owned7
892);
893
894impl_extract_query_system!(P0 state0 owned0);
895impl_extract_query_system!(P0 state0 owned0, P1 state1 owned1);
896impl_extract_query_system!(P0 state0 owned0, P1 state1 owned1, P2 state2 owned2);
897impl_extract_query_system!(P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3);
898impl_extract_query_system!(
899    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4
900);
901impl_extract_query_system!(
902    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
903    P5 state5 owned5
904);
905impl_extract_query_system!(
906    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
907    P5 state5 owned5, P6 state6 owned6
908);
909
910impl<Func, Q> IntoSystem<DynEcs, QuerySystemMarker<(), Q>> for Func
911where
912    Q: EcsParam,
913    Func: FnMut(Q) + for<'ecs> FnMut(Q::Item<'ecs>) + Send + 'static,
914{
915    fn into_runner(mut self) -> impl FnMut(&mut DynEcs) + Send + 'static {
916        move |ecs: &mut DynEcs| {
917            let query = Q::build_ecs(ecs);
918            self(query);
919        }
920    }
921}
922
923macro_rules! impl_extract_ecs_query_system {
924    ($($param:ident $state:ident $owned:ident),+) => {
925        impl<Func, $($param,)+ Q>
926            IntoSystem<DynEcs, QuerySystemMarker<($($param,)+), Q>> for Func
927        where
928            $($param: ExtractParam<DynEcs>,)+
929            Q: EcsParam,
930            Func: FnMut($($param,)+ Q)
931                + for<'item> FnMut($($param::Item<'item>,)+ Q::Item<'item>)
932                + Send
933                + 'static,
934        {
935            fn into_runner(mut self) -> impl FnMut(&mut DynEcs) + Send + 'static {
936                $(let mut $state = <$param as ExtractParam<DynEcs>>::init();)+
937                move |ecs: &mut DynEcs| {
938                    $(let mut $owned = <$param as ExtractParam<DynEcs>>::extract(&mut $state, ecs);)+
939                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
940                        let query = Q::build_ecs(ecs);
941                        self($(<$param as ExtractParam<DynEcs>>::build(&mut $owned),)+ query);
942                    }));
943                    $(<$param as ExtractParam<DynEcs>>::apply(&mut $state, $owned, ecs);)+
944                    if let Err(panic) = result {
945                        std::panic::resume_unwind(panic);
946                    }
947                }
948            }
949        }
950    };
951}
952
953impl_extract_ecs_query_system!(P0 state0 owned0);
954impl_extract_ecs_query_system!(P0 state0 owned0, P1 state1 owned1);
955impl_extract_ecs_query_system!(P0 state0 owned0, P1 state1 owned1, P2 state2 owned2);
956impl_extract_ecs_query_system!(
957    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3
958);
959impl_extract_ecs_query_system!(
960    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4
961);
962impl_extract_ecs_query_system!(
963    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
964    P5 state5 owned5
965);
966impl_extract_ecs_query_system!(
967    P0 state0 owned0, P1 state1 owned1, P2 state2 owned2, P3 state3 owned3, P4 state4 owned4,
968    P5 state5 owned5, P6 state6 owned6
969);
970
971/// The marker for a system with resource parameters and two or more
972/// world-borrowing query parameters resolved through a shared world cell.
973pub struct MultiQuerySystemMarker<R, Q>(PhantomData<fn() -> (R, Q)>);
974
975macro_rules! impl_multi_query_bare {
976    ($($query:ident $qbind:ident),+) => {
977        impl<Func, $($query,)+>
978            IntoSystem<DynWorld, MultiQuerySystemMarker<(), ($($query,)+)>> for Func
979        where
980            $($query: MultiQueryParam,)+
981            Func: FnMut($($query,)+)
982                + for<'world> FnMut($($query::Item<'world>,)+)
983                + Send
984                + 'static,
985        {
986            #[allow(non_snake_case)]
987            fn into_runner(mut self) -> impl FnMut(&mut DynWorld) + Send + 'static {
988                move |world: &mut DynWorld| {
989                    let cell = RefCell::new(world);
990                    $(let $qbind = $query::build_lazy(&cell);)+
991                    self($($qbind,)+);
992                }
993            }
994        }
995    };
996}
997
998macro_rules! impl_extract_multi_query {
999    (($($param:ident $state:ident $owned:ident),+), ($($query:ident $qbind:ident),+)) => {
1000        impl<Func, $($param,)+ $($query,)+>
1001            IntoSystem<DynWorld, MultiQuerySystemMarker<($($param,)+), ($($query,)+)>> for Func
1002        where
1003            $($param: ExtractParam<DynWorld>,)+
1004            $($query: MultiQueryParam,)+
1005            Func: FnMut($($param,)+ $($query,)+)
1006                + for<'item> FnMut($($param::Item<'item>,)+ $($query::Item<'item>,)+)
1007                + Send
1008                + 'static,
1009        {
1010            fn into_runner(mut self) -> impl FnMut(&mut DynWorld) + Send + 'static {
1011                $(let mut $state = <$param as ExtractParam<DynWorld>>::init();)+
1012                move |world: &mut DynWorld| {
1013                    $(let mut $owned =
1014                        <$param as ExtractParam<DynWorld>>::extract(&mut $state, world);)+
1015                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1016                        let cell = RefCell::new(&mut *world);
1017                        $(let $qbind = $query::build_lazy(&cell);)+
1018                        self(
1019                            $(<$param as ExtractParam<DynWorld>>::build(&mut $owned),)+
1020                            $($qbind,)+
1021                        );
1022                    }));
1023                    $(<$param as ExtractParam<DynWorld>>::apply(&mut $state, $owned, world);)+
1024                    if let Err(panic) = result {
1025                        std::panic::resume_unwind(panic);
1026                    }
1027                }
1028            }
1029        }
1030    };
1031}
1032
1033impl_multi_query_bare!(Q0 q0, Q1 q1);
1034impl_multi_query_bare!(Q0 q0, Q1 q1, Q2 q2);
1035impl_multi_query_bare!(Q0 q0, Q1 q1, Q2 q2, Q3 q3);
1036
1037impl_extract_multi_query!((P0 state0 owned0), (Q0 q0, Q1 q1));
1038impl_extract_multi_query!((P0 state0 owned0), (Q0 q0, Q1 q1, Q2 q2));
1039impl_extract_multi_query!((P0 state0 owned0, P1 state1 owned1), (Q0 q0, Q1 q1));
1040impl_extract_multi_query!((P0 state0 owned0, P1 state1 owned1), (Q0 q0, Q1 q1, Q2 q2));
1041impl_extract_multi_query!((P0 state0 owned0, P1 state1 owned1, P2 state2 owned2), (Q0 q0, Q1 q1));
1042impl_extract_multi_query!(
1043    (P0 state0 owned0, P1 state1 owned1, P2 state2 owned2),
1044    (Q0 q0, Q1 q1, Q2 q2)
1045);
1046
1047#[cfg(test)]
1048mod tests {
1049    use super::*;
1050    use crate::Schedule;
1051    use crate::dynamic::{ComponentRegistry, DynEcs, DynWorld, ResourceMap};
1052
1053    struct Engine {
1054        resources: ResourceMap,
1055        frames: u32,
1056    }
1057
1058    impl ResourceHost for Engine {
1059        fn resource_map_mut(&mut self) -> &mut ResourceMap {
1060            &mut self.resources
1061        }
1062        fn resource_map(&self) -> &ResourceMap {
1063            &self.resources
1064        }
1065    }
1066
1067    #[derive(Default, Clone, Debug, PartialEq)]
1068    struct Position {
1069        x: f32,
1070        y: f32,
1071    }
1072
1073    #[derive(Default, Clone, Debug, PartialEq)]
1074    struct Velocity {
1075        x: f32,
1076        y: f32,
1077    }
1078
1079    #[derive(Default, Clone, Debug, PartialEq)]
1080    struct Health {
1081        value: f32,
1082    }
1083
1084    crate::impl_component!(Position, Velocity, Health);
1085
1086    struct DeltaTime(f32);
1087    struct Score(u32);
1088    struct Tally(u32);
1089    struct Seen(Vec<u32>);
1090    struct Enabled(bool);
1091    struct Frozen;
1092
1093    #[derive(Clone, Debug, PartialEq)]
1094    struct Collision {
1095        entity: u32,
1096    }
1097
1098    fn run<Marker>(world: &mut DynWorld, system: impl IntoSystem<DynWorld, Marker>) {
1099        let mut runner = system.into_runner();
1100        runner(world);
1101    }
1102
1103    #[test]
1104    fn single_resource_param() {
1105        let mut world = DynWorld::new();
1106        world.insert_resource(Score(0));
1107        run(&mut world, |mut score: ResMut<Score>| score.0 += 5);
1108        assert_eq!(world.resource::<Score>().unwrap().0, 5);
1109    }
1110
1111    #[test]
1112    fn multiple_resource_params() {
1113        let mut world = DynWorld::new();
1114        world.insert_resource(DeltaTime(2.0));
1115        world.insert_resource(Score(1));
1116        run(
1117            &mut world,
1118            |delta: Res<DeltaTime>, mut score: ResMut<Score>| {
1119                score.0 += delta.0 as u32;
1120            },
1121        );
1122        assert_eq!(world.resource::<Score>().unwrap().0, 3);
1123    }
1124
1125    #[test]
1126    fn query_only_param() {
1127        let mut world = DynWorld::new();
1128        world.spawn((Position { x: 1.0, y: 2.0 },));
1129        run(&mut world, |query: Query<&mut Position>| {
1130            query.for_each(|_entity, position| position.x += 10.0);
1131        });
1132        let position = world.query_ref::<&Position>().single().unwrap().1.clone();
1133        assert_eq!(position, Position { x: 11.0, y: 2.0 });
1134    }
1135
1136    #[test]
1137    fn resources_and_query_together() {
1138        let mut world = DynWorld::new();
1139        world.insert_resource(DeltaTime(0.5));
1140        world.insert_resource(Score(0));
1141        world.spawn((
1142            Position { x: 0.0, y: 0.0 },
1143            Velocity { x: 2.0, y: 4.0 },
1144            Health { value: 100.0 },
1145        ));
1146        world.spawn((Position { x: 5.0, y: 5.0 }, Velocity { x: 1.0, y: 1.0 }));
1147
1148        run(
1149            &mut world,
1150            |delta: Res<DeltaTime>,
1151             mut score: ResMut<Score>,
1152             query: Query<(&mut Position, &Velocity, Option<&mut Health>)>| {
1153                query.for_each(|_entity, (position, velocity, health)| {
1154                    position.x += velocity.x * delta.0;
1155                    position.y += velocity.y * delta.0;
1156                    if let Some(health) = health {
1157                        health.value *= 0.9;
1158                    }
1159                });
1160                score.0 += 1;
1161            },
1162        );
1163
1164        assert_eq!(world.resource::<Score>().unwrap().0, 1);
1165        let mut positions: Vec<(f32, f32)> = world
1166            .query_ref::<&Position>()
1167            .iter()
1168            .map(|(_entity, position)| (position.x, position.y))
1169            .collect();
1170        positions.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
1171        assert_eq!(positions, vec![(1.0, 2.0), (5.5, 5.5)]);
1172    }
1173
1174    #[test]
1175    fn with_filter_narrows_match() {
1176        let mut world = DynWorld::new();
1177        let frozen = world.spawn((Position { x: 0.0, y: 0.0 },));
1178        world.add_tag_type::<Frozen>(frozen);
1179        world.spawn((Position { x: 0.0, y: 0.0 },));
1180
1181        run(&mut world, |query: Query<&mut Position, With<Health>>| {
1182            query.for_each(|_entity, position| position.x = 1.0);
1183        });
1184        assert!(
1185            world
1186                .query_ref::<&Position>()
1187                .iter()
1188                .all(|(_entity, position)| position.x == 0.0)
1189        );
1190
1191        run(
1192            &mut world,
1193            |query: Query<&mut Position, WithoutTag<Frozen>>| {
1194                query.for_each(|_entity, position| position.x = 2.0);
1195            },
1196        );
1197        let moved = world
1198            .query_ref::<&Position>()
1199            .iter()
1200            .filter(|(_entity, position)| position.x == 2.0)
1201            .count();
1202        assert_eq!(moved, 1);
1203    }
1204
1205    #[test]
1206    fn changed_filter_visits_only_mutated() {
1207        let mut world = DynWorld::new();
1208        world.set_change_detection(true);
1209        let entity = world.spawn((Position { x: 0.0, y: 0.0 },));
1210        world.spawn((Position { x: 0.0, y: 0.0 },));
1211        world.step();
1212        world.get_mut::<Position>(entity).unwrap().x = 1.0;
1213
1214        let mut visited = 0;
1215        run(&mut world, |query: Query<&Position, Changed<Position>>| {
1216            query.for_each(|_entity, _position| {});
1217        });
1218        world
1219            .query_ref::<&Position>()
1220            .changed::<Position>()
1221            .iter()
1222            .for_each(|_| visited += 1);
1223        assert_eq!(visited, 1);
1224    }
1225
1226    #[test]
1227    fn param_set_lends_conflicting_queries() {
1228        let mut world = DynWorld::new();
1229        world.spawn((Position { x: 1.0, y: 1.0 }, Velocity { x: 3.0, y: 3.0 }));
1230
1231        run(
1232            &mut world,
1233            |mut set: ParamSet<(Query<&mut Position>, Query<&Velocity>)>| {
1234                let mut velocity = (0.0, 0.0);
1235                set.p1()
1236                    .for_each(|_entity, value| velocity = (value.x, value.y));
1237                set.p0().for_each(|_entity, position| {
1238                    position.x += velocity.0;
1239                    position.y += velocity.1;
1240                });
1241            },
1242        );
1243
1244        let position = world.query_ref::<&Position>().single().unwrap().1.clone();
1245        assert_eq!(position, Position { x: 4.0, y: 4.0 });
1246    }
1247
1248    #[test]
1249    fn two_direct_queries_disjoint_components() {
1250        let mut world = DynWorld::new();
1251        world.spawn((Position { x: 1.0, y: 1.0 }, Velocity { x: 3.0, y: 4.0 }));
1252
1253        run(
1254            &mut world,
1255            |positions: Query<&mut Position>, velocities: Query<&Velocity>| {
1256                let mut sample = (0.0, 0.0);
1257                velocities.for_each(|_entity, velocity| sample = (velocity.x, velocity.y));
1258                positions.for_each(|_entity, position| {
1259                    position.x += sample.0;
1260                    position.y += sample.1;
1261                });
1262            },
1263        );
1264
1265        let position = world.query_ref::<&Position>().single().unwrap().1.clone();
1266        assert_eq!(position, Position { x: 4.0, y: 5.0 });
1267    }
1268
1269    #[test]
1270    fn two_direct_queries_same_component_run_sequentially() {
1271        let mut world = DynWorld::new();
1272        world.spawn((Position { x: 0.0, y: 0.0 },));
1273
1274        run(
1275            &mut world,
1276            |first: Query<&mut Position>, second: Query<&mut Position>| {
1277                first.for_each(|_entity, position| position.x += 1.0);
1278                second.for_each(|_entity, position| position.x += 10.0);
1279            },
1280        );
1281
1282        assert_eq!(world.query_ref::<&Position>().single().unwrap().1.x, 11.0);
1283    }
1284
1285    #[test]
1286    fn resource_with_two_direct_queries() {
1287        let mut world = DynWorld::new();
1288        world.insert_resource(Score(0));
1289        world.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 2.0, y: 0.0 }));
1290
1291        run(
1292            &mut world,
1293            |mut score: ResMut<Score>,
1294             positions: Query<&mut Position>,
1295             velocities: Query<&Velocity>| {
1296                let mut velocity_x = 0.0;
1297                velocities.for_each(|_entity, velocity| velocity_x = velocity.x);
1298                positions.for_each(|_entity, position| position.x += velocity_x);
1299                score.0 += 1;
1300            },
1301        );
1302
1303        assert_eq!(world.resource::<Score>().unwrap().0, 1);
1304        assert_eq!(world.query_ref::<&Position>().single().unwrap().1.x, 2.0);
1305    }
1306
1307    #[test]
1308    fn schedule_add_system_runs_param_systems() {
1309        let mut world = DynWorld::new();
1310        world.insert_resource(Score(0));
1311        world.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 0.0 }));
1312
1313        fn movement(query: Query<(&mut Position, &Velocity)>) {
1314            query.for_each(|_entity, (position, velocity)| position.x += velocity.x);
1315        }
1316        fn scoring(mut score: ResMut<Score>) {
1317            score.0 += 1;
1318        }
1319
1320        let mut schedule = Schedule::new();
1321        schedule.add_system("movement", movement);
1322        schedule.add_system("scoring", scoring);
1323        schedule.run(&mut world);
1324        schedule.run(&mut world);
1325
1326        assert_eq!(world.resource::<Score>().unwrap().0, 2);
1327        let position = world.query_ref::<&Position>().single().unwrap().1.clone();
1328        assert_eq!(position, Position { x: 2.0, y: 0.0 });
1329    }
1330
1331    #[test]
1332    fn add_systems_registers_a_tuple() {
1333        let mut world = DynWorld::new();
1334        world.insert_resource(Score(0));
1335        world.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 0.0 }));
1336
1337        fn movement(query: Query<(&mut Position, &Velocity)>) {
1338            query.for_each(|_entity, (position, velocity)| position.x += velocity.x);
1339        }
1340        fn scoring(mut score: ResMut<Score>) {
1341            score.0 += 1;
1342        }
1343
1344        let mut schedule = Schedule::new();
1345        schedule.add_systems((movement, scoring));
1346        assert_eq!(schedule.len(), 2);
1347        schedule.run(&mut world);
1348
1349        assert_eq!(world.resource::<Score>().unwrap().0, 1);
1350        assert_eq!(world.query_ref::<&Position>().single().unwrap().1.x, 1.0);
1351    }
1352
1353    #[test]
1354    fn add_systems_dedups_repeated_registration() {
1355        fn noop(_query: Query<&Position>) {}
1356
1357        let mut schedule = Schedule::<DynWorld>::new();
1358        schedule.add_systems((noop,));
1359        schedule.add_systems((noop,));
1360        assert_eq!(schedule.len(), 2);
1361    }
1362
1363    #[test]
1364    fn insert_resources_batches_a_tuple() {
1365        let mut world = DynWorld::new();
1366        world.insert_resources((DeltaTime(0.5), Score(7)));
1367        assert_eq!(world.resource::<DeltaTime>().unwrap().0, 0.5);
1368        assert_eq!(world.resource::<Score>().unwrap().0, 7);
1369    }
1370
1371    #[test]
1372    fn resource_system_runs_on_custom_host() {
1373        let mut engine = Engine {
1374            resources: ResourceMap::default(),
1375            frames: 0,
1376        };
1377        engine.resources.insert(Score(0));
1378
1379        let mut schedule = Schedule::<Engine>::new();
1380        schedule.add_system("bump", |mut score: ResMut<Score>| score.0 += 3);
1381        schedule.run(&mut engine);
1382
1383        assert_eq!(engine.resources.get::<Score>().unwrap().0, 3);
1384    }
1385
1386    #[test]
1387    fn host_system_gets_resources_and_the_host() {
1388        fn tick(mut score: ResMut<Score>, engine: &mut Engine) {
1389            score.0 += 1;
1390            engine.frames += 1;
1391        }
1392
1393        let mut engine = Engine {
1394            resources: ResourceMap::default(),
1395            frames: 0,
1396        };
1397        engine.resources.insert(Score(10));
1398
1399        let mut schedule = Schedule::<Engine>::new();
1400        schedule.add_system("tick", tick);
1401        schedule.run(&mut engine);
1402        schedule.run(&mut engine);
1403
1404        assert_eq!(engine.resources.get::<Score>().unwrap().0, 12);
1405        assert_eq!(engine.frames, 2);
1406    }
1407
1408    #[test]
1409    fn host_system_on_dynworld_can_query() {
1410        fn count_positions(mut score: ResMut<Score>, world: &mut DynWorld) {
1411            score.0 += world.query_ref::<&Position>().iter().count() as u32;
1412        }
1413
1414        let mut world = DynWorld::new();
1415        world.insert_resource(Score(0));
1416        world.spawn((Position { x: 0.0, y: 0.0 },));
1417        world.spawn((Position { x: 1.0, y: 1.0 },));
1418
1419        run(&mut world, count_positions);
1420
1421        assert_eq!(world.resource::<Score>().unwrap().0, 2);
1422    }
1423
1424    #[test]
1425    fn query_param_runs_over_a_group() {
1426        fn movement(query: Query<(&mut Position, &Velocity)>) {
1427            query.for_each(|_entity, (position, velocity)| position.x += velocity.x);
1428        }
1429
1430        let mut registry = ComponentRegistry::new();
1431        registry.register::<Position>();
1432        registry.register::<Velocity>();
1433        let mut ecs = DynEcs::new();
1434        ecs.add_world(registry);
1435        let entity = ecs.spawn_with((Position { x: 1.0, y: 0.0 }, Velocity { x: 3.0, y: 0.0 }));
1436
1437        let mut schedule = Schedule::<DynEcs>::new();
1438        schedule.add_system("movement", movement);
1439        schedule.run(&mut ecs);
1440
1441        assert_eq!(ecs.get::<Position>(entity).unwrap().x, 4.0);
1442    }
1443
1444    #[test]
1445    fn resource_and_query_over_a_group() {
1446        fn tick(mut score: ResMut<Score>, query: Query<(&mut Position, &Velocity)>) {
1447            query.for_each(|_entity, (position, velocity)| position.x += velocity.x);
1448            score.0 += 1;
1449        }
1450
1451        let mut registry = ComponentRegistry::new();
1452        registry.register::<Position>();
1453        registry.register::<Velocity>();
1454        let mut ecs = DynEcs::new();
1455        ecs.add_world(registry);
1456        ecs.insert_resource(Score(0));
1457        let entity = ecs.spawn_with((Position { x: 0.0, y: 0.0 }, Velocity { x: 5.0, y: 0.0 }));
1458
1459        let mut schedule = Schedule::<DynEcs>::new();
1460        schedule.add_system("tick", tick);
1461        schedule.run(&mut ecs);
1462
1463        assert_eq!(ecs.resource::<Score>().unwrap().0, 1);
1464        assert_eq!(ecs.get::<Position>(entity).unwrap().x, 5.0);
1465    }
1466
1467    #[test]
1468    fn added_filter_visits_only_new() {
1469        let mut world = DynWorld::new();
1470        world.set_change_detection(true);
1471        world.spawn((Position { x: 0.0, y: 0.0 },));
1472        world.step();
1473        world.spawn((Position { x: 9.0, y: 9.0 },));
1474
1475        run(
1476            &mut world,
1477            |query: Query<&mut Position, Added<Position>>| {
1478                query.for_each(|_entity, position| position.y = 100.0);
1479            },
1480        );
1481        let marked = world
1482            .query_ref::<&Position>()
1483            .iter()
1484            .filter(|(_entity, position)| position.y == 100.0)
1485            .map(|(_entity, position)| position.x)
1486            .collect::<Vec<_>>();
1487        assert_eq!(marked, vec![9.0]);
1488    }
1489
1490    #[test]
1491    fn tuple_filter_combines_constraints() {
1492        let mut world = DynWorld::new();
1493        world.spawn((Position { x: 0.0, y: 0.0 }, Health { value: 1.0 }));
1494        world.spawn((
1495            Position { x: 0.0, y: 0.0 },
1496            Health { value: 1.0 },
1497            Velocity { x: 0.0, y: 0.0 },
1498        ));
1499
1500        run(
1501            &mut world,
1502            |query: Query<&mut Position, (With<Health>, Without<Velocity>)>| {
1503                query.for_each(|_entity, position| position.x = 1.0);
1504            },
1505        );
1506        let moved = world
1507            .query_ref::<&Position>()
1508            .iter()
1509            .filter(|(_entity, position)| position.x == 1.0)
1510            .count();
1511        assert_eq!(moved, 1);
1512    }
1513
1514    #[test]
1515    fn par_for_each_runs_over_query() {
1516        let mut world = DynWorld::new();
1517        for index in 0..64 {
1518            world.spawn((
1519                Position {
1520                    x: index as f32,
1521                    y: 0.0,
1522                },
1523                Velocity { x: 1.0, y: 0.0 },
1524            ));
1525        }
1526        run(&mut world, |query: Query<(&mut Position, &Velocity)>| {
1527            query.par_for_each(|_entity, (position, velocity)| position.x += velocity.x);
1528        });
1529        let total: f32 = world
1530            .query_ref::<&Position>()
1531            .iter()
1532            .map(|(_entity, position)| position.x)
1533            .sum();
1534        assert_eq!(total, (0..64).map(|index| index as f32 + 1.0).sum());
1535    }
1536
1537    #[test]
1538    #[should_panic(expected = "DeltaTime")]
1539    fn missing_resource_panics() {
1540        let mut world = DynWorld::new();
1541        run(&mut world, |_delta: Res<DeltaTime>| {});
1542    }
1543
1544    fn emit_pair(mut writer: EventWriter<Collision>) {
1545        writer.send(Collision { entity: 1 });
1546        writer.send(Collision { entity: 2 });
1547    }
1548
1549    #[test]
1550    fn writer_flushes_after_run_and_reader_consumes_once() {
1551        let mut world = DynWorld::new();
1552        world.insert_resource(Score(0));
1553
1554        fn tally(reader: EventReader<Collision>, mut score: ResMut<Score>) {
1555            score.0 += reader.len() as u32;
1556        }
1557
1558        let mut schedule = Schedule::new();
1559        schedule.add_system("emit", emit_pair);
1560        schedule.add_system("tally", tally);
1561
1562        schedule.run(&mut world);
1563        assert_eq!(world.resource::<Score>().unwrap().0, 2);
1564
1565        world.step();
1566        schedule.run(&mut world);
1567        assert_eq!(
1568            world.resource::<Score>().unwrap().0,
1569            4,
1570            "the reader consumes only the events sent this frame"
1571        );
1572    }
1573
1574    #[test]
1575    fn reader_coexists_with_a_query() {
1576        let mut world = DynWorld::new();
1577        world.events.send::<Collision>(Collision { entity: 0 });
1578        world.spawn((Position { x: 0.0, y: 0.0 },));
1579
1580        fn shift(reader: EventReader<Collision>, query: Query<&mut Position>) {
1581            let delta = reader.len() as f32;
1582            query.for_each(|_entity, position| position.x += delta);
1583        }
1584
1585        let mut schedule = Schedule::new();
1586        schedule.add_system("shift", shift);
1587        schedule.run(&mut world);
1588
1589        assert_eq!(world.query_ref::<&Position>().single().unwrap().1.x, 1.0);
1590    }
1591
1592    #[test]
1593    fn resource_reader_and_query_in_one_system() {
1594        let mut world = DynWorld::new();
1595        world.insert_resource(DeltaTime(2.0));
1596        world.events.send::<Collision>(Collision { entity: 7 });
1597        world.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 0.0 }));
1598
1599        fn integrate(
1600            delta: Res<DeltaTime>,
1601            reader: EventReader<Collision>,
1602            query: Query<(&mut Position, &Velocity)>,
1603        ) {
1604            let bump = reader.len() as f32;
1605            query.for_each(|_entity, (position, velocity)| {
1606                position.x += velocity.x * delta.0 + bump;
1607            });
1608        }
1609
1610        let mut schedule = Schedule::new();
1611        schedule.add_system("integrate", integrate);
1612        schedule.run(&mut world);
1613
1614        assert_eq!(world.query_ref::<&Position>().single().unwrap().1.x, 3.0);
1615    }
1616
1617    #[test]
1618    fn writer_coexists_with_a_query() {
1619        let mut world = DynWorld::new();
1620        world.spawn((Position { x: 3.0, y: 0.0 },));
1621        world.spawn((Position { x: 5.0, y: 0.0 },));
1622
1623        fn report(mut writer: EventWriter<Collision>, query: Query<&Position>) {
1624            query.for_each(|_entity, position| {
1625                writer.send(Collision {
1626                    entity: position.x as u32,
1627                });
1628            });
1629        }
1630
1631        let mut schedule = Schedule::new();
1632        schedule.add_system("report", report);
1633        schedule.run(&mut world);
1634
1635        let mut ids: Vec<u32> = world
1636            .events
1637            .read::<Collision>()
1638            .iter()
1639            .map(|collision| collision.entity)
1640            .collect();
1641        ids.sort();
1642        assert_eq!(ids, vec![3, 5]);
1643    }
1644
1645    #[test]
1646    fn two_readers_keep_independent_cursors() {
1647        let mut world = DynWorld::new();
1648        world.insert_resource(Score(0));
1649        world.insert_resource(Tally(0));
1650        world.events.send::<Collision>(Collision { entity: 1 });
1651        world.events.send::<Collision>(Collision { entity: 2 });
1652        world.events.send::<Collision>(Collision { entity: 3 });
1653
1654        fn reader_a(reader: EventReader<Collision>, mut score: ResMut<Score>) {
1655            score.0 += reader.len() as u32;
1656        }
1657        fn reader_b(reader: EventReader<Collision>, mut tally: ResMut<Tally>) {
1658            tally.0 += reader.len() as u32;
1659        }
1660
1661        let mut schedule = Schedule::new();
1662        schedule.add_system("reader_a", reader_a);
1663        schedule.add_system("reader_b", reader_b);
1664        schedule.run(&mut world);
1665
1666        assert_eq!(world.resource::<Score>().unwrap().0, 3);
1667        assert_eq!(world.resource::<Tally>().unwrap().0, 3);
1668    }
1669
1670    #[test]
1671    fn reader_iterates_each_event_once() {
1672        let mut world = DynWorld::new();
1673        world.insert_resource(Seen(Vec::new()));
1674        world.events.send::<Collision>(Collision { entity: 10 });
1675        world.events.send::<Collision>(Collision { entity: 20 });
1676
1677        fn collect(reader: EventReader<Collision>, mut seen: ResMut<Seen>) {
1678            for collision in &reader {
1679                seen.0.push(collision.entity);
1680            }
1681        }
1682
1683        let mut schedule = Schedule::new();
1684        schedule.add_system("collect", collect);
1685        schedule.run(&mut world);
1686
1687        assert_eq!(world.resource::<Seen>().unwrap().0, vec![10, 20]);
1688    }
1689
1690    #[test]
1691    fn reader_is_empty_before_any_send() {
1692        let mut world = DynWorld::new();
1693        world.insert_resource(Score(0));
1694
1695        fn tally(reader: EventReader<Collision>, mut score: ResMut<Score>) {
1696            assert!(reader.is_empty());
1697            score.0 += reader.len() as u32;
1698        }
1699
1700        let mut schedule = Schedule::new();
1701        schedule.add_system("tally", tally);
1702        schedule.run(&mut world);
1703
1704        assert_eq!(world.resource::<Score>().unwrap().0, 0);
1705    }
1706
1707    #[test]
1708    fn events_flow_over_a_group() {
1709        let mut ecs = DynEcs::new();
1710        ecs.insert_resource(Score(0));
1711
1712        fn tally(reader: EventReader<Collision>, mut score: ResMut<Score>) {
1713            score.0 += reader.len() as u32;
1714        }
1715
1716        let mut schedule = Schedule::<DynEcs>::new();
1717        schedule.add_system("emit", emit_pair);
1718        schedule.add_system("tally", tally);
1719        schedule.run(&mut ecs);
1720
1721        assert_eq!(ecs.resource::<Score>().unwrap().0, 2);
1722    }
1723
1724    #[test]
1725    fn add_system_if_gates_a_param_system() {
1726        let mut world = DynWorld::new();
1727        world.insert_resource(Score(0));
1728        world.insert_resource(Enabled(false));
1729
1730        fn bump(mut score: ResMut<Score>) {
1731            score.0 += 1;
1732        }
1733
1734        let mut schedule = Schedule::new();
1735        schedule.add_system_if(
1736            "bump",
1737            |world: &DynWorld| world.resource::<Enabled>().unwrap().0,
1738            bump,
1739        );
1740
1741        schedule.run(&mut world);
1742        assert_eq!(
1743            world.resource::<Score>().unwrap().0,
1744            0,
1745            "the system is skipped while the condition is false"
1746        );
1747
1748        world.resource_mut::<Enabled>().unwrap().0 = true;
1749        schedule.run(&mut world);
1750        assert_eq!(
1751            world.resource::<Score>().unwrap().0,
1752            1,
1753            "the system runs once the condition holds"
1754        );
1755    }
1756}