kitgit

tirbofish/dropbear · diff

c8aafb9 · Thribhu K

wip: overhaul on assets, with uuid linking planned.

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index afea4c0..e863633 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index fe5d54c..0549ced 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -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(),
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index a66f4bf..f621c67 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -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
         }
     }
 }
+
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index 67c1951..d445556 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -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 = []
diff --git a/crates/eucalyptus-core/src/animation.rs b/crates/eucalyptus-core/src/animation.rs
index e2c69bc..9d5116a 100644
--- a/crates/eucalyptus-core/src/animation.rs
+++ b/crates/eucalyptus-core/src/animation.rs
@@ -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,
         }
     }
 
diff --git a/crates/eucalyptus-core/src/billboard.rs b/crates/eucalyptus-core/src/billboard.rs
index 9e7eb7d..d3fe2af 100644
--- a/crates/eucalyptus-core/src/billboard.rs
+++ b/crates/eucalyptus-core/src/billboard.rs
@@ -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()),
diff --git a/crates/eucalyptus-core/src/camera.rs b/crates/eucalyptus-core/src/camera.rs
index 6a25dad..70aac75 100644
--- a/crates/eucalyptus-core/src/camera.rs
+++ b/crates/eucalyptus-core/src/camera.rs
@@ -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()),
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index e389fc7..5ab7dc2 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -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;
                                         }
diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index 5860cae..dcf8e7e 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -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)]
diff --git a/crates/eucalyptus-core/src/entity_status.rs b/crates/eucalyptus-core/src/entity_status.rs
new file mode 100644
index 0000000..3cead89
--- /dev/null
+++ b/crates/eucalyptus-core/src/entity_status.rs
@@ -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... 
+    }
+}
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index bec4fe1..d1b71fe 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -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>();
diff --git a/crates/eucalyptus-core/src/lighting.rs b/crates/eucalyptus-core/src/lighting.rs
index c3c6d2d..ddb35f0 100644
--- a/crates/eucalyptus-core/src/lighting.rs
+++ b/crates/eucalyptus-core/src/lighting.rs
@@ -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()),
diff --git a/crates/eucalyptus-core/src/metadata.rs b/crates/eucalyptus-core/src/metadata.rs
new file mode 100644
index 0000000..b77bd87
--- /dev/null
+++ b/crates/eucalyptus-core/src/metadata.rs
@@ -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))
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index a2c3387..32c226d 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -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,
         }
     }
 
diff --git a/crates/eucalyptus-core/src/physics/kcc.rs b/crates/eucalyptus-core/src/physics/kcc.rs
index 3991495..e3ea6f0 100644
--- a/crates/eucalyptus-core/src/physics/kcc.rs
+++ b/crates/eucalyptus-core/src/physics/kcc.rs
@@ -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()),
diff --git a/crates/eucalyptus-core/src/physics/rigidbody.rs b/crates/eucalyptus-core/src/physics/rigidbody.rs
index c59ef0d..a765446 100644
--- a/crates/eucalyptus-core/src/physics/rigidbody.rs
+++ b/crates/eucalyptus-core/src/physics/rigidbody.rs
@@ -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()),
diff --git a/crates/eucalyptus-core/src/properties.rs b/crates/eucalyptus-core/src/properties.rs
index 82dbcdc..7cad385 100644
--- a/crates/eucalyptus-core/src/properties.rs
+++ b/crates/eucalyptus-core/src/properties.rs
@@ -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,
diff --git a/crates/eucalyptus-core/src/resource.rs b/crates/eucalyptus-core/src/resource.rs
new file mode 100644
index 0000000..cb887d4
--- /dev/null
+++ b/crates/eucalyptus-core/src/resource.rs
@@ -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)
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index ce90a3e..534d063 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -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)?;
 
diff --git a/crates/eucalyptus-core/src/ser/mod.rs b/crates/eucalyptus-core/src/ser/mod.rs
index 81adc73..648c67d 100644
--- a/crates/eucalyptus-core/src/ser/mod.rs
+++ b/crates/eucalyptus-core/src/ser/mod.rs
@@ -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.
diff --git a/crates/eucalyptus-core/src/ser/model.rs b/crates/eucalyptus-core/src/ser/model.rs
index 56cf5b8..0f787e3 100644
--- a/crates/eucalyptus-core/src/ser/model.rs
+++ b/crates/eucalyptus-core/src/ser/model.rs
@@ -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,
         }
     }
 
diff --git a/crates/eucalyptus-core/src/ser/templates.rs b/crates/eucalyptus-core/src/ser/templates.rs
new file mode 100644
index 0000000..6116cae
--- /dev/null
+++ b/crates/eucalyptus-core/src/ser/templates.rs
@@ -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, ) {
+
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 8921bca..01c0f44 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -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>,
 }
diff --git a/crates/eucalyptus-core/src/transform.rs b/crates/eucalyptus-core/src/transform.rs
index d91cb11..e1e935c 100644
--- a/crates/eucalyptus-core/src/transform.rs
+++ b/crates/eucalyptus-core/src/transform.rs
@@ -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,
diff --git a/crates/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rs
index ace0839..40db894 100644
--- a/crates/eucalyptus-core/src/ui.rs
+++ b/crates/eucalyptus-core/src/ui.rs
@@ -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()),
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 728a614..038d1f7 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -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()?;
diff --git a/crates/eucalyptus-core/src/uuid.rs b/crates/eucalyptus-core/src/uuid.rs
new file mode 100644
index 0000000..1cd4156
--- /dev/null
+++ b/crates/eucalyptus-core/src/uuid.rs
@@ -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])
+    }
+}
diff --git a/crates/eucalyptus-editor/src/build.rs b/crates/eucalyptus-editor/src/build.rs
index 7577154..38e1586 100644
--- a/crates/eucalyptus-editor/src/build.rs
+++ b/crates/eucalyptus-editor/src/build.rs
@@ -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?;
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index c6a691f..a556391 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -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,
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index 8cd133c..b84ce3f 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -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");
+                                }
                             }),
                     );
 
diff --git a/crates/eucalyptus-editor/src/editor/resource.rs b/crates/eucalyptus-editor/src/editor/resource.rs
index 489b1e1..0eaec04 100644
--- a/crates/eucalyptus-editor/src/editor/resource.rs
+++ b/crates/eucalyptus-editor/src/editor/resource.rs
@@ -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
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 782d7bd..0fad404 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -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);
+                }
+            }
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/menu.rs b/crates/eucalyptus-editor/src/menu.rs
index 870bb5f..f0af525 100644
--- a/crates/eucalyptus-editor/src/menu.rs
+++ b/crates/eucalyptus-editor/src/menu.rs
@@ -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 {
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index b598df5..d467bea 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -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;