use crate::ecs::World;
use crate::ecs::asset_id::AssetId;
use concinnity_cook::authoring::refs::referenced_names;
use concinnity_cook::authoring::registry::{self, RegisteredType};
use concinnity_cook::authoring::world::WorldJsonlAsset;
use concinnity_core::blob::{AssetKind, BlobAssetDef};
use concinnity_core::ecs::ComponentAsset;
use serde_json::{Map, Value};
use super::Apply;
pub(super) fn plan(
world: &World,
ct: RegisteredType,
name: &str,
before: &Map<String, Value>,
args: &Map<String, Value>,
) -> Option<Apply> {
if !ct.live() || changes_references(ct, name, before, args) {
return None;
}
let id = crate::ecs::asset_id::lookup(name)?;
let entity = world
.resource::<concinnity_core::ecs::EntityByName>()?
.get(id)?;
let asset = bake(ct, id, args).ok()?;
Some(Apply::Component {
entity,
asset: Box::new(asset),
})
}
fn changes_references(
ct: RegisteredType,
name: &str,
before: &Map<String, Value>,
after: &Map<String, Value>,
) -> bool {
names(ct, name, before) != names(ct, name, after)
}
fn names(ct: RegisteredType, name: &str, args: &Map<String, Value>) -> Vec<String> {
referenced_names(&WorldJsonlAsset {
name: name.to_string(),
asset_type: ct.as_str().to_string(),
args: Value::Object(args.clone()),
})
}
pub(super) fn bake(
ct: RegisteredType,
id: AssetId,
args: &Map<String, Value>,
) -> Result<ComponentAsset, concinnity_core::result::CnResult> {
let value = Value::Object(args.clone());
let args_bytes = match registry::bake_divergent(ct, &value)? {
Some(bytes) => bytes,
None => ct.reserialize_args(&value, crate::cook_platform())?,
};
let def = BlobAssetDef {
name: Some(id),
kind: AssetKind::Component,
discriminant: ct
.discriminant()
.ok_or(concinnity_core::result::CnResult::AssetInvalidType)?,
args_bytes,
payload: None,
};
ComponentAsset::from_baked(&def)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn args(v: Value) -> Map<String, Value> {
v.as_object().cloned().unwrap()
}
fn world_with(ct: RegisteredType, name: &str) -> World {
let mut world = World::new();
let id = crate::ecs::asset_id::intern(name);
let entity = world.add(bake(ct, id, &Map::new()).expect("default args bake"));
let mut by_name = std::collections::BTreeMap::new();
by_name.insert(id, entity);
world.insert_resource(concinnity_core::ecs::EntityByName(by_name));
world
}
#[test]
fn a_live_type_bakes_its_component() {
let id = crate::ecs::asset_id::intern("hud_dot");
let asset = bake(
RegisteredType::Sprite,
id,
&args(json!({ "width": 8.0, "height": 4.0 })),
)
.expect("Sprite args bake");
let ComponentAsset::Sprite(sprite) = asset else {
panic!("baked the wrong variant");
};
assert_eq!((sprite.width, sprite.height), (8.0, 4.0));
assert_eq!(sprite.asset_id, id, "the asset keeps its identity");
}
#[test]
fn a_divergent_type_bakes_through_its_translation() {
let id = crate::ecs::asset_id::intern("cam");
let asset = bake(
RegisteredType::Camera3D,
id,
&args(json!({ "position": [0.0, 2.0, 5.0] })),
)
.expect("Camera3D args bake");
let ComponentAsset::Camera3D(camera) = asset else {
panic!("baked the wrong variant");
};
assert_eq!(camera.position, [0.0, 2.0, 5.0]);
}
#[test]
fn only_live_types_are_eligible() {
let world = world_with(RegisteredType::Sprite, "solo");
assert!(
plan(
&world,
RegisteredType::Sprite,
"solo",
&args(json!({ "width": 1.0 })),
&args(json!({ "width": 2.0 })),
)
.is_some(),
"Sprite is live-readable"
);
assert!(
plan(
&world,
RegisteredType::PointLight,
"solo",
&args(json!({ "intensity": 1.0 })),
&args(json!({ "intensity": 2.0 })),
)
.is_none(),
"a light's data is built once at load"
);
}
#[test]
fn a_changed_reference_declines() {
let world = world_with(RegisteredType::Sprite, "badge");
assert!(
plan(
&world,
RegisteredType::Sprite,
"badge",
&args(json!({ "texture": "gold", "width": 2.0 })),
&args(json!({ "texture": "silver", "width": 2.0 })),
)
.is_none()
);
assert!(
plan(
&world,
RegisteredType::Sprite,
"badge",
&args(json!({ "texture": "gold", "width": 1.0 })),
&args(json!({ "texture": "gold", "width": 2.0 })),
)
.is_some(),
"the reference stayed put, so the size applies live"
);
}
#[test]
fn a_changed_structured_reference_declines() {
let follow = |target: &str, distance: f64| {
args(json!({
"controller": { "follow": { "target": target, "distance": distance } },
}))
};
let world = world_with(RegisteredType::Camera3D, "eye");
assert!(
plan(
&world,
RegisteredType::Camera3D,
"eye",
&follow("hero", 4.0),
&follow("villain", 4.0),
)
.is_none()
);
assert!(
plan(
&world,
RegisteredType::Camera3D,
"eye",
&follow("hero", 4.0),
&follow("hero", 6.0),
)
.is_some(),
"the orbit distance is not a reference"
);
}
fn spawning(template: &str, count: i64) -> Map<String, Value> {
args(json!({
"on": "tick",
"do": [{ "repeat": { "times": count, "do": [
{ "spawn": { "template": template } },
] } }],
}))
}
fn behavior_moves(before: &Map<String, Value>, after: &Map<String, Value>) -> bool {
changes_references(RegisteredType::Behavior, "spawner", before, after)
}
#[test]
fn behavior_logic_between_the_names_applies_live() {
assert!(!behavior_moves(
&spawning("crate", 1),
&spawning("crate", 4)
));
}
#[test]
fn a_retargeted_behavior_reference_declines() {
assert!(behavior_moves(
&spawning("crate", 1),
&spawning("barrel", 1)
));
}
#[test]
fn a_behavior_reference_the_edit_added_declines() {
let before = args(json!({ "on": "tick", "do": [] }));
assert!(behavior_moves(&before, &spawning("crate", 1)));
}
#[test]
fn a_retargeted_behavior_source_declines() {
let volume = |name: &str| args(json!({ "on": { "enter": name }, "do": [] }));
assert!(behavior_moves(&volume("porch"), &volume("hallway")));
assert!(!behavior_moves(&volume("porch"), &volume("porch")));
}
}