Skip to main content

galeon_engine/
function_system.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use std::marker::PhantomData;
4
5use crate::system_param::{Access, SystemParam};
6use crate::world::{UnsafeWorldCell, World};
7
8// =============================================================================
9// System trait — trait-object interface for all system types
10// =============================================================================
11
12/// Trait-object interface for all system types.
13pub trait System {
14    /// Human-readable system name (for diagnostics and conflict messages).
15    fn name(&self) -> &'static str;
16
17    /// Run the system against the world.
18    fn run(&mut self, world: &mut World);
19
20    /// Declare what world data this system accesses.
21    ///
22    /// Returns the union of all parameter accesses for parameterized systems.
23    fn access(&self) -> Vec<Access>;
24}
25
26// =============================================================================
27// IntoSystem — converts a compatible function into a boxed System
28// =============================================================================
29
30/// Converts a compatible function into a boxed [`System`].
31///
32/// Implemented for parameterized functions `fn(P0, P1, ...)` where each `P`
33/// is a [`SystemParam`].
34pub trait IntoSystem<Params> {
35    fn into_system(self, name: &'static str) -> Box<dyn System>;
36}
37
38// =============================================================================
39// Parameterized system — fn(P0, P1, ...) where each P: SystemParam
40// =============================================================================
41
42/// Marker trait bridging an `FnMut(P::Item<'_>, ...)` to `System::run`.
43pub(crate) trait SystemParamFunction<Params>: 'static {
44    fn run(&mut self, world: &mut World);
45    fn param_access() -> Vec<Access>;
46}
47
48struct ParamSystem<F, Params> {
49    name: &'static str,
50    func: F,
51    _marker: PhantomData<fn() -> Params>,
52}
53
54impl<F, Params> System for ParamSystem<F, Params>
55where
56    F: SystemParamFunction<Params>,
57{
58    fn name(&self) -> &'static str {
59        self.name
60    }
61
62    fn run(&mut self, world: &mut World) {
63        self.func.run(world);
64    }
65
66    fn access(&self) -> Vec<Access> {
67        F::param_access()
68    }
69}
70
71/// Panics if any two accesses within the same system conflict.
72fn validate_no_self_conflicts(access: &[Access], system_name: &'static str) {
73    for (i, a) in access.iter().enumerate() {
74        for b in &access[i + 1..] {
75            if a.conflicts_with(b) {
76                panic!(
77                    "system '{}' has conflicting parameter access: {:?} vs {:?}",
78                    system_name, a, b,
79                );
80            }
81        }
82    }
83}
84
85impl<Func> SystemParamFunction<()> for Func
86where
87    Func: FnMut() + 'static,
88{
89    fn run(&mut self, _world: &mut World) {
90        self();
91    }
92
93    fn param_access() -> Vec<Access> {
94        Vec::new()
95    }
96}
97
98impl<Func> IntoSystem<()> for Func
99where
100    Func: SystemParamFunction<()>,
101{
102    fn into_system(self, name: &'static str) -> Box<dyn System> {
103        Box::new(ParamSystem {
104            name,
105            func: self,
106            _marker: PhantomData,
107        })
108    }
109}
110
111// =============================================================================
112// Arity macros — 1..8 parameter systems
113// =============================================================================
114
115macro_rules! impl_system_param_function {
116    ($($P:ident),+) => {
117        #[allow(non_snake_case)]
118        impl<Func, $($P: SystemParam + 'static),+> SystemParamFunction<($($P,)+)> for Func
119        where
120            Func: FnMut($($P::Item<'_>),+) + 'static,
121        {
122            fn run(&mut self, world: &mut World) {
123                // SAFETY: Conflict detection at registration ensures no two
124                // params access the same TypeId mutably. UnsafeWorldCell
125                // provides field-level access via addr_of!, so fetch()
126                // impls never create intermediate &World / &mut World
127                // references — only field-level references to `resources`
128                // or `archetypes`, which live in separate memory regions.
129                let cell = unsafe { UnsafeWorldCell::new(world as *mut World) };
130                unsafe {
131                    self($($P::fetch(cell),)+);
132                }
133            }
134
135            fn param_access() -> Vec<Access> {
136                let mut acc = Vec::new();
137                $(acc.extend($P::access());)+
138                acc
139            }
140        }
141
142        impl<Func, $($P: SystemParam + 'static),+> IntoSystem<($($P,)+)> for Func
143        where
144            Func: SystemParamFunction<($($P,)+)>,
145        {
146            fn into_system(self, name: &'static str) -> Box<dyn System> {
147                let access = <Func as SystemParamFunction<($($P,)+)>>::param_access();
148                validate_no_self_conflicts(&access, name);
149                Box::new(ParamSystem {
150                    name,
151                    func: self,
152                    _marker: PhantomData,
153                })
154            }
155        }
156    };
157}
158
159impl_system_param_function!(P0);
160impl_system_param_function!(P0, P1);
161impl_system_param_function!(P0, P1, P2);
162impl_system_param_function!(P0, P1, P2, P3);
163impl_system_param_function!(P0, P1, P2, P3, P4);
164impl_system_param_function!(P0, P1, P2, P3, P4, P5);
165impl_system_param_function!(P0, P1, P2, P3, P4, P5, P6);
166impl_system_param_function!(P0, P1, P2, P3, P4, P5, P6, P7);
167
168// =============================================================================
169// Tests
170// =============================================================================
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::component::Component;
176    use crate::system_param::{Query, QueryMut, Res, ResMut};
177
178    #[derive(Debug)]
179    struct Counter(u32);
180    impl Component for Counter {}
181
182    struct Speed(f32);
183
184    fn count_entities(query: Query<'_, Counter>) {
185        let _ = query.len();
186    }
187
188    #[test]
189    fn one_param_into_system() {
190        let mut sys: Box<dyn System> =
191            IntoSystem::<(Query<'_, Counter>,)>::into_system(count_entities, "count");
192        let mut world = World::new();
193        world.spawn((Counter(0),));
194        sys.run(&mut world);
195    }
196
197    fn noop() {}
198
199    #[test]
200    fn zero_param_into_system() {
201        let mut sys: Box<dyn System> = IntoSystem::<()>::into_system(noop, "noop");
202        let mut world = World::new();
203        sys.run(&mut world);
204        assert!(sys.access().is_empty());
205    }
206
207    fn read_speed_count_entities(speed: Res<'_, Speed>, query: Query<'_, Counter>) {
208        let _ = *speed;
209        let _ = query.len();
210    }
211
212    #[test]
213    fn two_param_into_system() {
214        let mut sys: Box<dyn System> =
215            IntoSystem::<(Res<'_, Speed>, Query<'_, Counter>)>::into_system(
216                read_speed_count_entities,
217                "two_param",
218            );
219        let mut world = World::new();
220        world.insert_resource(Speed(1.5));
221        world.spawn((Counter(0),));
222        sys.run(&mut world);
223    }
224
225    fn increment_speed(mut speed: ResMut<'_, Speed>) {
226        speed.0 += 1.0;
227    }
228
229    #[test]
230    fn res_mut_system_mutates() {
231        let mut sys: Box<dyn System> =
232            IntoSystem::<(ResMut<'_, Speed>,)>::into_system(increment_speed, "inc_speed");
233        let mut world = World::new();
234        world.insert_resource(Speed(0.0));
235        sys.run(&mut world);
236        assert!((world.resource::<Speed>().0 - 1.0).abs() < f32::EPSILON);
237    }
238
239    fn increment_counters(mut counters: QueryMut<'_, Counter>) {
240        for (_, c) in counters.iter_mut() {
241            c.0 += 1;
242        }
243    }
244
245    #[test]
246    fn query_mut_system_mutates() {
247        let mut sys: Box<dyn System> =
248            IntoSystem::<(QueryMut<'_, Counter>,)>::into_system(increment_counters, "inc_counters");
249        let mut world = World::new();
250        world.spawn((Counter(0),));
251        world.spawn((Counter(10),));
252        sys.run(&mut world);
253        let mut vals: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
254        vals.sort();
255        assert_eq!(vals, vec![1, 11]);
256    }
257
258    fn conflicting_system(_a: Res<'_, Speed>, _b: ResMut<'_, Speed>) {}
259
260    #[test]
261    #[should_panic(expected = "conflicting parameter access")]
262    fn self_conflict_panics_on_registration() {
263        let _ = IntoSystem::<(Res<'_, Speed>, ResMut<'_, Speed>)>::into_system(
264            conflicting_system,
265            "conflict",
266        );
267    }
268
269    #[test]
270    fn system_reports_access() {
271        let sys: Box<dyn System> =
272            IntoSystem::<(ResMut<'_, Speed>,)>::into_system(increment_speed, "inc_speed");
273        let access = sys.access();
274        assert_eq!(access.len(), 1);
275    }
276}