tirbofish/dropbear · diff
wip: overhaul on assets, with uuid linking planned.
Signature present but could not be verified.
Unverified
@@ -89,6 +89,7 @@ float-derive = "0.1" rkyv = "0.8" bevy_mikktspace = "1.0.0" naga = { version = "29", features = ["wgsl-in"] } +uuid = { version = "1.22", features = ["v4", "serde"] } [workspace.dependencies.image] version = "0.25" @@ -108,9 +108,7 @@ impl ProcedurallyGeneratedObject { let model = Model { label: label.clone(), hash, - path: ResourceReference::from_reference(crate::utils::ResourceReferenceType::ProcObj( - self.clone(), - )), + path: ResourceReference::Procedural(self.clone()), meshes: vec![mesh], materials: vec![material], skins: Vec::new(), @@ -77,171 +77,136 @@ pub fn relative_path_from_euca(uri: &str) -> anyhow::Result<&str> { Ok(stripped.strip_prefix("resources/").unwrap_or(stripped)) } -/// An enum that contains the different types that a resource reference can possibly be. +/// A reference to an asset used to identify and load it. /// -/// # Example -/// ```rust -/// use dropbear_engine::utils::{ResourceReferenceType, ResourceReference}; +/// `File` holds a path relative to the project's `resources/` directory +/// (e.g. `"models/cube.glb"`). `Embedded` carries raw bytes. +/// `Procedural` stores the procedurally generated geometry directly. /// -/// let resource_ref = ResourceReference::from_reference( -/// ResourceReferenceType::File("euca://models/cube.obj".to_string()) -/// ); -/// assert_eq!(resource_ref.as_path().unwrap(), "models/cube.obj"); -/// ``` +/// An empty `File("")` acts as a "no asset" sentinel wherever an +/// `Option<ResourceReference>` is not available (e.g. rkyv-archived structs). #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] -pub enum ResourceReferenceType { - /// The default type; Specifies there being no resource reference type. - /// Typically creates errors, so watch out! - None, - - /// A file type. The [`String`] is the reference from the project or the runtime executable. +pub enum ResourceReference { + /// A resource-root-relative file path, e.g. `"models/cube.glb"`. File(String), - - /// The content in bytes. Sometimes, there is a model that is loaded into memory through the - /// [`include_bytes!`] macro, this type stores it. - Bytes(Arc<[u8]>), - - /// Any object that can be generated at runtime instead of from a file. - ProcObj(ProcedurallyGeneratedObject), + /// Raw bytes embedded in memory (e.g. from `include_bytes!`). + Embedded(Arc<[u8]>), + /// A procedurally generated object with embedded geometry. + Procedural(ProcedurallyGeneratedObject), } -impl Default for ResourceReferenceType { +impl Default for ResourceReference { fn default() -> Self { - Self::None + // Empty path = "no asset" sentinel. + Self::File(String::new()) } } -/// A struct used to "point" to the resource relative to -/// the executable directory or the project directory. -/// -/// # Example -/// `/home/tk/project/resources/models/cube.obj` is the file path to `cube.obj`. -/// -/// The resource reference will be `models/cube.obj`. -/// -/// - If ran in the editor, it translates to `/home/tk/project/resources/models/cube.obj`. -/// -/// - In the runtime (with redback-runtime), it -/// translates to `/home/tk/Downloads/Maze/resources/models/cube.obj` -/// _(assuming the executable is at `/home/tk/Downloads/Maze/maze_runner.exe`)_. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] -pub struct ResourceReference { - pub ref_type: ResourceReferenceType, -} - impl Display for ResourceReference { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.ref_type) + match self { + Self::File(path) if path.is_empty() => write!(f, "File(<none>)"), + Self::File(path) => write!(f, "File({path})"), + Self::Embedded(bytes) => write!(f, "Embedded({} bytes)", bytes.len()), + Self::Procedural(_) => write!(f, "Procedural"), + } } } impl ResourceReference { - /// Creates an empty `ResourceReference` struct. - pub fn new() -> Self { - Self { - ref_type: ResourceReferenceType::None, - } + /// Creates a `ResourceReference::File` with a resource-root-relative path. + pub fn file(path: impl Into<String>) -> Self { + Self::File(path.into()) } - /// Creates a new `ResourceReference` from bytes + /// Creates a bytes-backed reference. pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - Self { - ref_type: ResourceReferenceType::Bytes(Arc::<[u8]>::from(bytes.as_ref())), - } + Self::Embedded(Arc::<[u8]>::from(bytes.as_ref())) } - /// Returns true when this reference points to a file path rather than embedded bytes. + /// Returns true when this reference points to a non-empty file path. pub fn is_file_backed(&self) -> bool { - matches!(self.ref_type, ResourceReferenceType::File(_)) + matches!(self, Self::File(s) if !s.is_empty()) } - pub fn from_reference(ref_type: ResourceReferenceType) -> Self { - match ref_type { - ResourceReferenceType::File(reference) => { - let canonical = canonicalize_euca_uri(&reference) - .unwrap_or_else(|err| panic!("Invalid euca URI '{}': {}", reference, err)); - Self { - ref_type: ResourceReferenceType::File(canonical), - } - } - other => Self { ref_type: other }, - } - } - - /// Creates a [`ResourceReference`] directly from an euca URI (e.g. `euca://models/cube.glb`). - pub fn from_euca_uri(uri: impl AsRef<str>) -> anyhow::Result<Self> { - let canonical = canonicalize_euca_uri(uri.as_ref())?; - Ok(Self { - ref_type: ResourceReferenceType::File(canonical), - }) - } - - /// Returns the canonical euca URI for this reference if it points to a file asset. - pub fn as_uri(&self) -> Option<&str> { - match &self.ref_type { - ResourceReferenceType::File(reference) => Some(reference.as_str()), - _ => None, - } - } - - /// Returns the resource path relative to the `resources/` directory if this reference represents a file. - pub fn relative_path(&self) -> Option<&str> { - match &self.ref_type { - ResourceReferenceType::File(reference) => relative_path_from_euca(reference).ok(), - _ => None, - } - } - - /// Creates a `ResourceReference` from a full path by extracting the part after "resources/". + /// Creates a `ResourceReference` from a full absolute path by extracting + /// the component after `resources/`. /// /// # Examples /// ``` /// use dropbear_engine::utils::ResourceReference; /// /// let path = "/home/tk/project/resources/models/cube.obj"; - /// let resource_ref = ResourceReference::from_path(path).unwrap(); - /// assert_eq!(resource_ref.as_path().unwrap(), "models/cube.obj"); + /// let r = ResourceReference::from_path(path).unwrap(); + /// assert_eq!(r.relative_path(), Some("models/cube.obj")); /// ``` - /// - /// Returns `None` if the path doesn't contain "resources" or if the path after resources is empty. pub fn from_path(full_path: impl AsRef<Path>) -> anyhow::Result<Self> { puffin::profile_function!(full_path.as_ref().display().to_string()); let path = full_path.as_ref(); let components: Vec<_> = path.components().collect(); - for (i, component) in components.iter().enumerate() { if let std::path::Component::Normal(name) = component && *name == "resources" { - let remaining_components = &components[i + 1..]; - if remaining_components.is_empty() { - anyhow::bail!("Unable to locate any remaining components"); + let remaining = &components[i + 1..]; + if remaining.is_empty() { + anyhow::bail!( + "Path has no components after 'resources/': {}", + path.display() + ); } - - let resource_path = remaining_components + let resource_path = remaining .iter() .map(|c| match c { - std::path::Component::Normal(name) => name.to_str().unwrap_or(""), + std::path::Component::Normal(n) => n.to_str().unwrap_or(""), _ => "", }) + .filter(|s| !s.is_empty()) .collect::<Vec<_>>() .join("/"); - let canonical = canonicalize_euca_uri(&format!("{EUCA_SCHEME}{resource_path}"))?; - - return Ok(Self { - ref_type: ResourceReferenceType::File(canonical), - }); + return Ok(Self::File(resource_path)); } } - anyhow::bail!("Nothing here") + anyhow::bail!( + "Path does not contain a 'resources' component: {}", + path.display() + ) + } + + /// Creates a `ResourceReference` directly from an euca URI + /// (e.g. `euca://models/cube.glb`). + /// + /// Strips the `euca://` scheme and any `resources/` prefix, storing + /// only the resource-root-relative path. + pub fn from_euca_uri(uri: impl AsRef<str>) -> anyhow::Result<Self> { + let canonical = canonicalize_euca_uri(uri.as_ref())?; + // canonical is "euca://models/cube.glb" — resources/ is already stripped. + let relative = canonical.strip_prefix(EUCA_SCHEME).unwrap_or(&canonical); + Ok(Self::File(relative.to_string())) + } + + /// Returns the resource-root-relative path for `File` references. + /// Returns `None` for empty paths and all other variants. + pub fn as_uri(&self) -> Option<&str> { + match self { + Self::File(s) if !s.is_empty() => Some(s.as_str()), + _ => None, + } + } + + /// Returns the resource path relative to the `resources/` directory. + /// Alias for [`as_uri`]. + pub fn relative_path(&self) -> Option<&str> { + self.as_uri() } + /// Returns the raw bytes for `Embedded` references. pub fn as_bytes(&self) -> Option<&[u8]> { - match &self.ref_type { - ResourceReferenceType::Bytes(bytes) => Some(bytes.as_ref()), + match self { + Self::Embedded(bytes) => Some(bytes.as_ref()), _ => None, } } @@ -272,3 +237,4 @@ where } } } + @@ -43,13 +43,14 @@ semver.workspace = true rustc_version_runtime.workspace = true rapier3d.workspace = true bytemuck.workspace = true -#yakui.workspace = true thiserror.workspace = true combine.workspace = true dyn-clone.workspace = true downcast-rs.workspace = true wgpu = {workspace = true, features = ["serde"]} rkyv.workspace = true +bitflags.workspace = true +uuid.workspace = true [features] default = [] @@ -1,4 +1,4 @@ -use crate::component::{Component, ComponentDescriptor, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::ptr::WorldPtr; use crate::scripting::native::DropbearNativeError; use crate::scripting::result::DropbearNativeResult; @@ -23,6 +23,8 @@ impl Component for AnimationComponent { type_name: "AnimationComponent".to_string(), category: Some("Animation".to_string()), description: Some("Animates a 3D MeshRenderer".to_string()), + disabled_flags: DisabilityFlags::Disabled, + internal: false, } } @@ -2,7 +2,7 @@ use std::sync::Arc; use hecs::{Entity, World}; use dropbear_engine::entity::inspect_rotation_quat; use dropbear_engine::graphics::SharedGraphicsContext; -use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::physics::PhysicsState; use egui::{CollapsingHeader, Ui}; @@ -49,6 +49,8 @@ impl Component for BillboardComponent { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "eucalyptus_core::billboard::BillboardComponent".to_string(), type_name: "Billboard".to_string(), category: Some("UI".to_string()), @@ -1,6 +1,6 @@ //! Additional information and context for cameras from the [`dropbear_engine::camera`] use crate::component::{ - Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent, + Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent, }; use crate::ptr::WorldPtr; use crate::scripting::result::DropbearNativeResult; @@ -29,6 +29,8 @@ impl Component for Camera { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "dropbear_engine::camera::Camera".to_string(), type_name: "Camera3D".to_string(), category: Some("Camera".to_string()), @@ -10,7 +10,7 @@ use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::model::Model; use dropbear_engine::procedural::{ProcObjType, ProcedurallyGeneratedObject}; use dropbear_engine::texture::{Texture, TextureBuilder, TextureReference}; -use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use dropbear_engine::utils::ResourceReference; use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, UiBuilder}; use hecs::{Entity, World}; pub use serde::{Deserialize, Serialize}; @@ -182,13 +182,25 @@ impl ComponentRegistry { }), ); + let disabled_flags = T::descriptor().disabled_flags; self.updaters.insert( type_id, - Box::new(|world, physics, dt, graphics| { + Box::new(move |world, physics, dt, graphics| { let world_ptr = world as *mut hecs::World; // safe assuming world is kept at the DropbearAppBuilder application level (lifetime) let mut query = world.query::<(hecs::Entity, &mut T)>(); for (entity, component) in query.iter() { let world_ref = unsafe { &*world_ptr }; + // skip update on DisabledFlags::Hidden + if !matches!(disabled_flags, DisabilityFlags::Never) { + if let Ok(status) = world_ref.get::<&crate::entity_status::EntityStatus>(entity) { + if status.disabled { + continue; + } + if status.hidden && matches!(disabled_flags, DisabilityFlags::Hidden) { + continue; + } + } + } component.update_component(world_ref, physics, entity, dt, graphics.clone()); } }), @@ -441,6 +453,17 @@ pub trait SerializedComponent: Downcast + dyn_clone::DynClone + Send + Sync {} impl_downcast!(SerializedComponent); dyn_clone::clone_trait_object!(SerializedComponent); +#[derive(Debug, Clone, Default)] +pub enum DisabilityFlags { + /// Skip this component's `update_component` when the entity is Disabled. + #[default] + Disabled, + /// Skip this component's `update_component` when the entity is Hidden or Disabled. + Hidden, + /// Never skip this component. Used for meta-components such as `EntityStatus`. + Never, +} + #[derive(Clone, Debug)] pub struct ComponentDescriptor { /// Fully qualified type name of the component, such as `eucalyptus_core::components::MeshRenderer`. @@ -451,6 +474,10 @@ pub struct ComponentDescriptor { pub category: Option<String>, /// Description of the component, such as `Renders a 3D model`. pub description: Option<String>, + /// Governs when this component's logic is skipped based on the entity's [`EntityStatus`]. + pub disabled_flags: DisabilityFlags, + /// Internal components are not shown in the "Add Component" picker. + pub internal: bool, } /// Defines a type that can be considered a component of an entity. @@ -518,6 +545,8 @@ impl Component for MeshRenderer { type_name: "MeshRenderer".to_string(), category: Some("Rendering".to_string()), description: Some("Renders a mesh".to_string()), + disabled_flags: DisabilityFlags::Disabled, + internal: false, } } @@ -566,39 +595,43 @@ impl Component for MeshRenderer { } } - let handle = match &ser.handle.ref_type { - ResourceReferenceType::None => { - log::debug!("ResourceReferenceType is None, setting to `Handle::NULL`"); - Handle::NULL - } - ResourceReferenceType::File(reference) => { - log::debug!("Loading model from file: {:?}", ser.handle); - load_model_from_reference( - ser.handle.clone(), - reference.clone(), - graphics.clone(), - ) - .await? - } - ResourceReferenceType::Bytes(bytes) => { - log::debug!("Loading model from bytes [Len: {}]", bytes.len()); - log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save"); - log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)"); - Model::load_from_memory_raw( - graphics.clone(), - bytes, - Some(ser.handle.clone()), - Some(ser.label.as_str()), - ASSET_REGISTRY.clone(), - ) - .await? - } - ResourceReferenceType::ProcObj(obj) => { - // log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save"); - // log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)"); - // warnings removed, only use in euca-runner on release. - obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone()) + let handle = if let Some(uuid) = ser.uuid { + let project_root = crate::states::PROJECT.read().project_path.clone(); + match crate::metadata::find_asset_by_uuid(&project_root, uuid) { + Ok(entry) => { + if let crate::resource::ResourceReference::File(rel) = &entry.location { + let abs = project_root.join("resources").join(rel); + match ResourceReference::from_path(&abs) { + Ok(engine_ref) => { + log::debug!("Loading model '{}' via UUID {}", entry.name, uuid); + load_model_from_reference( + engine_ref, + entry.name.clone(), + graphics.clone(), + ) + .await? + } + Err(e) => { + log::warn!("Failed to build engine reference for {abs:?}: {e}"); + Handle::NULL + } + } + } else { + log::warn!("Asset {} location is not a file path", uuid); + Handle::NULL + } + } + Err(e) => { + log::warn!("UUID {} not found in .eucmeta files: {e}", uuid); + Handle::NULL + } } + } else if let Some(obj) = &ser.proc_obj { + log::debug!("Rebuilding procedural mesh from saved geometry"); + obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone()) + } else { + log::debug!("No model reference, setting to Handle::NULL"); + Handle::NULL }; let mut renderer = MeshRenderer::from_handle(handle); @@ -621,9 +654,8 @@ impl Component for MeshRenderer { let get_tex_handle = async |resource: &Option<TextureReference>| -> Option<anyhow::Result<Handle<Texture>>> { match resource { - Some(TextureReference::Resource(dif)) => match &dif.ref_type { - ResourceReferenceType::None => None, - ResourceReferenceType::File(file_path) => { + Some(TextureReference::Resource(dif)) => match dif { + ResourceReference::File(file_path) if !file_path.is_empty() => { let path = dif.resolve().ok()?; let bytes = std::fs::read(&path).ok()?; let mut texture = TextureBuilder::new(&graphics.device) @@ -634,7 +666,7 @@ impl Component for MeshRenderer { let mut registry = ASSET_REGISTRY.write(); Some(Ok(registry.add_texture_with_label(file_path.clone(), texture))) } - ResourceReferenceType::Bytes(bytes) => { + ResourceReference::Embedded(bytes) => { let texture = TextureBuilder::new(&graphics.device) .with_bytes(graphics.clone(), bytes) .label(label.as_str()) @@ -642,9 +674,10 @@ impl Component for MeshRenderer { let mut registry = ASSET_REGISTRY.write(); Some(Ok(registry.add_texture_with_label(label.clone(), texture))) } - ResourceReferenceType::ProcObj(_) => { - Some(Err(anyhow::anyhow!("Using a ProcObj as a texture is not valid, for texture with label {}", label))) + ResourceReference::Procedural(_) => { + Some(Err(anyhow::anyhow!("Using a Procedural object as a texture is not valid, for texture with label {}", label))) } + _ => None, }, Some(TextureReference::RGBAColour(rgba)) => { let to_u8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; @@ -747,9 +780,9 @@ impl Component for MeshRenderer { // let entity_label = sanitize_segment(entity_label_raw.as_str()); let save_reference = |reference: ResourceReference, context: &str| -> ResourceReference { - match &reference.ref_type { - ResourceReferenceType::None | ResourceReferenceType::File(_) | ResourceReferenceType::ProcObj(_) => reference, - ResourceReferenceType::Bytes(_) => { + match &reference { + ResourceReference::File(_) | ResourceReference::Procedural(_) => reference, + ResourceReference::Embedded(_) => { match reference .as_file(None) .and_then(|path| ResourceReference::from_path(path.as_path())) @@ -774,11 +807,21 @@ impl Component for MeshRenderer { let asset = ASSET_REGISTRY.read(); let model = asset.get_model(self.model()); - let (label, handle) = if let Some(model) = model.as_ref() { - ( - model.label.clone(), - save_reference(model.path.clone(), "mesh renderer model handle"), - ) + let (label, uuid, proc_obj) = if let Some(model) = model.as_ref() { + match &model.path { + ResourceReference::File(rel) if !rel.is_empty() => { + let project_root = crate::states::PROJECT.read().project_path.clone(); + let abs = project_root.join("resources").join(rel); + let uuid = crate::metadata::generate_eucmeta(&abs, &project_root) + .ok() + .map(|e| e.uuid); + (model.label.clone(), uuid, None) + } + ResourceReference::Procedural(obj) => { + (model.label.clone(), None, Some(obj.clone())) + } + _ => (model.label.clone(), None, None), + } } else { if !self.model().is_null() { log::warn!( @@ -786,7 +829,7 @@ impl Component for MeshRenderer { self.model().id ); } - ("None".to_string(), ResourceReference::default()) + ("None".to_string(), None, None) }; let mut texture_override: HashMap<String, SerializedMaterialCustomisation> = HashMap::new(); @@ -890,7 +933,8 @@ impl Component for MeshRenderer { Box::new(SerializedMeshRenderer { label, - handle, + uuid, + proc_obj, import_scale: Some(self.import_scale()), texture_override, }) @@ -1012,9 +1056,9 @@ impl InspectableComponent for MeshRenderer { .map(|model| model.label.clone()) .unwrap_or_else(|| "None".to_string()); - let model_title = match &model_reference.ref_type { - ResourceReferenceType::None => "None".to_string(), - ResourceReferenceType::ProcObj(obj) => match obj.ty { + let model_title = match &model_reference { + ResourceReference::File(s) if s.is_empty() => "None".to_string(), + ResourceReference::Procedural(obj) => match obj.ty { ProcObjType::Cuboid => "Cuboid".to_string(), }, _ => model_label, @@ -1092,8 +1136,8 @@ impl InspectableComponent for MeshRenderer { if ui .selectable_label( matches!( - model_reference.ref_type, - ResourceReferenceType::None + model_reference, + ResourceReference::File(ref s) if s.is_empty() ), "None", ) @@ -1107,8 +1151,8 @@ impl InspectableComponent for MeshRenderer { if ui .selectable_label( matches!( - model_reference.ref_type, - ResourceReferenceType::ProcObj(_) + model_reference, + ResourceReference::Procedural(_) ), "Cuboid", ) @@ -1171,8 +1215,8 @@ impl InspectableComponent for MeshRenderer { ui.ctx().data_mut(|d| d.insert_temp(expand_id, expanded)); if choose_proc_cuboid { - let default_size = match &model_reference.ref_type { - ResourceReferenceType::ProcObj(obj) => { + let default_size = match &model_reference { + ResourceReference::Procedural(obj) => { proc_obj_size(obj).unwrap_or([1.0, 1.0, 1.0]) } _ => [1.0, 1.0, 1.0], @@ -1189,7 +1233,7 @@ impl InspectableComponent for MeshRenderer { if expanded { ui.add_space(6.0); - if let ResourceReferenceType::File(reference) = &model_reference.ref_type { + if let ResourceReference::File(reference) = &model_reference { if is_probably_model_uri(reference) { let mut import_scale = self.import_scale(); ui.horizontal(|ui| { @@ -1212,7 +1256,7 @@ impl InspectableComponent for MeshRenderer { } } - if let ResourceReferenceType::ProcObj(obj) = &model_reference.ref_type { + if let ResourceReference::Procedural(obj) = &model_reference { if let Some(mut size) = proc_obj_size(obj) { ui.label(RichText::new("Cuboid").strong()); ui.horizontal(|ui| { @@ -1282,7 +1326,7 @@ impl InspectableComponent for MeshRenderer { .filter_map(|(handle, label, reference)| { let is_file_reference = reference .as_ref() - .is_some_and(|r| matches!(r.ref_type, ResourceReferenceType::File(_))); + .is_some_and(|r| matches!(r, ResourceReference::File(s) if !s.is_empty())); if !is_file_reference { return None; } @@ -1,7 +1,7 @@ //! The eucalyptus configuration files and its metadata. use crate::runtime::{Authoring, RuntimeSettings}; use crate::scene::SceneConfig; -use crate::states::{File, Folder, Node, RESOURCES, ResourceType, SCENES, SOURCE}; +use crate::states::{File, Folder, Node, ResourceType, SCENES, SOURCE}; use chrono::Utc; use ron::ser::PrettyConfig; use serde::{Deserialize, Serialize}; @@ -114,40 +114,11 @@ impl ProjectConfig { Ok(config) } - /// This function loads a `source.eucc`, `resources.eucc` or a `{scene}.eucs` config file into memory, allowing + /// This function loads a `source.eucc` or a `{scene}.eucs` config file into memory, allowing /// you to reference and load the nodes located inside them. pub fn load_config_to_memory(&mut self) -> anyhow::Result<()> { let project_root = PathBuf::from(&self.project_path); - // resource config - match ResourceConfig::read_from(&project_root) { - Ok(resources) => { - let mut cfg = RESOURCES.write(); - *cfg = resources; - } - Err(e) => { - if let Some(io_err) = e.downcast_ref::<std::io::Error>() { - if io_err.kind() == std::io::ErrorKind::NotFound { - log::warn!("resources.eucc not found, creating default."); - let default = ResourceConfig { - path: project_root.join("resources"), - nodes: vec![], - }; - log::debug!("Writing to {}", default.path.display()); - default.write_to(&project_root)?; - { - let mut cfg = RESOURCES.write(); - *cfg = default; - } - } else { - log::warn!("Failed to load resources.eucc: {}", e); - } - } else { - log::warn!("Failed to load resources.eucc: {}", e); - } - } - } - // src config let mut source_config = SOURCE.write(); match SourceConfig::read_from(&project_root) { @@ -176,7 +147,7 @@ impl ProjectConfig { scene_configs.clear(); // iterate through each scene file in the folder - let scene_folder = &project_root.join("scenes"); + let scene_folder = &project_root.join("resources").join("scenes"); if !scene_folder.exists() { fs::create_dir_all(scene_folder)?; @@ -335,11 +306,6 @@ impl ProjectConfig { let path = self.project_path.clone(); { - let resources_config = RESOURCES.read(); - resources_config.write_to(&path)?; - } - - { let source_config = SOURCE.read(); source_config.write_to(&path)?; } @@ -366,22 +332,6 @@ pub struct ResourceConfig { } impl ResourceConfig { - /// # Parameters - /// - path: The root **folder** of the project - pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> { - let resource_dir = path.as_ref().join("resources"); - let updated_config = ResourceConfig { - path: resource_dir.clone(), - nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["thumbnails"].as_slice()), - }; - let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) - .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let config_path = path.as_ref().join("resources").join("resources.eucc"); - fs::create_dir_all(config_path.parent().unwrap())?; - fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(()) - } - /// Updates the in-memory ResourceConfig by re-scanning the resource directory. pub fn update_mem(&mut self) -> anyhow::Result<ResourceConfig> { let resource_dir = self.path.clone(); @@ -392,16 +342,6 @@ impl ResourceConfig { }; Ok(updated_config) } - - /// # Parameters - /// - path: The location to the **resources.eucc** file - pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { - let config_path = path.as_ref().join("resources").join("resources.eucc"); - let ron_str = fs::read_to_string(&config_path)?; - let config: ResourceConfig = ron::de::from_str(&ron_str) - .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; - Ok(config) - } } #[derive(Default, Debug, Serialize, Deserialize, Clone)] @@ -0,0 +1,90 @@ +use crate::component::{ + Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, + SerializedComponent, +}; +use crate::physics::PhysicsState; +use dropbear_engine::graphics::SharedGraphicsContext; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// Serialized form of [`EntityStatus`], stored as part of the scene file. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SerializedEntityStatus { + /// When `true` the entity's [`crate::entity::MeshRenderer`] is not submitted to the GPU but + /// logic (scripts, physics) still runs normally. + #[serde(default)] + pub hidden: bool, + /// When `true` **all** component updates for this entity are suppressed — no rendering, + /// no physics, no script execution. + #[serde(default)] + pub disabled: bool, +} + +#[typetag::serde] +impl SerializedComponent for SerializedEntityStatus {} + +/// Runtime entity-level visibility / activation flags. +/// +/// - **Hidden** — the entity is not rendered but logic still executes. +/// - **Disabled** — the entity is fully inert: no rendering, no physics, no script updates. +/// +/// This component is optionally added to any entity. Entities without it behave as +/// if they have `hidden = false, disabled = false`. +#[derive(Debug, Clone)] +pub struct EntityStatus { + pub hidden: bool, + pub disabled: bool, +} + +impl Component for EntityStatus { + type SerializedForm = SerializedEntityStatus; + type RequiredComponentTypes = (Self,); + + fn descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "eucalyptus_core::entity_status::EntityStatus".to_string(), + type_name: "EntityStatus".to_string(), + category: None, + description: Some("Controls entity visibility and activation".to_string()), + disabled_flags: DisabilityFlags::Never, + internal: true, + } + } + + fn init( + ser: &'_ Self::SerializedForm, + _graphics: Arc<SharedGraphicsContext>, + ) -> ComponentInitFuture<'_, Self> { + let hidden = ser.hidden; + let disabled = ser.disabled; + Box::pin(async move { Ok((Self { hidden, disabled },)) }) + } + + fn update_component( + &mut self, + _world: &hecs::World, + _physics: &mut PhysicsState, + _entity: hecs::Entity, + _dt: f32, + _graphics: Arc<SharedGraphicsContext>, + ) { } + + fn save(&self, _world: &hecs::World, _entity: hecs::Entity) -> Box<dyn SerializedComponent> { + Box::new(SerializedEntityStatus { + hidden: self.hidden, + disabled: self.disabled, + }) + } +} + +impl InspectableComponent for EntityStatus { + fn inspect( + &mut self, + _world: &hecs::World, + _entity: hecs::Entity, + _ui: &mut egui::Ui, + _graphics: Arc<SharedGraphicsContext>, + ) { + // nothing... + } +} @@ -2,12 +2,14 @@ extern crate core; pub mod animation; pub mod asset; +pub mod billboard; pub mod camera; pub mod command; pub mod component; pub mod config; pub mod engine; pub mod entity; +pub mod entity_status; pub mod hierarchy; pub mod input; pub mod lighting; @@ -23,9 +25,11 @@ pub mod states; pub mod transform; pub mod types; pub mod utils; -pub mod billboard; pub mod ui; pub mod ser; +pub mod metadata; +pub mod resource; +pub mod uuid; pub use dropbear_macro as macros; @@ -34,6 +38,7 @@ use crate::physics::collider::ColliderGroup; use crate::physics::kcc::KCC; use crate::physics::rigidbody::RigidBody; use crate::billboard::BillboardComponent; +use crate::entity_status::EntityStatus; use crate::states::Script; use crate::ui::HUDComponent; use dropbear_engine::animation::AnimationComponent; @@ -55,6 +60,7 @@ pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { /// Registers all available and potential serializers and deserializers of an entity. pub fn register_components(component_registry: &mut ComponentRegistry) { component_registry.register::<EntityTransform>(); + component_registry.register::<EntityStatus>(); component_registry.register::<CustomProperties>(); component_registry.register::<Light>(); component_registry.register::<Script>(); @@ -1,5 +1,5 @@ use crate::component::{ - Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent, + Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent, }; use crate::ptr::WorldPtr; use crate::scripting::jni::utils::{FromJObject, ToJObject}; @@ -29,6 +29,8 @@ impl Component for Light { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "dropbear_engine::lighting::Light".to_string(), type_name: "Light".to_string(), category: Some("Lighting".to_string()), @@ -0,0 +1,254 @@ +use std::fs; +use std::time::SystemTime; +use uuid::Uuid; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use rkyv::Archive; +use ron::ser::PrettyConfig; +use serde::{Serialize, Deserialize}; +use sha2::{Digest, Sha256}; +use crate::resource::ResourceReference; +use crate::uuid::UuidV4; + +/// The type of asset, used for filtering in the asset browser +/// and determining which importer to invoke. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AssetType { + Mesh, + Texture, + Audio, + Material, + Scene, + Script, +} + +/// A single entry in the editor's asset registry. +/// +/// This is an editor-only struct — it is never rkyv-archived or packed. +/// It is rebuilt at editor startup by scanning `.eucmeta` files. +/// The serialized form of this struct (written to `.eucmeta`) uses serde. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AssetEntry { + /// The stable, permanent identity of this asset. + /// Never changes even if the file is renamed or moved. + /// All in-engine references to this asset use this UUID exclusively. + pub uuid: Uuid, + + /// Display name shown in the editor asset browser. + /// Derived from filename on first import, but user-overridable. + pub name: String, + + /// The kind of asset. Used for browser filtering and importer dispatch. + pub asset_type: AssetType, + + /// Where the source file lives. Purely a location — identity is the UUID. + pub location: ResourceReference, + + /// Path to the compiled output (e.g. `compiled/meshes/f47ac10b.eucmdl`), + /// relative to the project root. + /// `None` for `Embedded` and `Procedural` assets which have no compiled form. + pub compiled_path: Option<PathBuf>, + + /// SHA-256 (or xxHash) of the source file at last successful import. + /// + /// On editor startup, the source is rehashed and compared. + /// + /// If it differs, the asset is marked dirty and recompilation is triggered. + /// + /// More reliable than `import_time` — timestamps can lie (e.g. git checkouts). + pub source_hash: [u8; 32], + + /// When the asset was last successfully imported. + /// + /// Used for "recently imported" sorting in the browser — not for staleness. + pub import_time: SystemTime, + + /// UUIDs of other assets this asset directly references. + /// + /// e.g. a Mesh entry lists its Material UUIDs; a Material lists its Texture UUIDs. + pub dependencies: Vec<Uuid>, +} + +impl AssetEntry { + /// Resolves this entry to a `ResourceReference` for use by the asset loader. + /// + /// `project_root` is the absolute path to the project's `resources/` directory. + /// Requires the caller (typically the `AssetRegistry`) to supply it — + /// `AssetEntry` itself only stores project-relative paths. + /// + /// Returns `None` if the location is `Embedded` — use `to_embedded_reference` + /// instead, supplying the actual bytes. + pub fn to_reference(&self, project_root: &Path) -> Option<ResourceReference> { + match &self.location { + ResourceReference::File(relative) => { + Some(ResourceReference::File(project_root.join(relative))) + } + ResourceReference::Procedural => { + Some(ResourceReference::Procedural) + } + ResourceReference::Embedded(_) | ResourceReference::Packed { .. } => None, + } + } + + /// Resolves an `Embedded` asset to a `ResourceReference`. + /// The caller is responsible for supplying the correct bytes + /// (typically a `&'static [u8]` from `include_bytes!`). + pub fn to_embedded_reference(&self, bytes: &'static [u8]) -> ResourceReference { + ResourceReference::Embedded(Arc::from(bytes)) + } + + /// Returns true if the asset's source file needs to be reimported. + /// Call this on editor startup after rehashing the source file. + pub fn is_stale(&self, current_hash: &[u8; 32]) -> bool { + &self.source_hash != current_hash + } +} + +/// Detects the `AssetType` for a file based on its extension. +/// Returns `None` if the extension is unknown or absent. +pub fn detect_asset_type(path: &Path) -> Option<AssetType> { + match path.extension()?.to_str()?.to_ascii_lowercase().as_str() { + "obj" | "gltf" | "glb" | "fbx" | "eucmdl" | "eucbin" => Some(AssetType::Mesh), + "png" | "jpg" | "jpeg" | "webp" | "hdr" | "tga" | "bmp" | "exr" => Some(AssetType::Texture), + "wav" | "ogg" | "flac" | "mp3" => Some(AssetType::Audio), + "eucs" => Some(AssetType::Scene), + "kt" | "kts" => Some(AssetType::Script), + _ => None, + } +} + +fn hash_file(path: &Path) -> anyhow::Result<[u8; 32]> { + let bytes = fs::read(path)?; + let digest = Sha256::digest(&bytes); + let mut hash = [0u8; 32]; + hash.copy_from_slice(&digest); + Ok(hash) +} + +/// Writes a `.eucmeta` sidecar next to `source_path` and returns the created `AssetEntry`. +/// +/// If a sidecar already exists it is read and returned unchanged — +/// the file's UUID and other metadata are preserved across re-imports. +/// +/// `source_path` — absolute path to the resource file inside the project. +/// `project_root` — absolute path to the project root, used to store a project-relative +/// path inside the entry so that the project stays relocatable. +pub fn generate_eucmeta(source_path: &Path, project_root: &Path) -> anyhow::Result<AssetEntry> { + let meta_path = PathBuf::from(format!("{}.eucmeta", source_path.display())); + + if meta_path.exists() { + let ron_str = fs::read_to_string(&meta_path)?; + let entry: AssetEntry = ron::de::from_str(&ron_str) + .map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", meta_path.display(), e))?; + return Ok(entry); + } + + let asset_type = detect_asset_type(source_path) + .ok_or_else(|| anyhow::anyhow!("Unknown asset type for: {}", source_path.display()))?; + + let name = source_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unnamed") + .to_string(); + + let relative = source_path + .strip_prefix(project_root) + .unwrap_or(source_path) + .to_path_buf(); + + let source_hash = hash_file(source_path)?; + + let entry = AssetEntry { + uuid: Uuid::new_v4(), + name, + asset_type, + location: ResourceReference::File(relative), + compiled_path: None, + source_hash, + import_time: SystemTime::now(), + dependencies: vec![], + }; + + let ron_str = ron::ser::to_string_pretty(&entry, PrettyConfig::default()) + .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; + fs::write(&meta_path, &ron_str)?; + log::info!("Generated .eucmeta for {}", source_path.display()); + Ok(entry) +} + +/// The asset type, duplicated here as a lean copy without editor-only variants. +/// Must stay in sync with `AssetType` in the editor crate. +#[derive(Clone, Debug, PartialEq, Eq, Archive, rkyv::Serialize, rkyv::Deserialize)] +pub enum PackedAssetType { + Mesh, + Texture, + Audio, + Material, + Scene, + Script, +} + +/// A single entry in the runtime packed asset manifest. +/// +/// Stored in the header block of a `.eucpak` file, rkyv-archived. +/// The loader uses this to resolve a UUID to a byte range within the pak, +/// then reads and deserializes the asset from those bytes. +/// +/// Everything editor-specific (name, source path, hash, import time, +/// thumbnail) is stripped — the runtime has no use for it. +#[derive(Clone, Debug, Archive, rkyv::Serialize, rkyv::Deserialize)] +pub struct PackedAssetEntry { + /// The same UUID as the editor's `AssetEntry`. + /// This is the only field shared between the two representations. + pub uuid: UuidV4, + + /// The type of asset. Needed so the loader knows which + /// deserializer to invoke without having to peek at the bytes. + pub asset_type: PackedAssetType, + + /// Byte offset from the start of the `.eucpak` data block + /// where this asset's compiled bytes begin. + pub offset: u64, + + /// Length in bytes of this asset's compiled data. + /// `offset + length` is the exclusive end of the asset slice. + pub length: u64, + + /// UUIDs of assets that must be loaded before this one. + /// Kept from `AssetEntry::dependencies` for streaming and + /// load-order resolution at runtime. Can be empty for leaf assets + /// (e.g. a Texture that references nothing). + pub dependencies: Vec<UuidV4>, +} + +/// Scans all `.eucmeta` files under `<project_root>/resources/` and returns +/// the [`AssetEntry`] whose UUID matches `uuid`. +/// +/// Returns an error if no matching entry is found. +pub fn find_asset_by_uuid(project_root: &Path, uuid: Uuid) -> anyhow::Result<AssetEntry> { + fn scan_dir(dir: &Path, uuid: Uuid) -> Option<AssetEntry> { + let read = std::fs::read_dir(dir).ok()?; + for entry in read.flatten() { + let path = entry.path(); + if path.is_dir() { + if let Some(found) = scan_dir(&path, uuid) { + return Some(found); + } + } else if path.extension().and_then(|e| e.to_str()) == Some("eucmeta") { + if let Ok(s) = std::fs::read_to_string(&path) { + if let Ok(entry) = ron::de::from_str::<AssetEntry>(&s) { + if entry.uuid == uuid { + return Some(entry); + } + } + } + } + } + None + } + + let resources_dir = project_root.join("resources"); + scan_dir(&resources_dir, uuid) + .ok_or_else(|| anyhow::anyhow!("No .eucmeta file found for UUID {}", uuid)) +} @@ -16,7 +16,7 @@ pub mod collider_group; pub mod shader; -use crate::component::{Component, ComponentDescriptor, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::physics::PhysicsState; use crate::physics::collider::shared::{get_collider, get_collider_mut}; use crate::ptr::PhysicsStatePtr; @@ -68,6 +68,8 @@ impl Component for ColliderGroup { type_name: "ColliderGroup".to_string(), category: Some("Physics".to_string()), description: Some("A group of colliders".to_string()), + disabled_flags: DisabilityFlags::Disabled, + internal: false, } } @@ -3,7 +3,7 @@ pub mod character_collision; -use crate::component::{Component, ComponentDescriptor, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::physics::PhysicsState; use crate::ptr::{WorldPtr}; use crate::scripting::jni::utils::ToJObject; @@ -78,6 +78,8 @@ impl Component for KCC { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "eucalyptus_core::physics::kcc::KCC".to_string(), type_name: "KinematicCharacterController".to_string(), category: Some("Physics".to_string()), @@ -1,6 +1,6 @@ //! [rapier3d] RigidBodies -use crate::component::{Component, ComponentDescriptor, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::physics::PhysicsState; use crate::ptr::{PhysicsStatePtr, WorldPtr}; use crate::scripting::jni::utils::{FromJObject, ToJObject}; @@ -201,6 +201,8 @@ impl Component for RigidBody { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "eucalyptus_core::physics::rigidbody::RigidBody".to_string(), type_name: "RigidBody".to_string(), category: Some("Physics".to_string()), @@ -1,5 +1,5 @@ use crate::component::{ - Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent, + Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent, }; use crate::ptr::WorldPtr; use crate::scripting::native::DropbearNativeError; @@ -31,6 +31,8 @@ impl Component for CustomProperties { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "eucalyptus_core::properties::CustomProperties".to_string(), type_name: "CustomProperties".to_string(), category: None, @@ -0,0 +1,55 @@ +use std::path::PathBuf; +use std::sync::Arc; +use serde::{Serialize, Deserialize}; + +/// A resolved pointer to asset data, handed to the loader. +/// +/// You should never construct this by hand with a raw string — +/// always derive it from an `AssetEntry` via `AssetEntry::to_reference()`, +/// or from a `PackedAssetEntry` via the pak reader. +/// +/// This is the *loading strategy*, not the identity. +/// Identity is always the `Uuid` on `AssetEntry`. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ResourceReference { + /// A file within the project's `resources/` directory. + /// `PathBuf` is absolute at runtime, resolved by the registry + /// from the project-relative `AssetLocation::ProjectFile`. + File(PathBuf), + + /// Bytes compiled into the binary via `include_bytes!`. + /// The `Arc<[u8]>` is cheap to clone and share across systems. + Embedded(Arc<[u8]>), + + /// A byte range within a loaded `.eucpak` file. + /// The loader holds the pak in memory and slices it directly — + /// zero additional allocation. + Packed { + offset: u64, + length: u64, + }, + + /// No backing data — generated entirely at runtime. + Procedural, +} + +impl ResourceReference { + /// Resolves to a byte slice if the reference is `Embedded`. + /// Returns `None` for all other variants. + pub fn as_embedded_bytes(&self) -> Option<&[u8]> { + match self { + Self::Embedded(bytes) => Some(bytes), + _ => None, + } + } + + /// Returns true if this reference requires filesystem I/O to load. + pub fn is_file_backed(&self) -> bool { + matches!(self, Self::File(_)) + } + + /// Returns true if this reference can be loaded without any I/O. + pub fn is_in_memory(&self) -> bool { + matches!(self, Self::Embedded(_) | Self::Packed { .. } | Self::Procedural) + } +} @@ -145,7 +145,7 @@ impl SceneConfig { let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let scenes_dir = project_path.as_ref().join("scenes"); + let scenes_dir = project_path.as_ref().join("resources").join("scenes"); log::debug!("Creating scene dir at {}", scenes_dir.display()); fs::create_dir_all(&scenes_dir)?; @@ -2,6 +2,7 @@ use std::fmt::Display; use std::string::ToString; pub mod model; +pub mod templates; pub enum SerializedType { /// This is a `*.eucbin` file type. @@ -5,7 +5,7 @@ use dropbear_engine::asset::{Handle, ASSET_REGISTRY}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::model::{AlphaMode, Animation, Material, Mesh, Model, ModelVertex, Node, Skin}; use dropbear_engine::texture::{Texture, TextureWrapMode}; -use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use dropbear_engine::utils::ResourceReference; use dropbear_engine::wgpu::util::DeviceExt; use dropbear_engine::wgpu; @@ -224,9 +224,8 @@ impl EucalyptusMaterial { reference: &ResourceReference, suffix: &str, ) -> Option<Handle<Texture>> { - match &reference.ref_type { - ResourceReferenceType::None => None, - ResourceReferenceType::File(_) => { + match reference { + ResourceReference::File(path) if !path.is_empty() => { let path = reference.resolve().ok()?; let bytes = std::fs::read(path).ok()?; let label = format!("{}_{}", self.name, suffix); @@ -239,7 +238,7 @@ impl EucalyptusMaterial { let mut registry = ASSET_REGISTRY.write(); Some(registry.add_texture(texture)) } - ResourceReferenceType::Bytes(bytes) => { + ResourceReference::Embedded(bytes) => { let label = format!("{}_{}", self.name, suffix); let texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device) .with_bytes(graphics.clone(), bytes) @@ -249,7 +248,7 @@ impl EucalyptusMaterial { let mut registry = ASSET_REGISTRY.write(); Some(registry.add_texture(texture)) } - ResourceReferenceType::ProcObj(_) => None, + _ => None, } } @@ -0,0 +1,17 @@ +/// A template that can be used to display entities and their children. +/// +/// Contains all the assets required. On final compilation, it will be resolved, +#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)] +pub struct Template { + pub label: String, +} + +impl Template { + pub fn new(label: String) -> Self { + Self { label } + } + + pub fn update(&mut self, ) { + + } +} @@ -4,9 +4,9 @@ use crate::camera::{CameraComponent, CameraType}; use crate::component::{ - Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent, + Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent, }; -use crate::config::{ProjectConfig, ResourceConfig, SourceConfig}; +use crate::config::{ProjectConfig, SourceConfig}; use crate::properties::Value; use crate::scene::SceneConfig; use dropbear_engine::camera::{Camera, CameraSettings}; @@ -15,7 +15,7 @@ use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::LightComponent; use dropbear_engine::model::AlphaMode; use dropbear_engine::texture::{TextureReference, TextureWrapMode}; -use dropbear_engine::utils::ResourceReference; +use dropbear_engine::procedural::ProcedurallyGeneratedObject; use egui::{CollapsingHeader, TextEdit, Ui}; use hecs::{Entity, World}; use once_cell::sync::Lazy; @@ -28,14 +28,12 @@ use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; use std::sync::Arc; +use uuid::Uuid; /// A global "singleton" that contains the configuration of a project. pub static PROJECT: Lazy<RwLock<ProjectConfig>> = Lazy::new(|| RwLock::new(ProjectConfig::default())); -pub static RESOURCES: Lazy<RwLock<ResourceConfig>> = - Lazy::new(|| RwLock::new(ResourceConfig::default())); - pub static SOURCE: Lazy<RwLock<SourceConfig>> = Lazy::new(|| RwLock::new(SourceConfig::default())); pub static SCENES: Lazy<RwLock<Vec<SceneConfig>>> = Lazy::new(|| RwLock::new(Vec::new())); @@ -69,6 +67,7 @@ pub fn load_scene(scene_name: &str) -> anyhow::Result<SceneConfig> { project .project_path + .join("resources") .join("scenes") .join(format!("{}.eucs", scene_name)) }; @@ -166,6 +165,8 @@ impl Component for Script { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "eucalyptus_core::states::Script".to_string(), type_name: "Script".to_string(), category: Some("Logic".to_string()), @@ -456,7 +457,13 @@ impl DerefMut for Label { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct SerializedMeshRenderer { pub label: String, - pub handle: ResourceReference, + /// Stable UUID for this asset. When set, the runtime resolves the asset path + /// by looking up the corresponding `.eucmeta` file. + #[serde(default)] + pub uuid: Option<Uuid>, + /// Procedural geometry for this mesh, if it is procedurally generated. + #[serde(default)] + pub proc_obj: Option<ProcedurallyGeneratedObject>, pub import_scale: Option<f32>, pub texture_override: HashMap<String, SerializedMaterialCustomisation>, } @@ -1,4 +1,4 @@ -use crate::component::{Component, ComponentDescriptor, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::hierarchy::EntityTransformExt; use crate::ptr::WorldPtr; use crate::scripting::jni::utils::{FromJObject, ToJObject}; @@ -23,6 +23,8 @@ impl Component for EntityTransform { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "dropbear_engine::entity::EntityTransform".to_string(), type_name: "EntityTransform".to_string(), category: None, @@ -2,7 +2,7 @@ use std::sync::Arc; use egui::{CollapsingHeader, Ui}; use hecs::{Entity, World}; use dropbear_engine::graphics::SharedGraphicsContext; -use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent}; +use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::physics::PhysicsState; #[derive(Default, Clone, serde::Serialize, serde::Deserialize)] @@ -29,6 +29,8 @@ impl Component for HUDComponent { fn descriptor() -> ComponentDescriptor { ComponentDescriptor { + disabled_flags: DisabilityFlags::Disabled, + internal: false, fqtn: "eucalyptus_core::ui::HUDComponent".to_string(), type_name: "HUD".to_string(), category: Some("UI".to_string()), @@ -5,7 +5,7 @@ pub mod option; use crate::scripting::result::DropbearNativeResult; use crate::states::Node; use crate::ser::model::{EucalyptusMaterial, EucalyptusMesh, EucalyptusModel}; -use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca}; +use dropbear_engine::utils::ResourceReference; use jni::JNIEnv; use jni::objects::{JObject, JValue}; use std::collections::hash_map::DefaultHasher; @@ -254,8 +254,7 @@ pub fn keycode_from_ordinal(ordinal: i32) -> Option<KeyCode> { pub trait ResolveReference { /// This function attempts to resolve the [`ResourceReference`] - /// (specifically the [`ResourceReferenceType::File`]) into - /// a [`PathBuf`]. + /// (specifically `ResourceReference::File`) into a [`PathBuf`]. /// /// It does this by checking if the app is the `eucalyptus-editor` /// through the `editor` flag, or the redback-runtime. @@ -267,9 +266,9 @@ pub trait ResolveReference { impl ResolveReference for ResourceReference { fn resolve(&self) -> anyhow::Result<PathBuf> { - match &self.ref_type { - ResourceReferenceType::File(path) => { - let relative = relative_path_from_euca(path)?; + match self { + ResourceReference::File(path) if !path.is_empty() => { + let relative = path.as_str(); #[cfg(feature = "editor")] { @@ -296,10 +295,10 @@ impl ResolveReference for ResourceReference { } let root = runtime_resources_dir()?; - return resolve_resource_from_root(relative, &root); + resolve_resource_from_root(relative, &root) } _ => { - anyhow::bail!("Cannot resolve any other ResourceReferenceType that is not File") + anyhow::bail!("Cannot resolve a non-file ResourceReference") } } } @@ -310,20 +309,20 @@ pub trait AsFile { /// Converts a [`ResourceReference`] into a file. /// /// # Different type's behaviours - /// - [`ResourceReferenceType::None`] => This just returns an error as it's impossible. - /// - [`ResourceReferenceType::File`] => Returns that file resolved. - /// - [`ResourceReferenceType::Bytes`] => Converts the bytes into a `*.eucbin`, with the name derived from `new_ref` (arg). - /// - [`ResourceReferenceType::ProcObj`] => Converts the vertices into a `*.eucmdl`, with the name derived from `new_ref` (arg). + /// - `ResourceReference::File("")` => Returns an error (empty path). + /// - `ResourceReference::File(path)` => Returns the resolved file path. + /// - `ResourceReference::Embedded` => Saves bytes to a `*.eucbin` in the gen/ folder. + /// - `ResourceReference::Procedural` => Serializes geometry to a `*.eucmdl` in the gen/ folder. fn as_file(&self, new_ref: Option<String>) -> anyhow::Result<PathBuf>; } impl AsFile for ResourceReference { fn as_file(&self, new_ref: Option<String>) -> anyhow::Result<PathBuf> { - match &self.ref_type { - ResourceReferenceType::None => { - anyhow::bail!("Cannot convert ResourceReferenceType::None to a file") + match self { + ResourceReference::File(s) if s.is_empty() => { + anyhow::bail!("Cannot convert an empty File reference to a path") } - ResourceReferenceType::File(_) => { + ResourceReference::File(_) => { let resolved = self.resolve()?; if let Some(relative) = new_ref { let root = resources_root_for_write()?; @@ -336,7 +335,7 @@ impl AsFile for ResourceReference { Ok(resolved) } } - ResourceReferenceType::Bytes(bytes) => { + ResourceReference::Embedded(bytes) => { let hash = hash_value(bytes); let root = resources_root_for_write()?; let out_path = root @@ -350,7 +349,7 @@ impl AsFile for ResourceReference { Ok(out_path) } - ResourceReferenceType::ProcObj(obj) => { + ResourceReference::Procedural(obj) => { let hash = hash_value(obj); let out_path = if let Some(relative) = new_ref.as_ref() { let root = resources_root_for_write()?; @@ -0,0 +1,43 @@ +use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// A newtype around the raw bytes of a UUID that satisfies rkyv's `Archive` bound. +/// +/// The inner `[u8; 16]` is trivially archivable, giving us zero-copy access at +/// runtime. Use `From`/`Into` to convert to/from the standard `uuid::Uuid` type. +#[derive( + Clone, Debug, PartialEq, Eq, Hash, + Archive, RkyvSerialize, RkyvDeserialize, + Serialize, Deserialize, +)] +#[repr(transparent)] +pub struct UuidV4([u8; 16]); + +impl UuidV4 { + pub fn new_v4() -> Self { + UuidV4(*Uuid::new_v4().as_bytes()) + } + + pub fn as_uuid(&self) -> Uuid { + Uuid::from_bytes(self.0) + } +} + +impl From<Uuid> for UuidV4 { + fn from(u: Uuid) -> Self { + UuidV4(*u.as_bytes()) + } +} + +impl From<UuidV4> for Uuid { + fn from(v: UuidV4) -> Self { + Uuid::from_bytes(v.0) + } +} + +impl Default for UuidV4 { + fn default() -> Self { + UuidV4([0u8; 16]) + } +} @@ -39,7 +39,7 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> { // load scenes let mut scenes = Vec::new(); - let scene_folder = project_root.join("scenes"); + let scene_folder = project_root.join("resources").join("scenes"); if scene_folder.exists() { for entry in fs::read_dir(scene_folder)? { let entry = entry?; @@ -183,7 +183,7 @@ impl<'a> EditorTabViewer<'a> { self.walk_resource_directory(cfg, builder, base_path, &entry.path); builder.close_dir(); } else { - if entry.name.eq_ignore_ascii_case("resources.eucc") { + if entry.name.ends_with(".eucmeta") { continue; } @@ -733,7 +733,7 @@ impl<'a> EditorTabViewer<'a> { project_root: &Path, ) { let label = "euca://scenes"; - let scenes_root = project_root.join("scenes"); + let scenes_root = project_root.join("resources").join("scenes"); let root_info = AssetNodeInfo { path: scenes_root.clone(), division: AssetDivision::Scenes, @@ -30,6 +30,7 @@ impl<'a> EditorTabViewer<'a> { .unwrap_or("Scene".to_string()) }; builder.node( + // root node/scene display NodeBuilder::dir(u64::MAX) .label(format!("Scene: {}", current_scene_name)) .context_menu(|ui| { @@ -38,6 +39,9 @@ impl<'a> EditorTabViewer<'a> { self.world.spawn((label,)); ui.close(); } + ui.menu_button("Import Template", |ui| { + + }); }), ); // the root scene must be the biggest number possible to remove any ambiguity @@ -114,6 +118,9 @@ impl<'a> EditorTabViewer<'a> { }); } }); + if ui.button("Create Template").clicked() { + eucalyptus_core::fatal!("Not implemented yet"); + } }), ); @@ -1,4 +1,5 @@ use hecs::Entity; +use eucalyptus_core::entity_status::EntityStatus; use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, TABS_GLOBAL}; use dropbear_engine::camera::Camera; use eucalyptus_core::camera::{CameraComponent, CameraType}; @@ -17,6 +18,41 @@ impl<'a> EditorTabViewer<'a> { ui.label(format!("Entity ID: {}", inspect_entity.id())); ui.separator(); + // entity status + { + let (mut hidden, mut disabled) = self + .world + .get::<&EntityStatus>(inspect_entity) + .map(|s| (s.hidden, s.disabled)) + .unwrap_or((false, false)); + + let mut status_changed = false; + ui.horizontal(|ui| { + if ui.checkbox(&mut hidden, "Hidden").changed() { + status_changed = true; + } + if ui.checkbox(&mut disabled, "Disabled").changed() { + status_changed = true; + } + }); + + if status_changed { + let has_status = self.world.get::<&EntityStatus>(inspect_entity).is_ok(); + if has_status { + if let Ok(mut s) = self.world.get::<&mut EntityStatus>(inspect_entity) { + s.hidden = hidden; + s.disabled = disabled; + } + } else { + let _ = self.world.insert_one( + inspect_entity, + EntityStatus { hidden, disabled }, + ); + } + } + } + ui.separator(); + let mut local_unset_comp = false; if let Ok((_, comp)) = self.world.query_one::<(&Camera, &CameraComponent)>(inspect_entity).get() { let is_active = self @@ -13,6 +13,7 @@ use dropbear_engine::{ scene::{Scene, SceneCommand}, }; use eucalyptus_core::billboard::BillboardComponent; +use eucalyptus_core::entity_status::EntityStatus; use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry}; @@ -492,6 +493,13 @@ impl Scene for Editor { for (entity, renderer, animation) in query.iter() { puffin::profile_scope!(format!("locating {:?}", entity)); + let world_ptr = &*self.world as *const hecs::World; + let world_ref = unsafe { &*world_ptr }; + if let Ok(status) = world_ref.get::<&EntityStatus>(entity) { + if status.hidden || status.disabled { + continue; + } + } let handle = renderer.model(); if handle.is_null() { continue; @@ -1310,7 +1318,7 @@ impl Editor { let target_dir = match extension.as_deref() { Some("kt") => Self::script_drop_dir(&project_root), - Some("eucp") | Some("eucs") => project_root.join("scenes"), + Some("eucp") | Some("eucs") => project_root.join("resources").join("scenes"), _ => project_root.join("resources"), }; @@ -1338,6 +1346,17 @@ impl Editor { ); } else { log::info!("Dropped asset copied to '{}'", target_path.display()); + + // Generate a .eucmeta sidecar for resource files (not scenes, not scripts). + let is_resource = !matches!( + extension.as_deref(), + Some("kt") | Some("eucp") | Some("eucs") + ); + if is_resource { + if let Err(e) = eucalyptus_core::metadata::generate_eucmeta(&target_path, &project_root) { + log::warn!("Failed to generate .eucmeta for '{}': {}", target_path.display(), e); + } + } } } @@ -86,7 +86,7 @@ impl MainMenu { ("resources/shaders", 0.4, "Creating shaders folder..."), ("resources/textures", 0.5, "Creating textures folder..."), ("src2", 0.6, "Generating project config..."), - ("scenes", 0.7, "Creating scenes folder..."), + ("resources/scenes", 0.7, "Creating scenes folder..."), ]; if let Some(path) = &project_path { @@ -13,6 +13,7 @@ use dropbear_engine::model::{DrawLight, DrawModel}; use dropbear_engine::pipelines::DropbearShaderPipeline; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::billboard::BillboardComponent; +use eucalyptus_core::entity_status::EntityStatus; use eucalyptus_core::command::CommandBufferPoller; use eucalyptus_core::egui::CentralPanel; use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; @@ -162,8 +163,24 @@ impl Scene for PlayMode { } let mut entity_label_map = HashMap::new(); - for (entity, label) in self.world.query::<(Entity, &Label)>().iter() { - entity_label_map.insert(entity, label.clone()); + { + let all: Vec<(Entity, Label)> = self + .world + .query::<(Entity, &Label)>() + .iter() + .map(|(e, l)| (e, l.clone())) + .collect(); + for (entity, label) in all { + // disabled physics cannot partake + let disabled = self + .world + .get::<&EntityStatus>(entity) + .map(|s| s.disabled) + .unwrap_or(false); + if !disabled { + entity_label_map.insert(entity, label); + } + } } self.physics_state.step( @@ -687,6 +704,14 @@ impl Scene for PlayMode { for (entity, renderer, animation) in query.iter() { puffin::profile_scope!(format!("locating {:?}", entity)); + // skip entities that are hidden or disabled + let world_ptr = &*self.world as *const hecs::World; + let world_ref = unsafe { &*world_ptr }; + if let Ok(status) = world_ref.get::<&EntityStatus>(entity) { + if status.hidden || status.disabled { + continue; + } + } let handle = renderer.model(); if handle.is_null() { continue;