kitgit

tirbofish/dropbear · commit

0aabf452e529c31042eab04a607b1a0e5d689705

refactor: move exports to `eucalyptus-exports` crate to clean up eucalyptus-core.

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-04-03 12:01

view full diff

diff --git a/README.md b/README.md
index 44b8560..adc6a95 100644
--- a/README.md
+++ b/README.md
@@ -11,20 +11,19 @@ If you might have not realised, all the crates/projects names are after Australi
 
 ## Projects
 
-- [dropbear-engine](https://github.com/tirbofish/dropbear/tree/main/crates/dropbear-engine) is the rendering engine that uses wgpu and the main name of the project.
-- [eucalyptus-editor](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
-- [eucalyptus-core](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other.
-- [redback-runtime](https://github.com/tirbofish/dropbear/tree/main/crates/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
-- [magna-carta](https://github.com/tirbofish/dropbear/tree/main/crates/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching.
-- [kino-ui](https://github.com/tirbofish/dropbear/tree/main/crates/kino-ui) is the main runtime-side UI system to render widgets and elements. 
-
-[//]: # (- [eucalyptus-sdk]&#40;https://github.com/tirbofish/dropbear/tree/main/eucalyptus-sdk&#41; is used to develop plugins to be used with the `eucalyptus-editor`)
+- [dropbear-engine](crates/dropbear-engine) is the rendering engine that uses wgpu and the main name of the project.
+- [eucalyptus-editor](crates/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
+- [eucalyptus-core](crates/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other.
+- [eucalyptus-exports](crates/eucalyptus-core) is used to scripting-based logic for the editor and the engine itself. 
+- [redback-runtime](crates/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
+- [magna-carta](crates/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching.
+- [kino-ui](crates/kino-ui) is the main runtime-side UI system to render widgets and elements. 
 
 ### Related Projects
 
-- [dropbear_future-queue](https://github.com/tirbofish/dropbear/tree/main/dropbear_future-queue) is a handy library for dealing with async in a sync context
+- [dropbear_future-queue](crates/dropbear_future-queue) is a handy library for dealing with async in a sync context
 - [model_to_image](https://github.com/tirbofish/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` and a custom made rasterizer. _(very crude but usable)_
-- [slank](https://github.com/tirbofish/dropbear/tree/main/crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time.
+- [slank](crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time.
 
 ## Build
 
@@ -45,7 +44,7 @@ yeah thats about it
 
 <summary>Linux</summary>
 
-With Unix systems (macOS not tested), you will have to download a couple of dependencies if building locally:
+With Linux systems (macOS not tested), you will have to download a couple of dependencies if building locally:
 
 ```bash
 # ubuntu
@@ -98,8 +97,9 @@ cargo build
 
 <summary>For the engine developers</summary>
 
-Ensure that `cargo build` is ran for each iteration of testing instead of `cargo build -p eucalyptus-editor` due to some weird ABI issue with Rust. If you do only want to build
-a specific package, ensure you run `cargo build -p {package} -p eucalyptus-core`. 
+Ensure that `cargo build` is run for each iteration of testing instead of `cargo build -p eucalyptus-editor` due to 
+some weird ABI issue with Rust. If you do only want to build a specific package, ensure 
+you run `cargo build -p {package} -p eucalyptus-exports`. 
 
 If you get any FFI errors (likely a getter), you compiled the library wrong. 
 
diff --git a/crates/dropbear-engine/src/shaders/common/light.wesl b/crates/dropbear-engine/src/shaders/common/light.wesl
index 129eb23..a90707d 100644
--- a/crates/dropbear-engine/src/shaders/common/light.wesl
+++ b/crates/dropbear-engine/src/shaders/common/light.wesl
@@ -39,19 +39,20 @@ fn specular(
     return light_color * spec;
 }
 
-fn attenuation(light: Light, world_pos: vec3<f32>) -> f32 {
-    let d = length(light.position.xyz - world_pos);
+fn attenuation(light: Light, tangent_light_pos: vec3<f32>, tangent_pos: vec3<f32>) -> f32 {
+    let d = length(tangent_light_pos - tangent_pos);
     return 1.0 / (light.constant + light.lin * d + light.quadratic * d * d);
 }
 
 fn calculate_light(
-    light:        Light,
-    world_pos:    vec3<f32>,
-    normal:       vec3<f32>,   // normalised world-space surface normal
-    view_dir:     vec3<f32>,   // normalised direction from fragment to camera
-    object_color: vec3<f32>,
-    shininess:    f32,
-    spec_scale:   f32,
+    light:          Light,
+    tangent_pos:    vec3<f32>,
+    tangent_matrix: mat3x3<f32>,
+    normal:         vec3<f32>,   // normalised tangent-space surface normal
+    view_dir:       vec3<f32>,   // normalised tangent-space direction from fragment to camera
+    object_color:   vec3<f32>,
+    shininess:      f32,
+    spec_scale:     f32,
 ) -> vec3<f32> {
     let light_type  = u32(light.color.w);
     let light_color = light.color.xyz;
@@ -61,15 +62,16 @@ fn calculate_light(
     var intensity: f32 = 1.0;
 
     if light_type == LIGHT_DIRECTIONAL {
-        light_dir = normalize(-light.direction.xyz);
+        light_dir = normalize(tangent_matrix * (-light.direction.xyz));
     } else {
         // point and spot included
-        light_dir = normalize(light.position.xyz - world_pos);
-        atten     = attenuation(light, world_pos);
+        let tangent_light_pos = tangent_matrix * light.position.xyz;
+        light_dir = normalize(tangent_light_pos - tangent_pos);
+        atten     = attenuation(light, tangent_light_pos, tangent_pos);
 
         if light_type == LIGHT_SPOT {
             // smooth edge
-            let theta        = dot(light_dir, normalize(-light.direction.xyz));
+            let theta        = dot(light_dir, normalize(tangent_matrix * (-light.direction.xyz)));
             let inner_cutoff = light.cutoff;
             let outer_cutoff = light.direction.w;
             let epsilon      = inner_cutoff - outer_cutoff;
@@ -85,19 +87,21 @@ fn calculate_light(
 
 
 fn calculate_lighting(
-    world_pos:    vec3<f32>,
-    normal:       vec3<f32>,
-    view_dir:     vec3<f32>,
-    object_color: vec3<f32>,
-    shininess:    f32,
-    spec_scale:   f32,
+    tangent_pos:    vec3<f32>,
+    tangent_matrix: mat3x3<f32>,
+    normal:         vec3<f32>,
+    view_dir:       vec3<f32>,
+    object_color:   vec3<f32>,
+    shininess:      f32,
+    spec_scale:     f32,
 ) -> vec3<f32> {
     var result = u_globals.ambient_strength * object_color;
     let num_lights = u_globals.num_lights;
     for (var i = 0u; i < num_lights; i++) {
         result += calculate_light(
             s_light_array[i],
-            world_pos,
+            tangent_pos,
+            tangent_matrix,
             normal,
             view_dir,
             object_color,
diff --git a/crates/dropbear-engine/src/shaders/shader.wesl b/crates/dropbear-engine/src/shaders/shader.wesl
index 4fb0a49..8fa81d9 100644
--- a/crates/dropbear-engine/src/shaders/shader.wesl
+++ b/crates/dropbear-engine/src/shaders/shader.wesl
@@ -47,11 +47,10 @@ struct VertexInput {
 struct VertexOutput {
     @builtin(position) clip_position:     vec4<f32>,
     @location(0) tex_coords:              vec2<f32>,
-    @location(1) world_normal:            vec3<f32>,
-    @location(2) world_position:          vec3<f32>,
-    @location(3) world_tangent:           vec3<f32>,
-    @location(4) world_bitangent:         vec3<f32>,
-    @location(5) world_view_position:     vec3<f32>,
+    @location(1) world_position:          vec3<f32>,
+    @location(2) world_tangent:           vec3<f32>,
+    @location(3) world_bitangent:         vec3<f32>,
+    @location(4) world_normal:            vec3<f32>,
 };
 
 
@@ -80,17 +79,17 @@ fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput {
 
     let world_position  = model_matrix * vec4<f32>(anim.position, 1.0);
     let world_normal    = normalize(normal_matrix * anim.normal);
-    let world_tangent   = normalize(normal_matrix * anim.tangent);
+    let model_mat3      = mat3x3<f32>(model_matrix[0].xyz, model_matrix[1].xyz, model_matrix[2].xyz);
+    let world_tangent   = normalize(model_mat3 * anim.tangent);
     let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w);
 
     var out: VertexOutput;
-    out.clip_position       = u_camera.view_proj * world_position;
-    out.tex_coords          = model.tex_coords0;
-    out.world_normal        = world_normal;
-    out.world_position      = world_position.xyz;
-    out.world_tangent       = world_tangent;
-    out.world_bitangent     = world_bitangent;
-    out.world_view_position = u_camera.view_pos.xyz;
+    out.clip_position   = u_camera.view_proj * world_position;
+    out.tex_coords      = model.tex_coords0;
+    out.world_position  = world_position.xyz;
+    out.world_tangent   = world_tangent;
+    out.world_bitangent = world_bitangent;
+    out.world_normal    = world_normal;
     return out;
 }
 
@@ -105,17 +104,19 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
         discard;
     }
 
-    // normal
-    var N = normalize(in.world_normal);
+    let world_N = normalize(in.world_normal);
+
+    let world_T = normalize(in.world_tangent - dot(in.world_tangent, world_N) * world_N);
+    let world_B_cross = cross(world_N, world_T);
+    let world_B = world_B_cross * sign(dot(in.world_bitangent, world_B_cross));
+    let tangent_matrix = transpose(mat3x3<f32>(world_T, world_B, world_N));
+
+    var N: vec3<f32>;
     if material::u_material.has_normal_texture != 0u {
-        let tbn = mat3x3<f32>(
-            normalize(in.world_tangent),
-            normalize(in.world_bitangent),
-            N,
-        );
         let n_sample = textureSample(material::t_normal, material::s_normal, uv).xyz;
-        let n_ts     = (n_sample * 2.0 - 1.0) * vec3<f32>(material::u_material.normal_scale, material::u_material.normal_scale, 1.0);
-        N = normalize(tbn * n_ts);
+        N = normalize((n_sample * 2.0 - 1.0) * vec3<f32>(material::u_material.normal_scale, material::u_material.normal_scale, 1.0));
+    } else {
+        N = vec3<f32>(0.0, 0.0, 1.0);
     }
 
     // occlusion
@@ -127,11 +128,15 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
 
     let roughness = material::u_material.roughness;
     let shininess = max(2.0 / max(roughness * roughness, 0.001) - 2.0, 2.0);
-    let spec_scale = (1.0 - roughness) * (1.0 - roughness);
+    let spec_scale = max((1.0 - roughness) * (1.0 - roughness), 0.04);
+
+    let tangent_pos      = tangent_matrix * in.world_position;
+    let tangent_view_pos = tangent_matrix * u_camera.view_pos.xyz;
 
-    let view_dir = normalize(in.world_view_position - in.world_position);
+    let view_dir = normalize(tangent_view_pos - tangent_pos);
     let lit = light::calculate_lighting(
-        in.world_position,
+        tangent_pos,
+        tangent_matrix,
         N,
         view_dir,
         albedo.rgb,
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index 530276f..5cf5c84 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -6,9 +6,6 @@ license.workspace = true
 repository.workspace = true
 readme = "README.md"
 
-[lib]
-crate-type = ["rlib", "cdylib"]
-
 [dependencies]
 dropbear-macro = { path = "../dropbear-macro" }
 magna-carta = { path = "../magna-carta" }
diff --git a/crates/eucalyptus-core/README.md b/crates/eucalyptus-core/README.md
index b73f4cd..6acfe4e 100644
--- a/crates/eucalyptus-core/README.md
+++ b/crates/eucalyptus-core/README.md
@@ -1,40 +1,4 @@
 # eucalyptus-core
 
-The core libraries of the eucalyptus editor. Great for embedding into `redback-runtime` and `eucalyptus-editor` as one big change instead of a bunch of features.
-
-It also produces a shared library for Kotlin/Native and the JVM.
-
-Kotlin Multiplatform is prioritised and fully supported on this library, however that is not to say that you (yes you 
-the reader) could implement this for another language (as really it just reads from a .dll or a .jar file). 
-
-[//]: # (## Export requirements)
-
-[//]: # ()
-[//]: # (To have your own scripting module to be able to be run, you will have to implement the following exports into)
-
-[//]: # (your own app:)
-
-[//]: # ()
-[//]: # (- `dropbear_init`)
-
-[//]: # (  - Args: `pointerContext: DropbearContext*`)
-
-[//]: # (  - Returns: `int`)
-
-[//]: # (- `dropbear_load_tagged`)
-
-[//]: # (  - Args: `tag: *mut c_char`)
-
-[//]: # (  - Returns: `int`)
-
-[//]: # (- `dropbear_update_all`)
-
-[//]: # (  - Args: `dt: float`)
-
-[//]: # (  - Returns: `int`)
-
-[//]: # (- `dropbear_update_tagged`)
-
-[//]: # (  - Args: `tag: *mut c_char, `)
-
-[//]: # (  - )
\ No newline at end of file
+The core libraries of the eucalyptus editor. Great for embedding into `redback-runtime` and `eucalyptus-editor` as 
+one big change instead of a bunch of features.
\ No newline at end of file
diff --git a/crates/eucalyptus-core/build.rs b/crates/eucalyptus-core/build.rs
deleted file mode 100644
index 9c26395..0000000
--- a/crates/eucalyptus-core/build.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-fn main() -> anyhow::Result<()> {
-    // fuck you windows :(
-    #[cfg(target_os = "windows")]
-    {
-        println!("cargo:rustc-link-arg=/FORCE:MULTIPLE");
-        println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib");
-    }
-
-    goanna_gen::generate_c_header()?;
-
-    println!("cargo:rerun-if-changed=build.rs");
-    println!("cargo:rerun-if-changed=src");
-    Ok(())
-}
diff --git a/crates/eucalyptus-core/src/animation.rs b/crates/eucalyptus-core/src/animation.rs
index 682e82a..8353514 100644
--- a/crates/eucalyptus-core/src/animation.rs
+++ b/crates/eucalyptus-core/src/animation.rs
@@ -30,10 +30,10 @@ impl Component for AnimationComponent {
         }
     }
 
-    fn init<'a>(
-        ser: &'a Self::SerializedForm,
+    fn init(
+        ser: &Self::SerializedForm,
         _graphics: Arc<SharedGraphicsContext>,
-    ) -> crate::component::ComponentInitFuture<'a, Self> {
+    ) -> crate::component::ComponentInitFuture<Self> {
         Box::pin(async move { Ok((ser.clone(),)) })
     }
 
@@ -219,395 +219,4 @@ impl InspectableComponent for AnimationComponent {
                 }
             });
     }
-}
-
-fn collect_available_animations(
-    world: &World,
-    entity: Entity,
-    component: &AnimationComponent,
-) -> Vec<String> {
-    if !component.available_animations.is_empty() {
-        return component.available_animations.clone();
-    }
-
-    let Ok(renderer) = world.get::<&MeshRenderer>(entity) else {
-        return Vec::new();
-    };
-
-    let handle = renderer.model();
-    if handle.is_null() {
-        return Vec::new();
-    }
-
-    let registry = ASSET_REGISTRY.read();
-    let Some(model) = registry.get_model(handle) else {
-        return Vec::new();
-    };
-
-    model
-        .animations
-        .iter()
-        .map(|animation| animation.name.clone())
-        .collect()
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "animationComponentExistsForEntity"
-    ),
-    c
-)]
-fn exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(world.get::<&AnimationComponent>(entity).is_ok())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getActiveAnimationIndex"
-    ),
-    c
-)]
-fn get_active_animation_index(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<Option<i32>> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(component.active_animation_index.map(|index| index as i32))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "setActiveAnimationIndex"
-    ),
-    c
-)]
-fn set_active_animation_index(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    index: &Option<i32>,
-) -> DropbearNativeResult<()> {
-    let mut component = world
-        .get::<&mut AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    let index = match index {
-        Some(value) if *value >= 0 => Some(*value as usize),
-        Some(_) => return Err(DropbearNativeError::InvalidArgument),
-        None => None,
-    };
-
-    if let Some(value) = index {
-        if !component.available_animations.is_empty()
-            && value >= component.available_animations.len()
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-    }
-
-    component.active_animation_index = index;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getTime"
-    ),
-    c
-)]
-fn get_time(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<f64> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    if let Some(index) = component.active_animation_index {
-        if let Some(settings) = component.animation_settings.get(&index) {
-            return Ok(settings.time as f64);
-        }
-    }
-
-    Ok(component.time as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "setTime"
-    ),
-    c
-)]
-fn set_time(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    value: f64,
-) -> DropbearNativeResult<()> {
-    let mut component = world
-        .get::<&mut AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    component.time = value as f32;
-
-    if let Some(index) = component.active_animation_index {
-        let (time, speed, looping, is_playing) = (
-            component.time,
-            component.speed,
-            component.looping,
-            component.is_playing,
-        );
-        let settings = component
-            .animation_settings
-            .entry(index)
-            .or_insert_with(|| AnimationSettings {
-                time,
-                speed,
-                looping,
-                is_playing,
-            });
-        settings.time = time;
-    }
-
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getSpeed"
-    ),
-    c
-)]
-fn get_speed(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<f64> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    if let Some(index) = component.active_animation_index {
-        if let Some(settings) = component.animation_settings.get(&index) {
-            return Ok(settings.speed as f64);
-        }
-    }
-
-    Ok(component.speed as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "setSpeed"
-    ),
-    c
-)]
-fn set_speed(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    value: f64,
-) -> DropbearNativeResult<()> {
-    let mut component = world
-        .get::<&mut AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    component.speed = value as f32;
-
-    if let Some(index) = component.active_animation_index {
-        let (time, speed, looping, is_playing) = (
-            component.time,
-            component.speed,
-            component.looping,
-            component.is_playing,
-        );
-        let settings = component
-            .animation_settings
-            .entry(index)
-            .or_insert_with(|| AnimationSettings {
-                time,
-                speed,
-                looping,
-                is_playing,
-            });
-        settings.speed = speed;
-    }
-
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getLooping"
-    ),
-    c
-)]
-fn get_looping(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    if let Some(index) = component.active_animation_index {
-        if let Some(settings) = component.animation_settings.get(&index) {
-            return Ok(settings.looping);
-        }
-    }
-
-    Ok(component.looping)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "setLooping"
-    ),
-    c
-)]
-fn set_looping(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    value: bool,
-) -> DropbearNativeResult<()> {
-    let mut component = world
-        .get::<&mut AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    component.looping = value;
-
-    if let Some(index) = component.active_animation_index {
-        let (time, speed, looping, is_playing) = (
-            component.time,
-            component.speed,
-            component.looping,
-            component.is_playing,
-        );
-        let settings = component
-            .animation_settings
-            .entry(index)
-            .or_insert_with(|| AnimationSettings {
-                time,
-                speed,
-                looping,
-                is_playing,
-            });
-        settings.looping = value;
-    }
-
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getIsPlaying"
-    ),
-    c
-)]
-fn get_is_playing(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    if let Some(index) = component.active_animation_index {
-        if let Some(settings) = component.animation_settings.get(&index) {
-            return Ok(settings.is_playing);
-        }
-    }
-
-    Ok(component.is_playing)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "setIsPlaying"
-    ),
-    c
-)]
-fn set_is_playing(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    value: bool,
-) -> DropbearNativeResult<()> {
-    let mut component = world
-        .get::<&mut AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    component.is_playing = value;
-
-    if let Some(index) = component.active_animation_index {
-        let (time, speed, looping, is_playing) = (
-            component.time,
-            component.speed,
-            component.looping,
-            component.is_playing,
-        );
-        let settings = component
-            .animation_settings
-            .entry(index)
-            .or_insert_with(|| AnimationSettings {
-                time,
-                speed,
-                looping,
-                is_playing,
-            });
-        settings.is_playing = value;
-    }
-
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getIndexFromString"
-    ),
-    c
-)]
-fn get_index_from_string(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    name: String,
-) -> DropbearNativeResult<Option<i32>> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    Ok(component
-        .available_animations
-        .iter()
-        .enumerate()
-        .find_map(|(i, l)| if *l == name { Some(i as i32) } else { None }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.animation.AnimationComponentNative",
-        func = "getAvailableAnimations"
-    ),
-    c
-)]
-fn get_available_animations(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<Vec<String>> {
-    let component = world
-        .get::<&AnimationComponent>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(collect_available_animations(world, entity, &component))
-}
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/asset/mod.rs b/crates/eucalyptus-core/src/asset/mod.rs
deleted file mode 100644
index c8043bb..0000000
--- a/crates/eucalyptus-core/src/asset/mod.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-pub mod model;
-pub mod texture;
-
-use crate::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped};
-use crate::scripting::jni::utils::FromJObject;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use dropbear_engine::asset::AssetKind;
-use jni::objects::JObject;
-use jni::{Env, jni_sig, jni_str};
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.DropbearEngineNative", func = "getAsset"),
-    c(name = "dropbear_engine_get_asset")
-)]
-fn dropbear_asset_get_asset(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    label: String,
-    kind: &AssetKind,
-) -> DropbearNativeResult<Option<u64>> {
-    let reader = asset.read();
-    match kind {
-        AssetKind::Texture => {
-            let result = reader.get_texture_handle_from_label(&label);
-            if let Some(handle) = result {
-                Ok(Some(handle.id))
-            } else {
-                Ok(None)
-            }
-        }
-        AssetKind::Model => {
-            let result = reader.get_model_handle_from_label(&label);
-            if let Some(handle) = result {
-                Ok(Some(handle.id))
-            } else {
-                Ok(None)
-            }
-        }
-    }
-}
-
-impl FromJObject for AssetKind {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let ordinal = env
-            .call_method(obj, jni_str!("ordinal"), jni_sig!(() -> i32), &[])?
-            .i()?;
-
-        match ordinal {
-            0 => Ok(AssetKind::Texture),
-            1 => Ok(AssetKind::Model),
-            _ => Err(DropbearNativeError::InvalidEnumOrdinal),
-        }
-    }
-}
diff --git a/crates/eucalyptus-core/src/asset/model.rs b/crates/eucalyptus-core/src/asset/model.rs
deleted file mode 100644
index 02d3c7f..0000000
--- a/crates/eucalyptus-core/src/asset/model.rs
+++ /dev/null
@@ -1,734 +0,0 @@
-use crate::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped};
-use crate::scripting::jni::utils::ToJObject;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use crate::types::{NQuaternion, NVector2, NVector3, NVector4};
-use dropbear_engine::asset::Handle;
-use dropbear_engine::model::{
-    Animation, AnimationChannel, AnimationInterpolation, ChannelValues, Material, Mesh,
-    ModelVertex, Node, NodeTransform, Skin,
-};
-use jni::objects::{JObject, JValue};
-use jni::{Env, jni_sig, jni_str};
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NModelVertex {
-    pub position: NVector3,
-    pub normal: NVector3,
-    pub tangent: NVector4,
-    pub tex_coords0: NVector2,
-    pub tex_coords1: NVector2,
-    pub colour0: NVector4,
-    pub joints0: Vec<i32>,
-    pub weights0: NVector4,
-}
-
-impl ToJObject for NModelVertex {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/asset/model/ModelVertex"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let position = self.position.to_jobject(env)?;
-        let normal = self.normal.to_jobject(env)?;
-        let tangent = self.tangent.to_jobject(env)?;
-        let tex_coords0 = self.tex_coords0.to_jobject(env)?;
-        let tex_coords1 = self.tex_coords1.to_jobject(env)?;
-        let colour0 = self.colour0.to_jobject(env)?;
-        let joints0 = self.joints0.as_slice().to_jobject(env)?;
-        let weights0 = self.weights0.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&position),
-            JValue::Object(&normal),
-            JValue::Object(&tangent),
-            JValue::Object(&tex_coords0),
-            JValue::Object(&tex_coords1),
-            JValue::Object(&colour0),
-            JValue::Object(&joints0),
-            JValue::Object(&weights0),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!((com.dropbear.math.Vector3f, com.dropbear.math.Vector3f, com.dropbear.math.Vector4f, com.dropbear.math.Vector2f, com.dropbear.math.Vector2f, com.dropbear.math.Vector4f, [int], com.dropbear.math.Vector4f) -> ()),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NMesh {
-    pub name: String,
-    pub num_elements: i32,
-    pub material_index: i32,
-    pub vertices: Vec<NModelVertex>,
-}
-
-impl ToJObject for NMesh {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.Mesh"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = env
-            .new_string(&self.name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let vertices = self.vertices.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&name),
-            JValue::Int(self.num_elements),
-            JValue::Int(self.material_index),
-            JValue::Object(&vertices),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!((java.lang.String, int, int, java.util.List) -> ()),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NMaterial {
-    pub name: String,
-    pub diffuse_texture: u64,
-    pub normal_texture: Option<u64>,
-    pub emissive_texture: Option<u64>,
-    pub metallic_roughness_texture: Option<u64>,
-    pub occlusion_texture: Option<u64>,
-    pub tint: NVector4,
-    pub emissive_factor: NVector3,
-    pub metallic_factor: f32,
-    pub roughness_factor: f32,
-    pub alpha_cutoff: Option<f32>,
-    pub occlusion_strength: f32,
-    pub normal_scale: f32,
-    pub uv_tiling: NVector2,
-}
-
-impl ToJObject for NMaterial {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.Material"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = env
-            .new_string(&self.name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let diffuse_texture = new_texture(env, self.diffuse_texture)?;
-        let normal_texture = match self.normal_texture {
-            Some(id) => new_texture(env, id)?,
-            None => JObject::null(),
-        };
-        let emissive_texture = match self.emissive_texture {
-            Some(id) => new_texture(env, id)?,
-            None => JObject::null(),
-        };
-        let metallic_roughness_texture = match self.metallic_roughness_texture {
-            Some(id) => new_texture(env, id)?,
-            None => JObject::null(),
-        };
-        let occlusion_texture = match self.occlusion_texture {
-            Some(id) => new_texture(env, id)?,
-            None => JObject::null(),
-        };
-        let tint = self.tint.to_jobject(env)?;
-        let emissive_factor = self.emissive_factor.to_jobject(env)?;
-        let uv_tiling = self.uv_tiling.to_jobject(env)?;
-        let alpha_cutoff = self.alpha_cutoff.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&name),
-            JValue::Object(&diffuse_texture),
-            JValue::Object(&normal_texture),
-            JValue::Object(&tint),
-            JValue::Object(&emissive_factor),
-            JValue::Double(self.metallic_factor as f64),
-            JValue::Double(self.roughness_factor as f64),
-            JValue::Object(&alpha_cutoff),
-            JValue::Double(self.occlusion_strength as f64),
-            JValue::Double(self.normal_scale as f64),
-            JValue::Object(&uv_tiling),
-            JValue::Object(&emissive_texture),
-            JValue::Object(&metallic_roughness_texture),
-            JValue::Object(&occlusion_texture),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!((
-                    java.lang.String,
-                    com.dropbear.asset.Texture,
-                    com.dropbear.asset.Texture,
-                    com.dropbear.math.Vector4d,
-                    com.dropbear.math.Vector3d,
-                    double, double,
-                    java.lang.Double,
-                    double, double,
-                    com.dropbear.math.Vector2d,
-                    com.dropbear.asset.Texture,
-                    com.dropbear.asset.Texture,
-                    com.dropbear.asset.Texture
-                ) -> ()),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NNodeTransform {
-    pub translation: NVector3,
-    pub rotation: NQuaternion,
-    pub scale: NVector3,
-}
-
-impl ToJObject for NNodeTransform {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.NodeTransform"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let translation = self.translation.to_jobject(env)?;
-        let rotation = self.rotation.to_jobject(env)?;
-        let scale = self.scale.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&translation),
-            JValue::Object(&rotation),
-            JValue::Object(&scale),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!("(Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Quaterniond;Lcom/dropbear/math/Vector3d;)V"),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NNode {
-    pub name: String,
-    pub parent: Option<i32>,
-    pub children: Vec<i32>,
-    pub transform: NNodeTransform,
-}
-
-impl ToJObject for NNode {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.Node"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = env
-            .new_string(&self.name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let parent = self.parent.to_jobject(env)?;
-        let children = self.children.as_slice().to_jobject(env)?;
-        let transform = self.transform.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&name),
-            JValue::Object(&parent),
-            JValue::Object(&children),
-            JValue::Object(&transform),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!("(Ljava/lang/String;Ljava/lang/Integer;Ljava/util/List;Lcom/dropbear/asset/model/NodeTransform;)V"),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NSkin {
-    pub name: String,
-    pub joints: Vec<i32>,
-    pub inverse_bind_matrices: Vec<Vec<f64>>,
-    pub skeleton_root: Option<i32>,
-}
-
-impl ToJObject for NSkin {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.Skin"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = env
-            .new_string(&self.name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let joints = self.joints.as_slice().to_jobject(env)?;
-        let inverse_bind_matrices = self.inverse_bind_matrices.as_slice().to_jobject(env)?;
-        let skeleton_root = self.skeleton_root.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&name),
-            JValue::Object(&joints),
-            JValue::Object(&inverse_bind_matrices),
-            JValue::Object(&skeleton_root),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!(
-                    "(Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;)V"
-                ),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NAnimation {
-    pub name: String,
-    pub channels: Vec<NAnimationChannel>,
-    pub duration: f32,
-}
-
-impl ToJObject for NAnimation {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.Animation"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = env
-            .new_string(&self.name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let channels = self.channels.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&name),
-            JValue::Object(&channels),
-            JValue::Double(self.duration as f64),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!("(Ljava/lang/String;Ljava/util/List;D)V"),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-pub struct NAnimationChannel {
-    pub target_node: i32,
-    pub times: Vec<f64>,
-    pub values: NChannelValues,
-    pub interpolation: NAnimationInterpolation,
-}
-
-impl ToJObject for NAnimationChannel {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.AnimationChannel"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let times = self.times.as_slice().to_jobject(env)?;
-        let values = self.values.to_jobject(env)?;
-        let interpolation = self.interpolation.to_jobject(env)?;
-
-        let args = [
-            JValue::Int(self.target_node),
-            JValue::Object(&times),
-            JValue::Object(&values),
-            JValue::Object(&interpolation),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!("(I[DLcom/dropbear/asset/model/ChannelValues;Lcom/dropbear/asset/model/AnimationInterpolation;)V"),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-#[dropbear_macro::repr_c_enum]
-pub enum NAnimationInterpolation {
-    Linear,
-    Step,
-    CubicSpline,
-}
-
-impl ToJObject for NAnimationInterpolation {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com.dropbear.asset.model.AnimationInterpolation"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let field_name = match self {
-            NAnimationInterpolation::Linear => "LINEAR",
-            NAnimationInterpolation::Step => "STEP",
-            NAnimationInterpolation::CubicSpline => "CUBIC_SPLINE",
-        };
-
-        let value = env
-            .get_static_field(
-                class,
-                jni::strings::JNIString::from(field_name),
-                jni_sig!("Lcom/dropbear/asset/model/AnimationInterpolation;"),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(value)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Debug)]
-#[dropbear_macro::repr_c_enum]
-pub enum NChannelValues {
-    Translations { values: Vec<NVector3> },
-    Rotations { values: Vec<NQuaternion> },
-    Scales { values: Vec<NVector3> },
-    MorphWeights { values: Vec<Vec<f64>> },
-}
-
-impl ToJObject for NChannelValues {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        match self {
-            NChannelValues::Translations { values } => {
-                let class = env
-                    .load_class(jni_str!(
-                        "com.dropbear.asset.model.ChannelValues$Translations"
-                    ))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-                let list = values.to_jobject(env)?;
-                let obj = env
-                    .new_object(
-                        &class,
-                        jni_sig!("(Ljava/util/List;)V"),
-                        &[JValue::Object(&list)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-                Ok(obj)
-            }
-            NChannelValues::Rotations { values } => {
-                let class = env
-                    .load_class(jni_str!("com.dropbear.asset.model.ChannelValues$Rotations"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-                let list = values.to_jobject(env)?;
-                let obj = env
-                    .new_object(
-                        &class,
-                        jni_sig!("(Ljava/util/List;)V"),
-                        &[JValue::Object(&list)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-                Ok(obj)
-            }
-            NChannelValues::Scales { values } => {
-                let class = env
-                    .load_class(jni_str!("com.dropbear.asset.model.ChannelValues$Scales"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-                let list = values.to_jobject(env)?;
-                let obj = env
-                    .new_object(
-                        &class,
-                        jni_sig!("(Ljava/util/List;)V"),
-                        &[JValue::Object(&list)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-                Ok(obj)
-            }
-            NChannelValues::MorphWeights { values } => {
-                let class = env
-                    .load_class(jni_str!(
-                        "com.dropbear.asset.model.ChannelValues$MorphWeights"
-                    ))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-                let list = values.to_jobject(env)?;
-                let obj = env
-                    .new_object(
-                        &class,
-                        jni_sig!("(Ljava/util/List;)V"),
-                        &[JValue::Object(&list)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-                Ok(obj)
-            }
-        }
-    }
-}
-
-fn new_texture<'a>(env: &mut Env<'a>, texture_id: u64) -> DropbearNativeResult<JObject<'a>> {
-    let class = env
-        .load_class(jni_str!("com.dropbear.asset.Texture"))
-        .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-    env.new_object(&class, jni_sig!("(J)V"), &[JValue::Long(texture_id as i64)])
-        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-}
-
-fn map_vertex(vertex: &ModelVertex) -> NModelVertex {
-    NModelVertex {
-        position: NVector3::from(vertex.position),
-        normal: NVector3::from(vertex.normal),
-        tangent: NVector4::from(vertex.tangent),
-        tex_coords0: NVector2::from(vertex.tex_coords0),
-        tex_coords1: NVector2::from(vertex.tex_coords1),
-        colour0: NVector4::from(vertex.colour0),
-        joints0: vertex.joints0.iter().map(|v| *v as i32).collect(),
-        weights0: NVector4::from(vertex.weights0),
-    }
-}
-
-fn map_mesh(mesh: &Mesh) -> NMesh {
-    NMesh {
-        name: mesh.name.clone(),
-        num_elements: mesh.num_elements as i32,
-        material_index: mesh.material as i32,
-        vertices: mesh.vertices.iter().map(map_vertex).collect(),
-    }
-}
-
-fn map_material(
-    _registry: &dropbear_engine::asset::AssetRegistry,
-    material: &Material,
-) -> NMaterial {
-    NMaterial {
-        name: material.name.clone(),
-        diffuse_texture: material.diffuse_texture.id,
-        normal_texture: material.normal_texture.and_then(|v| Some(v.id)),
-        tint: NVector4::from(material.base_colour),
-        emissive_factor: NVector3::from(material.emissive_factor),
-        metallic_factor: material.metallic_factor,
-        roughness_factor: material.roughness_factor,
-        alpha_cutoff: material.alpha_cutoff,
-        occlusion_strength: material.occlusion_strength,
-        normal_scale: material.normal_scale,
-        uv_tiling: NVector2::from(material.uv_tiling),
-        emissive_texture: material.emissive_texture.and_then(|v| Some(v.id)),
-        metallic_roughness_texture: material.metallic_roughness_texture.and_then(|v| Some(v.id)),
-        occlusion_texture: material.occlusion_texture.and_then(|v| Some(v.id)),
-    }
-}
-
-fn map_node_transform(transform: &NodeTransform) -> NNodeTransform {
-    NNodeTransform {
-        translation: NVector3::from(transform.translation),
-        rotation: NQuaternion::from(transform.rotation),
-        scale: NVector3::from(transform.scale),
-    }
-}
-
-fn map_node(node: &Node) -> NNode {
-    NNode {
-        name: node.name.clone(),
-        parent: node.parent.map(|v| v as i32),
-        children: node.children.iter().map(|v| *v as i32).collect(),
-        transform: map_node_transform(&node.transform),
-    }
-}
-
-fn map_skin(skin: &Skin) -> NSkin {
-    let inverse_bind_matrices = skin
-        .inverse_bind_matrices
-        .iter()
-        .map(|matrix| matrix.to_cols_array().iter().map(|v| *v as f64).collect())
-        .collect();
-
-    NSkin {
-        name: skin.name.clone(),
-        joints: skin.joints.iter().map(|v| *v as i32).collect(),
-        inverse_bind_matrices,
-        skeleton_root: skin.skeleton_root.map(|v| v as i32),
-    }
-}
-
-fn map_interpolation(value: &AnimationInterpolation) -> NAnimationInterpolation {
-    match value {
-        AnimationInterpolation::Linear => NAnimationInterpolation::Linear,
-        AnimationInterpolation::Step => NAnimationInterpolation::Step,
-        AnimationInterpolation::CubicSpline => NAnimationInterpolation::CubicSpline,
-    }
-}
-
-fn map_channel_values(values: &ChannelValues) -> NChannelValues {
-    match values {
-        ChannelValues::Translations(list) => NChannelValues::Translations {
-            values: list.iter().map(|v| NVector3::from(*v)).collect(),
-        },
-        ChannelValues::Rotations(list) => NChannelValues::Rotations {
-            values: list.iter().map(|v| NQuaternion::from(*v)).collect(),
-        },
-        ChannelValues::Scales(list) => NChannelValues::Scales {
-            values: list.iter().map(|v| NVector3::from(*v)).collect(),
-        },
-        ChannelValues::MorphWeights(items) => NChannelValues::MorphWeights {
-            values: items
-                .iter()
-                .map(|weights| weights.iter().map(|v| *v as f64).collect())
-                .collect(),
-        },
-    }
-}
-
-fn map_animation_channel(channel: &AnimationChannel) -> NAnimationChannel {
-    NAnimationChannel {
-        target_node: channel.target_node as i32,
-        times: channel.times.iter().map(|v| *v as f64).collect(),
-        values: map_channel_values(&channel.values),
-        interpolation: map_interpolation(&channel.interpolation),
-    }
-}
-
-fn map_animation(animation: &Animation) -> NAnimation {
-    NAnimation {
-        name: animation.name.clone(),
-        channels: animation
-            .channels
-            .iter()
-            .map(map_animation_channel)
-            .collect(),
-        duration: animation.duration,
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.ModelNative", func = "getLabel"),
-    c(name = "dropbear_asset_model_get_label")
-)]
-fn dropbear_asset_model_get_label(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    model_handle: u64,
-) -> DropbearNativeResult<String> {
-    let label = asset
-        .read()
-        .get_label_from_model_handle(Handle::new(model_handle))
-        .ok_or_else(|| DropbearNativeError::InvalidHandle)?;
-    Ok(label)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.ModelNative", func = "getMeshes"),
-    c(name = "dropbear_asset_model_get_meshes")
-)]
-fn dropbear_asset_model_get_meshes(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    model_handle: u64,
-) -> DropbearNativeResult<Vec<NMesh>> {
-    let reader = asset.read();
-    let model = reader
-        .get_model(Handle::new(model_handle))
-        .ok_or(DropbearNativeError::InvalidHandle)?;
-
-    Ok(model.meshes.iter().map(map_mesh).collect())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.ModelNative", func = "getMaterials"),
-    c(name = "dropbear_asset_model_get_materials")
-)]
-fn dropbear_asset_model_get_materials(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    model_handle: u64,
-) -> DropbearNativeResult<Vec<NMaterial>> {
-    let reader = asset.read();
-    let model = reader
-        .get_model(Handle::new(model_handle))
-        .ok_or(DropbearNativeError::InvalidHandle)?;
-
-    Ok(model
-        .materials
-        .iter()
-        .map(|mat| map_material(&reader, mat))
-        .collect())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.ModelNative", func = "getSkins"),
-    c(name = "dropbear_asset_model_get_skins")
-)]
-pub fn dropbear_asset_model_get_skins(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    model_handle: u64,
-) -> DropbearNativeResult<Vec<NSkin>> {
-    let reader = asset.read();
-    let model = reader
-        .get_model(Handle::new(model_handle))
-        .ok_or(DropbearNativeError::InvalidHandle)?;
-
-    Ok(model.skins.iter().map(map_skin).collect())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.ModelNative", func = "getAnimations"),
-    c(name = "dropbear_asset_model_get_animations")
-)]
-pub fn dropbear_asset_model_get_animations(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    model_handle: u64,
-) -> DropbearNativeResult<Vec<NAnimation>> {
-    let reader = asset.read();
-    let model = reader
-        .get_model(Handle::new(model_handle))
-        .ok_or(DropbearNativeError::InvalidHandle)?;
-
-    Ok(model.animations.iter().map(map_animation).collect())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.ModelNative", func = "getNodes"),
-    c(name = "dropbear_asset_model_get_nodes")
-)]
-pub fn dropbear_asset_model_get_nodes(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    model_handle: u64,
-) -> DropbearNativeResult<Vec<NNode>> {
-    let reader = asset.read();
-    let model = reader
-        .get_model(Handle::new(model_handle))
-        .ok_or(DropbearNativeError::InvalidHandle)?;
-
-    Ok(model.nodes.iter().map(map_node).collect())
-}
diff --git a/crates/eucalyptus-core/src/asset/texture.rs b/crates/eucalyptus-core/src/asset/texture.rs
deleted file mode 100644
index 55334ae..0000000
--- a/crates/eucalyptus-core/src/asset/texture.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use crate::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use dropbear_engine::asset::Handle;
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.TextureNative", func = "getLabel"),
-    c(name = "dropbear_asset_texture_get_label")
-)]
-fn get_texture_label(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset_manager: &AssetRegistryUnwrapped,
-    texture_handle: u64,
-) -> DropbearNativeResult<Option<String>> {
-    Ok(asset_manager
-        .read()
-        .get_label_from_texture_handle(Handle::new(texture_handle)))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.TextureNative", func = "getWidth"),
-    c(name = "dropbear_asset_texture_get_width")
-)]
-fn get_texture_width(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset_manager: &AssetRegistryUnwrapped,
-    texture_handle: u64,
-) -> DropbearNativeResult<u32> {
-    asset_manager
-        .read()
-        .get_texture(Handle::new(texture_handle))
-        .map(|v| v.size.width)
-        .ok_or(DropbearNativeError::AssetNotFound)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.asset.TextureNative", func = "getHeight"),
-    c(name = "dropbear_asset_texture_get_height")
-)]
-fn get_texture_height(
-    #[dropbear_macro::define(AssetRegistryPtr)] asset_manager: &AssetRegistryUnwrapped,
-    texture_handle: u64,
-) -> DropbearNativeResult<u32> {
-    asset_manager
-        .read()
-        .get_texture(Handle::new(texture_handle))
-        .map(|v| v.size.height)
-        .ok_or(DropbearNativeError::AssetNotFound)
-}
diff --git a/crates/eucalyptus-core/src/camera.rs b/crates/eucalyptus-core/src/camera.rs
index 40e46de..a57819a 100644
--- a/crates/eucalyptus-core/src/camera.rs
+++ b/crates/eucalyptus-core/src/camera.rs
@@ -291,383 +291,3 @@ pub mod shared {
     }
 }
 
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "cameraExistsForEntity"
-    ),
-    c
-)]
-fn exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::camera_exists_for_entity(world, entity))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraEye"),
-    c
-)]
-fn get_eye(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NVector3> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.eye.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraEye"),
-    c
-)]
-fn set_eye(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    eye: &NVector3,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.eye = (*eye).into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "getCameraTarget"
-    ),
-    c
-)]
-fn get_target(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NVector3> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.target.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "setCameraTarget"
-    ),
-    c
-)]
-fn set_target(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    target: &NVector3,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.target = target.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraUp"),
-    c
-)]
-fn get_up(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NVector3> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.up.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraUp"),
-    c
-)]
-fn set_up(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    up: &NVector3,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.up = up.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "getCameraAspect"
-    ),
-    c
-)]
-fn get_aspect(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.aspect.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraFovY"),
-    c
-)]
-fn get_fovy(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.settings.fov_y.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraFovY"),
-    c
-)]
-fn set_fovy(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    fovy: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.settings.fov_y = fovy.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "getCameraZNear"
-    ),
-    c
-)]
-fn get_znear(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.znear.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "setCameraZNear"
-    ),
-    c
-)]
-fn set_znear(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    znear: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.znear = znear.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraZFar"),
-    c
-)]
-fn get_zfar(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.zfar.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraZFar"),
-    c
-)]
-fn set_zfar(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    zfar: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.zfar = zfar.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraYaw"),
-    c
-)]
-fn get_yaw(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.yaw.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraYaw"),
-    c
-)]
-fn set_yaw(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    yaw: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.yaw = yaw.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "getCameraPitch"
-    ),
-    c
-)]
-fn get_pitch(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.pitch.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "setCameraPitch"
-    ),
-    c
-)]
-fn set_pitch(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    pitch: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.pitch = pitch.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "getCameraSpeed"
-    ),
-    c
-)]
-fn get_speed(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.settings.speed.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "setCameraSpeed"
-    ),
-    c
-)]
-fn set_speed(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    speed: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.settings.speed = speed.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "getCameraSensitivity"
-    ),
-    c
-)]
-fn get_sensitivity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f64> {
-    match world.get::<&Camera>(entity) {
-        Ok(camera) => Ok(camera.settings.sensitivity.into()),
-        Err(e) => Err(e.into()),
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CameraNative",
-        func = "setCameraSensitivity"
-    ),
-    c
-)]
-fn set_sensitivity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
-    sensitivity: f64,
-) -> DropbearNativeResult<()> {
-    match world.get::<&mut Camera>(entity) {
-        Ok(mut camera) => {
-            camera.settings.sensitivity = sensitivity.into();
-            Ok(())
-        }
-        Err(e) => Err(e.into()),
-    }
-}
diff --git a/crates/eucalyptus-core/src/debug.rs b/crates/eucalyptus-core/src/debug.rs
index b4bff93..18b9fc8 100644
--- a/crates/eucalyptus-core/src/debug.rs
+++ b/crates/eucalyptus-core/src/debug.rs
@@ -1,6 +1,4 @@
 use crate::ptr::GraphicsContextPtr;
-use crate::scripting::result::DropbearNativeResult;
-use crate::types::{NColour, NQuaternion, NVector3};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use glam::{Quat, Vec3};
 use dropbear_engine::debug::DebugDraw;
@@ -81,245 +79,3 @@ macro_rules! with_debug {
     };
 }
 
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawLine"),
-    c
-)]
-fn draw_line(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    start: &NVector3,
-    end: &NVector3,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let a = Vec3::new(start.x as f32, start.y as f32, start.z as f32);
-    let b = Vec3::new(end.x as f32, end.y as f32, end.z as f32);
-    with_debug!(graphics, |dd| dd.draw_line(a, b, colour.to_f32_array()));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawRay"),
-    c
-)]
-fn draw_ray(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    origin: &NVector3,
-    dir: &NVector3,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let o = Vec3::new(origin.x as f32, origin.y as f32, origin.z as f32);
-    let d = Vec3::new(dir.x as f32, dir.y as f32, dir.z as f32);
-    with_debug!(graphics, |dd| dd.draw_ray(o, d, colour.to_f32_array()));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawArrow"),
-    c
-)]
-fn draw_arrow(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    start: &NVector3,
-    end: &NVector3,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let a = Vec3::new(start.x as f32, start.y as f32, start.z as f32);
-    let b = Vec3::new(end.x as f32, end.y as f32, end.z as f32);
-    with_debug!(graphics, |dd| dd.draw_arrow(a, b, colour.to_f32_array()));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawPoint"),
-    c
-)]
-fn draw_point(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    pos: &NVector3,
-    size: f32,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let p = Vec3::new(pos.x as f32, pos.y as f32, pos.z as f32);
-    with_debug!(graphics, |dd| dd.draw_point(p, size, colour.to_f32_array()));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCircle"),
-    c
-)]
-fn draw_circle(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    center: &NVector3,
-    radius: f32,
-    normal: &NVector3,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
-    let n = Vec3::new(normal.x as f32, normal.y as f32, normal.z as f32);
-    with_debug!(graphics, |dd| dd.draw_circle(
-        c,
-        radius,
-        n,
-        colour.to_f32_array()
-    ));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawSphere"),
-    c
-)]
-fn draw_sphere(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    center: &NVector3,
-    radius: f32,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
-    with_debug!(graphics, |dd| dd.draw_sphere(
-        c,
-        radius,
-        colour.to_f32_array()
-    ));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawGlobe"),
-    c
-)]
-fn draw_globe(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    center: &NVector3,
-    radius: f32,
-    lat_lines: u32,
-    lon_lines: u32,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
-    with_debug!(graphics, |dd| dd.draw_globe(
-        c,
-        radius,
-        lat_lines,
-        lon_lines,
-        colour.to_f32_array()
-    ));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawAabb"),
-    c
-)]
-fn draw_aabb(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    min: &NVector3,
-    max: &NVector3,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let mn = Vec3::new(min.x as f32, min.y as f32, min.z as f32);
-    let mx = Vec3::new(max.x as f32, max.y as f32, max.z as f32);
-    with_debug!(graphics, |dd| dd.draw_aabb(mn, mx, colour.to_f32_array()));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawObb"),
-    c
-)]
-fn draw_obb(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    center: &NVector3,
-    half_extents: &NVector3,
-    rotation: &NQuaternion,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
-    let he = Vec3::new(
-        half_extents.x as f32,
-        half_extents.y as f32,
-        half_extents.z as f32,
-    );
-    let r = Quat::from_xyzw(
-        rotation.x as f32,
-        rotation.y as f32,
-        rotation.z as f32,
-        rotation.w as f32,
-    );
-    with_debug!(graphics, |dd| dd.draw_obb(c, he, r, colour.to_f32_array()));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCapsule"),
-    c
-)]
-fn draw_capsule(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    a: &NVector3,
-    b: &NVector3,
-    radius: f32,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let va = Vec3::new(a.x as f32, a.y as f32, a.z as f32);
-    let vb = Vec3::new(b.x as f32, b.y as f32, b.z as f32);
-    with_debug!(graphics, |dd| dd.draw_capsule(
-        va,
-        vb,
-        radius,
-        colour.to_f32_array()
-    ));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.rendering.DebugDrawNative",
-        func = "drawCylinder"
-    ),
-    c
-)]
-fn draw_cylinder(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    center: &NVector3,
-    half_height: f32,
-    radius: f32,
-    axis: &NVector3,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
-    let ax = Vec3::new(axis.x as f32, axis.y as f32, axis.z as f32);
-    with_debug!(graphics, |dd| dd.draw_cylinder(
-        c,
-        half_height,
-        radius,
-        ax,
-        colour.to_f32_array()
-    ));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCone"),
-    c
-)]
-fn draw_cone(
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    apex: &NVector3,
-    dir: &NVector3,
-    angle: f32,
-    length: f32,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let a = Vec3::new(apex.x as f32, apex.y as f32, apex.z as f32);
-    let d = Vec3::new(dir.x as f32, dir.y as f32, dir.z as f32);
-    with_debug!(graphics, |dd| dd.draw_cone(
-        a,
-        d,
-        angle,
-        length,
-        colour.to_f32_array()
-    ));
-    Ok(())
-}
diff --git a/crates/eucalyptus-core/src/engine.rs b/crates/eucalyptus-core/src/engine.rs
deleted file mode 100644
index 19f2c8f..0000000
--- a/crates/eucalyptus-core/src/engine.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use crate::ptr::{CommandBufferPtr, CommandBufferUnwrapped, WorldPtr};
-use crate::scripting::result::DropbearNativeResult;
-
-pub mod shared {
-    use crate::command::CommandBuffer;
-    use crate::scripting::native::DropbearNativeError;
-    use crate::scripting::result::DropbearNativeResult;
-    use crate::states::Label;
-    use hecs::{Entity, World};
-
-    pub fn get_entity(world: &World, label: &str) -> DropbearNativeResult<u64> {
-        for (id, entity_label) in world.query::<(Entity, &Label)>().iter() {
-            if entity_label.as_str() == label {
-                return Ok(id.to_bits().get());
-            }
-        }
-        Err(DropbearNativeError::EntityNotFound)
-    }
-
-    pub fn quit(
-        command_buffer: &crossbeam_channel::Sender<CommandBuffer>,
-    ) -> DropbearNativeResult<()> {
-        command_buffer
-            .send(CommandBuffer::Quit)
-            .map_err(|_| DropbearNativeError::SendError)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.DropbearEngineNative", func = "getEntity",),
-    c
-)]
-fn get_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    label: String,
-) -> DropbearNativeResult<u64> {
-    shared::get_entity(&world, &label)
-}
-
-#[dropbear_macro::export(kotlin(class = "com.dropbear.DropbearEngineNative", func = "quit",), c)]
-fn quit(
-    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
-) -> DropbearNativeResult<()> {
-    shared::quit(command_buffer)
-}
diff --git a/crates/eucalyptus-core/src/entity.rs b/crates/eucalyptus-core/src/entity.rs
deleted file mode 100644
index 2c08349..0000000
--- a/crates/eucalyptus-core/src/entity.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-use crate::hierarchy::{Children, Parent};
-use crate::ptr::WorldPtr;
-use crate::scripting::result::DropbearNativeResult;
-use crate::states::Label;
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.LabelNative",
-        func = "labelExistsForEntity"
-    ),
-    c
-)]
-fn label_exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(world.get::<&Label>(entity).is_ok())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.EntityRefNative", func = "getEntityLabel"),
-    c
-)]
-fn get_label(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<String> {
-    let label = world.get::<&Label>(entity)?.as_str().to_string();
-    Ok(label)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.EntityRefNative", func = "getChildren"),
-    c
-)]
-fn get_children(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<Vec<u64>> {
-    if let Ok(children) = world.query_one::<&Children>(entity).get() {
-        let entity_bytes = children
-            .children()
-            .iter()
-            .map(|c| c.to_bits().get())
-            .collect::<Vec<_>>();
-        Ok(entity_bytes)
-    } else {
-        // could be that the entity just doesn't have any children, so no need to throw error
-        Ok(vec![])
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.EntityRefNative", func = "getChildByLabel"),
-    c
-)]
-fn get_child_by_label(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    target: String,
-) -> 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>(*child) {
-                if label.as_str() == target {
-                    let found = child.clone();
-                    return Ok(Some(found.to_bits().get()));
-                }
-            } else {
-                // skip if error or no entity
-                continue;
-            }
-        }
-        Ok(None)
-    } else {
-        Ok(None)
-    }
-}
-
-#[dropbear_macro::export(kotlin(class = "com.dropbear.EntityRefNative", func = "getParent"), c)]
-fn get_parent(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<Option<u64>> {
-    if let Ok(parent) = world.query_one::<&Parent>(entity).get() {
-        Ok(Some(parent.parent().to_bits().get()))
-    } else {
-        Ok(None)
-    }
-}
diff --git a/crates/eucalyptus-core/src/input.rs b/crates/eucalyptus-core/src/input.rs
index 3e26b6d..9eaa801 100644
--- a/crates/eucalyptus-core/src/input.rs
+++ b/crates/eucalyptus-core/src/input.rs
@@ -1,16 +1,9 @@
 //! Input management and input state.
 
-pub mod gamepad;
 pub mod ndc;
 
-use crate::scripting::jni::utils::ToJObject;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
 use dropbear_engine::gilrs::{Button, GamepadId};
 use glam::Vec2;
-use jni::Env;
-use jni::objects::JObject;
-use jni::sys::jlong;
 use std::sync::Arc;
 use std::{
     collections::{HashMap, HashSet},
@@ -116,257 +109,3 @@ impl InputState {
     }
 }
 
-pub mod shared {
-    use super::*;
-    use crate::command::{CommandBuffer, WindowCommand};
-    use crate::types::NVector2;
-    use crossbeam_channel::Sender;
-
-    pub fn map_ordinal_to_mouse_button(ordinal: i32) -> Option<MouseButton> {
-        match ordinal {
-            0 => Some(MouseButton::Left),
-            1 => Some(MouseButton::Right),
-            2 => Some(MouseButton::Middle),
-            3 => Some(MouseButton::Back),
-            4 => Some(MouseButton::Forward),
-            ordinal if ordinal >= 0 => Some(MouseButton::Other(ordinal as u16)),
-            _ => None,
-        }
-    }
-
-    pub fn is_key_pressed(input: &InputState, key_ordinal: i32) -> bool {
-        if let Some(key) = crate::utils::keycode_from_ordinal(key_ordinal) {
-            input.is_key_pressed(key)
-        } else {
-            false
-        }
-    }
-
-    pub fn is_mouse_button_pressed(input: &InputState, btn_ordinal: i32) -> bool {
-        if let Some(btn) = map_ordinal_to_mouse_button(btn_ordinal) {
-            input.mouse_button.contains(&btn)
-        } else {
-            false
-        }
-    }
-
-    pub fn get_mouse_position(input: &InputState) -> NVector2 {
-        NVector2 {
-            x: input.mouse_pos.0,
-            y: input.mouse_pos.1,
-        }
-    }
-
-    pub fn get_mouse_delta(input: &InputState) -> NVector2 {
-        input
-            .mouse_delta
-            .map(NVector2::from)
-            .unwrap_or(NVector2 { x: 0.0, y: 0.0 })
-    }
-
-    pub fn get_last_mouse_pos(input: &InputState) -> NVector2 {
-        input
-            .last_mouse_pos
-            .map(NVector2::from)
-            .unwrap_or(NVector2 { x: 0.0, y: 0.0 })
-    }
-
-    pub fn set_cursor_locked(
-        input: &mut InputState,
-        sender: &Sender<CommandBuffer>,
-        locked: bool,
-    ) -> DropbearNativeResult<()> {
-        input.is_cursor_locked = locked;
-
-        sender
-            .send(CommandBuffer::WindowCommand(WindowCommand::WindowGrab(
-                locked,
-            )))
-            .map_err(|_| DropbearNativeError::SendError)?;
-
-        Ok(())
-    }
-
-    pub fn set_cursor_hidden(
-        input: &mut InputState,
-        sender: &Sender<CommandBuffer>,
-        hidden: bool,
-    ) -> DropbearNativeResult<()> {
-        input.is_cursor_hidden = hidden;
-        sender
-            .send(CommandBuffer::WindowCommand(WindowCommand::HideCursor(
-                hidden,
-            )))
-            .map_err(|_| DropbearNativeError::SendError)?;
-        Ok(())
-    }
-
-    pub fn get_connected_gamepads(input: &InputState) -> Vec<u64> {
-        input
-            .connected_gamepads
-            .iter()
-            .map(|id| Into::<usize>::into(*id) as u64)
-            .collect()
-    }
-}
-
-#[repr(C)]
-struct ConnectedGamepadIds {
-    ids: Vec<u64>,
-}
-
-impl ToJObject for ConnectedGamepadIds {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let output = env
-            .new_long_array(self.ids.len())
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let long_ids: Vec<jlong> = self.ids.iter().map(|&id| id as jlong).collect();
-        output
-            .set_region(env, 0, &long_ids)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        Ok(JObject::from(output))
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "printInputState"
-    ),
-    c
-)]
-fn print_input_state(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<()> {
-    println!("Input State: {:?}", input);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.input.InputStateNative", func = "isKeyPressed"),
-    c
-)]
-fn is_key_pressed(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-    key_code: i32,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::is_key_pressed(input, key_code))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "getMousePosition"
-    ),
-    c
-)]
-fn get_mouse_position(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<crate::types::NVector2> {
-    Ok(shared::get_mouse_position(input))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "isMouseButtonPressed"
-    ),
-    c
-)]
-fn is_mouse_button_pressed(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-    button_ordinal: i32,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::is_mouse_button_pressed(input, button_ordinal))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.input.InputStateNative", func = "getMouseDelta"),
-    c
-)]
-fn get_mouse_delta(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<crate::types::NVector2> {
-    Ok(shared::get_mouse_delta(input))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.input.InputStateNative", func = "isCursorLocked"),
-    c
-)]
-fn is_cursor_locked(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<bool> {
-    Ok(input.is_cursor_locked)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "setCursorLocked"
-    ),
-    c
-)]
-fn set_cursor_locked(
-    #[dropbear_macro::define(crate::ptr::CommandBufferPtr)]
-    command_buffer: &crate::ptr::CommandBufferUnwrapped,
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &mut InputState,
-    locked: bool,
-) -> DropbearNativeResult<()> {
-    shared::set_cursor_locked(input, command_buffer, locked)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "getLastMousePos"
-    ),
-    c
-)]
-fn get_last_mouse_pos(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<crate::types::NVector2> {
-    Ok(shared::get_last_mouse_pos(input))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.input.InputStateNative", func = "isCursorHidden"),
-    c
-)]
-fn is_cursor_hidden(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<bool> {
-    Ok(input.is_cursor_hidden)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "setCursorHidden"
-    ),
-    c
-)]
-fn set_cursor_hidden(
-    #[dropbear_macro::define(crate::ptr::CommandBufferPtr)]
-    command_buffer: &crate::ptr::CommandBufferUnwrapped,
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &mut InputState,
-    hidden: bool,
-) -> DropbearNativeResult<()> {
-    shared::set_cursor_hidden(input, command_buffer, hidden)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.InputStateNative",
-        func = "getConnectedGamepads"
-    ),
-    c
-)]
-fn get_connected_gamepads(
-    #[dropbear_macro::define(crate::ptr::InputStatePtr)] input: &InputState,
-) -> DropbearNativeResult<ConnectedGamepadIds> {
-    Ok(ConnectedGamepadIds {
-        ids: shared::get_connected_gamepads(input),
-    })
-}
diff --git a/crates/eucalyptus-core/src/input/gamepad.rs b/crates/eucalyptus-core/src/input/gamepad.rs
deleted file mode 100644
index 8d542e7..0000000
--- a/crates/eucalyptus-core/src/input/gamepad.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use crate::input::InputState;
-use crate::ptr::InputStatePtr;
-use crate::scripting::result::DropbearNativeResult;
-use crate::types::NVector2;
-
-pub mod shared {
-    use crate::input::InputState;
-    use crate::scripting::jni::utils::{FromJObject, ToJObject};
-    use crate::scripting::native::DropbearNativeError;
-    use crate::scripting::result::DropbearNativeResult;
-    use crate::types::NVector2;
-    use jni::objects::{JObject, JValue};
-    use jni::{Env, jni_sig, jni_str};
-
-    fn map_int_to_gamepad_button(ordinal: i32) -> Option<dropbear_engine::gilrs::Button> {
-        match ordinal {
-            0 => Some(dropbear_engine::gilrs::Button::Unknown),
-            1 => Some(dropbear_engine::gilrs::Button::South),
-            2 => Some(dropbear_engine::gilrs::Button::East),
-            3 => Some(dropbear_engine::gilrs::Button::North),
-            4 => Some(dropbear_engine::gilrs::Button::West),
-            5 => Some(dropbear_engine::gilrs::Button::C),
-            6 => Some(dropbear_engine::gilrs::Button::Z),
-            7 => Some(dropbear_engine::gilrs::Button::LeftTrigger),
-            8 => Some(dropbear_engine::gilrs::Button::RightTrigger),
-            9 => Some(dropbear_engine::gilrs::Button::LeftTrigger2),
-            10 => Some(dropbear_engine::gilrs::Button::RightTrigger2),
-            11 => Some(dropbear_engine::gilrs::Button::Select),
-            12 => Some(dropbear_engine::gilrs::Button::Start),
-            13 => Some(dropbear_engine::gilrs::Button::Mode),
-            14 => Some(dropbear_engine::gilrs::Button::LeftThumb),
-            15 => Some(dropbear_engine::gilrs::Button::RightThumb),
-            16 => Some(dropbear_engine::gilrs::Button::DPadUp),
-            17 => Some(dropbear_engine::gilrs::Button::DPadDown),
-            18 => Some(dropbear_engine::gilrs::Button::DPadLeft),
-            19 => Some(dropbear_engine::gilrs::Button::DPadRight),
-            _ => None,
-        }
-    }
-
-    pub fn get_gamepad_id(
-        input: &InputState,
-        target: usize,
-    ) -> Option<dropbear_engine::gilrs::GamepadId> {
-        input
-            .connected_gamepads
-            .iter()
-            .find(|g| usize::from(**g) == target)
-            .copied()
-    }
-
-    pub fn is_gamepad_button_pressed(
-        input: &InputState,
-        gamepad_id: u64,
-        button_ordinal: i32,
-    ) -> bool {
-        let Some(id) = get_gamepad_id(input, gamepad_id as usize) else {
-            return false;
-        };
-
-        if let Some(btn) = map_int_to_gamepad_button(button_ordinal) {
-            input.is_button_pressed(id, btn)
-        } else {
-            false
-        }
-    }
-
-    pub fn get_left_stick(input: &InputState, gamepad_id: u64) -> NVector2 {
-        let Some(id) = get_gamepad_id(input, gamepad_id as usize) else {
-            return NVector2 { x: 0.0, y: 0.0 };
-        };
-        let (x, y) = input.get_left_stick(id);
-        NVector2 {
-            x: x as f64,
-            y: y as f64,
-        }
-    }
-
-    pub fn get_right_stick(input: &InputState, gamepad_id: u64) -> NVector2 {
-        let Some(id) = get_gamepad_id(input, gamepad_id as usize) else {
-            return NVector2 { x: 0.0, y: 0.0 };
-        };
-        let (x, y) = input.get_right_stick(id);
-        NVector2 {
-            x: x as f64,
-            y: y as f64,
-        }
-    }
-
-    impl ToJObject for NVector2 {
-        fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-            let cls = env
-                .load_class(jni_str!("com/dropbear/math/Vector2d"))
-                .map_err(|e| {
-                    eprintln!("Could not find Vector2d class: {:?}", e);
-                    DropbearNativeError::GenericError
-                })?;
-
-            let obj = env
-                .new_object(
-                    cls,
-                    jni_sig!((double, double) -> void),
-                    &[JValue::Double(self.x), JValue::Double(self.y)],
-                )
-                .map_err(|e| {
-                    eprintln!("Failed to create Vector2d object: {:?}", e);
-                    DropbearNativeError::GenericError
-                })?;
-
-            Ok(obj)
-        }
-    }
-
-    impl FromJObject for NVector2 {
-        fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-        where
-            Self: Sized,
-        {
-            let x_val = env
-                .get_field(obj, jni_str!("x"), jni_sig!(double))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-            let y_val = env
-                .get_field(obj, jni_str!("y"), jni_sig!(double))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-            Ok(NVector2 { x: x_val, y: y_val })
-        }
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.GamepadNative",
-        func = "isGamepadButtonPressed"
-    ),
-    c
-)]
-fn is_button_pressed(
-    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
-    gamepad_id: u64,
-    button_ordinal: i32,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::is_gamepad_button_pressed(
-        &input,
-        gamepad_id,
-        button_ordinal,
-    ))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.GamepadNative",
-        func = "getLeftStickPosition"
-    ),
-    c
-)]
-fn get_left_stick_position(
-    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
-    gamepad_id: u64,
-) -> DropbearNativeResult<NVector2> {
-    Ok(shared::get_left_stick(&input, gamepad_id))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.input.GamepadNative",
-        func = "getRightStickPosition"
-    ),
-    c
-)]
-fn get_right_stick_position(
-    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
-    gamepad_id: u64,
-) -> DropbearNativeResult<NVector2> {
-    Ok(shared::get_right_stick(&input, gamepad_id))
-}
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index 02d72ad..31d6cde 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -1,13 +1,10 @@
 pub mod animation;
-pub mod asset;
 pub mod billboard;
 pub mod camera;
 pub mod command;
 pub mod component;
 pub mod config;
 pub mod debug;
-pub mod engine;
-pub mod entity;
 pub mod entity_status;
 pub mod hierarchy;
 pub mod input;
diff --git a/crates/eucalyptus-core/src/lighting.rs b/crates/eucalyptus-core/src/lighting.rs
index d6c7deb..fa13cbc 100644
--- a/crates/eucalyptus-core/src/lighting.rs
+++ b/crates/eucalyptus-core/src/lighting.rs
@@ -3,11 +3,7 @@ use crate::component::{
     SerializedComponent,
 };
 use crate::ptr::WorldPtr;
-use crate::scripting::jni::utils::{FromJObject, ToJObject};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
 use crate::states::SerializedLight;
-use crate::types::{NColour, NVector3};
 use dropbear_engine::attenuation::ATTENUATION_PRESETS;
 use dropbear_engine::entity::{EntityTransform, Transform, inspect_rotation_dquat};
 use crate::hierarchy::EntityTransformExt;
@@ -16,8 +12,6 @@ use dropbear_engine::lighting::{Light, LightType};
 use egui::{CollapsingHeader, ComboBox, DragValue, Ui};
 use glam::{DQuat, DVec3, Vec3};
 use hecs::{Entity, World};
-use jni::objects::{JObject, JValue};
-use jni::{Env, jni_sig, jni_str};
 use std::sync::Arc;
 
 const LIGHT_FORWARD_AXIS: DVec3 = DVec3::new(0.0, -1.0, 0.0);
@@ -365,617 +359,3 @@ impl InspectableComponent for Light {
     }
 }
 
-impl NColour {
-    fn to_linear_rgb(self) -> DVec3 {
-        DVec3::new(
-            self.r as f64 / 255.0,
-            self.g as f64 / 255.0,
-            self.b as f64 / 255.0,
-        )
-    }
-
-    fn from_linear_rgb(rgb: DVec3) -> Self {
-        fn clamp_to_u8(x: f64) -> u8 {
-            let v = (x * 255.0).round();
-            v.clamp(0.0, 255.0) as u8
-        }
-
-        Self {
-            r: clamp_to_u8(rgb.x),
-            g: clamp_to_u8(rgb.y),
-            b: clamp_to_u8(rgb.z),
-            a: 255,
-        }
-    }
-}
-
-impl FromJObject for NColour {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/utils/Colour"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let mut get_byte = |field| -> DropbearNativeResult<u8> {
-            let v = env
-                .get_field(obj, field, jni_sig!(byte))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .b()
-                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-            Ok(v as u8)
-        };
-
-        Ok(Self {
-            r: get_byte(jni_str!("r"))?,
-            g: get_byte(jni_str!("g"))?,
-            b: get_byte(jni_str!("b"))?,
-            a: get_byte(jni_str!("a"))?,
-        })
-    }
-}
-
-impl ToJObject for NColour {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/utils/Colour"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            JValue::Byte(self.r as i8),
-            JValue::Byte(self.g as i8),
-            JValue::Byte(self.b as i8),
-            JValue::Byte(self.a as i8),
-        ];
-
-        env.new_object(&class, jni_sig!((byte, byte, byte, byte) -> void), &args)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug)]
-struct NRange {
-    start: f32,
-    end: f32,
-}
-
-impl FromJObject for NRange {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/utils/Range"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let start = env
-            .get_field(obj, jni_str!("start"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;
-
-        let end = env
-            .get_field(obj, jni_str!("end"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;
-
-        Ok(Self { start, end })
-    }
-}
-
-impl ToJObject for NRange {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/utils/Range"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            JValue::Double(self.start as f64),
-            JValue::Double(self.end as f64),
-        ];
-
-        env.new_object(&class, jni_sig!((double, double) -> void), &args)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-    }
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug)]
-struct NAttenuation {
-    constant: f32,
-    linear: f32,
-    quadratic: f32,
-}
-
-impl FromJObject for NAttenuation {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/lighting/Attenuation"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let constant = env
-            .call_method(obj, jni_str!("getConstant"), jni_sig!(() -> float), &[])
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .f()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let linear = env
-            .call_method(obj, jni_str!("getLinear"), jni_sig!(() -> float), &[])
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .f()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let quadratic = env
-            .call_method(obj, jni_str!("getQuadratic"), jni_sig!(() -> float), &[])
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .f()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(Self {
-            constant,
-            linear,
-            quadratic,
-        })
-    }
-}
-
-impl ToJObject for NAttenuation {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/lighting/Attenuation"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            JValue::Float(self.constant),
-            JValue::Float(self.linear),
-            JValue::Float(self.quadratic),
-        ];
-
-        env.new_object(&class, jni_sig!((float, float, float) -> void), &args)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-    }
-}
-
-pub mod shared {
-    use hecs::{Entity, World};
-
-    pub fn light_exists_for_entity(world: &World, entity: Entity) -> bool {
-        world
-            .get::<&dropbear_engine::lighting::LightComponent>(entity)
-            .is_ok()
-    }
-}
-
-fn get_transform(world: &World, entity: Entity) -> DropbearNativeResult<Transform> {
-    if let Ok(et) = world.get::<&EntityTransform>(entity) {
-        Ok(et.sync())
-    } else if let Ok(t) = world.get::<&Transform>(entity) {
-        Ok(*t)
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-fn set_transform_position(
-    world: &mut World,
-    entity: Entity,
-    position: DVec3,
-) -> DropbearNativeResult<()> {
-    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-        et.local_mut().position = position;
-        Ok(())
-    } else if let Ok(mut t) = world.get::<&mut Transform>(entity) {
-        t.position = position;
-        Ok(())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-fn set_transform_rotation(
-    world: &mut World,
-    entity: Entity,
-    rotation: DQuat,
-) -> DropbearNativeResult<()> {
-    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-        et.local_mut().rotation = rotation;
-        Ok(())
-    } else if let Ok(mut t) = world.get::<&mut Transform>(entity) {
-        t.rotation = rotation;
-        Ok(())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.lighting.LightNative",
-        func = "lightExistsForEntity"
-    ),
-    c
-)]
-fn light_exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::light_exists_for_entity(world, entity))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getPosition"),
-    c
-)]
-fn get_position(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<NVector3> {
-    let transform = get_transform(world, entity)?;
-    Ok(NVector3::from(transform.position))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setPosition"),
-    c
-)]
-fn set_position(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    position: &NVector3,
-) -> DropbearNativeResult<()> {
-    set_transform_position(world, entity, (*position).into())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getDirection"),
-    c
-)]
-fn get_direction(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<NVector3> {
-    let transform = get_transform(world, entity)?;
-    let forward = LIGHT_FORWARD_AXIS;
-    let dir = (transform.rotation * forward).normalize_or_zero();
-    Ok(NVector3::from(dir))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setDirection"),
-    c
-)]
-fn set_direction(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    direction: &NVector3,
-) -> DropbearNativeResult<()> {
-    let dir: DVec3 = (*direction).into();
-    let desired = dir.normalize_or_zero();
-    if desired.length_squared() < 1e-12 {
-        return Err(DropbearNativeError::InvalidArgument);
-    }
-
-    let forward = LIGHT_FORWARD_AXIS;
-    let rotation = DQuat::from_rotation_arc(forward, desired);
-    set_transform_rotation(world, entity, rotation)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getColour"),
-    c
-)]
-fn get_colour(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<NColour> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(NColour::from_linear_rgb(light.component.colour))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setColour"),
-    c
-)]
-fn set_colour(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    colour: &NColour,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.colour = (*colour).to_linear_rgb();
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getLightType"),
-    c
-)]
-fn get_light_type(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<i32> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.component.light_type as i32)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setLightType"),
-    c
-)]
-fn set_light_type(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    light_type: i32,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    light.component.light_type = match light_type {
-        0 => LightType::Directional,
-        1 => LightType::Point,
-        2 => LightType::Spot,
-        _ => return Err(DropbearNativeError::InvalidArgument),
-    };
-
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getIntensity"),
-    c
-)]
-fn get_intensity(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<f64> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.component.intensity as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setIntensity"),
-    c
-)]
-fn set_intensity(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    intensity: f64,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.intensity = intensity as f32;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getAttenuation"),
-    c
-)]
-fn get_attenuation(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<NAttenuation> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    Ok(NAttenuation {
-        constant: light.component.attenuation.constant,
-        linear: light.component.attenuation.linear,
-        quadratic: light.component.attenuation.quadratic,
-    })
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setAttenuation"),
-    c
-)]
-fn set_attenuation(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    attenuation: &NAttenuation,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    light.component.attenuation.constant = attenuation.constant;
-    light.component.attenuation.linear = attenuation.linear;
-    light.component.attenuation.quadratic = attenuation.quadratic;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getEnabled"),
-    c
-)]
-fn get_enabled(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.component.enabled)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setEnabled"),
-    c
-)]
-fn set_enabled(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    enabled: bool,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.enabled = enabled;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getCutoffAngle"),
-    c
-)]
-fn get_cutoff_angle(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<f64> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.component.cutoff_angle as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setCutoffAngle"),
-    c
-)]
-fn set_cutoff_angle(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    cutoff_angle: f64,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.cutoff_angle = cutoff_angle as f32;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.lighting.LightNative",
-        func = "getOuterCutoffAngle"
-    ),
-    c
-)]
-fn get_outer_cutoff_angle(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<f64> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.component.outer_cutoff_angle as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.lighting.LightNative",
-        func = "setOuterCutoffAngle"
-    ),
-    c
-)]
-fn set_outer_cutoff_angle(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    outer_cutoff_angle: f64,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.outer_cutoff_angle = outer_cutoff_angle as f32;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getCastsShadows"),
-    c
-)]
-fn get_casts_shadows(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(light.component.cast_shadows)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setCastsShadows"),
-    c
-)]
-fn set_casts_shadows(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    casts_shadows: bool,
-) -> DropbearNativeResult<()> {
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.cast_shadows = casts_shadows;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "getDepth"),
-    c
-)]
-fn get_depth(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<NRange> {
-    let light = world
-        .get::<&Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-
-    Ok(NRange {
-        start: light.component.depth.start,
-        end: light.component.depth.end,
-    })
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.lighting.LightNative", func = "setDepth"),
-    c
-)]
-fn set_depth(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    depth: &NRange,
-) -> DropbearNativeResult<()> {
-    if !(depth.start.is_finite() && depth.end.is_finite()) {
-        return Err(DropbearNativeError::InvalidArgument);
-    }
-    if depth.end <= depth.start {
-        return Err(DropbearNativeError::InvalidArgument);
-    }
-
-    let mut light = world
-        .get::<&mut Light>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    light.component.depth = depth.start..depth.end;
-    Ok(())
-}
diff --git a/crates/eucalyptus-core/src/mesh.rs b/crates/eucalyptus-core/src/mesh.rs
index 62c1d48..8b13789 100644
--- a/crates/eucalyptus-core/src/mesh.rs
+++ b/crates/eucalyptus-core/src/mesh.rs
@@ -1,241 +1 @@
-pub mod shared {
-    use std::sync::Arc;
 
-    use dropbear_engine::asset::AssetRegistry;
-    use dropbear_engine::entity::MeshRenderer;
-    use dropbear_engine::model::Model;
-    use hecs::{Entity, World};
-
-    pub fn mesh_renderer_exists_for_entity(world: &World, entity: Entity) -> bool {
-        world.get::<&MeshRenderer>(entity).is_ok()
-    }
-
-    fn matches_material_label(material: &dropbear_engine::model::Material, target: &str) -> bool {
-        material.texture_tag.as_deref() == Some(target) || material.name == target
-    }
-
-    pub fn resolve_target_material_index(model: &Model, target_identifier: &str) -> Option<usize> {
-        model
-            .materials
-            .iter()
-            .position(|mat| matches_material_label(mat, target_identifier))
-    }
-
-    pub fn resolve_target_material_name(model: &Model, target_identifier: &str) -> Option<String> {
-        model
-            .materials
-            .iter()
-            .find(|mat| matches_material_label(mat, target_identifier))
-            .map(|mat| mat.name.clone())
-    }
-
-    pub fn model_for_renderer(
-        registry: &AssetRegistry,
-        renderer: &MeshRenderer,
-    ) -> Option<Arc<Model>> {
-        registry.get_model(renderer.model())
-    }
-}
-
-use crate::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped, GraphicsContextPtr, WorldPtr};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use dropbear_engine::asset::Handle;
-use dropbear_engine::entity::MeshRenderer;
-use dropbear_engine::graphics::SharedGraphicsContext;
-use dropbear_engine::texture::Texture;
-use std::collections::HashSet;
-
-#[dropbear_macro::export(kotlin(
-    class = "com.dropbear.components.MeshRendererNative",
-    func = "meshRendererExistsForEntity"
-))]
-fn mesh_renderer_exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::mesh_renderer_exists_for_entity(world, entity))
-}
-
-#[dropbear_macro::export(kotlin(
-    class = "com.dropbear.components.MeshRendererNative",
-    func = "getModel"
-))]
-fn get_model(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<u64> {
-    if let Ok(mesh) = world.get::<&MeshRenderer>(entity) {
-        Ok(mesh.model().id)
-    } else {
-        Err(DropbearNativeError::NoSuchComponent)
-    }
-}
-
-#[dropbear_macro::export(kotlin(
-    class = "com.dropbear.components.MeshRendererNative",
-    func = "setModel"
-))]
-fn set_model(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    model_handle: u64,
-) -> DropbearNativeResult<()> {
-    let handle = Handle::new(model_handle);
-    if asset.read().get_model(handle).is_none() {
-        return Err(DropbearNativeError::InvalidHandle);
-    }
-
-    if let Ok(mut mesh) = world.get::<&mut MeshRenderer>(entity) {
-        mesh.set_model(handle);
-        Ok(())
-    } else {
-        Err(DropbearNativeError::NoSuchComponent)
-    }
-}
-
-#[dropbear_macro::export(kotlin(
-    class = "com.dropbear.components.MeshRendererNative",
-    func = "getAllTextureIds"
-))]
-fn get_all_texture_ids(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<Vec<u64>> {
-    let reader = asset.read();
-    let renderer = world
-        .get::<&MeshRenderer>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    let model =
-        shared::model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
-
-    let mut ids = HashSet::new();
-    let mut push_handle = |handle: &Handle<Texture>| {
-        ids.insert(handle.id);
-    };
-
-    for material in &model.materials {
-        push_handle(&material.diffuse_texture);
-        if let Some(tex) = &material.normal_texture {
-            push_handle(tex);
-        }
-        if let Some(tex) = &material.emissive_texture {
-            push_handle(tex);
-        }
-        if let Some(tex) = &material.metallic_roughness_texture {
-            push_handle(tex);
-        }
-        if let Some(tex) = &material.occlusion_texture {
-            push_handle(tex);
-        }
-    }
-
-    Ok(ids.into_iter().collect())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.MeshRendererNative",
-        func = "getTexture"
-    ),
-    c
-)]
-fn get_texture(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    material_name: String,
-) -> DropbearNativeResult<Option<u64>> {
-    let reader = asset.read();
-    let renderer = world
-        .get::<&MeshRenderer>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    let model =
-        shared::model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
-    let idx = match shared::resolve_target_material_index(&model, &material_name) {
-        Some(value) => value,
-        None => return Ok(None),
-    };
-    let material = model
-        .materials
-        .get(idx)
-        .ok_or(DropbearNativeError::InvalidArgument)?;
-
-    Ok(Some(material.diffuse_texture.id))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.MeshRendererNative",
-        func = "setTextureOverride"
-    ),
-    c
-)]
-fn set_texture_override(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    material_name: String,
-    texture_handle: u64,
-) -> DropbearNativeResult<()> {
-    let reader = asset.read();
-    let renderer = world
-        .get::<&MeshRenderer>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    let model =
-        shared::model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
-    let _ = shared::resolve_target_material_name(&model, &material_name)
-        .ok_or(DropbearNativeError::InvalidArgument)?;
-
-    let handle = Handle::<Texture>::new(texture_handle);
-    if reader.get_texture(handle).is_none() {
-        return Err(DropbearNativeError::InvalidHandle);
-    }
-
-    let mut renderer = world
-        .get::<&mut MeshRenderer>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    if let Some(v) = renderer.material_snapshot.get_mut(&material_name) {
-        v.diffuse_texture = handle;
-    } else {
-        return Err(DropbearNativeError::InvalidArgument);
-    }
-
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.MeshRendererNative",
-        func = "setMaterialTint"
-    ),
-    c
-)]
-fn set_material_tint(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
-    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    material_name: String,
-    r: f32,
-    g: f32,
-    b: f32,
-    a: f32,
-) -> DropbearNativeResult<()> {
-    let _ = asset;
-    let mut renderer = world
-        .get::<&mut MeshRenderer>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    let material = renderer
-        .material_snapshot
-        .get_mut(&material_name)
-        .ok_or(DropbearNativeError::InvalidArgument)?;
-
-    material.base_colour = [r, g, b, a];
-    material.sync_uniform(graphics);
-    Ok(())
-}
diff --git a/crates/eucalyptus-core/src/physics.rs b/crates/eucalyptus-core/src/physics.rs
index ade63ab..8ab9dc6 100644
--- a/crates/eucalyptus-core/src/physics.rs
+++ b/crates/eucalyptus-core/src/physics.rs
@@ -262,346 +262,3 @@ impl Default for PhysicsState {
         Self::new()
     }
 }
-
-pub mod shared {
-    use crate::physics::PhysicsState;
-    use crate::types::NCollider;
-    use crate::types::NVector3;
-    use hecs::Entity;
-    use rapier3d::prelude::ColliderHandle;
-
-    pub fn get_gravity(physics: &PhysicsState) -> NVector3 {
-        NVector3::from(physics.gravity)
-    }
-
-    pub fn set_gravity(physics: &mut PhysicsState, new: NVector3) {
-        physics.gravity = new.to_float_array();
-    }
-
-    fn collider_handle_from_ffi(collider: &NCollider) -> ColliderHandle {
-        ColliderHandle::from_raw_parts(collider.index.index, collider.index.generation)
-    }
-
-    pub fn overlapping(
-        physics: &PhysicsState,
-        collider1: &NCollider,
-        collider2: &NCollider,
-    ) -> bool {
-        let h1 = collider_handle_from_ffi(collider1);
-        let h2 = collider_handle_from_ffi(collider2);
-
-        if physics.colliders.get(h1).is_none() || physics.colliders.get(h2).is_none() {
-            return false;
-        }
-
-        physics
-            .narrow_phase
-            .intersection_pair(h1, h2)
-            .unwrap_or(false)
-    }
-
-    pub fn triggering(
-        physics: &PhysicsState,
-        collider1: &NCollider,
-        collider2: &NCollider,
-    ) -> bool {
-        let h1 = collider_handle_from_ffi(collider1);
-        let h2 = collider_handle_from_ffi(collider2);
-
-        let is_sensor_1 = physics
-            .colliders
-            .get(h1)
-            .map(|c| c.is_sensor())
-            .unwrap_or(false);
-        let is_sensor_2 = physics
-            .colliders
-            .get(h2)
-            .map(|c| c.is_sensor())
-            .unwrap_or(false);
-
-        (is_sensor_1 || is_sensor_2) && overlapping(physics, collider1, collider2)
-    }
-
-    pub fn touching(physics: &PhysicsState, entity1: Entity, entity2: Entity) -> bool {
-        let Some(label1) = physics.entity_label_map.get(&entity1) else {
-            return false;
-        };
-        let Some(label2) = physics.entity_label_map.get(&entity2) else {
-            return false;
-        };
-
-        let Some(handles1) = physics.colliders_entity_map.get(label1) else {
-            return false;
-        };
-        let Some(handles2) = physics.colliders_entity_map.get(label2) else {
-            return false;
-        };
-
-        for (_, h1) in handles1 {
-            for (_, h2) in handles2 {
-                if let Some(pair) = physics.narrow_phase.contact_pair(*h1, *h2) {
-                    if pair.has_any_active_contact() {
-                        return true;
-                    }
-                }
-            }
-        }
-
-        false
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "getGravity"),
-    c
-)]
-fn get_gravity(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-) -> DropbearNativeResult<NVector3> {
-    Ok(shared::get_gravity(physics))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "setGravity"),
-    c
-)]
-fn set_gravity(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    gravity: &NVector3,
-) -> DropbearNativeResult<()> {
-    Ok(shared::set_gravity(physics, *gravity))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "raycast"),
-    c
-)]
-fn raycast(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    origin: &NVector3,
-    dir: &NVector3,
-    time_of_impact: f64,
-    solid: bool,
-) -> DropbearNativeResult<Option<RayHit>> {
-    let qp = physics.broad_phase.as_query_pipeline(
-        &DefaultQueryDispatcher,
-        &physics.bodies,
-        &physics.colliders,
-        QueryFilter::new(),
-    );
-
-    let ray = Ray::new(
-        point![origin.x as f32, origin.y as f32, origin.z as f32].into(),
-        vector![dir.x as f32, dir.y as f32, dir.z as f32].into(),
-    );
-
-    if let Some((hit, distance)) = qp.cast_ray(&ray, time_of_impact as f32, solid) {
-        let raw = hit.0;
-
-        let mut found = None;
-
-        for (l, colliders) in physics.colliders_entity_map.iter() {
-            for (_, c) in colliders {
-                if c.0 == hit.0 {
-                    found = Some((l, c.0));
-                }
-            }
-        }
-
-        if let Some((label, _)) = found {
-            let entity = physics.entity_label_map.iter().find(|(_, l)| *l == label);
-            if let Some((e, _)) = entity {
-                let rayhit = RayHit {
-                    collider: crate::types::NCollider {
-                        index: IndexNative::from(raw),
-                        entity_id: e.to_bits().get(),
-                        id: raw.into_raw_parts().0,
-                    },
-                    distance: distance as f64,
-                };
-
-                Ok(Some(rayhit))
-            } else {
-                Ok(None)
-            }
-        } else {
-            eprintln!("Unknown collider, still returning value without entity_id");
-
-            let rayhit = RayHit {
-                collider: crate::types::NCollider {
-                    index: IndexNative::from(raw),
-                    entity_id: Entity::DANGLING.to_bits().get(),
-                    id: raw.into_raw_parts().0,
-                },
-                distance: distance as f64,
-            };
-            Ok(Some(rayhit))
-        }
-    } else {
-        Ok(None)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "shapeCast"),
-    c
-)]
-fn shape_cast(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    origin: &NVector3,
-    direction: &NVector3,
-    shape: &collider::ColliderShape,
-    time_of_impact: f64,
-    solid: bool,
-) -> DropbearNativeResult<Option<NShapeCastHit>> {
-    let qp = physics.broad_phase.as_query_pipeline(
-        &DefaultQueryDispatcher,
-        &physics.bodies,
-        &physics.colliders,
-        QueryFilter::new(),
-    );
-
-    let dir_len =
-        ((direction.x * direction.x) + (direction.y * direction.y) + (direction.z * direction.z))
-            .sqrt();
-    if dir_len <= f64::EPSILON {
-        return Ok(None);
-    }
-
-    let dir_unit = NVector3 {
-        x: direction.x / dir_len,
-        y: direction.y / dir_len,
-        z: direction.z / dir_len,
-    };
-
-    let cast_shape = {
-        match shape {
-            crate::physics::collider::ColliderShape::Box { half_extents } => {
-                rapier3d::geometry::SharedShape::cuboid(
-                    half_extents.x as f32,
-                    half_extents.y as f32,
-                    half_extents.z as f32,
-                )
-            }
-            crate::physics::collider::ColliderShape::Sphere { radius } => {
-                rapier3d::geometry::SharedShape::ball(*radius)
-            }
-            crate::physics::collider::ColliderShape::Capsule {
-                half_height,
-                radius,
-            } => rapier3d::geometry::SharedShape::capsule_y(*half_height, *radius),
-            crate::physics::collider::ColliderShape::Cylinder {
-                half_height,
-                radius,
-            } => rapier3d::geometry::SharedShape::cylinder(*half_height, *radius),
-            crate::physics::collider::ColliderShape::Cone {
-                half_height,
-                radius,
-            } => rapier3d::geometry::SharedShape::cone(*half_height, *radius),
-        }
-    };
-    let iso: Pose3 =
-        nalgebra::Isometry3::translation(origin.x as f32, origin.y as f32, origin.z as f32).into();
-    let vel: Vec3 = vector![dir_unit.x as f32, dir_unit.y as f32, dir_unit.z as f32].into();
-
-    let options = ShapeCastOptions {
-        max_time_of_impact: time_of_impact as f32,
-        target_distance: 0.0,
-        stop_at_penetration: solid,
-        compute_impact_geometry_on_penetration: true,
-    };
-
-    let Some((hit_handle, toi)) = qp.cast_shape(&iso, vel, cast_shape.as_ref(), options) else {
-        return Ok(None);
-    };
-
-    let collider = collider_ffi_from_handle(&physics, hit_handle);
-
-    let hit = NShapeCastHit {
-        collider,
-        distance: toi.time_of_impact as f64,
-        witness1: NVector3::from([toi.witness1.x, toi.witness1.y, toi.witness1.z]),
-        witness2: NVector3::from([toi.witness2.x, toi.witness2.y, toi.witness2.z]),
-        normal1: NVector3::from([toi.normal1.x, toi.normal1.y, toi.normal1.z]),
-        normal2: NVector3::from([toi.normal2.x, toi.normal2.y, toi.normal2.z]),
-        status: toi.status.into(),
-    };
-
-    Ok(Some(hit))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "isOverlapping"),
-    c
-)]
-fn is_overlapping(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider1: &NCollider,
-    collider2: &NCollider,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::overlapping(physics, collider1, collider2))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "isTriggering"),
-    c
-)]
-fn is_triggering(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider1: &NCollider,
-    collider2: &NCollider,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::triggering(physics, collider1, collider2))
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "isTouching"),
-    c
-)]
-fn is_touching(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    #[dropbear_macro::entity] entity1: Entity,
-    #[dropbear_macro::entity] entity2: Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::touching(physics, entity1, entity2))
-}
-
-fn collider_ffi_from_handle(
-    physics: &PhysicsState,
-    handle: rapier3d::prelude::ColliderHandle,
-) -> NCollider {
-    let (idx, generation) = handle.into_raw_parts();
-
-    let mut found_label = None;
-    for (label, colliders) in physics.colliders_entity_map.iter() {
-        for (_, c) in colliders {
-            if c.0 == handle.0 {
-                found_label = Some(label);
-                break;
-            }
-        }
-        if found_label.is_some() {
-            break;
-        }
-    }
-
-    let entity_id = if let Some(label) = found_label {
-        physics
-            .entity_label_map
-            .iter()
-            .find(|(_, l)| *l == label)
-            .map(|(e, _)| e.to_bits().get())
-            .unwrap_or(Entity::DANGLING.to_bits().get())
-    } else {
-        Entity::DANGLING.to_bits().get()
-    };
-
-    NCollider {
-        index: IndexNative {
-            index: idx,
-            generation,
-        },
-        entity_id,
-        id: idx,
-    }
-}
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index 91984d8..b721137 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -13,28 +13,18 @@
 //!     - `col-` or `-col`
 //!     - `-colonly` (invisible collision mesh)
 
-pub mod collider_group;
-
 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;
-use crate::scripting::jni::utils::{FromJObject, ToJObject};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
 use crate::states::Label;
-use crate::types::{NCollider, NVector3};
-use ::jni::objects::{JObject, JValue};
-use ::jni::{Env, jni_sig, jni_str};
 use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::entity::{MeshRenderer, inspect_rotation_dquat};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::wgpu::util::{BufferInitDescriptor, DeviceExt};
 use dropbear_engine::wgpu::{Buffer, BufferUsages};
 use egui::{CollapsingHeader, ComboBox, Ui};
-use glam::DQuat;
+use glam::{DQuat, Vec3};
 use hecs::{Entity, World};
 use rapier3d::prelude::ColliderBuilder;
 use rapier3d::prelude::{Rotation, SharedShape, TypedShape, Vector};
@@ -480,7 +470,7 @@ impl From<&ColliderShape> for ColliderShapeKey {
 #[dropbear_macro::repr_c_enum]
 pub enum ColliderShape {
     /// Box shape with half-extents (half-width, half-height, half-depth).
-    Box { half_extents: NVector3 },
+    Box { half_extents: Vec3 },
 
     /// Sphere shape with radius.
     Sphere { radius: f32 },
@@ -498,247 +488,8 @@ pub enum ColliderShape {
 impl Default for ColliderShape {
     fn default() -> Self {
         ColliderShape::Box {
-            half_extents: NVector3::from([0.5, 0.5, 0.5]),
-        }
-    }
-}
-
-impl ToJObject for ColliderShape {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        match self {
-            ColliderShape::Box { half_extents } => {
-                let vec_cls = env
-                    .load_class(jni_str!("com/dropbear/math/Vector3d"))
-                    .map_err(|e| {
-                        eprintln!("[JNI Error] Vector3d class not found: {:?}", e);
-                        DropbearNativeError::JNIClassNotFound
-                    })?;
-
-                let vec_obj = env
-                    .new_object(
-                        &vec_cls,
-                        jni_sig!("(DDD)V"),
-                        &[
-                            JValue::Double(half_extents.x),
-                            JValue::Double(half_extents.y),
-                            JValue::Double(half_extents.z),
-                        ],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-                let cls = env
-                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Box"))
-                    .map_err(|e| {
-                        eprintln!("[JNI Error] ColliderShape$Box class not found: {:?}", e);
-                        DropbearNativeError::JNIClassNotFound
-                    })?;
-
-                let obj = env
-                    .new_object(
-                        &cls,
-                        jni_sig!("(Lcom/dropbear/math/Vector3d;)V"),
-                        &[JValue::Object(&vec_obj)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-                Ok(obj)
-            }
-            ColliderShape::Sphere { radius } => {
-                let cls = env
-                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Sphere"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                let obj = env
-                    .new_object(&cls, jni_sig!("(F)V"), &[JValue::Float(*radius)])
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-                Ok(obj)
-            }
-            ColliderShape::Capsule {
-                half_height,
-                radius,
-            } => {
-                let cls = env
-                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Capsule"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                let obj = env
-                    .new_object(
-                        &cls,
-                        jni_sig!("(FF)V"),
-                        &[JValue::Float(*half_height), JValue::Float(*radius)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-                Ok(obj)
-            }
-            ColliderShape::Cylinder {
-                half_height,
-                radius,
-            } => {
-                let cls = env
-                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Cylinder"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                // let ctor = env.get_method_id(&cls, "<init>", "(FF)V")
-                //     .map_err(|_| DropbearNativeError::JNIMethodNotFound)?;
-
-                let obj = env
-                    .new_object(
-                        &cls,
-                        jni_sig!("(FF)V"),
-                        &[JValue::Float(*half_height), JValue::Float(*radius)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-                Ok(obj)
-            }
-            ColliderShape::Cone {
-                half_height,
-                radius,
-            } => {
-                let cls = env
-                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Cone"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                let obj = env
-                    .new_object(
-                        &cls,
-                        jni_sig!("(FF)V"),
-                        &[JValue::Float(*half_height), JValue::Float(*radius)],
-                    )
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-                Ok(obj)
-            }
-        }
-    }
-}
-
-impl FromJObject for ColliderShape {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let is_instance = |env: &mut Env,
-                           obj: &JObject,
-                           class_name: &jni::strings::JNIStr|
-         -> bool { env.is_instance_of(obj, class_name).unwrap_or(false) };
-
-        if is_instance(env, obj, jni_str!("com/dropbear/physics/ColliderShape$Box")) {
-            let vec_obj_val = env
-                .get_field(
-                    obj,
-                    jni_str!("halfExtents"),
-                    jni_sig!("Lcom/dropbear/math/Vector3d;"),
-                )
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-            let vec_obj = vec_obj_val
-                .l()
-                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-            let x = env
-                .get_field(&vec_obj, jni_str!("x"), jni_sig!("D"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .unwrap_or(0.0);
-            let y = env
-                .get_field(&vec_obj, jni_str!("y"), jni_sig!("D"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .unwrap_or(0.0);
-            let z = env
-                .get_field(&vec_obj, jni_str!("z"), jni_sig!("D"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .unwrap_or(0.0);
-
-            return Ok(ColliderShape::Box {
-                half_extents: NVector3::from([x as f32, y as f32, z as f32]),
-            });
-        }
-
-        if is_instance(
-            env,
-            obj,
-            jni_str!("com/dropbear/physics/ColliderShape$Sphere"),
-        ) {
-            let radius = env
-                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-
-            return Ok(ColliderShape::Sphere { radius });
-        }
-
-        if is_instance(
-            env,
-            obj,
-            jni_str!("com/dropbear/physics/ColliderShape$Capsule"),
-        ) {
-            let hh = env
-                .get_field(obj, jni_str!("halfHeight"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-            let r = env
-                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-
-            return Ok(ColliderShape::Capsule {
-                half_height: hh,
-                radius: r,
-            });
-        }
-
-        if is_instance(
-            env,
-            obj,
-            jni_str!("com/dropbear/physics/ColliderShape$Cylinder"),
-        ) {
-            let hh = env
-                .get_field(obj, jni_str!("halfHeight"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-            let r = env
-                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-
-            return Ok(ColliderShape::Cylinder {
-                half_height: hh,
-                radius: r,
-            });
+            half_extents: Vec3::from([0.5, 0.5, 0.5]),
         }
-
-        if is_instance(
-            env,
-            obj,
-            jni_str!("com/dropbear/physics/ColliderShape$Cone"),
-        ) {
-            let hh = env
-                .get_field(obj, jni_str!("halfHeight"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-            let r = env
-                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .f()
-                .unwrap_or(0.0);
-
-            return Ok(ColliderShape::Cone {
-                half_height: hh,
-                radius: r,
-            });
-        }
-
-        Err(DropbearNativeError::GenericError)
     }
 }
 
@@ -768,7 +519,7 @@ impl Collider {
     pub fn box_collider(half_extents: [f32; 3]) -> Self {
         Self {
             shape: ColliderShape::Box {
-                half_extents: NVector3::from(half_extents),
+                half_extents: Vec3::from(half_extents),
             },
             ..Self::new()
         }
@@ -885,550 +636,4 @@ impl Collider {
             // ColliderShape::Compound { .. } => "Compound",
         }
     }
-}
-
-pub struct WireframeGeometry {
-    pub vertex_buffer: Buffer,
-    pub index_buffer: Buffer,
-    pub index_count: u32,
-}
-
-impl WireframeGeometry {
-    pub fn box_wireframe(graphics: Arc<SharedGraphicsContext>, half_extents: [f32; 3]) -> Self {
-        let [hx, hy, hz] = half_extents;
-
-        let vertices: Vec<[f32; 3]> = vec![
-            [-hx, -hy, -hz],
-            [-hx, -hy, hz],
-            [-hx, hy, -hz],
-            [-hx, hy, hz],
-            [hx, -hy, -hz],
-            [hx, -hy, hz],
-            [hx, hy, -hz],
-            [hx, hy, hz],
-        ];
-
-        let indices: Vec<u16> = vec![
-            0, 1, 0, 2, 0, 4, // from corner 0
-            1, 3, 1, 5, // from corner 1
-            2, 3, 2, 6, // from corner 2
-            3, 7, // from corner 3
-            4, 5, 4, 6, // from corner 4
-            5, 7, // from corner 5
-            6, 7, // from corner 6
-        ];
-
-        let vertex_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("box wireframe vertex buffer"),
-            contents: bytemuck::cast_slice(&vertices),
-            usage: BufferUsages::VERTEX,
-        });
-
-        let index_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("box wireframe index buffer"),
-            contents: bytemuck::cast_slice(&indices),
-            usage: BufferUsages::INDEX,
-        });
-
-        Self {
-            vertex_buffer,
-            index_buffer,
-            index_count: indices.len() as u32,
-        }
-    }
-
-    pub fn sphere_wireframe(
-        graphics: Arc<SharedGraphicsContext>,
-        radius: f32,
-        lat_segments: u32,
-        lon_segments: u32,
-    ) -> Self {
-        let mut vertices = Vec::new();
-        let mut indices = Vec::new();
-
-        for lat in 0..=lat_segments {
-            let theta = std::f32::consts::PI * lat as f32 / lat_segments as f32;
-            let sin_theta = theta.sin();
-            let cos_theta = theta.cos();
-
-            for lon in 0..=lon_segments {
-                let phi = 2.0 * std::f32::consts::PI * lon as f32 / lon_segments as f32;
-                let sin_phi = phi.sin();
-                let cos_phi = phi.cos();
-
-                let x = radius * sin_theta * cos_phi;
-                let y = radius * cos_theta;
-                let z = radius * sin_theta * sin_phi;
-
-                vertices.push([x, y, z]);
-            }
-        }
-
-        for lat in 0..lat_segments {
-            for lon in 0..lon_segments {
-                let first = (lat * (lon_segments + 1) + lon) as u16;
-                let second = first + lon_segments as u16 + 1;
-
-                indices.push(first);
-                indices.push(first + 1);
-
-                indices.push(first);
-                indices.push(second);
-            }
-        }
-
-        let vertex_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("sphere wireframe vertex buffer"),
-            contents: bytemuck::cast_slice(&vertices),
-            usage: BufferUsages::VERTEX,
-        });
-
-        let index_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("sphere wireframe index buffer"),
-            contents: bytemuck::cast_slice(&indices),
-            usage: BufferUsages::INDEX,
-        });
-
-        Self {
-            vertex_buffer,
-            index_buffer,
-            index_count: indices.len() as u32,
-        }
-    }
-
-    pub fn capsule_wireframe(
-        graphics: Arc<SharedGraphicsContext>,
-        half_height: f32,
-        radius: f32,
-        segments: u32,
-    ) -> Self {
-        let mut vertices = Vec::new();
-        let mut indices = Vec::new();
-
-        for i in 0..=segments / 2 {
-            let theta = std::f32::consts::PI * i as f32 / segments as f32;
-            let y = half_height + radius * theta.cos();
-            let r = radius * theta.sin();
-
-            for j in 0..=segments {
-                let phi = 2.0 * std::f32::consts::PI * j as f32 / segments as f32;
-                vertices.push([r * phi.cos(), y, r * phi.sin()]);
-            }
-        }
-
-        for j in 0..=segments {
-            let phi = 2.0 * std::f32::consts::PI * j as f32 / segments as f32;
-            vertices.push([radius * phi.cos(), half_height, radius * phi.sin()]);
-            vertices.push([radius * phi.cos(), -half_height, radius * phi.sin()]);
-        }
-
-        for i in 0..=segments / 2 {
-            let theta = std::f32::consts::PI * i as f32 / segments as f32;
-            let y = -half_height - radius * theta.cos();
-            let r = radius * theta.sin();
-
-            for j in 0..=segments {
-                let phi = 2.0 * std::f32::consts::PI * j as f32 / segments as f32;
-                vertices.push([r * phi.cos(), y, r * phi.sin()]);
-            }
-        }
-
-        for i in 0..(vertices.len() as u16 - 1) {
-            indices.push(i);
-            indices.push(i + 1);
-        }
-
-        let vertex_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("capsule wireframe vertex buffer"),
-            contents: bytemuck::cast_slice(&vertices),
-            usage: BufferUsages::VERTEX,
-        });
-
-        let index_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("capsule wireframe index buffer"),
-            contents: bytemuck::cast_slice(&indices),
-            usage: BufferUsages::INDEX,
-        });
-
-        Self {
-            vertex_buffer,
-            index_buffer,
-            index_count: indices.len() as u32,
-        }
-    }
-
-    pub fn cylinder_wireframe(
-        graphics: Arc<SharedGraphicsContext>,
-        half_height: f32,
-        radius: f32,
-        _segments: u32,
-    ) -> Self {
-        // TODO: Implement cylinder wireframe
-        Self::box_wireframe(graphics, [radius, half_height, radius]) // Placeholder
-    }
-
-    pub fn cone_wireframe(
-        graphics: Arc<SharedGraphicsContext>,
-        half_height: f32,
-        radius: f32,
-        _segments: u32,
-    ) -> Self {
-        // TODO: Implement cone wireframe
-        Self::box_wireframe(graphics, [radius, half_height, radius]) // Placeholder
-    }
-}
-
-pub mod shared {
-    use crate::physics::PhysicsState;
-    use crate::types::NCollider;
-    use rapier3d::prelude::ColliderHandle;
-
-    pub fn get_collider_mut<'a>(
-        physics: &'a mut PhysicsState,
-        ffi: &NCollider,
-    ) -> Option<&'a mut rapier3d::prelude::Collider> {
-        let handle = ColliderHandle::from_raw_parts(ffi.index.index, ffi.index.generation);
-        physics.colliders.get_mut(handle)
-    }
-
-    pub fn get_collider<'a>(
-        physics: &'a PhysicsState,
-        ffi: &NCollider,
-    ) -> Option<&'a rapier3d::prelude::Collider> {
-        let handle = ColliderHandle::from_raw_parts(ffi.index.index, ffi.index.generation);
-        physics.colliders.get(handle)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderShape"
-    ),
-    c
-)]
-fn get_collider_shape(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<ColliderShape> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-
-    let rapier_shape = collider.shape();
-    let my_shape = match rapier_shape.as_typed_shape() {
-        TypedShape::Cuboid(c) => {
-            let he = c.half_extents;
-            ColliderShape::Box {
-                half_extents: NVector3::from([he.x, he.y, he.z]),
-            }
-        }
-        TypedShape::Ball(b) => ColliderShape::Sphere { radius: b.radius },
-        TypedShape::Capsule(c) => {
-            let height = c.segment.length();
-            ColliderShape::Capsule {
-                half_height: height * 0.5,
-                radius: c.radius,
-            }
-        }
-        TypedShape::Cylinder(c) => ColliderShape::Cylinder {
-            half_height: c.half_height,
-            radius: c.radius,
-        },
-        TypedShape::Cone(c) => ColliderShape::Cone {
-            half_height: c.half_height,
-            radius: c.radius,
-        },
-        _ => return Err(DropbearNativeError::InvalidArgument),
-    };
-
-    Ok(my_shape)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderShape"
-    ),
-    c
-)]
-fn set_collider_shape(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    shape: &ColliderShape,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-
-    let new_shape = match shape {
-        ColliderShape::Box { half_extents } => SharedShape::cuboid(
-            half_extents.x as f32,
-            half_extents.y as f32,
-            half_extents.z as f32,
-        ),
-        ColliderShape::Sphere { radius } => SharedShape::ball(*radius),
-        ColliderShape::Capsule {
-            half_height,
-            radius,
-        } => SharedShape::capsule_y(*half_height, *radius),
-        ColliderShape::Cylinder {
-            half_height,
-            radius,
-        } => SharedShape::cylinder(*half_height, *radius),
-        ColliderShape::Cone {
-            half_height,
-            radius,
-        } => SharedShape::cone(*half_height, *radius),
-    };
-
-    collider.set_shape(new_shape);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderDensity"
-    ),
-    c
-)]
-fn get_collider_density(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<f64> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    Ok(collider.density() as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderDensity"
-    ),
-    c
-)]
-fn set_collider_density(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    density: f64,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    collider.set_density(density as f32);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderFriction"
-    ),
-    c
-)]
-fn get_collider_friction(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<f64> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    Ok(collider.friction() as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderFriction"
-    ),
-    c
-)]
-fn set_collider_friction(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    friction: f64,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    collider.set_friction(friction as f32);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderRestitution"
-    ),
-    c
-)]
-fn get_collider_restitution(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<f64> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    Ok(collider.restitution() as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderRestitution"
-    ),
-    c
-)]
-fn set_collider_restitution(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    restitution: f64,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    collider.set_restitution(restitution as f32);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderMass"
-    ),
-    c
-)]
-fn get_collider_mass(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<f64> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    Ok(collider.mass() as f64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderMass"
-    ),
-    c
-)]
-fn set_collider_mass(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    mass: f64,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    collider.set_mass(mass as f32);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderIsSensor"
-    ),
-    c
-)]
-fn get_collider_is_sensor(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<bool> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    Ok(collider.is_sensor())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderIsSensor"
-    ),
-    c
-)]
-fn set_collider_is_sensor(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    is_sensor: bool,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    collider.set_sensor(is_sensor);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderTranslation"
-    ),
-    c
-)]
-fn get_collider_translation(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<NVector3> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    let t: Vector = collider.translation();
-    Ok(NVector3::new(t.x as f64, t.y as f64, t.z as f64))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderTranslation"
-    ),
-    c
-)]
-fn set_collider_translation(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    translation: &NVector3,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    let t = Vector::new(
-        translation.x as f32,
-        translation.y as f32,
-        translation.z as f32,
-    );
-    collider.set_translation(t);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "getColliderRotation"
-    ),
-    c
-)]
-fn get_collider_rotation(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    collider: &NCollider,
-) -> DropbearNativeResult<NVector3> {
-    let collider =
-        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    let r: Rotation = collider.rotation();
-    let q = DQuat::from_xyzw(r.x as f64, r.y as f64, r.z as f64, r.w as f64);
-    let euler = q.to_euler(glam::EulerRot::XYZ);
-    Ok(NVector3::new(euler.0, euler.1, euler.2))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderNative",
-        func = "setColliderRotation"
-    ),
-    c
-)]
-fn set_collider_rotation(
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    collider: &NCollider,
-    rotation: &NVector3,
-) -> DropbearNativeResult<()> {
-    let collider =
-        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-    let q = DQuat::from_euler(glam::EulerRot::XYZ, rotation.x, rotation.y, rotation.z);
-    let r = Rotation::from_array([q.w as f32, q.x as f32, q.y as f32, q.z as f32]);
-    collider.set_rotation(r);
-    Ok(())
-}
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/physics/collider/collider_group.rs b/crates/eucalyptus-core/src/physics/collider/collider_group.rs
deleted file mode 100644
index d4de471..0000000
--- a/crates/eucalyptus-core/src/physics/collider/collider_group.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-//! Scripting module for collider groups.
-
-use crate::physics::PhysicsState;
-use crate::physics::collider::ColliderGroup;
-use crate::ptr::WorldPtr;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use crate::types::{IndexNative, NCollider};
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderGroupNative",
-        func = "colliderGroupExistsForEntity"
-    ),
-    c
-)]
-fn exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(world.get::<&ColliderGroup>(entity).is_ok())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.ColliderGroupNative",
-        func = "getColliderGroupColliders"
-    ),
-    c
-)]
-fn get_colliders(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(crate::ptr::PhysicsStatePtr)] physics: &PhysicsState,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<Vec<NCollider>> {
-    if world.get::<&ColliderGroup>(entity).is_ok() {
-        let handles_opt = physics
-            .entity_label_map
-            .get(&entity)
-            .and_then(|label| physics.colliders_entity_map.get(label));
-
-        let mut colliders: Vec<NCollider> = Vec::new();
-
-        if let Some(handles) = handles_opt {
-            for (_, handle) in handles {
-                let (idx, generation) = handle.into_raw_parts();
-
-                let col = NCollider {
-                    index: IndexNative {
-                        index: idx,
-                        generation,
-                    },
-                    entity_id: entity.to_bits().get(),
-                    id: idx,
-                };
-                colliders.push(col);
-            }
-        }
-
-        Ok(colliders)
-    } else {
-        Err(DropbearNativeError::MissingComponent)?
-    }
-}
diff --git a/crates/eucalyptus-core/src/physics/kcc.rs b/crates/eucalyptus-core/src/physics/kcc.rs
index 00786f1..1a22e1f 100644
--- a/crates/eucalyptus-core/src/physics/kcc.rs
+++ b/crates/eucalyptus-core/src/physics/kcc.rs
@@ -1,18 +1,14 @@
 //! Module that relates to the [Kinematic Character Controller](https://rapier.rs/docs/user_guides/rust/character_controller)
 //! (or kcc for short) in the [rapier3d] physics engine.
 
-pub mod character_collision;
-
 use crate::component::{
     Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent,
 };
 use crate::physics::PhysicsState;
 use crate::ptr::WorldPtr;
-use crate::scripting::jni::utils::ToJObject;
 use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
 use crate::states::Label;
-use crate::types::{IndexNative, NQuaternion, NVector3};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::{ComboBox, DragValue, Ui};
 use hecs::{Entity, World};
@@ -27,6 +23,8 @@ use rapier3d::na::{UnitVector3, Vector3};
 use rapier3d::prelude::QueryFilter;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
+use glam::Vec3;
+use rapier3d::data::Index;
 
 /// The kinematic character controller (kcc) component.
 #[derive(Debug, Default, Serialize, Deserialize, Clone)]
@@ -42,37 +40,11 @@ pub struct KCC {
 #[repr(C)]
 #[derive(Debug, Clone)]
 pub struct CharacterMovementResult {
-    pub translation: NVector3,
+    pub translation: Vec3,
     pub grounded: bool,
     pub is_sliding_down_slope: bool,
 }
 
-impl ToJObject for CharacterMovementResult {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/CharacterMovementResult"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let translation_obj = self.translation.to_jobject(env)?;
-
-        let args = [
-            JValue::Object(&translation_obj),
-            JValue::Bool(self.grounded),
-            JValue::Bool(self.is_sliding_down_slope),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!((com.dropbear.math.Vector3d, boolean, boolean) -> void),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
 #[typetag::serde]
 impl SerializedComponent for KCC {}
 
@@ -281,270 +253,5 @@ impl KCC {
 #[repr(C)]
 struct CharacterCollisionArray {
     entity_id: u64,
-    collisions: Vec<IndexNative>,
-}
-
-impl ToJObject for CharacterCollisionArray {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let collision_cls = env
-            .load_class(jni_str!("com/dropbear/physics/CharacterCollision"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let entity_cls = env
-            .load_class(jni_str!("com/dropbear/EntityId"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let entity_obj = env
-            .new_object(
-                &entity_cls,
-                jni_sig!((long) -> void),
-                &[JValue::Long(self.entity_id as i64)],
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let out = env
-            .new_object_array(
-                self.collisions.len() as i32,
-                &collision_cls,
-                JObject::null(),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        for (i, handle) in self.collisions.iter().enumerate() {
-            let index_obj = handle.to_jobject(env)?;
-            let collision_obj = env
-                .new_object(
-                    &collision_cls,
-                    jni_sig!((com.dropbear.EntityId, com.dropbear.physics.Index) -> void),
-                    &[JValue::Object(&entity_obj), JValue::Object(&index_obj)],
-                )
-                .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-            out.set_element(env, i, collision_obj)
-                .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        }
-
-        Ok(JObject::from(out))
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.KinematicCharacterControllerNative",
-        func = "existsForEntity"
-    ),
-    c
-)]
-fn kcc_exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(world.get::<&KCC>(entity).is_ok())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.KinematicCharacterControllerNative",
-        func = "moveCharacter"
-    ),
-    c
-)]
-fn move_character(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(crate::ptr::PhysicsStatePtr)] physics_state: &mut PhysicsState,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    translation: &NVector3,
-    delta_time: f64,
-) -> DropbearNativeResult<()> {
-    if let Ok((label, kcc)) = world.query_one::<(&Label, &mut KCC)>(entity).get() {
-        let rigid_body_handle = physics_state
-            .bodies_entity_map
-            .get(label)
-            .ok_or(DropbearNativeError::NoSuchHandle)?;
-
-        let (body_type, body_pos) = {
-            let body = physics_state
-                .bodies
-                .get(*rigid_body_handle)
-                .ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-            (body.body_type(), *body.position())
-        };
-
-        if body_type != RigidBodyType::KinematicPositionBased {
-            return Ok(()); // soft error, just tell the user
-        }
-
-        let collider_handles = physics_state
-            .colliders_entity_map
-            .get(label)
-            .ok_or(DropbearNativeError::NoSuchHandle)?;
-        let (_, collider_handle) = collider_handles
-            .first()
-            .ok_or(DropbearNativeError::NoSuchHandle)?;
-        let collider = physics_state
-            .colliders
-            .get(*collider_handle)
-            .ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-
-        let character_pos = if let Some(pos_wrt_parent) = collider.position_wrt_parent() {
-            body_pos * (*pos_wrt_parent)
-        } else {
-            *collider.position()
-        };
-
-        let filter = QueryFilter::default()
-            .exclude_rigid_body(*rigid_body_handle)
-            .exclude_sensors();
-        let query_pipeline = physics_state.broad_phase.as_query_pipeline(
-            physics_state.narrow_phase.query_dispatcher(),
-            &physics_state.bodies,
-            &physics_state.colliders,
-            filter,
-        );
-
-        let movement = kcc.controller.move_shape(
-            delta_time as f32,
-            &query_pipeline,
-            collider.shape(),
-            &character_pos,
-            rapier3d::prelude::Vector::new(
-                translation.x as f32,
-                translation.y as f32,
-                translation.z as f32,
-            ),
-            |collision| {
-                if let Some(collisions) =
-                    physics_state.collision_events_to_deal_with.get_mut(&entity)
-                {
-                    collisions.push(collision)
-                } else {
-                    physics_state
-                        .collision_events_to_deal_with
-                        .insert(entity, vec![collision]);
-                }
-            },
-        );
-
-        if let Some(body) = physics_state.bodies.get_mut(*rigid_body_handle) {
-            let current_pos = body.translation();
-            let new_pos = current_pos + movement.translation;
-            body.set_next_kinematic_translation(new_pos);
-        }
-
-        kcc.movement = Some(CharacterMovementResult {
-            translation: movement.translation.into(),
-            grounded: movement.grounded,
-            is_sliding_down_slope: movement.is_sliding_down_slope,
-        });
-
-        Ok(())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.KinematicCharacterControllerNative",
-        func = "setRotation"
-    ),
-    c
-)]
-fn set_rotation(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(crate::ptr::PhysicsStatePtr)] physics_state: &mut PhysicsState,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    rotation: &NQuaternion,
-) -> DropbearNativeResult<()> {
-    if let Ok((label, _)) = world.query_one::<(&Label, &KCC)>(entity).get() {
-        let rigid_body_handle = physics_state
-            .bodies_entity_map
-            .get(label)
-            .ok_or(DropbearNativeError::NoSuchHandle)?;
-
-        let body_type = {
-            let body = physics_state
-                .bodies
-                .get(*rigid_body_handle)
-                .ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
-            body.body_type()
-        };
-
-        if body_type != RigidBodyType::KinematicPositionBased {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let len = (rotation.x * rotation.x
-            + rotation.y * rotation.y
-            + rotation.z * rotation.z
-            + rotation.w * rotation.w)
-            .sqrt();
-        let (x, y, z, w) = if len > 0.0 {
-            (
-                rotation.x / len,
-                rotation.y / len,
-                rotation.z / len,
-                rotation.w / len,
-            )
-        } else {
-            (0.0, 0.0, 0.0, 1.0)
-        };
-
-        if let Some(body) = physics_state.bodies.get_mut(*rigid_body_handle) {
-            let rot = Rotation::from_xyzw(x as f32, y as f32, z as f32, w as f32);
-            body.set_next_kinematic_rotation(rot);
-        }
-
-        Ok(())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.KinematicCharacterControllerNative",
-        func = "getHit"
-    ),
-    c
-)]
-fn get_hit(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<CharacterCollisionArray> {
-    let kcc = world
-        .get::<&KCC>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    let mut collisions = Vec::with_capacity(kcc.collisions.len());
-    for collision in &kcc.collisions {
-        let (idx, generation) = collision.handle.into_raw_parts();
-        collisions.push(IndexNative {
-            index: idx,
-            generation,
-        });
-    }
-
-    Ok(CharacterCollisionArray {
-        entity_id: entity.to_bits().get(),
-        collisions,
-    })
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.KinematicCharacterControllerNative",
-        func = "getMovementResult"
-    ),
-    c
-)]
-fn get_movement_result(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<Option<CharacterMovementResult>> {
-    world
-        .get::<&KCC>(entity)
-        .map(|kcc| kcc.movement.clone())
-        .map(Ok)
-        .unwrap_or(Ok(None))
+    collisions: Vec<Index>,
 }
diff --git a/crates/eucalyptus-core/src/physics/kcc/character_collision.rs b/crates/eucalyptus-core/src/physics/kcc/character_collision.rs
deleted file mode 100644
index 0bf0104..0000000
--- a/crates/eucalyptus-core/src/physics/kcc/character_collision.rs
+++ /dev/null
@@ -1,373 +0,0 @@
-use crate::scripting::jni::utils::ToJObject;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use crate::types::{IndexNative, NCollider, NShapeCastStatus, NTransform, NVector3};
-use ::jni::objects::{JObject, JValue};
-use ::jni::{Env, jni_sig, jni_str};
-use hecs::{Entity, World};
-use rapier3d::control::CharacterCollision;
-
-use crate::physics::collider::ColliderGroup;
-use crate::physics::kcc::KCC;
-use crate::ptr::WorldPtr;
-
-fn get_collision_from_world(
-    world: &World,
-    entity: Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<CharacterCollision> {
-    let kcc = world
-        .get::<&KCC>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    kcc.collisions
-        .iter()
-        .copied()
-        .find(|c| {
-            let (idx, generation) = c.handle.into_raw_parts();
-            idx == collision_handle.index && generation == collision_handle.generation
-        })
-        .ok_or(DropbearNativeError::NoSuchHandle)
-}
-
-fn collider_ffi_from_handle(
-    world: &World,
-    handle: rapier3d::prelude::ColliderHandle,
-) -> Option<NCollider> {
-    let (idx, generation) = handle.into_raw_parts();
-
-    for (entity, group) in world.query::<(Entity, &ColliderGroup)>().iter() {
-        if group.colliders.iter().any(|c| c.id == idx) {
-            return Some(NCollider {
-                index: IndexNative {
-                    index: idx,
-                    generation,
-                },
-                entity_id: entity.to_bits().get(),
-                id: idx,
-            });
-        }
-    }
-
-    None
-}
-
-impl ToJObject for NShapeCastStatus {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/ShapeCastStatus"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = match self {
-            NShapeCastStatus::OutOfIterations => "OutOfIterations",
-            NShapeCastStatus::Converged => "Converged",
-            NShapeCastStatus::Failed => "Failed",
-            NShapeCastStatus::PenetratingOrWithinTargetDist => "PenetratingOrWithinTargetDist",
-        };
-
-        let name_jstring = env
-            .new_string(name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let value = env
-            .call_static_method(
-                class,
-                jni_str!("valueOf"),
-                jni_sig!((java.lang.String) -> com.dropbear.physics.ShapeCastStatus),
-                &[JValue::from(&name_jstring)],
-            )
-            .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(value)
-    }
-}
-
-pub mod shared {
-    use super::*;
-    use crate::types::NVector3;
-    use glam::{DQuat, DVec3};
-    use rapier3d::na::Quaternion;
-
-    pub fn get_collider(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NCollider> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        collider_ffi_from_handle(world, collision.handle)
-            .ok_or(DropbearNativeError::PhysicsObjectNotFound)
-    }
-
-    pub fn get_character_position(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NTransform> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-
-        let iso = collision.character_pos;
-        let t = iso.translation;
-        let rot = iso.rotation;
-        let q: Quaternion<f32> = Quaternion::from(rot);
-
-        Ok(NTransform {
-            position: DVec3::new(t.x as f64, t.y as f64, t.z as f64).into(),
-            rotation: DQuat::from_xyzw(q.i as f64, q.j as f64, q.k as f64, q.w as f64).into(),
-            scale: DVec3::ONE.into(),
-        })
-    }
-
-    pub fn get_translation_applied(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NVector3> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        let v = collision.translation_applied;
-        Ok(NVector3 {
-            x: v.x as f64,
-            y: v.y as f64,
-            z: v.z as f64,
-        })
-    }
-
-    pub fn get_translation_remaining(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NVector3> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        let v = collision.translation_remaining;
-        Ok(NVector3 {
-            x: v.x as f64,
-            y: v.y as f64,
-            z: v.z as f64,
-        })
-    }
-
-    pub fn get_time_of_impact(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<f64> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        Ok(collision.hit.time_of_impact as f64)
-    }
-
-    pub fn get_witness1(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NVector3> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        let p = collision.hit.witness1;
-        Ok(NVector3 {
-            x: p.x as f64,
-            y: p.y as f64,
-            z: p.z as f64,
-        })
-    }
-
-    pub fn get_witness2(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NVector3> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        let p = collision.hit.witness2;
-        Ok(NVector3 {
-            x: p.x as f64,
-            y: p.y as f64,
-            z: p.z as f64,
-        })
-    }
-
-    pub fn get_normal1(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NVector3> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        let n = collision.hit.normal1;
-        Ok(NVector3 {
-            x: n.x as f64,
-            y: n.y as f64,
-            z: n.z as f64,
-        })
-    }
-
-    pub fn get_normal2(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NVector3> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        let n = collision.hit.normal2;
-        Ok(NVector3 {
-            x: n.x as f64,
-            y: n.y as f64,
-            z: n.z as f64,
-        })
-    }
-
-    pub fn get_status(
-        world: &World,
-        entity: Entity,
-        collision_handle: &IndexNative,
-    ) -> DropbearNativeResult<NShapeCastStatus> {
-        let collision = get_collision_from_world(world, entity, collision_handle)?;
-        Ok(collision.hit.status.into())
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getCollider"
-    ),
-    c
-)]
-fn get_character_collision_collider(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<crate::types::NCollider> {
-    shared::get_collider(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getCharacterPosition"
-    ),
-    c
-)]
-fn get_character_collision_position(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NTransform> {
-    shared::get_character_position(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getTranslationApplied"
-    ),
-    c
-)]
-fn get_character_collision_translation_applied(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_translation_applied(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getTranslationRemaining"
-    ),
-    c
-)]
-fn get_character_collision_translation_remaining(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_translation_remaining(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getTimeOfImpact"
-    ),
-    c
-)]
-fn get_character_collision_time_of_impact(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<f64> {
-    shared::get_time_of_impact(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getWitness1"
-    ),
-    c
-)]
-fn get_character_collision_witness1(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_witness1(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getWitness2"
-    ),
-    c
-)]
-fn get_character_collision_witness2(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_witness2(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getNormal1"
-    ),
-    c
-)]
-fn get_character_collision_normal1(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_normal1(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getNormal2"
-    ),
-    c
-)]
-fn get_character_collision_normal2(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_normal2(world, entity, collision_handle)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.CharacterCollisionNative",
-        func = "getStatus"
-    ),
-    c
-)]
-fn get_character_collision_status(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    collision_handle: &IndexNative,
-) -> DropbearNativeResult<NShapeCastStatus> {
-    shared::get_status(world, entity, collision_handle)
-}
diff --git a/crates/eucalyptus-core/src/physics/rigidbody.rs b/crates/eucalyptus-core/src/physics/rigidbody.rs
index 9ab69b2..dab70d4 100644
--- a/crates/eucalyptus-core/src/physics/rigidbody.rs
+++ b/crates/eucalyptus-core/src/physics/rigidbody.rs
@@ -5,13 +5,10 @@ use crate::component::{
 };
 use crate::physics::PhysicsState;
 use crate::ptr::{PhysicsStatePtr, WorldPtr};
-use crate::scripting::jni::utils::{FromJObject, ToJObject};
 use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
 use crate::states::Label;
 use crate::types::{IndexNative, NCollider, NVector3, RigidBodyContext};
-use ::jni::objects::{JObject, JValue};
-use ::jni::{Env, jni_sig, jni_str};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::{CollapsingHeader, ComboBox, DragValue, Ui};
 use hecs::{Entity, World};
@@ -60,64 +57,6 @@ pub struct AxisLock {
     pub z: bool,
 }
 
-impl FromJObject for AxisLock {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/AxisLock"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let x = env
-            .get_field(obj, jni_str!("x"), jni_sig!(boolean))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .z()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let y = env
-            .get_field(obj, jni_str!("y"), jni_sig!(boolean))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .z()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let z = env
-            .get_field(obj, jni_str!("z"), jni_sig!(boolean))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .z()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(AxisLock { x, y, z })
-    }
-}
-
-impl ToJObject for AxisLock {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/AxisLock"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            JValue::Bool(self.x),
-            JValue::Bool(self.y),
-            JValue::Bool(self.z),
-        ];
-
-        let obj = env
-            .new_object(&class, jni_sig!((boolean, boolean, boolean) -> void), &args)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct RigidBody {
     /// The entity this component is attached to.
@@ -871,399 +810,3 @@ pub mod shared {
     }
 }
 
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "rigidBodyExistsForEntity"
-    ),
-    c
-)]
-fn exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<Option<IndexNative>> {
-    Ok(shared::rigid_body_exists_for_entity(world, physics, entity))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyMode"
-    ),
-    c
-)]
-fn get_rigidbody_mode(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<i32> {
-    let body_type = shared::get_rigidbody_type(physics, rigidbody)?;
-    Ok(match body_type {
-        RigidBodyType::Dynamic => 0,
-        RigidBodyType::Fixed => 1,
-        RigidBodyType::KinematicPositionBased => 2,
-        RigidBodyType::KinematicVelocityBased => 3,
-    })
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyMode"
-    ),
-    c
-)]
-fn set_rigidbody_mode(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    mode: i32,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_type(physics, world, rigidbody, mode as i64)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyGravityScale"
-    ),
-    c
-)]
-fn get_rigidbody_gravity_scale(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<f64> {
-    shared::get_rigidbody_gravity_scale(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyGravityScale"
-    ),
-    c
-)]
-fn set_rigidbody_gravity_scale(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    gravity_scale: f64,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_gravity_scale(physics, world, rigidbody, gravity_scale)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyLinearDamping"
-    ),
-    c
-)]
-fn get_rigidbody_linear_damping(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<f64> {
-    shared::get_rigidbody_linear_damping(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyLinearDamping"
-    ),
-    c
-)]
-fn set_rigidbody_linear_damping(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    linear_damping: f64,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_linear_damping(physics, world, rigidbody, linear_damping)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyAngularDamping"
-    ),
-    c
-)]
-fn get_rigidbody_angular_damping(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<f64> {
-    shared::get_rigidbody_angular_damping(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyAngularDamping"
-    ),
-    c
-)]
-fn set_rigidbody_angular_damping(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    angular_damping: f64,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_angular_damping(physics, world, rigidbody, angular_damping)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodySleep"
-    ),
-    c
-)]
-fn get_rigidbody_sleep(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<bool> {
-    shared::get_rigidbody_sleep(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodySleep"
-    ),
-    c
-)]
-fn set_rigidbody_sleep(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    sleep: bool,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_sleep(physics, world, rigidbody, sleep)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyCcdEnabled"
-    ),
-    c
-)]
-fn get_rigidbody_ccd_enabled(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<bool> {
-    shared::get_rigidbody_ccd(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyCcdEnabled"
-    ),
-    c
-)]
-fn set_rigidbody_ccd_enabled(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    ccd_enabled: bool,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_ccd(physics, world, rigidbody, ccd_enabled)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyLinearVelocity"
-    ),
-    c
-)]
-fn get_rigidbody_linear_velocity(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_rigidbody_linvel(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyLinearVelocity"
-    ),
-    c
-)]
-fn set_rigidbody_linear_velocity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    linear_velocity: &NVector3,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_linvel(physics, world, rigidbody, *linear_velocity)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyAngularVelocity"
-    ),
-    c
-)]
-fn get_rigidbody_angular_velocity(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<NVector3> {
-    shared::get_rigidbody_angvel(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyAngularVelocity"
-    ),
-    c
-)]
-fn set_rigidbody_angular_velocity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    angular_velocity: &NVector3,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_angvel(physics, world, rigidbody, *angular_velocity)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyLockTranslation"
-    ),
-    c
-)]
-fn get_rigidbody_lock_translation(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<AxisLock> {
-    shared::get_rigidbody_lock_translation(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyLockTranslation"
-    ),
-    c
-)]
-fn set_rigidbody_lock_translation(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    lock_translation: &AxisLock,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_lock_translation(physics, world, rigidbody, *lock_translation)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyLockRotation"
-    ),
-    c
-)]
-fn get_rigidbody_lock_rotation(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<AxisLock> {
-    shared::get_rigidbody_lock_rotation(physics, rigidbody)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "setRigidBodyLockRotation"
-    ),
-    c
-)]
-fn set_rigidbody_lock_rotation(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    lock_rotation: &AxisLock,
-) -> DropbearNativeResult<()> {
-    shared::set_rigidbody_lock_rotation(physics, world, rigidbody, *lock_rotation)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "getRigidBodyChildren"
-    ),
-    c
-)]
-fn get_rigidbody_children(
-    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
-    rigidbody: &RigidBodyContext,
-) -> DropbearNativeResult<Vec<NCollider>> {
-    let children = shared::get_rigidbody_children(physics, rigidbody)?;
-    let colliders = children
-        .into_iter()
-        .map(|handle| {
-            let (idx, generation) = handle.into_raw_parts();
-            NCollider {
-                index: IndexNative {
-                    index: idx,
-                    generation,
-                },
-                entity_id: rigidbody.entity_id,
-                id: idx,
-            }
-        })
-        .collect();
-
-    Ok(colliders)
-}
-
-#[dropbear_macro::export(
-    kotlin(class = "com.dropbear.physics.RigidBodyNative", func = "applyImpulse"),
-    c
-)]
-fn apply_impulse(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    x: f64,
-    y: f64,
-    z: f64,
-) -> DropbearNativeResult<()> {
-    let impulse = NVector3::new(x, y, z);
-    shared::apply_impulse(physics, world, rigidbody, impulse)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.physics.RigidBodyNative",
-        func = "applyTorqueImpulse"
-    ),
-    c
-)]
-fn apply_torque_impulse(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
-    rigidbody: &RigidBodyContext,
-    x: f64,
-    y: f64,
-    z: f64,
-) -> DropbearNativeResult<()> {
-    let torque = NVector3::new(x, y, z);
-    shared::apply_torque_impulse(physics, world, rigidbody, torque)
-}
diff --git a/crates/eucalyptus-core/src/properties.rs b/crates/eucalyptus-core/src/properties.rs
index 169849a..3f32b42 100644
--- a/crates/eucalyptus-core/src/properties.rs
+++ b/crates/eucalyptus-core/src/properties.rs
@@ -2,11 +2,7 @@ use crate::component::{
     Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent,
     SerializedComponent,
 };
-use crate::ptr::WorldPtr;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
 use crate::states::Property;
-use crate::types::NVector3;
 use crate::warn;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui};
@@ -41,10 +37,10 @@ impl Component for CustomProperties {
         }
     }
 
-    fn init<'a>(
-        ser: &'a Self::SerializedForm,
+    fn init(
+        ser: &Self::SerializedForm,
         _graphics: Arc<SharedGraphicsContext>,
-    ) -> ComponentInitFuture<'a, Self> {
+    ) -> ComponentInitFuture<Self> {
         Box::pin(async move { Ok((ser.clone(),)) })
     }
 
@@ -393,310 +389,3 @@ pub mod shared {
     }
 }
 
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "customPropertiesExistsForEntity"
-    ),
-    c
-)]
-fn custom_properties_exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(shared::custom_properties_exists_for_entity(world, entity))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getStringProperty"
-    ),
-    c
-)]
-fn get_string_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<String>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::String(s) => Some(s.clone()),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getIntProperty"
-    ),
-    c
-)]
-fn get_int_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<i32>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::Int(v) => Some(*v as i32),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getLongProperty"
-    ),
-    c
-)]
-fn get_long_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<i64>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::Int(v) => Some(*v),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getDoubleProperty"
-    ),
-    c
-)]
-fn get_double_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<f64>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::Double(v) => Some(*v),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getFloatProperty"
-    ),
-    c
-)]
-fn get_float_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<f32>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::Double(v) => Some(*v as f32),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getBoolProperty"
-    ),
-    c
-)]
-fn get_bool_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<bool>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::Bool(v) => Some(*v),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "getVec3Property"
-    ),
-    c
-)]
-fn get_vec3_property(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-) -> DropbearNativeResult<Option<NVector3>> {
-    let props = world
-        .get::<&CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-
-    Ok(props.get_property(&key).and_then(|value| match value {
-        Value::Vec3(v) => Some(NVector3::from(*v)),
-        _ => None,
-    }))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setStringProperty"
-    ),
-    c
-)]
-fn set_string_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: String,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::String(value));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setIntProperty"
-    ),
-    c
-)]
-fn set_int_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: i32,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::Int(value as i64));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setLongProperty"
-    ),
-    c
-)]
-fn set_long_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: i64,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::Int(value));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setDoubleProperty"
-    ),
-    c
-)]
-fn set_double_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: f64,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::Double(value));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setFloatProperty"
-    ),
-    c
-)]
-fn set_float_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: f64,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::Double(value));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setBoolProperty"
-    ),
-    c
-)]
-fn set_bool_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: bool,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::Bool(value));
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.CustomPropertiesNative",
-        func = "setVec3Property"
-    ),
-    c
-)]
-fn set_vec3_property(
-    #[dropbear_macro::define(WorldPtr)] world: &mut World,
-    #[dropbear_macro::entity] entity: Entity,
-    key: String,
-    value: &NVector3,
-) -> DropbearNativeResult<()> {
-    let mut props = world
-        .get::<&mut CustomProperties>(entity)
-        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
-    props.set_property(key, Value::Vec3(value.to_array()));
-    Ok(())
-}
diff --git a/crates/eucalyptus-core/src/scene/scripting.rs b/crates/eucalyptus-core/src/scene/scripting.rs
index 1bd9b44..76f2f54 100644
--- a/crates/eucalyptus-core/src/scene/scripting.rs
+++ b/crates/eucalyptus-core/src/scene/scripting.rs
@@ -92,7 +92,6 @@ pub mod shared {
         let mut loader = scene_loader.lock();
 
         if let Some(entry) = loader.get_entry_mut(scene_id) {
-            // Update progress from status channel if available
             if let Some(ref rx) = entry.status {
                 while let Ok(status) = rx.try_recv() {
                     match status {
@@ -137,124 +136,3 @@ pub mod shared {
     }
 }
 
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneManagerNative",
-        func = "loadSceneAsync"
-    ),
-    c
-)]
-fn load_scene_async(
-    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
-    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
-    scene_name: String,
-) -> DropbearNativeResult<u64> {
-    Ok(shared::load_scene_async(
-        command_buffer,
-        scene_loader,
-        scene_name,
-        None,
-    )?)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneManagerNative",
-        func = "loadSceneAsyncWithLoading"
-    ),
-    c
-)]
-fn load_scene_async_with_loading(
-    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
-    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
-    scene_name: String,
-    loading_scene: String,
-) -> DropbearNativeResult<u64> {
-    Ok(shared::load_scene_async(
-        command_buffer,
-        scene_loader,
-        scene_name,
-        Some(loading_scene),
-    )?)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneManagerNative",
-        func = "switchToSceneImmediate"
-    ),
-    c
-)]
-fn switch_to_scene_immediate(
-    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
-    scene_name: String,
-) -> DropbearNativeResult<()> {
-    Ok(shared::switch_to_scene_immediate(
-        command_buffer,
-        scene_name,
-    )?)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneLoadHandleNative",
-        func = "getSceneLoadHandleSceneName"
-    ),
-    c
-)]
-fn get_scene_load_handle_scene_name(
-    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
-    scene_id: u64,
-) -> DropbearNativeResult<String> {
-    Ok(shared::get_scene_load_handle_scene_name(
-        scene_loader,
-        scene_id,
-    )?)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneLoadHandleNative",
-        func = "switchToSceneAsync"
-    ),
-    c
-)]
-fn switch_to_scene_async(
-    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
-    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
-    scene_id: u64,
-) -> DropbearNativeResult<()> {
-    Ok(shared::switch_to_scene_async(
-        command_buffer,
-        scene_loader,
-        scene_id,
-    )?)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneLoadHandleNative",
-        func = "getSceneLoadProgress"
-    ),
-    c
-)]
-fn get_scene_load_progress(
-    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
-    scene_id: u64,
-) -> DropbearNativeResult<Progress> {
-    Ok(shared::get_scene_load_progress(scene_loader, scene_id)?)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.scene.SceneLoadHandleNative",
-        func = "getSceneLoadStatus"
-    ),
-    c
-)]
-fn get_scene_load_status(
-    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
-    scene_id: u64,
-) -> DropbearNativeResult<u32> {
-    Ok(shared::get_scene_load_status(scene_loader, scene_id)?)
-}
diff --git a/crates/eucalyptus-core/src/scripting/jni.rs b/crates/eucalyptus-core/src/scripting/jni.rs
index 884785c..bfb91f9 100644
--- a/crates/eucalyptus-core/src/scripting/jni.rs
+++ b/crates/eucalyptus-core/src/scripting/jni.rs
@@ -1,7 +1,6 @@
 #![allow(non_snake_case)]
 //! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate
 
-pub mod primitives;
 pub mod utils;
 
 use crate::APP_INFO;
diff --git a/crates/eucalyptus-core/src/scripting/jni/primitives.rs b/crates/eucalyptus-core/src/scripting/jni/primitives.rs
deleted file mode 100644
index a7a9a47..0000000
--- a/crates/eucalyptus-core/src/scripting/jni/primitives.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use crate::scripting::jni::utils::{FromJObject, ToJObject};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use jni::objects::{JObject, JValue};
-use jni::sys::{jdouble, jint, jlong};
-use jni::{Env, jni_sig, jni_str};
-
-impl ToJObject for Option<i32> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        match self {
-            Some(value) => {
-                let class = env
-                    .load_class(jni_str!("java.lang.Integer"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                env.new_object(&class, jni_sig!((int) -> void), &[JValue::Int(*value)])
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-            }
-            None => Ok(JObject::null()),
-        }
-    }
-}
-
-impl FromJObject for Option<i32> {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        if obj.is_null() {
-            return Ok(None);
-        }
-
-        let class = env
-            .load_class(jni_str!("java.lang.Integer"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let value = env
-            .call_method(obj, jni_str!("intValue"), jni_sig!(() -> i32), &[])
-            .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(Some(value as i32))
-    }
-}
-
-impl ToJObject for Vec<i32> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        self.as_slice().to_jobject(env)
-    }
-}
-
-impl ToJObject for &[i32] {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let array = env
-            .new_int_array(self.len())
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let buf: Vec<jint> = self.iter().map(|v| *v as jint).collect();
-        array
-            .set_region(env, 0, &buf)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        Ok(JObject::from(array))
-    }
-}
-
-impl ToJObject for &[Vec<i32>] {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let list = new_array_list(env)?;
-        for value in self.iter() {
-            let boxed = value.as_slice().to_jobject(env)?;
-            array_list_add(env, &list, &boxed)?;
-        }
-        Ok(list)
-    }
-}
-
-impl ToJObject for Option<f32> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        match self {
-            Some(value) => {
-                let class = env
-                    .load_class(jni_str!("java.lang.Float"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                env.new_object(&class, jni_sig!((f32) -> ()), &[JValue::Float(*value)])
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-            }
-            None => Ok(JObject::null()),
-        }
-    }
-}
-
-impl ToJObject for Option<f64> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        match self {
-            Some(value) => {
-                let class = env
-                    .load_class(jni_str!("java.lang.Double"))
-                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-                env.new_object(&class, jni_sig!((f64) -> ()), &[JValue::Double(*value)])
-                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-            }
-            None => Ok(JObject::null()),
-        }
-    }
-}
-
-impl ToJObject for Vec<f64> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        self.as_slice().to_jobject(env)
-    }
-}
-
-impl ToJObject for &[f64] {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let array = env
-            .new_double_array(self.len())
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        let buf: Vec<jdouble> = self.iter().map(|v| *v as jdouble).collect();
-        array
-            .set_region(env, 0, &buf)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-        Ok(JObject::from(array))
-    }
-}
-
-impl ToJObject for &[Vec<f64>] {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let list = new_array_list(env)?;
-        for value in self.iter() {
-            let array = value.as_slice().to_jobject(env)?;
-            array_list_add(env, &list, &array)?;
-        }
-        Ok(list)
-    }
-}
-
-fn new_array_list<'a>(env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-    let class = env
-        .load_class(jni_str!("java.util.ArrayList"))
-        .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-    env.new_object(&class, jni_sig!(() -> ()), &[])
-        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-}
-
-fn array_list_add(env: &mut Env, list: &JObject, item: &JObject) -> DropbearNativeResult<()> {
-    env.call_method(
-        list,
-        jni_str!("add"),
-        jni_sig!((java.lang.Object) -> boolean),
-        &[JValue::Object(item)],
-    )
-    .map_err(|_| DropbearNativeError::JNIMethodNotFound)?;
-    Ok(())
-}
-
-impl ToJObject for Vec<u64> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        self.as_slice().to_jobject(env)
-    }
-}
-
-impl ToJObject for &[u64] {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let array = env.new_long_array(self.len())?;
-        let buf: Vec<jlong> = self.iter().map(|v| *v as jlong).collect();
-        array.set_region(env, 0, &buf)?;
-        Ok(JObject::from(array))
-    }
-}
-
-impl ToJObject for String {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let result = JObject::from(env.new_string(self)?);
-        Ok(result)
-    }
-}
diff --git a/crates/eucalyptus-core/src/scripting/jni/utils.rs b/crates/eucalyptus-core/src/scripting/jni/utils.rs
index d49a8d9..77e4f76 100644
--- a/crates/eucalyptus-core/src/scripting/jni/utils.rs
+++ b/crates/eucalyptus-core/src/scripting/jni/utils.rs
@@ -1,16 +1,32 @@
 //! Utilities for JNI and JVM based code.
 
+use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
+use crate::types::{CollisionEvent, CollisionEventType, ContactForceEvent, IndexNative, NCollider, NVector3};
 use jni::objects::{JObject, JValue};
 use jni::sys::jint;
 use jni::{Env, jni_sig, jni_str};
 
+// todo: idk what to do about this module, considering about removing it
+
 const JAVA_MOUSE_BUTTON_LEFT: jint = 0;
 const JAVA_MOUSE_BUTTON_RIGHT: jint = 1;
 const JAVA_MOUSE_BUTTON_MIDDLE: jint = 2;
 const JAVA_MOUSE_BUTTON_BACK: jint = 3;
 const JAVA_MOUSE_BUTTON_FORWARD: jint = 4;
 
+/// Trait that defines conversion from a Java object to a Rust struct.
+pub trait FromJObject {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized;
+}
+
+/// Converts a Rust object into a Java [JObject].
+pub trait ToJObject {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>>;
+}
+
 pub fn Java_button_to_rust(button_code: jint) -> Option<winit::event::MouseButton> {
     match button_code {
         JAVA_MOUSE_BUTTON_LEFT => Some(winit::event::MouseButton::Left),
@@ -23,19 +39,6 @@ pub fn Java_button_to_rust(button_code: jint) -> Option<winit::event::MouseButto
     }
 }
 
-/// Trait that defines conversion from a Java object to a Rust struct.
-pub trait FromJObject {
-    /// Converts a Java object to a Rust struct.
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized;
-}
-
-/// Converts a Rust object (struct or enum) into a java [JObject]
-pub trait ToJObject {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>>;
-}
-
 impl<T> ToJObject for Vec<T>
 where
     T: ToJObject,
@@ -87,3 +90,160 @@ where
         Ok(out)
     }
 }
+
+// ─────────────────────────────────────────────────── Event ToJObject impls ──
+// These are needed by scripting/jni.rs to dispatch physics events to the JVM.
+
+impl ToJObject for NVector3 {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Vector3d"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let args = [
+            JValue::Double(self.x),
+            JValue::Double(self.y),
+            JValue::Double(self.z),
+        ];
+
+        env.new_object(&class, jni_sig!((double, double, double) -> void), &args)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+impl ToJObject for IndexNative {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let cls = env
+            .load_class(jni_str!("com/dropbear/physics/Index"))
+            .map_err(|_| DropbearNativeError::GenericError)?;
+
+        env.new_object(
+            cls,
+            jni_sig!((int, int) -> void),
+            &[
+                JValue::Int(self.index as i32),
+                JValue::Int(self.generation as i32),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::GenericError)
+    }
+}
+
+impl ToJObject for NCollider {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let collider_cls = env
+            .load_class(jni_str!("com/dropbear/physics/Collider"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let index_cls = env
+            .load_class(jni_str!("com/dropbear/physics/Index"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_cls = env
+            .load_class(jni_str!("com/dropbear/EntityId"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_obj = env
+            .new_object(&entity_cls, jni_sig!((long) -> void), &[JValue::Long(self.entity_id as i64)])
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        let index_obj = env
+            .new_object(
+                &index_cls,
+                jni_sig!((int, int) -> void),
+                &[
+                    JValue::Int(self.index.index as i32),
+                    JValue::Int(self.index.generation as i32),
+                ],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        env.new_object(
+            collider_cls,
+            jni_sig!((com.dropbear.physics.Index, com.dropbear.EntityId, int) -> void),
+            &[
+                JValue::Object(&index_obj),
+                JValue::Object(&entity_obj),
+                JValue::Int(self.id as i32),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+impl ToJObject for CollisionEventType {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/CollisionEventType"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = match self {
+            CollisionEventType::Started => "Started",
+            CollisionEventType::Stopped => "Stopped",
+        };
+        let name_jstring = env
+            .new_string(name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        env.call_static_method(
+            class,
+            jni_str!("valueOf"),
+            jni_sig!((java.lang.String) -> com.dropbear.physics.CollisionEventType),
+            &[JValue::from(&name_jstring)],
+        )
+        .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
+        .l()
+        .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
+    }
+}
+
+impl ToJObject for CollisionEvent {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/CollisionEvent"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let event_type = self.event_type.to_jobject(env)?;
+        let collider1 = self.collider1.to_jobject(env)?;
+        let collider2 = self.collider2.to_jobject(env)?;
+
+        env.new_object(
+            class,
+            jni_sig!("(Lcom/dropbear/physics/CollisionEventType;Lcom/dropbear/physics/Collider;Lcom/dropbear/physics/Collider;I)V"),
+            &[
+                JValue::Object(&event_type),
+                JValue::Object(&collider1),
+                JValue::Object(&collider2),
+                JValue::Int(self.flags as i32),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+impl ToJObject for ContactForceEvent {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/ContactForceEvent"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let collider1 = self.collider1.to_jobject(env)?;
+        let collider2 = self.collider2.to_jobject(env)?;
+        let total_force = self.total_force.to_jobject(env)?;
+        let max_force_direction = self.max_force_direction.to_jobject(env)?;
+
+        env.new_object(
+            class,
+            jni_sig!("(Lcom/dropbear/physics/Collider;Lcom/dropbear/physics/Collider;Lcom/dropbear/math/Vector3d;DLcom/dropbear/math/Vector3d;D)V"),
+            &[
+                JValue::Object(&collider1),
+                JValue::Object(&collider2),
+                JValue::Object(&total_force),
+                JValue::Double(self.total_force_magnitude),
+                JValue::Object(&max_force_direction),
+                JValue::Double(self.max_force_magnitude),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/scripting/native.rs b/crates/eucalyptus-core/src/scripting/native.rs
index dd14adb..d50e885 100644
--- a/crates/eucalyptus-core/src/scripting/native.rs
+++ b/crates/eucalyptus-core/src/scripting/native.rs
@@ -1,9 +1,7 @@
 //! Deals with Kotlin/Native library loading for different platforms.
 #![allow(clippy::missing_safety_doc)]
 
-// pub mod exports;
 pub mod sig;
-pub mod utils;
 
 use crate::scripting::error::LastErrorMessage;
 use crate::scripting::native::sig::{
diff --git a/crates/eucalyptus-core/src/scripting/native/utils.rs b/crates/eucalyptus-core/src/scripting/native/utils.rs
deleted file mode 100644
index dfe2889..0000000
--- a/crates/eucalyptus-core/src/scripting/native/utils.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use dropbear_engine::gilrs;
-
-pub fn button_from_ordinal(ordinal: i32) -> Result<gilrs::Button, ()> {
-    match ordinal {
-        0 => Ok(gilrs::Button::Unknown),
-        1 => Ok(gilrs::Button::South),
-        2 => Ok(gilrs::Button::East),
-        3 => Ok(gilrs::Button::North),
-        4 => Ok(gilrs::Button::West),
-        5 => Ok(gilrs::Button::C),
-        6 => Ok(gilrs::Button::Z),
-        7 => Ok(gilrs::Button::LeftTrigger),
-        8 => Ok(gilrs::Button::RightTrigger),
-        9 => Ok(gilrs::Button::LeftTrigger2),
-        10 => Ok(gilrs::Button::RightTrigger2),
-        11 => Ok(gilrs::Button::Select),
-        12 => Ok(gilrs::Button::Start),
-        13 => Ok(gilrs::Button::Mode),
-        14 => Ok(gilrs::Button::LeftThumb),
-        15 => Ok(gilrs::Button::RightThumb),
-        16 => Ok(gilrs::Button::DPadUp),
-        17 => Ok(gilrs::Button::DPadDown),
-        18 => Ok(gilrs::Button::DPadLeft),
-        19 => Ok(gilrs::Button::DPadRight),
-        _ => Err(()),
-    }
-}
diff --git a/crates/eucalyptus-core/src/scripting/types.rs b/crates/eucalyptus-core/src/scripting/types.rs
index 4686ac2..12994bf 100644
--- a/crates/eucalyptus-core/src/scripting/types.rs
+++ b/crates/eucalyptus-core/src/scripting/types.rs
@@ -3,7 +3,6 @@ use crate::component::{
     SerializedComponent,
 };
 use crate::physics::PhysicsState;
-use crate::scripting::result::DropbearNativeResult;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::{CollapsingHeader, Ui};
 use hecs::{Entity, World};
@@ -100,56 +99,3 @@ impl InspectableComponent for KotlinComponents {
             });
     }
 }
-
-/// Queries whether an entity has a specific Kotlin component attached.
-///
-/// Called from JVM via `ComponentNative.hasKotlinComponent(worldPtr, entityId, fqcn)`.
-#[dropbear_macro::export(kotlin(
-    class = "com.dropbear.components.ComponentNative",
-    func = "hasKotlinComponent",
-))]
-fn has_kotlin_component(
-    #[dropbear_macro::define(WorldPtr)] world: &World,
-    #[dropbear_macro::entity] entity: Entity,
-    fqcn: String,
-) -> DropbearNativeResult<bool> {
-    if let Ok(kc) = world.get::<&KotlinComponents>(entity) {
-        Ok(kc.has(&fqcn))
-    } else {
-        Ok(false)
-    }
-}
-
-/// Registers a Kotlin component type with the engine's [`crate::component::ComponentRegistry`].
-///
-/// Called from JVM during startup via `ComponentNative.registerKotlinComponent(...)`.
-/// The declaration is queued in [`crate::component::KOTLIN_COMPONENT_QUEUE`] and consumed
-/// by [`crate::component::ComponentRegistry::drain_kotlin_queue`] after JVM init completes.
-#[dropbear_macro::export(kotlin(
-    class = "com.dropbear.components.ComponentNative",
-    func = "registerKotlinComponent",
-))]
-fn register_kotlin_component_jni(
-    #[dropbear_macro::define(WorldPtr)] _world: &World,
-    fqcn: String,
-    type_name: String,
-    category: String,
-    description: String,
-) -> DropbearNativeResult<()> {
-    use crate::component::{KOTLIN_COMPONENT_QUEUE, KotlinComponentDecl};
-    KOTLIN_COMPONENT_QUEUE.lock().push(KotlinComponentDecl {
-        fqcn,
-        type_name,
-        category: if category.is_empty() {
-            None
-        } else {
-            Some(category)
-        },
-        description: if description.is_empty() {
-            None
-        } else {
-            Some(description)
-        },
-    });
-    Ok(())
-}
diff --git a/crates/eucalyptus-core/src/transform.rs b/crates/eucalyptus-core/src/transform.rs
index d69ecdc..3a7a76f 100644
--- a/crates/eucalyptus-core/src/transform.rs
+++ b/crates/eucalyptus-core/src/transform.rs
@@ -3,17 +3,9 @@ use crate::component::{
     Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent,
     SerializedComponent,
 };
-use crate::hierarchy::EntityTransformExt;
 use crate::physics::PhysicsState;
-use crate::ptr::WorldPtr;
-use crate::scripting::jni::utils::{FromJObject, ToJObject};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
-use crate::types::{NQuaternion, NTransform, NVector3};
-use ::jni::objects::{JObject, JValue};
-use ::jni::{Env, jni_sig, jni_str};
 use dropbear_engine::camera::Camera;
-use dropbear_engine::entity::{EntityTransform, Transform, inspect_rotation_dquat};
+use dropbear_engine::entity::{EntityTransform, inspect_rotation_dquat};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::{CollapsingHeader, ComboBox, Ui};
 use glam::{DMat3, DQuat, DVec3, Vec3};
@@ -668,781 +660,3 @@ impl InspectableComponent for EntityTransform {
     }
 }
 
-impl FromJObject for Transform {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        let pos_val = env
-            .get_field(
-                obj,
-                jni_str!("position"),
-                jni_sig!(com.dropbear.math.Vector3d),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let pos_obj = pos_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let rot_val = env
-            .get_field(
-                obj,
-                jni_str!("rotation"),
-                jni_sig!(com.dropbear.math.Quaterniond),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let rot_obj = rot_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let scale_val = env
-            .get_field(obj, jni_str!("scale"), jni_sig!(com.dropbear.math.Vector3d))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let scale_obj = scale_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let position: DVec3 = NVector3::from_jobject(env, &pos_obj)?.into();
-        let scale: DVec3 = NVector3::from_jobject(env, &scale_obj)?.into();
-
-        let mut get_double = |field| -> DropbearNativeResult<f64> {
-            env.get_field(&rot_obj, field, jni_sig!(double))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
-        };
-
-        let rx = get_double(jni_str!("x"))?;
-        let ry = get_double(jni_str!("y"))?;
-        let rz = get_double(jni_str!("z"))?;
-        let rw = get_double(jni_str!("w"))?;
-
-        let rotation = DQuat::from_xyzw(rx, ry, rz, rw);
-
-        Ok(Transform {
-            position,
-            rotation,
-            scale,
-        })
-    }
-}
-
-impl ToJObject for Transform {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let cls = env
-            .load_class(jni_str!("com/dropbear/math/Transform"))
-            .map_err(|e| {
-                eprintln!("Could not find Transform class: {:?}", e);
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let p = self.position;
-        let r = self.rotation;
-        let s = self.scale;
-
-        let args = [
-            JValue::Double(p.x),
-            JValue::Double(p.y),
-            JValue::Double(p.z),
-            JValue::Double(r.x),
-            JValue::Double(r.y),
-            JValue::Double(r.z),
-            JValue::Double(r.w),
-            JValue::Double(s.x),
-            JValue::Double(s.y),
-            JValue::Double(s.z),
-        ];
-
-        let obj = env.new_object(cls, jni_sig!((double, double, double, double, double, double, double, double, double, double) -> void), &args).map_err(|e| {
-            eprintln!("Failed to create Transform object: {:?}", e);
-            DropbearNativeError::JNIFailedToCreateObject
-        })?;
-
-        Ok(obj)
-    }
-}
-
-impl FromJObject for EntityTransform {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        let local_val = env
-            .get_field(
-                obj,
-                jni_str!("local"),
-                jni_sig!(com.dropbear.math.Transform),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let local_obj = local_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let world_val = env
-            .get_field(
-                obj,
-                jni_str!("world"),
-                jni_sig!(com.dropbear.math.Transform),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let world_obj = world_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let local = Transform::from_jobject(env, &local_obj)?;
-        let world = Transform::from_jobject(env, &world_obj)?;
-
-        Ok(EntityTransform::new(local, world))
-    }
-}
-
-impl ToJObject for EntityTransform {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let cls = env
-            .load_class(jni_str!("com/dropbear/components/EntityTransform"))
-            .map_err(|e| {
-                eprintln!("Could not find EntityTransform class: {:?}", e);
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let local_obj = self.local().to_jobject(env)?;
-        let world_obj = self.world().to_jobject(env)?;
-
-        let args = [JValue::Object(&local_obj), JValue::Object(&world_obj)];
-
-        let obj = env
-            .new_object(
-                cls,
-                jni_sig!((com.dropbear.math.Transform, com.dropbear.math.Transform) -> void),
-                &args,
-            )
-            .map_err(|e| {
-                eprintln!("Failed to create EntityTransform object: {:?}", e);
-                DropbearNativeError::JNIFailedToCreateObject
-            })?;
-
-        Ok(obj)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.EntityTransformNative",
-        func = "entityTransformExistsForEntity"
-    ),
-    c
-)]
-fn exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(world.get::<&EntityTransform>(entity).is_ok())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.EntityTransformNative",
-        func = "getLocalTransform"
-    ),
-    c
-)]
-fn get_local_transform(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NTransform> {
-    if let Ok(et) = world.get::<&EntityTransform>(entity) {
-        Ok((*et.local()).into())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.EntityTransformNative",
-        func = "setLocalTransform"
-    ),
-    c
-)]
-fn set_local_transform(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    transform: &NTransform,
-) -> DropbearNativeResult<()> {
-    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-        *et.local_mut() = (*transform).into();
-
-        Ok(())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.EntityTransformNative",
-        func = "getWorldTransform"
-    ),
-    c
-)]
-fn get_world_transform(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NTransform> {
-    if let Ok(et) = world.get::<&EntityTransform>(entity) {
-        Ok((*et.world()).into())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.EntityTransformNative",
-        func = "setWorldTransform"
-    ),
-    c
-)]
-fn set_world_transform(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    transform: &NTransform,
-) -> DropbearNativeResult<()> {
-    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-        *et.world_mut() = (*transform).into();
-
-        Ok(())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.EntityTransformNative",
-        func = "propogateTransform"
-    ),
-    c
-)]
-fn propagate_transform(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NTransform> {
-    if let Ok(et) = world.get::<&mut EntityTransform>(entity) {
-        let result = et.propagate(world, entity);
-        Ok(result.into())
-    } else {
-        Err(DropbearNativeError::MissingComponent)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getEnabled"
-    ),
-    c
-)]
-fn on_rails_get_enabled(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(rails.enabled)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "setEnabled"
-    ),
-    c
-)]
-fn on_rails_set_enabled(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    enabled: bool,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.enabled = enabled;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "existsForEntity"
-    ),
-    c
-)]
-fn on_rails_exists_for_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    Ok(world.get::<&OnRails>(entity).is_ok())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getProgress"
-    ),
-    c
-)]
-fn on_rails_get_progress(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f32> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(rails.progress)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "setProgress"
-    ),
-    c
-)]
-fn on_rails_set_progress(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    progress: f32,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.progress = progress.clamp(0.0, 1.0);
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getPathLen"
-    ),
-    c
-)]
-fn on_rails_get_path_len(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<i32> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    Ok(rails.path.len() as i32)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getPathPoint"
-    ),
-    c
-)]
-fn on_rails_get_path_point(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    index: i32,
-) -> DropbearNativeResult<NVector3> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    let point = rails
-        .path
-        .get(index as usize)
-        .ok_or(DropbearNativeError::InvalidArgument)?;
-    Ok(NVector3::from(&point.position))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "clearPath"
-    ),
-    c
-)]
-fn on_rails_clear_path(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.path.clear();
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "pushPathPoint"
-    ),
-    c
-)]
-fn on_rails_push_path_point(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    point: &NVector3,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.path.push(RailPoint { position: DVec3::from(point), rotation: None });
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getPathPointHasRotation"
-    ),
-    c
-)]
-fn on_rails_get_path_point_has_rotation(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    index: i32,
-) -> DropbearNativeResult<bool> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    let point = rails
-        .path
-        .get(index as usize)
-        .ok_or(DropbearNativeError::InvalidArgument)?;
-    Ok(point.rotation.is_some())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getPathPointRotation"
-    ),
-    c
-)]
-fn on_rails_get_path_point_rotation(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    index: i32,
-) -> DropbearNativeResult<NQuaternion> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    let point = rails
-        .path
-        .get(index as usize)
-        .ok_or(DropbearNativeError::InvalidArgument)?;
-    Ok(NQuaternion::from(point.rotation.unwrap_or(DQuat::IDENTITY)))
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "pushPathPointWithRotation"
-    ),
-    c
-)]
-fn on_rails_push_path_point_with_rotation(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    point: &NVector3,
-    rotation: &NQuaternion,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.path.push(RailPoint {
-        position: DVec3::from(point),
-        rotation: Some(DQuat::from(*rotation)),
-    });
-    Ok(())
-}
-
-// `0` = Automatic, `1` = FollowEntity, `2` = AxisDriven, `3` = Manual.
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveType"
-    ),
-    c
-)]
-fn on_rails_get_drive_type(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<i32> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    let tag = match &rails.drive {
-        RailDrive::Automatic { .. } => 0,
-        RailDrive::FollowEntity { .. } => 1,
-        RailDrive::AxisDriven { .. } => 2,
-        RailDrive::Manual => 3,
-    };
-    Ok(tag)
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "setDriveAutomatic"
-    ),
-    c
-)]
-fn on_rails_set_drive_automatic(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    speed: f32,
-    looping: bool,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.drive = RailDrive::Automatic { speed, looping };
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "setDriveFollowEntity"
-    ),
-    c
-)]
-fn on_rails_set_drive_follow_entity(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    target: u64,
-    monotonic: bool,
-) -> DropbearNativeResult<()> {
-    let target_entity =
-        hecs::Entity::from_bits(target).ok_or(DropbearNativeError::InvalidEntity)?;
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.drive = RailDrive::FollowEntity {
-        target: target_entity,
-        monotonic,
-    };
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "setDriveAxisDriven"
-    ),
-    c
-)]
-fn on_rails_set_drive_axis_driven(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-    target: u64,
-    axis: &NVector3,
-    range_min: f32,
-    range_max: f32,
-) -> DropbearNativeResult<()> {
-    let target_entity =
-        hecs::Entity::from_bits(target).ok_or(DropbearNativeError::InvalidEntity)?;
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.drive = RailDrive::AxisDriven {
-        target: target_entity,
-        axis: Vec3::new(axis.x as f32, axis.y as f32, axis.z as f32),
-        range: (range_min, range_max),
-    };
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "setDriveManual"
-    ),
-    c
-)]
-fn on_rails_set_drive_manual(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<()> {
-    let mut rails = world
-        .get::<&mut OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    rails.drive = RailDrive::Manual;
-    Ok(())
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveAutomaticSpeed"
-    ),
-    c
-)]
-fn on_rails_get_drive_automatic_speed(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f32> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::Automatic { speed, .. } = &rails.drive {
-        Ok(*speed)
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveAutomaticLooping"
-    ),
-    c
-)]
-fn on_rails_get_drive_automatic_looping(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::Automatic { looping, .. } = &rails.drive {
-        Ok(*looping)
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveFollowEntityTarget"
-    ),
-    c
-)]
-fn on_rails_get_drive_follow_entity_target(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<u64> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::FollowEntity { target, .. } = &rails.drive {
-        Ok(target.to_bits().get())
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveFollowEntityMonotonic"
-    ),
-    c
-)]
-fn on_rails_get_drive_follow_entity_monotonic(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<bool> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::FollowEntity { monotonic, .. } = &rails.drive {
-        Ok(*monotonic)
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveAxisDrivenTarget"
-    ),
-    c
-)]
-fn on_rails_get_drive_axis_driven_target(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<u64> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::AxisDriven { target, .. } = &rails.drive {
-        Ok(target.to_bits().get())
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveAxisDrivenAxis"
-    ),
-    c
-)]
-fn on_rails_get_drive_axis_driven_axis(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<NVector3> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::AxisDriven { axis, .. } = &rails.drive {
-        Ok(NVector3::from(*axis))
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveAxisDrivenRangeMin"
-    ),
-    c
-)]
-fn on_rails_get_drive_axis_driven_range_min(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f32> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::AxisDriven { range, .. } = &rails.drive {
-        Ok(range.0)
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
-
-#[dropbear_macro::export(
-    kotlin(
-        class = "com.dropbear.components.camera.OnRailsNative",
-        func = "getDriveAxisDrivenRangeMax"
-    ),
-    c
-)]
-fn on_rails_get_drive_axis_driven_range_max(
-    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropbear_macro::entity] entity: hecs::Entity,
-) -> DropbearNativeResult<f32> {
-    let rails = world
-        .get::<&OnRails>(entity)
-        .map_err(|_| DropbearNativeError::MissingComponent)?;
-    if let RailDrive::AxisDriven { range, .. } = &rails.drive {
-        Ok(range.1)
-    } else {
-        Err(DropbearNativeError::InvalidArgument)
-    }
-}
diff --git a/crates/eucalyptus-core/src/types.rs b/crates/eucalyptus-core/src/types.rs
index f739920..3b5da9a 100644
--- a/crates/eucalyptus-core/src/types.rs
+++ b/crates/eucalyptus-core/src/types.rs
@@ -1,37 +1,11 @@
-//! FFI and C types of other library types, as used in the scripting module.
 use crate::physics::PhysicsState;
-use crate::scripting::jni::utils::{FromJObject, ToJObject};
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
 use dropbear_engine::entity::Transform;
 use glam::{DQuat, DVec3, Vec3};
-use hecs::Entity;
-use jni::objects::{JObject, JValue};
-use jni::sys::jdouble;
-use jni::{Env, jni_sig, jni_str};
 use rapier3d::data::Index;
-use rapier3d::parry::query::ShapeCastStatus;
-use rapier3d::prelude::ColliderHandle;
+use rapier3d::geometry::ColliderHandle;
+use serde::{Deserialize, Serialize};
 
-#[repr(C)]
-#[derive(Clone, Copy, Debug)]
-pub struct NColour {
-    pub r: u8,
-    pub g: u8,
-    pub b: u8,
-    pub a: u8,
-}
-
-impl NColour {
-    pub fn to_f32_array(self) -> [f32; 4] {
-        [
-            self.r as f32 / 255.0,
-            self.g as f32 / 255.0,
-            self.b as f32 / 255.0,
-            self.a as f32 / 255.0,
-        ]
-    }
-}
+// --------------------------------------------------------------- math types --
 
 #[repr(C)]
 #[derive(Clone, Copy, Debug)]
@@ -51,42 +25,34 @@ impl From<glam::DVec2> for NVector2 {
         Self { x: v.x, y: v.y }
     }
 }
-
 impl From<[f64; 2]> for NVector2 {
-    fn from(value: [f64; 2]) -> Self {
-        NVector2 {
-            x: value[0],
-            y: value[1],
-        }
+    fn from(v: [f64; 2]) -> Self {
+        Self { x: v[0], y: v[1] }
     }
 }
-
 impl From<[f32; 2]> for NVector2 {
-    fn from(value: [f32; 2]) -> Self {
-        NVector2 {
-            x: value[0] as f64,
-            y: value[1] as f64,
+    fn from(v: [f32; 2]) -> Self {
+        Self {
+            x: v[0] as f64,
+            y: v[1] as f64,
         }
     }
 }
-
+impl From<(f64, f64)> for NVector2 {
+    fn from(v: (f64, f64)) -> Self {
+        Self { x: v.0, y: v.1 }
+    }
+}
 impl From<NVector2> for glam::DVec2 {
     fn from(v: NVector2) -> Self {
         Self::new(v.x, v.y)
     }
 }
 
-impl From<(f64, f64)> for NVector2 {
-    fn from(value: (f64, f64)) -> Self {
-        Self {
-            x: value.0,
-            y: value.1,
-        }
-    }
-}
+// -----------------------------------------------------------------------------
 
 #[repr(C)]
-#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
 pub struct NVector3 {
     pub x: f64,
     pub y: f64,
@@ -94,10 +60,9 @@ pub struct NVector3 {
 }
 
 impl NVector3 {
-    pub fn new(x: f64, y: f64, z: f64) -> NVector3 {
-        NVector3 { x, y, z }
+    pub fn new(x: f64, y: f64, z: f64) -> Self {
+        Self { x, y, z }
     }
-
     pub fn to_array(&self) -> [f64; 3] {
         [self.x, self.y, self.z]
     }
@@ -106,8 +71,8 @@ impl NVector3 {
     }
 }
 
-impl From<glam::DVec3> for NVector3 {
-    fn from(v: glam::DVec3) -> Self {
+impl From<DVec3> for NVector3 {
+    fn from(v: DVec3) -> Self {
         Self {
             x: v.x,
             y: v.y,
@@ -115,9 +80,8 @@ impl From<glam::DVec3> for NVector3 {
         }
     }
 }
-
-impl From<&glam::DVec3> for NVector3 {
-    fn from(v: &glam::DVec3) -> Self {
+impl From<&DVec3> for NVector3 {
+    fn from(v: &DVec3) -> Self {
         Self {
             x: v.x,
             y: v.y,
@@ -125,8 +89,7 @@ impl From<&glam::DVec3> for NVector3 {
         }
     }
 }
-
-impl From<&NVector3> for glam::DVec3 {
+impl From<&NVector3> for DVec3 {
     fn from(v: &NVector3) -> Self {
         Self {
             x: v.x,
@@ -135,101 +98,46 @@ impl From<&NVector3> for glam::DVec3 {
         }
     }
 }
-
-impl From<[f64; 3]> for NVector3 {
-    fn from(value: [f64; 3]) -> Self {
-        NVector3 {
-            x: value[0],
-            y: value[1],
-            z: value[2],
-        }
-    }
-}
-
-impl From<[f32; 3]> for NVector3 {
-    fn from(value: [f32; 3]) -> Self {
-        NVector3 {
-            x: value[0] as f64,
-            y: value[1] as f64,
-            z: value[2] as f64,
-        }
+impl From<NVector3> for DVec3 {
+    fn from(v: NVector3) -> Self {
+        Self::new(v.x, v.y, v.z)
     }
 }
-
-impl From<glam::Vec3> for NVector3 {
-    fn from(value: Vec3) -> Self {
+impl From<Vec3> for NVector3 {
+    fn from(v: Vec3) -> Self {
         Self {
-            x: value.x as f64,
-            y: value.y as f64,
-            z: value.z as f64,
+            x: v.x as f64,
+            y: v.y as f64,
+            z: v.z as f64,
         }
     }
 }
-
-impl From<NVector3> for glam::DVec3 {
+impl From<NVector3> for Vec3 {
     fn from(v: NVector3) -> Self {
-        Self::new(v.x, v.y, v.z)
+        Self::new(v.x as f32, v.y as f32, v.z as f32)
     }
 }
-
-impl FromJObject for NVector3 {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Vector3d"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
+impl From<[f64; 3]> for NVector3 {
+    fn from(v: [f64; 3]) -> Self {
+        Self {
+            x: v[0],
+            y: v[1],
+            z: v[2],
         }
-
-        let x = env
-            .get_field(obj, jni_str!("x"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let y = env
-            .get_field(obj, jni_str!("y"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let z = env
-            .get_field(obj, jni_str!("z"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(NVector3::new(x, y, z))
     }
 }
-
-impl ToJObject for NVector3 {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Vector3d"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            jni::objects::JValue::Double(self.x),
-            jni::objects::JValue::Double(self.y),
-            jni::objects::JValue::Double(self.z),
-        ];
-
-        let obj = env
-            .new_object(&class, jni_sig!((double, double, double) -> void), &args)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
+impl From<[f32; 3]> for NVector3 {
+    fn from(v: [f32; 3]) -> Self {
+        Self {
+            x: v[0] as f64,
+            y: v[1] as f64,
+            z: v[2] as f64,
+        }
     }
 }
 
+// -----------------------------------------------------------------------------
+
 #[repr(C)]
 #[derive(Clone, Copy, Debug)]
 pub struct NVector4 {
@@ -240,15 +148,11 @@ pub struct NVector4 {
 }
 
 impl NVector4 {
-    pub fn new(x: f64, y: f64, z: f64, w: f64) -> NVector4 {
-        NVector4 { x, y, z, w }
-    }
-
-    pub fn to_array(&self) -> [f64; 3] {
-        [self.x, self.y, self.z]
+    pub fn new(x: f64, y: f64, z: f64, w: f64) -> Self {
+        Self { x, y, z, w }
     }
-    pub fn to_float_array(&self) -> [f32; 3] {
-        [self.x as f32, self.y as f32, self.z as f32]
+    pub fn to_array(&self) -> [f64; 4] {
+        [self.x, self.y, self.z, self.w]
     }
 }
 
@@ -262,115 +166,44 @@ impl From<glam::DVec4> for NVector4 {
         }
     }
 }
-
-impl From<[f64; 4]> for NVector4 {
-    fn from(value: [f64; 4]) -> Self {
-        NVector4 {
-            x: value[0],
-            y: value[1],
-            z: value[2],
-            w: value[3],
-        }
-    }
-}
-
-impl From<[f32; 4]> for NVector4 {
-    fn from(value: [f32; 4]) -> Self {
-        NVector4 {
-            x: value[0] as f64,
-            y: value[1] as f64,
-            z: value[2] as f64,
-            w: value[3] as f64,
-        }
-    }
-}
-
 impl From<NVector4> for glam::DVec4 {
     fn from(v: NVector4) -> Self {
         Self::new(v.x, v.y, v.z, v.w)
     }
 }
-
-impl FromJObject for NVector4 {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Vector4d"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
+impl From<[f64; 4]> for NVector4 {
+    fn from(v: [f64; 4]) -> Self {
+        Self {
+            x: v[0],
+            y: v[1],
+            z: v[2],
+            w: v[3],
         }
-
-        let x = env
-            .get_field(obj, jni_str!("x"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let y = env
-            .get_field(obj, jni_str!("y"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let z = env
-            .get_field(obj, jni_str!("z"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let w = env
-            .get_field(obj, jni_str!("w"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(NVector4::new(x, y, z, w))
     }
 }
-
-impl ToJObject for NVector4 {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Vector3d"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            jni::objects::JValue::Double(self.x),
-            jni::objects::JValue::Double(self.y),
-            jni::objects::JValue::Double(self.z),
-            jni::objects::JValue::Double(self.w),
-        ];
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!((double, double, double, double) -> void),
-                &args,
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
+impl From<[f32; 4]> for NVector4 {
+    fn from(v: [f32; 4]) -> Self {
+        Self {
+            x: v[0] as f64,
+            y: v[1] as f64,
+            z: v[2] as f64,
+            w: v[3] as f64,
+        }
     }
 }
-
 impl From<NQuaternion> for NVector4 {
-    fn from(value: NQuaternion) -> Self {
+    fn from(q: NQuaternion) -> Self {
         Self {
-            x: value.x,
-            y: value.y,
-            z: value.z,
-            w: value.w,
+            x: q.x,
+            y: q.y,
+            z: q.z,
+            w: q.w,
         }
     }
 }
 
+// -----------------------------------------------------------------------------
+
 #[repr(C)]
 #[derive(Clone, Copy, Debug)]
 pub struct NQuaternion {
@@ -381,125 +214,57 @@ pub struct NQuaternion {
 }
 
 impl From<DQuat> for NQuaternion {
-    fn from(value: DQuat) -> Self {
+    fn from(v: DQuat) -> Self {
         Self {
-            x: value.x,
-            y: value.y,
-            z: value.z,
-            w: value.w,
+            x: v.x,
+            y: v.y,
+            z: v.z,
+            w: v.w,
         }
     }
 }
-
-impl From<NQuaternion> for glam::DQuat {
-    fn from(value: NQuaternion) -> Self {
+impl From<NQuaternion> for DQuat {
+    fn from(v: NQuaternion) -> Self {
         Self {
-            x: value.x,
-            y: value.y,
-            z: value.z,
-            w: value.w,
+            x: v.x,
+            y: v.y,
+            z: v.z,
+            w: v.w,
         }
     }
 }
-
 impl From<[f64; 4]> for NQuaternion {
-    fn from(value: [f64; 4]) -> Self {
+    fn from(v: [f64; 4]) -> Self {
         Self {
-            x: value[0],
-            y: value[1],
-            z: value[2],
-            w: value[3],
+            x: v[0],
+            y: v[1],
+            z: v[2],
+            w: v[3],
         }
     }
 }
-
 impl From<glam::Quat> for NQuaternion {
-    fn from(value: glam::Quat) -> Self {
+    fn from(v: glam::Quat) -> Self {
         Self {
-            x: value.x as f64,
-            y: value.y as f64,
-            z: value.z as f64,
-            w: value.w as f64,
+            x: v.x as f64,
+            y: v.y as f64,
+            z: v.z as f64,
+            w: v.w as f64,
         }
     }
 }
-
 impl From<NVector4> for NQuaternion {
-    fn from(value: NVector4) -> Self {
+    fn from(v: NVector4) -> Self {
         Self {
-            x: value.x,
-            y: value.y,
-            z: value.z,
-            w: value.w,
+            x: v.x,
+            y: v.y,
+            z: v.z,
+            w: v.w,
         }
     }
 }
 
-impl ToJObject for NQuaternion {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Quaterniond"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            JValue::Double(self.x),
-            JValue::Double(self.y),
-            JValue::Double(self.z),
-            JValue::Double(self.w),
-        ];
-
-        env.new_object(
-            &class,
-            jni_sig!((double, double, double, double) -> void),
-            &args,
-        )
-        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-    }
-}
-
-impl FromJObject for NQuaternion {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Quaterniond"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        if !env
-            .is_instance_of(obj, &class)
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
-        {
-            return Err(DropbearNativeError::InvalidArgument);
-        }
-
-        let x = env
-            .get_field(obj, jni_str!("x"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let y = env
-            .get_field(obj, jni_str!("y"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let z = env
-            .get_field(obj, jni_str!("z"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let w = env
-            .get_field(obj, jni_str!("w"), jni_sig!(double))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .d()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(NQuaternion { x, y, z, w })
-    }
-}
+// -----------------------------------------------------------------------------
 
 #[repr(C)]
 #[derive(Clone, Copy)]
@@ -510,496 +275,122 @@ pub struct NTransform {
 }
 
 impl From<Transform> for NTransform {
-    fn from(value: Transform) -> Self {
+    fn from(v: Transform) -> Self {
         Self {
-            position: NVector3::from(value.position),
-            rotation: NQuaternion::from(value.rotation),
-            scale: NVector3::from(value.scale),
+            position: NVector3::from(v.position),
+            rotation: NQuaternion::from(v.rotation),
+            scale: NVector3::from(v.scale),
         }
     }
 }
-
 impl From<NTransform> for Transform {
-    fn from(value: NTransform) -> Self {
+    fn from(v: NTransform) -> Self {
         Self {
-            position: DVec3::from(value.position),
-            rotation: DQuat::from(value.rotation),
-            scale: DVec3::from(value.scale),
+            position: DVec3::from(v.position),
+            rotation: DQuat::from(v.rotation),
+            scale: DVec3::from(v.scale),
         }
     }
 }
 
-impl FromJObject for NTransform {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
-        let pos_val = env
-            .get_field(
-                obj,
-                jni_str!("position"),
-                jni_sig!(com.dropbear.math.Vector3d),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let pos_obj = pos_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let rot_val = env
-            .get_field(
-                obj,
-                jni_str!("rotation"),
-                jni_sig!(com.dropbear.math.Quaterniond),
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let rot_obj = rot_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let scale_val = env
-            .get_field(obj, jni_str!("scale"), jni_sig!(com.dropbear.math.Vector3d))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
-
-        let scale_obj = scale_val
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let position: DVec3 = NVector3::from_jobject(env, &pos_obj)?.into();
-        let scale: DVec3 = NVector3::from_jobject(env, &scale_obj)?.into();
-
-        let mut get_double = |field| -> DropbearNativeResult<f64> {
-            env.get_field(&rot_obj, field, jni_sig!(double))
-                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-                .d()
-                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
-        };
-
-        let rx = get_double(jni_str!("x"))?;
-        let ry = get_double(jni_str!("y"))?;
-        let rz = get_double(jni_str!("z"))?;
-        let rw = get_double(jni_str!("w"))?;
-
-        let rotation = DQuat::from_xyzw(rx, ry, rz, rw);
-
-        Ok(NTransform {
-            position: position.into(),
-            rotation: rotation.into(),
-            scale: scale.into(),
-        })
-    }
-}
-
-impl ToJObject for NTransform {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/math/Transform"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let args = [
-            JValue::Double(self.position.x),
-            JValue::Double(self.position.y),
-            JValue::Double(self.position.z),
-            JValue::Double(self.rotation.x),
-            JValue::Double(self.rotation.y),
-            JValue::Double(self.rotation.z),
-            JValue::Double(self.rotation.w),
-            JValue::Double(self.scale.x),
-            JValue::Double(self.scale.y),
-            JValue::Double(self.scale.z),
-        ];
-
-        env.new_object(&class, jni_sig!((double, double, double, double, double, double, double, double, double, double) -> void), &args)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
-    }
-}
+// ------------------------------------------------------------------- colour --
 
 #[repr(C)]
-#[derive(Clone, Copy)]
-pub struct NCollider {
-    pub index: IndexNative,
-    pub entity_id: u64,
-    pub id: u32,
+#[derive(Clone, Copy, Debug)]
+pub struct NColour {
+    pub r: u8,
+    pub g: u8,
+    pub b: u8,
+    pub a: u8,
 }
 
-impl ToJObject for NCollider {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let collider_cls = env
-            .load_class(jni_str!("com/dropbear/physics/Collider"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let index_cls = env
-            .load_class(jni_str!("com/dropbear/physics/Index"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let entity_cls = env
-            .load_class(jni_str!("com/dropbear/EntityId"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let entity_obj = env
-            .new_object(
-                &entity_cls,
-                jni_sig!((long) -> void),
-                &[JValue::Long(self.entity_id as i64)],
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let index_obj = env
-            .new_object(
-                &index_cls,
-                jni_sig!((int, int) -> void),
-                &[
-                    JValue::Int(self.index.index as i32),
-                    JValue::Int(self.index.generation as i32),
-                ],
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let collider_obj = env
-            .new_object(
-                collider_cls,
-                jni_sig!((com.dropbear.physics.Index, com.dropbear.EntityId, int) -> void),
-                &[
-                    JValue::Object(&index_obj),
-                    JValue::Object(&entity_obj),
-                    JValue::Int(self.id as i32),
-                ],
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(collider_obj)
+impl NColour {
+    pub fn to_f32_array(self) -> [f32; 4] {
+        [
+            self.r as f32 / 255.0,
+            self.g as f32 / 255.0,
+            self.b as f32 / 255.0,
+            self.a as f32 / 255.0,
+        ]
     }
-}
-
-impl FromJObject for NCollider {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let index_obj = env
-            .get_field(obj, jni_str!("index"), jni_sig!(com.dropbear.physics.Index))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let entity_obj = env
-            .get_field(obj, jni_str!("entity"), jni_sig!(com.dropbear.EntityId))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let id_val = env
-            .get_field(obj, jni_str!("id"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
 
-        let entity_raw = env
-            .get_field(&entity_obj, jni_str!("raw"), jni_sig!(long))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .j()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let idx_val = env
-            .get_field(&index_obj, jni_str!("index"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let gen_val = env
-            .get_field(&index_obj, jni_str!("generation"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+    pub fn to_linear_rgb(self) -> DVec3 {
+        DVec3::new(
+            self.r as f64 / 255.0,
+            self.g as f64 / 255.0,
+            self.b as f64 / 255.0,
+        )
+    }
 
-        Ok(NCollider {
-            index: IndexNative {
-                index: idx_val as u32,
-                generation: gen_val as u32,
-            },
-            entity_id: entity_raw as u64,
-            id: id_val as u32,
-        })
+    pub fn from_linear_rgb(rgb: DVec3) -> Self {
+        fn clamp_u8(x: f64) -> u8 {
+            (x * 255.0).round().clamp(0.0, 255.0) as u8
+        }
+        Self {
+            r: clamp_u8(rgb.x),
+            g: clamp_u8(rgb.y),
+            b: clamp_u8(rgb.z),
+            a: 255,
+        }
     }
 }
 
+// ----------------------------------------------------------- physics types ---
+
 #[repr(C)]
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
 pub struct IndexNative {
     pub index: u32,
     pub generation: u32,
 }
 
 impl From<Index> for IndexNative {
-    fn from(value: Index) -> Self {
-        let raw = value.into_raw_parts();
+    fn from(v: Index) -> Self {
+        let raw = v.into_raw_parts();
         Self {
             index: raw.0,
             generation: raw.1,
         }
     }
 }
-
 impl From<IndexNative> for Index {
-    fn from(value: IndexNative) -> Self {
-        Self::from_raw_parts(value.index, value.generation)
-    }
-}
-
-impl ToJObject for IndexNative {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let cls = env
-            .load_class(jni_str!("com/dropbear/physics/Index"))
-            .map_err(|e| {
-                eprintln!("[JNI Error] Could not find Index class: {:?}", e);
-                DropbearNativeError::GenericError
-            })?;
-
-        let obj = env
-            .new_object(
-                cls,
-                jni_sig!((int, int) -> void),
-                &[
-                    JValue::Int(self.index as i32),
-                    JValue::Int(self.generation as i32),
-                ],
-            )
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create Index object: {:?}", e);
-                DropbearNativeError::GenericError
-            })?;
-
-        Ok(obj)
+    fn from(v: IndexNative) -> Self {
+        Self::from_raw_parts(v.index, v.generation)
     }
 }
 
-impl ToJObject for Option<IndexNative> {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        match self {
-            Some(value) => value.to_jobject(env),
-            None => Ok(JObject::null()),
-        }
-    }
-}
-
-impl FromJObject for IndexNative {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let idx_val = env
-            .get_field(obj, jni_str!("index"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let gen_val = env
-            .get_field(obj, jni_str!("generation"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(IndexNative {
-            index: idx_val as u32,
-            generation: gen_val as u32,
-        })
-    }
-}
+// -----------------------------------------------------------------------------
 
 #[repr(C)]
-#[derive(Clone, Copy)]
-pub struct RigidBodyContext {
+#[derive(Clone, Copy, Debug)]
+pub struct NCollider {
     pub index: IndexNative,
     pub entity_id: u64,
+    pub id: u32,
 }
 
-impl FromJObject for RigidBodyContext {
-    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
-    where
-        Self: Sized,
-    {
-        let index_obj = env
-            .get_field(obj, jni_str!("index"), jni_sig!(com.dropbear.physics.Index))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let idx_val = env
-            .get_field(&index_obj, jni_str!("index"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let gen_val = env
-            .get_field(&index_obj, jni_str!("generation"), jni_sig!(int))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .i()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let entity_obj = env
-            .get_field(obj, jni_str!("entity"), jni_sig!(com.dropbear.EntityId))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        let entity_raw = env
-            .get_field(&entity_obj, jni_str!("raw"), jni_sig!(long))
-            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
-            .j()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(RigidBodyContext {
-            index: IndexNative {
-                index: idx_val as u32,
-                generation: gen_val as u32,
-            },
-            entity_id: entity_raw as u64,
-        })
-    }
-}
-
-impl ToJObject for RigidBodyContext {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let index_cls = env
-            .load_class(jni_str!("com/dropbear/physics/Index"))
-            .map_err(|e| {
-                eprintln!(
-                    "[JNI Error] Class 'com/dropbear/physics/Index' not found: {:?}",
-                    e
-                );
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let entity_cls = env
-            .load_class(jni_str!("com/dropbear/EntityId"))
-            .map_err(|e| {
-                eprintln!(
-                    "[JNI Error] Class 'com/dropbear/EntityId' not found: {:?}",
-                    e
-                );
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let rb_cls = env
-            .load_class(jni_str!("com/dropbear/physics/RigidBody"))
-            .map_err(|e| {
-                eprintln!(
-                    "[JNI Error] Class 'com/dropbear/physics/RigidBody' not found: {:?}",
-                    e
-                );
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let index_obj = env
-            .new_object(
-                &index_cls,
-                jni_sig!((int, int) -> void),
-                &[
-                    JValue::Int(self.index.index as i32),
-                    JValue::Int(self.index.generation as i32),
-                ],
-            )
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create Index object: {:?}", e);
-                DropbearNativeError::JNIFailedToCreateObject
-            })?;
-
-        let entity_obj = env
-            .new_object(
-                &entity_cls,
-                jni_sig!((long) -> void),
-                &[JValue::Long(self.entity_id as i64)],
-            )
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create EntityId object: {:?}", e);
-                DropbearNativeError::JNIFailedToCreateObject
-            })?;
-
-        let rb_obj = env
-            .new_object(
-                rb_cls,
-                jni_sig!((com.dropbear.physics.Index, com.dropbear.EntityId) -> void),
-                &[JValue::Object(&index_obj), JValue::Object(&entity_obj)],
-            )
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create RigidBody object: {:?}", e);
-                DropbearNativeError::JNIFailedToCreateObject
-            })?;
-
-        Ok(rb_obj)
-    }
-}
+// -----------------------------------------------------------------------------
 
 #[repr(C)]
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
 pub struct RayHit {
     pub collider: NCollider,
     pub distance: f64,
 }
 
-impl ToJObject for RayHit {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let collider = self.collider.to_jobject(env)?;
-        let distance = self.distance as jdouble;
-
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/RayHit"))
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create RayHit object: {:?}", e);
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let object = env
-            .new_object(
-                class,
-                jni_sig!((com.dropbear.physics.Collider, double) -> void),
-                &[JValue::Object(&collider), JValue::Double(distance)],
-            )
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create RayHit object: {:?}", e);
-                DropbearNativeError::JNIFailedToCreateObject
-            })?;
-
-        Ok(object)
-    }
-}
+// -----------------------------------------------------------------------------
 
 #[repr(C)]
-#[dropbear_macro::repr_c_enum]
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum NShapeCastStatus {
-    OutOfIterations,
-    Converged,
-    Failed,
-    PenetratingOrWithinTargetDist,
-}
-
-impl Into<ShapeCastStatus> for NShapeCastStatus {
-    fn into(self) -> ShapeCastStatus {
-        match self {
-            NShapeCastStatus::OutOfIterations => ShapeCastStatus::OutOfIterations,
-            NShapeCastStatus::Converged => ShapeCastStatus::Converged,
-            NShapeCastStatus::Failed => ShapeCastStatus::Failed,
-            NShapeCastStatus::PenetratingOrWithinTargetDist => {
-                ShapeCastStatus::PenetratingOrWithinTargetDist
-            }
-        }
-    }
+#[derive(Clone, Copy, Debug)]
+pub struct RigidBodyContext {
+    pub index: IndexNative,
+    pub entity_id: u64,
 }
 
-impl Into<NShapeCastStatus> for ShapeCastStatus {
-    fn into(self) -> NShapeCastStatus {
-        match self {
-            ShapeCastStatus::OutOfIterations => NShapeCastStatus::OutOfIterations,
-            ShapeCastStatus::Converged => NShapeCastStatus::Converged,
-            ShapeCastStatus::Failed => NShapeCastStatus::Failed,
-            ShapeCastStatus::PenetratingOrWithinTargetDist => {
-                NShapeCastStatus::PenetratingOrWithinTargetDist
-            }
-        }
-    }
-}
+// -----------------------------------------------------------------------------
 
 #[repr(C)]
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
 pub struct NShapeCastHit {
     pub collider: NCollider,
     pub distance: f64,
@@ -1010,210 +401,108 @@ pub struct NShapeCastHit {
     pub status: NShapeCastStatus,
 }
 
-impl ToJObject for NShapeCastHit {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        use jni::sys::jdouble;
-
-        let collider = self.collider.to_jobject(env)?;
-        let witness1 = self.witness1.to_jobject(env)?;
-        let witness2 = self.witness2.to_jobject(env)?;
-        let normal1 = self.normal1.to_jobject(env)?;
-        let normal2 = self.normal2.to_jobject(env)?;
-        let status = self.status.to_jobject(env)?;
-
-        let distance = self.distance as jdouble;
-
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/ShapeCastHit"))
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to find ShapeCastHit class: {:?}", e);
-                DropbearNativeError::JNIClassNotFound
-            })?;
-
-        let object = env
-            .new_object(
-                class,
-                jni_sig!("(Lcom/dropbear/physics/Collider;DLcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/physics/ShapeCastStatus;)V"),
-                &[
-                    JValue::Object(&collider),
-                    JValue::Double(distance),
-                    JValue::Object(&witness1),
-                    JValue::Object(&witness2),
-                    JValue::Object(&normal1),
-                    JValue::Object(&normal2),
-                    JValue::Object(&status),
-                ],
-            )
-            .map_err(|e| {
-                eprintln!("[JNI Error] Failed to create ShapeCastHit object: {:?}", e);
-                DropbearNativeError::JNIFailedToCreateObject
-            })?;
+#[repr(C)]
+#[dropbear_macro::repr_c_enum]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum NShapeCastStatus {
+    OutOfIterations,
+    Converged,
+    Failed,
+    PenetratingOrWithinTargetDist,
+}
 
-        Ok(object)
+impl From<rapier3d::parry::query::ShapeCastStatus> for NShapeCastStatus {
+    fn from(v: rapier3d::parry::query::ShapeCastStatus) -> Self {
+        use rapier3d::parry::query::ShapeCastStatus;
+        match v {
+            ShapeCastStatus::OutOfIterations => Self::OutOfIterations,
+            ShapeCastStatus::Converged => Self::Converged,
+            ShapeCastStatus::Failed => Self::Failed,
+            ShapeCastStatus::PenetratingOrWithinTargetDist => Self::PenetratingOrWithinTargetDist,
+        }
     }
 }
 
+// -------------------------------------------------------------- event types --
+
 #[repr(C)]
-#[derive(Clone, Copy)]
-/// Class: `com.dropbear.physics.CollisionEventType`
+#[derive(Clone, Copy, Debug)]
 pub enum CollisionEventType {
     Started,
     Stopped,
 }
 
-impl ToJObject for CollisionEventType {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/CollisionEventType"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let name = match self {
-            CollisionEventType::Started => "Started",
-            CollisionEventType::Stopped => "Stopped",
-        };
-        let name_jstring = env
-            .new_string(name)
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let value = env
-            .call_static_method(
-                class,
-                jni_str!("valueOf"),
-                jni_sig!((java.lang.String) -> com.dropbear.physics.CollisionEventType),
-                &[JValue::from(&name_jstring)],
-            )
-            .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
-            .l()
-            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
-
-        Ok(value)
-    }
-}
-
 #[repr(C)]
 #[derive(Clone, Copy)]
 pub struct CollisionEvent {
-    pub(crate) event_type: CollisionEventType,
-    pub(crate) collider1: NCollider,
-    pub(crate) collider2: NCollider,
-    pub(crate) flags: u64,
+    pub event_type: CollisionEventType,
+    pub collider1: NCollider,
+    pub collider2: NCollider,
+    pub flags: u64,
 }
 
 impl CollisionEvent {
     pub fn collider1_entity_id(&self) -> u64 {
         self.collider1.entity_id
     }
-
     pub fn collider2_entity_id(&self) -> u64 {
         self.collider2.entity_id
     }
-}
 
-impl CollisionEvent {
     pub fn from_rapier3d(
         physics: &PhysicsState,
         value: rapier3d::geometry::CollisionEvent,
-    ) -> Option<CollisionEvent> {
+    ) -> Option<Self> {
+        fn resolve(
+            physics: &PhysicsState,
+            handle: ColliderHandle,
+        ) -> Option<hecs::Entity> {
+            let label = physics.colliders_entity_map.iter().find_map(|(l, s)| {
+                for (_, h) in s {
+                    if *h == handle {
+                        return Some(l.clone());
+                    }
+                }
+                None
+            })?;
+            physics
+                .entity_label_map
+                .iter()
+                .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
+        }
+
         match value {
             rapier3d::prelude::CollisionEvent::Started(col1, col2, flags) => {
-                let collider1_info = physics
-                    .colliders_entity_map
-                    .iter()
-                    .find_map(|(l, s)| {
-                        for (_, h) in s {
-                            if col1 == *h {
-                                return Some(l.clone());
-                            }
-                        }
-                        None
-                    })
-                    .and_then(|label| {
-                        physics
-                            .entity_label_map
-                            .iter()
-                            .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
-                    })?;
-
-                let collider2_info = physics
-                    .colliders_entity_map
-                    .iter()
-                    .find_map(|(l, s)| {
-                        for (_, h) in s {
-                            if col2 == *h {
-                                return Some(l.clone());
-                            }
-                        }
-                        None
-                    })
-                    .and_then(|label| {
-                        physics
-                            .entity_label_map
-                            .iter()
-                            .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
-                    })?;
-
+                let e1 = resolve(physics, col1)?;
+                let e2 = resolve(physics, col2)?;
                 Some(Self {
                     event_type: CollisionEventType::Started,
                     collider1: NCollider {
                         index: IndexNative::from(col1.0),
-                        entity_id: collider1_info.to_bits().get(),
+                        entity_id: e1.to_bits().get(),
                         id: col1.into_raw_parts().0,
                     },
                     collider2: NCollider {
                         index: IndexNative::from(col2.0),
-                        entity_id: collider2_info.to_bits().get(),
+                        entity_id: e2.to_bits().get(),
                         id: col2.into_raw_parts().0,
                     },
                     flags: flags.bits() as u64,
                 })
             }
             rapier3d::prelude::CollisionEvent::Stopped(col1, col2, flags) => {
-                let collider1_info = physics
-                    .colliders_entity_map
-                    .iter()
-                    .find_map(|(l, s)| {
-                        for (_, h) in s {
-                            if col1 == *h {
-                                return Some(l.clone());
-                            }
-                        }
-                        None
-                    })
-                    .and_then(|label| {
-                        physics
-                            .entity_label_map
-                            .iter()
-                            .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
-                    })?;
-
-                let collider2_info = physics
-                    .colliders_entity_map
-                    .iter()
-                    .find_map(|(l, s)| {
-                        for (_, h) in s {
-                            if col2 == *h {
-                                return Some(l.clone());
-                            }
-                        }
-                        None
-                    })
-                    .and_then(|label| {
-                        physics
-                            .entity_label_map
-                            .iter()
-                            .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
-                    })?;
-
+                let e1 = resolve(physics, col1)?;
+                let e2 = resolve(physics, col2)?;
                 Some(Self {
                     event_type: CollisionEventType::Stopped,
                     collider1: NCollider {
                         index: IndexNative::from(col1.0),
-                        entity_id: collider1_info.to_bits().get(),
+                        entity_id: e1.to_bits().get(),
                         id: col1.into_raw_parts().0,
                     },
                     collider2: NCollider {
                         index: IndexNative::from(col2.0),
-                        entity_id: collider2_info.to_bits().get(),
+                        entity_id: e2.to_bits().get(),
                         id: col2.into_raw_parts().0,
                     },
                     flags: flags.bits() as u64,
@@ -1223,80 +512,42 @@ impl CollisionEvent {
     }
 }
 
-impl ToJObject for CollisionEvent {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/CollisionEvent"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let event_type = self.event_type.to_jobject(env)?;
-        let collider1 = self.collider1.to_jobject(env)?;
-        let collider2 = self.collider2.to_jobject(env)?;
-
-        let flags = self.flags as i32;
-        let obj = env
-            .new_object(
-                class,
-                jni_sig!("(Lcom/dropbear/physics/CollisionEventType;Lcom/dropbear/physics/Collider;Lcom/dropbear/physics/Collider;I)V"),
-                &[
-                    JValue::Object(&event_type),
-                    JValue::Object(&collider1),
-                    JValue::Object(&collider2),
-                    JValue::Int(flags),
-                ],
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
-
 #[repr(C)]
 #[derive(Clone, Copy)]
 pub struct ContactForceEvent {
-    pub(crate) collider1: NCollider,
-    pub(crate) collider2: NCollider,
-    pub(crate) total_force: NVector3,
-    pub(crate) total_force_magnitude: f64,
-    pub(crate) max_force_direction: NVector3,
-    pub(crate) max_force_magnitude: f64,
+    pub collider1: NCollider,
+    pub collider2: NCollider,
+    pub total_force: NVector3,
+    pub total_force_magnitude: f64,
+    pub max_force_direction: NVector3,
+    pub max_force_magnitude: f64,
 }
 
 impl ContactForceEvent {
     pub fn collider1_entity_id(&self) -> u64 {
         self.collider1.entity_id
     }
-
     pub fn collider2_entity_id(&self) -> u64 {
         self.collider2.entity_id
     }
-}
 
-impl ContactForceEvent {
     pub fn from_rapier3d(
         physics: &PhysicsState,
         event: rapier3d::prelude::ContactForceEvent,
-    ) -> Option<ContactForceEvent> {
-        let find_entity = |collider_handle: ColliderHandle| -> Option<Entity> {
-            Some(
-                physics
-                    .colliders_entity_map
-                    .iter()
-                    .find_map(|(l, s)| {
-                        for (_, h) in s {
-                            if collider_handle == *h {
-                                return Some(l.clone());
-                            }
-                        }
-                        None
-                    })
-                    .and_then(|label| {
-                        physics
-                            .entity_label_map
-                            .iter()
-                            .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
-                    })?,
-            )
+    ) -> Option<Self> {
+        let find_entity = |handle: ColliderHandle| -> Option<hecs::Entity> {
+            let label = physics.colliders_entity_map.iter().find_map(|(l, s)| {
+                for (_, h) in s {
+                    if *h == handle {
+                        return Some(l.clone());
+                    }
+                }
+                None
+            })?;
+            physics
+                .entity_label_map
+                .iter()
+                .find_map(|(e, l)| if l == &label { Some(*e) } else { None })
         };
 
         Some(Self {
@@ -1311,47 +562,17 @@ impl ContactForceEvent {
                 id: event.collider2.into_raw_parts().0,
             },
             total_force: NVector3::new(
-                event.total_force.x.into(),
-                event.total_force.y.into(),
-                event.total_force.z.into(),
+                event.total_force.x as f64,
+                event.total_force.y as f64,
+                event.total_force.z as f64,
             ),
             total_force_magnitude: event.total_force_magnitude as f64,
             max_force_direction: NVector3::new(
-                event.max_force_direction.x.into(),
-                event.max_force_direction.y.into(),
-                event.max_force_direction.z.into(),
+                event.max_force_direction.x as f64,
+                event.max_force_direction.y as f64,
+                event.max_force_direction.z as f64,
             ),
             max_force_magnitude: event.max_force_magnitude as f64,
         })
     }
 }
-
-impl ToJObject for ContactForceEvent {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/physics/ContactForceEvent"))
-            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
-
-        let collider1 = self.collider1.to_jobject(env)?;
-        let collider2 = self.collider2.to_jobject(env)?;
-        let total_force = self.total_force.to_jobject(env)?;
-        let max_force_direction = self.max_force_direction.to_jobject(env)?;
-
-        let obj = env
-            .new_object(
-                class,
-                jni_sig!("(Lcom/dropbear/physics/Collider;Lcom/dropbear/physics/Collider;Lcom/dropbear/math/Vector3d;DLcom/dropbear/math/Vector3d;D)V"),
-                &[
-                    JValue::Object(&collider1),
-                    JValue::Object(&collider2),
-                    JValue::Object(&total_force),
-                    JValue::Double(self.total_force_magnitude),
-                    JValue::Object(&max_force_direction),
-                    JValue::Double(self.max_force_magnitude),
-                ],
-            )
-            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 912bb1e..4507ab4 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -2,12 +2,9 @@
 
 pub mod option;
 
-use crate::scripting::result::DropbearNativeResult;
 use crate::ser::model::{EucalyptusMaterial, EucalyptusMesh, EucalyptusModel};
 use crate::states::Node;
 use dropbear_engine::utils::ResourceReference;
-use jni::objects::{JObject, JValue};
-use jni::{Env, jni_sig, jni_str};
 use std::collections::hash_map::DefaultHasher;
 use std::fs;
 use std::hash::{Hash, Hasher};
@@ -850,9 +847,9 @@ pub fn start_deadlock_detector() {
 #[repr(C)]
 #[derive(Clone)]
 pub struct Progress {
-    pub(crate) current: usize,
-    pub(crate) total: usize,
-    pub(crate) message: String,
+    pub current: usize,
+    pub total: usize,
+    pub message: String,
 }
 
 impl Default for Progress {
@@ -864,29 +861,3 @@ impl Default for Progress {
         }
     }
 }
-
-impl crate::scripting::jni::utils::ToJObject for crate::utils::Progress {
-    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
-        let class = env
-            .load_class(jni_str!("com/dropbear/utils/Progress"))
-            .map_err(|_| crate::scripting::native::DropbearNativeError::JNIClassNotFound)?;
-
-        let message_jstring = env
-            .new_string(&self.message)
-            .map_err(|_| crate::scripting::native::DropbearNativeError::JNIFailedToCreateObject)?;
-
-        let obj = env
-            .new_object(
-                &class,
-                jni_sig!((double, double, java.lang.String) -> void),
-                &[
-                    JValue::Double(self.current as f64),
-                    JValue::Double(self.total as f64),
-                    JValue::from(&message_jstring),
-                ],
-            )
-            .map_err(|_| crate::scripting::native::DropbearNativeError::JNIFailedToCreateObject)?;
-
-        Ok(obj)
-    }
-}
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 7ad2d8a..b735abc 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -66,7 +66,6 @@ use std::cmp::PartialEq;
 use std::collections::{HashSet, VecDeque};
 use std::rc::Rc;
 use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
-use arc_swap::ArcSwap;
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
 use wgpu::{Color, Extent3d};
diff --git a/crates/eucalyptus-exports/Cargo.toml b/crates/eucalyptus-exports/Cargo.toml
new file mode 100644
index 0000000..ad0bfa3
--- /dev/null
+++ b/crates/eucalyptus-exports/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "eucalyptus-exports"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+readme = "README.md"
+authors.workspace = true
+description = "Deals with exports of functions"
+
+[lib]
+name = "eucalyptus_core" # keep the name for legacy reasons (it works as is, dont change please)
+crate-type = ["cdylib"]
+
+[dependencies]
+dropbear-engine = { path = "../dropbear-engine" }
+eucalyptus-core = { path = "../eucalyptus-core" }
+dropbear-macro = { path = "../dropbear-macro" }
+
+jni.workspace = true
+hecs.workspace = true
+glam.workspace = true
+crossbeam-channel.workspace = true
+
+[build-dependencies]
+goanna-gen = { path = "../goanna-gen" }
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/README.md b/crates/eucalyptus-exports/README.md
new file mode 100644
index 0000000..6a6ad87
--- /dev/null
+++ b/crates/eucalyptus-exports/README.md
@@ -0,0 +1,27 @@
+# eucalyptus-exports
+
+A shared library that absracts the exports from `eucalyptus-core` and presents them in as `libeucalyptus_core.so` 
+(or the equivalent for the related platform). 
+
+## Language Support
+
+Despite the project using ECS, OOP-based paradigms are used heavily (in Kotlin) and can be used for your own bindings. 
+
+Currently, this engine has support in these languages (priority in order)
+
+### JVM
+Primarily JVM-based languages (Kotlin/JVM first-class, Java available).
+#### Caveats
+- Some issues with JVM is that the final executable that is shipped will need to be enabled 
+through an executable argument `{} --enable-jvm` (or enabled through a shipping setting within Project Settings). 
+Within the dropbear project, the JVM is used primarily for editor plugins and easy game prototyping. 
+C-Native-based languages can be used as an alternative for prototyping, however, it will require a longer build time 
+and will require the binding developer to create their own reflection methods. 
+
+### C-Native
+It is primarily supported through Kotlin/Native; however, the same exports *can* be used with
+other languages, such as Python (no one has made such a thing). Follow the [Kotlin](../../scripting) class
+structure for OOP based languages (idk about other paradigm based languages). 
+
+There are headings created (albeit not always updated) under [dropbear.h](../../include/dropbear.h) that you can
+use for reference or your own bindings. 
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/build.rs b/crates/eucalyptus-exports/build.rs
new file mode 100644
index 0000000..410551a
--- /dev/null
+++ b/crates/eucalyptus-exports/build.rs
@@ -0,0 +1,13 @@
+fn main() {
+    // fuck you windows :(
+    #[cfg(target_os = "windows")]
+    {
+        println!("cargo:rustc-link-arg=/FORCE:MULTIPLE");
+        println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib");
+    }
+
+    goanna_gen::generate_c_header().unwrap();
+
+    println!("cargo:rerun-if-changed=build.rs");
+    println!("cargo:rerun-if-changed=src");
+}
diff --git a/crates/eucalyptus-exports/src/animation.rs b/crates/eucalyptus-exports/src/animation.rs
new file mode 100644
index 0000000..24f10f1
--- /dev/null
+++ b/crates/eucalyptus-exports/src/animation.rs
@@ -0,0 +1,400 @@
+use hecs::{Entity, World};
+use dropbear_engine::animation::{AnimationComponent, AnimationSettings};
+use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::entity::MeshRenderer;
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "animationComponentExistsForEntity"
+    ),
+    c
+)]
+fn exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&AnimationComponent>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getActiveAnimationIndex"
+    ),
+    c
+)]
+fn get_active_animation_index(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<Option<i32>> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(component.active_animation_index.map(|index| index as i32))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "setActiveAnimationIndex"
+    ),
+    c
+)]
+fn set_active_animation_index(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    index: &Option<i32>,
+) -> DropbearNativeResult<()> {
+    let mut component = world
+        .get::<&mut AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    let index = match index {
+        Some(value) if *value >= 0 => Some(*value as usize),
+        Some(_) => return Err(DropbearNativeError::InvalidArgument),
+        None => None,
+    };
+
+    if let Some(value) = index {
+        if !component.available_animations.is_empty()
+            && value >= component.available_animations.len()
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+    }
+
+    component.active_animation_index = index;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getTime"
+    ),
+    c
+)]
+fn get_time(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<f64> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    if let Some(index) = component.active_animation_index {
+        if let Some(settings) = component.animation_settings.get(&index) {
+            return Ok(settings.time as f64);
+        }
+    }
+
+    Ok(component.time as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "setTime"
+    ),
+    c
+)]
+fn set_time(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    value: f64,
+) -> DropbearNativeResult<()> {
+    let mut component = world
+        .get::<&mut AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    component.time = value as f32;
+
+    if let Some(index) = component.active_animation_index {
+        let (time, speed, looping, is_playing) = (
+            component.time,
+            component.speed,
+            component.looping,
+            component.is_playing,
+        );
+        let settings = component
+            .animation_settings
+            .entry(index)
+            .or_insert_with(|| AnimationSettings {
+                time,
+                speed,
+                looping,
+                is_playing,
+            });
+        settings.time = time;
+    }
+
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getSpeed"
+    ),
+    c
+)]
+fn get_speed(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<f64> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    if let Some(index) = component.active_animation_index {
+        if let Some(settings) = component.animation_settings.get(&index) {
+            return Ok(settings.speed as f64);
+        }
+    }
+
+    Ok(component.speed as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "setSpeed"
+    ),
+    c
+)]
+fn set_speed(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    value: f64,
+) -> DropbearNativeResult<()> {
+    let mut component = world
+        .get::<&mut AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    component.speed = value as f32;
+
+    if let Some(index) = component.active_animation_index {
+        let (time, speed, looping, is_playing) = (
+            component.time,
+            component.speed,
+            component.looping,
+            component.is_playing,
+        );
+        let settings = component
+            .animation_settings
+            .entry(index)
+            .or_insert_with(|| AnimationSettings {
+                time,
+                speed,
+                looping,
+                is_playing,
+            });
+        settings.speed = speed;
+    }
+
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getLooping"
+    ),
+    c
+)]
+fn get_looping(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    if let Some(index) = component.active_animation_index {
+        if let Some(settings) = component.animation_settings.get(&index) {
+            return Ok(settings.looping);
+        }
+    }
+
+    Ok(component.looping)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "setLooping"
+    ),
+    c
+)]
+fn set_looping(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    value: bool,
+) -> DropbearNativeResult<()> {
+    let mut component = world
+        .get::<&mut AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    component.looping = value;
+
+    if let Some(index) = component.active_animation_index {
+        let (time, speed, looping, is_playing) = (
+            component.time,
+            component.speed,
+            component.looping,
+            component.is_playing,
+        );
+        let settings = component
+            .animation_settings
+            .entry(index)
+            .or_insert_with(|| AnimationSettings {
+                time,
+                speed,
+                looping,
+                is_playing,
+            });
+        settings.looping = value;
+    }
+
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getIsPlaying"
+    ),
+    c
+)]
+fn get_is_playing(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    if let Some(index) = component.active_animation_index {
+        if let Some(settings) = component.animation_settings.get(&index) {
+            return Ok(settings.is_playing);
+        }
+    }
+
+    Ok(component.is_playing)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "setIsPlaying"
+    ),
+    c
+)]
+fn set_is_playing(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    value: bool,
+) -> DropbearNativeResult<()> {
+    let mut component = world
+        .get::<&mut AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    component.is_playing = value;
+
+    if let Some(index) = component.active_animation_index {
+        let (time, speed, looping, is_playing) = (
+            component.time,
+            component.speed,
+            component.looping,
+            component.is_playing,
+        );
+        let settings = component
+            .animation_settings
+            .entry(index)
+            .or_insert_with(|| AnimationSettings {
+                time,
+                speed,
+                looping,
+                is_playing,
+            });
+        settings.is_playing = value;
+    }
+
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getIndexFromString"
+    ),
+    c
+)]
+fn get_index_from_string(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    name: String,
+) -> DropbearNativeResult<Option<i32>> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+
+    Ok(component
+        .available_animations
+        .iter()
+        .enumerate()
+        .find_map(|(i, l)| if *l == name { Some(i as i32) } else { None }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.animation.AnimationComponentNative",
+        func = "getAvailableAnimations"
+    ),
+    c
+)]
+fn get_available_animations(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<Vec<String>> {
+    let component = world
+        .get::<&AnimationComponent>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(collect_available_animations(world, entity, &component))
+}
+
+// ---------------------- helpers ----------------------
+
+fn collect_available_animations(
+    world: &World,
+    entity: Entity,
+    component: &AnimationComponent,
+) -> Vec<String> {
+    if !component.available_animations.is_empty() {
+        return component.available_animations.clone();
+    }
+
+    let Ok(renderer) = world.get::<&MeshRenderer>(entity) else {
+        return Vec::new();
+    };
+
+    let handle = renderer.model();
+    if handle.is_null() {
+        return Vec::new();
+    }
+
+    let registry = ASSET_REGISTRY.read();
+    let Some(model) = registry.get_model(handle) else {
+        return Vec::new();
+    };
+
+    model
+        .animations
+        .iter()
+        .map(|animation| animation.name.clone())
+        .collect()
+}
diff --git a/crates/eucalyptus-exports/src/asset.rs b/crates/eucalyptus-exports/src/asset.rs
new file mode 100644
index 0000000..663a598
--- /dev/null
+++ b/crates/eucalyptus-exports/src/asset.rs
@@ -0,0 +1,58 @@
+pub mod model;
+pub mod texture;
+
+use jni::{jni_sig, jni_str, Env};
+use jni::objects::JObject;
+use dropbear_engine::asset::AssetKind;
+use eucalyptus_core::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::FromJObject;
+
+impl FromJObject for AssetKind {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized,
+    {
+        let ordinal = env
+            .call_method(obj, jni_str!("ordinal"), jni_sig!(() -> i32), &[])?
+            .i()?;
+
+        match ordinal {
+            0 => Ok(AssetKind::Texture),
+            1 => Ok(AssetKind::Model),
+            _ => Err(DropbearNativeError::InvalidEnumOrdinal),
+        }
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.DropbearEngineNative", func = "getAsset"),
+    c(name = "dropbear_engine_get_asset")
+)]
+fn dropbear_asset_get_asset(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    label: String,
+    kind: &AssetKind,
+) -> DropbearNativeResult<Option<u64>> {
+    let reader = asset.read();
+    match kind {
+        AssetKind::Texture => {
+            let result = reader.get_texture_handle_from_label(&label);
+            if let Some(handle) = result {
+                Ok(Some(handle.id))
+            } else {
+                Ok(None)
+            }
+        }
+        AssetKind::Model => {
+            let result = reader.get_model_handle_from_label(&label);
+            if let Some(handle) = result {
+                Ok(Some(handle.id))
+            } else {
+                Ok(None)
+            }
+        }
+    }
+}
+
diff --git a/crates/eucalyptus-exports/src/asset/model.rs b/crates/eucalyptus-exports/src/asset/model.rs
new file mode 100644
index 0000000..74fdae4
--- /dev/null
+++ b/crates/eucalyptus-exports/src/asset/model.rs
@@ -0,0 +1,106 @@
+use dropbear_engine::asset::Handle;
+use eucalyptus_core::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::asset::model::ty::{NAnimation, NMaterial, NMesh, NNode, NSkin};
+
+mod ty;
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.ModelNative", func = "getLabel"),
+    c(name = "dropbear_asset_model_get_label")
+)]
+fn dropbear_asset_model_get_label(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    model_handle: u64,
+) -> DropbearNativeResult<String> {
+    let label = asset
+        .read()
+        .get_label_from_model_handle(Handle::new(model_handle))
+        .ok_or_else(|| DropbearNativeError::InvalidHandle)?;
+    Ok(label)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.ModelNative", func = "getMeshes"),
+    c(name = "dropbear_asset_model_get_meshes")
+)]
+fn dropbear_asset_model_get_meshes(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    model_handle: u64,
+) -> DropbearNativeResult<Vec<NMesh>> {
+    let reader = asset.read();
+    let model = reader
+        .get_model(Handle::new(model_handle))
+        .ok_or(DropbearNativeError::InvalidHandle)?;
+
+    Ok(model.meshes.iter().map(|v| v.into()).collect())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.ModelNative", func = "getMaterials"),
+    c(name = "dropbear_asset_model_get_materials")
+)]
+fn dropbear_asset_model_get_materials(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    model_handle: u64,
+) -> DropbearNativeResult<Vec<NMaterial>> {
+    let reader = asset.read();
+    let model = reader
+        .get_model(Handle::new(model_handle))
+        .ok_or(DropbearNativeError::InvalidHandle)?;
+
+    Ok(model
+        .materials
+        .iter()
+        .map(|v| v.clone().into())
+        .collect())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.ModelNative", func = "getSkins"),
+    c(name = "dropbear_asset_model_get_skins")
+)]
+pub fn dropbear_asset_model_get_skins(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    model_handle: u64,
+) -> DropbearNativeResult<Vec<NSkin>> {
+    let reader = asset.read();
+    let model = reader
+        .get_model(Handle::new(model_handle))
+        .ok_or(DropbearNativeError::InvalidHandle)?;
+
+    Ok(model.skins.iter().map(|v| v.clone().into()).collect())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.ModelNative", func = "getAnimations"),
+    c(name = "dropbear_asset_model_get_animations")
+)]
+pub fn dropbear_asset_model_get_animations(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    model_handle: u64,
+) -> DropbearNativeResult<Vec<NAnimation>> {
+    let reader = asset.read();
+    let model = reader
+        .get_model(Handle::new(model_handle))
+        .ok_or(DropbearNativeError::InvalidHandle)?;
+
+    Ok(model.animations.iter().map(|v| v.clone().into()).collect())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.ModelNative", func = "getNodes"),
+    c(name = "dropbear_asset_model_get_nodes")
+)]
+pub fn dropbear_asset_model_get_nodes(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    model_handle: u64,
+) -> DropbearNativeResult<Vec<NNode>> {
+    let reader = asset.read();
+    let model = reader
+        .get_model(Handle::new(model_handle))
+        .ok_or(DropbearNativeError::InvalidHandle)?;
+
+    Ok(model.nodes.iter().map(|v| v.clone().into()).collect())
+}
diff --git a/crates/eucalyptus-exports/src/asset/model/ty.rs b/crates/eucalyptus-exports/src/asset/model/ty.rs
new file mode 100644
index 0000000..aeded95
--- /dev/null
+++ b/crates/eucalyptus-exports/src/asset/model/ty.rs
@@ -0,0 +1,652 @@
+use jni::{jni_sig, jni_str, Env, JValue};
+use jni::objects::JObject;
+use dropbear_engine::model::{Animation, AnimationChannel, AnimationInterpolation, ChannelValues, Material, Mesh, ModelVertex, Node, NodeTransform, Skin};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::math::{NQuaternion, NVector2, NVector3, NVector4};
+use crate::ToJObject;
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NModelVertex {
+    pub position: NVector3,
+    pub normal: NVector3,
+    pub tangent: NVector4,
+    pub tex_coords0: NVector2,
+    pub tex_coords1: NVector2,
+    pub colour0: NVector4,
+    pub joints0: Vec<i32>,
+    pub weights0: NVector4,
+}
+
+impl ToJObject for NModelVertex {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/asset/model/ModelVertex"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let position = self.position.to_jobject(env)?;
+        let normal = self.normal.to_jobject(env)?;
+        let tangent = self.tangent.to_jobject(env)?;
+        let tex_coords0 = self.tex_coords0.to_jobject(env)?;
+        let tex_coords1 = self.tex_coords1.to_jobject(env)?;
+        let colour0 = self.colour0.to_jobject(env)?;
+        let joints0 = self.joints0.as_slice().to_jobject(env)?;
+        let weights0 = self.weights0.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&position),
+            JValue::Object(&normal),
+            JValue::Object(&tangent),
+            JValue::Object(&tex_coords0),
+            JValue::Object(&tex_coords1),
+            JValue::Object(&colour0),
+            JValue::Object(&joints0),
+            JValue::Object(&weights0),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!((com.dropbear.math.Vector3f, com.dropbear.math.Vector3f, com.dropbear.math.Vector4f, com.dropbear.math.Vector2f, com.dropbear.math.Vector2f, com.dropbear.math.Vector4f, [int], com.dropbear.math.Vector4f) -> ()),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NModelVertex> for ModelVertex {
+    fn into(self) -> NModelVertex {
+        NModelVertex {
+            position: NVector3::from(self.position),
+            normal: NVector3::from(self.normal),
+            tangent: NVector4::from(self.tangent),
+            tex_coords0: NVector2::from(self.tex_coords0),
+            tex_coords1: NVector2::from(self.tex_coords1),
+            colour0: NVector4::from(self.colour0),
+            joints0: self.joints0.iter().map(|v| *v as i32).collect(),
+            weights0: NVector4::from(self.weights0),
+        }
+    }
+}
+
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NMesh {
+    pub name: String,
+    pub num_elements: i32,
+    pub material_index: i32,
+    pub vertices: Vec<NModelVertex>,
+}
+
+impl ToJObject for NMesh {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.Mesh"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = env
+            .new_string(&self.name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let vertices = self.vertices.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&name),
+            JValue::Int(self.num_elements),
+            JValue::Int(self.material_index),
+            JValue::Object(&vertices),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!((java.lang.String, int, int, java.util.List) -> ()),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl From<&Mesh> for NMesh {
+    fn from(mesh: &Mesh) -> NMesh {
+        NMesh {
+            name: mesh.name.clone(),
+            num_elements: mesh.num_elements as i32,
+            material_index: mesh.material as i32,
+            vertices: mesh.vertices.iter().map(|v| (*v).into()).collect(),
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NMaterial {
+    pub name: String,
+    pub diffuse_texture: u64,
+    pub normal_texture: Option<u64>,
+    pub emissive_texture: Option<u64>,
+    pub metallic_roughness_texture: Option<u64>,
+    pub occlusion_texture: Option<u64>,
+    pub tint: NVector4,
+    pub emissive_factor: NVector3,
+    pub metallic_factor: f32,
+    pub roughness_factor: f32,
+    pub alpha_cutoff: Option<f32>,
+    pub occlusion_strength: f32,
+    pub normal_scale: f32,
+    pub uv_tiling: NVector2,
+}
+
+impl ToJObject for NMaterial {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.Material"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = env
+            .new_string(&self.name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let diffuse_texture = new_texture(env, self.diffuse_texture)?;
+        let normal_texture = match self.normal_texture {
+            Some(id) => new_texture(env, id)?,
+            None => JObject::null(),
+        };
+        let emissive_texture = match self.emissive_texture {
+            Some(id) => new_texture(env, id)?,
+            None => JObject::null(),
+        };
+        let metallic_roughness_texture = match self.metallic_roughness_texture {
+            Some(id) => new_texture(env, id)?,
+            None => JObject::null(),
+        };
+        let occlusion_texture = match self.occlusion_texture {
+            Some(id) => new_texture(env, id)?,
+            None => JObject::null(),
+        };
+        let tint = self.tint.to_jobject(env)?;
+        let emissive_factor = self.emissive_factor.to_jobject(env)?;
+        let uv_tiling = self.uv_tiling.to_jobject(env)?;
+        let alpha_cutoff = self.alpha_cutoff.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&name),
+            JValue::Object(&diffuse_texture),
+            JValue::Object(&normal_texture),
+            JValue::Object(&tint),
+            JValue::Object(&emissive_factor),
+            JValue::Double(self.metallic_factor as f64),
+            JValue::Double(self.roughness_factor as f64),
+            JValue::Object(&alpha_cutoff),
+            JValue::Double(self.occlusion_strength as f64),
+            JValue::Double(self.normal_scale as f64),
+            JValue::Object(&uv_tiling),
+            JValue::Object(&emissive_texture),
+            JValue::Object(&metallic_roughness_texture),
+            JValue::Object(&occlusion_texture),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!((
+                    java.lang.String,
+                    com.dropbear.asset.Texture,
+                    com.dropbear.asset.Texture,
+                    com.dropbear.math.Vector4d,
+                    com.dropbear.math.Vector3d,
+                    double, double,
+                    java.lang.Double,
+                    double, double,
+                    com.dropbear.math.Vector2d,
+                    com.dropbear.asset.Texture,
+                    com.dropbear.asset.Texture,
+                    com.dropbear.asset.Texture
+                ) -> ()),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NMaterial> for Material {
+    fn into(self) -> NMaterial {
+        NMaterial {
+            name: self.name.clone(),
+            diffuse_texture: self.diffuse_texture.id,
+            normal_texture: self.normal_texture.and_then(|v| Some(v.id)),
+            tint: NVector4::from(self.base_colour),
+            emissive_factor: NVector3::from(self.emissive_factor),
+            metallic_factor: self.metallic_factor,
+            roughness_factor: self.roughness_factor,
+            alpha_cutoff: self.alpha_cutoff,
+            occlusion_strength: self.occlusion_strength,
+            normal_scale: self.normal_scale,
+            uv_tiling: NVector2::from(self.uv_tiling),
+            emissive_texture: self.emissive_texture.and_then(|v| Some(v.id)),
+            metallic_roughness_texture: self.metallic_roughness_texture.and_then(|v| Some(v.id)),
+            occlusion_texture: self.occlusion_texture.and_then(|v| Some(v.id)),
+        }
+    }
+}
+
+// todo: cant this just be represented as a `com.dropbear.math.Transform`?
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NNodeTransform {
+    pub translation: NVector3,
+    pub rotation: NQuaternion,
+    pub scale: NVector3,
+}
+
+impl ToJObject for NNodeTransform {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.NodeTransform"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let translation = self.translation.to_jobject(env)?;
+        let rotation = self.rotation.to_jobject(env)?;
+        let scale = self.scale.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&translation),
+            JValue::Object(&rotation),
+            JValue::Object(&scale),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!("(Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Quaterniond;Lcom/dropbear/math/Vector3d;)V"),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NNodeTransform> for NodeTransform {
+    fn into(self) -> NNodeTransform {
+        NNodeTransform {
+            translation: NVector3::from(self.translation),
+            rotation: NQuaternion::from(self.rotation),
+            scale: NVector3::from(self.scale),
+        }
+    }
+}
+
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NNode {
+    pub name: String,
+    pub parent: Option<i32>,
+    pub children: Vec<i32>,
+    pub transform: NNodeTransform,
+}
+
+impl ToJObject for NNode {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.Node"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = env
+            .new_string(&self.name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let parent = self.parent.to_jobject(env)?;
+        let children = self.children.as_slice().to_jobject(env)?;
+        let transform = self.transform.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&name),
+            JValue::Object(&parent),
+            JValue::Object(&children),
+            JValue::Object(&transform),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!("(Ljava/lang/String;Ljava/lang/Integer;Ljava/util/List;Lcom/dropbear/asset/model/NodeTransform;)V"),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NNode> for Node {
+    fn into(self) -> NNode {
+        NNode {
+            name: self.name.clone(),
+            parent: self.parent.map(|v| v as i32),
+            children: self.children.iter().map(|v| *v as i32).collect(),
+            transform: self.transform.into(),
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NSkin {
+    pub name: String,
+    pub joints: Vec<i32>,
+    pub inverse_bind_matrices: Vec<Vec<f64>>,
+    pub skeleton_root: Option<i32>,
+}
+
+impl ToJObject for NSkin {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.Skin"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = env
+            .new_string(&self.name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let joints = self.joints.as_slice().to_jobject(env)?;
+        let inverse_bind_matrices = self.inverse_bind_matrices.as_slice().to_jobject(env)?;
+        let skeleton_root = self.skeleton_root.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&name),
+            JValue::Object(&joints),
+            JValue::Object(&inverse_bind_matrices),
+            JValue::Object(&skeleton_root),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!(
+                    "(Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;)V"
+                ),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NSkin> for Skin {
+    fn into(self) -> NSkin {
+        let inverse_bind_matrices = self
+            .inverse_bind_matrices
+            .iter()
+            .map(|matrix| matrix.to_cols_array().iter().map(|v| *v as f64).collect())
+            .collect();
+
+        NSkin {
+            name: self.name.clone(),
+            joints: self.joints.iter().map(|v| *v as i32).collect(),
+            inverse_bind_matrices,
+            skeleton_root: self.skeleton_root.map(|v| v as i32),
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NAnimation {
+    pub name: String,
+    pub channels: Vec<NAnimationChannel>,
+    pub duration: f32,
+}
+
+impl ToJObject for NAnimation {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.Animation"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = env
+            .new_string(&self.name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let channels = self.channels.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&name),
+            JValue::Object(&channels),
+            JValue::Double(self.duration as f64),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!("(Ljava/lang/String;Ljava/util/List;D)V"),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NAnimation> for Animation {
+    fn into(self) -> NAnimation {
+        NAnimation {
+            name: self.name.clone(),
+            channels: self
+                .channels
+                .into_iter()
+                .map(|v| v.into())
+                .collect(),
+            duration: self.duration,
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct NAnimationChannel {
+    pub target_node: i32,
+    pub times: Vec<f64>,
+    pub values: NChannelValues,
+    pub interpolation: NAnimationInterpolation,
+}
+
+impl ToJObject for NAnimationChannel {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.AnimationChannel"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let times = self.times.as_slice().to_jobject(env)?;
+        let values = self.values.to_jobject(env)?;
+        let interpolation = self.interpolation.to_jobject(env)?;
+
+        let args = [
+            JValue::Int(self.target_node),
+            JValue::Object(&times),
+            JValue::Object(&values),
+            JValue::Object(&interpolation),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                jni_sig!("(I[DLcom/dropbear/asset/model/ChannelValues;Lcom/dropbear/asset/model/AnimationInterpolation;)V"),
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+impl Into<NAnimationChannel> for AnimationChannel {
+    fn into(self) -> NAnimationChannel {
+        NAnimationChannel {
+            target_node: self.target_node as i32,
+            times: self.times.iter().map(|v| *v as f64).collect(),
+            values: self.values.into(),
+            interpolation: self.interpolation.into(),
+        }
+    }
+}
+
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+#[dropbear_macro::repr_c_enum]
+pub enum NAnimationInterpolation {
+    Linear,
+    Step,
+    CubicSpline,
+}
+
+impl ToJObject for NAnimationInterpolation {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com.dropbear.asset.model.AnimationInterpolation"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let field_name = match self {
+            NAnimationInterpolation::Linear => "LINEAR",
+            NAnimationInterpolation::Step => "STEP",
+            NAnimationInterpolation::CubicSpline => "CUBIC_SPLINE",
+        };
+
+        let value = env
+            .get_static_field(
+                class,
+                jni::strings::JNIString::from(field_name),
+                jni_sig!("Lcom/dropbear/asset/model/AnimationInterpolation;"),
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(value)
+    }
+}
+
+impl Into<NAnimationInterpolation> for AnimationInterpolation {
+    fn into(self) -> NAnimationInterpolation {
+        match self {
+            AnimationInterpolation::Linear => NAnimationInterpolation::Linear,
+            AnimationInterpolation::Step => NAnimationInterpolation::Step,
+            AnimationInterpolation::CubicSpline => NAnimationInterpolation::CubicSpline,
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Debug)]
+#[dropbear_macro::repr_c_enum]
+pub enum NChannelValues {
+    Translations { values: Vec<NVector3> },
+    Rotations { values: Vec<NQuaternion> },
+    Scales { values: Vec<NVector3> },
+    MorphWeights { values: Vec<Vec<f64>> },
+}
+
+impl ToJObject for NChannelValues {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        match self {
+            NChannelValues::Translations { values } => {
+                let class = env
+                    .load_class(jni_str!(
+                        "com.dropbear.asset.model.ChannelValues$Translations"
+                    ))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+                let list = values.to_jobject(env)?;
+                let obj = env
+                    .new_object(
+                        &class,
+                        jni_sig!("(Ljava/util/List;)V"),
+                        &[JValue::Object(&list)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+                Ok(obj)
+            }
+            NChannelValues::Rotations { values } => {
+                let class = env
+                    .load_class(jni_str!("com.dropbear.asset.model.ChannelValues$Rotations"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+                let list = values.to_jobject(env)?;
+                let obj = env
+                    .new_object(
+                        &class,
+                        jni_sig!("(Ljava/util/List;)V"),
+                        &[JValue::Object(&list)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+                Ok(obj)
+            }
+            NChannelValues::Scales { values } => {
+                let class = env
+                    .load_class(jni_str!("com.dropbear.asset.model.ChannelValues$Scales"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+                let list = values.to_jobject(env)?;
+                let obj = env
+                    .new_object(
+                        &class,
+                        jni_sig!("(Ljava/util/List;)V"),
+                        &[JValue::Object(&list)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+                Ok(obj)
+            }
+            NChannelValues::MorphWeights { values } => {
+                let class = env
+                    .load_class(jni_str!(
+                        "com.dropbear.asset.model.ChannelValues$MorphWeights"
+                    ))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+                let list = values.to_jobject(env)?;
+                let obj = env
+                    .new_object(
+                        &class,
+                        jni_sig!("(Ljava/util/List;)V"),
+                        &[JValue::Object(&list)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+                Ok(obj)
+            }
+        }
+    }
+}
+
+
+impl Into<NChannelValues> for ChannelValues {
+    fn into(self) -> NChannelValues {
+        match self {
+            ChannelValues::Translations(list) => NChannelValues::Translations {
+                values: list.iter().map(|v| NVector3::from(*v)).collect(),
+            },
+            ChannelValues::Rotations(list) => NChannelValues::Rotations {
+                values: list.iter().map(|v| NQuaternion::from(*v)).collect(),
+            },
+            ChannelValues::Scales(list) => NChannelValues::Scales {
+                values: list.iter().map(|v| NVector3::from(*v)).collect(),
+            },
+            ChannelValues::MorphWeights(items) => NChannelValues::MorphWeights {
+                values: items
+                    .iter()
+                    .map(|weights| weights.iter().map(|v| *v as f64).collect())
+                    .collect(),
+            },
+        }
+    }
+}
+
+fn new_texture<'a>(env: &mut Env<'a>, texture_id: u64) -> DropbearNativeResult<JObject<'a>> {
+    let class = env
+        .load_class(jni_str!("com.dropbear.asset.Texture"))
+        .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+    env.new_object(&class, jni_sig!("(J)V"), &[JValue::Long(texture_id as i64)])
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/src/asset/texture.rs b/crates/eucalyptus-exports/src/asset/texture.rs
new file mode 100644
index 0000000..af19d58
--- /dev/null
+++ b/crates/eucalyptus-exports/src/asset/texture.rs
@@ -0,0 +1,47 @@
+use dropbear_engine::asset::Handle;
+use eucalyptus_core::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.TextureNative", func = "getLabel"),
+    c(name = "dropbear_asset_texture_get_label")
+)]
+fn get_texture_label(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset_manager: &AssetRegistryUnwrapped,
+    texture_handle: u64,
+) -> DropbearNativeResult<Option<String>> {
+    Ok(asset_manager
+        .read()
+        .get_label_from_texture_handle(Handle::new(texture_handle)))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.TextureNative", func = "getWidth"),
+    c(name = "dropbear_asset_texture_get_width")
+)]
+fn get_texture_width(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset_manager: &AssetRegistryUnwrapped,
+    texture_handle: u64,
+) -> DropbearNativeResult<u32> {
+    asset_manager
+        .read()
+        .get_texture(Handle::new(texture_handle))
+        .map(|v| v.size.width)
+        .ok_or(DropbearNativeError::AssetNotFound)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.asset.TextureNative", func = "getHeight"),
+    c(name = "dropbear_asset_texture_get_height")
+)]
+fn get_texture_height(
+    #[dropbear_macro::define(AssetRegistryPtr)] asset_manager: &AssetRegistryUnwrapped,
+    texture_handle: u64,
+) -> DropbearNativeResult<u32> {
+    asset_manager
+        .read()
+        .get_texture(Handle::new(texture_handle))
+        .map(|v| v.size.height)
+        .ok_or(DropbearNativeError::AssetNotFound)
+}
diff --git a/crates/eucalyptus-exports/src/camera.rs b/crates/eucalyptus-exports/src/camera.rs
new file mode 100644
index 0000000..2918604
--- /dev/null
+++ b/crates/eucalyptus-exports/src/camera.rs
@@ -0,0 +1,349 @@
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::types::NVector3;
+use dropbear_engine::camera::Camera;
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "cameraExistsForEntity"),
+    c
+)]
+fn exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&Camera>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraEye"),
+    c
+)]
+fn get_eye(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NVector3> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.eye.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraEye"),
+    c
+)]
+fn set_eye(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    eye: &NVector3,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.eye = (*eye).into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraTarget"),
+    c
+)]
+fn get_target(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NVector3> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.target.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraTarget"),
+    c
+)]
+fn set_target(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    target: &NVector3,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.target = target.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraUp"),
+    c
+)]
+fn get_up(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NVector3> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.up.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraUp"),
+    c
+)]
+fn set_up(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    up: &NVector3,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.up = up.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraAspect"),
+    c
+)]
+fn get_aspect(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.aspect.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraFovY"),
+    c
+)]
+fn get_fovy(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.settings.fov_y.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraFovY"),
+    c
+)]
+fn set_fovy(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    fovy: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.settings.fov_y = fovy.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraZNear"),
+    c
+)]
+fn get_znear(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.znear.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraZNear"),
+    c
+)]
+fn set_znear(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    znear: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.znear = znear.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraZFar"),
+    c
+)]
+fn get_zfar(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.zfar.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraZFar"),
+    c
+)]
+fn set_zfar(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    zfar: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.zfar = zfar.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraYaw"),
+    c
+)]
+fn get_yaw(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.yaw.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraYaw"),
+    c
+)]
+fn set_yaw(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    yaw: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.yaw = yaw.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraPitch"),
+    c
+)]
+fn get_pitch(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.pitch.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraPitch"),
+    c
+)]
+fn set_pitch(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    pitch: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.pitch = pitch.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraSpeed"),
+    c
+)]
+fn get_speed(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.settings.speed.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraSpeed"),
+    c
+)]
+fn set_speed(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    speed: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.settings.speed = speed.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "getCameraSensitivity"),
+    c
+)]
+fn get_sensitivity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f64> {
+    match world.get::<&Camera>(entity) {
+        Ok(camera) => Ok(camera.settings.sensitivity.into()),
+        Err(e) => Err(e.into()),
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.CameraNative", func = "setCameraSensitivity"),
+    c
+)]
+fn set_sensitivity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    sensitivity: f64,
+) -> DropbearNativeResult<()> {
+    match world.get::<&mut Camera>(entity) {
+        Ok(mut camera) => {
+            camera.settings.sensitivity = sensitivity.into();
+            Ok(())
+        }
+        Err(e) => Err(e.into()),
+    }
+}
diff --git a/crates/eucalyptus-exports/src/component.rs b/crates/eucalyptus-exports/src/component.rs
new file mode 100644
index 0000000..27b4be5
--- /dev/null
+++ b/crates/eucalyptus-exports/src/component.rs
@@ -0,0 +1,40 @@
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::scripting::types::KotlinComponents;
+use hecs::{Entity, World};
+
+#[dropbear_macro::export(kotlin(
+    class = "com.dropbear.components.ComponentNative",
+    func = "hasKotlinComponent",
+))]
+fn has_kotlin_component(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    fqcn: String,
+) -> DropbearNativeResult<bool> {
+    if let Ok(kc) = world.get::<&KotlinComponents>(entity) {
+        Ok(kc.has(&fqcn))
+    } else {
+        Ok(false)
+    }
+}
+
+#[dropbear_macro::export(kotlin(
+    class = "com.dropbear.components.ComponentNative",
+    func = "registerKotlinComponent",
+))]
+fn register_kotlin_component_jni(
+    #[dropbear_macro::define(WorldPtr)] _world: &World,
+    fqcn: String,
+    type_name: String,
+    category: String,
+    description: String,
+) -> DropbearNativeResult<()> {
+    use eucalyptus_core::component::{KOTLIN_COMPONENT_QUEUE, KotlinComponentDecl};
+    KOTLIN_COMPONENT_QUEUE.lock().push(KotlinComponentDecl {
+        fqcn,
+        type_name,
+        category: if category.is_empty() { None } else { Some(category) },
+        description: if description.is_empty() { None } else { Some(description) },
+    });
+    Ok(())
+}
diff --git a/crates/eucalyptus-exports/src/debug.rs b/crates/eucalyptus-exports/src/debug.rs
new file mode 100644
index 0000000..b3d2afe
--- /dev/null
+++ b/crates/eucalyptus-exports/src/debug.rs
@@ -0,0 +1,213 @@
+use eucalyptus_core::ptr::GraphicsContextPtr;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::types::{NColour, NVector3};
+use crate::math::NQuaternion;
+use dropbear_engine::graphics::SharedGraphicsContext;
+use glam::{Quat, Vec3};
+
+macro_rules! with_debug {
+    ($graphics:expr, |$dd:ident| $body:expr) => {
+        if let Some($dd) = $graphics.debug_draw.lock().as_mut() {
+            $body
+        }
+    };
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawLine"),
+    c
+)]
+fn draw_line(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    start: &NVector3,
+    end: &NVector3,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let a = Vec3::new(start.x as f32, start.y as f32, start.z as f32);
+    let b = Vec3::new(end.x as f32, end.y as f32, end.z as f32);
+    with_debug!(graphics, |dd| dd.draw_line(a, b, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawRay"),
+    c
+)]
+fn draw_ray(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    origin: &NVector3,
+    dir: &NVector3,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let o = Vec3::new(origin.x as f32, origin.y as f32, origin.z as f32);
+    let d = Vec3::new(dir.x as f32, dir.y as f32, dir.z as f32);
+    with_debug!(graphics, |dd| dd.draw_ray(o, d, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawArrow"),
+    c
+)]
+fn draw_arrow(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    start: &NVector3,
+    end: &NVector3,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let a = Vec3::new(start.x as f32, start.y as f32, start.z as f32);
+    let b = Vec3::new(end.x as f32, end.y as f32, end.z as f32);
+    with_debug!(graphics, |dd| dd.draw_arrow(a, b, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawPoint"),
+    c
+)]
+fn draw_point(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    pos: &NVector3,
+    size: f32,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let p = Vec3::new(pos.x as f32, pos.y as f32, pos.z as f32);
+    with_debug!(graphics, |dd| dd.draw_point(p, size, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCircle"),
+    c
+)]
+fn draw_circle(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    center: &NVector3,
+    radius: f32,
+    normal: &NVector3,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
+    let n = Vec3::new(normal.x as f32, normal.y as f32, normal.z as f32);
+    with_debug!(graphics, |dd| dd.draw_circle(c, radius, n, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawSphere"),
+    c
+)]
+fn draw_sphere(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    center: &NVector3,
+    radius: f32,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
+    with_debug!(graphics, |dd| dd.draw_sphere(c, radius, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawGlobe"),
+    c
+)]
+fn draw_globe(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    center: &NVector3,
+    radius: f32,
+    lat_lines: u32,
+    lon_lines: u32,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
+    with_debug!(graphics, |dd| dd.draw_globe(c, radius, lat_lines, lon_lines, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawAabb"),
+    c
+)]
+fn draw_aabb(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    min: &NVector3,
+    max: &NVector3,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let mn = Vec3::new(min.x as f32, min.y as f32, min.z as f32);
+    let mx = Vec3::new(max.x as f32, max.y as f32, max.z as f32);
+    with_debug!(graphics, |dd| dd.draw_aabb(mn, mx, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawObb"),
+    c
+)]
+fn draw_obb(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    center: &NVector3,
+    half_extents: &NVector3,
+    rotation: &NQuaternion,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
+    let he = Vec3::new(half_extents.x as f32, half_extents.y as f32, half_extents.z as f32);
+    let r = Quat::from_xyzw(rotation.x as f32, rotation.y as f32, rotation.z as f32, rotation.w as f32);
+    with_debug!(graphics, |dd| dd.draw_obb(c, he, r, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCapsule"),
+    c
+)]
+fn draw_capsule(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    a: &NVector3,
+    b: &NVector3,
+    radius: f32,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let va = Vec3::new(a.x as f32, a.y as f32, a.z as f32);
+    let vb = Vec3::new(b.x as f32, b.y as f32, b.z as f32);
+    with_debug!(graphics, |dd| dd.draw_capsule(va, vb, radius, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCylinder"),
+    c
+)]
+fn draw_cylinder(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    center: &NVector3,
+    half_height: f32,
+    radius: f32,
+    axis: &NVector3,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32);
+    let ax = Vec3::new(axis.x as f32, axis.y as f32, axis.z as f32);
+    with_debug!(graphics, |dd| dd.draw_cylinder(c, half_height, radius, ax, colour.to_f32_array()));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCone"),
+    c
+)]
+fn draw_cone(
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    apex: &NVector3,
+    dir: &NVector3,
+    angle: f32,
+    length: f32,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let a = Vec3::new(apex.x as f32, apex.y as f32, apex.z as f32);
+    let d = Vec3::new(dir.x as f32, dir.y as f32, dir.z as f32);
+    with_debug!(graphics, |dd| dd.draw_cone(a, d, angle, length, colour.to_f32_array()));
+    Ok(())
+}
diff --git a/crates/eucalyptus-exports/src/engine.rs b/crates/eucalyptus-exports/src/engine.rs
new file mode 100644
index 0000000..dbeaff7
--- /dev/null
+++ b/crates/eucalyptus-exports/src/engine.rs
@@ -0,0 +1,45 @@
+use eucalyptus_core::ptr::{CommandBufferPtr, CommandBufferUnwrapped, WorldPtr};
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+
+pub mod shared {
+    use hecs::{Entity, World};
+    use eucalyptus_core::command::CommandBuffer;
+    use eucalyptus_core::scripting::native::DropbearNativeError;
+    use eucalyptus_core::scripting::result::DropbearNativeResult;
+    use eucalyptus_core::states::Label;
+
+    pub fn get_entity(world: &World, label: &str) -> DropbearNativeResult<u64> {
+        for (id, entity_label) in world.query::<(Entity, &Label)>().iter() {
+            if entity_label.as_str() == label {
+                return Ok(id.to_bits().get());
+            }
+        }
+        Err(DropbearNativeError::EntityNotFound)
+    }
+
+    pub fn quit(
+        command_buffer: &crossbeam_channel::Sender<CommandBuffer>,
+    ) -> DropbearNativeResult<()> {
+        command_buffer
+            .send(CommandBuffer::Quit)
+            .map_err(|_| DropbearNativeError::SendError)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.DropbearEngineNative", func = "getEntity",),
+    c
+)]
+fn get_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    label: String,
+) -> DropbearNativeResult<u64> {
+    shared::get_entity(&world, &label)
+}
+
+#[dropbear_macro::export(kotlin(class = "com.dropbear.DropbearEngineNative", func = "quit",), c)]
+fn quit(
+    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
+) -> DropbearNativeResult<()> {
+    shared::quit(command_buffer)
+}
diff --git a/crates/eucalyptus-exports/src/entity.rs b/crates/eucalyptus-exports/src/entity.rs
new file mode 100644
index 0000000..2f39ded
--- /dev/null
+++ b/crates/eucalyptus-exports/src/entity.rs
@@ -0,0 +1,85 @@
+use eucalyptus_core::hierarchy::{Children, Parent};
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::states::Label;
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.LabelNative", func = "labelExistsForEntity"),
+    c
+)]
+fn label_exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&Label>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.EntityRefNative", func = "getEntityLabel"),
+    c
+)]
+fn get_label(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<String> {
+    let label = world.get::<&Label>(entity)?.as_str().to_string();
+    Ok(label)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.EntityRefNative", func = "getChildren"),
+    c
+)]
+fn get_children(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<Vec<u64>> {
+    if let Ok(children) = world.query_one::<&Children>(entity).get() {
+        let entity_bytes = children
+            .children()
+            .iter()
+            .map(|c| c.to_bits().get())
+            .collect::<Vec<_>>();
+        Ok(entity_bytes)
+    } else {
+        Ok(vec![])
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.EntityRefNative", func = "getChildByLabel"),
+    c
+)]
+fn get_child_by_label(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    target: String,
+) -> 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>(*child) {
+                if label.as_str() == target {
+                    let found = child.clone();
+                    return Ok(Some(found.to_bits().get()));
+                }
+            } else {
+                continue;
+            }
+        }
+        Ok(None)
+    } else {
+        Ok(None)
+    }
+}
+
+#[dropbear_macro::export(kotlin(class = "com.dropbear.EntityRefNative", func = "getParent"), c)]
+fn get_parent(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<Option<u64>> {
+    if let Ok(parent) = world.query_one::<&Parent>(entity).get() {
+        Ok(Some(parent.parent().to_bits().get()))
+    } else {
+        Ok(None)
+    }
+}
diff --git a/crates/eucalyptus-exports/src/input.rs b/crates/eucalyptus-exports/src/input.rs
new file mode 100644
index 0000000..ddcd36c
--- /dev/null
+++ b/crates/eucalyptus-exports/src/input.rs
@@ -0,0 +1,361 @@
+use jni::objects::JObject;
+use eucalyptus_core::input::InputState;
+use eucalyptus_core::ptr::{CommandBufferPtr, CommandBufferUnwrapped, InputStatePtr};
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::math::NVector2;
+
+pub mod shared {
+    use crossbeam_channel::Sender;
+    use dropbear_engine::winit::event::MouseButton;
+    use eucalyptus_core::command::{CommandBuffer, WindowCommand};
+    use eucalyptus_core::input::InputState;
+    use eucalyptus_core::scripting::native::DropbearNativeError;
+    use eucalyptus_core::scripting::result::DropbearNativeResult;
+    use eucalyptus_core::utils::keycode_from_ordinal;
+    use crate::math::NVector2;
+
+    fn map_int_to_gamepad_button(ordinal: i32) -> Option<dropbear_engine::gilrs::Button> {
+        match ordinal {
+            0 => Some(dropbear_engine::gilrs::Button::Unknown),
+            1 => Some(dropbear_engine::gilrs::Button::South),
+            2 => Some(dropbear_engine::gilrs::Button::East),
+            3 => Some(dropbear_engine::gilrs::Button::North),
+            4 => Some(dropbear_engine::gilrs::Button::West),
+            5 => Some(dropbear_engine::gilrs::Button::C),
+            6 => Some(dropbear_engine::gilrs::Button::Z),
+            7 => Some(dropbear_engine::gilrs::Button::LeftTrigger),
+            8 => Some(dropbear_engine::gilrs::Button::RightTrigger),
+            9 => Some(dropbear_engine::gilrs::Button::LeftTrigger2),
+            10 => Some(dropbear_engine::gilrs::Button::RightTrigger2),
+            11 => Some(dropbear_engine::gilrs::Button::Select),
+            12 => Some(dropbear_engine::gilrs::Button::Start),
+            13 => Some(dropbear_engine::gilrs::Button::Mode),
+            14 => Some(dropbear_engine::gilrs::Button::LeftThumb),
+            15 => Some(dropbear_engine::gilrs::Button::RightThumb),
+            16 => Some(dropbear_engine::gilrs::Button::DPadUp),
+            17 => Some(dropbear_engine::gilrs::Button::DPadDown),
+            18 => Some(dropbear_engine::gilrs::Button::DPadLeft),
+            19 => Some(dropbear_engine::gilrs::Button::DPadRight),
+            _ => None,
+        }
+    }
+
+    pub fn get_gamepad_id(
+        input: &InputState,
+        target: usize,
+    ) -> Option<dropbear_engine::gilrs::GamepadId> {
+        input
+            .connected_gamepads
+            .iter()
+            .find(|g| usize::from(**g) == target)
+            .copied()
+    }
+
+    pub fn is_gamepad_button_pressed(
+        input: &InputState,
+        gamepad_id: u64,
+        button_ordinal: i32,
+    ) -> bool {
+        let Some(id) = get_gamepad_id(input, gamepad_id as usize) else {
+            return false;
+        };
+
+        if let Some(btn) = map_int_to_gamepad_button(button_ordinal) {
+            input.is_button_pressed(id, btn)
+        } else {
+            false
+        }
+    }
+
+    pub fn get_left_stick(input: &InputState, gamepad_id: u64) -> NVector2 {
+        let Some(id) = get_gamepad_id(input, gamepad_id as usize) else {
+            return NVector2 { x: 0.0, y: 0.0 };
+        };
+        let (x, y) = input.get_left_stick(id);
+        NVector2 {
+            x: x as f64,
+            y: y as f64,
+        }
+    }
+
+    pub fn get_right_stick(input: &InputState, gamepad_id: u64) -> NVector2 {
+        let Some(id) = get_gamepad_id(input, gamepad_id as usize) else {
+            return NVector2 { x: 0.0, y: 0.0 };
+        };
+        let (x, y) = input.get_right_stick(id);
+        NVector2 {
+            x: x as f64,
+            y: y as f64,
+        }
+    }
+
+    pub fn map_ordinal_to_mouse_button(ordinal: i32) -> Option<MouseButton> {
+        match ordinal {
+            0 => Some(MouseButton::Left),
+            1 => Some(MouseButton::Right),
+            2 => Some(MouseButton::Middle),
+            3 => Some(MouseButton::Back),
+            4 => Some(MouseButton::Forward),
+            ordinal if ordinal >= 0 => Some(MouseButton::Other(ordinal as u16)),
+            _ => None,
+        }
+    }
+
+    pub fn is_key_pressed(input: &InputState, key_ordinal: i32) -> bool {
+        if let Some(key) = keycode_from_ordinal(key_ordinal) {
+            input.is_key_pressed(key)
+        } else {
+            false
+        }
+    }
+
+    pub fn is_mouse_button_pressed(input: &InputState, btn_ordinal: i32) -> bool {
+        if let Some(btn) = map_ordinal_to_mouse_button(btn_ordinal) {
+            input.mouse_button.contains(&btn)
+        } else {
+            false
+        }
+    }
+
+    pub fn get_mouse_position(input: &InputState) -> NVector2 {
+        NVector2 {
+            x: input.mouse_pos.0,
+            y: input.mouse_pos.1,
+        }
+    }
+
+    pub fn get_mouse_delta(input: &InputState) -> NVector2 {
+        input
+            .mouse_delta
+            .map(NVector2::from)
+            .unwrap_or(NVector2 { x: 0.0, y: 0.0 })
+    }
+
+    pub fn get_last_mouse_pos(input: &InputState) -> NVector2 {
+        input
+            .last_mouse_pos
+            .map(NVector2::from)
+            .unwrap_or(NVector2 { x: 0.0, y: 0.0 })
+    }
+
+    pub fn set_cursor_locked(
+        input: &mut InputState,
+        sender: &Sender<CommandBuffer>,
+        locked: bool,
+    ) -> DropbearNativeResult<()> {
+        input.is_cursor_locked = locked;
+
+        sender
+            .send(CommandBuffer::WindowCommand(WindowCommand::WindowGrab(
+                locked,
+            )))
+            .map_err(|_| DropbearNativeError::SendError)?;
+
+        Ok(())
+    }
+
+    pub fn set_cursor_hidden(
+        input: &mut InputState,
+        sender: &Sender<CommandBuffer>,
+        hidden: bool,
+    ) -> DropbearNativeResult<()> {
+        input.is_cursor_hidden = hidden;
+        sender
+            .send(CommandBuffer::WindowCommand(WindowCommand::HideCursor(
+                hidden,
+            )))
+            .map_err(|_| DropbearNativeError::SendError)?;
+        Ok(())
+    }
+
+    pub fn get_connected_gamepads(input: &InputState) -> Vec<u64> {
+        input
+            .connected_gamepads
+            .iter()
+            .map(|id| Into::<usize>::into(*id) as u64)
+            .collect()
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "printInputState"
+    ),
+    c
+)]
+fn print_input_state(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<()> {
+    println!("Input State: {:?}", input);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.input.InputStateNative", func = "isKeyPressed"),
+    c
+)]
+fn is_key_pressed(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+    key_code: i32,
+) -> DropbearNativeResult<bool> {
+    Ok(shared::is_key_pressed(input, key_code))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "getMousePosition"
+    ),
+    c
+)]
+fn get_mouse_position(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<NVector2> {
+    Ok(shared::get_mouse_position(input))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "isMouseButtonPressed"
+    ),
+    c
+)]
+fn is_mouse_button_pressed(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+    button_ordinal: i32,
+) -> DropbearNativeResult<bool> {
+    Ok(shared::is_mouse_button_pressed(input, button_ordinal))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.input.InputStateNative", func = "getMouseDelta"),
+    c
+)]
+fn get_mouse_delta(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<NVector2> {
+    Ok(shared::get_mouse_delta(input))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.input.InputStateNative", func = "isCursorLocked"),
+    c
+)]
+fn is_cursor_locked(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<bool> {
+    Ok(input.is_cursor_locked)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "setCursorLocked"
+    ),
+    c
+)]
+fn set_cursor_locked(
+    #[dropbear_macro::define(CommandBufferPtr)]
+    command_buffer: &CommandBufferUnwrapped,
+    #[dropbear_macro::define(InputStatePtr)] input: &mut InputState,
+    locked: bool,
+) -> DropbearNativeResult<()> {
+    shared::set_cursor_locked(input, command_buffer, locked)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "getLastMousePos"
+    ),
+    c
+)]
+fn get_last_mouse_pos(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<NVector2> {
+    Ok(shared::get_last_mouse_pos(input))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.input.InputStateNative", func = "isCursorHidden"),
+    c
+)]
+fn is_cursor_hidden(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<bool> {
+    Ok(input.is_cursor_hidden)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "setCursorHidden"
+    ),
+    c
+)]
+fn set_cursor_hidden(
+    #[dropbear_macro::define(CommandBufferPtr)]
+    command_buffer: &CommandBufferUnwrapped,
+    #[dropbear_macro::define(InputStatePtr)] input: &mut InputState,
+    hidden: bool,
+) -> DropbearNativeResult<()> {
+    shared::set_cursor_hidden(input, command_buffer, hidden)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.InputStateNative",
+        func = "getConnectedGamepads"
+    ),
+    c
+)]
+fn get_connected_gamepads(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+) -> DropbearNativeResult<Vec<u64>> {
+    Ok(shared::get_connected_gamepads(input))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.GamepadNative",
+        func = "isGamepadButtonPressed"
+    ),
+    c
+)]
+fn is_button_pressed(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+    gamepad_id: u64,
+    button_ordinal: i32,
+) -> DropbearNativeResult<bool> {
+    Ok(shared::is_gamepad_button_pressed(input, gamepad_id, button_ordinal))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.GamepadNative",
+        func = "getLeftStickPosition"
+    ),
+    c
+)]
+fn get_left_stick_position(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+    gamepad_id: u64,
+) -> DropbearNativeResult<NVector2> {
+    Ok(shared::get_left_stick(input, gamepad_id))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.input.GamepadNative",
+        func = "getRightStickPosition"
+    ),
+    c
+)]
+fn get_right_stick_position(
+    #[dropbear_macro::define(InputStatePtr)] input: &InputState,
+    gamepad_id: u64,
+) -> DropbearNativeResult<NVector2> {
+    Ok(shared::get_right_stick(input, gamepad_id))
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/src/lib.rs b/crates/eucalyptus-exports/src/lib.rs
new file mode 100644
index 0000000..c487722
--- /dev/null
+++ b/crates/eucalyptus-exports/src/lib.rs
@@ -0,0 +1,44 @@
+use jni::Env;
+use jni::objects::JObject;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+
+// Re-export macros so the dropbear_macro::export expansion can find them at crate::convert_ptr! etc.
+pub use eucalyptus_core::convert_ptr;
+pub use eucalyptus_core::ffi_error_return;
+pub use eucalyptus_core::convert_jstring;
+pub use eucalyptus_core::convert_jlong_to_entity;
+
+pub mod animation;
+pub mod asset;
+pub mod camera;
+pub mod component;
+pub mod debug;
+pub mod entity;
+pub mod input;
+pub mod lighting;
+pub mod math;
+pub mod mesh;
+pub mod physics;
+pub mod primitives;
+pub mod properties;
+pub mod scene;
+pub mod scripting;
+pub mod transform;
+pub mod utils;
+pub mod engine;
+
+pub mod ptr {
+    pub use eucalyptus_core::ptr::*;
+}
+
+/// Trait that defines conversion from a Java object to a Rust struct.
+pub trait FromJObject {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized;
+}
+
+/// Converts a Rust object (struct or enum) into a java [JObject].
+pub trait ToJObject {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>>;
+}
diff --git a/crates/eucalyptus-exports/src/lighting.rs b/crates/eucalyptus-exports/src/lighting.rs
new file mode 100644
index 0000000..9745fdf
--- /dev/null
+++ b/crates/eucalyptus-exports/src/lighting.rs
@@ -0,0 +1,525 @@
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::types::{NColour, NVector3};
+use crate::{FromJObject, ToJObject};
+use dropbear_engine::entity::{EntityTransform, Transform};
+use dropbear_engine::lighting::{Light, LightType};
+use glam::{DQuat, DVec3};
+use hecs::{Entity, World};
+use jni::objects::{JObject, JValue};
+use jni::{Env, jni_sig, jni_str};
+
+const LIGHT_FORWARD_AXIS: DVec3 = DVec3::new(0.0, -1.0, 0.0);
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct NRange {
+    pub start: f32,
+    pub end: f32,
+}
+
+impl FromJObject for NRange {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/utils/Range"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let start = env
+            .get_field(obj, jni_str!("start"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;
+
+        let end = env
+            .get_field(obj, jni_str!("end"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as f32;
+
+        Ok(Self { start, end })
+    }
+}
+
+impl ToJObject for NRange {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/utils/Range"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let args = [
+            JValue::Double(self.start as f64),
+            JValue::Double(self.end as f64),
+        ];
+
+        env.new_object(&class, jni_sig!((double, double) -> void), &args)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct NAttenuation {
+    pub constant: f32,
+    pub linear: f32,
+    pub quadratic: f32,
+}
+
+impl FromJObject for NAttenuation {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/lighting/Attenuation"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let constant = env
+            .call_method(obj, jni_str!("getConstant"), jni_sig!(() -> float), &[])
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .f()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let linear = env
+            .call_method(obj, jni_str!("getLinear"), jni_sig!(() -> float), &[])
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .f()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let quadratic = env
+            .call_method(obj, jni_str!("getQuadratic"), jni_sig!(() -> float), &[])
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .f()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(Self { constant, linear, quadratic })
+    }
+}
+
+impl ToJObject for NAttenuation {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/lighting/Attenuation"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let args = [
+            JValue::Float(self.constant),
+            JValue::Float(self.linear),
+            JValue::Float(self.quadratic),
+        ];
+
+        env.new_object(&class, jni_sig!((float, float, float) -> void), &args)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+fn get_transform(world: &World, entity: Entity) -> DropbearNativeResult<Transform> {
+    if let Ok(et) = world.get::<&EntityTransform>(entity) {
+        Ok(et.sync())
+    } else if let Ok(t) = world.get::<&Transform>(entity) {
+        Ok(*t)
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+fn set_transform_position(
+    world: &mut World,
+    entity: Entity,
+    position: DVec3,
+) -> DropbearNativeResult<()> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        et.local_mut().position = position;
+        Ok(())
+    } else if let Ok(mut t) = world.get::<&mut Transform>(entity) {
+        t.position = position;
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+fn set_transform_rotation(
+    world: &mut World,
+    entity: Entity,
+    rotation: DQuat,
+) -> DropbearNativeResult<()> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        et.local_mut().rotation = rotation;
+        Ok(())
+    } else if let Ok(mut t) = world.get::<&mut Transform>(entity) {
+        t.rotation = rotation;
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "lightExistsForEntity"),
+    c
+)]
+fn light_exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&dropbear_engine::lighting::LightComponent>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getPosition"),
+    c
+)]
+fn get_position(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<NVector3> {
+    let transform = get_transform(world, entity)?;
+    Ok(NVector3::from(transform.position))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setPosition"),
+    c
+)]
+fn set_position(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    position: &NVector3,
+) -> DropbearNativeResult<()> {
+    set_transform_position(world, entity, (*position).into())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getDirection"),
+    c
+)]
+fn get_direction(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<NVector3> {
+    let transform = get_transform(world, entity)?;
+    let forward = LIGHT_FORWARD_AXIS;
+    let dir = (transform.rotation * forward).normalize_or_zero();
+    Ok(NVector3::from(dir))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setDirection"),
+    c
+)]
+fn set_direction(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    direction: &NVector3,
+) -> DropbearNativeResult<()> {
+    let dir: DVec3 = (*direction).into();
+    let desired = dir.normalize_or_zero();
+    if desired.length_squared() < 1e-12 {
+        return Err(DropbearNativeError::InvalidArgument);
+    }
+    let forward = LIGHT_FORWARD_AXIS;
+    let rotation = DQuat::from_rotation_arc(forward, desired);
+    set_transform_rotation(world, entity, rotation)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getColour"),
+    c
+)]
+fn get_colour(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<NColour> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(NColour::from_linear_rgb(light.component.colour))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setColour"),
+    c
+)]
+fn set_colour(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    colour: &NColour,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.colour = colour.to_linear_rgb();
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getLightType"),
+    c
+)]
+fn get_light_type(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<i32> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(light.component.light_type as i32)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setLightType"),
+    c
+)]
+fn set_light_type(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    light_type: i32,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.light_type = match light_type {
+        0 => LightType::Directional,
+        1 => LightType::Point,
+        2 => LightType::Spot,
+        _ => return Err(DropbearNativeError::InvalidArgument),
+    };
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getIntensity"),
+    c
+)]
+fn get_intensity(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<f64> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(light.component.intensity as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setIntensity"),
+    c
+)]
+fn set_intensity(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    intensity: f64,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.intensity = intensity as f32;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getAttenuation"),
+    c
+)]
+fn get_attenuation(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<NAttenuation> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(NAttenuation {
+        constant: light.component.attenuation.constant,
+        linear: light.component.attenuation.linear,
+        quadratic: light.component.attenuation.quadratic,
+    })
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setAttenuation"),
+    c
+)]
+fn set_attenuation(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    attenuation: &NAttenuation,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.attenuation.constant = attenuation.constant;
+    light.component.attenuation.linear = attenuation.linear;
+    light.component.attenuation.quadratic = attenuation.quadratic;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getEnabled"),
+    c
+)]
+fn get_enabled(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(light.component.enabled)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setEnabled"),
+    c
+)]
+fn set_enabled(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    enabled: bool,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.enabled = enabled;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getCutoffAngle"),
+    c
+)]
+fn get_cutoff_angle(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<f64> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(light.component.cutoff_angle as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setCutoffAngle"),
+    c
+)]
+fn set_cutoff_angle(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    cutoff_angle: f64,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.cutoff_angle = cutoff_angle as f32;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getOuterCutoffAngle"),
+    c
+)]
+fn get_outer_cutoff_angle(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<f64> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(light.component.outer_cutoff_angle as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setOuterCutoffAngle"),
+    c
+)]
+fn set_outer_cutoff_angle(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    outer_cutoff_angle: f64,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.outer_cutoff_angle = outer_cutoff_angle as f32;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getCastsShadows"),
+    c
+)]
+fn get_casts_shadows(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(light.component.cast_shadows)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setCastsShadows"),
+    c
+)]
+fn set_casts_shadows(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    casts_shadows: bool,
+) -> DropbearNativeResult<()> {
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.cast_shadows = casts_shadows;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "getDepth"),
+    c
+)]
+fn get_depth(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<NRange> {
+    let light = world
+        .get::<&Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(NRange {
+        start: light.component.depth.start,
+        end: light.component.depth.end,
+    })
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.lighting.LightNative", func = "setDepth"),
+    c
+)]
+fn set_depth(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    depth: &NRange,
+) -> DropbearNativeResult<()> {
+    if !(depth.start.is_finite() && depth.end.is_finite()) {
+        return Err(DropbearNativeError::InvalidArgument);
+    }
+    if depth.end <= depth.start {
+        return Err(DropbearNativeError::InvalidArgument);
+    }
+    let mut light = world
+        .get::<&mut Light>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    light.component.depth = depth.start..depth.end;
+    Ok(())
+}
diff --git a/crates/eucalyptus-exports/src/math.rs b/crates/eucalyptus-exports/src/math.rs
new file mode 100644
index 0000000..4457274
--- /dev/null
+++ b/crates/eucalyptus-exports/src/math.rs
@@ -0,0 +1,290 @@
+pub use eucalyptus_core::types::{NQuaternion, NTransform, NVector2, NVector3, NVector4};
+
+use glam::{DQuat, DVec3};
+use jni::objects::JObject;
+use jni::{jni_sig, jni_str, Env, JValue};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::{FromJObject, ToJObject};
+
+// --------------------------------------------------------------- NVector2 ---
+
+impl ToJObject for NVector2 {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let cls = env
+            .load_class(jni_str!("com/dropbear/math/Vector2d"))
+            .map_err(|_| DropbearNativeError::GenericError)?;
+
+        env.new_object(
+            cls,
+            jni_sig!((double, double) -> void),
+            &[JValue::Double(self.x), JValue::Double(self.y)],
+        )
+        .map_err(|_| DropbearNativeError::GenericError)
+    }
+}
+
+impl FromJObject for NVector2 {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let x = env
+            .get_field(obj, jni_str!("x"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let y = env
+            .get_field(obj, jni_str!("y"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        Ok(NVector2 { x, y })
+    }
+}
+
+// --------------------------------------------------------------- NVector3 ---
+
+impl FromJObject for NVector3 {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Vector3d"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let x = env
+            .get_field(obj, jni_str!("x"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let y = env
+            .get_field(obj, jni_str!("y"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let z = env
+            .get_field(obj, jni_str!("z"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(NVector3::new(x, y, z))
+    }
+}
+
+impl ToJObject for NVector3 {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Vector3d"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        env.new_object(
+            &class,
+            jni_sig!((double, double, double) -> void),
+            &[
+                JValue::Double(self.x),
+                JValue::Double(self.y),
+                JValue::Double(self.z),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// --------------------------------------------------------------- NVector4 ---
+
+impl FromJObject for NVector4 {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Vector4d"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let x = env
+            .get_field(obj, jni_str!("x"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let y = env
+            .get_field(obj, jni_str!("y"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let z = env
+            .get_field(obj, jni_str!("z"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let w = env
+            .get_field(obj, jni_str!("w"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(NVector4::new(x, y, z, w))
+    }
+}
+
+impl ToJObject for NVector4 {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Vector3d"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        env.new_object(
+            &class,
+            jni_sig!((double, double, double, double) -> void),
+            &[
+                JValue::Double(self.x),
+                JValue::Double(self.y),
+                JValue::Double(self.z),
+                JValue::Double(self.w),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// --------------------------------------------------------------- NQuaternion -
+
+impl ToJObject for NQuaternion {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Quaterniond"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        env.new_object(
+            &class,
+            jni_sig!((double, double, double, double) -> void),
+            &[
+                JValue::Double(self.x),
+                JValue::Double(self.y),
+                JValue::Double(self.z),
+                JValue::Double(self.w),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+impl FromJObject for NQuaternion {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Quaterniond"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let x = env
+            .get_field(obj, jni_str!("x"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let y = env
+            .get_field(obj, jni_str!("y"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let z = env
+            .get_field(obj, jni_str!("z"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+        let w = env
+            .get_field(obj, jni_str!("w"), jni_sig!(double))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .d()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(NQuaternion { x, y, z, w })
+    }
+}
+
+// --------------------------------------------------------------- NTransform --
+
+impl FromJObject for NTransform {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let pos_obj = env
+            .get_field(obj, jni_str!("position"), jni_sig!(com.dropbear.math.Vector3d))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let rot_obj = env
+            .get_field(obj, jni_str!("rotation"), jni_sig!(com.dropbear.math.Quaterniond))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let scale_obj = env
+            .get_field(obj, jni_str!("scale"), jni_sig!(com.dropbear.math.Vector3d))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let position: DVec3 = NVector3::from_jobject(env, &pos_obj)?.into();
+        let scale: DVec3 = NVector3::from_jobject(env, &scale_obj)?.into();
+
+        let mut get_double = |field| -> DropbearNativeResult<f64> {
+            env.get_field(&rot_obj, field, jni_sig!(double))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .d()
+                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
+        };
+
+        let rx = get_double(jni_str!("x"))?;
+        let ry = get_double(jni_str!("y"))?;
+        let rz = get_double(jni_str!("z"))?;
+        let rw = get_double(jni_str!("w"))?;
+
+        let rotation = DQuat::from_xyzw(rx, ry, rz, rw);
+
+        Ok(NTransform {
+            position: position.into(),
+            rotation: rotation.into(),
+            scale: scale.into(),
+        })
+    }
+}
+
+impl ToJObject for NTransform {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/math/Transform"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let args = [
+            JValue::Double(self.position.x),
+            JValue::Double(self.position.y),
+            JValue::Double(self.position.z),
+            JValue::Double(self.rotation.x),
+            JValue::Double(self.rotation.y),
+            JValue::Double(self.rotation.z),
+            JValue::Double(self.rotation.w),
+            JValue::Double(self.scale.x),
+            JValue::Double(self.scale.y),
+            JValue::Double(self.scale.z),
+        ];
+
+        env.new_object(
+            &class,
+            jni_sig!((double, double, double, double, double, double, double, double, double, double) -> void),
+            &args,
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
diff --git a/crates/eucalyptus-exports/src/mesh.rs b/crates/eucalyptus-exports/src/mesh.rs
new file mode 100644
index 0000000..10dbc6f
--- /dev/null
+++ b/crates/eucalyptus-exports/src/mesh.rs
@@ -0,0 +1,224 @@
+use eucalyptus_core::ptr::{AssetRegistryPtr, AssetRegistryUnwrapped, GraphicsContextPtr, WorldPtr};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use dropbear_engine::asset::{AssetRegistry, Handle};
+use dropbear_engine::entity::MeshRenderer;
+use dropbear_engine::graphics::SharedGraphicsContext;
+use dropbear_engine::model::Model;
+use dropbear_engine::texture::Texture;
+use std::collections::HashSet;
+use std::sync::Arc;
+
+fn model_for_renderer(registry: &AssetRegistry, renderer: &MeshRenderer) -> Option<Arc<Model>> {
+    registry.get_model(renderer.model())
+}
+
+fn matches_material_label(material: &dropbear_engine::model::Material, target: &str) -> bool {
+    material.texture_tag.as_deref() == Some(target) || material.name == target
+}
+
+fn resolve_target_material_index(model: &Model, target_identifier: &str) -> Option<usize> {
+    model
+        .materials
+        .iter()
+        .position(|mat| matches_material_label(mat, target_identifier))
+}
+
+fn resolve_target_material_name(model: &Model, target_identifier: &str) -> Option<String> {
+    model
+        .materials
+        .iter()
+        .find(|mat| matches_material_label(mat, target_identifier))
+        .map(|mat| mat.name.clone())
+}
+
+#[dropbear_macro::export(kotlin(
+    class = "com.dropbear.components.MeshRendererNative",
+    func = "meshRendererExistsForEntity"
+))]
+fn mesh_renderer_exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&MeshRenderer>(entity).is_ok())
+}
+
+#[dropbear_macro::export(kotlin(
+    class = "com.dropbear.components.MeshRendererNative",
+    func = "getModel"
+))]
+fn get_model(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<u64> {
+    if let Ok(mesh) = world.get::<&MeshRenderer>(entity) {
+        Ok(mesh.model().id)
+    } else {
+        Err(DropbearNativeError::NoSuchComponent)
+    }
+}
+
+#[dropbear_macro::export(kotlin(
+    class = "com.dropbear.components.MeshRendererNative",
+    func = "setModel"
+))]
+fn set_model(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    model_handle: u64,
+) -> DropbearNativeResult<()> {
+    let handle = Handle::new(model_handle);
+    if asset.read().get_model(handle).is_none() {
+        return Err(DropbearNativeError::InvalidHandle);
+    }
+
+    if let Ok(mut mesh) = world.get::<&mut MeshRenderer>(entity) {
+        mesh.set_model(handle);
+        Ok(())
+    } else {
+        Err(DropbearNativeError::NoSuchComponent)
+    }
+}
+
+#[dropbear_macro::export(kotlin(
+    class = "com.dropbear.components.MeshRendererNative",
+    func = "getAllTextureIds"
+))]
+fn get_all_texture_ids(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<Vec<u64>> {
+    let reader = asset.read();
+    let renderer = world
+        .get::<&MeshRenderer>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    let model =
+        model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
+
+    let mut ids = HashSet::new();
+    let mut push_handle = |handle: &Handle<Texture>| {
+        ids.insert(handle.id);
+    };
+
+    for material in &model.materials {
+        push_handle(&material.diffuse_texture);
+        if let Some(tex) = &material.normal_texture {
+            push_handle(tex);
+        }
+        if let Some(tex) = &material.emissive_texture {
+            push_handle(tex);
+        }
+        if let Some(tex) = &material.metallic_roughness_texture {
+            push_handle(tex);
+        }
+        if let Some(tex) = &material.occlusion_texture {
+            push_handle(tex);
+        }
+    }
+
+    Ok(ids.into_iter().collect())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.MeshRendererNative", func = "getTexture"),
+    c
+)]
+fn get_texture(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    material_name: String,
+) -> DropbearNativeResult<Option<u64>> {
+    let reader = asset.read();
+    let renderer = world
+        .get::<&MeshRenderer>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    let model =
+        model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
+    let idx = match resolve_target_material_index(&model, &material_name) {
+        Some(value) => value,
+        None => return Ok(None),
+    };
+    let material = model
+        .materials
+        .get(idx)
+        .ok_or(DropbearNativeError::InvalidArgument)?;
+
+    Ok(Some(material.diffuse_texture.id))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.MeshRendererNative",
+        func = "setTextureOverride"
+    ),
+    c
+)]
+fn set_texture_override(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    material_name: String,
+    texture_handle: u64,
+) -> DropbearNativeResult<()> {
+    let reader = asset.read();
+    let renderer = world
+        .get::<&MeshRenderer>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    let model =
+        model_for_renderer(&reader, &renderer).ok_or(DropbearNativeError::AssetNotFound)?;
+    let _ = resolve_target_material_name(&model, &material_name)
+        .ok_or(DropbearNativeError::InvalidArgument)?;
+
+    let handle = Handle::<Texture>::new(texture_handle);
+    if reader.get_texture(handle).is_none() {
+        return Err(DropbearNativeError::InvalidHandle);
+    }
+
+    let mut renderer = world
+        .get::<&mut MeshRenderer>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    if let Some(v) = renderer.material_snapshot.get_mut(&material_name) {
+        v.diffuse_texture = handle;
+    } else {
+        return Err(DropbearNativeError::InvalidArgument);
+    }
+
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.MeshRendererNative",
+        func = "setMaterialTint"
+    ),
+    c
+)]
+fn set_material_tint(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(AssetRegistryPtr)] asset: &AssetRegistryUnwrapped,
+    #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    material_name: String,
+    r: f32,
+    g: f32,
+    b: f32,
+    a: f32,
+) -> DropbearNativeResult<()> {
+    let _ = asset;
+    let mut renderer = world
+        .get::<&mut MeshRenderer>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    let material = renderer
+        .material_snapshot
+        .get_mut(&material_name)
+        .ok_or(DropbearNativeError::InvalidArgument)?;
+
+    material.base_colour = [r, g, b, a];
+    material.sync_uniform(graphics);
+    Ok(())
+}
diff --git a/crates/eucalyptus-exports/src/physics/collider.rs b/crates/eucalyptus-exports/src/physics/collider.rs
new file mode 100644
index 0000000..b509578
--- /dev/null
+++ b/crates/eucalyptus-exports/src/physics/collider.rs
@@ -0,0 +1,676 @@
+use glam::{DQuat, Vec3};
+use jni::{jni_sig, jni_str, Env, JValue};
+use jni::objects::JObject;
+use jni::sys::jdouble;
+use eucalyptus_core::physics::collider::ColliderShape;
+use eucalyptus_core::physics::PhysicsState;
+use eucalyptus_core::ptr::PhysicsStatePtr;
+use eucalyptus_core::rapier3d::geometry::{SharedShape, TypedShape};
+use eucalyptus_core::rapier3d::math::{Rotation, Vector};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::{FromJObject, ToJObject};
+use crate::physics::NCollider;
+use crate::math::NVector3;
+use crate::physics::collider::shared::{get_collider, get_collider_mut};
+
+mod group {
+    use eucalyptus_core::physics::collider::ColliderGroup;
+    use eucalyptus_core::physics::PhysicsState;
+    use eucalyptus_core::ptr::{PhysicsStatePtr, WorldPtr};
+    use eucalyptus_core::scripting::native::DropbearNativeError;
+    use eucalyptus_core::scripting::result::DropbearNativeResult;
+    use crate::physics::{IndexNative, NCollider};
+
+    #[dropbear_macro::export(
+        kotlin(
+            class = "com.dropbear.physics.ColliderGroupNative",
+            func = "colliderGroupExistsForEntity"
+        ),
+        c
+    )]
+    fn exists_for_entity(
+        #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+        #[dropbear_macro::entity] entity: hecs::Entity,
+    ) -> DropbearNativeResult<bool> {
+        Ok(world.get::<&ColliderGroup>(entity).is_ok())
+    }
+
+    #[dropbear_macro::export(
+        kotlin(
+            class = "com.dropbear.physics.ColliderGroupNative",
+            func = "getColliderGroupColliders"
+        ),
+        c
+    )]
+    fn get_colliders(
+        #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+        #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+        #[dropbear_macro::entity] entity: hecs::Entity,
+    ) -> DropbearNativeResult<Vec<NCollider>> {
+        if world.get::<&ColliderGroup>(entity).is_ok() {
+            let handles_opt = physics
+                .entity_label_map
+                .get(&entity)
+                .and_then(|label| physics.colliders_entity_map.get(label));
+
+            let mut colliders: Vec<NCollider> = Vec::new();
+
+            if let Some(handles) = handles_opt {
+                for (_, handle) in handles {
+                    let (idx, generation) = handle.into_raw_parts();
+
+                    let col = NCollider {
+                        index: IndexNative {
+                            index: idx,
+                            generation,
+                        },
+                        entity_id: entity.to_bits().get(),
+                        id: idx,
+                    };
+                    colliders.push(col);
+                }
+            }
+
+            Ok(colliders)
+        } else {
+            Err(DropbearNativeError::MissingComponent)?
+        }
+    }
+}
+
+impl ToJObject for ColliderShape {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        match self {
+            ColliderShape::Box { half_extents } => {
+                let vec_cls = env
+                    .load_class(jni_str!("com/dropbear/math/Vector3d"))
+                    .map_err(|e| {
+                        eprintln!("[JNI Error] Vector3d class not found: {:?}", e);
+                        DropbearNativeError::JNIClassNotFound
+                    })?;
+
+                let vec_obj = env
+                    .new_object(
+                        &vec_cls,
+                        jni_sig!("(DDD)V"),
+                        &[
+                            JValue::Double(half_extents.x as jdouble),
+                            JValue::Double(half_extents.y as jdouble),
+                            JValue::Double(half_extents.z as jdouble),
+                        ],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+                let cls = env
+                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Box"))
+                    .map_err(|e| {
+                        eprintln!("[JNI Error] ColliderShape$Box class not found: {:?}", e);
+                        DropbearNativeError::JNIClassNotFound
+                    })?;
+
+                let obj = env
+                    .new_object(
+                        &cls,
+                        jni_sig!("(Lcom/dropbear/math/Vector3d;)V"),
+                        &[JValue::Object(&vec_obj)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+                Ok(obj)
+            }
+            ColliderShape::Sphere { radius } => {
+                let cls = env
+                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Sphere"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                let obj = env
+                    .new_object(&cls, jni_sig!("(F)V"), &[JValue::Float(*radius)])
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+                Ok(obj)
+            }
+            ColliderShape::Capsule {
+                half_height,
+                radius,
+            } => {
+                let cls = env
+                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Capsule"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                let obj = env
+                    .new_object(
+                        &cls,
+                        jni_sig!("(FF)V"),
+                        &[JValue::Float(*half_height), JValue::Float(*radius)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+                Ok(obj)
+            }
+            ColliderShape::Cylinder {
+                half_height,
+                radius,
+            } => {
+                let cls = env
+                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Cylinder"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                // let ctor = env.get_method_id(&cls, "<init>", "(FF)V")
+                //     .map_err(|_| DropbearNativeError::JNIMethodNotFound)?;
+
+                let obj = env
+                    .new_object(
+                        &cls,
+                        jni_sig!("(FF)V"),
+                        &[JValue::Float(*half_height), JValue::Float(*radius)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+                Ok(obj)
+            }
+            ColliderShape::Cone {
+                half_height,
+                radius,
+            } => {
+                let cls = env
+                    .load_class(jni_str!("com/dropbear/physics/ColliderShape$Cone"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                let obj = env
+                    .new_object(
+                        &cls,
+                        jni_sig!("(FF)V"),
+                        &[JValue::Float(*half_height), JValue::Float(*radius)],
+                    )
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+                Ok(obj)
+            }
+        }
+    }
+}
+
+impl FromJObject for ColliderShape {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized,
+    {
+        let is_instance = |env: &mut Env,
+                           obj: &JObject,
+                           class_name: &jni::strings::JNIStr|
+                           -> bool { env.is_instance_of(obj, class_name).unwrap_or(false) };
+
+        if is_instance(env, obj, jni_str!("com/dropbear/physics/ColliderShape$Box")) {
+            let vec_obj_val = env
+                .get_field(
+                    obj,
+                    jni_str!("halfExtents"),
+                    jni_sig!("Lcom/dropbear/math/Vector3d;"),
+                )
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+            let vec_obj = vec_obj_val
+                .l()
+                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+            let x = env
+                .get_field(&vec_obj, jni_str!("x"), jni_sig!("D"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .d()
+                .unwrap_or(0.0);
+            let y = env
+                .get_field(&vec_obj, jni_str!("y"), jni_sig!("D"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .d()
+                .unwrap_or(0.0);
+            let z = env
+                .get_field(&vec_obj, jni_str!("z"), jni_sig!("D"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .d()
+                .unwrap_or(0.0);
+
+            return Ok(ColliderShape::Box {
+                half_extents: Vec3::from([x as f32, y as f32, z as f32]),
+            });
+        }
+
+        if is_instance(
+            env,
+            obj,
+            jni_str!("com/dropbear/physics/ColliderShape$Sphere"),
+        ) {
+            let radius = env
+                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+
+            return Ok(ColliderShape::Sphere { radius });
+        }
+
+        if is_instance(
+            env,
+            obj,
+            jni_str!("com/dropbear/physics/ColliderShape$Capsule"),
+        ) {
+            let hh = env
+                .get_field(obj, jni_str!("halfHeight"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+            let r = env
+                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+
+            return Ok(ColliderShape::Capsule {
+                half_height: hh,
+                radius: r,
+            });
+        }
+
+        if is_instance(
+            env,
+            obj,
+            jni_str!("com/dropbear/physics/ColliderShape$Cylinder"),
+        ) {
+            let hh = env
+                .get_field(obj, jni_str!("halfHeight"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+            let r = env
+                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+
+            return Ok(ColliderShape::Cylinder {
+                half_height: hh,
+                radius: r,
+            });
+        }
+
+        if is_instance(
+            env,
+            obj,
+            jni_str!("com/dropbear/physics/ColliderShape$Cone"),
+        ) {
+            let hh = env
+                .get_field(obj, jni_str!("halfHeight"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+            let r = env
+                .get_field(obj, jni_str!("radius"), jni_sig!("F"))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .f()
+                .unwrap_or(0.0);
+
+            return Ok(ColliderShape::Cone {
+                half_height: hh,
+                radius: r,
+            });
+        }
+
+        Err(DropbearNativeError::GenericError)
+    }
+}
+
+// -------------------------------- collider ------------------------------------
+
+pub mod shared {
+    use eucalyptus_core::physics::PhysicsState;
+    use eucalyptus_core::types::NCollider;
+    use eucalyptus_core::rapier3d::prelude::ColliderHandle;
+
+    pub fn get_collider_mut<'a>(
+        physics: &'a mut PhysicsState,
+        ffi: &NCollider,
+    ) -> Option<&'a mut eucalyptus_core::rapier3d::prelude::Collider> {
+        let handle = ColliderHandle::from_raw_parts(ffi.index.index, ffi.index.generation);
+        physics.colliders.get_mut(handle)
+    }
+
+    pub fn get_collider<'a>(
+        physics: &'a PhysicsState,
+        ffi: &NCollider,
+    ) -> Option<&'a eucalyptus_core::rapier3d::prelude::Collider> {
+        let handle = ColliderHandle::from_raw_parts(ffi.index.index, ffi.index.generation);
+        physics.colliders.get(handle)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderShape"
+    ),
+    c
+)]
+fn get_collider_shape(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<ColliderShape> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+
+    let rapier_shape = collider.shape();
+    let my_shape = match rapier_shape.as_typed_shape() {
+        TypedShape::Cuboid(c) => {
+            let he = c.half_extents;
+            ColliderShape::Box {
+                half_extents: glam::Vec3::from([he.x, he.y, he.z]),
+            }
+        }
+        TypedShape::Ball(b) => ColliderShape::Sphere { radius: b.radius },
+        TypedShape::Capsule(c) => {
+            let height = c.segment.length();
+            ColliderShape::Capsule {
+                half_height: height * 0.5,
+                radius: c.radius,
+            }
+        }
+        TypedShape::Cylinder(c) => ColliderShape::Cylinder {
+            half_height: c.half_height,
+            radius: c.radius,
+        },
+        TypedShape::Cone(c) => ColliderShape::Cone {
+            half_height: c.half_height,
+            radius: c.radius,
+        },
+        _ => return Err(DropbearNativeError::InvalidArgument),
+    };
+
+    Ok(my_shape)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderShape"
+    ),
+    c
+)]
+fn set_collider_shape(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    shape: &ColliderShape,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+
+    let new_shape = match shape {
+        ColliderShape::Box { half_extents } => SharedShape::cuboid(
+            half_extents.x as f32,
+            half_extents.y as f32,
+            half_extents.z as f32,
+        ),
+        ColliderShape::Sphere { radius } => SharedShape::ball(*radius),
+        ColliderShape::Capsule {
+            half_height,
+            radius,
+        } => SharedShape::capsule_y(*half_height, *radius),
+        ColliderShape::Cylinder {
+            half_height,
+            radius,
+        } => SharedShape::cylinder(*half_height, *radius),
+        ColliderShape::Cone {
+            half_height,
+            radius,
+        } => SharedShape::cone(*half_height, *radius),
+    };
+
+    collider.set_shape(new_shape);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderDensity"
+    ),
+    c
+)]
+fn get_collider_density(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<f64> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    Ok(collider.density() as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderDensity"
+    ),
+    c
+)]
+fn set_collider_density(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    density: f64,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    collider.set_density(density as f32);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderFriction"
+    ),
+    c
+)]
+fn get_collider_friction(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<f64> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    Ok(collider.friction() as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderFriction"
+    ),
+    c
+)]
+fn set_collider_friction(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    friction: f64,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    collider.set_friction(friction as f32);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderRestitution"
+    ),
+    c
+)]
+fn get_collider_restitution(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<f64> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    Ok(collider.restitution() as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderRestitution"
+    ),
+    c
+)]
+fn set_collider_restitution(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    restitution: f64,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    collider.set_restitution(restitution as f32);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderMass"
+    ),
+    c
+)]
+fn get_collider_mass(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<f64> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    Ok(collider.mass() as f64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderMass"
+    ),
+    c
+)]
+fn set_collider_mass(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    mass: f64,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    collider.set_mass(mass as f32);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderIsSensor"
+    ),
+    c
+)]
+fn get_collider_is_sensor(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<bool> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    Ok(collider.is_sensor())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderIsSensor"
+    ),
+    c
+)]
+fn set_collider_is_sensor(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    is_sensor: bool,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    collider.set_sensor(is_sensor);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderTranslation"
+    ),
+    c
+)]
+fn get_collider_translation(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<NVector3> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    let t: Vector = collider.translation();
+    Ok(NVector3::new(t.x as f64, t.y as f64, t.z as f64))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderTranslation"
+    ),
+    c
+)]
+fn set_collider_translation(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    translation: &NVector3,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    let t = Vector::new(
+        translation.x as f32,
+        translation.y as f32,
+        translation.z as f32,
+    );
+    collider.set_translation(t);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "getColliderRotation"
+    ),
+    c
+)]
+fn get_collider_rotation(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider: &NCollider,
+) -> DropbearNativeResult<NVector3> {
+    let collider =
+        get_collider(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    let r: Rotation = collider.rotation();
+    let q = DQuat::from_xyzw(r.x as f64, r.y as f64, r.z as f64, r.w as f64);
+    let euler = q.to_euler(glam::EulerRot::XYZ);
+    Ok(NVector3::new(euler.0, euler.1, euler.2))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.ColliderNative",
+        func = "setColliderRotation"
+    ),
+    c
+)]
+fn set_collider_rotation(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    collider: &NCollider,
+    rotation: &NVector3,
+) -> DropbearNativeResult<()> {
+    let collider =
+        get_collider_mut(physics, &collider).ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+    let q = DQuat::from_euler(glam::EulerRot::XYZ, rotation.x, rotation.y, rotation.z);
+    let r = Rotation::from_array([q.w as f32, q.x as f32, q.y as f32, q.z as f32]);
+    collider.set_rotation(r);
+    Ok(())
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/src/physics/kcc.rs b/crates/eucalyptus-exports/src/physics/kcc.rs
new file mode 100644
index 0000000..e46fd1c
--- /dev/null
+++ b/crates/eucalyptus-exports/src/physics/kcc.rs
@@ -0,0 +1,569 @@
+use eucalyptus_core::physics::kcc::{CharacterMovementResult, KCC};
+use eucalyptus_core::physics::PhysicsState;
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::rapier3d;
+use eucalyptus_core::rapier3d::dynamics::RigidBodyType;
+use eucalyptus_core::rapier3d::math::Rotation;
+use eucalyptus_core::rapier3d::pipeline::QueryFilter;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::states::Label;
+use crate::math::{NTransform, NVector3, NQuaternion};
+use crate::physics::{IndexNative, NCollider, NShapeCastStatus, CharacterCollisionArray};
+
+pub mod shared {
+    use glam::{DQuat, DVec3};
+    use hecs::{Entity, World};
+    use eucalyptus_core::physics::collider::ColliderGroup;
+    use eucalyptus_core::physics::kcc::KCC;
+    use eucalyptus_core::rapier3d;
+    use eucalyptus_core::rapier3d::control::CharacterCollision;
+    use eucalyptus_core::rapier3d::na::Quaternion;
+    use eucalyptus_core::scripting::native::DropbearNativeError;
+    use crate::math::{NTransform, NVector3};
+    use crate::physics::{IndexNative, NCollider, NShapeCastStatus};
+    use super::*;
+
+    fn get_collision_from_world(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<CharacterCollision> {
+        let kcc = world
+            .get::<&KCC>(entity)
+            .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+        kcc.collisions
+            .iter()
+            .copied()
+            .find(|c| {
+                let (idx, generation) = c.handle.into_raw_parts();
+                idx == collision_handle.index && generation == collision_handle.generation
+            })
+            .ok_or(DropbearNativeError::NoSuchHandle)
+    }
+
+    fn collider_ffi_from_handle(
+        world: &World,
+        handle: rapier3d::prelude::ColliderHandle,
+    ) -> Option<NCollider> {
+        let (idx, generation) = handle.into_raw_parts();
+
+        for (entity, group) in world.query::<(Entity, &ColliderGroup)>().iter() {
+            if group.colliders.iter().any(|c| c.id == idx) {
+                return Some(NCollider {
+                    index: IndexNative {
+                        index: idx,
+                        generation,
+                    },
+                    entity_id: entity.to_bits().get(),
+                    id: idx,
+                });
+            }
+        }
+
+        None
+    }
+
+    pub fn get_collider(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NCollider> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        collider_ffi_from_handle(world, collision.handle)
+            .ok_or(DropbearNativeError::PhysicsObjectNotFound)
+    }
+
+    pub fn get_character_position(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NTransform> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+
+        let iso = collision.character_pos;
+        let t = iso.translation;
+        let rot = iso.rotation;
+        let q: Quaternion<f32> = Quaternion::from(rot);
+
+        Ok(NTransform {
+            position: DVec3::new(t.x as f64, t.y as f64, t.z as f64).into(),
+            rotation: DQuat::from_xyzw(q.i as f64, q.j as f64, q.k as f64, q.w as f64).into(),
+            scale: DVec3::ONE.into(),
+        })
+    }
+
+    pub fn get_translation_applied(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NVector3> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        let v = collision.translation_applied;
+        Ok(NVector3 {
+            x: v.x as f64,
+            y: v.y as f64,
+            z: v.z as f64,
+        })
+    }
+
+    pub fn get_translation_remaining(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NVector3> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        let v = collision.translation_remaining;
+        Ok(NVector3 {
+            x: v.x as f64,
+            y: v.y as f64,
+            z: v.z as f64,
+        })
+    }
+
+    pub fn get_time_of_impact(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<f64> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        Ok(collision.hit.time_of_impact as f64)
+    }
+
+    pub fn get_witness1(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NVector3> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        let p = collision.hit.witness1;
+        Ok(NVector3 {
+            x: p.x as f64,
+            y: p.y as f64,
+            z: p.z as f64,
+        })
+    }
+
+    pub fn get_witness2(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NVector3> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        let p = collision.hit.witness2;
+        Ok(NVector3 {
+            x: p.x as f64,
+            y: p.y as f64,
+            z: p.z as f64,
+        })
+    }
+
+    pub fn get_normal1(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NVector3> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        let n = collision.hit.normal1;
+        Ok(NVector3 {
+            x: n.x as f64,
+            y: n.y as f64,
+            z: n.z as f64,
+        })
+    }
+
+    pub fn get_normal2(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NVector3> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        let n = collision.hit.normal2;
+        Ok(NVector3 {
+            x: n.x as f64,
+            y: n.y as f64,
+            z: n.z as f64,
+        })
+    }
+
+    pub fn get_status(
+        world: &World,
+        entity: Entity,
+        collision_handle: &IndexNative,
+    ) -> DropbearNativeResult<NShapeCastStatus> {
+        let collision = get_collision_from_world(world, entity, collision_handle)?;
+        Ok(collision.hit.status.into())
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getCollider"
+    ),
+    c
+)]
+fn get_character_collision_collider(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NCollider> {
+    shared::get_collider(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getCharacterPosition"
+    ),
+    c
+)]
+fn get_character_collision_position(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NTransform> {
+    shared::get_character_position(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getTranslationApplied"
+    ),
+    c
+)]
+fn get_character_collision_translation_applied(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_translation_applied(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getTranslationRemaining"
+    ),
+    c
+)]
+fn get_character_collision_translation_remaining(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_translation_remaining(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getTimeOfImpact"
+    ),
+    c
+)]
+fn get_character_collision_time_of_impact(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<f64> {
+    shared::get_time_of_impact(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getWitness1"
+    ),
+    c
+)]
+fn get_character_collision_witness1(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_witness1(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getWitness2"
+    ),
+    c
+)]
+fn get_character_collision_witness2(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_witness2(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getNormal1"
+    ),
+    c
+)]
+fn get_character_collision_normal1(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_normal1(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getNormal2"
+    ),
+    c
+)]
+fn get_character_collision_normal2(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_normal2(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.CharacterCollisionNative",
+        func = "getStatus"
+    ),
+    c
+)]
+fn get_character_collision_status(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    collision_handle: &IndexNative,
+) -> DropbearNativeResult<NShapeCastStatus> {
+    shared::get_status(world, entity, collision_handle)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.KinematicCharacterControllerNative",
+        func = "existsForEntity"
+    ),
+    c
+)]
+fn kcc_exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&KCC>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.KinematicCharacterControllerNative",
+        func = "moveCharacter"
+    ),
+    c
+)]
+fn move_character(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(crate::ptr::PhysicsStatePtr)] physics_state: &mut PhysicsState,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    translation: &NVector3,
+    delta_time: f64,
+) -> DropbearNativeResult<()> {
+    if let Ok((label, kcc)) = world.query_one::<(&Label, &mut KCC)>(entity).get() {
+        let rigid_body_handle = physics_state
+            .bodies_entity_map
+            .get(label)
+            .ok_or(DropbearNativeError::NoSuchHandle)?;
+
+        let (body_type, body_pos) = {
+            let body = physics_state
+                .bodies
+                .get(*rigid_body_handle)
+                .ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+            (body.body_type(), *body.position())
+        };
+
+        if body_type != RigidBodyType::KinematicPositionBased {
+            return Ok(()); // soft error, just tell the user
+        }
+
+        let collider_handles = physics_state
+            .colliders_entity_map
+            .get(label)
+            .ok_or(DropbearNativeError::NoSuchHandle)?;
+        let (_, collider_handle) = collider_handles
+            .first()
+            .ok_or(DropbearNativeError::NoSuchHandle)?;
+        let collider = physics_state
+            .colliders
+            .get(*collider_handle)
+            .ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+
+        let character_pos = if let Some(pos_wrt_parent) = collider.position_wrt_parent() {
+            body_pos * (*pos_wrt_parent)
+        } else {
+            *collider.position()
+        };
+
+        let filter = QueryFilter::default()
+            .exclude_rigid_body(*rigid_body_handle)
+            .exclude_sensors();
+        let query_pipeline = physics_state.broad_phase.as_query_pipeline(
+            physics_state.narrow_phase.query_dispatcher(),
+            &physics_state.bodies,
+            &physics_state.colliders,
+            filter,
+        );
+
+        let movement = kcc.controller.move_shape(
+            delta_time as f32,
+            &query_pipeline,
+            collider.shape(),
+            &character_pos,
+            rapier3d::prelude::Vector::new(
+                translation.x as f32,
+                translation.y as f32,
+                translation.z as f32,
+            ),
+            |collision| {
+                if let Some(collisions) =
+                    physics_state.collision_events_to_deal_with.get_mut(&entity)
+                {
+                    collisions.push(collision)
+                } else {
+                    physics_state
+                        .collision_events_to_deal_with
+                        .insert(entity, vec![collision]);
+                }
+            },
+        );
+
+        if let Some(body) = physics_state.bodies.get_mut(*rigid_body_handle) {
+            let current_pos = body.translation();
+            let new_pos = current_pos + movement.translation;
+            body.set_next_kinematic_translation(new_pos);
+        }
+
+        kcc.movement = Some(CharacterMovementResult {
+            translation: movement.translation.into(),
+            grounded: movement.grounded,
+            is_sliding_down_slope: movement.is_sliding_down_slope,
+        });
+
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.KinematicCharacterControllerNative",
+        func = "setRotation"
+    ),
+    c
+)]
+fn set_rotation(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(crate::ptr::PhysicsStatePtr)] physics_state: &mut PhysicsState,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    rotation: &NQuaternion,
+) -> DropbearNativeResult<()> {
+    if let Ok((label, _)) = world.query_one::<(&Label, &KCC)>(entity).get() {
+        let rigid_body_handle = physics_state
+            .bodies_entity_map
+            .get(label)
+            .ok_or(DropbearNativeError::NoSuchHandle)?;
+
+        let body_type = {
+            let body = physics_state
+                .bodies
+                .get(*rigid_body_handle)
+                .ok_or(DropbearNativeError::PhysicsObjectNotFound)?;
+            body.body_type()
+        };
+
+        if body_type != RigidBodyType::KinematicPositionBased {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let len = (rotation.x * rotation.x
+            + rotation.y * rotation.y
+            + rotation.z * rotation.z
+            + rotation.w * rotation.w)
+            .sqrt();
+        let (x, y, z, w) = if len > 0.0 {
+            (
+                rotation.x / len,
+                rotation.y / len,
+                rotation.z / len,
+                rotation.w / len,
+            )
+        } else {
+            (0.0, 0.0, 0.0, 1.0)
+        };
+
+        if let Some(body) = physics_state.bodies.get_mut(*rigid_body_handle) {
+            let rot = Rotation::from_xyzw(x as f32, y as f32, z as f32, w as f32);
+            body.set_next_kinematic_rotation(rot);
+        }
+
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.KinematicCharacterControllerNative",
+        func = "getHit"
+    ),
+    c
+)]
+fn get_hit(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<CharacterCollisionArray> {
+    let kcc = world
+        .get::<&KCC>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    let mut collisions = Vec::with_capacity(kcc.collisions.len());
+    for collision in &kcc.collisions {
+        let (idx, generation) = collision.handle.into_raw_parts();
+        collisions.push(IndexNative {
+            index: idx,
+            generation,
+        });
+    }
+
+    Ok(CharacterCollisionArray {
+        entity_id: entity.to_bits().get(),
+        collisions,
+    })
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.KinematicCharacterControllerNative",
+        func = "getMovementResult"
+    ),
+    c
+)]
+fn get_movement_result(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<Option<CharacterMovementResult>> {
+    world
+        .get::<&KCC>(entity)
+        .map(|kcc| kcc.movement.clone())
+        .map(Ok)
+        .unwrap_or(Ok(None))
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/src/physics/mod.rs b/crates/eucalyptus-exports/src/physics/mod.rs
new file mode 100644
index 0000000..85bf507
--- /dev/null
+++ b/crates/eucalyptus-exports/src/physics/mod.rs
@@ -0,0 +1,504 @@
+pub use eucalyptus_core::types::{
+    CollisionEvent, CollisionEventType, ContactForceEvent, IndexNative, NCollider, NShapeCastHit,
+    NShapeCastStatus, RayHit, RigidBodyContext,
+};
+
+pub mod physics;
+
+use jni::objects::JObject;
+use jni::sys::jdouble;
+use jni::{jni_sig, jni_str, Env, JValue};
+use eucalyptus_core::physics::kcc::CharacterMovementResult;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::{FromJObject, ToJObject};
+use crate::math::NVector3;
+
+pub mod collider;
+pub mod kcc;
+pub mod rigidbody;
+
+// ----------------------------------------------------------- CharacterCollision
+
+/// Returned by `getHit` — an entity's KCC collision handles packed for JNI transfer.
+pub struct CharacterCollisionArray {
+    pub entity_id: u64,
+    pub collisions: Vec<IndexNative>,
+}
+
+// ------------------------------------------------------- NCollider JNI impls --
+
+impl FromJObject for NCollider {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let index_obj = env
+            .get_field(obj, jni_str!("index"), jni_sig!(com.dropbear.physics.Index))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let entity_obj = env
+            .get_field(obj, jni_str!("entity"), jni_sig!(com.dropbear.EntityId))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let id_val = env
+            .get_field(obj, jni_str!("id"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let entity_raw = env
+            .get_field(&entity_obj, jni_str!("raw"), jni_sig!(long))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .j()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let idx_val = env
+            .get_field(&index_obj, jni_str!("index"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let gen_val = env
+            .get_field(&index_obj, jni_str!("generation"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(NCollider {
+            index: IndexNative {
+                index: idx_val as u32,
+                generation: gen_val as u32,
+            },
+            entity_id: entity_raw as u64,
+            id: id_val as u32,
+        })
+    }
+}
+
+impl ToJObject for NCollider {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let collider_cls = env
+            .load_class(jni_str!("com/dropbear/physics/Collider"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let index_cls = env
+            .load_class(jni_str!("com/dropbear/physics/Index"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_cls = env
+            .load_class(jni_str!("com/dropbear/EntityId"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_obj = env
+            .new_object(
+                &entity_cls,
+                jni_sig!((long) -> void),
+                &[JValue::Long(self.entity_id as i64)],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        let index_obj = env
+            .new_object(
+                &index_cls,
+                jni_sig!((int, int) -> void),
+                &[
+                    JValue::Int(self.index.index as i32),
+                    JValue::Int(self.index.generation as i32),
+                ],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        let collider_obj = env
+            .new_object(
+                collider_cls,
+                jni_sig!((com.dropbear.physics.Index, com.dropbear.EntityId, int) -> void),
+                &[
+                    JValue::Object(&index_obj),
+                    JValue::Object(&entity_obj),
+                    JValue::Int(self.id as i32),
+                ],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(collider_obj)
+    }
+}
+
+// ----------------------------------------------- CollisionEventType JNI impl -
+
+impl ToJObject for CollisionEventType {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/CollisionEventType"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = match self {
+            CollisionEventType::Started => "Started",
+            CollisionEventType::Stopped => "Stopped",
+        };
+        let name_jstring = env
+            .new_string(name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        env.call_static_method(
+            class,
+            jni_str!("valueOf"),
+            jni_sig!((java.lang.String) -> com.dropbear.physics.CollisionEventType),
+            &[JValue::from(&name_jstring)],
+        )
+        .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
+        .l()
+        .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
+    }
+}
+
+// -------------------------------------------------- CollisionEvent JNI impl --
+
+impl ToJObject for CollisionEvent {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/CollisionEvent"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let event_type = self.event_type.to_jobject(env)?;
+        let collider1 = self.collider1.to_jobject(env)?;
+        let collider2 = self.collider2.to_jobject(env)?;
+
+        let flags = self.flags as i32;
+        env.new_object(
+            class,
+            jni_sig!("(Lcom/dropbear/physics/CollisionEventType;Lcom/dropbear/physics/Collider;Lcom/dropbear/physics/Collider;I)V"),
+            &[
+                JValue::Object(&event_type),
+                JValue::Object(&collider1),
+                JValue::Object(&collider2),
+                JValue::Int(flags),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// ----------------------------------------------- ContactForceEvent JNI impl --
+
+impl ToJObject for ContactForceEvent {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/ContactForceEvent"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let collider1 = self.collider1.to_jobject(env)?;
+        let collider2 = self.collider2.to_jobject(env)?;
+        let total_force = self.total_force.to_jobject(env)?;
+        let max_force_direction = self.max_force_direction.to_jobject(env)?;
+
+        env.new_object(
+            class,
+            jni_sig!("(Lcom/dropbear/physics/Collider;Lcom/dropbear/physics/Collider;Lcom/dropbear/math/Vector3d;DLcom/dropbear/math/Vector3d;D)V"),
+            &[
+                JValue::Object(&collider1),
+                JValue::Object(&collider2),
+                JValue::Object(&total_force),
+                JValue::Double(self.total_force_magnitude),
+                JValue::Object(&max_force_direction),
+                JValue::Double(self.max_force_magnitude),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// ------------------------------------------------ NShapeCastHit JNI impl -----
+
+impl ToJObject for NShapeCastHit {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let collider = self.collider.to_jobject(env)?;
+        let witness1 = self.witness1.to_jobject(env)?;
+        let witness2 = self.witness2.to_jobject(env)?;
+        let normal1 = self.normal1.to_jobject(env)?;
+        let normal2 = self.normal2.to_jobject(env)?;
+        let status = self.status.to_jobject(env)?;
+
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/ShapeCastHit"))
+            .map_err(|_| {
+                eprintln!("[JNI Error] Failed to find ShapeCastHit class");
+                DropbearNativeError::JNIClassNotFound
+            })?;
+
+        env.new_object(
+            class,
+            jni_sig!("(Lcom/dropbear/physics/Collider;DLcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/physics/ShapeCastStatus;)V"),
+            &[
+                JValue::Object(&collider),
+                JValue::Double(self.distance as jdouble),
+                JValue::Object(&witness1),
+                JValue::Object(&witness2),
+                JValue::Object(&normal1),
+                JValue::Object(&normal2),
+                JValue::Object(&status),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// ------------------------------------------------ NShapeCastStatus JNI impl --
+
+impl ToJObject for NShapeCastStatus {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/ShapeCastStatus"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let name = match self {
+            NShapeCastStatus::OutOfIterations => "OutOfIterations",
+            NShapeCastStatus::Converged => "Converged",
+            NShapeCastStatus::Failed => "Failed",
+            NShapeCastStatus::PenetratingOrWithinTargetDist => "PenetratingOrWithinTargetDist",
+        };
+
+        let name_jstring = env
+            .new_string(name)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        env.call_static_method(
+            class,
+            jni_str!("valueOf"),
+            jni_sig!((java.lang.String) -> com.dropbear.physics.ShapeCastStatus),
+            &[JValue::from(&name_jstring)],
+        )
+        .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
+        .l()
+        .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
+    }
+}
+
+// ------------------------------------------------ IndexNative JNI impls -------
+
+impl FromJObject for IndexNative {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let idx_val = env
+            .get_field(obj, jni_str!("index"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let gen_val = env
+            .get_field(obj, jni_str!("generation"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(IndexNative {
+            index: idx_val as u32,
+            generation: gen_val as u32,
+        })
+    }
+}
+
+impl ToJObject for IndexNative {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let cls = env
+            .load_class(jni_str!("com/dropbear/physics/Index"))
+            .map_err(|_| DropbearNativeError::GenericError)?;
+
+        env.new_object(
+            cls,
+            jni_sig!((int, int) -> void),
+            &[
+                JValue::Int(self.index as i32),
+                JValue::Int(self.generation as i32),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::GenericError)
+    }
+}
+
+impl ToJObject for Option<IndexNative> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        match self {
+            Some(v) => v.to_jobject(env),
+            None => Ok(JObject::null()),
+        }
+    }
+}
+
+// ------------------------------------------------ RigidBodyContext JNI impls -
+
+impl FromJObject for RigidBodyContext {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let index_obj = env
+            .get_field(obj, jni_str!("index"), jni_sig!(com.dropbear.physics.Index))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let idx_val = env
+            .get_field(&index_obj, jni_str!("index"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let gen_val = env
+            .get_field(&index_obj, jni_str!("generation"), jni_sig!(int))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let entity_obj = env
+            .get_field(obj, jni_str!("entity"), jni_sig!(com.dropbear.EntityId))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let entity_raw = env
+            .get_field(&entity_obj, jni_str!("raw"), jni_sig!(long))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .j()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(RigidBodyContext {
+            index: IndexNative {
+                index: idx_val as u32,
+                generation: gen_val as u32,
+            },
+            entity_id: entity_raw as u64,
+        })
+    }
+}
+
+impl ToJObject for RigidBodyContext {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let index_cls = env
+            .load_class(jni_str!("com/dropbear/physics/Index"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_cls = env
+            .load_class(jni_str!("com/dropbear/EntityId"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let rb_cls = env
+            .load_class(jni_str!("com/dropbear/physics/RigidBody"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let index_obj = env
+            .new_object(
+                &index_cls,
+                jni_sig!((int, int) -> void),
+                &[
+                    JValue::Int(self.index.index as i32),
+                    JValue::Int(self.index.generation as i32),
+                ],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        let entity_obj = env
+            .new_object(
+                &entity_cls,
+                jni_sig!((long) -> void),
+                &[JValue::Long(self.entity_id as i64)],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        env.new_object(
+            rb_cls,
+            jni_sig!((com.dropbear.physics.Index, com.dropbear.EntityId) -> void),
+            &[JValue::Object(&index_obj), JValue::Object(&entity_obj)],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// ---------------------------------------------------- RayHit JNI impl ---------
+
+impl ToJObject for RayHit {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let collider = self.collider.to_jobject(env)?;
+
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/RayHit"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        env.new_object(
+            class,
+            jni_sig!((com.dropbear.physics.Collider, double) -> void),
+            &[
+                JValue::Object(&collider),
+                JValue::Double(self.distance as jdouble),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// ------------------------------------- CharacterMovementResult JNI impl -------
+
+impl ToJObject for CharacterMovementResult {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/CharacterMovementResult"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let translation_obj = NVector3::from(self.translation).to_jobject(env)?;
+
+        env.new_object(
+            &class,
+            jni_sig!((com.dropbear.math.Vector3d, boolean, boolean) -> void),
+            &[
+                JValue::Object(&translation_obj),
+                JValue::Bool(self.grounded),
+                JValue::Bool(self.is_sliding_down_slope),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+// ------------------------------------- CharacterCollisionArray JNI impl -------
+
+impl ToJObject for CharacterCollisionArray {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let collision_cls = env
+            .load_class(jni_str!("com/dropbear/physics/CharacterCollision"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_cls = env
+            .load_class(jni_str!("com/dropbear/EntityId"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let entity_obj = env
+            .new_object(
+                &entity_cls,
+                jni_sig!((long) -> void),
+                &[JValue::Long(self.entity_id as i64)],
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        let out = env
+            .new_object_array(
+                self.collisions.len() as i32,
+                &collision_cls,
+                JObject::null(),
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        for (i, handle) in self.collisions.iter().enumerate() {
+            let index_obj = handle.to_jobject(env)?;
+            let collision_obj = env
+                .new_object(
+                    &collision_cls,
+                    jni_sig!((com.dropbear.EntityId, com.dropbear.physics.Index) -> void),
+                    &[JValue::Object(&entity_obj), JValue::Object(&index_obj)],
+                )
+                .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+            out.set_element(env, i, collision_obj)
+                .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        }
+
+        Ok(JObject::from(out))
+    }
+}
diff --git a/crates/eucalyptus-exports/src/physics/physics.rs b/crates/eucalyptus-exports/src/physics/physics.rs
new file mode 100644
index 0000000..927f1db
--- /dev/null
+++ b/crates/eucalyptus-exports/src/physics/physics.rs
@@ -0,0 +1,335 @@
+use glam::Vec3;
+use eucalyptus_core::ptr::PhysicsStatePtr;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::types::{IndexNative, NCollider, NShapeCastHit, NVector3, RayHit};
+use eucalyptus_core::rapier3d::parry::query::{DefaultQueryDispatcher, ShapeCastOptions};
+use hecs::Entity;
+use eucalyptus_core::physics::collider::ColliderShape;
+use eucalyptus_core::physics::PhysicsState;
+use eucalyptus_core::rapier3d::prelude::{nalgebra, point, vector, ColliderHandle, Pose3, QueryFilter, Ray, SharedShape};
+
+pub mod shared {
+    use hecs::Entity;
+    use eucalyptus_core::physics::PhysicsState;
+    use eucalyptus_core::rapier3d::prelude::ColliderHandle;
+    use eucalyptus_core::types::{NCollider, NVector3};
+
+    pub fn get_gravity(physics: &PhysicsState) -> NVector3 {
+        NVector3::from(physics.gravity)
+    }
+
+    pub fn set_gravity(physics: &mut PhysicsState, new: NVector3) {
+        physics.gravity = new.to_float_array();
+    }
+
+    fn collider_handle_from_ffi(collider: &NCollider) -> ColliderHandle {
+        ColliderHandle::from_raw_parts(collider.index.index, collider.index.generation)
+    }
+
+    pub fn overlapping(
+        physics: &PhysicsState,
+        collider1: &NCollider,
+        collider2: &NCollider,
+    ) -> bool {
+        let h1 = collider_handle_from_ffi(collider1);
+        let h2 = collider_handle_from_ffi(collider2);
+
+        if physics.colliders.get(h1).is_none() || physics.colliders.get(h2).is_none() {
+            return false;
+        }
+
+        physics
+            .narrow_phase
+            .intersection_pair(h1, h2)
+            .unwrap_or(false)
+    }
+
+    pub fn triggering(
+        physics: &PhysicsState,
+        collider1: &NCollider,
+        collider2: &NCollider,
+    ) -> bool {
+        let h1 = collider_handle_from_ffi(collider1);
+        let h2 = collider_handle_from_ffi(collider2);
+
+        let is_sensor_1 = physics
+            .colliders
+            .get(h1)
+            .map(|c| c.is_sensor())
+            .unwrap_or(false);
+        let is_sensor_2 = physics
+            .colliders
+            .get(h2)
+            .map(|c| c.is_sensor())
+            .unwrap_or(false);
+
+        (is_sensor_1 || is_sensor_2) && overlapping(physics, collider1, collider2)
+    }
+
+    pub fn touching(physics: &PhysicsState, entity1: Entity, entity2: Entity) -> bool {
+        let Some(label1) = physics.entity_label_map.get(&entity1) else {
+            return false;
+        };
+        let Some(label2) = physics.entity_label_map.get(&entity2) else {
+            return false;
+        };
+
+        let Some(handles1) = physics.colliders_entity_map.get(label1) else {
+            return false;
+        };
+        let Some(handles2) = physics.colliders_entity_map.get(label2) else {
+            return false;
+        };
+
+        for (_, h1) in handles1 {
+            for (_, h2) in handles2 {
+                if let Some(pair) = physics.narrow_phase.contact_pair(*h1, *h2) {
+                    if pair.has_any_active_contact() {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        false
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "getGravity"),
+    c
+)]
+fn get_gravity(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+) -> DropbearNativeResult<NVector3> {
+    Ok(shared::get_gravity(physics))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "setGravity"),
+    c
+)]
+fn set_gravity(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    gravity: &NVector3,
+) -> DropbearNativeResult<()> {
+    Ok(shared::set_gravity(physics, *gravity))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "raycast"),
+    c
+)]
+fn raycast(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    origin: &NVector3,
+    dir: &NVector3,
+    time_of_impact: f64,
+    solid: bool,
+) -> DropbearNativeResult<Option<RayHit>> {
+    let qp = physics.broad_phase.as_query_pipeline(
+        &DefaultQueryDispatcher,
+        &physics.bodies,
+        &physics.colliders,
+        QueryFilter::new(),
+    );
+
+    let ray = Ray::new(
+        point![origin.x as f32, origin.y as f32, origin.z as f32].into(),
+        vector![dir.x as f32, dir.y as f32, dir.z as f32].into(),
+    );
+
+    if let Some((hit, distance)) = qp.cast_ray(&ray, time_of_impact as f32, solid) {
+        let raw = hit.0;
+
+        let mut found = None;
+        for (l, colliders) in physics.colliders_entity_map.iter() {
+            for (_, c) in colliders {
+                if c.0 == hit.0 {
+                    found = Some((l, c.0));
+                }
+            }
+        }
+
+        if let Some((label, _)) = found {
+            let entity = physics.entity_label_map.iter().find(|(_, l)| *l == label);
+            if let Some((e, _)) = entity {
+                let rayhit = RayHit {
+                    collider: NCollider {
+                        index: IndexNative::from(raw),
+                        entity_id: e.to_bits().get(),
+                        id: raw.into_raw_parts().0,
+                    },
+                    distance: distance as f64,
+                };
+                Ok(Some(rayhit))
+            } else {
+                Ok(None)
+            }
+        } else {
+            eprintln!("Unknown collider, still returning value without entity_id");
+            let rayhit = RayHit {
+                collider: NCollider {
+                    index: IndexNative::from(raw),
+                    entity_id: Entity::DANGLING.to_bits().get(),
+                    id: raw.into_raw_parts().0,
+                },
+                distance: distance as f64,
+            };
+            Ok(Some(rayhit))
+        }
+    } else {
+        Ok(None)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "shapeCast"),
+    c
+)]
+fn shape_cast(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    origin: &NVector3,
+    direction: &NVector3,
+    shape: &ColliderShape,
+    time_of_impact: f64,
+    solid: bool,
+) -> DropbearNativeResult<Option<NShapeCastHit>> {
+    let qp = physics.broad_phase.as_query_pipeline(
+        &DefaultQueryDispatcher,
+        &physics.bodies,
+        &physics.colliders,
+        QueryFilter::new(),
+    );
+
+    let dir_len =
+        ((direction.x * direction.x) + (direction.y * direction.y) + (direction.z * direction.z))
+            .sqrt();
+    if dir_len <= f64::EPSILON {
+        return Ok(None);
+    }
+
+    let dir_unit = NVector3 {
+        x: direction.x / dir_len,
+        y: direction.y / dir_len,
+        z: direction.z / dir_len,
+    };
+
+    let cast_shape = match shape {
+        ColliderShape::Box { half_extents } => SharedShape::cuboid(
+            half_extents.x as f32,
+            half_extents.y as f32,
+            half_extents.z as f32,
+        ),
+        ColliderShape::Sphere { radius } => SharedShape::ball(*radius),
+        ColliderShape::Capsule { half_height, radius } => {
+            SharedShape::capsule_y(*half_height, *radius)
+        }
+        ColliderShape::Cylinder { half_height, radius } => {
+            SharedShape::cylinder(*half_height, *radius)
+        }
+        ColliderShape::Cone { half_height, radius } => {
+            SharedShape::cone(*half_height, *radius)
+        }
+    };
+
+    let iso: Pose3 =
+        nalgebra::Isometry3::translation(origin.x as f32, origin.y as f32, origin.z as f32)
+            .into();
+    let vel: Vec3 = vector![dir_unit.x as f32, dir_unit.y as f32, dir_unit.z as f32].into();
+
+    let options = ShapeCastOptions {
+        max_time_of_impact: time_of_impact as f32,
+        target_distance: 0.0,
+        stop_at_penetration: solid,
+        compute_impact_geometry_on_penetration: true,
+    };
+
+    let Some((hit_handle, toi)) = qp.cast_shape(&iso, vel, cast_shape.as_ref(), options) else {
+        return Ok(None);
+    };
+
+    let collider = collider_ffi_from_handle(physics, hit_handle);
+
+    let hit = NShapeCastHit {
+        collider,
+        distance: toi.time_of_impact as f64,
+        witness1: NVector3::from([toi.witness1.x, toi.witness1.y, toi.witness1.z]),
+        witness2: NVector3::from([toi.witness2.x, toi.witness2.y, toi.witness2.z]),
+        normal1: NVector3::from([toi.normal1.x, toi.normal1.y, toi.normal1.z]),
+        normal2: NVector3::from([toi.normal2.x, toi.normal2.y, toi.normal2.z]),
+        status: toi.status.into(),
+    };
+
+    Ok(Some(hit))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "isOverlapping"),
+    c
+)]
+fn is_overlapping(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider1: &NCollider,
+    collider2: &NCollider,
+) -> DropbearNativeResult<bool> {
+    Ok(shared::overlapping(physics, collider1, collider2))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "isTriggering"),
+    c
+)]
+fn is_triggering(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    collider1: &NCollider,
+    collider2: &NCollider,
+) -> DropbearNativeResult<bool> {
+    Ok(shared::triggering(physics, collider1, collider2))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.PhysicsNative", func = "isTouching"),
+    c
+)]
+fn is_touching(
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    #[dropbear_macro::entity] entity1: Entity,
+    #[dropbear_macro::entity] entity2: Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(shared::touching(physics, entity1, entity2))
+}
+
+fn collider_ffi_from_handle(physics: &PhysicsState, handle: ColliderHandle) -> NCollider {
+    let (idx, generation) = handle.into_raw_parts();
+
+    let mut found_label = None;
+    for (label, colliders) in physics.colliders_entity_map.iter() {
+        for (_, c) in colliders {
+            if c.0 == handle.0 {
+                found_label = Some(label);
+                break;
+            }
+        }
+        if found_label.is_some() {
+            break;
+        }
+    }
+
+    let entity_id = if let Some(label) = found_label {
+        physics
+            .entity_label_map
+            .iter()
+            .find(|(_, l)| *l == label)
+            .map(|(e, _)| e.to_bits().get())
+            .unwrap_or(Entity::DANGLING.to_bits().get())
+    } else {
+        Entity::DANGLING.to_bits().get()
+    };
+
+    NCollider {
+        index: IndexNative { index: idx, generation },
+        entity_id,
+        id: idx,
+    }
+}
diff --git a/crates/eucalyptus-exports/src/physics/rigidbody.rs b/crates/eucalyptus-exports/src/physics/rigidbody.rs
new file mode 100644
index 0000000..530fe3b
--- /dev/null
+++ b/crates/eucalyptus-exports/src/physics/rigidbody.rs
@@ -0,0 +1,470 @@
+use eucalyptus_core::physics::PhysicsState;
+use eucalyptus_core::physics::rigidbody::AxisLock;
+use eucalyptus_core::physics::rigidbody::shared;
+use eucalyptus_core::ptr::{PhysicsStatePtr, WorldPtr};
+use eucalyptus_core::rapier3d::prelude::RigidBodyType;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::types::{IndexNative, NCollider, NVector3, RigidBodyContext};
+use jni::objects::{JObject, JValue};
+use jni::{Env, jni_sig, jni_str};
+use crate::{FromJObject, ToJObject};
+
+// ---------------------------------------------------- AxisLock JNI impls --
+
+impl FromJObject for AxisLock {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized,
+    {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/AxisLock"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let x = env
+            .get_field(obj, jni_str!("x"), jni_sig!(boolean))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .z()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let y = env
+            .get_field(obj, jni_str!("y"), jni_sig!(boolean))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .z()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let z = env
+            .get_field(obj, jni_str!("z"), jni_sig!(boolean))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+            .z()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(AxisLock { x, y, z })
+    }
+}
+
+impl ToJObject for AxisLock {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/physics/AxisLock"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let args = [
+            JValue::Bool(self.x),
+            JValue::Bool(self.y),
+            JValue::Bool(self.z),
+        ];
+
+        let obj = env
+            .new_object(&class, jni_sig!((boolean, boolean, boolean) -> void), &args)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
+}
+
+// ------------------------------------------------ RigidBodyNative exports --
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "rigidBodyExistsForEntity"
+    ),
+    c
+)]
+fn exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<Option<IndexNative>> {
+    Ok(shared::rigid_body_exists_for_entity(world, physics, entity))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyMode"
+    ),
+    c
+)]
+fn get_rigidbody_mode(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<i32> {
+    let body_type = shared::get_rigidbody_type(physics, rigidbody)?;
+    Ok(match body_type {
+        RigidBodyType::Dynamic => 0,
+        RigidBodyType::Fixed => 1,
+        RigidBodyType::KinematicPositionBased => 2,
+        RigidBodyType::KinematicVelocityBased => 3,
+    })
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyMode"
+    ),
+    c
+)]
+fn set_rigidbody_mode(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    mode: i32,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_type(physics, world, rigidbody, mode as i64)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyGravityScale"
+    ),
+    c
+)]
+fn get_rigidbody_gravity_scale(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<f64> {
+    shared::get_rigidbody_gravity_scale(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyGravityScale"
+    ),
+    c
+)]
+fn set_rigidbody_gravity_scale(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    gravity_scale: f64,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_gravity_scale(physics, world, rigidbody, gravity_scale)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyLinearDamping"
+    ),
+    c
+)]
+fn get_rigidbody_linear_damping(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<f64> {
+    shared::get_rigidbody_linear_damping(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyLinearDamping"
+    ),
+    c
+)]
+fn set_rigidbody_linear_damping(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    linear_damping: f64,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_linear_damping(physics, world, rigidbody, linear_damping)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyAngularDamping"
+    ),
+    c
+)]
+fn get_rigidbody_angular_damping(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<f64> {
+    shared::get_rigidbody_angular_damping(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyAngularDamping"
+    ),
+    c
+)]
+fn set_rigidbody_angular_damping(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    angular_damping: f64,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_angular_damping(physics, world, rigidbody, angular_damping)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodySleep"
+    ),
+    c
+)]
+fn get_rigidbody_sleep(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<bool> {
+    shared::get_rigidbody_sleep(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodySleep"
+    ),
+    c
+)]
+fn set_rigidbody_sleep(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    sleep: bool,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_sleep(physics, world, rigidbody, sleep)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyCcdEnabled"
+    ),
+    c
+)]
+fn get_rigidbody_ccd_enabled(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<bool> {
+    shared::get_rigidbody_ccd(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyCcdEnabled"
+    ),
+    c
+)]
+fn set_rigidbody_ccd_enabled(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    ccd_enabled: bool,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_ccd(physics, world, rigidbody, ccd_enabled)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyLinearVelocity"
+    ),
+    c
+)]
+fn get_rigidbody_linear_velocity(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_rigidbody_linvel(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyLinearVelocity"
+    ),
+    c
+)]
+fn set_rigidbody_linear_velocity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    linear_velocity: &NVector3,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_linvel(physics, world, rigidbody, *linear_velocity)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyAngularVelocity"
+    ),
+    c
+)]
+fn get_rigidbody_angular_velocity(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<NVector3> {
+    shared::get_rigidbody_angvel(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyAngularVelocity"
+    ),
+    c
+)]
+fn set_rigidbody_angular_velocity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    angular_velocity: &NVector3,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_angvel(physics, world, rigidbody, *angular_velocity)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyLockTranslation"
+    ),
+    c
+)]
+fn get_rigidbody_lock_translation(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<AxisLock> {
+    shared::get_rigidbody_lock_translation(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyLockTranslation"
+    ),
+    c
+)]
+fn set_rigidbody_lock_translation(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    lock_translation: &AxisLock,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_lock_translation(physics, world, rigidbody, *lock_translation)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyLockRotation"
+    ),
+    c
+)]
+fn get_rigidbody_lock_rotation(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<AxisLock> {
+    shared::get_rigidbody_lock_rotation(physics, rigidbody)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "setRigidBodyLockRotation"
+    ),
+    c
+)]
+fn set_rigidbody_lock_rotation(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    lock_rotation: &AxisLock,
+) -> DropbearNativeResult<()> {
+    shared::set_rigidbody_lock_rotation(physics, world, rigidbody, *lock_rotation)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "getRigidBodyChildren"
+    ),
+    c
+)]
+fn get_rigidbody_children(
+    #[dropbear_macro::define(WorldPtr)] _world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &PhysicsState,
+    rigidbody: &RigidBodyContext,
+) -> DropbearNativeResult<Vec<NCollider>> {
+    let children = shared::get_rigidbody_children(physics, rigidbody)?;
+    let colliders = children
+        .into_iter()
+        .map(|handle| {
+            let (idx, generation) = handle.into_raw_parts();
+            NCollider {
+                index: IndexNative {
+                    index: idx,
+                    generation,
+                },
+                entity_id: rigidbody.entity_id,
+                id: idx,
+            }
+        })
+        .collect();
+
+    Ok(colliders)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.physics.RigidBodyNative", func = "applyImpulse"),
+    c
+)]
+fn apply_impulse(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    x: f64,
+    y: f64,
+    z: f64,
+) -> DropbearNativeResult<()> {
+    let impulse = NVector3::new(x, y, z);
+    shared::apply_impulse(physics, world, rigidbody, impulse)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.RigidBodyNative",
+        func = "applyTorqueImpulse"
+    ),
+    c
+)]
+fn apply_torque_impulse(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::define(PhysicsStatePtr)] physics: &mut PhysicsState,
+    rigidbody: &RigidBodyContext,
+    x: f64,
+    y: f64,
+    z: f64,
+) -> DropbearNativeResult<()> {
+    let torque = NVector3::new(x, y, z);
+    shared::apply_torque_impulse(physics, world, rigidbody, torque)
+}
diff --git a/crates/eucalyptus-exports/src/primitives.rs b/crates/eucalyptus-exports/src/primitives.rs
new file mode 100644
index 0000000..3b29e4a
--- /dev/null
+++ b/crates/eucalyptus-exports/src/primitives.rs
@@ -0,0 +1,234 @@
+use crate::{FromJObject, ToJObject};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use jni::objects::{JObject, JValue};
+use jni::sys::{jdouble, jint, jlong};
+use jni::{Env, jni_sig, jni_str};
+
+impl ToJObject for Option<i32> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        match self {
+            Some(value) => {
+                let class = env
+                    .load_class(jni_str!("java.lang.Integer"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                env.new_object(&class, jni_sig!((int) -> void), &[JValue::Int(*value)])
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+            }
+            None => Ok(JObject::null()),
+        }
+    }
+}
+
+impl FromJObject for Option<i32> {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        if obj.is_null() {
+            return Ok(None);
+        }
+
+        let class = env
+            .load_class(jni_str!("java.lang.Integer"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let value = env
+            .call_method(obj, jni_str!("intValue"), jni_sig!(() -> i32), &[])
+            .map_err(|_| DropbearNativeError::JNIMethodNotFound)?
+            .i()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        Ok(Some(value as i32))
+    }
+}
+
+impl ToJObject for Vec<i32> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        self.as_slice().to_jobject(env)
+    }
+}
+
+impl ToJObject for &[i32] {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let array = env
+            .new_int_array(self.len())
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let buf: Vec<jint> = self.iter().map(|v| *v as jint).collect();
+        array
+            .set_region(env, 0, &buf)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        Ok(JObject::from(array))
+    }
+}
+
+impl ToJObject for &[Vec<i32>] {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let list = new_array_list(env)?;
+        for value in self.iter() {
+            let boxed = value.as_slice().to_jobject(env)?;
+            array_list_add(env, &list, &boxed)?;
+        }
+        Ok(list)
+    }
+}
+
+impl ToJObject for Option<f32> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        match self {
+            Some(value) => {
+                let class = env
+                    .load_class(jni_str!("java.lang.Float"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                env.new_object(&class, jni_sig!((f32) -> ()), &[JValue::Float(*value)])
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+            }
+            None => Ok(JObject::null()),
+        }
+    }
+}
+
+impl ToJObject for Option<f64> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        match self {
+            Some(value) => {
+                let class = env
+                    .load_class(jni_str!("java.lang.Double"))
+                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+                env.new_object(&class, jni_sig!((f64) -> ()), &[JValue::Double(*value)])
+                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+            }
+            None => Ok(JObject::null()),
+        }
+    }
+}
+
+impl ToJObject for Vec<f64> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        self.as_slice().to_jobject(env)
+    }
+}
+
+impl ToJObject for &[f64] {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let array = env
+            .new_double_array(self.len())
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        let buf: Vec<jdouble> = self.iter().map(|v| *v as jdouble).collect();
+        array
+            .set_region(env, 0, &buf)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+        Ok(JObject::from(array))
+    }
+}
+
+impl ToJObject for &[Vec<f64>] {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let list = new_array_list(env)?;
+        for value in self.iter() {
+            let array = value.as_slice().to_jobject(env)?;
+            array_list_add(env, &list, &array)?;
+        }
+        Ok(list)
+    }
+}
+
+fn new_array_list<'a>(env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+    let class = env
+        .load_class(jni_str!("java.util.ArrayList"))
+        .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+    env.new_object(&class, jni_sig!(() -> ()), &[])
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+}
+
+fn array_list_add(env: &mut Env, list: &JObject, item: &JObject) -> DropbearNativeResult<()> {
+    env.call_method(
+        list,
+        jni_str!("add"),
+        jni_sig!((java.lang.Object) -> boolean),
+        &[JValue::Object(item)],
+    )
+    .map_err(|_| DropbearNativeError::JNIMethodNotFound)?;
+    Ok(())
+}
+
+impl ToJObject for Vec<u64> {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        self.as_slice().to_jobject(env)
+    }
+}
+
+impl ToJObject for &[u64] {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let array = env.new_long_array(self.len())?;
+        let buf: Vec<jlong> = self.iter().map(|v| *v as jlong).collect();
+        array.set_region(env, 0, &buf)?;
+        Ok(JObject::from(array))
+    }
+}
+
+impl ToJObject for String {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let result = JObject::from(env.new_string(self)?);
+        Ok(result)
+    }
+}
+
+impl<T> ToJObject for Vec<T>
+where
+    T: ToJObject,
+{
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let list_class = env.load_class(jni_str!("java.util.ArrayList"))?;
+        let list_obj = env.new_object(&list_class, jni_sig!(()), &[])?;
+
+        for item in self {
+            let obj = item.to_jobject(env)?;
+            let _ = env.call_method(
+                &list_obj,
+                jni_str!("add"),
+                jni_sig!((java.lang.Object) -> boolean),
+                &[JValue::Object(&obj)],
+            )?;
+        }
+
+        Ok(list_obj)
+    }
+}
+
+impl<T> FromJObject for Vec<T>
+where
+    T: FromJObject,
+{
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized,
+    {
+        let size = env
+            .call_method(obj, jni_str!("size"), jni_sig!(() -> int), &[])?
+            .i()? as jint;
+        let mut out = Vec::with_capacity(size as usize);
+
+        for i in 0..size {
+            let item = env
+                .call_method(
+                    obj,
+                    jni_str!("get"),
+                    jni_sig!((int) -> java.lang.Object),
+                    &[JValue::Int(i)],
+                )?
+                .l()?;
+            let value = T::from_jobject(env, &item)?;
+            out.push(value);
+        }
+
+        Ok(out)
+    }
+}
diff --git a/crates/eucalyptus-exports/src/properties.rs b/crates/eucalyptus-exports/src/properties.rs
new file mode 100644
index 0000000..d716055
--- /dev/null
+++ b/crates/eucalyptus-exports/src/properties.rs
@@ -0,0 +1,314 @@
+use eucalyptus_core::properties::{CustomProperties, Value};
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::types::NVector3;
+use hecs::{Entity, World};
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "customPropertiesExistsForEntity"
+    ),
+    c
+)]
+fn custom_properties_exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&CustomProperties>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getStringProperty"
+    ),
+    c
+)]
+fn get_string_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<String>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::String(s) => Some(s.clone()),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getIntProperty"
+    ),
+    c
+)]
+fn get_int_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<i32>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::Int(v) => Some(*v as i32),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getLongProperty"
+    ),
+    c
+)]
+fn get_long_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<i64>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::Int(v) => Some(*v),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getDoubleProperty"
+    ),
+    c
+)]
+fn get_double_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<f64>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::Double(v) => Some(*v),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getFloatProperty"
+    ),
+    c
+)]
+fn get_float_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<f32>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::Double(v) => Some(*v as f32),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getBoolProperty"
+    ),
+    c
+)]
+fn get_bool_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<bool>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::Bool(v) => Some(*v),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "getVec3Property"
+    ),
+    c
+)]
+fn get_vec3_property(
+    #[dropbear_macro::define(WorldPtr)] world: &World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+) -> DropbearNativeResult<Option<NVector3>> {
+    let props = world
+        .get::<&CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+
+    Ok(props.get_property(&key).and_then(|value| match value {
+        Value::Vec3(v) => Some(NVector3::from(*v)),
+        _ => None,
+    }))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setStringProperty"
+    ),
+    c
+)]
+fn set_string_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: String,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::String(value));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setIntProperty"
+    ),
+    c
+)]
+fn set_int_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: i32,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::Int(value as i64));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setLongProperty"
+    ),
+    c
+)]
+fn set_long_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: i64,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::Int(value));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setDoubleProperty"
+    ),
+    c
+)]
+fn set_double_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: f64,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::Double(value));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setFloatProperty"
+    ),
+    c
+)]
+fn set_float_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: f64,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::Double(value));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setBoolProperty"
+    ),
+    c
+)]
+fn set_bool_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: bool,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::Bool(value));
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.CustomPropertiesNative",
+        func = "setVec3Property"
+    ),
+    c
+)]
+fn set_vec3_property(
+    #[dropbear_macro::define(WorldPtr)] world: &mut World,
+    #[dropbear_macro::entity] entity: Entity,
+    key: String,
+    value: &NVector3,
+) -> DropbearNativeResult<()> {
+    let mut props = world
+        .get::<&mut CustomProperties>(entity)
+        .map_err(|_| DropbearNativeError::NoSuchComponent)?;
+    props.set_property(key, Value::Vec3(value.to_array()));
+    Ok(())
+}
diff --git a/crates/eucalyptus-exports/src/scene.rs b/crates/eucalyptus-exports/src/scene.rs
new file mode 100644
index 0000000..1922643
--- /dev/null
+++ b/crates/eucalyptus-exports/src/scene.rs
@@ -0,0 +1,113 @@
+use eucalyptus_core::ptr::{CommandBufferPtr, CommandBufferUnwrapped, SceneLoaderPtr, SceneLoaderUnwrapped};
+use eucalyptus_core::scene::scripting::shared;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::utils::Progress;
+use crate::ToJObject;
+use jni::objects::{JObject, JValue};
+use jni::{Env, jni_sig, jni_str};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+
+impl ToJObject for Progress {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/utils/Progress"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let message_obj = env
+            .new_string(&self.message)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        let args = [
+            JValue::Double(self.current as f64),
+            JValue::Double(self.total as f64),
+            JValue::Object(&JObject::from(message_obj)),
+        ];
+
+        env.new_object(&class, jni_sig!((double, double, java.lang.String) -> void), &args)
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.scene.SceneManagerNative", func = "loadSceneAsync"),
+    c
+)]
+fn load_scene_async(
+    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
+    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
+    scene_name: String,
+) -> DropbearNativeResult<u64> {
+    Ok(shared::load_scene_async(command_buffer, scene_loader, scene_name, None)?)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.scene.SceneManagerNative", func = "loadSceneAsyncWithLoading"),
+    c
+)]
+fn load_scene_async_with_loading(
+    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
+    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
+    scene_name: String,
+    loading_scene: String,
+) -> DropbearNativeResult<u64> {
+    Ok(shared::load_scene_async(command_buffer, scene_loader, scene_name, Some(loading_scene))?)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.scene.SceneManagerNative", func = "switchToSceneImmediate"),
+    c
+)]
+fn switch_to_scene_immediate(
+    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
+    scene_name: String,
+) -> DropbearNativeResult<()> {
+    Ok(shared::switch_to_scene_immediate(command_buffer, scene_name)?)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.scene.SceneLoadHandleNative",
+        func = "getSceneLoadHandleSceneName"
+    ),
+    c
+)]
+fn get_scene_load_handle_scene_name(
+    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
+    scene_id: u64,
+) -> DropbearNativeResult<String> {
+    Ok(shared::get_scene_load_handle_scene_name(scene_loader, scene_id)?)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.scene.SceneLoadHandleNative", func = "switchToSceneAsync"),
+    c
+)]
+fn switch_to_scene_async(
+    #[dropbear_macro::define(CommandBufferPtr)] command_buffer: &CommandBufferUnwrapped,
+    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
+    scene_id: u64,
+) -> DropbearNativeResult<()> {
+    Ok(shared::switch_to_scene_async(command_buffer, scene_loader, scene_id)?)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.scene.SceneLoadHandleNative", func = "getSceneLoadProgress"),
+    c
+)]
+fn get_scene_load_progress(
+    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
+    scene_id: u64,
+) -> DropbearNativeResult<Progress> {
+    Ok(shared::get_scene_load_progress(scene_loader, scene_id)?)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.scene.SceneLoadHandleNative", func = "getSceneLoadStatus"),
+    c
+)]
+fn get_scene_load_status(
+    #[dropbear_macro::define(SceneLoaderPtr)] scene_loader: &SceneLoaderUnwrapped,
+    scene_id: u64,
+) -> DropbearNativeResult<u32> {
+    Ok(shared::get_scene_load_status(scene_loader, scene_id)?)
+}
diff --git a/crates/eucalyptus-exports/src/scripting/mod.rs b/crates/eucalyptus-exports/src/scripting/mod.rs
new file mode 100644
index 0000000..065e6c0
--- /dev/null
+++ b/crates/eucalyptus-exports/src/scripting/mod.rs
@@ -0,0 +1,14 @@
+pub mod native {
+    pub use eucalyptus_core::scripting::native::DropbearNativeError;
+}
+
+pub mod result {
+    pub use eucalyptus_core::scripting::result::DropbearNativeResult;
+}
+
+pub mod jni {
+    pub mod utils {
+        pub use crate::FromJObject;
+        pub use crate::ToJObject;
+    }
+}
diff --git a/crates/eucalyptus-exports/src/transform.rs b/crates/eucalyptus-exports/src/transform.rs
new file mode 100644
index 0000000..1f7d43d
--- /dev/null
+++ b/crates/eucalyptus-exports/src/transform.rs
@@ -0,0 +1,706 @@
+use eucalyptus_core::hierarchy::EntityTransformExt;
+use eucalyptus_core::ptr::WorldPtr;
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use eucalyptus_core::transform::{OnRails, RailDrive, RailPoint};
+use crate::math::{NQuaternion, NTransform, NVector3};
+use crate::{FromJObject, ToJObject};
+use dropbear_engine::entity::{EntityTransform, Transform};
+use glam::{DQuat, DVec3, Vec3};
+use jni::objects::{JObject, JValue};
+use jni::{Env, jni_sig, jni_str};
+
+impl FromJObject for Transform {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let pos_val = env
+            .get_field(obj, jni_str!("position"), jni_sig!(com.dropbear.math.Vector3d))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+        let pos_obj = pos_val.l().map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let rot_val = env
+            .get_field(obj, jni_str!("rotation"), jni_sig!(com.dropbear.math.Quaterniond))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+        let rot_obj = rot_val.l().map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let scale_val = env
+            .get_field(obj, jni_str!("scale"), jni_sig!(com.dropbear.math.Vector3d))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+        let scale_obj = scale_val.l().map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let position: DVec3 = NVector3::from_jobject(env, &pos_obj)?.into();
+        let scale: DVec3 = NVector3::from_jobject(env, &scale_obj)?.into();
+
+        let mut get_double = |field| -> DropbearNativeResult<f64> {
+            env.get_field(&rot_obj, field, jni_sig!(double))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .d()
+                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
+        };
+
+        let rx = get_double(jni_str!("x"))?;
+        let ry = get_double(jni_str!("y"))?;
+        let rz = get_double(jni_str!("z"))?;
+        let rw = get_double(jni_str!("w"))?;
+        let rotation = DQuat::from_xyzw(rx, ry, rz, rw);
+
+        Ok(Transform { position, rotation, scale })
+    }
+}
+
+impl ToJObject for Transform {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let cls = env
+            .load_class(jni_str!("com/dropbear/math/Transform"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let p = self.position;
+        let r = self.rotation;
+        let s = self.scale;
+
+        let args = [
+            JValue::Double(p.x),
+            JValue::Double(p.y),
+            JValue::Double(p.z),
+            JValue::Double(r.x),
+            JValue::Double(r.y),
+            JValue::Double(r.z),
+            JValue::Double(r.w),
+            JValue::Double(s.x),
+            JValue::Double(s.y),
+            JValue::Double(s.z),
+        ];
+
+        env.new_object(
+            cls,
+            jni_sig!((double, double, double, double, double, double, double, double, double, double) -> void),
+            &args,
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+impl FromJObject for EntityTransform {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let local_val = env
+            .get_field(obj, jni_str!("local"), jni_sig!(com.dropbear.math.Transform))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+        let local_obj = local_val.l().map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let world_val = env
+            .get_field(obj, jni_str!("world"), jni_sig!(com.dropbear.math.Transform))
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+        let world_obj = world_val.l().map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let local = Transform::from_jobject(env, &local_obj)?;
+        let world = Transform::from_jobject(env, &world_obj)?;
+
+        Ok(EntityTransform::new(local, world))
+    }
+}
+
+impl ToJObject for EntityTransform {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let cls = env
+            .load_class(jni_str!("com/dropbear/components/EntityTransform"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let local_obj = self.local().to_jobject(env)?;
+        let world_obj = self.world().to_jobject(env)?;
+
+        let args = [JValue::Object(&local_obj), JValue::Object(&world_obj)];
+
+        env.new_object(
+            cls,
+            jni_sig!((com.dropbear.math.Transform, com.dropbear.math.Transform) -> void),
+            &args,
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.EntityTransformNative",
+        func = "entityTransformExistsForEntity"
+    ),
+    c
+)]
+fn exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&EntityTransform>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.EntityTransformNative",
+        func = "getLocalTransform"
+    ),
+    c
+)]
+fn get_local_transform(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NTransform> {
+    if let Ok(et) = world.get::<&EntityTransform>(entity) {
+        Ok((*et.local()).into())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.EntityTransformNative",
+        func = "setLocalTransform"
+    ),
+    c
+)]
+fn set_local_transform(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    transform: &NTransform,
+) -> DropbearNativeResult<()> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        *et.local_mut() = (*transform).into();
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.EntityTransformNative",
+        func = "getWorldTransform"
+    ),
+    c
+)]
+fn get_world_transform(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NTransform> {
+    if let Ok(et) = world.get::<&EntityTransform>(entity) {
+        Ok((*et.world()).into())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.EntityTransformNative",
+        func = "setWorldTransform"
+    ),
+    c
+)]
+fn set_world_transform(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    transform: &NTransform,
+) -> DropbearNativeResult<()> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        *et.world_mut() = (*transform).into();
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.EntityTransformNative",
+        func = "propogateTransform"
+    ),
+    c
+)]
+fn propagate_transform(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NTransform> {
+    if let Ok(et) = world.get::<&mut EntityTransform>(entity) {
+        let result = et.propagate(world, entity);
+        Ok(result.into())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getEnabled"),
+    c
+)]
+fn on_rails_get_enabled(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(rails.enabled)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setEnabled"),
+    c
+)]
+fn on_rails_set_enabled(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    enabled: bool,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.enabled = enabled;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "existsForEntity"),
+    c
+)]
+fn on_rails_exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&OnRails>(entity).is_ok())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getProgress"),
+    c
+)]
+fn on_rails_get_progress(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f32> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(rails.progress)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setProgress"),
+    c
+)]
+fn on_rails_set_progress(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    progress: f32,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.progress = progress.clamp(0.0, 1.0);
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getPathLen"),
+    c
+)]
+fn on_rails_get_path_len(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<i32> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    Ok(rails.path.len() as i32)
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getPathPoint"),
+    c
+)]
+fn on_rails_get_path_point(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    index: i32,
+) -> DropbearNativeResult<NVector3> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    let point = rails
+        .path
+        .get(index as usize)
+        .ok_or(DropbearNativeError::InvalidArgument)?;
+    Ok(NVector3::from(&point.position))
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "clearPath"),
+    c
+)]
+fn on_rails_clear_path(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.path.clear();
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "pushPathPoint"),
+    c
+)]
+fn on_rails_push_path_point(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    point: &NVector3,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.path.push(RailPoint { position: DVec3::from(point), rotation: None });
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getPathPointHasRotation"
+    ),
+    c
+)]
+fn on_rails_get_path_point_has_rotation(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    index: i32,
+) -> DropbearNativeResult<bool> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    let point = rails
+        .path
+        .get(index as usize)
+        .ok_or(DropbearNativeError::InvalidArgument)?;
+    Ok(point.rotation.is_some())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getPathPointRotation"
+    ),
+    c
+)]
+fn on_rails_get_path_point_rotation(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    index: i32,
+) -> DropbearNativeResult<NQuaternion> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    let point = rails
+        .path
+        .get(index as usize)
+        .ok_or(DropbearNativeError::InvalidArgument)?;
+    Ok(NQuaternion::from(point.rotation.unwrap_or(DQuat::IDENTITY)))
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "pushPathPointWithRotation"
+    ),
+    c
+)]
+fn on_rails_push_path_point_with_rotation(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    point: &NVector3,
+    rotation: &NQuaternion,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.path.push(RailPoint {
+        position: DVec3::from(point),
+        rotation: Some(DQuat::from(*rotation)),
+    });
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveType"),
+    c
+)]
+fn on_rails_get_drive_type(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<i32> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    let tag = match &rails.drive {
+        RailDrive::Automatic { .. } => 0,
+        RailDrive::FollowEntity { .. } => 1,
+        RailDrive::AxisDriven { .. } => 2,
+        RailDrive::Manual => 3,
+    };
+    Ok(tag)
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "setDriveAutomatic"
+    ),
+    c
+)]
+fn on_rails_set_drive_automatic(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    speed: f32,
+    looping: bool,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.drive = RailDrive::Automatic { speed, looping };
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "setDriveFollowEntity"
+    ),
+    c
+)]
+fn on_rails_set_drive_follow_entity(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    target: u64,
+    monotonic: bool,
+) -> DropbearNativeResult<()> {
+    let target_entity =
+        hecs::Entity::from_bits(target).ok_or(DropbearNativeError::InvalidEntity)?;
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.drive = RailDrive::FollowEntity { target: target_entity, monotonic };
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "setDriveAxisDriven"
+    ),
+    c
+)]
+fn on_rails_set_drive_axis_driven(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+    target: u64,
+    axis: &NVector3,
+    range_min: f32,
+    range_max: f32,
+) -> DropbearNativeResult<()> {
+    let target_entity =
+        hecs::Entity::from_bits(target).ok_or(DropbearNativeError::InvalidEntity)?;
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.drive = RailDrive::AxisDriven {
+        target: target_entity,
+        axis: Vec3::new(axis.x as f32, axis.y as f32, axis.z as f32),
+        range: (range_min, range_max),
+    };
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "setDriveManual"
+    ),
+    c
+)]
+fn on_rails_set_drive_manual(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<()> {
+    let mut rails = world
+        .get::<&mut OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    rails.drive = RailDrive::Manual;
+    Ok(())
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveAutomaticSpeed"
+    ),
+    c
+)]
+fn on_rails_get_drive_automatic_speed(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f32> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::Automatic { speed, .. } = &rails.drive {
+        Ok(*speed)
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveAutomaticLooping"
+    ),
+    c
+)]
+fn on_rails_get_drive_automatic_looping(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::Automatic { looping, .. } = &rails.drive {
+        Ok(*looping)
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveFollowEntityTarget"
+    ),
+    c
+)]
+fn on_rails_get_drive_follow_entity_target(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<u64> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::FollowEntity { target, .. } = &rails.drive {
+        Ok(target.to_bits().get())
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveFollowEntityMonotonic"
+    ),
+    c
+)]
+fn on_rails_get_drive_follow_entity_monotonic(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::FollowEntity { monotonic, .. } = &rails.drive {
+        Ok(*monotonic)
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveAxisDrivenTarget"
+    ),
+    c
+)]
+fn on_rails_get_drive_axis_driven_target(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<u64> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::AxisDriven { target, .. } = &rails.drive {
+        Ok(target.to_bits().get())
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveAxisDrivenAxis"
+    ),
+    c
+)]
+fn on_rails_get_drive_axis_driven_axis(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<NVector3> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::AxisDriven { axis, .. } = &rails.drive {
+        Ok(NVector3::from(*axis))
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveAxisDrivenRangeMin"
+    ),
+    c
+)]
+fn on_rails_get_drive_axis_driven_range_min(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f32> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::AxisDriven { range, .. } = &rails.drive {
+        Ok(range.0)
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.components.camera.OnRailsNative",
+        func = "getDriveAxisDrivenRangeMax"
+    ),
+    c
+)]
+fn on_rails_get_drive_axis_driven_range_max(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<f32> {
+    let rails = world
+        .get::<&OnRails>(entity)
+        .map_err(|_| DropbearNativeError::MissingComponent)?;
+    if let RailDrive::AxisDriven { range, .. } = &rails.drive {
+        Ok(range.1)
+    } else {
+        Err(DropbearNativeError::InvalidArgument)
+    }
+}
diff --git a/crates/eucalyptus-exports/src/utils/mod.rs b/crates/eucalyptus-exports/src/utils/mod.rs
new file mode 100644
index 0000000..3544191
--- /dev/null
+++ b/crates/eucalyptus-exports/src/utils/mod.rs
@@ -0,0 +1 @@
+pub mod ty;
\ No newline at end of file
diff --git a/crates/eucalyptus-exports/src/utils/ty.rs b/crates/eucalyptus-exports/src/utils/ty.rs
new file mode 100644
index 0000000..730d15e
--- /dev/null
+++ b/crates/eucalyptus-exports/src/utils/ty.rs
@@ -0,0 +1,58 @@
+pub use eucalyptus_core::types::NColour;
+
+use jni::objects::JObject;
+use jni::{jni_sig, jni_str, Env, JValue};
+use eucalyptus_core::scripting::native::DropbearNativeError;
+use eucalyptus_core::scripting::result::DropbearNativeResult;
+use crate::{FromJObject, ToJObject};
+
+impl FromJObject for NColour {
+    fn from_jobject(env: &mut Env, obj: &JObject) -> DropbearNativeResult<Self> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/utils/Colour"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        if !env
+            .is_instance_of(obj, &class)
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
+        {
+            return Err(DropbearNativeError::InvalidArgument);
+        }
+
+        let mut get_byte = |field| -> DropbearNativeResult<u8> {
+            let v = env
+                .get_field(obj, field, jni_sig!(byte))
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .b()
+                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+            Ok(v as u8)
+        };
+
+        Ok(Self {
+            r: get_byte(jni_str!("r"))?,
+            g: get_byte(jni_str!("g"))?,
+            b: get_byte(jni_str!("b"))?,
+            a: get_byte(jni_str!("a"))?,
+        })
+    }
+}
+
+impl ToJObject for NColour {
+    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .load_class(jni_str!("com/dropbear/utils/Colour"))
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        env.new_object(
+            &class,
+            jni_sig!((byte, byte, byte, byte) -> void),
+            &[
+                JValue::Byte(self.r as i8),
+                JValue::Byte(self.g as i8),
+                JValue::Byte(self.b as i8),
+                JValue::Byte(self.a as i8),
+            ],
+        )
+        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
+    }
+}
diff --git a/include/dropbear.h b/include/dropbear.h
index 8cb0d38..1b85def 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -16,58 +16,6 @@ typedef enum AssetKind {
     AssetKind_Model = 1,
 } AssetKind;
 
-typedef struct NVector3 {
-    double x;
-    double y;
-    double z;
-} NVector3;
-
-typedef enum ColliderShapeTag {
-    ColliderShapeTag_Box = 0,
-    ColliderShapeTag_Sphere = 1,
-    ColliderShapeTag_Capsule = 2,
-    ColliderShapeTag_Cylinder = 3,
-    ColliderShapeTag_Cone = 4,
-} ColliderShapeTag;
-
-typedef struct ColliderShapeBox {
-    NVector3 half_extents;
-} ColliderShapeBox;
-
-typedef struct ColliderShapeSphere {
-    float radius;
-} ColliderShapeSphere;
-
-typedef struct ColliderShapeCapsule {
-    float half_height;
-    float radius;
-} ColliderShapeCapsule;
-
-typedef struct ColliderShapeCylinder {
-    float half_height;
-    float radius;
-} ColliderShapeCylinder;
-
-typedef struct ColliderShapeCone {
-    float half_height;
-    float radius;
-} ColliderShapeCone;
-
-typedef union ColliderShapeData {
-    ColliderShapeBox Box;
-    ColliderShapeSphere Sphere;
-    ColliderShapeCapsule Capsule;
-    ColliderShapeCylinder Cylinder;
-    ColliderShapeCone Cone;
-} ColliderShapeData;
-
-typedef struct ColliderShapeFfi {
-    ColliderShapeTag tag;
-    ColliderShapeData data;
-} ColliderShapeFfi;
-
-typedef ColliderShapeFfi ColliderShape;
-
 typedef enum NAnimationInterpolationTag {
     NAnimationInterpolationTag_Linear = 0,
     NAnimationInterpolationTag_Step = 1,
@@ -96,18 +44,15 @@ typedef struct NAnimationInterpolationFfi {
 
 typedef NAnimationInterpolationFfi NAnimationInterpolation;
 
+typedef struct NVector3 NVector3;// opaque
+
 typedef struct NVector3Array {
     NVector3* values;
     size_t length;
     size_t capacity;
 } NVector3Array;
 
-typedef struct NQuaternion {
-    double x;
-    double y;
-    double z;
-    double w;
-} NQuaternion;
+typedef struct NQuaternion NQuaternion;// opaque
 
 typedef struct NQuaternionArray {
     NQuaternion* values;
@@ -158,51 +103,11 @@ typedef struct NChannelValuesFfi {
 
 typedef NChannelValuesFfi NChannelValues;
 
-typedef enum NShapeCastStatusTag {
-    NShapeCastStatusTag_OutOfIterations = 0,
-    NShapeCastStatusTag_Converged = 1,
-    NShapeCastStatusTag_Failed = 2,
-    NShapeCastStatusTag_PenetratingOrWithinTargetDist = 3,
-} NShapeCastStatusTag;
-
-typedef struct NShapeCastStatusOutOfIterations {
-} NShapeCastStatusOutOfIterations;
-
-typedef struct NShapeCastStatusConverged {
-} NShapeCastStatusConverged;
-
-typedef struct NShapeCastStatusFailed {
-} NShapeCastStatusFailed;
-
-typedef struct NShapeCastStatusPenetratingOrWithinTargetDist {
-} NShapeCastStatusPenetratingOrWithinTargetDist;
-
-typedef union NShapeCastStatusData {
-    NShapeCastStatusOutOfIterations OutOfIterations;
-    NShapeCastStatusConverged Converged;
-    NShapeCastStatusFailed Failed;
-    NShapeCastStatusPenetratingOrWithinTargetDist PenetratingOrWithinTargetDist;
-} NShapeCastStatusData;
-
-typedef struct NShapeCastStatusFfi {
-    NShapeCastStatusTag tag;
-    NShapeCastStatusData data;
-} NShapeCastStatusFfi;
-
-typedef NShapeCastStatusFfi NShapeCastStatus;
-
 typedef void* AssetRegistryPtr;
 
-typedef struct AxisLock {
-    bool x;
-    bool y;
-    bool z;
-} AxisLock;
+typedef struct AxisLock AxisLock;// opaque
 
-typedef struct IndexNative {
-    uint32_t index;
-    uint32_t generation;
-} IndexNative;
+typedef struct IndexNative IndexNative;// opaque
 
 typedef struct IndexNativeArray {
     IndexNative* values;
@@ -210,28 +115,17 @@ typedef struct IndexNativeArray {
     size_t capacity;
 } IndexNativeArray;
 
+// NOTE: type is not #[repr(C)] in Rust; ensure C ABI safety.
 typedef struct CharacterCollisionArray {
     uint64_t entity_id;
     IndexNativeArray collisions;
 } CharacterCollisionArray;
 
-typedef struct CharacterMovementResult {
-    NVector3 translation;
-    bool grounded;
-    bool is_sliding_down_slope;
-} CharacterMovementResult;
+typedef struct CharacterMovementResult CharacterMovementResult;// opaque
 
-typedef void* CommandBufferPtr;
+typedef struct ColliderShape ColliderShape;// opaque
 
-typedef struct u64Array {
-    uint64_t* values;
-    size_t length;
-    size_t capacity;
-} u64Array;
-
-typedef struct ConnectedGamepadIds {
-    u64Array ids;
-} ConnectedGamepadIds;
+typedef void* CommandBufferPtr;
 
 typedef void* GraphicsContextPtr;
 
@@ -274,11 +168,7 @@ typedef struct NAttenuation {
     float quadratic;
 } NAttenuation;
 
-typedef struct NCollider {
-    IndexNative index;
-    uint64_t entity_id;
-    uint32_t id;
-} NCollider;
+typedef struct NCollider NCollider;// opaque
 
 typedef struct NColliderArray {
     NCollider* values;
@@ -286,24 +176,11 @@ typedef struct NColliderArray {
     size_t capacity;
 } NColliderArray;
 
-typedef struct NColour {
-    uint8_t r;
-    uint8_t g;
-    uint8_t b;
-    uint8_t a;
-} NColour;
-
-typedef struct NVector4 {
-    double x;
-    double y;
-    double z;
-    double w;
-} NVector4;
-
-typedef struct NVector2 {
-    double x;
-    double y;
-} NVector2;
+typedef struct NColour NColour;// opaque
+
+typedef struct NVector4 NVector4;// opaque
+
+typedef struct NVector2 NVector2;// opaque
 
 typedef struct NMaterial {
     const char* name;
@@ -388,15 +265,9 @@ typedef struct NRange {
     float end;
 } NRange;
 
-typedef struct NShapeCastHit {
-    NCollider collider;
-    double distance;
-    NVector3 witness1;
-    NVector3 witness2;
-    NVector3 normal1;
-    NVector3 normal2;
-    NShapeCastStatus status;
-} NShapeCastHit;
+typedef struct NShapeCastHit NShapeCastHit;// opaque
+
+typedef struct NShapeCastStatus NShapeCastStatus;// opaque
 
 typedef struct NSkin {
     const char* name;
@@ -411,29 +282,15 @@ typedef struct NSkinArray {
     size_t capacity;
 } NSkinArray;
 
-typedef struct NTransform {
-    NVector3 position;
-    NQuaternion rotation;
-    NVector3 scale;
-} NTransform;
+typedef struct NTransform NTransform;// opaque
 
 typedef void* PhysicsStatePtr;
 
-typedef struct Progress {
-    size_t current;
-    size_t total;
-    const char* message;
-} Progress;
+typedef struct Progress Progress;// opaque
 
-typedef struct RayHit {
-    NCollider collider;
-    double distance;
-} RayHit;
+typedef struct RayHit RayHit;// opaque
 
-typedef struct RigidBodyContext {
-    IndexNative index;
-    uint64_t entity_id;
-} RigidBodyContext;
+typedef struct RigidBodyContext RigidBodyContext;// opaque
 
 typedef void* SceneLoaderPtr;
 
@@ -447,6 +304,12 @@ typedef struct StringArray {
 
 typedef void* WorldPtr;
 
+typedef struct u64Array {
+    uint64_t* values;
+    size_t length;
+    size_t capacity;
+} u64Array;
+
 int32_t dropbear_animation_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
 int32_t dropbear_animation_get_active_animation_index(WorldPtr world, uint64_t entity, int32_t* out0, bool* out0_present);
 int32_t dropbear_animation_get_available_animations(WorldPtr world, uint64_t entity, StringArray* out0);
@@ -491,16 +354,6 @@ int32_t dropbear_camera_set_up(WorldPtr world, uint64_t entity, const NVector3* 
 int32_t dropbear_camera_set_yaw(WorldPtr world, uint64_t entity, double yaw);
 int32_t dropbear_camera_set_zfar(WorldPtr world, uint64_t entity, double zfar);
 int32_t dropbear_camera_set_znear(WorldPtr world, uint64_t entity, double znear);
-int32_t dropbear_character_collision_get_character_collision_collider(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NCollider* out0);
-int32_t dropbear_character_collision_get_character_collision_normal1(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
-int32_t dropbear_character_collision_get_character_collision_normal2(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
-int32_t dropbear_character_collision_get_character_collision_position(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NTransform* out0);
-int32_t dropbear_character_collision_get_character_collision_status(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NShapeCastStatus* out0);
-int32_t dropbear_character_collision_get_character_collision_time_of_impact(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, double* out0);
-int32_t dropbear_character_collision_get_character_collision_translation_applied(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
-int32_t dropbear_character_collision_get_character_collision_translation_remaining(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
-int32_t dropbear_character_collision_get_character_collision_witness1(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
-int32_t dropbear_character_collision_get_character_collision_witness2(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
 int32_t dropbear_collider_get_collider_density(PhysicsStatePtr physics, const NCollider* collider, double* out0);
 int32_t dropbear_collider_get_collider_friction(PhysicsStatePtr physics, const NCollider* collider, double* out0);
 int32_t dropbear_collider_get_collider_is_sensor(PhysicsStatePtr physics, const NCollider* collider, bool* out0);
@@ -509,8 +362,6 @@ int32_t dropbear_collider_get_collider_restitution(PhysicsStatePtr physics, cons
 int32_t dropbear_collider_get_collider_rotation(PhysicsStatePtr physics, const NCollider* collider, NVector3* out0);
 int32_t dropbear_collider_get_collider_shape(PhysicsStatePtr physics, const NCollider* collider, ColliderShape* out0);
 int32_t dropbear_collider_get_collider_translation(PhysicsStatePtr physics, const NCollider* collider, NVector3* out0);
-int32_t dropbear_collider_group_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
-int32_t dropbear_collider_group_get_colliders(WorldPtr world, PhysicsStatePtr physics, uint64_t entity, NColliderArray* out0);
 int32_t dropbear_collider_set_collider_density(PhysicsStatePtr physics, const NCollider* collider, double density);
 int32_t dropbear_collider_set_collider_friction(PhysicsStatePtr physics, const NCollider* collider, double friction);
 int32_t dropbear_collider_set_collider_is_sensor(PhysicsStatePtr physics, const NCollider* collider, bool is_sensor);
@@ -539,13 +390,13 @@ int32_t dropbear_entity_get_children(WorldPtr world, uint64_t entity, u64Array* 
 int32_t dropbear_entity_get_label(WorldPtr world, uint64_t entity, char** out0);
 int32_t dropbear_entity_get_parent(WorldPtr world, uint64_t entity, uint64_t* out0, bool* out0_present);
 int32_t dropbear_entity_label_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
-int32_t dropbear_gamepad_get_left_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
-int32_t dropbear_gamepad_get_right_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
-int32_t dropbear_gamepad_is_button_pressed(InputStatePtr input, uint64_t gamepad_id, int32_t button_ordinal, bool* out0);
-int32_t dropbear_input_get_connected_gamepads(InputStatePtr input, ConnectedGamepadIds* out0);
+int32_t dropbear_input_get_connected_gamepads(InputStatePtr input, u64Array* out0);
 int32_t dropbear_input_get_last_mouse_pos(InputStatePtr input, NVector2* out0);
+int32_t dropbear_input_get_left_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
 int32_t dropbear_input_get_mouse_delta(InputStatePtr input, NVector2* out0);
 int32_t dropbear_input_get_mouse_position(InputStatePtr input, NVector2* out0);
+int32_t dropbear_input_get_right_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
+int32_t dropbear_input_is_button_pressed(InputStatePtr input, uint64_t gamepad_id, int32_t button_ordinal, bool* out0);
 int32_t dropbear_input_is_cursor_hidden(InputStatePtr input, bool* out0);
 int32_t dropbear_input_is_cursor_locked(InputStatePtr input, bool* out0);
 int32_t dropbear_input_is_key_pressed(InputStatePtr input, int32_t key_code, bool* out0);
@@ -553,6 +404,16 @@ int32_t dropbear_input_is_mouse_button_pressed(InputStatePtr input, int32_t butt
 int32_t dropbear_input_print_input_state(InputStatePtr input);
 int32_t dropbear_input_set_cursor_hidden(CommandBufferPtr command_buffer, InputStatePtr input, bool hidden);
 int32_t dropbear_input_set_cursor_locked(CommandBufferPtr command_buffer, InputStatePtr input, bool locked);
+int32_t dropbear_kcc_get_character_collision_collider(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NCollider* out0);
+int32_t dropbear_kcc_get_character_collision_normal1(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
+int32_t dropbear_kcc_get_character_collision_normal2(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
+int32_t dropbear_kcc_get_character_collision_position(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NTransform* out0);
+int32_t dropbear_kcc_get_character_collision_status(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NShapeCastStatus* out0);
+int32_t dropbear_kcc_get_character_collision_time_of_impact(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, double* out0);
+int32_t dropbear_kcc_get_character_collision_translation_applied(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
+int32_t dropbear_kcc_get_character_collision_translation_remaining(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
+int32_t dropbear_kcc_get_character_collision_witness1(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
+int32_t dropbear_kcc_get_character_collision_witness2(WorldPtr world, uint64_t entity, const IndexNative* collision_handle, NVector3* out0);
 int32_t dropbear_kcc_get_hit(WorldPtr world, uint64_t entity, CharacterCollisionArray* out0);
 int32_t dropbear_kcc_get_movement_result(WorldPtr world, uint64_t entity, CharacterMovementResult* out0, bool* out0_present);
 int32_t dropbear_kcc_kcc_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
@@ -630,13 +491,13 @@ int32_t dropbear_rigidbody_set_rigidbody_lock_rotation(WorldPtr world, PhysicsSt
 int32_t dropbear_rigidbody_set_rigidbody_lock_translation(WorldPtr world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, const AxisLock* lock_translation);
 int32_t dropbear_rigidbody_set_rigidbody_mode(WorldPtr world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, int32_t mode);
 int32_t dropbear_rigidbody_set_rigidbody_sleep(WorldPtr world, PhysicsStatePtr physics, const RigidBodyContext* rigidbody, bool sleep);
-int32_t dropbear_scripting_get_scene_load_handle_scene_name(SceneLoaderPtr scene_loader, uint64_t scene_id, char** out0);
-int32_t dropbear_scripting_get_scene_load_progress(SceneLoaderPtr scene_loader, uint64_t scene_id, Progress* out0);
-int32_t dropbear_scripting_get_scene_load_status(SceneLoaderPtr scene_loader, uint64_t scene_id, uint32_t* out0);
-int32_t dropbear_scripting_load_scene_async(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, const char* scene_name, uint64_t* out0);
-int32_t dropbear_scripting_load_scene_async_with_loading(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, const char* scene_name, const char* loading_scene, uint64_t* out0);
-int32_t dropbear_scripting_switch_to_scene_async(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, uint64_t scene_id);
-int32_t dropbear_scripting_switch_to_scene_immediate(CommandBufferPtr command_buffer, const char* scene_name);
+int32_t dropbear_scene_get_scene_load_handle_scene_name(SceneLoaderPtr scene_loader, uint64_t scene_id, char** out0);
+int32_t dropbear_scene_get_scene_load_progress(SceneLoaderPtr scene_loader, uint64_t scene_id, Progress* out0);
+int32_t dropbear_scene_get_scene_load_status(SceneLoaderPtr scene_loader, uint64_t scene_id, uint32_t* out0);
+int32_t dropbear_scene_load_scene_async(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, const char* scene_name, uint64_t* out0);
+int32_t dropbear_scene_load_scene_async_with_loading(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, const char* scene_name, const char* loading_scene, uint64_t* out0);
+int32_t dropbear_scene_switch_to_scene_async(CommandBufferPtr command_buffer, SceneLoaderPtr scene_loader, uint64_t scene_id);
+int32_t dropbear_scene_switch_to_scene_immediate(CommandBufferPtr command_buffer, const char* scene_name);
 int32_t dropbear_transform_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
 int32_t dropbear_transform_get_local_transform(WorldPtr world, uint64_t entity, NTransform* out0);
 int32_t dropbear_transform_get_world_transform(WorldPtr world, uint64_t entity, NTransform* out0);