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(
142 content,
143 crate::project::assets_dir().as_deref(),
144 crate::cook_platform(),
145 )
146 .map_err(|errs| concinnity_cook::check::report_validation_errors(&errs))?;
147
148 if loaded.assets.is_empty() {
149 println!("{} expands to no assets.", json_path);
150 return Ok(());
151 }
152
153 let rows: Vec<(String, String, String)> = loaded
154 .assets
155 .iter()
156 .map(|a| {
157 (
158 a.name.clone(),
159 a.asset_type.clone(),
160 provenance(&loaded, &a.name),
161 )
162 })
163 .collect();
164
165 let w_name = rows.iter().map(|r| r.0.len()).max().unwrap_or(4).max(4);
166 let w_type = rows.iter().map(|r| r.1.len()).max().unwrap_or(4).max(4);
167
168 println!(
169 "{:<w_name$} {:<w_type$} PROVENANCE",
170 "NAME",
171 "TYPE",
172 w_name = w_name,
173 w_type = w_type,
174 );
175 println!("{}", "-".repeat(w_name + w_type + 14));
176
177 for (name, type_str, prov) in &rows {
178 println!(
179 "{:<w_name$} {:<w_type$} {}",
180 name,
181 type_str,
182 prov,
183 w_name = w_name,
184 w_type = w_type,
185 );
186 }
187
188 let injected = loaded.injected.len();
189 println!(
190 "\n{} asset(s) after expansion ({} injected) in {}",
191 rows.len(),
192 injected,
193 json_path
194 );
195 println!("Use `cn explain <name>` to print an entry for overriding.");
196 Ok(())
197}
198
199fn list_systems(content: &str, json_path: &str) -> std::io::Result<()> {
204 let mut world = crate::build_world_from_str(content)?;
205 complete(&mut world)?;
206 let lines = manifest_lines(&world);
207
208 if lines.is_empty() {
209 println!("{} runs no systems.", json_path);
210 return Ok(());
211 }
212
213 println!("{} runs {} system(s), in order:", json_path, lines.len());
214 for line in &lines {
215 println!(" {}", line);
216 }
217 Ok(())
218}
219
220fn complete(world: &mut concinnity_engine::ecs::World) -> std::io::Result<()> {
224 let Some(complete) = concinnity_engine::ecs::SYSTEMS.complete_world else {
225 return Ok(());
226 };
227 complete(&mut world.context())
228 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
229}
230
231fn manifest_lines(world: &concinnity_engine::ecs::World) -> Vec<String> {
236 let manifest = world.system_manifest(concinnity_engine::ecs::SYSTEMS);
237 let width = manifest.iter().map(|n| n.len()).max().unwrap_or(0);
238 manifest
239 .iter()
240 .map(|name| {
241 let reason = concinnity_engine::ecs::SYSTEMS
242 .entries
243 .iter()
244 .find(|e| e.name == *name)
245 .map(|e| e.present_when)
246 .unwrap_or("");
247 format!("{name:<width$} {reason}")
248 })
249 .collect()
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn write_world(content: &str) -> (tempfile::TempDir, String) {
257 let dir = tempfile::tempdir().unwrap();
258 let path = dir.path().join("world.jsonl");
259 std::fs::write(&path, content).unwrap();
260 (dir, path.to_string_lossy().into_owned())
261 }
262
263 #[test]
264 fn resolve_world_path_prefers_an_explicit_existing_path() {
265 let (_dir, path) = write_world("");
266 assert_eq!(resolve_world_path(Some(&path)).unwrap(), path);
267 }
268
269 #[test]
273 fn manifest_lines_report_the_world_schedule_with_reasons() {
274 let world = crate::build_world_from_str(
275 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n\
276 {\"name\":\"cam\",\"type\":\"Camera3D\",\"args\":{\"controller\":{\"free_fly\":true}}}\n",
277 )
278 .unwrap();
279 let lines = manifest_lines(&world);
280 let joined = lines.join("\n");
281 assert!(joined.contains("GraphicsSystem"), "{joined}");
282 assert!(joined.contains("Camera3DSystem"), "{joined}");
283 assert!(joined.contains("GraphicsConfig"), "{joined}");
285 }
286
287 #[test]
290 fn the_manifest_reports_systems_the_engine_defaults_turn_on() {
291 let mut world = crate::build_world_from_str(
292 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
293 )
294 .unwrap();
295 assert!(
296 !manifest_lines(&world).join("\n").contains("DebugHud"),
297 "the world declares no DebugHud of its own"
298 );
299 complete(&mut world).unwrap();
300 assert!(
301 manifest_lines(&world).join("\n").contains("DebugHud"),
302 "the injected debug HUD brings its system with it"
303 );
304 }
305
306 #[test]
308 fn list_with_systems_flag_is_ok() {
309 let (_dir, path) =
310 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
311 list(Some(&path), false, true).unwrap();
312 }
313
314 fn loaded_world_fixture() -> concinnity_cook::build_only::LoadedWorld {
315 concinnity_cook::build_only::LoadedWorld {
316 assets: Vec::new(),
317 injected: vec![concinnity_cook::build_only::InjectedAsset {
318 name: "debug_hud".to_string(),
319 asset_type: "DebugHud".to_string(),
320 args: serde_json::json!({}),
321 injected_by: "debug_hud",
322 }],
323 generated: vec![concinnity_cook::build_only::GeneratedAsset {
324 name: "bistro_mat_wood".to_string(),
325 asset_type: "Material".to_string(),
326 generated_by: "bistro".to_string(),
327 }],
328 shadowed: vec![concinnity_cook::build_only::ShadowedAsset {
329 name: "bistro_mat_glass".to_string(),
330 asset_type: "Material".to_string(),
331 generated_by: "bistro".to_string(),
332 args: serde_json::json!({}),
333 }],
334 authored: vec!["cam".to_string(), "bistro_mat_glass".to_string()],
335 }
336 }
337
338 #[test]
339 fn provenance_reports_authored_names() {
340 assert_eq!(provenance(&loaded_world_fixture(), "cam"), "authored");
341 }
342
343 #[test]
344 fn provenance_reports_the_injection_pass() {
345 assert_eq!(
346 provenance(&loaded_world_fixture(), "debug_hud"),
347 "injected:debug_hud"
348 );
349 }
350
351 #[test]
354 fn provenance_reports_the_generating_import() {
355 assert_eq!(
356 provenance(&loaded_world_fixture(), "bistro_mat_wood"),
357 "generated:bistro"
358 );
359 }
360
361 #[test]
364 fn provenance_reports_what_an_authored_copy_shadows() {
365 assert_eq!(
366 provenance(&loaded_world_fixture(), "bistro_mat_glass"),
367 "authored (shadows bistro)"
368 );
369 }
370
371 #[test]
372 fn provenance_falls_back_to_expanded() {
373 assert_eq!(provenance(&loaded_world_fixture(), "anything"), "expanded");
374 }
375
376 #[test]
377 fn list_of_an_empty_world_is_ok() {
378 let (_dir, path) = write_world("");
379 list(Some(&path), false, false).unwrap();
380 }
381
382 #[test]
383 fn list_prints_known_and_unknown_types() {
384 let (_dir, path) = write_world(concat!(
388 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
389 "{\"name\":\"clip\",\"type\":\"AudioClip\",\"args\":{}}\n",
390 "{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n",
391 "{\"type\":\"GraphicsConfig\",\"args\":{}}\n",
392 ));
393 list(Some(&path), false, false).unwrap();
394 }
395
396 #[test]
397 fn registration_for_resolves_component_types() {
398 let r = registration_for("GraphicsConfig").unwrap();
399 assert_eq!(r.type_name, "GraphicsConfig");
400 }
401
402 #[test]
403 fn registration_for_resolves_resource_assets() {
404 for name in ["AudioClip", "Texture"] {
407 assert!(RegisteredType::parse(name).is_some_and(|t| t.is_resource()));
408 let r = registration_for(name).unwrap();
409 assert_eq!(
410 r.origin,
411 concinnity_cook::authoring::registry::AssetOrigin::External
412 );
413 assert_eq!(
414 r.payload,
415 concinnity_cook::authoring::registry::AssetPayload::Compiled
416 );
417 }
418 }
419
420 #[test]
421 fn registration_for_unknown_type_is_none() {
422 assert!(registration_for("NotARealAssetType").is_none());
423 }
424
425 #[test]
426 fn list_with_invalid_json_errors() {
427 let (_dir, path) = write_world("{ not json\n");
428 let err = list(Some(&path), false, false).unwrap_err();
429 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
430 }
431
432 #[test]
433 fn list_expanded_runs_the_build_front_half() {
434 let (_dir, path) =
435 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
436 list(Some(&path), true, false).unwrap();
437 }
438
439 #[test]
440 fn list_expanded_rejects_an_unknown_asset_type() {
441 let (_dir, path) =
442 write_world("{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n");
443 assert!(list(Some(&path), true, false).is_err());
444 }
445
446 #[test]
450 fn list_of_an_unreadable_world_surfaces_the_read_error() {
451 let dir = tempfile::tempdir().unwrap();
452 let path = dir.path().to_string_lossy().into_owned();
453 assert!(list(Some(&path), false, false).is_err());
454 }
455
456 #[test]
459 fn list_expanded_of_an_empty_world_is_ok() {
460 let (_dir, path) = write_world("");
461 list(Some(&path), true, false).unwrap();
462 }
463
464 #[test]
466 fn list_systems_of_an_empty_world_is_ok() {
467 let (_dir, path) = write_world("");
468 list(Some(&path), false, true).unwrap();
469 }
470
471 #[test]
472 fn manifest_lines_of_an_empty_world_are_empty() {
473 let world = crate::build_world_from_str("").unwrap();
474 assert!(manifest_lines(&world).is_empty());
475 }
476}