Skip to main content

nexus_rt/
system.rs

1//! System parameter resolution and dispatch primitives.
2
3use std::ops::{Deref, DerefMut};
4
5use crate::callback::Callback;
6use crate::resource::{Res, ResMut};
7use crate::world::{Registry, ResourceId, World};
8
9// =============================================================================
10// SystemParam
11// =============================================================================
12
13/// Trait for types that can be resolved from a [`Registry`] at build time
14/// and fetched from [`World`] at dispatch time.
15///
16/// Build time: [`init`](Self::init) resolves opaque state (e.g. a
17/// [`ResourceId`]) from the registry. This panics if the required type
18/// isn't registered — giving an early build-time error.
19///
20/// Dispatch time: [`fetch`](Self::fetch) uses the cached state to produce
21/// a reference in ~3 cycles.
22pub trait SystemParam {
23    /// Opaque state cached at build time (e.g. [`ResourceId`]).
24    type State;
25
26    /// The item produced at dispatch time.
27    type Item<'w>;
28
29    /// Resolve state from the registry. Called once at build time.
30    ///
31    /// # Panics
32    ///
33    /// Panics if the required resource is not registered.
34    fn init(registry: &Registry) -> Self::State;
35
36    /// Fetch the item using cached state.
37    ///
38    /// # Safety
39    ///
40    /// - `state` must have been produced by [`init`](Self::init) on the
41    ///   same registry that built the `world`.
42    /// - Caller ensures no aliasing violations.
43    unsafe fn fetch<'w>(world: &'w World, state: &'w mut Self::State) -> Self::Item<'w>;
44
45    /// Returns `true` if any resource this param depends on was modified
46    /// during the current sequence.
47    ///
48    /// Used by drivers to skip handlers whose inputs haven't changed.
49    fn any_changed(state: &Self::State, world: &World) -> bool;
50
51    /// The ResourceId this param accesses, if any.
52    ///
53    /// Returns `None` for params that don't access World resources
54    /// (e.g. `Local<T>`). Used by [`Registry::check_access`] to enforce
55    /// one borrow per resource per handler.
56    fn resource_id(state: &Self::State) -> Option<ResourceId> {
57        let _ = state;
58        None
59    }
60}
61
62// -- Res<T> ------------------------------------------------------------------
63
64impl<T: 'static> SystemParam for Res<'_, T> {
65    type State = ResourceId;
66    type Item<'w> = Res<'w, T>;
67
68    fn init(registry: &Registry) -> ResourceId {
69        registry.id::<T>()
70    }
71
72    #[inline(always)]
73    unsafe fn fetch<'w>(world: &'w World, state: &'w mut ResourceId) -> Res<'w, T> {
74        let id = *state;
75        // SAFETY: state was produced by init() on the same world.
76        // Caller ensures no mutable alias exists for T.
77        unsafe {
78            Res::new(
79                world.get::<T>(id),
80                world.changed_at(id),
81                world.current_sequence(),
82            )
83        }
84    }
85
86    fn any_changed(state: &ResourceId, world: &World) -> bool {
87        // SAFETY: state was produced by init() on the same registry.
88        unsafe { world.changed_at(*state) == world.current_sequence() }
89    }
90
91    fn resource_id(state: &ResourceId) -> Option<ResourceId> {
92        Some(*state)
93    }
94}
95
96// -- ResMut<T> ---------------------------------------------------------------
97
98impl<T: 'static> SystemParam for ResMut<'_, T> {
99    type State = ResourceId;
100    type Item<'w> = ResMut<'w, T>;
101
102    fn init(registry: &Registry) -> ResourceId {
103        registry.id::<T>()
104    }
105
106    #[inline(always)]
107    unsafe fn fetch<'w>(world: &'w World, state: &'w mut ResourceId) -> ResMut<'w, T> {
108        let id = *state;
109        // SAFETY: state was produced by init() on the same world.
110        // Caller ensures no aliases exist for T.
111        unsafe {
112            ResMut::new(
113                world.get_mut::<T>(id),
114                world.changed_at_cell(id),
115                world.current_sequence(),
116            )
117        }
118    }
119
120    fn any_changed(state: &ResourceId, world: &World) -> bool {
121        // SAFETY: state was produced by init() on the same registry.
122        unsafe { world.changed_at(*state) == world.current_sequence() }
123    }
124
125    fn resource_id(state: &ResourceId) -> Option<ResourceId> {
126        Some(*state)
127    }
128}
129
130// -- Option<Res<T>> ----------------------------------------------------------
131
132impl<T: 'static> SystemParam for Option<Res<'_, T>> {
133    type State = Option<ResourceId>;
134    type Item<'w> = Option<Res<'w, T>>;
135
136    fn init(registry: &Registry) -> Option<ResourceId> {
137        registry.try_id::<T>()
138    }
139
140    #[inline(always)]
141    unsafe fn fetch<'w>(world: &'w World, state: &'w mut Option<ResourceId>) -> Option<Res<'w, T>> {
142        // SAFETY: state was produced by init() on the same world.
143        // Caller ensures no mutable alias exists for T.
144        state.map(|id| unsafe {
145            Res::new(
146                world.get::<T>(id),
147                world.changed_at(id),
148                world.current_sequence(),
149            )
150        })
151    }
152
153    fn any_changed(state: &Option<ResourceId>, world: &World) -> bool {
154        state.is_some_and(|id| {
155            // SAFETY: id was produced by init() on the same registry.
156            unsafe { world.changed_at(id) == world.current_sequence() }
157        })
158    }
159
160    fn resource_id(state: &Option<ResourceId>) -> Option<ResourceId> {
161        *state
162    }
163}
164
165// -- Option<ResMut<T>> -------------------------------------------------------
166
167impl<T: 'static> SystemParam for Option<ResMut<'_, T>> {
168    type State = Option<ResourceId>;
169    type Item<'w> = Option<ResMut<'w, T>>;
170
171    fn init(registry: &Registry) -> Option<ResourceId> {
172        registry.try_id::<T>()
173    }
174
175    #[inline(always)]
176    unsafe fn fetch<'w>(
177        world: &'w World,
178        state: &'w mut Option<ResourceId>,
179    ) -> Option<ResMut<'w, T>> {
180        // SAFETY: state was produced by init() on the same world.
181        // Caller ensures no aliases exist for T.
182        state.map(|id| unsafe {
183            ResMut::new(
184                world.get_mut::<T>(id),
185                world.changed_at_cell(id),
186                world.current_sequence(),
187            )
188        })
189    }
190
191    fn any_changed(state: &Option<ResourceId>, world: &World) -> bool {
192        state.is_some_and(|id| {
193            // SAFETY: id was produced by init() on the same registry.
194            unsafe { world.changed_at(id) == world.current_sequence() }
195        })
196    }
197
198    fn resource_id(state: &Option<ResourceId>) -> Option<ResourceId> {
199        *state
200    }
201}
202
203// =============================================================================
204// Tuple impls
205// =============================================================================
206
207/// Unit impl — event-only handlers with no resource parameters.
208impl SystemParam for () {
209    type State = ();
210    type Item<'w> = ();
211
212    fn init(_registry: &Registry) {}
213
214    #[inline(always)]
215    unsafe fn fetch<'w>(_world: &'w World, _state: &'w mut ()) {}
216
217    fn any_changed(_state: &(), _world: &World) -> bool {
218        false
219    }
220}
221
222macro_rules! impl_system_param_tuple {
223    ($($P:ident),+) => {
224        impl<$($P: SystemParam),+> SystemParam for ($($P,)+) {
225            type State = ($($P::State,)+);
226            type Item<'w> = ($($P::Item<'w>,)+);
227
228            fn init(registry: &Registry) -> Self::State {
229                ($($P::init(registry),)+)
230            }
231
232            #[inline(always)]
233            #[allow(non_snake_case)]
234            unsafe fn fetch<'w>(world: &'w World, state: &'w mut Self::State) -> Self::Item<'w> {
235                let ($($P,)+) = state;
236                // SAFETY: caller upholds aliasing invariants for all params.
237                unsafe { ($($P::fetch(world, $P),)+) }
238            }
239
240            #[allow(non_snake_case)]
241            fn any_changed(state: &Self::State, world: &World) -> bool {
242                let ($($P,)+) = state;
243                $($P::any_changed($P, world))||+
244            }
245        }
246    };
247}
248
249macro_rules! all_tuples {
250    ($m:ident) => {
251        $m!(P0);
252        $m!(P0, P1);
253        $m!(P0, P1, P2);
254        $m!(P0, P1, P2, P3);
255        $m!(P0, P1, P2, P3, P4);
256        $m!(P0, P1, P2, P3, P4, P5);
257        $m!(P0, P1, P2, P3, P4, P5, P6);
258        $m!(P0, P1, P2, P3, P4, P5, P6, P7);
259    };
260}
261
262all_tuples!(impl_system_param_tuple);
263
264// =============================================================================
265// Local<T> — per-handler state
266// =============================================================================
267
268/// Per-handler local state. Stored inside the dispatch wrapper (e.g.
269/// [`Callback`] or pipeline [`Stage`](crate::Stage)), not in [`World`].
270///
271/// Initialized with [`Default::default()`] at handler creation time. Mutated
272/// freely at dispatch time — each handler/stage instance has its own
273/// independent copy.
274///
275/// # Examples
276///
277/// ```ignore
278/// fn count_events(mut count: Local<u64>, event: u32) {
279///     *count += 1;
280///     println!("event #{}: {}", *count, event);
281/// }
282/// ```
283pub struct Local<'s, T: Default + 'static> {
284    value: &'s mut T,
285}
286
287impl<'s, T: Default + 'static> Local<'s, T> {
288    pub(crate) fn new(value: &'s mut T) -> Self {
289        Self { value }
290    }
291}
292
293impl<T: Default + 'static> Deref for Local<'_, T> {
294    type Target = T;
295
296    #[inline(always)]
297    fn deref(&self) -> &T {
298        self.value
299    }
300}
301
302impl<T: Default + 'static> DerefMut for Local<'_, T> {
303    #[inline(always)]
304    fn deref_mut(&mut self) -> &mut T {
305        self.value
306    }
307}
308
309impl<T: Default + 'static> SystemParam for Local<'_, T> {
310    type State = T;
311    type Item<'s> = Local<'s, T>;
312
313    fn init(_registry: &Registry) -> T {
314        T::default()
315    }
316
317    #[inline(always)]
318    unsafe fn fetch<'s>(_world: &'s World, state: &'s mut T) -> Local<'s, T> {
319        // SAFETY: The dispatch wrapper (Callback or Stage) owns state
320        // exclusively. Single-threaded dispatch ensures no aliasing.
321        // Lifetime 's is bounded by the handler/stage's run() call.
322        Local::new(state)
323    }
324
325    fn any_changed(_state: &T, _world: &World) -> bool {
326        // Local state is per-handler, not a World resource — never "changed."
327        false
328    }
329}
330
331// =============================================================================
332// Handler<E> — object-safe dispatch trait
333// =============================================================================
334
335/// Object-safe dispatch trait for event handlers.
336///
337/// Enables `Box<dyn Handler<E>>` for type-erased heterogeneous dispatch.
338/// Storage and scheduling are the driver's responsibility — this trait
339/// only defines the dispatch interface.
340///
341/// Takes `&mut World` — drivers call this directly in their poll loop.
342pub trait Handler<E> {
343    /// Run this handler with the given event.
344    fn run(&mut self, world: &mut World, event: E);
345
346    /// Returns `true` if any input resource was modified this sequence.
347    ///
348    /// Default returns `true` (always run). [`Callback`] overrides
349    /// by checking its state via [`SystemParam::any_changed`].
350    fn inputs_changed(&self, world: &World) -> bool {
351        let _ = world;
352        true
353    }
354
355    /// Returns the handler's name.
356    ///
357    /// Default returns `"<unnamed>"`. [`Callback`] captures the
358    /// function's [`type_name`](std::any::type_name) at construction time.
359    fn name(&self) -> &'static str {
360        "<unnamed>"
361    }
362}
363
364// =============================================================================
365// CtxFree<F> — coherence wrapper for context-free handlers
366// =============================================================================
367
368/// Wrapper that marks a function as context-free.
369///
370/// Prevents coherence overlap between the context-owning and context-free
371/// [`Handler`] impls on [`Callback`]. `CtxFree<F>` is a plain struct and
372/// can never satisfy `FnMut` bounds, so the compiler proves the two impls
373/// are disjoint.
374///
375/// Users don't construct this directly — [`IntoHandler`] wraps the
376/// function automatically.
377#[doc(hidden)]
378pub struct CtxFree<F>(pub(crate) F);
379
380/// Type alias for context-free handlers (no owned context).
381///
382/// This is `Callback<(), CtxFree<F>, Params>` — the `ctx: ()` field
383/// is a ZST (zero bytes), identical codegen to the old `FunctionSystem`.
384pub type HandlerFn<F, Params> = Callback<(), CtxFree<F>, Params>;
385
386// =============================================================================
387// IntoHandler — conversion trait
388// =============================================================================
389
390/// Converts a plain function into a [`Handler`].
391///
392/// Event `E` is always the last function parameter. Everything before
393/// it is resolved as [`SystemParam`] from a [`Registry`].
394///
395/// # Named functions only
396///
397/// Closures do not work with `IntoHandler` due to Rust's HRTB inference
398/// limitations with GATs. Use named `fn` items instead. This is the same
399/// limitation as Bevy's system registration.
400///
401/// # Examples
402///
403/// ```
404/// use nexus_rt::{Res, ResMut, IntoHandler, WorldBuilder};
405///
406/// fn tick(counter: Res<u64>, mut flag: ResMut<bool>, event: u32) {
407///     if event > 0 {
408///         *flag = true;
409///     }
410/// }
411///
412/// let mut builder = WorldBuilder::new();
413/// builder.register::<u64>(0);
414/// builder.register::<bool>(false);
415///
416/// let mut handler = tick.into_handler(builder.registry_mut());
417/// ```
418pub trait IntoHandler<E, Params> {
419    /// The concrete handler type produced.
420    type Handler: Handler<E> + 'static;
421
422    /// Convert this function into a handler, resolving parameters from the registry.
423    fn into_handler(self, registry: &mut Registry) -> Self::Handler;
424}
425
426// =============================================================================
427// Per-arity impls via macro — context-free path (Callback<(), CtxFree<F>, P>)
428// =============================================================================
429
430// Arity 0: fn(E) — event-only handler, no resource params.
431impl<E, F: FnMut(E) + 'static> IntoHandler<E, ()> for F {
432    type Handler = Callback<(), CtxFree<F>, ()>;
433
434    fn into_handler(self, registry: &mut Registry) -> Self::Handler {
435        Callback {
436            ctx: (),
437            f: CtxFree(self),
438            state: <() as SystemParam>::init(registry),
439            name: std::any::type_name::<F>(),
440        }
441    }
442}
443
444impl<E, F: FnMut(E) + 'static> Handler<E> for Callback<(), CtxFree<F>, ()> {
445    fn run(&mut self, _world: &mut World, event: E) {
446        (self.f.0)(event);
447    }
448
449    fn inputs_changed(&self, _world: &World) -> bool {
450        // Event-only handler — no resource dependencies to check.
451        // Always returns true so drivers never skip it.
452        true
453    }
454
455    fn name(&self) -> &'static str {
456        self.name
457    }
458}
459
460macro_rules! impl_into_handler {
461    ($($P:ident),+) => {
462        impl<E, F: 'static, $($P: SystemParam + 'static),+> IntoHandler<E, ($($P,)+)> for F
463        where
464            // Double-bound pattern (from Bevy):
465            // - First bound: compiler uses P directly to infer SystemParam
466            //   types from the function signature (GATs aren't injective,
467            //   so P::Item<'w> alone can't determine P).
468            // - Second bound: verifies the function is callable with the
469            //   fetched items at any lifetime.
470            for<'a> &'a mut F: FnMut($($P,)+ E) + FnMut($($P::Item<'a>,)+ E),
471        {
472            type Handler = Callback<(), CtxFree<F>, ($($P,)+)>;
473
474            fn into_handler(self, registry: &mut Registry) -> Self::Handler {
475                let state = <($($P,)+) as SystemParam>::init(registry);
476                {
477                    #[allow(non_snake_case)]
478                    let ($($P,)+) = &state;
479                    registry.check_access(&[
480                        $(
481                            (<$P as SystemParam>::resource_id($P),
482                             std::any::type_name::<$P>()),
483                        )+
484                    ]);
485                }
486                Callback {
487                    ctx: (),
488                    f: CtxFree(self),
489                    state,
490                    name: std::any::type_name::<F>(),
491                }
492            }
493        }
494
495        impl<E, F: 'static, $($P: SystemParam + 'static),+> Handler<E>
496            for Callback<(), CtxFree<F>, ($($P,)+)>
497        where
498            for<'a> &'a mut F: FnMut($($P,)+ E) + FnMut($($P::Item<'a>,)+ E),
499        {
500            #[allow(non_snake_case)]
501            fn run(&mut self, world: &mut World, event: E) {
502                // Helper binds the HRTB lifetime at a concrete call site.
503                #[allow(clippy::too_many_arguments)]
504                fn call_inner<$($P,)+ Ev>(
505                    mut f: impl FnMut($($P,)+ Ev),
506                    $($P: $P,)+
507                    event: Ev,
508                ) {
509                    f($($P,)+ event);
510                }
511
512                // SAFETY: state was produced by init() on the same registry
513                // that built this world. Single-threaded sequential dispatch
514                // ensures no mutable aliasing across params.
515                let ($($P,)+) = unsafe {
516                    <($($P,)+) as SystemParam>::fetch(world, &mut self.state)
517                };
518                call_inner(&mut self.f.0, $($P,)+ event);
519            }
520
521            fn inputs_changed(&self, world: &World) -> bool {
522                <($($P,)+) as SystemParam>::any_changed(&self.state, world)
523            }
524
525            fn name(&self) -> &'static str {
526                self.name
527            }
528        }
529    };
530}
531
532all_tuples!(impl_into_handler);
533
534// =============================================================================
535// Tests
536// =============================================================================
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::WorldBuilder;
542
543    // -- SystemParam tests ----------------------------------------------------
544
545    #[test]
546    fn res_system_param() {
547        let mut builder = WorldBuilder::new();
548        builder.register::<u64>(42);
549        let mut world = builder.build();
550
551        let mut state = <Res<u64> as SystemParam>::init(world.registry_mut());
552        // SAFETY: state from init on same registry, no aliasing.
553        let res = unsafe { <Res<u64> as SystemParam>::fetch(&world, &mut state) };
554        assert_eq!(*res, 42);
555    }
556
557    #[test]
558    fn res_mut_system_param() {
559        let mut builder = WorldBuilder::new();
560        builder.register::<u64>(1);
561        let mut world = builder.build();
562
563        let mut state = <ResMut<u64> as SystemParam>::init(world.registry_mut());
564        // SAFETY: state from init on same registry, no aliasing.
565        unsafe {
566            let mut res = <ResMut<u64> as SystemParam>::fetch(&world, &mut state);
567            *res = 99;
568        }
569        unsafe {
570            let mut read_state = <Res<u64> as SystemParam>::init(world.registry_mut());
571            let res = <Res<u64> as SystemParam>::fetch(&world, &mut read_state);
572            assert_eq!(*res, 99);
573        }
574    }
575
576    #[test]
577    fn tuple_system_param() {
578        let mut builder = WorldBuilder::new();
579        builder.register::<u64>(10).register::<bool>(true);
580        let mut world = builder.build();
581
582        let mut state = <(Res<u64>, ResMut<bool>) as SystemParam>::init(world.registry_mut());
583        // SAFETY: different types, no aliasing.
584        unsafe {
585            let (counter, mut flag) =
586                <(Res<u64>, ResMut<bool>) as SystemParam>::fetch(&world, &mut state);
587            assert_eq!(*counter, 10);
588            assert!(*flag);
589            *flag = false;
590        }
591        unsafe {
592            let mut read_state = <Res<bool> as SystemParam>::init(world.registry_mut());
593            let res = <Res<bool> as SystemParam>::fetch(&world, &mut read_state);
594            assert!(!*res);
595        }
596    }
597
598    #[test]
599    fn empty_tuple_param() {
600        let mut world = WorldBuilder::new().build();
601        let mut state = <() as SystemParam>::init(world.registry_mut());
602        // SAFETY: no params to alias.
603        let () = unsafe { <() as SystemParam>::fetch(&world, &mut state) };
604    }
605
606    // -- Handler dispatch tests -----------------------------------------------
607
608    fn event_only_handler(event: u32) {
609        assert_eq!(event, 42);
610    }
611
612    #[test]
613    fn event_only_system() {
614        let mut world = WorldBuilder::new().build();
615        let mut sys = event_only_handler.into_handler(world.registry_mut());
616        sys.run(&mut world, 42u32);
617    }
618
619    fn one_res_handler(counter: Res<u64>, event: u32) {
620        assert_eq!(*counter, 10);
621        assert_eq!(event, 5);
622    }
623
624    #[test]
625    fn one_res_and_event() {
626        let mut builder = WorldBuilder::new();
627        builder.register::<u64>(10);
628        let mut world = builder.build();
629
630        let mut sys = one_res_handler.into_handler(world.registry_mut());
631        sys.run(&mut world, 5u32);
632    }
633
634    fn two_res_handler(counter: Res<u64>, flag: Res<bool>, event: u32) {
635        assert_eq!(*counter, 10);
636        assert!(*flag);
637        assert_eq!(event, 7);
638    }
639
640    #[test]
641    fn two_res_and_event() {
642        let mut builder = WorldBuilder::new();
643        builder.register::<u64>(10).register::<bool>(true);
644        let mut world = builder.build();
645
646        let mut sys = two_res_handler.into_handler(world.registry_mut());
647        sys.run(&mut world, 7u32);
648    }
649
650    fn accumulate(mut counter: ResMut<u64>, event: u64) {
651        *counter += event;
652    }
653
654    #[test]
655    fn mutation_through_res_mut() {
656        let mut builder = WorldBuilder::new();
657        builder.register::<u64>(0);
658        let mut world = builder.build();
659
660        let mut sys = accumulate.into_handler(world.registry_mut());
661
662        sys.run(&mut world, 10u64);
663        sys.run(&mut world, 5u64);
664
665        assert_eq!(*world.resource::<u64>(), 15);
666    }
667
668    fn add_handler(mut counter: ResMut<u64>, event: u64) {
669        *counter += event;
670    }
671
672    fn mul_handler(mut counter: ResMut<u64>, event: u64) {
673        *counter *= event;
674    }
675
676    #[test]
677    fn box_dyn_type_erasure() {
678        let mut builder = WorldBuilder::new();
679        builder.register::<u64>(0);
680        let mut world = builder.build();
681
682        let sys_a = add_handler.into_handler(world.registry_mut());
683        let sys_b = mul_handler.into_handler(world.registry_mut());
684
685        let mut handlers: Vec<Box<dyn Handler<u64>>> = vec![Box::new(sys_a), Box::new(sys_b)];
686
687        for h in handlers.iter_mut() {
688            h.run(&mut world, 3u64);
689        }
690        // 0 + 3 = 3, then 3 * 3 = 9
691        assert_eq!(*world.resource::<u64>(), 9);
692    }
693
694    // -- Local<T> tests -------------------------------------------------------
695
696    fn local_counter(mut count: Local<u64>, _event: u32) {
697        *count += 1;
698    }
699
700    #[test]
701    fn local_default_init() {
702        let mut world = WorldBuilder::new().build();
703        let mut sys = local_counter.into_handler(world.registry_mut());
704        // Ran once — count should be 1 (started at 0). No panic means init worked.
705        sys.run(&mut world, 1u32);
706    }
707
708    #[test]
709    fn local_persists_across_runs() {
710        let mut builder = WorldBuilder::new();
711        builder.register::<u64>(0);
712        let mut world = builder.build();
713
714        fn accumulate_local(mut count: Local<u64>, mut total: ResMut<u64>, _event: u32) {
715            *count += 1;
716            *total = *count;
717        }
718
719        let mut sys = accumulate_local.into_handler(world.registry_mut());
720        sys.run(&mut world, 0u32);
721        sys.run(&mut world, 0u32);
722        sys.run(&mut world, 0u32);
723
724        assert_eq!(*world.resource::<u64>(), 3);
725    }
726
727    #[test]
728    fn local_independent_per_system() {
729        let mut builder = WorldBuilder::new();
730        builder.register::<u64>(0);
731        let mut world = builder.build();
732
733        fn inc_local(mut count: Local<u64>, mut total: ResMut<u64>, _event: u32) {
734            *count += 1;
735            *total += *count;
736        }
737
738        let mut sys_a = inc_local.into_handler(world.registry_mut());
739        let mut sys_b = inc_local.into_handler(world.registry_mut());
740
741        sys_a.run(&mut world, 0u32); // local=1, total=0+1=1
742        sys_b.run(&mut world, 0u32); // local=1, total=1+1=2
743        sys_a.run(&mut world, 0u32); // local=2, total=2+2=4
744
745        assert_eq!(*world.resource::<u64>(), 4);
746    }
747
748    // -- Option<Res<T>> / Option<ResMut<T>> tests -----------------------------
749
750    #[test]
751    fn option_res_none_when_missing() {
752        let mut world = WorldBuilder::new().build();
753        let mut state = <Option<Res<u64>> as SystemParam>::init(world.registry_mut());
754        let opt = unsafe { <Option<Res<u64>> as SystemParam>::fetch(&world, &mut state) };
755        assert!(opt.is_none());
756    }
757
758    #[test]
759    fn option_res_some_when_present() {
760        let mut builder = WorldBuilder::new();
761        builder.register::<u64>(42);
762        let mut world = builder.build();
763
764        let mut state = <Option<Res<u64>> as SystemParam>::init(world.registry_mut());
765        let opt = unsafe { <Option<Res<u64>> as SystemParam>::fetch(&world, &mut state) };
766        assert_eq!(*opt.unwrap(), 42);
767    }
768
769    #[test]
770    fn option_res_mut_some_when_present() {
771        let mut builder = WorldBuilder::new();
772        builder.register::<u64>(1);
773        let mut world = builder.build();
774
775        let mut state = <Option<ResMut<u64>> as SystemParam>::init(world.registry_mut());
776        unsafe {
777            let opt = <Option<ResMut<u64>> as SystemParam>::fetch(&world, &mut state);
778            *opt.unwrap() = 99;
779        }
780        unsafe {
781            let mut read_state = <Res<u64> as SystemParam>::init(world.registry_mut());
782            let res = <Res<u64> as SystemParam>::fetch(&world, &mut read_state);
783            assert_eq!(*res, 99);
784        }
785    }
786
787    fn optional_handler(opt: Option<Res<String>>, _event: u32) {
788        assert!(opt.is_none());
789    }
790
791    #[test]
792    fn option_in_handler() {
793        let mut world = WorldBuilder::new().build();
794        let mut sys = optional_handler.into_handler(world.registry_mut());
795        sys.run(&mut world, 0u32);
796    }
797
798    // -- Change detection tests -----------------------------------------------
799
800    #[test]
801    fn inputs_changed_true_when_resource_stamped() {
802        let mut builder = WorldBuilder::new();
803        builder.register::<u64>(0);
804        let mut world = builder.build();
805
806        fn reader(val: Res<u64>, _e: ()) {
807            let _ = *val;
808        }
809
810        let sys = reader.into_handler(world.registry_mut());
811
812        // Tick 0: changed_at=0, current_sequence=0 → inputs changed
813        assert!(sys.inputs_changed(&world));
814
815        world.next_sequence(); // tick=1
816
817        // Tick 1: changed_at=0, current_sequence=1 → not changed
818        assert!(!sys.inputs_changed(&world));
819
820        // Stamp u64 at tick=1
821        *world.resource_mut::<u64>() = 42;
822        assert!(sys.inputs_changed(&world));
823    }
824
825    #[test]
826    fn inputs_changed_false_when_not_stamped() {
827        let mut builder = WorldBuilder::new();
828        builder.register::<u64>(0).register::<bool>(false);
829        let mut world = builder.build();
830
831        fn reads_bool(flag: Res<bool>, _e: ()) {
832            let _ = *flag;
833        }
834
835        let sys = reads_bool.into_handler(world.registry_mut());
836
837        world.next_sequence(); // tick=1
838
839        // Only stamp u64, not bool
840        *world.resource_mut::<u64>() = 42;
841
842        // Handler reads bool, which wasn't stamped → not changed
843        assert!(!sys.inputs_changed(&world));
844    }
845
846    #[test]
847    fn inputs_changed_with_optional_none() {
848        // Optional param with no resource registered → always false
849        let mut world = WorldBuilder::new().build();
850
851        fn optional_sys(opt: Option<Res<String>>, _e: ()) {
852            let _ = opt;
853        }
854
855        let sys = optional_sys.into_handler(world.registry_mut());
856        assert!(!sys.inputs_changed(&world));
857    }
858
859    #[test]
860    fn inputs_changed_event_only_handler() {
861        let mut world = WorldBuilder::new().build();
862
863        fn event_handler(_e: u32) {}
864
865        let sys = event_handler.into_handler(world.registry_mut());
866        // Event-only handlers always run — drivers must not skip them.
867        assert!(sys.inputs_changed(&world));
868    }
869
870    // -- Access conflict detection ----------------------------------------
871
872    #[test]
873    #[should_panic(expected = "conflicting access")]
874    fn duplicate_res_panics() {
875        let mut builder = WorldBuilder::new();
876        builder.register::<u64>(0);
877        let mut world = builder.build();
878
879        fn bad(a: Res<u64>, b: Res<u64>, _e: ()) {
880            let _ = (*a, *b);
881        }
882
883        let _sys = bad.into_handler(world.registry_mut());
884    }
885
886    #[test]
887    #[should_panic(expected = "conflicting access")]
888    fn duplicate_res_mut_panics() {
889        let mut builder = WorldBuilder::new();
890        builder.register::<u64>(0);
891        let mut world = builder.build();
892
893        fn bad(a: ResMut<u64>, b: ResMut<u64>, _e: ()) {
894            let _ = (&*a, &*b);
895        }
896
897        let _sys = bad.into_handler(world.registry_mut());
898    }
899
900    #[test]
901    #[should_panic(expected = "conflicting access")]
902    fn duplicate_mixed_panics() {
903        let mut builder = WorldBuilder::new();
904        builder.register::<u64>(0);
905        let mut world = builder.build();
906
907        fn bad(a: Res<u64>, b: ResMut<u64>, _e: ()) {
908            let _ = (*a, &*b);
909        }
910
911        let _sys = bad.into_handler(world.registry_mut());
912    }
913
914    #[test]
915    fn different_types_no_conflict() {
916        let mut builder = WorldBuilder::new();
917        builder.register::<u64>(0);
918        builder.register::<u32>(0);
919        let mut world = builder.build();
920
921        fn ok(a: Res<u64>, b: ResMut<u32>, _e: ()) {
922            let _ = (*a, &*b);
923        }
924
925        let _sys = ok.into_handler(world.registry_mut());
926    }
927
928    #[test]
929    fn local_no_conflict() {
930        let mut builder = WorldBuilder::new();
931        builder.register::<u64>(0);
932        let mut world = builder.build();
933
934        fn ok(local: Local<u64>, val: ResMut<u64>, _e: ()) {
935            let _ = (&*local, &*val);
936        }
937
938        let _sys = ok.into_handler(world.registry_mut());
939    }
940
941    #[test]
942    fn end_to_end_change_detection() {
943        let mut builder = WorldBuilder::new();
944        builder.register::<u64>(0).register::<bool>(false);
945        let mut world = builder.build();
946
947        // Tick 0: all resources changed_at=0, current_sequence=0 → changed.
948        fn writer(mut val: ResMut<u64>, _e: ()) {
949            *val = 42;
950        }
951        fn observer(val: Res<u64>, flag: Res<bool>, _e: ()) {
952            // On tick 0: both are "changed" (changed_at == current_sequence == 0).
953            // After next_sequence: only u64 should be changed (writer stamps it).
954            let _ = (*val, *flag);
955        }
956
957        let mut writer_sys = writer.into_handler(world.registry_mut());
958        let mut observer_sys = observer.into_handler(world.registry_mut());
959
960        // Tick 0: everything is "changed"
961        writer_sys.run(&mut world, ());
962        observer_sys.run(&mut world, ());
963
964        world.next_sequence(); // tick=1
965
966        // Tick 1: writer runs → stamps u64 to tick=1.
967        // bool was not written → still at tick=0.
968        writer_sys.run(&mut world, ());
969
970        let u64_id = world.id::<u64>();
971        let bool_id = world.id::<bool>();
972        unsafe {
973            assert_eq!(world.changed_at(u64_id), world.current_sequence());
974            assert_ne!(world.changed_at(bool_id), world.current_sequence());
975        }
976    }
977}