nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::world::World;
use crate::mcp::McpResponse;

fn spawn_mesh_by_type(world: &mut World, mesh_type: &str) -> Option<freecs::Entity> {
    match mesh_type.to_lowercase().as_str() {
        "cube" => Some(crate::ecs::world::spawn_cube_at(
            world,
            nalgebra_glm::Vec3::zeros(),
        )),
        "sphere" => Some(crate::ecs::world::spawn_sphere_at(
            world,
            nalgebra_glm::Vec3::zeros(),
        )),
        "cylinder" => Some(crate::ecs::world::spawn_cylinder_at(
            world,
            nalgebra_glm::Vec3::zeros(),
        )),
        "cone" => Some(crate::ecs::world::spawn_cone_at(
            world,
            nalgebra_glm::Vec3::zeros(),
        )),
        "plane" => Some(crate::ecs::world::spawn_plane_at(
            world,
            nalgebra_glm::Vec3::zeros(),
        )),
        "torus" => Some(crate::ecs::world::spawn_torus_at(
            world,
            nalgebra_glm::Vec3::zeros(),
        )),
        _ => None,
    }
}

pub(crate) fn mcp_spawn_entity(
    world: &mut World,
    request: crate::mcp::SpawnEntityRequest,
) -> McpResponse {
    let crate::mcp::SpawnEntityRequest {
        name,
        mesh,
        position,
        scale,
        color,
        emissive,
        parent: parent_name,
        alpha: alpha_mode,
    } = request;
    let scale = scale.unwrap_or([1.0, 1.0, 1.0]);
    use crate::ecs::transform::components::Parent;
    use crate::ecs::world::{GLOBAL_TRANSFORM, LOCAL_TRANSFORM, NAME, PARENT};

    if world.resources.entity_names.contains_key(&name) {
        return McpResponse::Error(format!("Entity '{}' already exists", name));
    }

    let parent_entity = if let Some(ref pname) = parent_name {
        let Some(&parent) = world.resources.entity_names.get(pname) else {
            return McpResponse::Error(format!("Parent entity '{}' not found", pname));
        };
        Some(parent)
    } else {
        None
    };

    let pivot = world.spawn_entities(LOCAL_TRANSFORM | GLOBAL_TRANSFORM | PARENT, 1)[0];
    if let Some(transform) = world.core.get_local_transform_mut(pivot) {
        transform.translation = nalgebra_glm::Vec3::new(position[0], position[1], position[2]);
    }
    crate::ecs::transform::commands::mark_local_transform_dirty(world, pivot);

    if let Some(parent) = parent_entity {
        world.update_parent(pivot, Some(Parent(Some(parent))));
    }

    let Some(entity) = spawn_mesh_by_type(world, &mesh) else {
        crate::ecs::world::commands::despawn_recursive_immediate(world, pivot);
        return McpResponse::Error(format!(
            "Unknown mesh type '{}'. Valid types: Cube, Sphere, Cylinder, Cone, Plane, Torus",
            mesh
        ));
    };

    world.update_parent(entity, Some(Parent(Some(pivot))));

    if scale != [1.0, 1.0, 1.0] {
        if let Some(transform) = world.core.get_local_transform_mut(pivot) {
            transform.scale = nalgebra_glm::Vec3::new(scale[0], scale[1], scale[2]);
        }
        crate::ecs::transform::commands::mark_local_transform_dirty(world, pivot);
    }

    let material_name = name.clone();
    let mut material = crate::ecs::material::components::Material::default();
    if let Some(c) = color {
        material.base_color = c;
    }
    if let Some(e) = emissive {
        material.emissive_factor = e;
    }
    if let Some(ref alpha) = alpha_mode {
        material.alpha_mode = match alpha.to_lowercase().as_str() {
            "opaque" => crate::ecs::material::components::AlphaMode::Opaque,
            "mask" => crate::ecs::material::components::AlphaMode::Mask,
            "blend" => crate::ecs::material::components::AlphaMode::Blend,
            _ => crate::ecs::material::components::AlphaMode::Opaque,
        };
    }
    crate::ecs::material::resources::material_registry_insert(
        &mut world.resources.material_registry,
        material_name.clone(),
        material,
    );
    if let Some(&index) = world
        .resources
        .material_registry
        .registry
        .name_to_index
        .get(&material_name)
    {
        world
            .resources
            .material_registry
            .registry
            .add_reference(index);
    }
    world.core.set_material_ref(
        entity,
        crate::ecs::material::components::MaterialRef::new(material_name),
    );

    world.core.add_components(pivot, NAME);
    world
        .core
        .set_name(pivot, crate::ecs::name::components::Name(name.clone()));
    world.resources.entity_names.insert(name.clone(), pivot);

    let parent_info = parent_name
        .map(|p| format!(" (child of '{}')", p))
        .unwrap_or_default();
    McpResponse::Success(format!(
        "Spawned entity '{}' with {} mesh at {:?}{}",
        name, mesh, position, parent_info
    ))
}

