Skip to main content

concinnity_engine/ecs/
registry.rs

1// src/ecs/registry.rs
2//
3// The system table: the one place a system is registered and the one schedule
4// document. `define_systems!` generates the entries from it; table order is run
5// order. Every system is internal: it has no declarable asset and carries no
6// discriminant. `World::start` runs each entry's gate against the world's
7// content and pushes the systems the gates return, in table order. To add a
8// system: implement `System` on it, write its gate in `schedule`, and add one
9// entry here in its run position with its ordering edges.
10//
11// `after`/`before` are the cross-system ordering constraints, validated
12// against table order when the world's schedule is built.
13// Each edge's rationale:
14//   * Overlay first: publishes the menu state (`MenuActive`) that gates
15//     simulation, input, and the draw this same tick.
16//   * Behavior before Spawn/Settings/Story/Audio: the requests its firing
17//     rules emit (spawn/despawn, scene, story, audio) drain the same tick.
18//   * Spawn/Settings/Streaming before Graphics: despawns leave the transform
19//     push, setting and streaming ops land before this frame's submit, and
20//     `CameraRelativeView` is ready for the draw.
21//   * Input after Graphics: on Metal the OS event pump runs inside
22//     draw_frame, so sampling right after the draw snapshots the freshest
23//     events (the mailbox deposit happens in Graphics' step).
24//   * LoadingOverlay after Streaming (reads the residency status published
25//     this tick) and before UiInput (its screen commands apply same tick).
26//   * Physics before the camera controllers: physics consumes the camera's
27//     previous-frame `desired_move` (a one-frame-lagged resolution).
28//   * Cameras and Story before Audio: the listener reads the camera, and a
29//     `PlayCue` page audio is heard the same tick.
30// Event-carried couplings (RootMotionEvent, GroundProbes, SettingCommand) are
31// order-robust thanks to the event store's two-frame retention.
32
33use crate::ecs::{PipelineContext, access_ids, decompose, schedule};
34
35// Runs once at world start, after the gates have built the systems and before
36// their `init`. The engine defaults the world is completed with land earlier
37// still (`complete_world` below), so a Prop one of them injects decomposes
38// here like any other.
39fn before_init(ctx: &mut PipelineContext) {
40    // Every context touch a stepping system makes is asserted against what it
41    // declared; the hook is installed before the first step can happen.
42    #[cfg(debug_assertions)]
43    access_ids::install_hook();
44    decompose::run(ctx);
45}
46
47crate::define_systems! {
48    complete_world: concinnity_core::defaults::run,
49    before_init: before_init,
50    prepare_events: access_ids::ensure_event_queues,
51
52    OverlaySystem => crate::gfx::overlay::OverlaySystem {
53        gate: schedule::overlay,
54        present_when: "the world declares a GraphicsConfig",
55        after: [],
56        before: [BehaviorSystem, SpawnSystem, GraphicsSystem, InputSystem, PhysicsSystem, AnimationSystem],
57    },
58    BehaviorSystem => concinnity_core::behavior::BehaviorSystem {
59        gate: schedule::behavior,
60        present_when: "the world declares any Behavior",
61        after: [OverlaySystem],
62        before: [SpawnSystem, SettingsSystem, StorySystem, AudioSystem],
63    },
64    SpawnSystem => crate::spawn::SpawnSystem {
65        gate: schedule::spawn,
66        present_when: "the world declares a GraphicsConfig",
67        after: [BehaviorSystem],
68        before: [GraphicsSystem],
69    },
70    SettingsSystem => crate::gfx::settings_system::SettingsSystem {
71        gate: schedule::settings,
72        present_when: "the world declares a GraphicsConfig",
73        after: [],
74        before: [GraphicsSystem],
75    },
76    StreamingSystem => crate::gfx::streaming_system::StreamingSystem {
77        gate: schedule::streaming,
78        present_when: "the world declares a GraphicsConfig",
79        after: [],
80        before: [GraphicsSystem],
81    },
82    GraphicsSystem => crate::gfx::graphics_system::GraphicsSystem {
83        gate: schedule::graphics,
84        present_when: "the world declares a GraphicsConfig",
85        after: [SpawnSystem, SettingsSystem, StreamingSystem],
86        before: [InputSystem],
87    },
88    InputSystem => crate::gfx::input_system::InputSystem {
89        gate: schedule::input,
90        present_when: "the world declares a GraphicsConfig",
91        after: [GraphicsSystem],
92        before: [],
93    },
94    StatHud => crate::hud::stat_hud::StatHudSystem {
95        gate: schedule::stat_hud,
96        present_when: "the world declares a StatHud",
97        after: [],
98        before: [],
99    },
100    DebugHud => crate::hud::debug_hud::DebugHudSystem {
101        gate: schedule::debug_hud,
102        present_when: "the world declares a DebugHud AND the binary is a debug build or a `cn debug` session",
103        after: [],
104        before: [],
105    },
106    LoadingOverlaySystem => crate::hud::loading_overlay::LoadingOverlaySystem {
107        gate: schedule::loading_overlay,
108        present_when: "the world declares a LoadingOverlay",
109        after: [StreamingSystem],
110        before: [UiInputSystem],
111    },
112    PhysicsSystem => concinnity_core::physics::PhysicsSystem {
113        gate: schedule::physics,
114        present_when: "the world declares a PhysicsConfig, RigidBody, PropBody, or TriggerVolume, or a skinned mesh bakes a character capsule",
115        after: [OverlaySystem],
116        before: [Camera3DSystem, ThirdPersonSystem],
117    },
118    Camera3DSystem => crate::gfx::camera_controller::Camera3DSystem {
119        gate: schedule::camera3d,
120        present_when: "the first controlled Camera3D has no follow block",
121        after: [PhysicsSystem],
122        before: [AudioSystem],
123    },
124    ThirdPersonSystem => crate::gfx::third_person::ThirdPersonSystem {
125        gate: schedule::third_person,
126        present_when: "the first controlled Camera3D has a follow block",
127        after: [PhysicsSystem],
128        before: [AudioSystem],
129    },
130    FpsCounter => crate::hud::fps_counter::FpsCounterSystem {
131        gate: schedule::fps_counter,
132        present_when: "the world declares an FpsCounter",
133        after: [],
134        before: [],
135    },
136    AnimationSystem => crate::gfx::animation::AnimationSystem {
137        gate: schedule::animation,
138        present_when: "the world declares any Animation or AnimationGraph",
139        after: [OverlaySystem],
140        before: [],
141    },
142    StorySystem => crate::story::StorySystem {
143        gate: schedule::story,
144        present_when: "the world declares a Story",
145        after: [BehaviorSystem],
146        before: [AudioSystem],
147    },
148    AudioSystem => crate::audio::AudioSystem {
149        gate: schedule::audio,
150        present_when: "the world declares any AudioEmitter, AudioCue, a Story page/choice with audio, or a Behavior with a sound node",
151        after: [BehaviorSystem, Camera3DSystem, ThirdPersonSystem, StorySystem],
152        before: [],
153    },
154    UiInputSystem => crate::ui::UiInputSystem {
155        gate: schedule::ui_input,
156        present_when: "the world declares any HitRegion, Screen, or KeyBinding",
157        after: [LoadingOverlaySystem],
158        before: [],
159    },
160    TextInputSystem => crate::text_input_system::TextInputSystem {
161        gate: schedule::text_input,
162        present_when: "the world declares any TextInput",
163        after: [],
164        before: [],
165    },
166}
167
168#[cfg(test)]
169mod tests {
170    use super::SYSTEMS;
171    use crate::ecs::{ComponentAsset, World};
172
173    // The table's entries, in run order.
174    const ENTRIES: &[crate::ecs::SystemEntry] = SYSTEMS.entries;
175
176    // The overlay HUD components each gate their internal system and build in
177    // the fixed schedule order (StatHud, then DebugHud, then FpsCounter).
178    // DebugHud is developer-only but `cfg!(debug_assertions)` holds under test.
179    #[test]
180    fn hud_components_spawn_in_schedule_order() {
181        use crate::components::{DebugHud, FpsCounter, StatHud};
182
183        let mut world = World::new();
184        world.add_component(FpsCounter::default());
185        world.add_component(StatHud::default());
186        world.add_component(DebugHud::default());
187        world.start(SYSTEMS).unwrap();
188
189        let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
190        assert_eq!(names, ["StatHud", "DebugHud", "FpsCounter"]);
191    }
192
193    // The manifest reports exactly the systems `start()` builds, in the same
194    // order, for a world gating several table entries. Audio is left ungated
195    // so `start()` opens no device here.
196    #[test]
197    fn system_manifest_matches_started_systems() {
198        use crate::components::{DebugHud, FpsCounter, StatHud, Story, TextInput};
199
200        let mut world = World::new();
201        world.add_component(StatHud::default());
202        world.add_component(DebugHud::default());
203        world.add_component(FpsCounter::default());
204        world.add_component(Story::default());
205        world.add_component(TextInput::default());
206
207        let manifest = world.system_manifest(SYSTEMS);
208        world.start(SYSTEMS).unwrap();
209        let built: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
210        assert_eq!(manifest, built);
211    }
212
213    // The table's completion pass runs from `start`, before the gates read the
214    // world: a world with physics content gets the config its simulation runs
215    // on, and the PhysicsSystem is built either way.
216    #[test]
217    fn start_completes_the_world_with_its_engine_defaults() {
218        use crate::components::{EngineDefaults, PhysicsConfig, PropBody};
219
220        let mut world = World::new();
221        world.add_component(PropBody::default());
222        world.start(SYSTEMS).unwrap();
223
224        assert_eq!(world.query::<PhysicsConfig>().count(), 1);
225        // The directive is consumed by the pass, so nothing holds one after.
226        assert_eq!(world.query::<EngineDefaults>().count(), 0);
227        let built: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
228        assert_eq!(built, ["PhysicsSystem"]);
229    }
230
231    // Manifest names come out in table order, and every name is a real table
232    // entry (the manifest is a filtered view of `SYSTEMS`, nothing else).
233    #[test]
234    fn system_manifest_is_a_table_order_subset() {
235        use crate::components::{FpsCounter, StatHud};
236
237        let mut world = World::new();
238        world.add_component(FpsCounter::default());
239        world.add_component(StatHud::default());
240
241        let table: Vec<&str> = ENTRIES.iter().map(|e| e.name).collect();
242        let manifest = world.system_manifest(SYSTEMS);
243        let mut cursor = table.iter();
244        for name in &manifest {
245            assert!(
246                cursor.any(|t| t == name),
247                "'{name}' out of table order or unknown: {manifest:?}"
248            );
249        }
250    }
251
252    // A GraphicsConfig world gates the whole render band, and StreamingSystem
253    // runs immediately before GraphicsSystem so its `CameraRelativeView` is
254    // ready for that frame's submit. (Manifest-only: gating a GraphicsConfig
255    // never builds a GPU, unlike `start()`.)
256    #[test]
257    fn streaming_runs_immediately_before_graphics() {
258        let mut world = World::new();
259        world.add_component(crate::components::GraphicsConfig::default());
260        let manifest = world.system_manifest(SYSTEMS);
261        let s = manifest
262            .iter()
263            .position(|n| *n == "StreamingSystem")
264            .expect("StreamingSystem present for a GraphicsConfig world");
265        let g = manifest
266            .iter()
267            .position(|n| *n == "GraphicsSystem")
268            .expect("GraphicsSystem present for a GraphicsConfig world");
269        assert_eq!(
270            g,
271            s + 1,
272            "StreamingSystem is directly before GraphicsSystem: {manifest:?}"
273        );
274    }
275
276    // The two camera-controller entries are mutually exclusive: the first
277    // controlled camera's `follow` block picks exactly one of them.
278    #[test]
279    fn camera_controller_gates_are_exclusive() {
280        use crate::components::{Camera3D, CameraController, FollowController};
281
282        let mut fly_cam = Camera3D::bake(Default::default());
283        fly_cam.controller = Some(CameraController::default());
284        let mut fly = World::new();
285        fly.add_component(fly_cam);
286        assert_eq!(fly.system_manifest(SYSTEMS), ["Camera3DSystem"]);
287
288        let mut follow_cam = Camera3D::bake(Default::default());
289        follow_cam.controller = Some(CameraController {
290            follow: Some(FollowController::default()),
291            ..Default::default()
292        });
293        let mut follow = World::new();
294        follow.add_component(follow_cam);
295        assert_eq!(follow.system_manifest(SYSTEMS), ["ThirdPersonSystem"]);
296    }
297
298    // An audio-gating component is visible in the manifest without a device:
299    // the gate probe constructs the system, and device acquisition waits for
300    // `System::init`.
301    #[test]
302    fn audio_gate_probes_without_a_device() {
303        let mut world = World::new();
304        world.add_component(crate::components::AudioEmitter::default());
305        assert_eq!(world.system_manifest(SYSTEMS), ["AudioSystem"]);
306    }
307
308    // A Story gates the StorySystem. An empty-node story pulls in no audio
309    // device (build_audio needs a page/choice cue), so this stays device-free.
310    #[test]
311    fn story_component_spawns_story_system() {
312        let mut world = World::new();
313        world.add_component(crate::components::Story::default());
314        world.start(SYSTEMS).unwrap();
315
316        let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
317        assert_eq!(names, ["StorySystem"]);
318    }
319
320    // A fresh world holds nothing; adding a component (through either the blob
321    // path or the typed one) fills it, and `start()` is what gives it systems.
322    #[test]
323    fn empty_world_fills_from_components_then_systems() {
324        use crate::components::{FpsCounter, TextLabel};
325
326        let mut world = World::new();
327        assert!(world.is_empty());
328        assert_eq!(world.component_count(), 0);
329        assert_eq!(world.system_count(), 0);
330
331        world.add(ComponentAsset::from(TextLabel::default()));
332        assert!(!world.is_empty());
333        assert_eq!(world.component_count(), 1);
334
335        world.add_component(FpsCounter::default());
336        world.start(SYSTEMS).unwrap();
337        assert_eq!(world.system_count(), 1, "the FpsCounter gate built one");
338    }
339
340    // Every declared edge agrees with table order. The table is the one
341    // execution order, so an edge that contradicts it is a schedule-build panic
342    // at world start; this catches it at the document instead.
343    #[test]
344    fn declared_edges_respect_table_order() {
345        let position = |name: &str| {
346            ENTRIES
347                .iter()
348                .position(|e| e.name == name)
349                .expect("a known entry")
350        };
351        for (i, entry) in ENTRIES.iter().enumerate() {
352            for after in entry.after {
353                assert!(
354                    position(after) < i,
355                    "{} runs after {after}, but the table runs {after} later",
356                    entry.name,
357                );
358            }
359            for before in entry.before {
360                assert!(
361                    i < position(before),
362                    "{} runs before {before}, but the table runs {before} earlier",
363                    entry.name,
364                );
365            }
366        }
367    }
368
369    // Every name in a declared edge is a real table entry: a typo would
370    // silently drop the constraint.
371    #[test]
372    fn edge_names_exist() {
373        for entry in ENTRIES {
374            for name in entry.after.iter().chain(entry.before) {
375                assert!(
376                    ENTRIES.iter().any(|e| e.name == *name),
377                    "{} names unknown system {name}",
378                    entry.name
379                );
380            }
381        }
382    }
383
384    // Every table entry carries a non-empty human-readable gate description.
385    #[test]
386    fn every_entry_documents_its_gate() {
387        for entry in ENTRIES {
388            assert!(
389                !entry.present_when.is_empty(),
390                "{} has no present_when",
391                entry.name
392            );
393        }
394    }
395}