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