pub(crate) fn mcp_despawn_entity(world: &mut World, name: String) -> McpResponse {
    if let Some(entity) = world.resources.entity_names.remove(&name) {
        crate::ecs::world::commands::despawn_recursive_immediate(world, entity);
        McpResponse::Success(format!("Despawned entity '{}'", name))
    } else {
        McpResponse::Error(format!("Entity '{}' not found", name))
    }
}

pub(crate) fn mcp_query_entity(
    world: &World,
    request: crate::mcp::QueryEntityRequest,
) -> Result<McpResponse, McpResponse> {
    let entity = super::resolve_entity(world, &request.name)?;

    let Some(transform) = world.core.get_local_transform(entity) else {
        return Err(McpResponse::Error(format!(
            "Entity '{}' has no transform",
            request.name
        )));
    };

    let tags = world
        .resources
        .entity_tags
        .get(&entity)
        .cloned()
        .unwrap_or_default();

    let metadata = world
        .resources
        .entity_metadata
        .get(&entity)
        .map(|map| {
            map.iter()
                .map(|(key, value)| (key.clone(), metadata_value_to_json(value)))
                .collect()
        })
        .unwrap_or_default();

    Ok(McpResponse::EntityInfo(Box::new(
        crate::mcp::EntityInfoData {
            name: request.name,
            position: [
                transform.translation.x,
                transform.translation.y,
                transform.translation.z,
            ],
            rotation: [
                transform.rotation.i,
                transform.rotation.j,
                transform.rotation.k,
                transform.rotation.w,
            ],
            scale: [transform.scale.x, transform.scale.y, transform.scale.z],
            tags,
            metadata,
        },
    )))
}

fn metadata_value_to_json(value: &crate::ecs::scene::MetadataValue) -> serde_json::Value {
    match value {
        crate::ecs::scene::MetadataValue::Bool(v) => serde_json::Value::Bool(*v),
        crate::ecs::scene::MetadataValue::Integer(v) => serde_json::json!(*v),
        crate::ecs::scene::MetadataValue::Float(v) => serde_json::json!(*v),
        crate::ecs::scene::MetadataValue::String(v) => serde_json::Value::String(v.clone()),
        crate::ecs::scene::MetadataValue::Array(arr) => {
            serde_json::Value::Array(arr.iter().map(metadata_value_to_json).collect())
        }
        crate::ecs::scene::MetadataValue::Map(map) => {
            let obj: serde_json::Map<String, serde_json::Value> = map
                .iter()
                .map(|(k, v)| (k.clone(), metadata_value_to_json(v)))
                .collect();
            serde_json::Value::Object(obj)
        }
    }
}

