kitgit

tirbofish/dropbear · diff

f5ca1fd · Thribhu K

wip: stupid ass bug related to not updating Components. todo: i need to upgrade egui to 0.34.1 to get wgpu 29.0.0 working (only took like 4 months for them to make a new release smh)

Unverified

diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index daf780c..efa9b0d 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -71,6 +71,7 @@ pub struct Camera {
     pub uniform: CameraUniform,
 
     buffer: UniformBuffer<CameraUniform>,
+    pub bind_group: wgpu::BindGroup,
 
     /// View matrix
     pub view_mat: DMat4,
@@ -116,6 +117,14 @@ impl Camera {
         uniform.view_proj = mvp.as_mat4().to_cols_array_2d();
 
         let buffer = UniformBuffer::new(&graphics.device, "camera uniform");
+        let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("editor camera bind group"),
+            layout: &graphics.layouts.camera_bind_group_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: buffer.buffer().as_entire_binding(),
+            }],
+        });
 
         let camera = Self {
             eye: builder.eye,
@@ -136,6 +145,7 @@ impl Camera {
             },
             view_mat,
             proj_mat,
+            bind_group,
         };
 
         log::debug!(
diff --git a/crates/dropbear-engine/src/debug.rs b/crates/dropbear-engine/src/debug.rs
index 737d3ad..9a73a76 100644
--- a/crates/dropbear-engine/src/debug.rs
+++ b/crates/dropbear-engine/src/debug.rs
@@ -491,7 +491,7 @@ impl DebugDrawPipeline {
         let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
             label: Some("debug draw pass"),
             color_attachments: &[Some(RenderPassColorAttachment {
-                view: hdr.view(),
+                view: hdr.render_view(),
                 depth_slice: None,
                 resolve_target: hdr.resolve_target(),
                 ops: Operations {
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index d2b8095..4e6116f 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -22,7 +22,7 @@ use std::collections::hash_map::DefaultHasher;
 use std::future::Future;
 use std::hash::{Hash, Hasher};
 use std::pin::Pin;
-use std::sync::{Arc, LazyLock};
+use std::sync::{Arc, LazyLock, OnceLock};
 use std::time::{SystemTime, UNIX_EPOCH};
 
 pub use typetag::*;
@@ -50,6 +50,10 @@ pub struct KotlinComponentDecl {
 pub static KOTLIN_COMPONENT_QUEUE: LazyLock<Mutex<Vec<KotlinComponentDecl>>> =
     LazyLock::new(|| Mutex::new(Vec::new()));
 
+/// Fn signature for the Kotlin per-entity update callback stored on [`ComponentRegistry`].
+/// Arguments: `(fqcn, entity_bits, delta_time_secs)`.
+type KotlinUpdateFn = Box<dyn Fn(&str, u64, f32) + Send + Sync>;
+
 pub struct ComponentRegistry {
     /// Maps TypeId to ComponentDescriptor for quick lookups
     descriptors: HashMap<LanguageTypeId, ComponentDescriptor>,
@@ -73,6 +77,10 @@ pub struct ComponentRegistry {
     finders: HashMap<LanguageTypeId, FindFn>,
     /// Allows for inspecting the component in the Resource Inspector dock.
     inspectors: HashMap<LanguageTypeId, InspectFn>,
+    /// Shared JVM update callback, set after JVM init via [`Self::set_kotlin_update_fn`].
+    /// Captured by `Arc::clone` in each Kotlin updater closure so it works even when
+    /// the fn is set after [`Self::drain_kotlin_queue`] has already registered descriptors.
+    kotlin_update_fn: Arc<OnceLock<KotlinUpdateFn>>,
 }
 
 /// Describes a handy little future for [`Component::init`], which deals with initialising a component from its serialized form.
@@ -138,9 +146,18 @@ impl ComponentRegistry {
             removers: HashMap::new(),
             finders: HashMap::new(),
             inspectors: HashMap::new(),
+            kotlin_update_fn: Arc::new(OnceLock::new()),
         }
     }
 
+    /// Sets the JVM update callback used to call `NativeComponent.updateComponent()` from Rust.
+    ///
+    /// Must be called once after the JVM is ready (after `drain_kotlin_queue`). Subsequent calls
+    /// are no-ops because the inner `OnceLock` can only be set once.
+    pub fn set_kotlin_update_fn(&mut self, f: impl Fn(&str, u64, f32) + Send + Sync + 'static) {
+        let _ = self.kotlin_update_fn.set(Box::new(f));
+    }
+
     /// Register a component type with the registry
     pub fn register<T>(&mut self)
     where
@@ -241,6 +258,9 @@ impl ComponentRegistry {
                 let world_ptr = world as *const hecs::World;
                 if let Ok(mut comp) = world.get::<&mut T>(entity) {
                     let world_ref = unsafe { &*world_ptr };
+                    if T::descriptor().internal {
+                        return;
+                    }
                     comp.inspect(world_ref, entity, ui, graphics);
                 }
             }),
@@ -447,6 +467,19 @@ impl ComponentRegistry {
 
             ui.separator();
         }
+
+        let kotlin_fqcns: Vec<String> = world
+            .get::<&KotlinComponents>(entity)
+            .map(|kc| kc.fqcns.clone())
+            .unwrap_or_default();
+
+        for fqcn in kotlin_fqcns {
+            let kotlin_type_id = LanguageTypeId::Kotlin(fqcn);
+            if let Some(inspector) = self.inspectors.get(&kotlin_type_id) {
+                inspector(world, entity, ui, graphics.clone());
+                ui.separator();
+            }
+        }
     }
 
     fn numeric_id(type_id: &LanguageTypeId) -> u64 {
@@ -489,6 +522,8 @@ impl ComponentRegistry {
             return;
         }
 
+        let type_name_display = decl.type_name.clone();
+
         self.fqtn_to_type.insert(decl.fqcn.clone(), id.clone());
         if let Some(ref cat) = decl.category {
             self.categories
@@ -518,6 +553,46 @@ impl ComponentRegistry {
             }),
         );
 
+        let fqcn = decl.fqcn.clone();
+        self.defaults.insert(
+            id.clone(),
+            Box::new(move || {
+                Box::new(KotlinComponents { fqcns: vec![fqcn.clone()] })
+            }),
+        );
+
+        let fqcn_for_update = decl.fqcn.clone();
+        let update_slot = Arc::clone(&self.kotlin_update_fn);
+        self.updaters.insert(
+            id.clone(),
+            Box::new(move |world, _physics, dt, _graphics| {
+                let entity_bits: Vec<u64> = world
+                    .query::<(hecs::Entity, &KotlinComponents)>()
+                    .iter()
+                    .filter_map(|(entity, kc)| {
+                        if kc.has(&fqcn_for_update) { Some(entity.to_bits().get()) } else { None }
+                    })
+                    .collect();
+
+                if let Some(update_fn) = update_slot.get() {
+                    for bits in entity_bits {
+                        update_fn(&fqcn_for_update, bits, dt);
+                    }
+                }
+            }),
+        );
+
+        self.inspectors.insert(
+            id.clone(),
+            Box::new(move |_world, _entity, ui, _graphics| {
+                CollapsingHeader::new(type_name_display.as_str())
+                    .default_open(true)
+                    .show(ui, |ui| {
+                        ui.label("inspect() not yet wired.");
+                    });
+            }),
+        );
+
         let fqcn = decl.fqcn;
         self.finders.insert(
             id,
diff --git a/crates/eucalyptus-core/src/entity.rs b/crates/eucalyptus-core/src/entity.rs
index 6453984..f9d42ee 100644
--- a/crates/eucalyptus-core/src/entity.rs
+++ b/crates/eucalyptus-core/src/entity.rs
@@ -19,7 +19,7 @@ fn label_exists_for_entity(
 
 #[dropbear_macro::export(
     kotlin(
-        class = "com.dropbear.components.EntityRefNative",
+        class = "com.dropbear.EntityRefNative",
         func = "getEntityLabel"
     ),
     c
@@ -34,7 +34,7 @@ fn get_label(
 
 #[dropbear_macro::export(
     kotlin(
-        class = "com.dropbear.components.EntityRefNative",
+        class = "com.dropbear.EntityRefNative",
         func = "getChildren"
     ),
     c
@@ -58,7 +58,7 @@ fn get_children(
 
 #[dropbear_macro::export(
     kotlin(
-        class = "com.dropbear.components.EntityRefNative",
+        class = "com.dropbear.EntityRefNative",
         func = "getChildByLabel"
     ),
     c
@@ -70,7 +70,7 @@ fn get_child_by_label(
 ) -> DropbearNativeResult<Option<u64>> {
     if let Ok(children) = world.query_one::<&Children>(entity).get() {
         for child in children.children() {
-            if let Ok(label) = world.get::<&Label>(entity) {
+            if let Ok(label) = world.get::<&Label>(*child) {
                 if label.as_str() == target {
                     let found = child.clone();
                     return Ok(Some(found.to_bits().get()));
@@ -87,7 +87,7 @@ fn get_child_by_label(
 }
 
 #[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.EntityRefNative", func = "getParent"),
+    kotlin(class = "com.dropbear.EntityRefNative", func = "getParent"),
     c
 )]
 fn get_parent(
diff --git a/crates/eucalyptus-core/src/scripting/jni.rs b/crates/eucalyptus-core/src/scripting/jni.rs
index 115bb48..8df2bc2 100644
--- a/crates/eucalyptus-core/src/scripting/jni.rs
+++ b/crates/eucalyptus-core/src/scripting/jni.rs
@@ -19,6 +19,7 @@ use once_cell::sync::OnceCell;
 use sha2::{Digest, Sha256};
 use std::fs;
 use std::path::PathBuf;
+use std::sync::Arc;
 
 #[cfg(feature = "jvm_debug")]
 use crate::scripting::AWAIT_JDB;
@@ -34,6 +35,9 @@ pub enum RuntimeMode {
 
 const LIBRARY_PATH: &[u8] = include_bytes!("../../../../build/libs/dropbear-1.0-SNAPSHOT-all.jar");
 pub static RUNTIME_MODE: OnceCell<RuntimeMode> = OnceCell::new();
+/// Shared handle to the active [`JavaVM`], set once during [`JavaContext::new`].
+/// Callers may clone the `Arc` to attach new threads without any unsafe code.
+pub static GLOBAL_JVM: OnceCell<Arc<JavaVM>> = OnceCell::new();
 
 #[cfg(feature = "jvm_debug")]
 fn is_port_available(port: u16) -> bool {
@@ -48,7 +52,7 @@ fn is_port_available(port: u16) -> bool {
 
 /// Provides a context for any eucalyptus-core JNI calls and JVM hosting.
 pub struct JavaContext {
-    pub(crate) jvm: JavaVM,
+    pub(crate) jvm: Arc<JavaVM>,
     native_engine_instance: Option<GlobalRef>,
     dropbear_engine_class: Option<GlobalRef>,
     system_manager_instance: Option<GlobalRef>,
@@ -277,6 +281,9 @@ impl JavaContext {
 
         log::info!("Created JVM instance");
 
+        let jvm = Arc::new(jvm);
+        let _ = GLOBAL_JVM.set(jvm.clone());
+
         Ok(Self {
             jvm,
             native_engine_instance: None,
@@ -430,6 +437,8 @@ impl JavaContext {
                 self.system_manager_instance = Some(manager_global_ref);
             }
 
+            Self::call_component_manager_register_all(&mut env);
+
             Ok(())
         })();
 
@@ -437,6 +446,27 @@ impl JavaContext {
 
         result
     }
+        /// Registers the components within the .jar file as created in the ComponentManager. 
+    fn call_component_manager_register_all(env: &mut JNIEnv) {
+        match env.find_class("com/dropbear/decl/ComponentManager") {
+            Err(_) => {
+                let _ = env.exception_clear();
+                log::debug!(
+                    "ComponentManager class not found in user JAR, no @EcsComponent types will be registered"
+                );
+            }
+            Ok(class) => {
+                if let Err(e) =
+                    env.call_static_method(&class, "registerAll", "()V", &[])
+                {
+                    let _ = env.exception_clear();
+                    log::warn!("ComponentManager.registerAll() failed: {:?}", e);
+                } else {
+                    log::debug!("ComponentManager.registerAll() called successfully");
+                }
+            }
+        }
+    }
 
     pub fn get_exception(env: &mut JNIEnv) -> anyhow::Result<()> {
         if let Ok(ex) = env.exception_occurred() {
@@ -511,6 +541,7 @@ impl JavaContext {
                     "(Ljava/lang/String;)V",
                     &[JValue::Object(&jar_path_jstring)],
                 )?;
+                Self::call_component_manager_register_all(&mut env);
                 Ok(())
             })();
 
diff --git a/crates/eucalyptus-core/src/scripting/native.rs b/crates/eucalyptus-core/src/scripting/native.rs
index 005d329..c35be72 100644
--- a/crates/eucalyptus-core/src/scripting/native.rs
+++ b/crates/eucalyptus-core/src/scripting/native.rs
@@ -50,6 +50,10 @@ pub struct NativeLibrary {
     pub(crate) get_last_err_msg_fn: Symbol<'static, sig::GetLastErrorMessage>,
     #[allow(dead_code)]
     pub(crate) set_last_err_msg_fn: Symbol<'static, sig::SetLastErrorMessage>,
+
+    update_kotlin_component_fn: Option<Symbol<'static, sig::UpdateKotlinComponent>>,
+    #[allow(dead_code)]
+    inspect_kotlin_component_fn: Option<Symbol<'static, sig::InspectKotlinComponent>>,
 }
 
 impl NativeLibrary {
@@ -150,6 +154,21 @@ impl NativeLibrary {
                 "dropbear_set_last_error_message",
             )?;
 
+            let update_kotlin_component_fn = library
+                .get::<sig::UpdateKotlinComponent>(b"dropbear_update_kotlin_component\0")
+                .ok()
+                .map(|s| std::mem::transmute::<
+                    Symbol<sig::UpdateKotlinComponent>,
+                    Symbol<'static, sig::UpdateKotlinComponent>,
+                >(s));
+            let inspect_kotlin_component_fn = library
+                .get::<sig::InspectKotlinComponent>(b"dropbear_inspect_kotlin_component\0")
+                .ok()
+                .map(|s| std::mem::transmute::<
+                    Symbol<sig::InspectKotlinComponent>,
+                    Symbol<'static, sig::InspectKotlinComponent>,
+                >(s));
+
             Ok(Self {
                 library,
                 init_fn,
@@ -169,6 +188,8 @@ impl NativeLibrary {
                 contact_force_event_fn,
                 get_last_err_msg_fn,
                 set_last_err_msg_fn,
+                update_kotlin_component_fn,
+                inspect_kotlin_component_fn,
             })
         }
     }
diff --git a/crates/eucalyptus-core/src/scripting/native/sig.rs b/crates/eucalyptus-core/src/scripting/native/sig.rs
index c53b902..ed408f2 100644
--- a/crates/eucalyptus-core/src/scripting/native/sig.rs
+++ b/crates/eucalyptus-core/src/scripting/native/sig.rs
@@ -82,3 +82,9 @@ pub type DestroyAll = unsafe extern "C" fn() -> i32;
 pub type GetLastErrorMessage = unsafe extern "C" fn() -> *const c_char;
 /// CName: `dropbear_set_last_error_message`
 pub type SetLastErrorMessage = unsafe extern "C" fn(msg: *const c_char);
+
+/// CName: `dropbear_update_kotlin_component`
+pub type UpdateKotlinComponent =
+    unsafe extern "C" fn(fqcn: *const c_char, entity_id: u64, dt: f64) -> i32;
+/// CName: `dropbear_inspect_kotlin_component`
+pub type InspectKotlinComponent = unsafe extern "C" fn(fqcn: *const c_char) -> i32;
diff --git a/crates/eucalyptus-core/src/scripting/types.rs b/crates/eucalyptus-core/src/scripting/types.rs
index 905d261..475e737 100644
--- a/crates/eucalyptus-core/src/scripting/types.rs
+++ b/crates/eucalyptus-core/src/scripting/types.rs
@@ -91,6 +91,10 @@ impl InspectableComponent for KotlinComponents {
                 for fqcn in &self.fqcns {
                     ui.label(fqcn);
                 }
+
+                ui.separator();
+
+                ui.label("This should not be visible in the editor. this is considered `internal=true`");
             });
     }
 }
diff --git a/crates/eucalyptus-core/src/transform.rs b/crates/eucalyptus-core/src/transform.rs
index fe3d817..acf34e7 100644
--- a/crates/eucalyptus-core/src/transform.rs
+++ b/crates/eucalyptus-core/src/transform.rs
@@ -8,10 +8,11 @@ use crate::scripting::result::DropbearNativeResult;
 use crate::types::{NTransform, NVector3};
 use ::jni::JNIEnv;
 use ::jni::objects::{JObject, JValue};
+use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, Transform};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::{CollapsingHeader, ComboBox, Ui};
-use glam::{DQuat, DVec3, Vec3};
+use glam::{DMat3, DQuat, DVec3, Vec3};
 use hecs::{Entity, World};
 use splines::{Interpolation, Key, Spline};
 use std::sync::Arc;
@@ -62,8 +63,23 @@ fn project_to_path(path: &[DVec3], pos: DVec3) -> f32 {
     best_t
 }
 
-/// Linearly samples the path at `t` in [0, 1]. Matches the `Interpolation::Linear` splines used
-/// in `update_component`, so the editor gizmo stays consistent with runtime behaviour.
+fn path_tangent_rotation(path: &[DVec3], t: f32) -> DQuat {
+    let n = path.len();
+    if n < 2 {
+        return DQuat::IDENTITY;
+    }
+    let t_clamped = (t as f64 * (n - 1) as f64).clamp(0.0, (n - 2) as f64);
+    let i = t_clamped as usize;
+    let forward = (path[i + 1] - path[i]).normalize_or_zero();
+    if forward.length_squared() < 1e-10 {
+        return DQuat::IDENTITY;
+    }
+    let world_up = if forward.dot(DVec3::Y).abs() > 0.99 { DVec3::Z } else { DVec3::Y };
+    let right = world_up.cross(forward).normalize();
+    let up = forward.cross(right).normalize();
+    DQuat::from_mat3(&DMat3::from_cols(right, up, -forward))
+}
+
 fn sample_path_linear(path: &[DVec3], t: f32) -> DVec3 {
     let n = path.len();
     if n == 0 {
@@ -152,9 +168,6 @@ impl Component for OnRails {
     }
 
     fn update_component(&mut self, world: &World, _physics: &mut PhysicsState, entity: Entity, dt: f32, _graphics: Arc<SharedGraphicsContext>) {
-        if !self.enabled {
-            return;
-        }
         let n = self.path.len();
         if n < 2 {
             return;
@@ -238,8 +251,16 @@ impl Component for OnRails {
             sy.clamped_sample(t),
             sz.clamped_sample(t),
         ) {
+            let new_pos = DVec3::new(x, y, z);
+            let rot = path_tangent_rotation(&self.path, self.progress);
             if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-                et.world_mut().position = DVec3::new(x, y, z);
+                et.world_mut().position = new_pos;
+                et.world_mut().rotation = rot;
+            }
+            if let Ok(mut camera) = world.get::<&mut Camera>(entity) {
+                camera.eye = new_pos;
+                let forward = rot * DVec3::NEG_Z;
+                camera.target = new_pos + forward;
             }
         }
     }
@@ -255,12 +276,11 @@ impl InspectableComponent for OnRails {
         world: &World,
         entity: Entity,
         ui: &mut Ui,
-        _graphics: Arc<SharedGraphicsContext>,
+        graphics: Arc<SharedGraphicsContext>,
     ) {
         let n = self.path.len();
 
-        // if the entity is dragged off the path, it will 
-        if self.enabled && n >= 2 {
+        if n >= 2 {
             let expected = sample_path_linear(&self.path, self.progress);
             let current_pos = world
                 .get::<&EntityTransform>(entity)
@@ -270,8 +290,15 @@ impl InspectableComponent for OnRails {
                 if pos.distance_squared(expected) > 1e-8 {
                     self.progress = project_to_path(&self.path, pos);
                     let snapped = sample_path_linear(&self.path, self.progress);
+                    let rot = path_tangent_rotation(&self.path, self.progress);
                     if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
                         et.world_mut().position = snapped;
+                        et.world_mut().rotation = rot;
+                    }
+                    if let Ok(mut camera) = world.get::<&mut Camera>(entity) {
+                        camera.eye = snapped;
+                        let forward = rot * DVec3::NEG_Z;
+                        camera.target = snapped + forward;
                     }
                 }
             }
@@ -292,8 +319,15 @@ impl InspectableComponent for OnRails {
                 );
                 if slider.changed() && n >= 2 {
                     let pos = sample_path_linear(&self.path, self.progress);
+                    let rot = path_tangent_rotation(&self.path, self.progress);
                     if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
                         et.world_mut().position = pos;
+                        et.world_mut().rotation = rot;
+                    }
+                    if let Ok(mut camera) = world.get::<&mut Camera>(entity) {
+                        camera.eye = pos;
+                        let forward = rot * DVec3::NEG_Z;
+                        camera.target = pos + forward;
                     }
                 }
 
@@ -389,6 +423,19 @@ impl InspectableComponent for OnRails {
 
                 ui.add_space(4.0);
                 ui.checkbox(&mut self.debug, "Debug path");
+
+                if self.debug && self.path.len() >= 2 {
+                    if let Some(dd) = graphics.debug_draw.lock().as_mut() {
+                        let red = [1.0, 0.0, 0.0, 1.0];
+                        dd.draw_point(self.path[0].as_vec3(), 0.1, red);
+                        for i in 0..(self.path.len() - 1) {
+                            let a = self.path[i].as_vec3();
+                            let b = self.path[i + 1].as_vec3();
+                            dd.draw_line(a, b, red);
+                            dd.draw_point(b, 0.1, red);
+                        }
+                    }
+                }
             });
     }
 }
@@ -412,9 +459,9 @@ impl Component for EntityTransform {
     }
 
     fn init(
-        ser: &Self::SerializedForm,
+        ser: &'_ Self::SerializedForm,
         _: Arc<SharedGraphicsContext>,
-    ) -> crate::component::ComponentInitFuture<Self> {
+    ) -> crate::component::ComponentInitFuture<'_, Self> {
         Box::pin(async move { Ok((ser.clone(),)) })
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/docks/entity_list.rs b/crates/eucalyptus-editor/src/editor/docks/entity_list.rs
index bf2a525..4eaf679 100644
--- a/crates/eucalyptus-editor/src/editor/docks/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/docks/entity_list.rs
@@ -85,6 +85,7 @@ impl<'a> EditorTabViewer<'a> {
                                         BTreeMap::new();
 
                                     for (id, desc) in registry.iter_available_components() {
+                                        if desc.internal { continue; }
                                         let category = desc
                                             .category
                                             .clone()
@@ -177,11 +178,12 @@ impl<'a> EditorTabViewer<'a> {
                         }
                     }
 
-                    let children_entities = if let Ok(children) = world.get::<&Children>(entity) {
+                    let mut children_entities = if let Ok(children) = world.get::<&Children>(entity) {
                         children.children().to_vec()
                     } else {
                         Vec::new()
                     };
+                    children_entities.sort_by_key(|e| e.to_bits().get());
 
                     for child in children_entities {
                         if let Err(e) =
@@ -221,7 +223,8 @@ impl<'a> EditorTabViewer<'a> {
                     });
                 {
                     puffin::profile_scope!("entity_list.index_components");
-                    for (component_id, _) in self.component_registry.iter_available_components() {
+                    for (component_id, desc) in self.component_registry.iter_available_components() {
+                        if desc.internal { continue; }
                         for entity in self
                             .component_registry
                             .find_entities_by_numeric_id(self.world, component_id)
@@ -234,13 +237,14 @@ impl<'a> EditorTabViewer<'a> {
                     }
                 }
 
-                let root_entities: Vec<Entity> = self
+                let mut root_entities: Vec<Entity> = self
                     .world
                     .query::<Entity>()
                     .without::<&Parent>()
                     .iter()
                     .map(|e| e)
                     .collect();
+                root_entities.sort_by_key(|e| e.to_bits().get());
 
                 for entity in root_entities {
                     puffin::profile_scope!("entity_list.root_entity");
diff --git a/crates/eucalyptus-editor/src/editor/docks/viewport.rs b/crates/eucalyptus-editor/src/editor/docks/viewport.rs
index 9ed054c..39bd591 100644
--- a/crates/eucalyptus-editor/src/editor/docks/viewport.rs
+++ b/crates/eucalyptus-editor/src/editor/docks/viewport.rs
@@ -88,7 +88,6 @@ impl<'a> EditorTabViewer<'a> {
             });
             log::debug!("Pointer down - {:?}", entity);
         } else {
-            *self.selected_entity = None;
             *self.viewport_drag = None;
         }
     }
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 4af9744..e015971 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -105,7 +105,6 @@ pub struct Editor {
     pub sky_pipeline: Option<SkyPipeline>,
     pub billboard_pipeline: Option<BillboardPipeline>,
     pub kino: Option<KinoState>,
-    pub(crate) camera_bind_group: Option<wgpu::BindGroup>,
     pub(crate) default_skinning_buffer: Option<wgpu::Buffer>,
     pub(crate) default_morph_deltas_buffer: Option<wgpu::Buffer>,
     pub(crate) default_morph_weights_buffer: Option<wgpu::Buffer>,
@@ -329,7 +328,6 @@ impl Editor {
             sky_pipeline: None,
             billboard_pipeline: None,
             kino: None,
-            camera_bind_group: None,
             default_skinning_buffer: None,
             default_morph_deltas_buffer: None,
             default_morph_weights_buffer: None,
@@ -481,7 +479,8 @@ impl Editor {
             scene.scene_name
         );
 
-        let entity_ids = self.world.query::<Entity>().iter().collect::<Vec<_>>();
+        let mut entity_ids = self.world.query::<Entity>().iter().collect::<Vec<_>>();
+        entity_ids.sort_by_key(|e| e.to_bits().get());
 
         for id in entity_ids {
             let Ok(label) = self.world.get::<&Label>(id) else {
@@ -1594,6 +1593,7 @@ impl Editor {
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
         self.mipmapper = None;
         self.billboard_pipeline = Some(BillboardPipeline::new(graphics.clone()));
+        *graphics.debug_draw.lock() = Some(dropbear_engine::debug::DebugDraw::new(graphics.clone()));
         self.kino = Some(KinoState::new(
             KinoWGPURenderer::new(
                 &graphics.device,
@@ -1607,7 +1607,6 @@ impl Editor {
             KinoWinitWindowing::new(graphics.window.clone(), None),
         ));
 
-        self.camera_bind_group = None;
         self.default_skinning_buffer = None;
         self.default_morph_deltas_buffer = None;
         self.default_morph_weights_buffer = None;
@@ -1623,15 +1622,6 @@ impl Editor {
         let active_camera = self.active_camera.lock().clone();
         if let Some(camera_entity) = active_camera {
             if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() {
-                self.camera_bind_group = Some(graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("editor camera bind group"),
-                    layout: &graphics.layouts.camera_bind_group_layout,
-                    entries: &[wgpu::BindGroupEntry {
-                        binding: 0,
-                        resource: camera.buffer().as_entire_binding(),
-                    }],
-                }));
-
                 let max_skinning_matrices = 256usize;
                 let identity = vec![Mat4::IDENTITY; max_skinning_matrices];
                 let skinning_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 75202c5..8df8ecc 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::component::KotlinComponentDecl;
 use eucalyptus_core::entity_status::EntityStatus;
 use eucalyptus_core::physics::collider::ColliderGroup;
 use eucalyptus_core::physics::collider::ColliderShapeKey;
@@ -22,6 +23,7 @@ use eucalyptus_core::states::{Label, WorldLoadingStatus, SCENES};
 use eucalyptus_core::ui::HUDComponent;
 use hecs::Entity;
 use log;
+use magna_carta::ScriptManifest;
 use parking_lot::Mutex;
 use std::collections::HashMap;
 use std::sync::Arc;
@@ -36,6 +38,41 @@ use kino_ui::rendering::KinoRenderTargetId;
 
 impl Scene for Editor {
     fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        {
+            let src_path = {
+                let project = PROJECT.read();
+                project.project_path.join("src")
+            };
+            if src_path.exists() {
+                if let Some(registry) = Arc::get_mut(&mut self.component_registry) {
+                    let mut processor = match magna_carta::KotlinProcessor::new() {
+                        Ok(p) => p,
+                        Err(e) => {
+                            log::warn!("Failed to create KotlinProcessor for component scan: {}", e);
+                            return;
+                        }
+                    };
+                    let mut manifest = ScriptManifest::new();
+                    if let Err(e) = magna_carta::visit_kotlin_files(&src_path, &mut processor, &mut manifest) {
+                        log::warn!("Kotlin component scan failed: {}", e);
+                    } else {
+                        let count = manifest.components().len();
+                        for item in manifest.components() {
+                            registry.register_kotlin_descriptor(KotlinComponentDecl {
+                                fqcn: item.fqcn().to_owned(),
+                                type_name: item.simple_name().to_owned(),
+                                category: None,
+                                description: None,
+                            });
+                        }
+                        log::info!("Registered {} Kotlin component descriptor(s) from project sources", count);
+                    }
+                } else {
+                    log::warn!("Could not obtain exclusive access to component_registry for Kotlin component scan");
+                }
+            }
+        }
+
         self.current_scene_name = {
             let last_opened = {
                 let project = PROJECT.read();
@@ -419,14 +456,10 @@ impl Scene for Editor {
         let Some(camera) = q else {
             return;
         };
+
         log_once::debug_once!("Camera ready");
         log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
-        let Some(camera_bind_group) = self.camera_bind_group.as_ref() else {
-            log_once::warn_once!("Camera bind group not ready");
-            return;
-        };
-
         // clear viewport render pass
         {
             puffin::profile_scope!("Clearing viewport");
@@ -670,7 +703,7 @@ impl Scene for Editor {
                         if !light.component.visible {
                             continue;
                         }
-                        render_pass.draw_light_model(&model, camera_bind_group, &light.bind_group);
+                        render_pass.draw_light_model(&model, &camera.bind_group, &light.bind_group);
                     }
                 }
             } else {
@@ -1034,7 +1067,7 @@ impl Scene for Editor {
                     });
 
                     render_pass.set_pipeline(&collider_pipeline.pipeline);
-                    render_pass.set_bind_group(0, camera_bind_group, &[]);
+                    render_pass.set_bind_group(0, Some(&camera.bind_group), &[]);
 
                     let mut instances_by_shape: HashMap<
                         ColliderShapeKey,
@@ -1242,6 +1275,12 @@ impl Scene for Editor {
             }
         }
 
+        // debug draw flush
+        if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() {
+            let view_proj = Mat4::from_cols_array_2d(&camera.uniform.view_proj);
+            debug_draw.flush(graphics.clone(), &mut encoder, view_proj);
+        }
+
         hdr.process(&mut encoder, &graphics.viewport_texture.view);
 
         if let Err(e) = encoder.submit() {
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index db35b53..b3f1497 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -7,6 +7,7 @@ use egui::Align2;
 use eucalyptus_core::camera::{CameraComponent, CameraType};
 use eucalyptus_core::scene::SceneEntity;
 use eucalyptus_core::scripting::{BuildStatus, build_jvm};
+use eucalyptus_core::scripting::types::KotlinComponents;
 use eucalyptus_core::states::{Label, PROJECT};
 use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console};
 use std::collections::{HashMap, HashSet};
@@ -546,6 +547,28 @@ impl SignalController for Editor {
                     Ok(())
                 }
                 Signal::AddComponent(entity, component) => {
+                    if let Some(kc_box) = component.as_any().downcast_ref::<KotlinComponents>() {
+                        for fqcn in kc_box.fqcns.clone() {
+                            if let Ok(mut existing) = self.world.get::<&mut KotlinComponents>(entity) {
+                                if existing.has(&fqcn) {
+                                    warn!("Entity {:?} already has Kotlin component '{}'", entity, fqcn);
+                                } else {
+                                    existing.attach(&fqcn);
+                                    success!("Added Kotlin component '{}' to entity {:?}", fqcn, entity);
+                                }
+                            } else {
+                                let mut new_kc = KotlinComponents::default();
+                                new_kc.attach(&fqcn);
+                                if let Err(e) = self.world.insert_one(entity, new_kc) {
+                                    warn!("Failed to insert KotlinComponents for '{}': {}", fqcn, e);
+                                } else {
+                                    success!("Added Kotlin component '{}' to entity {:?}", fqcn, entity);
+                                }
+                            }
+                        }
+                        return Ok(());
+                    }
+
                     let registry = self.component_registry.clone();
 
                     let Some(component_id) = registry.id_for_component(component.as_ref()) else {
diff --git a/crates/magna-carta/src/generator/jvm.rs b/crates/magna-carta/src/generator/jvm.rs
index 1552f46..95d201f 100644
--- a/crates/magna-carta/src/generator/jvm.rs
+++ b/crates/magna-carta/src/generator/jvm.rs
@@ -112,17 +112,37 @@ impl Generator for KotlinJVMGenerator {
 
         writeln!(output)?;
         writeln!(output, "object ComponentManager {{")?;
+        writeln!(output, "    private val instances: MutableMap<String, com.dropbear.ecs.NativeComponent> = mutableMapOf()")?;
+        writeln!(output)?;
+        writeln!(output, "    private fun registerOne(component: com.dropbear.ecs.NativeComponent) {{")?;
+        writeln!(output, "        component.register()")?;
+        writeln!(output, "        instances[component.fullyQualifiedTypeName] = component")?;
+        writeln!(output, "    }}")?;
+        writeln!(output)?;
         writeln!(output, "    @Synchronized")?;
         writeln!(output, "    fun registerAll() {{")?;
         for component in manifest.components() {
             writeln!(
                 output,
-                "        com.dropbear.ecs.registerKotlinComponentType({fqcn}, {simple}, null, null)",
-                fqcn = format!("\"{}\"", component.fqcn()),
-                simple = format!("\"{}\"", component.simple_name()),
+                "        registerOne({simple}())",
+                simple = component.simple_name(),
             )?;
         }
         writeln!(output, "    }}")?;
+        writeln!(output)?;
+        writeln!(output, "    fun updateKotlinComponent(fqcn: String, entityId: Long, dt: Double) {{")?;
+        writeln!(output, "        val instance = instances[fqcn] ?: return")?;
+        writeln!(output, "        val engine = com.dropbear.DropbearEngine(com.dropbear.DropbearEngine.native)")?;
+        writeln!(output, "        instance.setCurrentEntity(entityId)")?;
+        writeln!(output, "        instance.updateComponent(engine, dt)")?;
+        writeln!(output, "        instance.clearCurrentEntity()")?;
+        writeln!(output, "    }}")?;
+        writeln!(output)?;
+        writeln!(output, "    fun inspectKotlinComponent(fqcn: String) {{")?;
+        writeln!(output, "        val instance = instances[fqcn] ?: return")?;
+        writeln!(output, "        val engine = com.dropbear.DropbearEngine(com.dropbear.DropbearEngine.native)")?;
+        writeln!(output, "        instance.inspect(engine)")?;
+        writeln!(output, "    }}")?;
         writeln!(output, "}}")?;
 
         Ok(output)
diff --git a/crates/magna-carta/src/generator/native.rs b/crates/magna-carta/src/generator/native.rs
index 2a5e0a1..ac5508c 100644
--- a/crates/magna-carta/src/generator/native.rs
+++ b/crates/magna-carta/src/generator/native.rs
@@ -556,16 +556,36 @@ object ScriptManager {{
 
         writeln!(output)?;
         writeln!(output, "object ComponentManager {{")?;
+        writeln!(output, "    private val instances: MutableMap<String, com.dropbear.ecs.NativeComponent> = mutableMapOf()")?;
+        writeln!(output)?;
+        writeln!(output, "    private fun registerOne(component: com.dropbear.ecs.NativeComponent) {{")?;
+        writeln!(output, "        component.register()")?;
+        writeln!(output, "        instances[component.fullyQualifiedTypeName] = component")?;
+        writeln!(output, "    }}")?;
+        writeln!(output)?;
         writeln!(output, "    fun registerAll() {{")?;
         for component in manifest.components() {
             writeln!(
                 output,
-                "        com.dropbear.ecs.registerKotlinComponentType({fqcn}, {simple}, null, null)",
-                fqcn = format!("\"{}\"", component.fqcn()),
-                simple = format!("\"{}\"", component.simple_name()),
+                "        registerOne({simple}())",
+                simple = component.simple_name(),
             )?;
         }
         writeln!(output, "    }}")?;
+        writeln!(output)?;
+        writeln!(output, "    fun updateKotlinComponent(fqcn: String, entityId: Long, dt: Double) {{")?;
+        writeln!(output, "        val instance = instances[fqcn] ?: return")?;
+        writeln!(output, "        val engine = com.dropbear.DropbearEngine(com.dropbear.DropbearEngine.native)")?;
+        writeln!(output, "        instance.setCurrentEntity(entityId)")?;
+        writeln!(output, "        instance.updateComponent(engine, dt)")?;
+        writeln!(output, "        instance.clearCurrentEntity()")?;
+        writeln!(output, "    }}")?;
+        writeln!(output)?;
+        writeln!(output, "    fun inspectKotlinComponent(fqcn: String) {{")?;
+        writeln!(output, "        val instance = instances[fqcn] ?: return")?;
+        writeln!(output, "        val engine = com.dropbear.DropbearEngine(com.dropbear.DropbearEngine.native)")?;
+        writeln!(output, "        instance.inspect(engine)")?;
+        writeln!(output, "    }}")?;
         writeln!(output, "}}")?;
 
         // ADD CNAME FUNCTIONS HERE
@@ -724,6 +744,20 @@ fun dropbear_destroy_all(): Int {{
     return ScriptManager.destroyAll()
 }}
 
+@CName("dropbear_update_kotlin_component")
+fun dropbear_update_kotlin_component(fqcn: String?, entityId: ULong, dt: Double): Int {{
+    if (fqcn == null) return -1
+    ComponentManager.updateKotlinComponent(fqcn, entityId.toLong(), dt)
+    return 0
+}}
+
+@CName("dropbear_inspect_kotlin_component")
+fun dropbear_inspect_kotlin_component(fqcn: String?): Int {{
+    if (fqcn == null) return -1
+    ComponentManager.inspectKotlinComponent(fqcn)
+    return 0
+}}
+
 @CName("dropbear_get_last_error")
 fun dropbear_get_last_error(): String? {{
     return com.dropbear.lastErrorMessage
diff --git a/crates/redback-runtime/Cargo.toml b/crates/redback-runtime/Cargo.toml
index 0737371..05a2076 100644
--- a/crates/redback-runtime/Cargo.toml
+++ b/crates/redback-runtime/Cargo.toml
@@ -18,6 +18,7 @@ tokio.workspace = true
 chrono.workspace = true
 env_logger.workspace = true
 colored.workspace = true
+jni.workspace = true
 parking_lot.workspace = true
 postcard.workspace = true
 ron.workspace = true
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 0a9b875..0ac45dc 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -35,6 +35,7 @@ use log::error;
 use std::collections::HashMap;
 use std::path::PathBuf;
 use std::sync::Arc;
+use jni::objects::JValue;
 use wgpu::SurfaceConfiguration;
 use wgpu::util::DeviceExt;
 use winit::window::Fullscreen;
@@ -137,7 +138,6 @@ pub struct PlayMode {
     instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>,
     animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>,
     sky_pipeline: Option<SkyPipeline>,
-    camera_bind_group: Option<wgpu::BindGroup>,
     default_skinning_buffer: Option<wgpu::Buffer>,
     default_morph_deltas_buffer: Option<wgpu::Buffer>,
     default_morph_weights_buffer: Option<wgpu::Buffer>,
@@ -221,7 +221,7 @@ impl PlayMode {
             event_collector,
             display_settings: DisplaySettings {
                 window_mode: WindowMode::Windowed,
-                maintain_aspect_ratio: true,
+                maintain_aspect_ratio: false,
                 vsync: false,
                 last_window_mode: WindowMode::BorderlessFullscreen,
                 last_vsync: true,
@@ -229,7 +229,6 @@ impl PlayMode {
             },
             kino: None,
             sky_pipeline: None,
-            camera_bind_group: None,
             default_skinning_buffer: None,
             default_morph_deltas_buffer: None,
             default_morph_weights_buffer: None,
@@ -257,7 +256,6 @@ impl PlayMode {
         self.shader_globals = None;
         self.kino = None;
         self.sky_pipeline = None;
-        self.camera_bind_group = None;
         self.default_skinning_buffer = None;
         self.default_morph_deltas_buffer = None;
         self.default_morph_weights_buffer = None;
@@ -279,7 +277,6 @@ impl PlayMode {
             Some("runtime shader globals"),
         ));
 
-        let mut camera_bind_group = None;
         let mut pending_sky_pipeline = None;
 
         if self.default_skinning_buffer.is_none() {
@@ -399,17 +396,6 @@ impl PlayMode {
 
         if let Some(camera_entity) = self.active_camera {
             if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() {
-                camera_bind_group = Some(graphics.device.create_bind_group(
-                    &wgpu::BindGroupDescriptor {
-                        label: Some("runtime camera bind group"),
-                        layout: &graphics.layouts.camera_bind_group_layout,
-                        entries: &[wgpu::BindGroupEntry {
-                            binding: 0,
-                            resource: camera.buffer().as_entire_binding(),
-                        }],
-                    },
-                ));
-
                 match sky_texture_result {
                     Ok(sky_texture) => {
                         pending_sky_pipeline = Some(SkyPipeline::new(
@@ -442,7 +428,6 @@ impl PlayMode {
             error!("Unable to create bind groups without an active camera");
         }
 
-        self.camera_bind_group = camera_bind_group;
         if let Some(sky_pipeline) = pending_sky_pipeline {
             self.sky_pipeline = Some(sky_pipeline);
         }
@@ -498,6 +483,46 @@ impl PlayMode {
             log::debug!("Loaded scripts successfully!");
         }
 
+        // todo: this wont work for native contexts.
+        if let Some(registry) = Arc::get_mut(&mut self.component_registry) {
+            registry.drain_kotlin_queue();
+
+            if let Some(jvm) = eucalyptus_core::scripting::jni::GLOBAL_JVM.get() {
+                let jvm = jvm.clone();
+                registry.set_kotlin_update_fn(move |fqcn: &str, entity_id: u64, dt: f32| {
+                    let Ok(mut env) = jvm.attach_current_thread() else {
+                        log::warn!("kotlin_update_fn: failed to attach JVM thread");
+                        return;
+                    };
+                    let Ok(fqcn_jstr) = env.new_string(fqcn) else { return };
+                    let Ok(class) = env.find_class("com/dropbear/decl/ComponentManager") else {
+                        log::warn!(
+                            "kotlin_update_fn: ComponentManager class not found - has the project JAR been loaded?"
+                        );
+                        return;
+                    };
+                    if let Err(e) = env.call_static_method(
+                        class,
+                        "updateKotlinComponent",
+                        "(Ljava/lang/String;JD)V",
+                        &[
+                            JValue::Object(&fqcn_jstr),
+                            JValue::Long(entity_id as i64),
+                            JValue::Double(dt as f64),
+                        ],
+                    ) {
+                        log::warn!(
+                            "kotlin_update_fn: updateKotlinComponent('{}', {}) failed: {:?}",
+                            fqcn, entity_id, e
+                        );
+                        let _ = env.exception_clear();
+                    }
+                });
+            }
+        } else {
+            log::warn!("drain_kotlin_queue: could not get exclusive access to component_registry; Kotlin component descriptors were not registered");
+        }
+
         self.scripts_ready = true;
         log::debug!("Scripts reloaded successfully!");
     }
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 267793e..01f8cda 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -630,11 +630,6 @@ impl Scene for PlayMode {
         log_once::debug_once!("Camera ready");
         log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
-        let Some(camera_bind_group) = self.camera_bind_group.as_ref() else {
-            log_once::warn_once!("Camera bind group not ready");
-            return;
-        };
-
         // clear viewport render pass
         {
             puffin::profile_scope!("Clearing viewport");
@@ -879,7 +874,7 @@ impl Scene for PlayMode {
                         if !light.component.visible {
                             continue;
                         }
-                        render_pass.draw_light_model(&model, camera_bind_group, &light.bind_group);
+                        render_pass.draw_light_model(&model, &camera.bind_group, &light.bind_group);
                     }
                 }
             } else {
diff --git a/scripting/commonMain/kotlin/com/dropbear/EntityRef.kt b/scripting/commonMain/kotlin/com/dropbear/EntityRef.kt
index 4c56508..8efd571 100644
--- a/scripting/commonMain/kotlin/com/dropbear/EntityRef.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/EntityRef.kt
@@ -2,7 +2,6 @@ package com.dropbear
 
 import com.dropbear.ecs.Component
 import com.dropbear.ecs.ComponentType
-import com.dropbear.physics.RigidBody
 
 /**
  * A reference to an ECS Entity stored inside the dropbear engine.
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
index 8311721..66698c6 100644
--- a/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
@@ -1,10 +1,12 @@
 package com.dropbear.components.camera
 
+import com.dropbear.DropbearEngine
 import com.dropbear.ecs.NativeComponent
 import com.dropbear.math.Vector3d
 import com.dropbear.physics.ColliderShape
 import com.dropbear.physics.Physics
 import com.dropbear.ui.Inspect
+import com.dropbear.ui.UIInstructionSet
 import com.dropbear.ui.WidgetType
 
 /**
@@ -55,6 +57,11 @@ class SpringyCameraController: NativeComponent(
         return playerPos + (dir * currentDistance)
     }
 
-    override fun inspect() {}
-    override fun updateComponent() {}
+    override fun inspect(engine: DropbearEngine): UIInstructionSet? {
+        return null
+    }
+
+    override fun updateComponent(engine: DropbearEngine, deltaTime: Double) {
+
+    }
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt b/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt
index 1b2a5a8..f7c70f3 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt
@@ -1,22 +1,6 @@
 package com.dropbear.ecs
 
-import com.dropbear.EntityId
-
-/**
- * Marks a class as a user-defined ECS component for the dropbear engine.
- *
- * Annotated classes must extend [NativeComponent]. The `magna-carta` tool scans for this annotation
- * at build time to generate a [ComponentManager] that registers each component type with the
- * native engine's ComponentRegistry via [registerKotlinComponentType].
- *
- * Retention is [AnnotationRetention.RUNTIME] so JNI reflection can discover types at startup
- * without relying on build-time scanning alone.
- */
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.RUNTIME)
-annotation class EcsComponent
-
-internal expect fun registerKotlinComponentType(
+expect fun registerKotlinComponentType(
     fqcn: String,
     typeName: String?,
     category: String?,
@@ -28,87 +12,4 @@ internal expect fun registerKotlinComponentType(
  *
  * Extend [NativeComponent] or [ExternalComponent], not this class directly.
  */
-sealed class Component
-
-/**
- * States a Kotlin-defined ECS component.
- *
- * Annotate subclasses with [@EcsComponent][EcsComponent] so that `magna-carta` can discover them
- * at build time and emit a `ComponentManager` that calls [register] for every discovered type.
- *
- * @param fullyQualifiedTypeName The fully qualified class name used as the stable key inside the
- *   Rust ComponentRegistry (e.g. `"com.example.PlayerHealth"`). Prefer using the literal string
- *   directly so the value survives code shrinking / obfuscation.
- * @param typeName Human-readable short name shown in the editor (e.g. `"PlayerHealth"`).
- * @param category Optional grouping shown in the "Add Component" picker (e.g. `"Gameplay"`).
- * @param description Optional one-line description shown as a tooltip in the editor.
- */
-abstract class NativeComponent(
-    val fullyQualifiedTypeName: String,
-    val typeName: String,
-    val category: String? = null,
-    val description: String? = null,
-): Component() {
-    /**
-     * Registers this component type with the engine's ComponentRegistry.
-     *
-     * This is called once per type at startup
-     */
-    fun register() {
-        registerKotlinComponentType(fullyQualifiedTypeName, typeName, category, description)
-    }
-
-    /**
-     * Renders the inspector UI for this component in the editor.
-     *
-     * Called by the editor for every entity that has this component type attached.
-     */
-    abstract fun inspect()
-
-    /**
-     * Updates the component for all entities that contain this component.
-     */
-    abstract fun updateComponent()
-}
-
-/**
- * Proxy for an ECS component that is defined outside Kotlin, typically in a Rust crate or
- * another native shared library, and registered with the engine's ComponentRegistry under a
- * known [fullyQualifiedTypeName].
- *
- * Use this when you need to query whether an entity carries a non-Kotlin component, without
- * owning or duplicating its data on the Kotlin side.
- *
- * @param fullyQualifiedTypeName The fully qualified type name that the external component was
- *   registered under in the ComponentRegistry (e.g. `"eucalyptus_core::components::RigidBody"`).
- */
-abstract class ExternalComponent(
-    val fullyQualifiedTypeName: String,
-): Component() {
-}
-
-/**
- * An interface that all components must include to be queryable.
- *
- * Basically looks like this:
- * ```
- * companion object : ComponentType<T> {
- *     override fun get(entityId: EntityId): T? {
- *         // chuck in whatever validation stuff you want idk as long as it works...
- *     }
- * }
- * ```
- *
- * @param T The component that can be queried. It must extend the [Component] class.
- */
-interface ComponentType<T : Component> {
-    /**
-     * Uses the FFI to check if the entity is a component.
-     *
-     * Since most components are technically just classes with a ctor containing the parentEntity's id,
-     * and getters and setters, it just needs to query the world.
-     *
-     * @return The components type ([T]) if it exists or `null` if not
-     */
-    fun get(entityId: EntityId): T?
-}
\ No newline at end of file
+sealed class Component
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ecs/ComponentType.kt b/scripting/commonMain/kotlin/com/dropbear/ecs/ComponentType.kt
new file mode 100644
index 0000000..d50aa7d
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ecs/ComponentType.kt
@@ -0,0 +1,29 @@
+package com.dropbear.ecs
+
+import com.dropbear.EntityId
+
+/**
+ * An interface that all components must include to be queryable.
+ *
+ * Basically looks like this:
+ * ```
+ * companion object : ComponentType<T> {
+ *     override fun get(entityId: EntityId): T? {
+ *         // chuck in whatever validation stuff you want idk as long as it works...
+ *     }
+ * }
+ * ```
+ *
+ * @param T The component that can be queried. It must extend the [Component] class.
+ */
+interface ComponentType<T : Component> {
+    /**
+     * Uses the FFI to check if the entity is a component.
+     *
+     * Since most components are technically just classes with a ctor containing the parentEntity's id,
+     * and getters and setters, it just needs to query the world.
+     *
+     * @return The components type ([T]) if it exists or `null` if not
+     */
+    fun get(entityId: EntityId): T?
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ecs/EcsComponent.kt b/scripting/commonMain/kotlin/com/dropbear/ecs/EcsComponent.kt
new file mode 100644
index 0000000..1a541ea
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ecs/EcsComponent.kt
@@ -0,0 +1,15 @@
+package com.dropbear.ecs
+
+/**
+ * Marks a class as a user-defined ECS component for the dropbear engine.
+ *
+ * Annotated classes must extend [NativeComponent]. The `magna-carta` tool scans for this annotation
+ * at build time to generate a [ComponentManager] that registers each component type with the
+ * native engine's ComponentRegistry via [registerKotlinComponentType].
+ *
+ * Retention is [AnnotationRetention.RUNTIME] so JNI reflection can discover types at startup
+ * without relying on build-time scanning alone.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class EcsComponent
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ecs/ExternalComponent.kt b/scripting/commonMain/kotlin/com/dropbear/ecs/ExternalComponent.kt
new file mode 100644
index 0000000..ae07d71
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ecs/ExternalComponent.kt
@@ -0,0 +1,17 @@
+package com.dropbear.ecs
+
+/**
+ * Proxy for an ECS component that is defined outside Kotlin, typically in a Rust crate or
+ * another native shared library, and registered with the engine's ComponentRegistry under a
+ * known [fullyQualifiedTypeName].
+ *
+ * Use this when you need to query whether an entity carries a non-Kotlin component, without
+ * owning or duplicating its data on the Kotlin side.
+ *
+ * @param fullyQualifiedTypeName The fully qualified type name that the external component was
+ *   registered under in the ComponentRegistry (e.g. `"eucalyptus_core::components::RigidBody"`).
+ */
+abstract class ExternalComponent(
+    val fullyQualifiedTypeName: String,
+): Component() {
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ecs/NativeComponent.kt b/scripting/commonMain/kotlin/com/dropbear/ecs/NativeComponent.kt
new file mode 100644
index 0000000..ae0c3b2
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ecs/NativeComponent.kt
@@ -0,0 +1,71 @@
+package com.dropbear.ecs
+
+import com.dropbear.DropbearEngine
+import com.dropbear.EntityId
+import com.dropbear.EntityRef
+import com.dropbear.ui.UIInstructionSet
+
+/**
+ * States a Kotlin-defined ECS component.
+ *
+ * Annotate subclasses with [@EcsComponent][EcsComponent] so that `magna-carta` can discover them
+ * at build time and emit a `ComponentManager` that calls [register] for every discovered type.
+ *
+ * @param fullyQualifiedTypeName The fully qualified class name used as the stable key inside the
+ *   Rust ComponentRegistry (e.g. `"com.example.PlayerHealth"`). Prefer using the literal string
+ *   directly so the value survives code shrinking / obfuscation.
+ * @param typeName Human-readable short name shown in the editor (e.g. `"PlayerHealth"`).
+ * @param category Optional grouping shown in the "Add Component" picker (e.g. `"Gameplay"`).
+ * @param description Optional one-line description shown as a tooltip in the editor.
+ */
+abstract class NativeComponent(
+    val fullyQualifiedTypeName: String,
+    val typeName: String,
+    val category: String? = null,
+    val description: String? = null,
+): Component() {
+    /**
+     * The entity currently being updated.
+     *
+     * Set by the engine immediately before [updateComponent] is called and cleared
+     * immediately after. It is `null` outside of an active update call.
+     */
+    var currentEntity: EntityRef? = null
+        private set
+
+    /**
+     * Registers this component type with the engine's ComponentRegistry.
+     *
+     * This is called once per type at startup by the generated [ComponentManager].
+     */
+    fun register() {
+        registerKotlinComponentType(fullyQualifiedTypeName, typeName, category, description)
+    }
+
+    /** Internal: sets [currentEntity] before calling [updateComponent]. */
+    fun setCurrentEntity(entity: Long) {
+        currentEntity = EntityRef(EntityId(entity))
+    }
+
+    /** Internal: clears [currentEntity] after [updateComponent] returns. */
+    fun clearCurrentEntity() {
+        currentEntity = null
+    }
+
+    /**
+     * Renders the inspector UI for this component in the editor.
+     *
+     * Called by the editor for every entity that has this component type attached.
+     */
+    abstract fun inspect(engine: DropbearEngine): UIInstructionSet?
+
+    /**
+     * Called once per frame for every entity that holds this component type.
+     *
+     * [currentEntity] is set to the entity being processed for the duration of this call.
+     *
+     * @param engine  The engine facade for queries and mutations.
+     * @param deltaTime Seconds elapsed since the last frame.
+     */
+    abstract fun updateComponent(engine: DropbearEngine, deltaTime: Double)
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt b/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
index bf52a09..b0c7017 100644
--- a/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
@@ -5,8 +5,6 @@ import com.dropbear.ecs.ComponentType
 import com.dropbear.ecs.ExternalComponent
 import com.dropbear.math.Quaterniond
 import com.dropbear.math.Vector3d
-import com.dropbear.math.degreesToRadians
-import kotlin.math.cos
 
 class KinematicCharacterController(
     val entity: EntityId,
diff --git a/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt
index 28deed7..f6b2610 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt
@@ -4,7 +4,7 @@ import com.dropbear.DropbearEngine
 import com.dropbear.EntityId
 import com.dropbear.components.ComponentNative
 
-internal actual fun registerKotlinComponentType(
+actual fun registerKotlinComponentType(
     fqcn: String,
     typeName: String?,
     category: String?,
diff --git a/scripting/jvmMain/kotlin/com/dropbear/host/SystemManager.kt b/scripting/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
index 592c4cb..3453a84 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
@@ -43,7 +43,7 @@ class SystemManager(
                     typed.clearCurrentEntity()
                     typedInstances.add(typed)
                     Logger.trace("Instantiated system: ${typed.javaClass.name} for tag: $tag")
-                } catch (ex: Exception) {
+                } catch (ex: Throwable) {
                     Logger.error("Failed to instantiate system ${typed.javaClass.name}: ${ex.message}")
                 }
             }
@@ -64,13 +64,13 @@ class SystemManager(
                 system.attachEngine(engine)
                 system.clearCurrentEntity()
                 system.destroy(engine)
-            } catch (ex: Exception) {
+            } catch (ex: Throwable) {
                 Logger.error("Failed to destroy system ${system.javaClass.name} for tag $tag: ${ex.message}")
             } finally {
                 try {
                     system.clearCurrentEntity()
-                } catch (_: Exception) {
-                    // ignore
+                } catch (e: Throwable) {
+                    Logger.warn("Failed to clear current entity for system ${system.javaClass.name} for tag $tag: ${e.message}")
                 }
             }
         }
@@ -107,13 +107,13 @@ class SystemManager(
                     system.attachEngine(engine)
                     system.clearCurrentEntity()
                     system.load(engine)
-                } catch (ex: Exception) {
+                } catch (ex: Throwable) {
                     Logger.error("Failed to load system ${system.javaClass.name} for tag $tag: ${ex.message}")
                 } finally {
                     try {
                         system.clearCurrentEntity()
-                    } catch (_: Exception) {
-                        // ignore
+                    } catch (e: Throwable) {
+                        Logger.warn("Failed to clear current entity for system ${system.javaClass.name} for tag $tag: ${e.message}")
                     }
                 }
             }
@@ -126,13 +126,13 @@ class SystemManager(
                 system.attachEngine(engine)
                 system.clearCurrentEntity()
                 system.load(engine)
-            } catch (ex: Exception) {
+            } catch (ex: Throwable) {
                 Logger.error("Failed to load system ${system.javaClass.name} for tag $tag: ${ex.message}")
             } finally {
                 try {
                     system.clearCurrentEntity()
-                } catch (_: Exception) {
-                    // ignore
+                } catch (e: Throwable) {
+                    Logger.warn("Failed to clear current entity for system ${system.javaClass.name} for tag $tag: ${e.message}")
                 }
             }
         }
@@ -156,13 +156,13 @@ class SystemManager(
                     system.attachEngine(engine)
                     system.setCurrentEntity(entityId)
                     system.load(engine)
-                } catch (ex: Exception) {
+                } catch (ex: Throwable) {
                     Logger.error("Failed to load system ${system.javaClass.name} for entity $entityId: ${ex.message}")
                 } finally {
                     try {
                         system.clearCurrentEntity()
-                    } catch (_: Exception) {
-                        // ignore
+                    } catch (e: Throwable) {
+                        Logger.warn("Failed to clear current entity for system ${system.javaClass.name} for tag $tag: ${e.message}")
                     }
                 }
             }
@@ -214,8 +214,8 @@ class SystemManager(
                     system.attachEngine(engine)
                     system.setCurrentEntity(entityId)
                     system.update(engine, deltaTime)
-                } catch (ex: Exception) {
-                    Logger.error("Failed to update system ${system.javaClass.name} for entity $entityId: ${ex.message}")
+                } catch (e: Throwable) {
+                    Logger.error("Failed to update system ${system.javaClass.name} for entity $entityId: ${e.message}")
                 }
             }
         }
@@ -244,8 +244,8 @@ class SystemManager(
                     system.attachEngine(engine)
                     system.setCurrentEntity(entityId)
                     system.physicsUpdate(engine, deltaTime)
-                } catch (ex: Exception) {
-                    Logger.error("Failed to physics update system ${system.javaClass.name} for entity $entityId: ${ex.message}")
+                } catch (e: Throwable) {
+                    Logger.error("Failed to physics update system ${system.javaClass.name} for entity $entityId: ${e.message}")
                 }
             }
         }
@@ -263,7 +263,7 @@ class SystemManager(
                 system.attachEngine(engine)
                 system.setCurrentEntity(entityId)
                 system.collisionEvent(engine, event)
-            } catch (ex: Exception) {
+            } catch (ex: Throwable) {
                 Logger.error("Failed to deliver collision event to ${system.javaClass.name} for entity $entityId: ${ex.message}")
             }
         }
@@ -281,7 +281,7 @@ class SystemManager(
                 system.attachEngine(engine)
                 system.setCurrentEntity(entityId)
                 system.collisionForceEvent(engine, event)
-            } catch (ex: Exception) {
+            } catch (ex: Throwable) {
                 Logger.error("Failed to deliver contact-force event to ${system.javaClass.name} for entity $entityId: ${ex.message}")
             }
         }
@@ -297,7 +297,7 @@ class SystemManager(
                 system.attachEngine(engine)
                 system.clearCurrentEntity()
                 system.update(engine, deltaTime)
-            } catch (ex: Exception) {
+            } catch (ex: Throwable) {
                 Logger.error("Failed to update system ${system.javaClass.name} for tag $tag: ${ex.message}")
             }
         }
@@ -309,7 +309,7 @@ class SystemManager(
                 system.attachEngine(engine)
                 system.clearCurrentEntity()
                 system.physicsUpdate(engine, deltaTime)
-            } catch (ex: Exception) {
+            } catch (ex: Throwable) {
                 Logger.error("Failed to physics update system ${system.javaClass.name} for tag $tag: ${ex.message}")
             }
         }
diff --git a/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt
index df305ba..16e1c9f 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt
@@ -3,7 +3,7 @@ package com.dropbear.ecs
 import com.dropbear.EntityId
 
 
-internal actual fun registerKotlinComponentType(
+actual fun registerKotlinComponentType(
     fqcn: String,
     typeName: String?,
     category: String?,