1use crate::ecs::{PipelineContext, access_ids, decompose, schedule};
34
35fn before_init(ctx: &mut PipelineContext) {
40 #[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 const ENTRIES: &[crate::ecs::SystemEntry] = SYSTEMS.entries;
175
176 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}