1use concinnity_cook::authoring::registry::RegisteredType;
3use concinnity_cook::authoring::world::{find_world_jsonl, parse_world_jsonl, resolve_includes};
4
5fn registration_for(type_str: &str) -> Option<concinnity_cook::authoring::registry::Registration> {
7 RegisteredType::parse(type_str).map(RegisteredType::registration)
8}
9
10pub(crate) fn resolve_world_path(json_path: Option<&str>) -> std::io::Result<String> {
13 match json_path {
14 Some(p) if std::path::Path::new(p).exists() => Ok(p.to_string()),
15 _ => find_world_jsonl(crate::project::worlds_dir().as_deref(), None),
16 }
17}
18
19pub(crate) fn provenance(loaded: &concinnity_cook::build_only::LoadedWorld, name: &str) -> String {
24 loaded.provenance(name).to_string()
25}
26
27pub fn list(json_path: Option<&str>, expanded: bool, systems: bool) -> std::io::Result<()> {
30 let json_path = resolve_world_path(json_path)?;
31
32 let content = std::fs::read_to_string(&json_path).map_err(|e| {
33 tracing::error!("Could not read {}: {}", json_path, e);
34 e
35 })?;
36
37 if systems {
38 return list_systems(&content, &json_path);
39 }
40 if expanded {
41 return list_expanded(&content, &json_path);
42 }
43
44 let assets = parse_world_jsonl(&content).map_err(|e| {
45 std::io::Error::new(
46 std::io::ErrorKind::InvalidData,
47 format!("Failed to parse {}: {}", json_path, e),
48 )
49 })?;
50
51 let raw = resolve_includes(assets)?;
52
53 if raw.is_empty() {
54 println!("{} has no assets.", json_path);
55 return Ok(());
56 }
57
58 struct Row {
60 name: String,
61 type_str: String,
62 origin: String,
63 payload: String,
64 }
65
66 let rows: Vec<Row> = raw
67 .iter()
68 .map(|v| {
69 let name = v
70 .get("name")
71 .and_then(|n| n.as_str())
72 .unwrap_or("(unnamed)")
73 .to_string();
74 let type_str = v
75 .get("type")
76 .and_then(|t| t.as_str())
77 .unwrap_or("?")
78 .to_string();
79
80 let (origin, payload) = if let Some(r) = registration_for(&type_str) {
81 (format!("{:?}", r.origin), format!("{:?}", r.payload))
82 } else {
83 ("?".to_string(), "?".to_string())
84 };
85
86 Row {
87 name,
88 type_str,
89 origin,
90 payload,
91 }
92 })
93 .collect();
94
95 let w_name = rows.iter().map(|r| r.name.len()).max().unwrap_or(4).max(4);
96 let w_type = rows
97 .iter()
98 .map(|r| r.type_str.len())
99 .max()
100 .unwrap_or(4)
101 .max(4);
102 let w_origin = rows
103 .iter()
104 .map(|r| r.origin.len())
105 .max()
106 .unwrap_or(6)
107 .max(6);
108
109 println!(
110 "{:<w_name$} {:<w_type$} {:<w_origin$} PAYLOAD",
111 "NAME",
112 "TYPE",
113 "ORIGIN",
114 w_name = w_name,
115 w_type = w_type,
116 w_origin = w_origin,
117 );
118 println!("{}", "-".repeat(w_name + w_type + w_origin + 16));
119
120 for r in &rows {
121 println!(
122 "{:<w_name$} {:<w_type$} {:<w_origin$} {}",
123 r.name,
124 r.type_str,
125 r.origin,
126 r.payload,
127 w_name = w_name,
128 w_type = w_type,
129 w_origin = w_origin,
130 );
131 }
132
133 println!("\n{} asset(s) in {}", rows.len(), json_path);
134 Ok(())
135}
136
137fn list_expanded(content: &str, json_path: &str) -> std::io::Result<()> {
141 let loaded = concinnity_cook::prepare_world(content, crate::project::assets_dir().as_deref())
142 .map_err(|errs| concinnity_cook::check::report_validation_errors(&errs))?;
143
144 if loaded.assets.is_empty() {
145 println!("{} expands to no assets.", json_path);
146 return Ok(());
147 }
148
149 let rows: Vec<(String, String, String)> = loaded
150 .assets
151 .iter()
152 .map(|a| {
153 (
154 a.name.clone(),
155 a.asset_type.clone(),
156 provenance(&loaded, &a.name),
157 )
158 })
159 .collect();
160
161 let w_name = rows.iter().map(|r| r.0.len()).max().unwrap_or(4).max(4);
162 let w_type = rows.iter().map(|r| r.1.len()).max().unwrap_or(4).max(4);
163
164 println!(
165 "{:<w_name$} {:<w_type$} PROVENANCE",
166 "NAME",
167 "TYPE",
168 w_name = w_name,
169 w_type = w_type,
170 );
171 println!("{}", "-".repeat(w_name + w_type + 14));
172
173 for (name, type_str, prov) in &rows {
174 println!(
175 "{:<w_name$} {:<w_type$} {}",
176 name,
177 type_str,
178 prov,
179 w_name = w_name,
180 w_type = w_type,
181 );
182 }
183
184 let injected = loaded.injected.len();
185 println!(
186 "\n{} asset(s) after expansion ({} injected) in {}",
187 rows.len(),
188 injected,
189 json_path
190 );
191 println!("Use `cn explain <name>` to print an entry for overriding.");
192 Ok(())
193}
194
195fn list_systems(content: &str, json_path: &str) -> std::io::Result<()> {
200 let mut world = crate::build_world_from_str(content)?;
201 complete(&mut world)?;
202 let lines = manifest_lines(&world);
203
204 if lines.is_empty() {
205 println!("{} runs no systems.", json_path);
206 return Ok(());
207 }
208
209 println!("{} runs {} system(s), in order:", json_path, lines.len());
210 for line in &lines {
211 println!(" {}", line);
212 }
213 Ok(())
214}
215
216fn complete(world: &mut concinnity_engine::ecs::World) -> std::io::Result<()> {
220 let Some(complete) = concinnity_engine::ecs::SYSTEMS.complete_world else {
221 return Ok(());
222 };
223 complete(&mut world.context())
224 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
225}
226
227fn manifest_lines(world: &concinnity_engine::ecs::World) -> Vec<String> {
232 let manifest = world.system_manifest(concinnity_engine::ecs::SYSTEMS);
233 let width = manifest.iter().map(|n| n.len()).max().unwrap_or(0);
234 manifest
235 .iter()
236 .map(|name| {
237 let reason = concinnity_engine::ecs::SYSTEMS
238 .entries
239 .iter()
240 .find(|e| e.name == *name)
241 .map(|e| e.present_when)
242 .unwrap_or("");
243 format!("{name:<width$} {reason}")
244 })
245 .collect()
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn write_world(content: &str) -> (tempfile::TempDir, String) {
253 let dir = tempfile::tempdir().unwrap();
254 let path = dir.path().join("world.jsonl");
255 std::fs::write(&path, content).unwrap();
256 (dir, path.to_string_lossy().into_owned())
257 }
258
259 #[test]
260 fn resolve_world_path_prefers_an_explicit_existing_path() {
261 let (_dir, path) = write_world("");
262 assert_eq!(resolve_world_path(Some(&path)).unwrap(), path);
263 }
264
265 #[test]
269 fn manifest_lines_report_the_world_schedule_with_reasons() {
270 let world = crate::build_world_from_str(
271 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n\
272 {\"name\":\"cam\",\"type\":\"Camera3D\",\"args\":{\"controller\":{\"free_fly\":true}}}\n",
273 )
274 .unwrap();
275 let lines = manifest_lines(&world);
276 let joined = lines.join("\n");
277 assert!(joined.contains("GraphicsSystem"), "{joined}");
278 assert!(joined.contains("Camera3DSystem"), "{joined}");
279 assert!(joined.contains("GraphicsConfig"), "{joined}");
281 }
282
283 #[test]
286 fn the_manifest_reports_systems_the_engine_defaults_turn_on() {
287 let mut world = crate::build_world_from_str(
288 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
289 )
290 .unwrap();
291 assert!(
292 !manifest_lines(&world).join("\n").contains("DebugHud"),
293 "the world declares no DebugHud of its own"
294 );
295 complete(&mut world).unwrap();
296 assert!(
297 manifest_lines(&world).join("\n").contains("DebugHud"),
298 "the injected debug HUD brings its system with it"
299 );
300 }
301
302 #[test]
304 fn list_with_systems_flag_is_ok() {
305 let (_dir, path) =
306 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
307 list(Some(&path), false, true).unwrap();
308 }
309
310 fn loaded_world_fixture() -> concinnity_cook::build_only::LoadedWorld {
311 concinnity_cook::build_only::LoadedWorld {
312 assets: Vec::new(),
313 injected: vec![concinnity_cook::build_only::InjectedAsset {
314 name: "debug_hud".to_string(),
315 asset_type: "DebugHud".to_string(),
316 args: serde_json::json!({}),
317 injected_by: "debug_hud",
318 }],
319 generated: vec![concinnity_cook::build_only::GeneratedAsset {
320 name: "bistro_mat_wood".to_string(),
321 asset_type: "Material".to_string(),
322 generated_by: "bistro".to_string(),
323 }],
324 shadowed: vec![concinnity_cook::build_only::ShadowedAsset {
325 name: "bistro_mat_glass".to_string(),
326 asset_type: "Material".to_string(),
327 generated_by: "bistro".to_string(),
328 args: serde_json::json!({}),
329 }],
330 authored: vec!["cam".to_string(), "bistro_mat_glass".to_string()],
331 }
332 }
333
334 #[test]
335 fn provenance_reports_authored_names() {
336 assert_eq!(provenance(&loaded_world_fixture(), "cam"), "authored");
337 }
338
339 #[test]
340 fn provenance_reports_the_injection_pass() {
341 assert_eq!(
342 provenance(&loaded_world_fixture(), "debug_hud"),
343 "injected:debug_hud"
344 );
345 }
346
347 #[test]
350 fn provenance_reports_the_generating_import() {
351 assert_eq!(
352 provenance(&loaded_world_fixture(), "bistro_mat_wood"),
353 "generated:bistro"
354 );
355 }
356
357 #[test]
360 fn provenance_reports_what_an_authored_copy_shadows() {
361 assert_eq!(
362 provenance(&loaded_world_fixture(), "bistro_mat_glass"),
363 "authored (shadows bistro)"
364 );
365 }
366
367 #[test]
368 fn provenance_falls_back_to_expanded() {
369 assert_eq!(provenance(&loaded_world_fixture(), "anything"), "expanded");
370 }
371
372 #[test]
373 fn list_of_an_empty_world_is_ok() {
374 let (_dir, path) = write_world("");
375 list(Some(&path), false, false).unwrap();
376 }
377
378 #[test]
379 fn list_prints_known_and_unknown_types() {
380 let (_dir, path) = write_world(concat!(
384 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
385 "{\"name\":\"clip\",\"type\":\"AudioClip\",\"args\":{}}\n",
386 "{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n",
387 "{\"type\":\"GraphicsConfig\",\"args\":{}}\n",
388 ));
389 list(Some(&path), false, false).unwrap();
390 }
391
392 #[test]
393 fn registration_for_resolves_component_types() {
394 let r = registration_for("GraphicsConfig").unwrap();
395 assert_eq!(r.type_name, "GraphicsConfig");
396 }
397
398 #[test]
399 fn registration_for_resolves_resource_assets() {
400 for name in ["AudioClip", "Texture"] {
403 assert!(RegisteredType::parse(name).is_some_and(|t| t.is_resource()));
404 let r = registration_for(name).unwrap();
405 assert_eq!(
406 r.origin,
407 concinnity_cook::authoring::registry::AssetOrigin::External
408 );
409 assert_eq!(
410 r.payload,
411 concinnity_cook::authoring::registry::AssetPayload::Compiled
412 );
413 }
414 }
415
416 #[test]
417 fn registration_for_unknown_type_is_none() {
418 assert!(registration_for("NotARealAssetType").is_none());
419 }
420
421 #[test]
422 fn list_with_invalid_json_errors() {
423 let (_dir, path) = write_world("{ not json\n");
424 let err = list(Some(&path), false, false).unwrap_err();
425 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
426 }
427
428 #[test]
429 fn list_expanded_runs_the_build_front_half() {
430 let (_dir, path) =
431 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
432 list(Some(&path), true, false).unwrap();
433 }
434
435 #[test]
436 fn list_expanded_rejects_an_unknown_asset_type() {
437 let (_dir, path) =
438 write_world("{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n");
439 assert!(list(Some(&path), true, false).is_err());
440 }
441
442 #[test]
446 fn list_of_an_unreadable_world_surfaces_the_read_error() {
447 let dir = tempfile::tempdir().unwrap();
448 let path = dir.path().to_string_lossy().into_owned();
449 assert!(list(Some(&path), false, false).is_err());
450 }
451
452 #[test]
455 fn list_expanded_of_an_empty_world_is_ok() {
456 let (_dir, path) = write_world("");
457 list(Some(&path), true, false).unwrap();
458 }
459
460 #[test]
462 fn list_systems_of_an_empty_world_is_ok() {
463 let (_dir, path) = write_world("");
464 list(Some(&path), false, true).unwrap();
465 }
466
467 #[test]
468 fn manifest_lines_of_an_empty_world_are_empty() {
469 let world = crate::build_world_from_str("").unwrap();
470 assert!(manifest_lines(&world).is_empty());
471 }
472}