1use crate::ecs::{PipelineContext, access_ids, decompose, schedule};
34
35fn before_init(ctx: &mut PipelineContext) {
38 #[cfg(debug_assertions)]
41 access_ids::install_hook();
42 decompose::run(ctx);
43}
44
45crate::define_systems! {
46 before_init: before_init,
47 prepare_events: access_ids::ensure_event_queues,
48
49 OverlaySystem => crate::gfx::overlay::OverlaySystem {
50 gate: schedule::overlay,
51 present_when: "the world declares a GraphicsConfig",
52 after: [],
53 before: [BehaviorSystem, SpawnSystem, GraphicsSystem, InputSystem, PhysicsSystem, AnimationSystem],
54 },
55 BehaviorSystem => concinnity_core::behavior::BehaviorSystem {
56 gate: schedule::behavior,
57 present_when: "the world declares any Behavior",
58 after: [OverlaySystem],
59 before: [SpawnSystem, SettingsSystem, StorySystem, AudioSystem],
60 },
61 SpawnSystem => crate::spawn::SpawnSystem {
62 gate: schedule::spawn,
63 present_when: "the world declares a GraphicsConfig",
64 after: [BehaviorSystem],
65 before: [GraphicsSystem],
66 },
67 SettingsSystem => crate::gfx::settings_system::SettingsSystem {
68 gate: schedule::settings,
69 present_when: "the world declares a GraphicsConfig",
70 after: [],
71 before: [GraphicsSystem],
72 },
73 StreamingSystem => crate::gfx::streaming_system::StreamingSystem {
74 gate: schedule::streaming,
75 present_when: "the world declares a GraphicsConfig",
76 after: [],
77 before: [GraphicsSystem],
78 },
79 GraphicsSystem => crate::gfx::graphics_system::GraphicsSystem {
80 gate: schedule::graphics,
81 present_when: "the world declares a GraphicsConfig",
82 after: [SpawnSystem, SettingsSystem, StreamingSystem],
83 before: [InputSystem],
84 },
85 InputSystem => crate::gfx::input_system::InputSystem {
86 gate: schedule::input,
87 present_when: "the world declares a GraphicsConfig",
88 after: [GraphicsSystem],
89 before: [],
90 },
91 StatHud => crate::hud::stat_hud::StatHudSystem {
92 gate: schedule::stat_hud,
93 present_when: "the world declares a StatHud",
94 after: [],
95 before: [],
96 },
97 DebugHud => crate::hud::debug_hud::DebugHudSystem {
98 gate: schedule::debug_hud,
99 present_when: "the world declares a DebugHud AND the binary is a debug build or a `cn debug` session",
100 after: [],
101 before: [],
102 },
103 LoadingOverlaySystem => crate::hud::loading_overlay::LoadingOverlaySystem {
104 gate: schedule::loading_overlay,
105 present_when: "the world declares a LoadingOverlay",
106 after: [StreamingSystem],
107 before: [UiInputSystem],
108 },
109 PhysicsSystem => concinnity_core::physics::PhysicsSystem {
110 gate: schedule::physics,
111 present_when: "the world declares a PhysicsConfig, RigidBody, PropBody, or TriggerVolume, or a skinned mesh bakes a character capsule",
112 after: [OverlaySystem],
113 before: [Camera3DSystem, ThirdPersonSystem],
114 },
115 Camera3DSystem => crate::gfx::camera_controller::Camera3DSystem {
116 gate: schedule::camera3d,
117 present_when: "the first controlled Camera3D has no follow block",
118 after: [PhysicsSystem],
119 before: [AudioSystem],
120 },
121 ThirdPersonSystem => crate::gfx::third_person::ThirdPersonSystem {
122 gate: schedule::third_person,
123 present_when: "the first controlled Camera3D has a follow block",
124 after: [PhysicsSystem],
125 before: [AudioSystem],
126 },
127 FpsCounter => crate::hud::fps_counter::FpsCounterSystem {
128 gate: schedule::fps_counter,
129 present_when: "the world declares an FpsCounter",
130 after: [],
131 before: [],
132 },
133 AnimationSystem => crate::gfx::animation::AnimationSystem {
134 gate: schedule::animation,
135 present_when: "the world declares any Animation or AnimationGraph",
136 after: [OverlaySystem],
137 before: [],
138 },
139 StorySystem => crate::story::StorySystem {
140 gate: schedule::story,
141 present_when: "the world declares a Story",
142 after: [BehaviorSystem],
143 before: [AudioSystem],
144 },
145 AudioSystem => crate::audio::AudioSystem {
146 gate: schedule::audio,
147 present_when: "the world declares any AudioEmitter, AudioCue, a Story page/choice with audio, or a Behavior with a sound node",
148 after: [BehaviorSystem, Camera3DSystem, ThirdPersonSystem, StorySystem],
149 before: [],
150 },
151 UiInputSystem => crate::ui::UiInputSystem {
152 gate: schedule::ui_input,
153 present_when: "the world declares any HitRegion, Screen, or KeyBinding",
154 after: [LoadingOverlaySystem],
155 before: [],
156 },
157 TextInputSystem => crate::text_input_system::TextInputSystem {
158 gate: schedule::text_input,
159 present_when: "the world declares any TextInput",
160 after: [],
161 before: [],
162 },
163}
164
165#[cfg(test)]
166mod tests {
167 use super::SYSTEMS;
168 use crate::ecs::{ComponentAsset, World};
169
170 const ENTRIES: &[crate::ecs::SystemEntry] = SYSTEMS.entries;
172
173 #[test]
177 fn hud_components_spawn_in_schedule_order() {
178 use crate::components::{DebugHud, FpsCounter, StatHud};
179
180 let mut world = World::new();
181 world.add_component(FpsCounter::default());
182 world.add_component(StatHud::default());
183 world.add_component(DebugHud::default());
184 world.start(SYSTEMS).unwrap();
185
186 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
187 assert_eq!(names, ["StatHud", "DebugHud", "FpsCounter"]);
188 }
189
190 #[test]
194 fn system_manifest_matches_started_systems() {
195 use crate::components::{DebugHud, FpsCounter, StatHud, Story, TextInput};
196
197 let mut world = World::new();
198 world.add_component(StatHud::default());
199 world.add_component(DebugHud::default());
200 world.add_component(FpsCounter::default());
201 world.add_component(Story::default());
202 world.add_component(TextInput::default());
203
204 let manifest = world.system_manifest(SYSTEMS);
205 world.start(SYSTEMS).unwrap();
206 let built: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
207 assert_eq!(manifest, built);
208 }
209
210 #[test]
213 fn system_manifest_is_a_table_order_subset() {
214 use crate::components::{FpsCounter, StatHud};
215
216 let mut world = World::new();
217 world.add_component(FpsCounter::default());
218 world.add_component(StatHud::default());
219
220 let table: Vec<&str> = ENTRIES.iter().map(|e| e.name).collect();
221 let manifest = world.system_manifest(SYSTEMS);
222 let mut cursor = table.iter();
223 for name in &manifest {
224 assert!(
225 cursor.any(|t| t == name),
226 "'{name}' out of table order or unknown: {manifest:?}"
227 );
228 }
229 }
230
231 #[test]
236 fn streaming_runs_immediately_before_graphics() {
237 let mut world = World::new();
238 world.add_component(crate::components::GraphicsConfig::default());
239 let manifest = world.system_manifest(SYSTEMS);
240 let s = manifest
241 .iter()
242 .position(|n| *n == "StreamingSystem")
243 .expect("StreamingSystem present for a GraphicsConfig world");
244 let g = manifest
245 .iter()
246 .position(|n| *n == "GraphicsSystem")
247 .expect("GraphicsSystem present for a GraphicsConfig world");
248 assert_eq!(
249 g,
250 s + 1,
251 "StreamingSystem is directly before GraphicsSystem: {manifest:?}"
252 );
253 }
254
255 #[test]
258 fn camera_controller_gates_are_exclusive() {
259 use crate::components::{Camera3D, CameraController, FollowController};
260
261 let mut fly_cam = Camera3D::bake(Default::default());
262 fly_cam.controller = Some(CameraController::default());
263 let mut fly = World::new();
264 fly.add_component(fly_cam);
265 assert_eq!(fly.system_manifest(SYSTEMS), ["Camera3DSystem"]);
266
267 let mut follow_cam = Camera3D::bake(Default::default());
268 follow_cam.controller = Some(CameraController {
269 follow: Some(FollowController::default()),
270 ..Default::default()
271 });
272 let mut follow = World::new();
273 follow.add_component(follow_cam);
274 assert_eq!(follow.system_manifest(SYSTEMS), ["ThirdPersonSystem"]);
275 }
276
277 #[test]
281 fn audio_gate_probes_without_a_device() {
282 let mut world = World::new();
283 world.add_component(crate::components::AudioEmitter::default());
284 assert_eq!(world.system_manifest(SYSTEMS), ["AudioSystem"]);
285 }
286
287 #[test]
290 fn story_component_spawns_story_system() {
291 let mut world = World::new();
292 world.add_component(crate::components::Story::default());
293 world.start(SYSTEMS).unwrap();
294
295 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
296 assert_eq!(names, ["StorySystem"]);
297 }
298
299 #[test]
302 fn empty_world_fills_from_components_then_systems() {
303 use crate::components::{FpsCounter, TextLabel};
304
305 let mut world = World::new();
306 assert!(world.is_empty());
307 assert_eq!(world.component_count(), 0);
308 assert_eq!(world.system_count(), 0);
309
310 world.add(ComponentAsset::from(TextLabel::default()));
311 assert!(!world.is_empty());
312 assert_eq!(world.component_count(), 1);
313
314 world.add_component(FpsCounter::default());
315 world.start(SYSTEMS).unwrap();
316 assert_eq!(world.system_count(), 1, "the FpsCounter gate built one");
317 }
318
319 #[test]
323 fn declared_edges_respect_table_order() {
324 let position = |name: &str| {
325 ENTRIES
326 .iter()
327 .position(|e| e.name == name)
328 .expect("a known entry")
329 };
330 for (i, entry) in ENTRIES.iter().enumerate() {
331 for after in entry.after {
332 assert!(
333 position(after) < i,
334 "{} runs after {after}, but the table runs {after} later",
335 entry.name,
336 );
337 }
338 for before in entry.before {
339 assert!(
340 i < position(before),
341 "{} runs before {before}, but the table runs {before} earlier",
342 entry.name,
343 );
344 }
345 }
346 }
347
348 #[test]
351 fn edge_names_exist() {
352 for entry in ENTRIES {
353 for name in entry.after.iter().chain(entry.before) {
354 assert!(
355 ENTRIES.iter().any(|e| e.name == *name),
356 "{} names unknown system {name}",
357 entry.name
358 );
359 }
360 }
361 }
362
363 #[test]
365 fn every_entry_documents_its_gate() {
366 for entry in ENTRIES {
367 assert!(
368 !entry.present_when.is_empty(),
369 "{} has no present_when",
370 entry.name
371 );
372 }
373 }
374}