mod component;
mod diff;
mod draw;
mod lighting;
mod placement;
mod shape;
use crate::components::{
CharacterCapsule, CharacterShape, DirectionalLight, GraphicsConfig, PostProcessConfig,
Transform, VolumetricFog,
};
use crate::ecs::{Entity, World};
use concinnity_core::ecs::ComponentAsset;
use concinnity_world::registry::{RegisteredType, ScopeResolution};
use serde_json::Value;
use std::collections::BTreeMap;
pub(crate) use diff::{args_changes, same_assets};
pub(crate) type ShadowBaselines = BTreeMap<String, Value>;
pub(crate) enum Apply {
Component {
entity: Entity,
asset: Box<ComponentAsset>,
},
Transform {
entity: Entity,
transform: Transform,
},
Shape {
shape: CharacterShape,
capsule: Option<CharacterCapsule>,
},
Sun {
entity: Entity,
light: DirectionalLight,
},
RenderConfig(RenderConfig),
Draw(draw::DrawChange),
}
pub(crate) enum RenderConfig {
Fog(VolumetricFog),
Graphics(GraphicsConfig),
Post(Box<PostProcessConfig>),
}
pub(crate) fn plan(
world: &World,
entries: &[Value],
changes: &[diff::ArgsChange],
) -> Option<Vec<Apply>> {
changes
.iter()
.map(|change| plan_one(world, entries, change))
.collect()
}
pub(crate) fn commit(world: &mut World, plan: Vec<Apply>) {
for apply in plan {
match apply {
Apply::Component { entity, asset } => {
world.replace_component(entity, *asset);
}
Apply::Transform { entity, transform } => {
if let Some(slot) = world.get_mut::<Transform>(entity) {
*slot = transform;
}
}
Apply::Shape { shape, capsule } => {
crate::gfx::shape_preview::apply(world, &shape, capsule.as_ref());
}
Apply::Sun { entity, light } => lighting::commit_sun(world, entity, light),
Apply::RenderConfig(config) => lighting::commit(world, config),
Apply::Draw(change) => draw::commit(world, change),
}
}
}
fn plan_one(world: &World, entries: &[Value], change: &diff::ArgsChange) -> Option<Apply> {
let ct = RegisteredType::parse(&change.ty)?;
if ct.scope_resolution() == ScopeResolution::Expanded {
return None;
}
if ct == RegisteredType::CharacterShape {
return shape::plan(world, entries, &change.args, &change.keys);
}
lighting::plan(world, ct, &change.name, &change.args, &change.keys)
.or_else(|| component::plan(world, ct, &change.name, &change.before, &change.args))
.or_else(|| placement::plan(world, &change.name, &change.args, &change.keys))
.or_else(|| draw::plan(world, ct, &change.name, &change.args, &change.keys))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::Sprite;
use serde_json::json;
fn change(name: &str, ty: &str, args: Value, keys: &[&str]) -> diff::ArgsChange {
diff::ArgsChange {
name: name.to_string(),
ty: ty.to_string(),
before: serde_json::Map::new(),
args: args.as_object().cloned().unwrap(),
keys: keys.iter().map(|k| k.to_string()).collect(),
}
}
fn world_with_sprite(name: &str) -> World {
let mut world = World::new();
let entity = world.push(Sprite::default());
let mut by_name = BTreeMap::new();
by_name.insert(crate::ecs::asset_id::intern(name), entity);
world.insert_resource(concinnity_core::ecs::EntityByName(by_name));
world
}
#[test]
fn a_live_edit_is_written_to_the_world() {
let mut world = world_with_sprite("badge");
let changes = [change(
"badge",
"Sprite",
json!({ "width": 16.0, "height": 8.0 }),
&["width"],
)];
let plan = plan(&world, &[], &changes).expect("plans");
commit(&mut world, plan);
let sprite = world.query::<Sprite>().next().unwrap();
assert_eq!((sprite.width, sprite.height), (16.0, 8.0));
}
#[test]
fn one_unplannable_change_abandons_the_batch() {
let world = world_with_sprite("badge");
let changes = [
change("badge", "Sprite", json!({ "width": 16.0 }), &["width"]),
change(
"lamp",
"PointLight",
json!({ "intensity": 3.0 }),
&["intensity"],
),
];
assert!(plan(&world, &[], &changes).is_none());
}
#[test]
fn a_moved_reference_declines_through_every_path() {
let world = world_with_sprite("spawner");
let spawning =
|template: &str| json!({ "on": "tick", "do": [{ "spawn": { "template": template } }] });
let object = |v: Value| v.as_object().cloned().unwrap();
let changes = [diff::ArgsChange {
name: "spawner".to_string(),
ty: "Behavior".to_string(),
before: object(spawning("crate")),
args: object(spawning("barrel")),
keys: vec!["do".to_string()],
}];
assert!(plan(&world, &[], &changes).is_none());
}
#[test]
fn an_unknown_type_declines() {
let world = world_with_sprite("badge");
let changes = [change("badge", "NotAType", json!({ "a": 1 }), &["a"])];
assert!(plan(&world, &[], &changes).is_none());
}
#[test]
fn a_build_only_type_declines() {
let mut world = World::new();
let e = world.push(Transform::default());
let mut by_name = BTreeMap::new();
by_name.insert(crate::ecs::asset_id::intern("hero"), e);
world.insert_resource(concinnity_core::ecs::EntityByName(by_name));
let changes = [change(
"hero",
"CharacterModel",
json!({ "position": [1.0, 0.0, 0.0] }),
&["position"],
)];
assert!(plan(&world, &[], &changes).is_none());
}
}