fn json_to_metadata_value(value: &serde_json::Value) -> crate::ecs::scene::MetadataValue {
    match value {
        serde_json::Value::Bool(v) => crate::ecs::scene::MetadataValue::Bool(*v),
        serde_json::Value::Number(n) => {
            if let Some(integer) = n.as_i64() {
                crate::ecs::scene::MetadataValue::Integer(integer)
            } else {
                crate::ecs::scene::MetadataValue::Float(n.as_f64().unwrap_or(0.0))
            }
        }
        serde_json::Value::String(s) => crate::ecs::scene::MetadataValue::String(s.clone()),
        serde_json::Value::Array(arr) => crate::ecs::scene::MetadataValue::Array(
            arr.iter().map(json_to_metadata_value).collect(),
        ),
        serde_json::Value::Object(map) => {
            let converted: std::collections::HashMap<String, crate::ecs::scene::MetadataValue> =
                map.iter()
                    .map(|(k, v)| (k.clone(), json_to_metadata_value(v)))
                    .collect();
            crate::ecs::scene::MetadataValue::Map(converted)
        }
        serde_json::Value::Null => crate::ecs::scene::MetadataValue::String(String::new()),
    }
}

pub(crate) fn mcp_set_entity_tags(
    world: &mut World,
    request: crate::mcp::SetEntityTagsRequest,
) -> Result<McpResponse, McpResponse> {
    let entity = super::resolve_entity(world, &request.name)?;

    if request.tags.is_empty() {
        world.resources.entity_tags.remove(&entity);
    } else {
        world
            .resources
            .entity_tags
            .insert(entity, request.tags.clone());
    }

    Ok(McpResponse::Success(format!(
        "Set {} tags on '{}'",
        request.tags.len(),
        request.name
    )))
}

pub(crate) fn mcp_set_entity_metadata(
    world: &mut World,
    request: crate::mcp::SetEntityMetadataRequest,
) -> Result<McpResponse, McpResponse> {
    let entity = super::resolve_entity(world, &request.name)?;

    if request.metadata.is_empty() {
        world.resources.entity_metadata.remove(&entity);
    } else {
        let converted: std::collections::HashMap<String, crate::ecs::scene::MetadataValue> =
            request
                .metadata
                .iter()
                .map(|(key, value)| (key.clone(), json_to_metadata_value(value)))
                .collect();
        world.resources.entity_metadata.insert(entity, converted);
    }

    Ok(McpResponse::Success(format!(
        "Set {} metadata entries on '{}'",
        request.metadata.len(),
        request.name
    )))
}

pub(crate) fn mcp_rename_entity(
    world: &mut World,
    request: crate::mcp::RenameEntityRequest,
) -> Result<McpResponse, McpResponse> {
    if world.resources.entity_names.contains_key(&request.new_name) {
        return Err(McpResponse::Error(format!(
            "Entity '{}' already exists",
            request.new_name
        )));
    }

    let entity = world
        .resources
        .entity_names
        .remove(&request.name)
        .ok_or_else(|| McpResponse::Error(format!("Entity '{}' not found", request.name)))?;

    world
        .resources
        .entity_names
        .insert(request.new_name.clone(), entity);
    world.core.set_name(
        entity,
        crate::ecs::name::components::Name(request.new_name.clone()),
    );

    Ok(McpResponse::Success(format!(
        "Renamed '{}' to '{}'",
        request.name, request.new_name
    )))
}

pub(crate) fn mcp_clear_scene(world: &mut World) -> McpResponse {
    let entities_to_despawn: Vec<_> = world.resources.entity_names.values().copied().collect();
    let count = entities_to_despawn.len();
    for entity in entities_to_despawn {
        crate::ecs::world::commands::despawn_recursive_immediate(world, entity);
    }
    world.resources.entity_names.clear();
    McpResponse::Success(format!("Cleared {} entities from scene", count))
}

pub(crate) fn mcp_set_visibility(
    world: &mut World,
    request: crate::mcp::SetVisibilityRequest,
) -> Result<McpResponse, McpResponse> {
    use crate::ecs::visibility::components::Visibility;

    let entity = super::resolve_entity(world, &request.name)?;

    world.core.set_visibility(
        entity,
        Visibility {
            visible: request.visible,
        },
    );
    Ok(McpResponse::Success(format!(
        "Visibility set to {} for '{}'",
        request.visible, request.name
    )))
}