use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_cook::authoring::world::{find_world_jsonl, parse_world_jsonl, resolve_includes};
fn registration_for(type_str: &str) -> Option<concinnity_cook::authoring::registry::Registration> {
RegisteredType::parse(type_str).map(RegisteredType::registration)
}
pub(crate) fn resolve_world_path(json_path: Option<&str>) -> std::io::Result<String> {
match json_path {
Some(p) if std::path::Path::new(p).exists() => Ok(p.to_string()),
_ => find_world_jsonl(None),
}
}
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
})?;
if systems {
return list_systems(&content, &json_path);
}
if expanded {
return list_expanded(&content, &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)?;
if raw.is_empty() {
println!("{} has no assets.", json_path);
return Ok(());
}
struct Row {
name: String,
type_str: String,
origin: String,
payload: String,
}
let rows: Vec<Row> = raw
.iter()
.map(|v| {
let name = v
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("(unnamed)")
.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",
"NAME",
"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(content: &str, json_path: &str) -> std::io::Result<()> {
let loaded = concinnity_cook::prepare_world(
content,
concinnity_cook::paths::assets_dir().as_deref(),
crate::cook_platform(),
)
.map_err(|errs| concinnity_cook::check::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.name.clone(),
a.asset_type.clone(),
provenance(&loaded, &a.name),
)
})
.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",
"NAME",
"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 <name>` to print an entry for overriding.");
Ok(())
}
fn list_systems(content: &str, json_path: &str) -> std::io::Result<()> {
let mut world = crate::build_world_from_str(content)?;
complete(&mut world)?;
let lines = manifest_lines(&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_engine::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: &concinnity_engine::ecs::World) -> Vec<String> {
let manifest = world.system_manifest(concinnity_engine::ecs::SYSTEMS);
let width = manifest.iter().map(|n| n.len()).max().unwrap_or(0);
manifest
.iter()
.map(|name| {
let reason = concinnity_engine::ecs::SYSTEMS
.entries
.iter()
.find(|e| e.name == *name)
.map(|e| e.present_when)
.unwrap_or("");
format!("{name:<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 resolve_world_path_prefers_an_explicit_existing_path() {
let (_dir, path) = write_world("");
assert_eq!(resolve_world_path(Some(&path)).unwrap(), path);
}
#[test]
fn manifest_lines_report_the_world_schedule_with_reasons() {
let world = crate::build_world_from_str(
"{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n\
{\"name\":\"cam\",\"type\":\"Camera3D\",\"args\":{\"controller\":{\"free_fly\":true}}}\n",
)
.unwrap();
let lines = manifest_lines(&world);
let joined = lines.join("\n");
assert!(joined.contains("GraphicsSystem"), "{joined}");
assert!(joined.contains("Camera3DSystem"), "{joined}");
assert!(joined.contains("GraphicsConfig"), "{joined}");
}
#[test]
fn the_manifest_reports_systems_the_engine_defaults_turn_on() {
let mut world = crate::build_world_from_str(
"{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
)
.unwrap();
assert!(
!manifest_lines(&world).join("\n").contains("DebugHud"),
"the world declares no DebugHud of its own"
);
complete(&mut world).unwrap();
assert!(
manifest_lines(&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("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\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!(
"{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
"{\"name\":\"clip\",\"type\":\"AudioClip\",\"args\":{}}\n",
"{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n",
"{\"type\":\"GraphicsConfig\",\"args\":{}}\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_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("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
list(Some(&path), true, false).unwrap();
}
#[test]
fn list_expanded_rejects_an_unknown_asset_type() {
let (_dir, path) =
write_world("{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\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 world = crate::build_world_from_str("").unwrap();
assert!(manifest_lines(&world).is_empty());
}
}