use std::path::Path;
use concinnity_cook::build_only::LoadedWorld;
use concinnity_cook::pipeline::PipelineResult;
use concinnity_cook::{build_compiled, check::report_validation_errors, prepare_world};
use concinnity_engine::blob::BlobData;
use concinnity_engine::ecs::ComponentAsset;
use concinnity_cook::authoring::registry::{asset_line, set_reference};
pub use concinnity_cook::authoring::registry::Authored;
pub use concinnity_cook::authoring::registry::build_only::{
CameraShot, CharacterModel, CharacterSchema, KeyPolarity, LightRig, MainMenu, MainMenuItem,
MaterialPalette, OptionSelect, PaletteEntry, Panel, PanelSection, Prefab, PrefabEntry,
PrefabKind, ProportionGroup, SceneImport, SchemaJoint, SchemaKey, SchemaRegion,
SettingsProfile, ShapePreset, Slider, StoryImport, SynthParams, SynthesizedTarget,
};
pub use concinnity_core::components::cook::*;
use crate::World;
#[derive(Default)]
pub struct WorldBuilder {
lines: Vec<String>,
assets_dir: Option<std::path::PathBuf>,
declared: Vec<(String, &'static str)>,
error: Option<std::io::Error>,
}
pub fn world() -> WorldBuilder {
WorldBuilder::default()
}
impl WorldBuilder {
pub fn assets_in(&mut self, dir: impl Into<std::path::PathBuf>) -> &mut Self {
self.assets_dir = Some(dir.into());
self
}
pub fn assets_dir(&self) -> Option<&Path> {
self.assets_dir.as_deref()
}
pub fn add<T: Authored>(&mut self, name: impl Into<String>, value: T) -> &mut Self {
let name = name.into();
match asset_line(&name, &value) {
Ok(line) => {
self.lines.push(line);
self.declared.push((name, T::TYPE));
}
Err(e) => {
self.error.get_or_insert(e);
}
}
self
}
pub fn declared(&self) -> impl Iterator<Item = (&str, &str)> {
self.declared.iter().map(|(n, t)| (n.as_str(), *t))
}
pub fn reference(&mut self, field: &str, target: impl Into<String>) -> &mut Self {
let invalid = |msg: String| std::io::Error::new(std::io::ErrorKind::InvalidInput, msg);
let Some(line) = self.lines.pop() else {
self.error.get_or_insert(invalid(format!(
"reference(\"{field}\") before any asset was added"
)));
return self;
};
match set_reference(&line, field, &target.into()) {
Ok(patched) => self.lines.push(patched),
Err(e) => {
self.error.get_or_insert(e);
}
}
self
}
pub fn compile(&self) -> std::io::Result<World> {
let mut result = self.build()?;
let payload_sections: Vec<Option<Vec<u8>>> = std::mem::take(&mut 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);
}
concinnity_engine::resource::install_resource_tables(&mut world, &mut result.resources);
Ok(World::from_inner(world))
}
pub fn write_blob(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
let result = self.build()?;
concinnity_cook::pipeline::write_blobs_to(&result, path.as_ref())?;
Ok(())
}
fn build(&self) -> std::io::Result<PipelineResult> {
if let Some(e) = &self.error {
return Err(std::io::Error::new(e.kind(), e.to_string()));
}
concinnity_shader::install();
let assets_dir = self.assets_dir.clone();
let platform = concinnity_engine::platform::current();
let loaded: LoadedWorld =
prepare_world(&self.lines.concat(), assets_dir.as_deref(), platform)
.map_err(|errs| report_validation_errors(&errs))?;
build_compiled(loaded.assets, assets_dir.as_deref(), None, platform)
}
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_engine::components::{Camera3D, DirectionalLight};
#[test]
fn typed_builder_compiles_a_world() {
use concinnity_engine::components::DirectionalLight;
let world = world()
.add(
"sun",
DirectionalLight {
color: [1.0, 0.96, 0.86],
direction: [-0.35, 0.85, 0.35],
intensity: 2.2,
},
)
.add(
"room",
Room {
size: Some([16.0, 20.0, 5.0]),
..Default::default()
},
)
.compile()
.expect("typed specs compile");
let sun = world
.inner()
.query::<DirectionalLight>()
.next()
.expect("the sun compiled into a component");
assert_eq!(sun.intensity, 2.2);
let room = world
.inner()
.query::<concinnity_engine::components::Room>()
.next()
.expect("the room compiled into a component");
assert_eq!(room.half_width, 8.0, "size is halved by the bake");
assert!(room.locator.is_some(), "generated geometry is in the blob");
}
#[test]
fn compile_reports_validation_errors() {
let mut spec = world();
spec.add(
"orphan",
concinnity_engine::components::CharacterShape::default(),
)
.reference("target", "no_such_body");
let err = spec.compile().expect_err("an unresolved reference fails");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn an_empty_world_yields_the_injected_defaults() {
let world = world().compile().expect("an empty world compiles");
assert!(world.inner().query::<Camera3D>().next().is_none());
}
#[test]
fn a_named_reference_resolves_to_its_handle() {
use concinnity_engine::components::{Material, ProceduralMesh, Prop};
let world = world()
.add(
"floor_mat",
Material {
roughness: 0.8,
..Default::default()
},
)
.add(
"floor_mesh",
ProceduralMesh {
generator: "plane".into(),
half_width: 4.0,
half_depth: 4.0,
..Default::default()
},
)
.add("floor", Prop::default())
.reference("mesh", "floor_mesh")
.reference("material", "floor_mat")
.compile()
.expect("a named reference compiles");
let prop = world
.inner()
.query::<Prop>()
.next()
.expect("the prop compiled into a component");
assert_eq!(prop.mesh.map(|h| h.index()), Some(0));
assert_eq!(prop.material.map(|h| h.index()), Some(0));
}
#[test]
fn declared_reports_names_and_types_in_order() {
let mut spec = world();
spec.add("menu", concinnity_engine::components::Scene::default())
.add("sun", DirectionalLight::default());
let declared: Vec<_> = spec.declared().collect();
assert_eq!(declared, [("menu", "Scene"), ("sun", "DirectionalLight")]);
}
#[test]
fn a_reference_before_any_asset_is_a_compile_error() {
let err = world()
.reference("target", "hero")
.compile()
.expect_err("nothing to reference");
assert!(err.to_string().contains("before any asset"), "{err}");
}
#[test]
fn write_blob_writes_a_readable_world_at_the_named_path() {
use concinnity_engine::ecs::ComponentSlot;
let tree = concinnity_testing::TempTree::new();
let primary = tree.join("data/0");
world()
.add(
"sun",
DirectionalLight {
intensity: 3.5,
..Default::default()
},
)
.write_blob(&primary)
.expect("the world is written");
let (meta, _) = concinnity_engine::blob::read_cnb(&primary.to_string_lossy())
.expect("the written blob parses");
assert!(
meta.defs
.iter()
.any(|d| d.discriminant == DirectionalLight::DISCRIMINANT),
"the sun is in the def table"
);
}
}