use crate::ecs::generational_registry::registry_entry_by_name;
use crate::ecs::prefab::resources::mesh_cache_insert;
use crate::ecs::prefab::{import_gltf_from_path, spawn_prefab};
use crate::ecs::transform::components::LocalTransformDirty;
use crate::ecs::world::commands::{
set_material_with_textures, spawn_cone_at, spawn_cube_at, spawn_cylinder_at, spawn_plane_at,
spawn_sphere_at,
};
use crate::ecs::world::{World, WorldCommand};
use crate::plugin::{EngineCommand, EngineEvent, Primitive};
use crate::render::wgpu::texture_cache::texture_cache_add_reference;
use nalgebra_glm::{Quat, Vec3};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use super::async_results::AsyncResult;
use super::entity_mapping::EntityMapping;
use super::events::{PluginInstance, send_engine_event_to_plugin};
pub struct CommandContext<'a> {
pub plugins: &'a mut [PluginInstance],
pub plugin_id_to_index: &'a std::collections::HashMap<u64, usize>,
pub entity_mapping: &'a mut EntityMapping,
pub texture_id_to_name: &'a mut std::collections::HashMap<u64, String>,
pub async_sender: &'a Sender<AsyncResult>,
pub next_texture_id: &'a mut u64,
pub base_path: PathBuf,
pub canonical_base_path: PathBuf,
}
pub fn process_engine_commands(
world: &mut World,
context: &mut CommandContext<'_>,
commands: Vec<(u64, EngineCommand)>,
) {
for (plugin_id, command) in commands {
match command {
EngineCommand::Log { message } => {
tracing::info!("[Plugin {}] {}", plugin_id, message);
}
EngineCommand::SpawnPrimitive {
primitive,
x,
y,
z,
request_id,
} => {
let pos = Vec3::new(x, y, z);
let entity = match primitive {
Primitive::Cube => spawn_cube_at(world, pos),
Primitive::Sphere => spawn_sphere_at(world, pos),
Primitive::Cylinder => spawn_cylinder_at(world, pos),
Primitive::Plane => spawn_plane_at(world, pos),
Primitive::Cone => spawn_cone_at(world, pos),
};
let entity_id = context.entity_mapping.register(entity);
let event = EngineEvent::EntitySpawned {
request_id,
entity_id,
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
EngineCommand::DespawnEntity { entity_id } => {
if let Some(entity) = context.entity_mapping.get(entity_id) {
world.queue_command(WorldCommand::DespawnRecursive { entity });
context.entity_mapping.unregister(entity_id);
} else {
tracing::warn!(
"Plugin {} tried to despawn invalid entity {}",
plugin_id,
entity_id
);
}
}
EngineCommand::SetEntityPosition { entity_id, x, y, z } => {
if let Some(entity) = context.entity_mapping.get(entity_id) {
if let Some(transform) = world.mutate_local_transform(entity) {
transform.translation = Vec3::new(x, y, z);
}
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
} else {
tracing::warn!(
"Plugin {} tried to set position on invalid entity {}",
plugin_id,
entity_id
);
}
}
EngineCommand::SetEntityScale { entity_id, x, y, z } => {
if let Some(entity) = context.entity_mapping.get(entity_id) {
if let Some(transform) = world.mutate_local_transform(entity) {
transform.scale = Vec3::new(x, y, z);
}
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
} else {
tracing::warn!(
"Plugin {} tried to set scale on invalid entity {}",
plugin_id,
entity_id
);
}
}
EngineCommand::SetEntityRotation {
entity_id,
x,
y,
z,
w,
} => {
if let Some(entity) = context.entity_mapping.get(entity_id) {
if let Some(transform) = world.mutate_local_transform(entity) {
transform.rotation = Quat::new(w, x, y, z);
}
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
} else {
tracing::warn!(
"Plugin {} tried to set rotation on invalid entity {}",
plugin_id,
entity_id
);
}
}
EngineCommand::GetEntityPosition {
entity_id,
request_id,
} => {
let event = match context.entity_mapping.get(entity_id) {
Some(entity) => {
if let Some(transform) = world.core.get_local_transform(entity) {
EngineEvent::EntityPosition {
request_id,
entity_id,
x: transform.translation.x,
y: transform.translation.y,
z: transform.translation.z,
}
} else {
EngineEvent::EntityNotFound {
request_id,
entity_id,
}
}
}
None => EngineEvent::EntityNotFound {
request_id,
entity_id,
},
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
EngineCommand::GetEntityScale {
entity_id,
request_id,
} => {
let event = match context.entity_mapping.get(entity_id) {
Some(entity) => {
if let Some(transform) = world.core.get_local_transform(entity) {
EngineEvent::EntityScale {
request_id,
entity_id,
x: transform.scale.x,
y: transform.scale.y,
z: transform.scale.z,
}
} else {
EngineEvent::EntityNotFound {
request_id,
entity_id,
}
}
}
None => EngineEvent::EntityNotFound {
request_id,
entity_id,
},
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
EngineCommand::GetEntityRotation {
entity_id,
request_id,
} => {
let event = match context.entity_mapping.get(entity_id) {
Some(entity) => {
if let Some(transform) = world.core.get_local_transform(entity) {
EngineEvent::EntityRotation {
request_id,
entity_id,
x: transform.rotation.i,
y: transform.rotation.j,
z: transform.rotation.k,
w: transform.rotation.w,
}
} else {
EngineEvent::EntityNotFound {
request_id,
entity_id,
}
}
}
None => EngineEvent::EntityNotFound {
request_id,
entity_id,
},
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
EngineCommand::ReadFile { path, request_id } => {
if let Some(safe_path) =
sanitize_path(&context.base_path, &context.canonical_base_path, &path)
{
let sender = context.async_sender.clone();
std::thread::spawn(move || {
let result = std::fs::read(&safe_path);
let _ = sender.send(AsyncResult::File {
plugin_id,
request_id,
result,
});
});
} else {
tracing::warn!("Plugin {} tried to read invalid path: {}", plugin_id, path);
let event = EngineEvent::FileError {
request_id,
error: "Invalid path: access denied".to_string(),
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
}
EngineCommand::LoadTexture { path, request_id } => {
if let Some(safe_path) =
sanitize_path(&context.base_path, &context.canonical_base_path, &path)
{
let sender = context.async_sender.clone();
let texture_name = path.clone();
let texture_id = *context.next_texture_id;
*context.next_texture_id += 1;
std::thread::spawn(move || {
let result = std::fs::read(&safe_path)
.map_err(|error| error.to_string())
.and_then(|data| {
image::load_from_memory(&data)
.map_err(|error| format!("Failed to decode image: {}", error))
.map(|img| {
let rgba = img.to_rgba8();
let (width, height) = rgba.dimensions();
(rgba.into_raw(), width, height)
})
});
let _ = sender.send(AsyncResult::Texture {
plugin_id,
request_id,
texture_name,
texture_id,
result,
});
});
} else {
tracing::warn!(
"Plugin {} tried to load texture from invalid path: {}",
plugin_id,
path
);
let event = EngineEvent::AssetError {
request_id,
error: "Invalid path: access denied".to_string(),
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
}
EngineCommand::LoadPrefab {
path,
x,
y,
z,
request_id,
} => {
if let Some(safe_path) =
sanitize_path(&context.base_path, &context.canonical_base_path, &path)
{
let sender = context.async_sender.clone();
let position = Vec3::new(x, y, z);
std::thread::spawn(move || {
let result =
import_gltf_from_path(&safe_path).map_err(|error| error.to_string());
let _ = sender.send(AsyncResult::Prefab {
plugin_id,
request_id,
position,
result,
});
});
} else {
tracing::warn!(
"Plugin {} tried to load prefab from invalid path: {}",
plugin_id,
path
);
let event = EngineEvent::AssetError {
request_id,
error: "Invalid path: access denied".to_string(),
};
dispatch_event_to_plugin(
context.plugins,
context.plugin_id_to_index,
plugin_id,
&event,
);
}
}
EngineCommand::SetEntityMaterial {
entity_id,
texture_id,
} => {
if let Some(entity) = context.entity_mapping.get(entity_id) {
if let Some(texture_name) = context.texture_id_to_name.get(&texture_id).cloned()
{
let material_ref = world.core.get_material_ref(entity).cloned();
if let Some(mat_ref) = material_ref {
if let Some(current_material) = registry_entry_by_name(
&world.resources.material_registry.registry,
&mat_ref.name,
)
.cloned()
{
let mut new_material = current_material;
new_material.base_texture = Some(texture_name.clone());
texture_cache_add_reference(
&mut world.resources.texture_cache,
&texture_name,
);
set_material_with_textures(world, entity, new_material);
}
} else {
tracing::warn!(
"Plugin {} tried to set material on entity {} without material",
plugin_id,
entity_id
);
}
} else {
tracing::warn!(
"Plugin {} tried to use invalid texture {}",
plugin_id,
texture_id
);
}
} else {
tracing::warn!(
"Plugin {} tried to set material on invalid entity {}",
plugin_id,
entity_id
);
}
}
EngineCommand::SetEntityColor {
entity_id,
r,
g,
b,
a,
} => {
if let Some(entity) = context.entity_mapping.get(entity_id) {
let material_ref = world.core.get_material_ref(entity).cloned();
if let Some(mat_ref) = material_ref {
if let Some(current_material) = registry_entry_by_name(
&world.resources.material_registry.registry,
&mat_ref.name,
)
.cloned()
{
let mut new_material = current_material;
new_material.base_color = [r, g, b, a];
set_material_with_textures(world, entity, new_material);
}
} else {
tracing::warn!(
"Plugin {} tried to set color on entity {} without material",
plugin_id,
entity_id
);
}
} else {
tracing::warn!(
"Plugin {} tried to set color on invalid entity {}",
plugin_id,
entity_id
);
}
}
}
}
}
pub fn process_async_results(
world: &mut World,
plugins: &mut [PluginInstance],
plugin_id_to_index: &std::collections::HashMap<u64, usize>,
entity_mapping: &mut EntityMapping,
texture_id_to_name: &mut std::collections::HashMap<u64, String>,
async_receiver: &std::sync::mpsc::Receiver<AsyncResult>,
) {
while let Ok(result) = async_receiver.try_recv() {
match result {
AsyncResult::File {
plugin_id,
request_id,
result,
} => {
let event = match result {
Ok(data) => EngineEvent::FileLoaded { request_id, data },
Err(error) => EngineEvent::FileError {
request_id,
error: error.to_string(),
},
};
dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
}
AsyncResult::Texture {
plugin_id,
request_id,
texture_name,
texture_id,
result,
} => match result {
Ok((rgba_data, width, height)) => {
world.queue_command(WorldCommand::LoadTexture {
name: texture_name.clone(),
rgba_data,
width,
height,
});
texture_id_to_name.insert(texture_id, texture_name);
let event = EngineEvent::TextureLoaded {
request_id,
texture_id,
};
dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
}
Err(error) => {
let event = EngineEvent::AssetError { request_id, error };
dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
}
},
AsyncResult::Prefab {
plugin_id,
request_id,
position,
result,
} => match result {
Ok(gltf_result) => {
for (mesh_name, mesh) in gltf_result.meshes {
mesh_cache_insert(&mut world.resources.mesh_cache, mesh_name, mesh);
}
for (texture_name, (rgba_data, width, height)) in gltf_result.textures {
world.queue_command(WorldCommand::LoadTexture {
name: texture_name,
rgba_data,
width,
height,
});
}
if let Some(prefab) = gltf_result.prefabs.first() {
let entity = spawn_prefab(world, prefab, position);
let entity_id = entity_mapping.register(entity);
let event = EngineEvent::PrefabLoaded {
request_id,
entity_id,
};
dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
} else {
let event = EngineEvent::AssetError {
request_id,
error: "No prefabs found in glTF file".to_string(),
};
dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
}
}
Err(error) => {
let event = EngineEvent::AssetError { request_id, error };
dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
}
},
}
}
}
fn dispatch_event_to_plugin(
plugins: &mut [PluginInstance],
plugin_id_to_index: &std::collections::HashMap<u64, usize>,
plugin_id: u64,
event: &EngineEvent,
) {
if let Some(&index) = plugin_id_to_index.get(&plugin_id) {
if let Some(plugin) = plugins.get_mut(index) {
send_engine_event_to_plugin(plugin, event);
}
} else {
tracing::warn!("Plugin {} no longer exists, dropping event", plugin_id);
}
}
fn sanitize_path(
base_path: &Path,
canonical_base_path: &Path,
path: &str,
) -> Option<std::path::PathBuf> {
let path = Path::new(path);
if path.is_absolute() {
return None;
}
for component in path.components() {
match component {
std::path::Component::ParentDir => return None,
std::path::Component::Prefix(_) => return None,
std::path::Component::RootDir => return None,
_ => {}
}
}
let full_path = base_path.join(path);
let canonical_full = match std::fs::canonicalize(&full_path) {
Ok(path) => path,
Err(_) => return None,
};
if !canonical_full.starts_with(canonical_base_path) {
return None;
}
Some(canonical_full)
}