use concinnity_cook::authoring::registry::RegisteredType;
use std::path::Path;
use concinnity_cook::WorldSource;
use concinnity_cook::authoring::world::{entry_handles, parse_world_jsonl};
use concinnity_cook::build_only::include::resolve_includes;
use crate::command::resolve_world_path;
fn registration_for(type_str: &str) -> Option<concinnity_cook::authoring::registry::Registration> {
RegisteredType::parse(type_str).map(RegisteredType::registration)
}
pub(crate) fn provenance(loaded: &concinnity_cook::build_only::LoadedWorld, name: &str) -> String {
loaded.provenance(name).to_string()
}
pub fn list(json_path: Option<&str>, expanded: bool, systems: bool) -> std::io::Result<()> {
let json_path = resolve_world_path(json_path)?;
let content = std::fs::read_to_string(&json_path).map_err(|e| {
tracing::error!("Could not read {}: {}", json_path, e);
e
})?;
let source = WorldSource::file(&content, Path::new(&json_path));
if systems {
return list_systems(source, &json_path);
}
if expanded {
return list_expanded(source, &json_path);
}
let assets = parse_world_jsonl(&content).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to parse {}: {}", json_path, e),
)
})?;
let raw = resolve_includes(assets, source.file)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
if raw.is_empty() {
println!("{} has no assets.", json_path);
return Ok(());
}
struct Row {
name: String,
type_str: String,
origin: String,
payload: String,
}
let handles = entry_handles(&raw);
let rows: Vec<Row> = raw
.iter()
.zip(handles)
.map(|(v, handle)| {
let name = handle.unwrap_or_else(|| "(untyped)".to_string());
let type_str = v
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("?")
.to_string();
let (origin, payload) = if let Some(r) = registration_for(&type_str) {
(format!("{:?}", r.origin), format!("{:?}", r.payload))
} else {
("?".to_string(), "?".to_string())
};
Row {
name,
type_str,
origin,
payload,
}
})
.collect();
let w_name = rows.iter().map(|r| r.name.len()).max().unwrap_or(4).max(4);
let w_type = rows
.iter()
.map(|r| r.type_str.len())
.max()
.unwrap_or(4)
.max(4);
let w_origin = rows
.iter()
.map(|r| r.origin.len())
.max()
.unwrap_or(6)
.max(6);
println!(
"{:<w_name$} {:<w_type$} {:<w_origin$} PAYLOAD",
"ID",
"TYPE",
"ORIGIN",
w_name = w_name,
w_type = w_type,
w_origin = w_origin,
);
println!("{}", "-".repeat(w_name + w_type + w_origin + 16));
for r in &rows {
println!(
"{:<w_name$} {:<w_type$} {:<w_origin$} {}",
r.name,
r.type_str,
r.origin,
r.payload,
w_name = w_name,
w_type = w_type,
w_origin = w_origin,
);
}
println!("\n{} asset(s) in {}", rows.len(), json_path);
Ok(())
}
fn list_expanded(source: WorldSource<'_>, json_path: &str) -> std::io::Result<()> {
let loaded = concinnity_cook::prepare_world(source, crate::project::assets_dir().as_deref())
.map_err(|errs| crate::authoring::report_validation_errors(&errs))?;
if loaded.assets.is_empty() {
println!("{} expands to no assets.", json_path);
return Ok(());
}
let rows: Vec<(String, String, String)> = loaded
.assets
.iter()
.map(|a| {
(
a.id.clone(),
a.asset_type.as_str().to_string(),
provenance(&loaded, &a.id),
)
})
.collect();
let w_name = rows.iter().map(|r| r.0.len()).max().unwrap_or(4).max(4);
let w_type = rows.iter().map(|r| r.1.len()).max().unwrap_or(4).max(4);
println!(
"{:<w_name$} {:<w_type$} PROVENANCE",
"ID",
"TYPE",
w_name = w_name,
w_type = w_type,
);
println!("{}", "-".repeat(w_name + w_type + 14));
for (name, type_str, prov) in &rows {
println!(
"{:<w_name$} {:<w_type$} {}",
name,
type_str,
prov,
w_name = w_name,
w_type = w_type,
);
}
let injected = loaded.injected.len();
println!(
"\n{} asset(s) after expansion ({} injected) in {}",
rows.len(),
injected,
json_path
);
println!("Use `cn explain <id>` to print an entry for overriding.");
Ok(())
}
fn list_systems(source: WorldSource<'_>, json_path: &str) -> std::io::Result<()> {
let mut world = crate::authoring::build_world_from_str(source)?;
complete(&mut world)?;
let lines = manifest_lines(&mut world);
if lines.is_empty() {
println!("{} runs no systems.", json_path);
return Ok(());
}
println!("{} runs {} system(s), in order:", json_path, lines.len());
for line in &lines {
println!(" {}", line);
}
Ok(())
}
fn complete(world: &mut concinnity_core::ecs::World) -> std::io::Result<()> {
let Some(complete) = concinnity_engine::ecs::SYSTEMS.complete_world else {
return Ok(());
};
complete(&mut world.context())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
}
fn manifest_lines(world: &mut concinnity_core::ecs::World) -> Vec<String> {
let mode = concinnity_engine::ecs::render_mode::from_content(world);
world.insert_resource(mode);
let manifest = world.system_manifest(concinnity_engine::ecs::SYSTEMS);
let width = manifest.iter().map(|n| n.len()).max().unwrap_or(0);
let phase_width = concinnity_core::ecs::Phase::ALL
.iter()
.map(|p| p.as_str().len())
.max()
.unwrap_or(0);
manifest
.iter()
.map(|name| {
let entry = concinnity_engine::ecs::SYSTEMS
.entries
.iter()
.find(|e| e.name == *name);
let phase = entry.map(|e| e.phase.as_str()).unwrap_or("");
let reason = entry.map(|e| e.present_when).unwrap_or("");
format!("{name:<width$} {phase:<phase_width$} {reason}")
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn write_world(content: &str) -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("world.jsonl");
std::fs::write(&path, content).unwrap();
(dir, path.to_string_lossy().into_owned())
}
#[test]
fn manifest_lines_report_the_world_schedule_with_reasons() {
let mut world = crate::authoring::build_world_from_str(
"[\"GraphicsConfig\",{\"$id\":\"gfx\"}]\n\
[\"Camera3D\",{\"$id\":\"cam\",\"controller\":{\"free_fly\":true}}]\n",
)
.unwrap();
let lines = manifest_lines(&mut world);
let joined = lines.join("\n");
assert!(joined.contains("GraphicsSystem"), "{joined}");
assert!(joined.contains("Camera3DSystem"), "{joined}");
assert!(joined.contains("the world runs with a window"), "{joined}");
assert!(joined.contains("PreRender"), "{joined}");
assert!(joined.contains("Late"), "{joined}");
}
#[test]
fn the_manifest_reports_systems_the_engine_defaults_turn_on() {
let mut world =
crate::authoring::build_world_from_str("[\"GraphicsConfig\",{\"$id\":\"gfx\"}]\n")
.unwrap();
assert!(
!manifest_lines(&mut world).join("\n").contains("DebugHud"),
"the world declares no DebugHud of its own"
);
complete(&mut world).unwrap();
assert!(
manifest_lines(&mut world).join("\n").contains("DebugHud"),
"the injected debug HUD brings its system with it"
);
}
#[test]
fn list_with_systems_flag_is_ok() {
let (_dir, path) = write_world("[\"GraphicsConfig\",{\"$id\":\"gfx\"}]\n");
list(Some(&path), false, true).unwrap();
}
fn loaded_world_fixture() -> concinnity_cook::build_only::LoadedWorld {
concinnity_cook::build_only::LoadedWorld {
assets: Vec::new(),
injected: vec![concinnity_cook::build_only::InjectedAsset {
name: "debug_hud".to_string(),
asset_type: "DebugHud".to_string(),
args: serde_json::json!({}),
injected_by: "debug_hud",
}],
generated: vec![concinnity_cook::build_only::GeneratedAsset {
name: "bistro_mat_wood".to_string(),
asset_type: "Material".to_string(),
generated_by: "bistro".to_string(),
}],
shadowed: vec![concinnity_cook::build_only::ShadowedAsset {
name: "bistro_mat_glass".to_string(),
asset_type: "Material".to_string(),
generated_by: "bistro".to_string(),
args: serde_json::json!({}),
}],
authored: vec!["cam".to_string(), "bistro_mat_glass".to_string()],
}
}
#[test]
fn provenance_reports_authored_names() {
assert_eq!(provenance(&loaded_world_fixture(), "cam"), "authored");
}
#[test]
fn provenance_reports_the_injection_pass() {
assert_eq!(
provenance(&loaded_world_fixture(), "debug_hud"),
"injected:debug_hud"
);
}
#[test]
fn provenance_reports_the_generating_import() {
assert_eq!(
provenance(&loaded_world_fixture(), "bistro_mat_wood"),
"generated:bistro"
);
}
#[test]
fn provenance_reports_what_an_authored_copy_shadows() {
assert_eq!(
provenance(&loaded_world_fixture(), "bistro_mat_glass"),
"authored (shadows bistro)"
);
}
#[test]
fn provenance_falls_back_to_expanded() {
assert_eq!(provenance(&loaded_world_fixture(), "anything"), "expanded");
}
#[test]
fn list_of_an_empty_world_is_ok() {
let (_dir, path) = write_world("");
list(Some(&path), false, false).unwrap();
}
#[test]
fn list_prints_known_and_unknown_types() {
let (_dir, path) = write_world(concat!(
"[\"GraphicsConfig\",{\"$id\":\"gfx\"}]\n",
"[\"AudioClip\",{\"$id\":\"clip\"}]\n",
"[\"NotARealAssetType\",{\"$id\":\"odd\"}]\n",
"[\"GraphicsConfig\",{}]\n",
));
list(Some(&path), false, false).unwrap();
}
#[test]
fn registration_for_resolves_component_types() {
let r = registration_for("GraphicsConfig").unwrap();
assert_eq!(r.type_name, "GraphicsConfig");
}
#[test]
fn registration_for_resolves_resource_assets() {
for name in ["AudioClip", "Texture"] {
assert!(RegisteredType::parse(name).is_some_and(|t| t.is_resource()));
let r = registration_for(name).unwrap();
assert_eq!(
r.origin,
concinnity_cook::authoring::registry::AssetOrigin::External
);
assert_eq!(
r.payload,
concinnity_cook::authoring::registry::AssetPayload::Compiled
);
}
}
#[test]
fn registration_for_unknown_type_is_none() {
assert!(registration_for("NotARealAssetType").is_none());
}
#[test]
fn list_of_a_missing_explicit_world_is_not_found() {
let dir = tempfile::tempdir().unwrap();
let path = dir
.path()
.join("absent.jsonl")
.to_string_lossy()
.into_owned();
let err = list(Some(&path), false, false).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn list_with_invalid_json_errors() {
let (_dir, path) = write_world("{ not json\n");
let err = list(Some(&path), false, false).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn list_expanded_runs_the_build_front_half() {
let (_dir, path) = write_world("[\"GraphicsConfig\",{\"$id\":\"gfx\"}]\n");
list(Some(&path), true, false).unwrap();
}
#[test]
fn list_expanded_rejects_an_unknown_asset_type() {
let (_dir, path) = write_world("[\"NotARealAssetType\",{\"$id\":\"odd\"}]\n");
assert!(list(Some(&path), true, false).is_err());
}
#[test]
fn list_of_an_unreadable_world_surfaces_the_read_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().to_string_lossy().into_owned();
assert!(list(Some(&path), false, false).is_err());
}
#[test]
fn list_expanded_of_an_empty_world_is_ok() {
let (_dir, path) = write_world("");
list(Some(&path), true, false).unwrap();
}
#[test]
fn list_systems_of_an_empty_world_is_ok() {
let (_dir, path) = write_world("");
list(Some(&path), false, true).unwrap();
}
#[test]
fn manifest_lines_of_an_empty_world_are_empty() {
let mut world = crate::authoring::build_world_from_str("").unwrap();
assert!(manifest_lines(&mut world).is_empty());
}
}