use std::path::Path;
use concinnity_cook::WorldSource;
use concinnity_cook::authoring::registry::RegisteredType;
pub(crate) use concinnity_cook::build_compiled;
use concinnity_cook::build_only::LoadedWorld;
use concinnity_core::ecs::{ComponentAsset, World};
use concinnity_engine::ecs::BlobMeshBounds;
use concinnity_engine::ecs::BlobSceneGroups;
use concinnity_engine::resource::ColorLutSources;
use concinnity_engine::resource::EnvironmentMapSourceInfo;
use concinnity_engine::resource::EnvironmentMapSources;
use concinnity_engine::resource::MaterialNames;
use concinnity_engine::resource::MeshSource;
use concinnity_engine::resource::MeshSources;
use concinnity_engine::resource::TextureSource;
use concinnity_engine::resource::TextureSources;
use concinnity_engine::resource::install_resource_tables;
use concinnity_host::store::blob::BlobData;
use crate::build_status::{BuildStatus, Verbosity};
pub(crate) fn prepare(source: WorldSource<'_>) -> std::io::Result<LoadedWorld> {
let loaded = concinnity_cook::prepare_world(source, crate::project::assets_dir().as_deref())
.map_err(|errs| crate::authoring::report_validation_errors(&errs))?;
Ok(loaded)
}
fn scan_color_lut_source(
assets: &[concinnity_cook::authoring::world::WorldJsonlAsset],
) -> Option<String> {
assets
.iter()
.find(|a| a.asset_type == RegisteredType::ColorLut)
.and_then(|a| a.args.get("source").and_then(|v| v.as_str()))
.filter(|s| !s.is_empty())
.map(str::to_string)
}
fn scan_environment_map_source(
assets: &[concinnity_cook::authoring::world::WorldJsonlAsset],
) -> Option<EnvironmentMapSourceInfo> {
let a = assets
.iter()
.find(|a| a.asset_type == RegisteredType::EnvironmentMap)?;
let generator = a
.args
.get("generator")
.and_then(|v| v.as_str())
.unwrap_or("");
let source = a.args.get("source").and_then(|v| v.as_str()).unwrap_or("");
if !generator.is_empty() || source.is_empty() {
return None;
}
let u32_arg = |key: &str, default: u32| {
a.args
.get(key)
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(default)
};
Some(EnvironmentMapSourceInfo {
source: source.to_string(),
prefilter_face_size: u32_arg("prefilter_face_size", 512),
irradiance_face_size: u32_arg("irradiance_face_size", 8),
prefilter_samples: u32_arg("prefilter_samples", 1024),
prefilter_clamp: a
.args
.get("prefilter_clamp")
.and_then(|v| v.as_f64())
.map(|v| v as f32)
.unwrap_or(12.0),
})
}
fn world_from_loaded(loaded: LoadedWorld) -> std::io::Result<World> {
let color_lut_source = scan_color_lut_source(&loaded.assets);
let environment_map_source = scan_environment_map_source(&loaded.assets);
let mut result = build_compiled(
loaded.assets,
crate::project::assets_dir().as_deref(),
crate::cook_platform(),
)?;
let material_names =
MaterialNames(result.resource_names(concinnity_core::blob::ResourceKind::Material));
let payload_sections: Vec<Option<Vec<u8>>> = result.payloads.into_iter().map(Some).collect();
let mut world = concinnity_engine::blob::world_from(BlobData::new(payload_sections));
for def in &result.defs {
let mut component = ComponentAsset::from_baked(def).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Asset construction failed: {e}"),
)
})?;
if let Some(locator) = &def.payload {
component.inject_locator(locator.clone());
}
world.add(component, def.name);
}
install_resource_tables(&mut world, &mut result.resources);
world.insert_resource(BlobSceneGroups(result.scene_groups));
world.insert_resource(BlobMeshBounds(result.mesh_bounds));
if let Some(budget) = result.physics_budget {
world.insert_resource(concinnity_core::ecs::WorldPhysicsBudget(budget));
}
world.insert_resource(ColorLutSources(color_lut_source));
world.insert_resource(EnvironmentMapSources(environment_map_source));
world.insert_resource(TextureSources(
result
.texture_sources
.iter()
.map(|t| TextureSource {
name_id: t.name_id,
source: t.source.clone(),
image_index: t.image_index,
})
.collect(),
));
world.insert_resource(material_names);
world.insert_resource(MeshSources(
result
.mesh_sources
.iter()
.map(|m| MeshSource {
source: m.source.clone(),
primitive_index: m.primitive_index,
lod_levels: m.lod_levels,
lod_distances: m.lod_distances.clone(),
})
.collect(),
));
Ok(world)
}
pub(crate) fn build_world_from_str<'a>(
source: impl Into<WorldSource<'a>>,
) -> std::io::Result<World> {
Ok(build_world_and_shadows(source)?.0)
}
pub(crate) fn build_world_and_shadows<'a>(
source: impl Into<WorldSource<'a>>,
) -> std::io::Result<(World, Vec<concinnity_cook::build_only::ShadowedAsset>)> {
let loaded = prepare(source.into())?;
let shadowed = loaded.shadowed.clone();
Ok((world_from_loaded(loaded)?, shadowed))
}
pub(crate) fn build_world_from_path(world_path: &str) -> std::io::Result<World> {
let content = std::fs::read_to_string(world_path)?;
build_world_from_str(WorldSource::file(&content, Path::new(world_path)))
}
pub(crate) fn build_world_file(json_path: &str) -> std::io::Result<()> {
build_world_file_as(json_path, json_path, Verbosity::Normal)
}
pub(crate) fn build_world_file_as(
json_path: &str,
shown: &str,
verbosity: Verbosity,
) -> std::io::Result<()> {
let tree = crate::project::require()?;
let content = std::fs::read_to_string(json_path)?;
let platform = crate::cook_platform();
let status = BuildStatus::start(shown, platform, verbosity);
let source = WorldSource::file(&content, Path::new(json_path));
let loaded = match concinnity_cook::prepare_world(source, Some(&tree.assets_dir())) {
Ok(loaded) => loaded,
Err(errs) => {
status.fail_validation(&errs);
return Err(crate::authoring::validation_failed(errs.len()));
}
};
status.loaded(&loaded);
let report = |p: concinnity_cook::BuildProgress<'_>| status.progress(p);
match concinnity_cook::build_loaded(&tree, loaded, platform, Some(&report)) {
Ok(built) => {
status.finish(&built);
Ok(())
}
Err(e) => {
status.fail();
Err(e)
}
}
}
pub(crate) fn build_world_str_to_disk(
content: &str,
progress: Option<concinnity_cook::ProgressFn<'_>>,
) -> std::io::Result<()> {
let loaded = prepare(content.into())?;
let result = concinnity_cook::build_compiled_with_progress(
loaded.assets,
crate::project::assets_dir().as_deref(),
crate::cook_platform(),
progress,
)?;
concinnity_cook::write_build_outputs(
&crate::project::require()?,
&result,
&loaded.injected,
&loaded.shadowed,
progress,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_host::thread::asset_id;
#[test]
fn prepare_accepts_a_valid_world() {
let loaded = prepare("[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n".into()).unwrap();
assert!(loaded.assets.iter().any(|a| a.id == "phys"));
assert!(loaded.authored.contains(&"phys".to_string()));
}
#[test]
fn prepare_rejects_an_invalid_world() {
assert!(prepare("[\"NotARealAssetType\",{\"$id\":\"odd\"}]\n".into()).is_err());
assert!(prepare("{ not json\n".into()).is_err());
}
fn asset(json: serde_json::Value) -> concinnity_cook::authoring::world::WorldJsonlAsset {
concinnity_cook::authoring::world::WorldJsonlAsset::from_value(&json).expect("typed asset")
}
#[test]
fn the_lut_scan_takes_the_first_source() {
let assets = [asset(
serde_json::json!({"type":"ColorLut","args":{"$id":"grade","source":"luts/warm.cube"}}),
)];
assert_eq!(
scan_color_lut_source(&assets),
Some("luts/warm.cube".to_string())
);
let assets = [
asset(serde_json::json!({"type":"ColorLut","args":{"$id":"a","source":"first.cube"}})),
asset(serde_json::json!({"type":"ColorLut","args":{"$id":"b","source":"second.cube"}})),
];
assert_eq!(
scan_color_lut_source(&assets),
Some("first.cube".to_string())
);
}
#[test]
fn the_lut_scan_yields_nothing_without_a_source() {
assert_eq!(scan_color_lut_source(&[]), None);
let no_source = [asset(
serde_json::json!({"type":"ColorLut","args":{"$id":"grade"}}),
)];
assert_eq!(scan_color_lut_source(&no_source), None);
let empty = [asset(
serde_json::json!({"type":"ColorLut","args":{"$id":"grade","source":""}}),
)];
assert_eq!(scan_color_lut_source(&empty), None);
let other_kind = [asset(
serde_json::json!({"type":"EnvironmentMap","args":{"$id":"sky","source":"x.hdr"}}),
)];
assert_eq!(scan_color_lut_source(&other_kind), None);
}
#[test]
fn the_environment_map_scan_defaults_the_unset_bake_inputs() {
let assets = [asset(
serde_json::json!({"type":"EnvironmentMap","args":{"$id":"sky","source":"studio.hdr"}}),
)];
let info = scan_environment_map_source(&assets).expect("a file-backed map");
assert_eq!(info.source, "studio.hdr");
assert_eq!(info.prefilter_face_size, 512);
assert_eq!(info.irradiance_face_size, 8);
assert_eq!(info.prefilter_samples, 1024);
assert_eq!(info.prefilter_clamp, 12.0);
}
#[test]
fn the_environment_map_scan_carries_the_authored_bake_inputs() {
let assets = [asset(serde_json::json!({
"type":"EnvironmentMap","args":{
"$id":"sky",
"source":"studio.hdr",
"prefilter_face_size": 256,
"irradiance_face_size": 16,
"prefilter_samples": 64,
"prefilter_clamp": 4.5
}
}))];
let info = scan_environment_map_source(&assets).expect("a file-backed map");
assert_eq!(info.prefilter_face_size, 256);
assert_eq!(info.irradiance_face_size, 16);
assert_eq!(info.prefilter_samples, 64);
assert_eq!(info.prefilter_clamp, 4.5);
}
#[test]
fn the_environment_map_scan_skips_a_procedural_map() {
let generated = [asset(serde_json::json!({
"type":"EnvironmentMap","args":{"$id":"sky","generator":"sky"}
}))];
assert!(scan_environment_map_source(&generated).is_none());
let both = [asset(serde_json::json!({
"type":"EnvironmentMap","args":{"$id":"sky","generator":"sky","source":"studio.hdr"}
}))];
assert!(scan_environment_map_source(&both).is_none());
assert!(scan_environment_map_source(&[]).is_none());
let no_source = [asset(
serde_json::json!({"type":"EnvironmentMap","args":{"$id":"sky"}}),
)];
assert!(scan_environment_map_source(&no_source).is_none());
}
#[test]
fn the_assembled_world_publishes_the_watcher_source_catalogs() {
let loaded = prepare("[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n".into()).unwrap();
let world = world_from_loaded(loaded).unwrap();
assert!(
world
.resource::<ColorLutSources>()
.is_some_and(|s| s.0.is_none()),
"a world with no LUT publishes an empty catalog, not none at all"
);
assert!(world.resource::<EnvironmentMapSources>().is_some());
}
#[test]
fn world_from_loaded_assembles_an_in_memory_world() {
let loaded = prepare("[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n".into()).unwrap();
let expanded = loaded.assets.len();
let world = world_from_loaded(loaded).unwrap();
let identified = world
.query::<concinnity_core::components::Identity>()
.count();
assert_eq!(identified, expanded);
assert_eq!(world.component_count(), 2 * expanded);
let index = world
.resource::<concinnity_core::ecs::EntityById>()
.expect("assembly identifies every entity");
assert_eq!(index.len(), expanded);
}
#[test]
fn build_world_from_str_assembles_an_in_memory_world() {
let world = build_world_from_str("[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n").unwrap();
assert!(world.component_count() >= 1);
}
#[test]
fn build_world_from_missing_path_is_not_found() {
let err = build_world_from_path("/no/such/concinnity-world-xyz.jsonl")
.expect_err("a missing world path must error");
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
struct ProjectGuard;
impl Drop for ProjectGuard {
fn drop(&mut self) {
crate::project::close();
}
}
#[test]
fn an_in_memory_build_records_its_material_identities() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let world = build_world_from_str(concat!(
"[\"Material\",{\"$id\":\"steel\",\"roughness\":0.4}]\n",
"[\"Material\",{\"$id\":\"glass\",\"transparent\":true}]\n",
))
.expect("a material-only world compiles");
let names = world
.resource::<MaterialNames>()
.expect("the catalog is installed");
assert_eq!(
names.0,
vec![asset_id::intern("steel").0, asset_id::intern("glass").0,],
"declaration order is handle order"
);
}
#[test]
fn build_world_str_to_disk_writes_blobs_and_lock() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
let build_root = dir.path().join(".concinnity");
crate::project::open(
concinnity_host::store::paths::StateTree::at(dir.path()).with_build(&build_root),
);
let _project = ProjectGuard;
let world = dir.path().join("worlds").join("world.jsonl");
std::fs::create_dir_all(world.parent().unwrap()).unwrap();
std::fs::write(&world, "[\"PhysicsConfig\",{\"$id\":\"phys\"}]\n").unwrap();
let content = std::fs::read_to_string(&world).unwrap();
build_world_str_to_disk(&content, None).expect("compile + write should succeed");
assert!(
concinnity_host::store::blob::primary_in(
&crate::project::data_dir().expect("the test opened a project")
)
.exists(),
"data/0 blob written"
);
assert!(
build_root.join("world-lock.json").exists(),
"world-lock.json written under the build root"
);
assert!(
!dir.path().join("world-lock.json").exists(),
"the lock must not land beside the authored world"
);
}
}