kitgit

tirbofish/dropbear · commit

0e5688b8b82c6becaf0a2eb99965ae6bd59f7604

wip: component should support kotlin wip: debug drawing for camera splines

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-03-26 08:57

view full diff

diff --git a/crates/dropbear-engine/src/debug.rs b/crates/dropbear-engine/src/debug.rs
new file mode 100644
index 0000000..e38f424
--- /dev/null
+++ b/crates/dropbear-engine/src/debug.rs
@@ -0,0 +1,59 @@
+use std::sync::Arc;
+use wgpu::{RenderPipeline, RenderPipelineDescriptor, VertexState};
+use crate::graphics::SharedGraphicsContext;
+
+pub struct DebugLine {
+
+}
+
+pub struct DebugDraw {
+    pipeline: Arc<DebugDrawPipeline>,
+}
+
+impl DebugDraw {
+    pub fn draw_line(&self) {
+
+    }
+}
+
+pub struct DebugDrawPipeline {
+    pipeline: RenderPipeline,
+}
+
+impl DebugDrawPipeline {
+    pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
+        let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("debug draw pipeline layout"),
+            bind_group_layouts: &[],
+            push_constant_ranges: &[],
+        });
+
+        let pipeline = graphics.device.create_render_pipeline(&RenderPipelineDescriptor {
+            label: Some("debug draw render pipeline"),
+            layout: Some(&pipeline_layout),
+            vertex: VertexState {
+                module: &(),
+                entry_point: None,
+                compilation_options: Default::default(),
+                buffers: &[],
+            },
+            primitive: Default::default(),
+            depth_stencil: None,
+            multisample: Default::default(),
+            fragment: None,
+            multiview: None,
+            cache: None,
+        });
+    }
+
+    pub fn draw(&self, graphics: Arc<SharedGraphicsContext>) {
+
+    }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct DebugVertex {
+    pub position: [f32; 3],
+    pub color: [f32; 4],
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index c97fd51..650d786 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -24,6 +24,7 @@ pub mod texture;
 pub mod utils;
 pub mod multisampling;
 pub mod billboarding;
+mod debug;
 
 features! {
     pub mod feature_list {
diff --git a/crates/dropbear-engine/src/shaders/basic.wgsl b/crates/dropbear-engine/src/shaders/basic.wgsl
new file mode 100644
index 0000000..d5ac96e
--- /dev/null
+++ b/crates/dropbear-engine/src/shaders/basic.wgsl
@@ -0,0 +1,29 @@
+// basic.wgsl - shader used for drawing debug stuff like lines and shii
+
+struct CameraUniform {
+    view_proj: mat4x4<f32>,
+}
+@group(0) @binding(0) var<uniform> camera: CameraUniform;
+
+struct VertexIn {
+    @location(0) position: vec3<f32>,
+    @location(1) color: vec4<f32>,
+}
+
+struct VertexOut {
+    @builtin(position) clip_position: vec4<f32>,
+    @location(0) color: vec4<f32>,
+}
+
+@vertex
+fn vs_main(in: VertexIn) -> VertexOut {
+    var out: VertexOut;
+    out.clip_position = camera.view_proj * vec4<f32>(in.position, 1.0);
+    out.color = in.color;
+    return out;
+}
+
+@fragment
+fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
+    return in.color;
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index dad4208..83c98ed 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -29,27 +29,27 @@ pub const DRAGGED_ASSET_ID: &str = "dragged_asset_reference";
 
 pub struct ComponentRegistry {
     /// Maps TypeId to ComponentDescriptor for quick lookups
-    descriptors: HashMap<TypeId, ComponentDescriptor>,
+    descriptors: HashMap<LanguageTypeId, ComponentDescriptor>,
     /// Maps fully qualified type name to TypeId for lookups by string
-    fqtn_to_type: HashMap<String, TypeId>,
+    fqtn_to_type: HashMap<String, LanguageTypeId>,
     /// Maps category name to list of TypeIds in that category
-    categories: HashMap<String, Vec<TypeId>>,
+    categories: HashMap<String, Vec<LanguageTypeId>>,
     /// Maps serialized TypeId to component TypeId
-    serialized_to_component: HashMap<TypeId, TypeId>,
+    serialized_to_component: HashMap<LanguageTypeId, LanguageTypeId>,
     /// Functions that extract and serialize components from entities
-    extractors: HashMap<TypeId, ExtractorFn>,
+    extractors: HashMap<LanguageTypeId, ExtractorFn>,
     /// Functions that allow for the entity to load.
-    loaders: HashMap<TypeId, LoaderFn>,
+    loaders: HashMap<LanguageTypeId, LoaderFn>,
     /// Functions that update the contents of the component.
-    updaters: HashMap<TypeId, UpdateFn>,
+    updaters: HashMap<LanguageTypeId, UpdateFn>,
     /// Functions that create default serialized components.
-    defaults: HashMap<TypeId, DefaultFn>,
+    defaults: HashMap<LanguageTypeId, DefaultFn>,
     /// Functions that remove components by type.
-    removers: HashMap<TypeId, RemoveFn>,
+    removers: HashMap<LanguageTypeId, RemoveFn>,
     /// Functions that find entities with a component.
-    finders: HashMap<TypeId, FindFn>,
+    finders: HashMap<LanguageTypeId, FindFn>,
     /// Allows for inspecting the component in the Resource Inspector dock.
-    inspectors: HashMap<TypeId, InspectFn>,
+    inspectors: HashMap<LanguageTypeId, InspectFn>,
 }
 
 /// Describes a handy little future for [`Component::init`], which deals with initialising a component from its serialized form.
@@ -93,6 +93,12 @@ type InspectFn = Box<
 
 // fn inspect(&mut self, world: &hecs::World, entity: hecs::Entity, ui: &mut egui::Ui, graphics: Arc<SharedGraphicsContext>);
 
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum LanguageTypeId {
+    Rust(TypeId),
+    Kotlin(String),
+}
+
 impl ComponentRegistry {
     pub fn new() -> Self {
         Self {
@@ -117,23 +123,23 @@ impl ComponentRegistry {
         T::SerializedForm: 'static + Default,
         T::RequiredComponentTypes: Send + Sync,
     {
-        let type_id = TypeId::of::<T>();
-        let serialized_type_id = TypeId::of::<T::SerializedForm>();
+        let type_id = LanguageTypeId::Rust(TypeId::of::<T>());
+        let serialized_type_id = LanguageTypeId::Rust(TypeId::of::<T::SerializedForm>());
         let desc = T::descriptor();
 
-        self.fqtn_to_type.insert(desc.fqtn.clone(), type_id);
+        self.fqtn_to_type.insert(desc.fqtn.clone(), type_id.clone());
         if let Some(ref cat) = desc.category {
             self.categories
                 .entry(cat.clone())
                 .or_default()
-                .push(type_id);
+                .push(type_id.clone());
         }
-        self.descriptors.insert(type_id, desc);
+        self.descriptors.insert(type_id.clone(), desc);
         self.serialized_to_component
-            .insert(serialized_type_id, type_id);
+            .insert(serialized_type_id.clone(), type_id.clone());
 
         self.extractors.insert(
-            type_id,
+            type_id.clone(),
             Box::new(|world, entity| {
                 let Ok(c) = world.get::<&T>(entity) else {
                     return None;
@@ -162,17 +168,17 @@ impl ComponentRegistry {
         );
 
         self.defaults
-            .insert(type_id, Box::new(|| Box::new(T::SerializedForm::default())));
+            .insert(type_id.clone(), Box::new(|| Box::new(T::SerializedForm::default())));
 
         self.removers.insert(
-            type_id,
+            type_id.clone(),
             Box::new(|world, entity| {
                 let _ = world.remove_one::<T>(entity);
             }),
         );
 
         self.finders.insert(
-            type_id,
+            type_id.clone(),
             Box::new(|world| {
                 world
                     .query::<(hecs::Entity, &T)>()
@@ -184,7 +190,7 @@ impl ComponentRegistry {
 
         let disabled_flags = T::descriptor().disabled_flags;
         self.updaters.insert(
-            type_id,
+            type_id.clone(),
             Box::new(move |world, physics, dt, graphics| {
                 let world_ptr = world as *mut hecs::World; // safe assuming world is kept at the DropbearAppBuilder application level (lifetime)
                 let mut query = world.query::<(hecs::Entity, &mut T)>();
@@ -220,7 +226,7 @@ impl ComponentRegistry {
 
     /// Get descriptor for a specific component type
     pub fn get_descriptor<T: Component + 'static>(&self) -> Option<&ComponentDescriptor> {
-        self.descriptors.get(&TypeId::of::<T>())
+        self.descriptors.get(&LanguageTypeId::Rust(TypeId::of::<T>()))
     }
 
     /// Get descriptor by fully qualified type name
@@ -255,12 +261,12 @@ impl ComponentRegistry {
 
     /// Check if a component type is registered
     pub fn is_registered<T: Component + 'static>(&self) -> bool {
-        self.descriptors.contains_key(&TypeId::of::<T>())
+        self.descriptors.contains_key(&LanguageTypeId::Rust(TypeId::of::<T>()))
     }
 
-    /// Get the TypeId for a component by its fully qualified type name
-    pub fn get_type_id(&self, fqtn: &str) -> Option<TypeId> {
-        self.fqtn_to_type.get(fqtn).copied()
+    /// Get the LanguageTypeId for a component by its fully qualified type name
+    pub fn get_type_id(&self, fqtn: &str) -> Option<LanguageTypeId> {
+        self.fqtn_to_type.get(fqtn).cloned()
     }
 
     /// Get count of registered components
@@ -272,7 +278,7 @@ impl ComponentRegistry {
     pub fn iter_available_components(&self) -> impl Iterator<Item = (u64, &ComponentDescriptor)> {
         self.descriptors
             .iter()
-            .map(|(type_id, desc)| (Self::numeric_id(*type_id), desc))
+            .map(|(type_id, desc)| (Self::numeric_id(type_id), desc))
     }
 
     /// Extract all registered components from an entity
@@ -293,7 +299,7 @@ impl ComponentRegistry {
         world: &hecs::World,
         entity: hecs::Entity,
     ) -> Option<Box<dyn SerializedComponent>> {
-        let type_id = TypeId::of::<T>();
+        let type_id = LanguageTypeId::Rust(TypeId::of::<T>());
         self.extractors
             .get(&type_id)
             .and_then(|extractor| extractor(world, entity))
@@ -323,10 +329,9 @@ impl ComponentRegistry {
 
     /// Gets the numeric id for a serialized component instance.
     pub fn id_for_component(&self, component: &dyn SerializedComponent) -> Option<u64> {
-        let serialized_type_id = component.as_any().type_id();
+        let serialized_type_id = LanguageTypeId::Rust(component.as_any().type_id());
         self.serialized_to_component
             .get(&serialized_type_id)
-            .copied()
             .map(Self::numeric_id)
     }
 
@@ -366,7 +371,7 @@ impl ComponentRegistry {
         serialized: &'a dyn SerializedComponent,
         graphics: Arc<SharedGraphicsContext>,
     ) -> Option<LoaderFuture<'a>> {
-        let serialized_type_id = serialized.as_any().type_id();
+        let serialized_type_id = LanguageTypeId::Rust(serialized.as_any().type_id());
         self.loaders
             .get(&serialized_type_id)
             .map(|loader| loader(serialized, graphics))
@@ -403,7 +408,7 @@ impl ComponentRegistry {
 
         let type_ids = world
             .entity(entity)
-            .map(|e| e.component_types().collect::<Vec<_>>())
+            .map(|e| e.component_types().map(LanguageTypeId::Rust).collect::<Vec<_>>())
             .unwrap_or_default();
 
         for type_id in type_ids {
@@ -421,22 +426,18 @@ impl ComponentRegistry {
         }
     }
 
-    fn numeric_id(type_id: TypeId) -> u64 {
-        use std::hash::{Hash, Hasher};
+    fn numeric_id(type_id: &LanguageTypeId) -> u64 {
         let mut hasher = std::collections::hash_map::DefaultHasher::new();
         type_id.hash(&mut hasher);
-        let mut id = hasher.finish();
-        if id == 0 {
-            id = 1;
-        }
-        id
+        let id = hasher.finish();
+        if id == 0 { 1 } else { id }
     }
 
-    fn type_id_from_numeric_id(&self, id: u64) -> Option<TypeId> {
+    fn type_id_from_numeric_id(&self, id: u64) -> Option<LanguageTypeId> {
         self.descriptors
             .keys()
-            .copied()
-            .find(|type_id| Self::numeric_id(*type_id) == id)
+            .find(|&type_id| Self::numeric_id(type_id) == id)
+            .cloned()
     }
 }
 
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index d1b71fe..8cf1095 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -1,5 +1,3 @@
-extern crate core;
-
 pub mod animation;
 pub mod asset;
 pub mod billboard;
@@ -30,6 +28,7 @@ pub mod ser;
 pub mod metadata;
 pub mod resource;
 pub mod uuid;
+pub mod plugin;
 
 pub use dropbear_macro as macros;
 
diff --git a/crates/eucalyptus-core/src/plugin.rs b/crates/eucalyptus-core/src/plugin.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/eucalyptus-core/src/plugin.rs
diff --git a/crates/magna-carta/src/lib.rs b/crates/magna-carta/src/lib.rs
index 5ad17dd..9ad5a5e 100644
--- a/crates/magna-carta/src/lib.rs
+++ b/crates/magna-carta/src/lib.rs
@@ -62,6 +62,27 @@ pub struct ManifestItem {
     file_path: PathBuf,
 }
 
+/// Represents a single user-defined Kotlin component discovered via `@EcsComponent`.
+#[derive(Debug, Clone)]
+pub struct ComponentManifestItem {
+    /// Fully qualified class name, e.g. `com.game.PlayerHealth`
+    fqcn: String,
+    /// Simple name of the class, e.g. `PlayerHealth`
+    simple_name: String,
+    /// Path to the source file
+    file_path: PathBuf,
+}
+
+impl ComponentManifestItem {
+    pub fn new(fqcn: String, simple_name: String, file_path: PathBuf) -> Self {
+        Self { fqcn, simple_name, file_path }
+    }
+
+    pub fn fqcn(&self) -> &str { &self.fqcn }
+    pub fn simple_name(&self) -> &str { &self.simple_name }
+    pub fn file_path(&self) -> &PathBuf { &self.file_path }
+}
+
 impl ManifestItem {
     /// Creates a new manifest item from an fqcn (fully qualified class name), simple name, tags
     /// and file_path.
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/Camera.kt b/scripting/commonMain/kotlin/com/dropbear/components/Camera.kt
index 7593614..2e9a05b 100644
--- a/scripting/commonMain/kotlin/com/dropbear/components/Camera.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/components/Camera.kt
@@ -10,7 +10,10 @@ import com.dropbear.math.Vector3d
  */
 class Camera(
     internal val entity: EntityId,
-): Component(entity, "Camera3D") {
+): Component(
+    fullyQualifiedTypeName = "dropbear_engine::camera::Camera",
+    typeName = "Camera3D",
+) {
     /**
      * The eye/position of the camera.
      */
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/CustomProperties.kt b/scripting/commonMain/kotlin/com/dropbear/components/CustomProperties.kt
index f25f7bc..9ab5dc7 100644
--- a/scripting/commonMain/kotlin/com/dropbear/components/CustomProperties.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/components/CustomProperties.kt
@@ -12,7 +12,10 @@ import com.dropbear.math.Vector3i
  *
  * The entity must include the `CustomProperties` component in the editor.
  */
-class CustomProperties(val id: EntityId): Component(id, "CustomProperties") {
+class CustomProperties(val id: EntityId): Component(
+    fullyQualifiedTypeName = "eucalyptus_core::properties::CustomProperties",
+    typeName = "CustomProperties",
+) {
     /**
      * Fetches the property of the ModelProperty component on the entity.
      *
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/EntityTransform.kt b/scripting/commonMain/kotlin/com/dropbear/components/EntityTransform.kt
index 75ce0b4..07f2947 100644
--- a/scripting/commonMain/kotlin/com/dropbear/components/EntityTransform.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/components/EntityTransform.kt
@@ -10,7 +10,10 @@ import com.dropbear.math.Transform
  *
  * This entity must contain the `EntityTransform` component to be queryable.
  */
-class EntityTransform(val id: EntityId): Component(id, "EntityTransform") {
+class EntityTransform(val id: EntityId): Component(
+    fullyQualifiedTypeName = "dropbear_engine::entity::EntityTransform",
+    typeName = "EntityTransform",
+) {
     /**
      * The local transform.
      *
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/Label.kt b/scripting/commonMain/kotlin/com/dropbear/components/Label.kt
index df2731e..c24a09e 100644
--- a/scripting/commonMain/kotlin/com/dropbear/components/Label.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/components/Label.kt
@@ -15,7 +15,10 @@ import com.dropbear.getEntityLabel
  */
 class Label(
     internal val entity: EntityId
-): Component(entity, "Label") {
+): Component(
+    fullyQualifiedTypeName = "eucalyptus_core::states::Label",
+    typeName = "Label",
+) {
     val name: String
         get() = EntityRef.getEntityLabel(entity)
 
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/MeshRenderer.kt b/scripting/commonMain/kotlin/com/dropbear/components/MeshRenderer.kt
index 3af12d6..4d08c80 100644
--- a/scripting/commonMain/kotlin/com/dropbear/components/MeshRenderer.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/components/MeshRenderer.kt
@@ -11,7 +11,10 @@ import com.dropbear.ecs.ComponentType
  *
  * It must require the `MeshRenderer` component in the editor to be queryable.
  */
-class MeshRenderer(val id: EntityId) : Component(id, "MeshRenderer") {
+class MeshRenderer(val id: EntityId) : Component(
+    fullyQualifiedTypeName = "dropbear_engine::entity::MeshRenderer",
+    typeName = "MeshRenderer",
+) {
 
     /**
      * The active model currently assigned to this entity.
diff --git a/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt b/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt
index d19eb87..a4a1031 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ecs/Component.kt
@@ -3,24 +3,47 @@ package com.dropbear.ecs
 import com.dropbear.EntityId
 
 /**
+ * Marks a class as a user-defined ECS component for the dropbear engine.
+ *
+ * Annotated classes must extend [Component]. The magna-carta tool scans for this annotation at
+ * build time to generate a [ComponentManager] that registers the component type with the native
+ * engine and dispatches lifecycle callbacks from Rust.
+ *
+ * Retention is [AnnotationRetention.RUNTIME] so JNI reflection can discover types at startup
+ * without relying on build-time scanning alone.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class EcsComponent
+/**
  * A custom property that *can* be attached to an entity in the dropbear ECS system.
  *
  * # Note
  * A [ComponentType] `companion object` is required to be implemented to be queryable. Take a look at
  * the documentation for the object.
  *
- * @property parentEntity The [EntityId] to which this component is attached.
- * @property typeName The string name of the component type as defined in Rust. The name is
- *                   found as the type name, such as `component_registry.register_with_default::<Camera3D>();`,
- *                   where the type name would be `"Camera3D"`.
+ *
  */
 abstract class Component(
-    private val parentEntity: EntityId,
-    internal val typeName: String,
+    val fullyQualifiedTypeName: String?,
+    val typeName: String?,
+    val category: String?,
+    val description: String?,
 ) {
-    override fun toString(): String {
-        return "Component(parentEntity: ${this.parentEntity}, typeName: $typeName)"
-    }
+    /**
+     * Ran on a component being attached to an entity.
+     */
+    abstract fun onAttach()
+
+    /**
+     * Ran on a component being updated.
+     */
+    abstract fun update()
+
+    /**
+     * Ran on a component being detached from an entity.
+     */
+    abstract fun onDetach()
 }
 
 /**
diff --git a/scripting/jvmMain/java/com/dropbear/components/ComponentNative.java b/scripting/jvmMain/java/com/dropbear/components/ComponentNative.java
new file mode 100644
index 0000000..5feae0c
--- /dev/null
+++ b/scripting/jvmMain/java/com/dropbear/components/ComponentNative.java
@@ -0,0 +1,40 @@
+package com.dropbear.components;
+
+import com.dropbear.EucalyptusCoreLoader;
+
+/**
+ * JNI bridge for Kotlin-defined ECS components.
+ *
+ * <p>The Rust side implements each {@code native} method. The component lifecycle
+ * (onAttach / update / onDetach) is driven by Rust calling back into the JVM's
+ * ComponentManager (generated by magna-carta for JVM targets); this class covers
+ * the Kotlin → Rust direction only.
+ */
+public class ComponentNative {
+    static {
+        new EucalyptusCoreLoader().ensureLoaded();
+    }
+
+    /**
+     * Returns {@code true} if the entity identified by {@code entityId} has a
+     * {@code KotlinComponent} entry keyed by {@code fqcn} in the Rust world.
+     */
+    public static native boolean hasKotlinComponent(long worldPtr, long entityId, String fqcn);
+
+    /**
+     * Registers a Kotlin component type with Rust's {@code ComponentRegistry} so that it is
+     * serialisable, inspectable in the editor, and dispatchable.
+     *
+     * <p>Call this once per type at startup, typically from the generated
+     * {@code ComponentManager.registerAll()}.
+     *
+     * @param category    may be null or empty
+     * @param description may be null or empty
+     */
+    public static native void registerKotlinComponent(
+            long worldPtr,
+            String fqcn,
+            String typeName,
+            String category,
+            String description);
+}
diff --git a/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt
new file mode 100644
index 0000000..d8333aa
--- /dev/null
+++ b/scripting/jvmMain/kotlin/com/dropbear/ecs/Component.jvm.kt
@@ -0,0 +1,21 @@
+package com.dropbear.ecs
+
+import com.dropbear.DropbearEngine
+import com.dropbear.EntityId
+import com.dropbear.components.ComponentNative
+
+internal actual fun hasKotlinComponent(entityId: EntityId, fqcn: String): Boolean =
+    ComponentNative.hasKotlinComponent(DropbearEngine.native.worldHandle, entityId.raw, fqcn)
+
+internal actual fun registerKotlinComponentType(
+    fqcn: String,
+    typeName: String?,
+    category: String?,
+    description: String?,
+) = ComponentNative.registerKotlinComponent(
+    DropbearEngine.native.worldHandle,
+    fqcn,
+    typeName ?: "",
+    category ?: "",
+    description ?: "",
+)
diff --git a/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt
new file mode 100644
index 0000000..ce42197
--- /dev/null
+++ b/scripting/nativeMain/kotlin/com/dropbear/ecs/Component.native.kt
@@ -0,0 +1,22 @@
+@file:OptIn(ExperimentalForeignApi::class)
+
+package com.dropbear.ecs
+
+import com.dropbear.DropbearEngine
+import com.dropbear.EntityId
+import com.dropbear.ffi.generated.*
+import kotlinx.cinterop.*
+
+internal actual fun hasKotlinComponent(entityId: EntityId, fqcn: String): Boolean = memScoped {
+    val world = DropbearEngine.native.worldHandle ?: return@memScoped false
+    dropbear_kotlin_component_exists(world, entityId.raw.toULong(), fqcn)
+}
+
+internal actual fun registerKotlinComponentType(
+    fqcn: String,
+    typeName: String?,
+    category: String?,
+    description: String?,
+) {
+    dropbear_register_kotlin_component(fqcn, typeName, category, description)
+}