kitgit

tirbofish/dropbear · commit

485d155dde0a20a063767c9afe4668425e12ec2a

added keyboard input detection for kotlin and windows native building works

Unverified

tk <4tkbytes@pm.me> · 2025-10-19 04:51

view full diff

diff --git a/.run/Build gradle.run.xml b/.run/Build gradle.run.xml
deleted file mode 100644
index 87c40f0..0000000
--- a/.run/Build gradle.run.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Build gradle" type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="build" />
-        </list>
-      </option>
-      <option name="vmOptions" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
-    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <RunAsTest>false</RunAsTest>
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Build literally everything.run.xml b/.run/Build literally everything.run.xml
deleted file mode 100644
index bf24eb2..0000000
--- a/.run/Build literally everything.run.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Build literally everything" type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="build" />
-        </list>
-      </option>
-      <option name="vmOptions" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
-    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <RunAsTest>false</RunAsTest>
-    <method v="2">
-      <option name="RunConfigurationTask" enabled="true" run_configuration_name="Build eucalyptus-editor" run_configuration_type="CargoCommandRunConfiguration" />
-    </method>
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Publish to GitHub Packages.run.xml b/.run/Publish to GitHub Packages.run.xml
deleted file mode 100644
index 3d068c8..0000000
--- a/.run/Publish to GitHub Packages.run.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Publish to GitHub Packages" type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="publish" />
-        </list>
-      </option>
-      <option name="vmOptions" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
-    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <RunAsTest>false</RunAsTest>
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Publish to Maven Local.run.xml b/.run/Publish to Maven Local.run.xml
deleted file mode 100644
index c6aec2a..0000000
--- a/.run/Publish to Maven Local.run.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Publish to Maven Local" type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="publishToMavenLocal" />
-        </list>
-      </option>
-      <option name="vmOptions" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
-    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <RunAsTest>false</RunAsTest>
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Run eucalyptus-editor.run.xml b/.run/Run eucalyptus-editor.run.xml
index 4c5ecc4..5b01a7e 100644
--- a/.run/Run eucalyptus-editor.run.xml
+++ b/.run/Run eucalyptus-editor.run.xml
@@ -14,6 +14,7 @@
     <option name="isRedirectInput" value="false" />
     <option name="redirectInputPath" value="" />
     <method v="2">
+      <option name="RunConfigurationTask" enabled="true" run_configuration_name="fatJar" run_configuration_type="GradleRunConfiguration" />
       <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
     </method>
   </configuration>
diff --git a/.run/fatJar.run.xml b/.run/fatJar.run.xml
new file mode 100644
index 0000000..cb795fa
--- /dev/null
+++ b/.run/fatJar.run.xml
@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="fatJar" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="fatJar" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index cb032b0..2d688e0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,6 +63,8 @@ tree-sitter-kotlin = "0.3.8"
 libloading = "0.8"
 indexmap = "2.11"
 rand = "0.9"
+num_enum = "0.7"
+sha2 = "0.10"
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/build.gradle.kts b/build.gradle.kts
index 53dfe23..5f3b449 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,5 +1,3 @@
-import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
-
 plugins {
     alias(libs.plugins.kotlinMultiplatform)
     alias(libs.plugins.kotlinxSerialization)
@@ -14,59 +12,52 @@ repositories {
     mavenCentral()
 }
 
+val hostOs = providers.systemProperty("os.name").get()
+val isArm64 = providers.systemProperty("os.arch").map { it == "aarch64" }.get()
+val isMingwX64 = hostOs.startsWith("Windows")
+val isLinux = hostOs == "Linux"
+val isMacOs = hostOs == "Mac OS X"
+
+val libName = when {
+    isMacOs -> "libeucalyptus_core.dylib"
+    isLinux -> "libeucalyptus_core.so"
+    isMingwX64 -> "eucalyptus_core.dll"
+    else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
+}
+
+val libPathProvider = provider {
+    val candidates = listOf(
+        layout.projectDirectory.file("target/debug/$libName").asFile,
+        layout.projectDirectory.file("target/release/$libName").asFile,
+        layout.projectDirectory.file("libs/$libName").asFile
+    )
+
+    candidates.firstOrNull { it.exists() }?.absolutePath
+        ?: println("No Rust library exists")
+}
+
 kotlin {
     jvm {
         withJava()
     }
 
-    val hostOs = System.getProperty("os.name")
-    val isArm64 = System.getProperty("os.arch") == "aarch64"
-    val isMingwX64 = hostOs.startsWith("Windows")
     val nativeTarget = when {
-        hostOs == "Mac OS X" && isArm64 -> macosArm64("nativeLib")
-        hostOs == "Mac OS X" && !isArm64 -> macosX64("nativeLib")
-        hostOs == "Linux" && isArm64 -> linuxArm64("nativeLib")
-        hostOs == "Linux" && !isArm64 -> linuxX64("nativeLib")
+        isMacOs && isArm64 -> macosArm64("nativeLib")
+        isMacOs && !isArm64 -> macosX64("nativeLib")
+        isLinux && isArm64 -> linuxArm64("nativeLib")
+        isLinux && !isArm64 -> linuxX64("nativeLib")
         isMingwX64 -> mingwX64("nativeLib")
         else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
     }
 
-    val libName = when {
-        hostOs == "Mac OS X" -> "libeucalyptus_core.dylib"
-        hostOs == "Linux" -> "libeucalyptus_core.so"
-        isMingwX64 -> "eucalyptus_core.dll"
-        else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
-    }
-
-    val (libDir, libNameForLinking) = when {
-        file("${project.rootDir}/target/debug").exists() -> {
-            val debugLibDir = "${project.rootDir}/target/debug"
-            if (isMingwX64) {
-                Pair(debugLibDir, "eucalyptus_core")
-            } else {
-                Pair(debugLibDir, "eucalyptus_core")
-            }
-        }
-        file("${project.rootDir}/target/release").exists() -> {
-            val releaseLibDir = "${project.rootDir}/target/release"
-            if (isMingwX64) {
-                Pair(releaseLibDir, "eucalyptus_core")
-            } else {
-                Pair(releaseLibDir, "eucalyptus_core")
-            }
-        }
-        file("${project.rootDir}/libs").exists() -> {
-            val libsDir = "${project.rootDir}/libs"
-            if (isMingwX64) {
-                Pair(libsDir, "eucalyptus_core")
-            } else {
-                Pair(libsDir, "eucalyptus_core")
-            }
-        }
-        else -> {
-            println("WARNING: Rust library directory not found!")
-            Pair(null, null)
-        }
+    val nativeLibPath = libPathProvider.get()
+    val nativeLibDir = file(nativeLibPath).parentFile.absolutePath
+    val nativeLibFileName = file(nativeLibPath).name
+    val nativeLibNameForLinking = when {
+        isMacOs -> nativeLibFileName.removePrefix("lib").removeSuffix(".dylib")
+        isLinux -> nativeLibFileName.removePrefix("lib").removeSuffix(".so")
+        isMingwX64 -> nativeLibFileName.removeSuffix(".dll")
+        else -> throw GradleException("Unsupported OS for library name derivation.")
     }
 
     nativeTarget.apply {
@@ -82,12 +73,14 @@ kotlin {
             sharedLib {
                 baseName = "dropbear"
 
-                if (libDir != null && libNameForLinking != null) {
-                    if (isMingwX64) {
-                        linkerOpts("${libDir}/${libName}.lib")
-                    } else {
-                        linkerOpts("-L${libDir}", "-l${libNameForLinking}")
-                    }
+                if (isLinux || isMacOs) {
+                    linkerOpts("-L$nativeLibDir", "-l$nativeLibNameForLinking", "-Wl,-rpath,\\\$ORIGIN")
+                } else if (isMingwX64) {
+                    val importLibName = "$nativeLibNameForLinking.lib"
+                    val importLibPath = file("$nativeLibDir/$importLibName").absolutePath
+                    linkerOpts(
+                        importLibPath
+                    )
                 }
             }
         }
@@ -194,4 +187,4 @@ tasks.register<Jar>("fatJar") {
     }
 
     manifest {}
-}
+}
\ No newline at end of file
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index 532c2af..64567d9 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -28,10 +28,11 @@ winit.workspace = true
 tokio.workspace = true
 rayon.workspace = true
 jni.workspace = true
-crossbeam-channel = "0.5.15"
-libloading = "0.8.9"
+sha2.workspace = true
+libloading.workspace = true
+crossbeam-channel.workspace = true
 app_dirs2.workspace = true
-sha2 = "0.10"
+num_enum.workspace = true
 
 [features]
 # editor only stuff
diff --git a/eucalyptus-core/src/input.rs b/eucalyptus-core/src/input.rs
index fcda409..e4d7017 100644
--- a/eucalyptus-core/src/input.rs
+++ b/eucalyptus-core/src/input.rs
@@ -3,8 +3,9 @@ use std::{
     time::{Duration, Instant},
 };
 use winit::{event::MouseButton, keyboard::KeyCode};
+use dropbear_engine::gilrs::{Button, GamepadId};
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct InputState {
     #[allow(dead_code)]
     pub last_key_press_times: HashMap<KeyCode, Instant>,
@@ -16,6 +17,13 @@ pub struct InputState {
     pub mouse_delta: Option<(f64, f64)>,
     pub is_cursor_locked: bool,
     pub last_mouse_pos: Option<(f64, f64)>,
+
+    pub connected_gamepads: HashSet<GamepadId>,
+    pub pressed_buttons: HashMap<GamepadId, HashSet<Button>>,
+    pub left_stick_position: HashMap<GamepadId, (f32, f32)>,
+    pub right_stick_position: HashMap<GamepadId, (f32, f32)>,
+    pub left_trigger: HashMap<GamepadId, f32>,
+    pub right_trigger: HashMap<GamepadId, f32>,
 }
 
 impl Default for InputState {
@@ -35,6 +43,12 @@ impl InputState {
             mouse_delta: None,
             is_cursor_locked: false,
             last_mouse_pos: Default::default(),
+            connected_gamepads: Default::default(),
+            pressed_buttons: Default::default(),
+            left_stick_position: Default::default(),
+            right_stick_position: Default::default(),
+            left_trigger: Default::default(),
+            right_trigger: Default::default(),
         }
     }
 
@@ -45,4 +59,29 @@ impl InputState {
     pub fn is_key_pressed(&self, key: KeyCode) -> bool {
         self.pressed_keys.contains(&key)
     }
+
+    #[allow(clippy::unnecessary_map_or)]
+    pub fn is_button_pressed(&self, gamepad_id: GamepadId, button: Button) -> bool {
+        self.pressed_buttons
+            .get(&gamepad_id)
+            .map_or(false, |buttons| buttons.contains(&button))
+    }
+
+    pub fn get_left_stick(&self, gamepad_id: GamepadId) -> (f32, f32) {
+        self.left_stick_position
+            .get(&gamepad_id)
+            .copied()
+            .unwrap_or((0.0, 0.0))
+    }
+
+    pub fn get_right_stick(&self, gamepad_id: GamepadId) -> (f32, f32) {
+        self.right_stick_position
+            .get(&gamepad_id)
+            .copied()
+            .unwrap_or((0.0, 0.0))
+    }
+
+    pub fn is_gamepad_connected(&self, gamepad_id: GamepadId) -> bool {
+        self.connected_gamepads.contains(&gamepad_id)
+    }
 }
diff --git a/eucalyptus-core/src/ptr.rs b/eucalyptus-core/src/ptr.rs
index f0e1593..b06bdcb 100644
--- a/eucalyptus-core/src/ptr.rs
+++ b/eucalyptus-core/src/ptr.rs
@@ -1,3 +1,5 @@
 use hecs::World;
+use crate::input::InputState;
 
 pub type WorldPtr = *mut World;
+pub type InputStatePtr = *mut InputState;
diff --git a/eucalyptus-core/src/scripting.rs b/eucalyptus-core/src/scripting.rs
index 8a3b69f..78d3caf 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -1,5 +1,5 @@
 pub mod jni;
-pub mod kmp;
+pub mod native;
 
 use crate::input::InputState;
 use crate::scripting::jni::JavaContext;
@@ -11,7 +11,7 @@ use anyhow::Context;
 use crossbeam_channel::Sender;
 use tokio::io::{AsyncBufReadExt, BufReader};
 use tokio::process::Command;
-use crate::ptr::WorldPtr;
+use crate::ptr::{InputStatePtr, WorldPtr};
 
 pub const TEMPLATE_SCRIPT: &str = include_str!("../../resources/scripting/kotlin/Template.kt");
 
@@ -109,12 +109,12 @@ impl ScriptManager {
     pub fn load_script(
         &mut self,
         world: WorldPtr,
-        _input_state: &InputState,
+        input_state: InputStatePtr,
     ) -> anyhow::Result<()> {
         match &self.script_target {
             ScriptTarget::JVM { library_path: _ } => {
                 if let Some(jvm) = &mut self.jvm {
-                    jvm.init(world)?;
+                    jvm.init(world, input_state)?;
                     for tag in self.entity_tag_database.keys() {
                         log::trace!("Loading systems for tag: {}", tag);
                         jvm.load_systems_for_tag(tag)?;
diff --git a/eucalyptus-core/src/scripting/jni.rs b/eucalyptus-core/src/scripting/jni.rs
index 86b7194..7e1ac66 100644
--- a/eucalyptus-core/src/scripting/jni.rs
+++ b/eucalyptus-core/src/scripting/jni.rs
@@ -2,19 +2,17 @@
 //! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate
 
 pub mod exception;
+mod exports;
 
 use std::fs;
-use dropbear_engine::entity::{AdoptedEntity, Transform};
-use hecs::World;
-use jni::objects::{GlobalRef, JClass, JObject, JString, JValue};
-use jni::sys::{jclass, jlong};
-use jni::{InitArgsBuilder, JNIEnv, JNIVersion, JavaVM};
+use jni::objects::{GlobalRef, JClass, JValue};
+use jni::sys::{jlong};
+use jni::{InitArgsBuilder, JNIVersion, JavaVM};
 use std::path::{PathBuf};
-use glam::{DQuat, DVec3};
 use crate::{success, APP_INFO};
 use sha2::{Digest, Sha256};
 use crate::logging::{LOG_LEVEL};
-use crate::ptr::WorldPtr;
+use crate::ptr::{InputStatePtr, WorldPtr};
 use crate::scripting::jni::exception::get_exception_info;
 
 const LIBRARY_PATH: &[u8] = include_bytes!("../../../build/libs/dropbear-1.0-SNAPSHOT-all.jar");
@@ -103,7 +101,7 @@ impl JavaContext {
         })
     }
 
-    pub fn init(&mut self, world: WorldPtr) -> anyhow::Result<()> {
+    pub fn init(&mut self, world: WorldPtr, input: InputStatePtr) -> anyhow::Result<()> {
         let mut env = self.jvm.attach_current_thread()?;
 
         if let Some(old_ref) = self.dropbear_engine_class.take() {
@@ -123,12 +121,17 @@ impl JavaContext {
         if result.is_some() { return Err(anyhow::anyhow!("{}", result.unwrap())); }
 
         let world_handle = world as jlong;
-        log::trace!("Calling NativeEngine.init() with arg [{} as JValue::Long]", world_handle);
+        let input_handle = input as jlong;
+        
+        log::trace!("Calling NativeEngine.init() with arg [{} as JValue::Long, {} as JValue::Long]", 
+            world_handle,
+            input_handle
+        );
         env.call_method(
             &native_engine_obj,
             "init",
-            "(J)V",
-            &[JValue::Long(world_handle)],
+            "(JJ)V",
+            &[JValue::Long(world_handle), JValue::Long(input_handle)],
         )?;
 
         let result = get_exception_info(&mut env);
@@ -382,202 +385,4 @@ impl Drop for JavaContext {
             let _ = old_ref;
         }
     }
-}
-
-// JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getEntity
-//   (JNIEnv *, jclass, jlong, jstring);
-#[unsafe(no_mangle)]
-pub fn Java_com_dropbear_ffi_JNINative_getEntity(
-    mut env: JNIEnv,
-    _obj: jclass,
-    world_handle: jlong,
-    label: JString,
-) -> jlong {
-    let label_jni_result = env.get_string(&label);
-    let label_str = match label_jni_result {
-        Ok(java_string) => {
-            match java_string.to_str() {
-                Ok(rust_str) => rust_str.to_string(),
-                Err(e) => {
-                    println!("[Java_com_dropbear_ffi_JNINative_getEntity] [ERROR] Failed to convert Java string to Rust string: {}", e);
-                    return -1;
-                }
-            }
-        },
-        Err(e) => {
-            println!("[Java_com_dropbear_ffi_JNINative_getEntity] [ERROR] Failed to get string from JNI: {}", e);
-            return -1;
-        }
-    };
-
-    let world = world_handle as *mut World;
-
-    if world.is_null() {
-        println!("[Java_com_dropbear_ffi_JNINative_getEntity] [ERROR] World pointer is null");
-        return -1;
-    }
-
-    let world = unsafe { &mut *world };
-
-    for (id, entity) in world.query::<&AdoptedEntity>().iter() {
-        if entity.model.label == label_str {
-            return id.id() as jlong;
-        }
-    }
-    -1
-}
-
-// JNIEXPORT jobject JNICALL Java_com_dropbear_ffi_JNINative_getTransform
-//   (JNIEnv *, jclass, jlong, jlong);
-#[unsafe(no_mangle)]
-pub fn Java_com_dropbear_ffi_JNINative_getTransform(
-    mut env: JNIEnv,
-    _class: jclass,
-    world_handle: jlong,
-    entity_id: jlong,
-) -> JObject {
-    let world = world_handle as *mut World;
-
-    if world.is_null() {
-        println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] World pointer is null");
-        return JObject::null();
-    }
-
-    let world = unsafe { &mut *world };
-
-    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
-
-    if let Ok(mut q) = world.query_one::<(&AdoptedEntity, &Transform)>(entity)
-        && let Some((_, transform)) = q.get()
-    {
-        let new_transform = *transform;
-
-        let transform_class = match env.find_class("com/dropbear/math/Transform") {
-            Ok(c) => c,
-            Err(_) => return JObject::null(),
-        };
-
-        return match env.new_object(
-            &transform_class,
-            "(DDDDDDDDDD)V",
-            &[
-                new_transform.position.x.into(),
-                new_transform.position.y.into(),
-                new_transform.position.z.into(),
-                new_transform.rotation.x.into(),
-                new_transform.rotation.y.into(),
-                new_transform.rotation.z.into(),
-                new_transform.rotation.w.into(),
-                new_transform.scale.x.into(),
-                new_transform.scale.y.into(),
-                new_transform.scale.z.into(),
-            ],
-        ) {
-            Ok(java_transform) => java_transform,
-            Err(_) => {
-                println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create Transform object");
-                JObject::null()
-            }
-        };
-    }
-
-    println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to query for transform value for entity: {}", entity_id);
-    JObject::null()
-}
-
-// JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_setTransform
-//   (JNIEnv *, jclass, jlong, jlong, jobject);
-#[unsafe(no_mangle)]
-pub fn Java_com_dropbear_ffi_JNINative_setTransform(
-    mut env: JNIEnv,
-    _class: jclass,
-    world_handle: jlong,
-    entity_id: jlong,
-    transform_obj: JObject,
-) {
-    let world = world_handle as *mut World;
-
-    if world.is_null() {
-        println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] World pointer is null");
-        return;
-    }
-
-    let world = unsafe { &mut *world };
-    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
-
-    let get_number_field = |env: &mut JNIEnv, obj: &JObject, field_name: &str| -> f64 {
-        match env.get_field(obj, field_name, "Ljava/lang/Number;") {
-            Ok(v) => match v.l() {
-                Ok(num_obj) => {
-                    match env.call_method(&num_obj, "doubleValue", "()D", &[]) {
-                        Ok(result) => result.d().unwrap_or_else(|_| {
-                            println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract double from {}", field_name);
-                            0.0
-                        }),
-                        Err(_) => {
-                            println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to call doubleValue on {}", field_name);
-                            0.0
-                        }
-                    }
-                }
-                Err(_) => {
-                    println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract Number object for {}", field_name);
-                    0.0
-                }
-            },
-            Err(_) => {
-                println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to get field {}", field_name);
-                0.0
-            }
-        }
-    };
-
-    let position_obj: JObject = match env.get_field(&transform_obj, "position", "Lcom/dropbear/math/Vector3;") {
-        Ok(v) => v.l().unwrap_or_else(|_| JObject::null()),
-        Err(_) => JObject::null(),
-    };
-
-    let rotation_obj: JObject = match env.get_field(&transform_obj, "rotation", "Lcom/dropbear/math/Quaternion;") {
-        Ok(v) => v.l().unwrap_or_else(|_| JObject::null()),
-        Err(_) => JObject::null(),
-    };
-
-    let scale_obj: JObject = match env.get_field(&transform_obj, "scale", "Lcom/dropbear/math/Vector3;") {
-        Ok(v) => v.l().unwrap_or_else(|_| JObject::null()),
-        Err(_) => JObject::null(),
-    };
-
-    if position_obj.is_null() || rotation_obj.is_null() || scale_obj.is_null() {
-        println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract position/rotation/scale objects");
-        return;
-    }
-
-    let px = get_number_field(&mut env, &position_obj, "x");
-    let py = get_number_field(&mut env, &position_obj, "y");
-    let pz = get_number_field(&mut env, &position_obj, "z");
-
-    let rx = get_number_field(&mut env, &rotation_obj, "x");
-    let ry = get_number_field(&mut env, &rotation_obj, "y");
-    let rz = get_number_field(&mut env, &rotation_obj, "z");
-    let rw = get_number_field(&mut env, &rotation_obj, "w");
-
-    let sx = get_number_field(&mut env, &scale_obj, "x");
-    let sy = get_number_field(&mut env, &scale_obj, "y");
-    let sz = get_number_field(&mut env, &scale_obj, "z");
-
-    let new_transform = Transform {
-        position: DVec3::new(px, py, pz),
-        rotation: DQuat::from_axis_angle(DVec3::new(rx, ry, rz), rw),
-        scale: DVec3::new(sx, sy, sz),
-    };
-
-    if let Ok(mut q) = world.query_one::<&mut Transform>(entity) {
-        if let Some(transform) = q.get() {
-            *transform = new_transform;
-        } else {
-            println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to query for transform component");
-        }
-    } else {
-        println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to query entity for transform component");
-    }
 }
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/jni/exports.rs b/eucalyptus-core/src/scripting/jni/exports.rs
new file mode 100644
index 0000000..97f3a65
--- /dev/null
+++ b/eucalyptus-core/src/scripting/jni/exports.rs
@@ -0,0 +1,262 @@
+use glam::{DQuat, DVec3};
+use hecs::World;
+use jni::JNIEnv;
+use jni::objects::{JObject, JString};
+use jni::sys::{jboolean, jclass, jint, jlong};
+use winit::keyboard::{KeyCode, PhysicalKey};
+use winit::platform::scancode::PhysicalKeyExtScancode;
+use dropbear_engine::entity::{AdoptedEntity, Transform};
+use crate::ptr::InputStatePtr;
+use num_enum::FromPrimitive;
+use crate::utils::keycode_from_ordinal;
+
+// JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getEntity
+//   (JNIEnv *, jclass, jlong, jstring);
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_getEntity(
+    mut env: JNIEnv,
+    _obj: jclass,
+    world_handle: jlong,
+    label: JString,
+) -> jlong {
+    let label_jni_result = env.get_string(&label);
+    let label_str = match label_jni_result {
+        Ok(java_string) => {
+            match java_string.to_str() {
+                Ok(rust_str) => rust_str.to_string(),
+                Err(e) => {
+                    println!("[Java_com_dropbear_ffi_JNINative_getEntity] [ERROR] Failed to convert Java string to Rust string: {}", e);
+                    return -1;
+                }
+            }
+        },
+        Err(e) => {
+            println!("[Java_com_dropbear_ffi_JNINative_getEntity] [ERROR] Failed to get string from JNI: {}", e);
+            return -1;
+        }
+    };
+
+    let world = world_handle as *mut World;
+
+    if world.is_null() {
+        println!("[Java_com_dropbear_ffi_JNINative_getEntity] [ERROR] World pointer is null");
+        return -1;
+    }
+
+    let world = unsafe { &mut *world };
+
+    for (id, entity) in world.query::<&AdoptedEntity>().iter() {
+        if entity.model.label == label_str {
+            return id.id() as jlong;
+        }
+    }
+    -1
+}
+
+// JNIEXPORT jobject JNICALL Java_com_dropbear_ffi_JNINative_getTransform
+//   (JNIEnv *, jclass, jlong, jlong);
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_getTransform(
+    mut env: JNIEnv,
+    _class: jclass,
+    world_handle: jlong,
+    entity_id: jlong,
+) -> JObject {
+    let world = world_handle as *mut World;
+
+    if world.is_null() {
+        println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] World pointer is null");
+        return JObject::null();
+    }
+
+    let world = unsafe { &mut *world };
+
+    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
+
+    if let Ok(mut q) = world.query_one::<(&AdoptedEntity, &Transform)>(entity)
+        && let Some((_, transform)) = q.get()
+    {
+        let new_transform = *transform;
+
+        let transform_class = match env.find_class("com/dropbear/math/Transform") {
+            Ok(c) => c,
+            Err(_) => return JObject::null(),
+        };
+
+        return match env.new_object(
+            &transform_class,
+            "(DDDDDDDDDD)V",
+            &[
+                new_transform.position.x.into(),
+                new_transform.position.y.into(),
+                new_transform.position.z.into(),
+                new_transform.rotation.x.into(),
+                new_transform.rotation.y.into(),
+                new_transform.rotation.z.into(),
+                new_transform.rotation.w.into(),
+                new_transform.scale.x.into(),
+                new_transform.scale.y.into(),
+                new_transform.scale.z.into(),
+            ],
+        ) {
+            Ok(java_transform) => java_transform,
+            Err(_) => {
+                println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create Transform object");
+                JObject::null()
+            }
+        };
+    }
+
+    println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to query for transform value for entity: {}", entity_id);
+    JObject::null()
+}
+
+// JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_setTransform
+//   (JNIEnv *, jclass, jlong, jlong, jobject);
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_setTransform(
+    mut env: JNIEnv,
+    _class: jclass,
+    world_handle: jlong,
+    entity_id: jlong,
+    transform_obj: JObject,
+) {
+    let world = world_handle as *mut World;
+
+    if world.is_null() {
+        println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] World pointer is null");
+        return;
+    }
+
+    let world = unsafe { &mut *world };
+    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
+
+    let get_number_field = |env: &mut JNIEnv, obj: &JObject, field_name: &str| -> f64 {
+        match env.get_field(obj, field_name, "Ljava/lang/Number;") {
+            Ok(v) => match v.l() {
+                Ok(num_obj) => {
+                    match env.call_method(&num_obj, "doubleValue", "()D", &[]) {
+                        Ok(result) => result.d().unwrap_or_else(|_| {
+                            println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract double from {}", field_name);
+                            0.0
+                        }),
+                        Err(_) => {
+                            println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to call doubleValue on {}", field_name);
+                            0.0
+                        }
+                    }
+                }
+                Err(_) => {
+                    println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract Number object for {}", field_name);
+                    0.0
+                }
+            },
+            Err(_) => {
+                println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to get field {}", field_name);
+                0.0
+            }
+        }
+    };
+
+    let position_obj: JObject = match env.get_field(&transform_obj, "position", "Lcom/dropbear/math/Vector3;") {
+        Ok(v) => v.l().unwrap_or_else(|_| JObject::null()),
+        Err(_) => JObject::null(),
+    };
+
+    let rotation_obj: JObject = match env.get_field(&transform_obj, "rotation", "Lcom/dropbear/math/Quaternion;") {
+        Ok(v) => v.l().unwrap_or_else(|_| JObject::null()),
+        Err(_) => JObject::null(),
+    };
+
+    let scale_obj: JObject = match env.get_field(&transform_obj, "scale", "Lcom/dropbear/math/Vector3;") {
+        Ok(v) => v.l().unwrap_or_else(|_| JObject::null()),
+        Err(_) => JObject::null(),
+    };
+
+    if position_obj.is_null() || rotation_obj.is_null() || scale_obj.is_null() {
+        println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract position/rotation/scale objects");
+        return;
+    }
+
+    let px = get_number_field(&mut env, &position_obj, "x");
+    let py = get_number_field(&mut env, &position_obj, "y");
+    let pz = get_number_field(&mut env, &position_obj, "z");
+
+    let rx = get_number_field(&mut env, &rotation_obj, "x");
+    let ry = get_number_field(&mut env, &rotation_obj, "y");
+    let rz = get_number_field(&mut env, &rotation_obj, "z");
+    let rw = get_number_field(&mut env, &rotation_obj, "w");
+
+    let sx = get_number_field(&mut env, &scale_obj, "x");
+    let sy = get_number_field(&mut env, &scale_obj, "y");
+    let sz = get_number_field(&mut env, &scale_obj, "z");
+
+    let new_transform = Transform {
+        position: DVec3::new(px, py, pz),
+        rotation: DQuat::from_axis_angle(DVec3::new(rx, ry, rz), rw),
+        scale: DVec3::new(sx, sy, sz),
+    };
+
+    if let Ok(mut q) = world.query_one::<&mut Transform>(entity) {
+        if let Some(transform) = q.get() {
+            *transform = new_transform;
+        } else {
+            println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to query for transform component");
+        }
+    } else {
+        println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to query entity for transform component");
+    }
+}
+
+// JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_printInputState
+//   (JNIEnv *, jclass, jlong);
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_printInputState(
+    _env: JNIEnv,
+    _class: jclass,
+    input_handle: jlong,
+) {
+    let input = input_handle as InputStatePtr;
+
+    if input.is_null() {
+        println!("[Java_com_dropbear_ffi_JNINative_printInputState] [ERROR] Input state pointer is null");
+        return;
+    }
+
+    let input = unsafe { &*input };
+    println!("{:#?}", input);
+}
+
+// JNIEXPORT jboolean JNICALL Java_com_dropbear_ffi_JNINative_isKeyPressed
+//   (JNIEnv *, jclass, jlong, jint);
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_isKeyPressed(
+    _env: JNIEnv,
+    _class: jclass,
+    input_handle: jlong,
+    key: jint,
+) -> jboolean {
+    let input = input_handle as InputStatePtr;
+    if input.is_null() {
+        println!("[Java_com_dropbear_ffi_JNINative_isKeyPressed] [ERROR] Input state pointer is null");
+        return false.into();
+    }
+    let input = unsafe { &*input };
+
+    println!("[Java_com_dropbear_ffi_JNINative_isKeyPressed] [DEBUG] Original code: {:?}", key);
+
+    match keycode_from_ordinal(key) {
+        Some(k) => {
+            println!("[Java_com_dropbear_ffi_JNINative_isKeyPressed] [DEBUG] Keycode: {:?}", k);
+            if input.pressed_keys.contains(&k) {
+                true.into()
+            } else {
+                false.into()
+            }
+        }
+        None => {
+            println!("[Java_com_dropbear_ffi_JNINative_isKeyPressed] [WARN] Ordinal keycode is invalid");
+            false.into()
+        }
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/kmp.rs b/eucalyptus-core/src/scripting/kmp.rs
deleted file mode 100644
index 40ee728..0000000
--- a/eucalyptus-core/src/scripting/kmp.rs
+++ /dev/null
@@ -1,129 +0,0 @@
-//! Deals with Kotlin/Native library loading for different platforms.
-#![allow(clippy::missing_safety_doc)]
-
-use dropbear_engine::entity::{AdoptedEntity, Transform};
-use std::ffi::{CStr, c_char};
-
-/// Displays the types of errors that can be returned by the native library.
-pub enum DropbearNativeError {
-    Success = 0,
-    NullPointer = -1,
-    QueryFailed = -2,
-    EntityNotFound = -3,
-    NoSuchComponent = -4,
-    NoSuchEntity = -5,
-    WorldInsertError = -6,
-
-    InvalidUTF8 = -108,
-    /// A generic error when the library doesn't know what happened or cannot find a
-    /// suitable error code.
-    ///
-    /// The number `1274` comes from the total sum of the word "UnknownError" into decimal
-    UnknownError = -1274,
-}
-
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn dropbear_get_entity(
-    label: *const c_char,
-    world_ptr: *const hecs::World,
-    out_entity: *mut i64,
-) -> i32 {
-    unsafe {
-        if label.is_null() || world_ptr.is_null() || out_entity.is_null() {
-            println!("[dropbear_get_entity] [ERROR] received null pointer");
-            return -1;
-        }
-
-        let world =  &*world_ptr;
-
-        let label_str = match CStr::from_ptr(label).to_str() {
-            Ok(s) => s,
-            Err(_) => {
-                println!("[dropbear_get_entity] [ERROR] invalid UTF-8 in label");
-                return -108;
-            }
-        };
-
-        let mut hit: bool = false;
-
-        for (id, entity) in world.query::<&AdoptedEntity>().iter() {
-            if entity.model.label == label_str {
-                #[allow(unused_assignments)]
-                { hit = true; }
-                *out_entity = id.id() as i64;
-                log::debug!("Found entity with label: {:?}", label_str);
-                return 0;
-            }
-        }
-
-        if !hit {
-            println!("[dropbear_get_entity] [ERROR] Entity with label '{:?}' not found", label_str);
-            -3
-        } else {
-            DropbearNativeError::UnknownError as i32
-        }
-    }
-}
-
-#[unsafe(no_mangle)]
-pub unsafe extern "C" fn dropbear_get_transform(
-    world_ptr: *const hecs::World,
-    entity_id: i64,
-    out_transform: *mut Transform,
-) -> i32 {
-    if world_ptr.is_null() {
-        eprintln!("[dropbear_get_transform] [ERROR] World pointer is null");
-        return -1;
-    }
-
-    if out_transform.is_null() {
-        eprintln!("[dropbear_get_transform] [ERROR] Output transform pointer is null");
-        return -1;
-    }
-
-    let world = unsafe { &*world_ptr };
-
-    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
-
-    match world.query_one::<&Transform>(entity) {
-        Ok(mut q) => {
-            if let Some(transform) = q.get() {
-                unsafe { *out_transform = *transform };
-                0
-            } else {
-                eprintln!("[dropbear_get_transform] [ERROR] Entity has no Transform component");
-                -4
-            }
-        }
-        Err(_) => {
-            eprintln!("[dropbear_get_transform] [ERROR] Failed to query entity for Transform component");
-            -2
-        }
-    }
-}
-
-pub unsafe extern "C" fn dropbear_set_transform(
-    world_ptr: *mut hecs::World,
-    entity_id: i64,
-    transform: Transform,
-) -> i32 {
-    if world_ptr.is_null() {
-        eprintln!("[dropbear_get_transform] [ERROR] World pointer is null");
-        return -1;
-    }
-
-    let world = unsafe { &mut *world_ptr };
-
-    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
-
-    let result = world.insert_one(entity, transform);
-
-    match result {
-        Ok(_) => 0,
-        Err(_) => {
-            eprintln!("[dropbear_set_transform] [ERROR] Failed to insert transform component");
-            -6
-        },
-    }
-}
-
diff --git a/eucalyptus-core/src/scripting/native.rs b/eucalyptus-core/src/scripting/native.rs
new file mode 100644
index 0000000..4b2d357
--- /dev/null
+++ b/eucalyptus-core/src/scripting/native.rs
@@ -0,0 +1,129 @@
+//! Deals with Kotlin/Native library loading for different platforms.
+#![allow(clippy::missing_safety_doc)]
+
+pub mod sig;
+mod exports;
+
+use crate::ptr::{InputStatePtr, WorldPtr};
+use crate::scripting::native::sig::{DestroyAll, DestroyTagged, Init, LoadTagged, UpdateAll, UpdateTagged};
+use libloading::{Library, Symbol};
+use std::ffi::CString;
+use std::path::Path;
+
+pub struct NativeLibrary {
+    #[allow(dead_code)]
+    library: Library,
+    init_fn: Symbol<'static, Init>,
+    load_systems_fn: Symbol<'static, LoadTagged>,
+    update_all_fn: Symbol<'static, UpdateAll>,
+    update_tag_fn: Symbol<'static, UpdateTagged>,
+    destroy_all_fn: Symbol<'static, DestroyAll>,
+    destroy_tagged_fn: Symbol<'static, DestroyTagged>,
+}
+
+impl NativeLibrary {
+    pub fn new(lib_path: impl AsRef<Path>) -> anyhow::Result<Self> {
+        let lib_path = lib_path.as_ref();
+        unsafe {
+            let library: Library = Library::new(lib_path)?;
+
+            let init_fn: Symbol<'static, Init> = std::mem::transmute(
+                library.get::<Init>(b"dropbear_init\0")?
+            );
+            let load_systems_fn: Symbol<'static, LoadTagged> = std::mem::transmute(
+                library.get::<LoadTagged>(b"dropbear_load_systems\0")?
+            );
+            let update_all_fn: Symbol<'static, UpdateAll> = std::mem::transmute(
+                library.get::<UpdateAll>(b"dropbear_update_all\0")?
+            );
+            let update_tag_fn: Symbol<'static, UpdateTagged> = std::mem::transmute(
+                library.get::<UpdateTagged>(b"dropbear_update_tagged\0")?
+            );
+            let destroy_all_fn: Symbol<'static, DestroyAll> = std::mem::transmute(
+                library.get::<DestroyAll>(b"dropbear_destroy_all\0")?
+            );
+            let destroy_tagged_fn: Symbol<'static, DestroyTagged> = std::mem::transmute(
+                library.get::<DestroyTagged>(b"dropbear_destroy_tagged\0")?
+            );
+
+            Ok(Self {
+                library,
+                init_fn,
+                load_systems_fn,
+                update_all_fn,
+                update_tag_fn,
+                destroy_all_fn,
+                destroy_tagged_fn,
+            })
+        }
+    }
+
+    pub fn init(&mut self, world_ptr: WorldPtr, input_state_ptr: InputStatePtr) -> anyhow::Result<()> {
+        unsafe {
+            let result = (self.init_fn)(world_ptr, input_state_ptr);
+            if result != 0 {
+                anyhow::bail!("Init function failed with code: {}", result);
+            }
+            Ok(())
+        }
+    }
+
+    pub fn load_systems(&mut self, tag: String) -> anyhow::Result<()> {
+        unsafe {
+            let c_string: CString = CString::new(tag)?;
+            let result = (self.load_systems_fn)(c_string.as_ptr());
+            if result != 0 {
+                anyhow::bail!("Load systems failed with code: {}", result);
+            }
+            Ok(())
+        }
+    }
+
+    pub fn update_all(&mut self, dt: f32) -> anyhow::Result<()> {
+        unsafe {
+            (self.update_all_fn)(dt);
+            Ok(())
+        }
+    }
+
+    pub fn update_tagged(&mut self, tag: String, dt: f32) -> anyhow::Result<()> {
+        unsafe {
+            let c_string: CString = CString::new(tag)?;
+            (self.update_tag_fn)(c_string.as_ptr(), dt);
+            Ok(())
+        }
+    }
+
+    pub fn destroy_all(&mut self) -> anyhow::Result<()> {
+        unsafe {
+            (self.destroy_all_fn)();
+            Ok(())
+        }
+    }
+
+    pub fn destroy_tagged(&mut self, tag: String) -> anyhow::Result<()> {
+        unsafe {
+            let c_string: CString = CString::new(tag)?;
+            (self.destroy_tagged_fn)(c_string.as_ptr());
+            Ok(())
+        }
+    }
+}
+
+/// Displays the types of errors that can be returned by the native library.
+pub enum DropbearNativeError {
+    Success = 0,
+    NullPointer = -1,
+    QueryFailed = -2,
+    EntityNotFound = -3,
+    NoSuchComponent = -4,
+    NoSuchEntity = -5,
+    WorldInsertError = -6,
+
+    InvalidUTF8 = -108,
+    /// A generic error when the library doesn't know what happened or cannot find a
+    /// suitable error code.
+    ///
+    /// The number `1274` comes from the total sum of the word "UnknownError" in decimal
+    UnknownError = -1274,
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/native/exports.rs b/eucalyptus-core/src/scripting/native/exports.rs
new file mode 100644
index 0000000..45c5d8a
--- /dev/null
+++ b/eucalyptus-core/src/scripting/native/exports.rs
@@ -0,0 +1,157 @@
+use std::ffi::{c_char, CStr};
+use winit::keyboard::{KeyCode, PhysicalKey};
+use winit::platform::scancode::PhysicalKeyExtScancode;
+use dropbear_engine::entity::{AdoptedEntity, Transform};
+use crate::ptr::InputStatePtr;
+use crate::scripting::native::DropbearNativeError;
+use crate::utils::keycode_from_ordinal;
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn dropbear_get_entity(
+    label: *const c_char,
+    world_ptr: *const hecs::World,
+    out_entity: *mut i64,
+) -> i32 {
+    unsafe {
+        if label.is_null() || world_ptr.is_null() || out_entity.is_null() {
+            println!("[dropbear_get_entity] [ERROR] received null pointer");
+            return -1;
+        }
+
+        let world =  &*world_ptr;
+
+        let label_str = match CStr::from_ptr(label).to_str() {
+            Ok(s) => s,
+            Err(_) => {
+                println!("[dropbear_get_entity] [ERROR] invalid UTF-8 in label");
+                return -108;
+            }
+        };
+
+        let mut hit: bool = false;
+
+        for (id, entity) in world.query::<&AdoptedEntity>().iter() {
+            if entity.model.label == label_str {
+                #[allow(unused_assignments)]
+                { hit = true; }
+                *out_entity = id.id() as i64;
+                log::debug!("Found entity with label: {:?}", label_str);
+                return 0;
+            }
+        }
+
+        if !hit {
+            println!("[dropbear_get_entity] [ERROR] Entity with label '{:?}' not found", label_str);
+            -3
+        } else {
+            DropbearNativeError::UnknownError as i32
+        }
+    }
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn dropbear_get_transform(
+    world_ptr: *const hecs::World,
+    entity_id: i64,
+    out_transform: *mut Transform,
+) -> i32 {
+    if world_ptr.is_null() {
+        eprintln!("[dropbear_get_transform] [ERROR] World pointer is null");
+        return DropbearNativeError::NullPointer as i32;
+    }
+
+    if out_transform.is_null() {
+        eprintln!("[dropbear_get_transform] [ERROR] Output transform pointer is null");
+        return DropbearNativeError::NullPointer as i32;
+    }
+
+    let world = unsafe { &*world_ptr };
+
+    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
+
+    match world.query_one::<&Transform>(entity) {
+        Ok(mut q) => {
+            if let Some(transform) = q.get() {
+                unsafe { *out_transform = *transform };
+                DropbearNativeError::Success as i32
+            } else {
+                eprintln!("[dropbear_get_transform] [ERROR] Entity has no Transform component");
+                -4
+            }
+        }
+        Err(_) => {
+            eprintln!("[dropbear_get_transform] [ERROR] Failed to query entity for Transform component");
+            -2
+        }
+    }
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn dropbear_set_transform(
+    world_ptr: *mut hecs::World,
+    entity_id: i64,
+    transform: Transform,
+) -> i32 {
+    if world_ptr.is_null() {
+        eprintln!("[dropbear_get_transform] [ERROR] World pointer is null");
+        return DropbearNativeError::NullPointer as i32;
+    }
+
+    let world = unsafe { &mut *world_ptr };
+
+    let entity = unsafe { world.find_entity_from_id(entity_id as u32) };
+
+    let result = world.insert_one(entity, transform);
+
+    match result {
+        Ok(_) => 0,
+        Err(_) => {
+            eprintln!("[dropbear_set_transform] [ERROR] Failed to insert transform component");
+            -6
+        },
+    }
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn dropbear_print_input_state(
+    input_state_ptr: InputStatePtr,
+) {
+    if input_state_ptr.is_null() {
+        eprintln!("[dropbear_print_inputstate] [ERROR] Input state pointer is null");
+        return;
+    }
+
+    let input_state = unsafe { &*input_state_ptr };
+    println!("{:#?}", input_state);
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn dropbear_is_key_pressed(
+    input_state_ptr: InputStatePtr,
+    key: i32,
+    out_is_pressed: *mut bool,
+) -> i32 {
+    if input_state_ptr.is_null() {
+        eprintln!("[dropbear_is_key_pressed] [ERROR] Input state pointer is null");
+        unsafe { *out_is_pressed = false };
+        return DropbearNativeError::NullPointer as i32;
+    }
+
+    let input = unsafe { &*input_state_ptr };
+
+    match keycode_from_ordinal(key) {
+        Some(k) => {
+            println!("[dropbear_is_key_pressed] [DEBUG] Keycode: {:?}", k);
+            if input.pressed_keys.contains(&k) {
+                true.into()
+            } else {
+                false.into()
+            }
+        }
+        None => {
+            println!("[dropbear_is_key_pressed] [WARN] Ordinal keycode is invalid");
+            false.into()
+        }
+    }
+}
+
diff --git a/eucalyptus-core/src/scripting/native/sig.rs b/eucalyptus-core/src/scripting/native/sig.rs
new file mode 100644
index 0000000..dcb5965
--- /dev/null
+++ b/eucalyptus-core/src/scripting/native/sig.rs
@@ -0,0 +1,16 @@
+/// Different signatures for Native implementations
+use std::ffi::c_char;
+use crate::ptr::{InputStatePtr, WorldPtr};
+
+/// CName: `dropbear_init`
+pub type Init = unsafe extern "C" fn(world: WorldPtr, input: InputStatePtr) -> i32;
+/// CName: `dropbear_load_tagged`
+pub type LoadTagged = unsafe extern "C" fn(tag: *const c_char) -> i32;
+/// CName: `dropbear_update_all`
+pub type UpdateAll = unsafe extern "C" fn(dt: f32) -> i32;
+/// CName: `dropbear_update_tagged`
+pub type UpdateTagged = unsafe extern "C" fn(tag: *const c_char, dt: f32) -> i32;
+/// CName: `dropbear_destroy_tagged`
+pub type DestroyTagged = unsafe extern "C" fn(tag: *const c_char) -> i32;
+/// CName: `dropbear_destroy_all`
+pub type DestroyAll = unsafe extern "C" fn() -> i32;
\ No newline at end of file
diff --git a/eucalyptus-core/src/utils.rs b/eucalyptus-core/src/utils.rs
index 91eb54a..a62f4a6 100644
--- a/eucalyptus-core/src/utils.rs
+++ b/eucalyptus-core/src/utils.rs
@@ -1,3 +1,4 @@
+use winit::keyboard::KeyCode;
 use crate::states::Node;
 
 pub const PROTO_TEXTURE: &[u8] = include_bytes!("../../resources/textures/proto.png");
@@ -36,3 +37,203 @@ pub enum ViewportMode {
     CameraMove,
     Gizmo,
 }
+
+pub fn keycode_from_ordinal(ordinal: i32) -> Option<KeyCode> {
+    match ordinal {
+        0 => Some(KeyCode::Backquote),
+        1 => Some(KeyCode::Backslash),
+        2 => Some(KeyCode::BracketLeft),
+        3 => Some(KeyCode::BracketRight),
+        4 => Some(KeyCode::Comma),
+        5 => Some(KeyCode::Digit0),
+        6 => Some(KeyCode::Digit1),
+        7 => Some(KeyCode::Digit2),
+        8 => Some(KeyCode::Digit3),
+        9 => Some(KeyCode::Digit4),
+        10 => Some(KeyCode::Digit5),
+        11 => Some(KeyCode::Digit6),
+        12 => Some(KeyCode::Digit7),
+        13 => Some(KeyCode::Digit8),
+        14 => Some(KeyCode::Digit9),
+        15 => Some(KeyCode::Equal),
+        16 => Some(KeyCode::IntlBackslash),
+        17 => Some(KeyCode::IntlRo),
+        18 => Some(KeyCode::IntlYen),
+        19 => Some(KeyCode::KeyA),
+        20 => Some(KeyCode::KeyB),
+        21 => Some(KeyCode::KeyC),
+        22 => Some(KeyCode::KeyD),
+        23 => Some(KeyCode::KeyE),
+        24 => Some(KeyCode::KeyF),
+        25 => Some(KeyCode::KeyG),
+        26 => Some(KeyCode::KeyH),
+        27 => Some(KeyCode::KeyI),
+        28 => Some(KeyCode::KeyJ),
+        29 => Some(KeyCode::KeyK),
+        30 => Some(KeyCode::KeyL),
+        31 => Some(KeyCode::KeyM),
+        32 => Some(KeyCode::KeyN),
+        33 => Some(KeyCode::KeyO),
+        34 => Some(KeyCode::KeyP),
+        35 => Some(KeyCode::KeyQ),
+        36 => Some(KeyCode::KeyR),
+        37 => Some(KeyCode::KeyS),
+        38 => Some(KeyCode::KeyT),
+        39 => Some(KeyCode::KeyU),
+        40 => Some(KeyCode::KeyV),
+        41 => Some(KeyCode::KeyW),
+        42 => Some(KeyCode::KeyX),
+        43 => Some(KeyCode::KeyY),
+        44 => Some(KeyCode::KeyZ),
+        45 => Some(KeyCode::Minus),
+        46 => Some(KeyCode::Period),
+        47 => Some(KeyCode::Quote),
+        48 => Some(KeyCode::Semicolon),
+        49 => Some(KeyCode::Slash),
+        50 => Some(KeyCode::AltLeft),
+        51 => Some(KeyCode::AltRight),
+        52 => Some(KeyCode::Backspace),
+        53 => Some(KeyCode::CapsLock),
+        54 => Some(KeyCode::ContextMenu),
+        55 => Some(KeyCode::ControlLeft),
+        56 => Some(KeyCode::ControlRight),
+        57 => Some(KeyCode::Enter),
+        58 => Some(KeyCode::SuperLeft),
+        59 => Some(KeyCode::SuperRight),
+        60 => Some(KeyCode::ShiftLeft),
+        61 => Some(KeyCode::ShiftRight),
+        62 => Some(KeyCode::Space),
+        63 => Some(KeyCode::Tab),
+        64 => Some(KeyCode::Convert),
+        65 => Some(KeyCode::KanaMode),
+        66 => Some(KeyCode::Lang1),
+        67 => Some(KeyCode::Lang2),
+        68 => Some(KeyCode::Lang3),
+        69 => Some(KeyCode::Lang4),
+        70 => Some(KeyCode::Lang5),
+        71 => Some(KeyCode::NonConvert),
+        72 => Some(KeyCode::Delete),
+        73 => Some(KeyCode::End),
+        74 => Some(KeyCode::Help),
+        75 => Some(KeyCode::Home),
+        76 => Some(KeyCode::Insert),
+        77 => Some(KeyCode::PageDown),
+        78 => Some(KeyCode::PageUp),
+        79 => Some(KeyCode::ArrowDown),
+        80 => Some(KeyCode::ArrowLeft),
+        81 => Some(KeyCode::ArrowRight),
+        82 => Some(KeyCode::ArrowUp),
+        83 => Some(KeyCode::NumLock),
+        84 => Some(KeyCode::Numpad0),
+        85 => Some(KeyCode::Numpad1),
+        86 => Some(KeyCode::Numpad2),
+        87 => Some(KeyCode::Numpad3),
+        88 => Some(KeyCode::Numpad4),
+        89 => Some(KeyCode::Numpad5),
+        90 => Some(KeyCode::Numpad6),
+        91 => Some(KeyCode::Numpad7),
+        92 => Some(KeyCode::Numpad8),
+        93 => Some(KeyCode::Numpad9),
+        94 => Some(KeyCode::NumpadAdd),
+        95 => Some(KeyCode::NumpadBackspace),
+        96 => Some(KeyCode::NumpadClear),
+        97 => Some(KeyCode::NumpadClearEntry),
+        98 => Some(KeyCode::NumpadComma),
+        99 => Some(KeyCode::NumpadDecimal),
+        100 => Some(KeyCode::NumpadDivide),
+        101 => Some(KeyCode::NumpadEnter),
+        102 => Some(KeyCode::NumpadEqual),
+        103 => Some(KeyCode::NumpadHash),
+        104 => Some(KeyCode::NumpadMemoryAdd),
+        105 => Some(KeyCode::NumpadMemoryClear),
+        106 => Some(KeyCode::NumpadMemoryRecall),
+        107 => Some(KeyCode::NumpadMemoryStore),
+        108 => Some(KeyCode::NumpadMemorySubtract),
+        109 => Some(KeyCode::NumpadMultiply),
+        110 => Some(KeyCode::NumpadParenLeft),
+        111 => Some(KeyCode::NumpadParenRight),
+        112 => Some(KeyCode::NumpadStar),
+        113 => Some(KeyCode::NumpadSubtract),
+        114 => Some(KeyCode::Escape),
+        115 => Some(KeyCode::Fn),
+        116 => Some(KeyCode::FnLock),
+        117 => Some(KeyCode::PrintScreen),
+        118 => Some(KeyCode::ScrollLock),
+        119 => Some(KeyCode::Pause),
+        120 => Some(KeyCode::BrowserBack),
+        121 => Some(KeyCode::BrowserFavorites),
+        122 => Some(KeyCode::BrowserForward),
+        123 => Some(KeyCode::BrowserHome),
+        124 => Some(KeyCode::BrowserRefresh),
+        125 => Some(KeyCode::BrowserSearch),
+        126 => Some(KeyCode::BrowserStop),
+        127 => Some(KeyCode::Eject),
+        128 => Some(KeyCode::LaunchApp1),
+        129 => Some(KeyCode::LaunchApp2),
+        130 => Some(KeyCode::LaunchMail),
+        131 => Some(KeyCode::MediaPlayPause),
+        132 => Some(KeyCode::MediaSelect),
+        133 => Some(KeyCode::MediaStop),
+        134 => Some(KeyCode::MediaTrackNext),
+        135 => Some(KeyCode::MediaTrackPrevious),
+        136 => Some(KeyCode::Power),
+        137 => Some(KeyCode::Sleep),
+        138 => Some(KeyCode::AudioVolumeDown),
+        139 => Some(KeyCode::AudioVolumeMute),
+        140 => Some(KeyCode::AudioVolumeUp),
+        141 => Some(KeyCode::WakeUp),
+        142 => Some(KeyCode::Meta),
+        143 => Some(KeyCode::Hyper),
+        144 => Some(KeyCode::Turbo),
+        145 => Some(KeyCode::Abort),
+        146 => Some(KeyCode::Resume),
+        147 => Some(KeyCode::Suspend),
+        148 => Some(KeyCode::Again),
+        149 => Some(KeyCode::Copy),
+        150 => Some(KeyCode::Cut),
+        151 => Some(KeyCode::Find),
+        152 => Some(KeyCode::Open),
+        153 => Some(KeyCode::Paste),
+        154 => Some(KeyCode::Props),
+        155 => Some(KeyCode::Select),
+        156 => Some(KeyCode::Undo),
+        157 => Some(KeyCode::Hiragana),
+        158 => Some(KeyCode::Katakana),
+        159 => Some(KeyCode::F1),
+        160 => Some(KeyCode::F2),
+        161 => Some(KeyCode::F3),
+        162 => Some(KeyCode::F4),
+        163 => Some(KeyCode::F5),
+        164 => Some(KeyCode::F6),
+        165 => Some(KeyCode::F7),
+        166 => Some(KeyCode::F8),
+        167 => Some(KeyCode::F9),
+        168 => Some(KeyCode::F10),
+        169 => Some(KeyCode::F11),
+        170 => Some(KeyCode::F12),
+        171 => Some(KeyCode::F13),
+        172 => Some(KeyCode::F14),
+        173 => Some(KeyCode::F15),
+        174 => Some(KeyCode::F16),
+        175 => Some(KeyCode::F17),
+        176 => Some(KeyCode::F18),
+        177 => Some(KeyCode::F19),
+        178 => Some(KeyCode::F20),
+        179 => Some(KeyCode::F21),
+        180 => Some(KeyCode::F22),
+        181 => Some(KeyCode::F23),
+        182 => Some(KeyCode::F24),
+        183 => Some(KeyCode::F25),
+        184 => Some(KeyCode::F26),
+        185 => Some(KeyCode::F27),
+        186 => Some(KeyCode::F28),
+        187 => Some(KeyCode::F29),
+        188 => Some(KeyCode::F30),
+        189 => Some(KeyCode::F31),
+        190 => Some(KeyCode::F32),
+        191 => Some(KeyCode::F33),
+        192 => Some(KeyCode::F34),
+        193 => Some(KeyCode::F35),
+        _ => None,
+    }
+}
diff --git a/eucalyptus-editor/src/editor/dock.rs b/eucalyptus-editor/src/editor/dock.rs
index b6690e1..5617bbc 100644
--- a/eucalyptus-editor/src/editor/dock.rs
+++ b/eucalyptus-editor/src/editor/dock.rs
@@ -798,16 +798,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     ui.horizontal(|ui| {
                                         ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                             if ui.button("Remove Component ❌").clicked() {
-                                                // if let Some(target) = &follow_target {
-                                                //     *self.signal = Signal::RemoveComponent(
-                                                //         *entity,
-                                                //         Box::new(ComponentType::Camera(
-                                                //             Box::new(camera.clone()),
-                                                //             camera_component.clone(),
-                                                //             Some((*target).to_owned()),
-                                                //         )),
-                                                //     );
-                                                // } else {
                                                     *self.signal = Signal::RemoveComponent(
                                                         *entity,
                                                         Box::new(ComponentType::Camera(
@@ -839,17 +829,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                         &mut camera.label.clone(),
                                     );
 
-                                    // if let Some(target) = follow_target {
-                                    //     target.inspect(
-                                    //         entity,
-                                    //         &mut cfg,
-                                    //         ui,
-                                    //         self.undo_stack,
-                                    //         self.signal,
-                                    //         &mut camera.label.clone(),
-                                    //     );
-                                    // }
-
                                     ui.separator();
                                     ui.label("Camera Controls:");
                                     let mut active_camera = self.active_camera.lock();
diff --git a/eucalyptus-editor/src/editor/input.rs b/eucalyptus-editor/src/editor/input.rs
index 05860f3..8c05b41 100644
--- a/eucalyptus-editor/src/editor/input.rs
+++ b/eucalyptus-editor/src/editor/input.rs
@@ -313,15 +313,38 @@ impl Mouse for Editor {
 }
 
 impl Controller for Editor {
-    fn button_down(&mut self, _button: Button, _id: GamepadId) {}
+    fn button_down(&mut self, button: Button, id: GamepadId) {
+        self.input_state
+            .pressed_buttons
+            .entry(id)
+            .or_insert_with(HashSet::new)
+            .insert(button);
+    }
 
-    fn button_up(&mut self, _button: Button, _id: GamepadId) {}
+    fn button_up(&mut self, button: Button, id: GamepadId) {
+        if let Some(buttons) = self.input_state.pressed_buttons.get_mut(&id) {
+            buttons.remove(&button);
+        }
+    }
 
-    fn left_stick_changed(&mut self, _x: f32, _y: f32, _id: GamepadId) {}
+    fn left_stick_changed(&mut self, x: f32, y: f32, id: GamepadId) {
+        self.input_state.left_stick_position.insert(id, (x, y));
+    }
 
-    fn right_stick_changed(&mut self, _x: f32, _y: f32, _id: GamepadId) {}
+    fn right_stick_changed(&mut self, x: f32, y: f32, id: GamepadId) {
+        self.input_state.right_stick_position.insert(id, (x, y));
+    }
 
-    fn on_connect(&mut self, _id: GamepadId) {}
+    fn on_connect(&mut self, id: GamepadId) {
+        self.input_state.connected_gamepads.insert(id);
+    }
 
-    fn on_disconnect(&mut self, _id: GamepadId) {}
-}
+    fn on_disconnect(&mut self, id: GamepadId) {
+        self.input_state.connected_gamepads.remove(&id);
+        self.input_state.pressed_buttons.remove(&id);
+        self.input_state.left_stick_position.remove(&id);
+        self.input_state.right_stick_position.remove(&id);
+        self.input_state.left_trigger.remove(&id);
+        self.input_state.right_trigger.remove(&id);
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index f857ca1..1a3ec7c 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -84,7 +84,7 @@ pub struct Editor {
     pub play_mode_backup: Option<PlayModeBackup>,
 
     /// State of the input
-    pub(crate) input_state: InputState,
+    pub(crate) input_state: Box<InputState>,
 
     // channels
     /// A threadsafe Unbounded Receiver, typically used for checking the status of the world loading
@@ -181,7 +181,7 @@ impl Editor {
             editor_state: EditorState::Editing,
             gizmo_mode: EnumSet::empty(),
             play_mode_backup: None,
-            input_state: InputState::new(),
+            input_state: Box::new(InputState::new()),
             light_manager: LightManager::new(),
             active_camera: Arc::new(Mutex::new(None)),
             progress_tx: None,
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index caaf980..83cb1d6 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -17,6 +17,7 @@ use std::collections::HashMap;
 use std::path::PathBuf;
 use std::sync::Arc;
 use winit::keyboard::KeyCode;
+use eucalyptus_core::input::InputState;
 
 pub trait SignalController {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>;
@@ -257,11 +258,12 @@ impl SignalController for Editor {
                             }
 
                             let world_ptr = self.world.as_mut() as *mut World;
+                            let input_ptr = self.input_state.as_mut() as *mut InputState;
 
                             if let Err(e) = self.script_manager
                                 .load_script(
                                     world_ptr,
-                                    &self.input_state,
+                                    input_ptr,
                                 ) {
                                 fatal!(
                                     "Failed to initialise script because {}",
@@ -453,11 +455,12 @@ impl SignalController for Editor {
                                     }
 
                                     let world_ptr = self.world.as_mut() as *mut World;
+                                    let input_ptr = self.input_state.as_mut() as *mut InputState;
 
                                     if let Err(e) = self.script_manager
                                         .load_script(
                                             world_ptr,
-                                            &self.input_state,
+                                            input_ptr,
                                         ) {
                                         fatal!(
                                             "Failed to initialise script because {}",
diff --git a/gradle.properties b/gradle.properties
index 97ff880..272096b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,8 +1,25 @@
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.daemon=true
+org.gradle.workers.max=8
+
+# windows specific
+org.gradle.vfs.watch=false
+org.gradle.unsafe.watch-fs=false
+
+# config cache
+org.gradle.configuration-cache=true
+org.gradle.configuration-cache.problems=warn
+
+# Kotlin stuff
+kotlin.incremental=true
+kotlin.compiler.execution.strategy=daemon
 kotlin.code.style=official
 
+# other optimisations
+scan=false
+
+# Dokka docs
 org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
 org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
 
-org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxMetaspaceSize=512m
-org.gradle.daemon=true
-org.gradle.caching=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 37f853b..ca025c8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/headers/dropbear.h b/headers/dropbear.h
index 14fadb9..3be9a85 100644
--- a/headers/dropbear.h
+++ b/headers/dropbear.h
@@ -4,7 +4,12 @@
 #include <stddef.h>
 #include <stdint.h>
 
+// ===========================================
+
 typedef struct World World; // opaque pointer
+typedef struct InputState InputState; // opaque pointer
+
+// ===========================================
 
 #ifdef __cplusplus
 extern "C" {
@@ -41,6 +46,10 @@ int dropbear_set_transform(
     const NativeTransform transform
 );
 
+void dropbear_print_input_state(const InputState* input_state_ptr);
+
+int dropbear_is_key_pressed(const InputState* input_state_ptr, int keycode, int* out_value); // out_value is a boolean 0 or 1
+
 // ===========================================
 
 #ifdef __cplusplus
diff --git a/magna-carta/src/generator/native.rs b/magna-carta/src/generator/native.rs
index 9ad3982..935fdd9 100644
--- a/magna-carta/src/generator/native.rs
+++ b/magna-carta/src/generator/native.rs
@@ -19,20 +19,21 @@ impl Generator for KotlinNativeGenerator {
             output,
             "@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)"
         )?;
+        writeln!(output, "@file:Suppress(\"UNUSED_PARAMETER\", \"unused\")")?;
         writeln!(output)?;
 
         writeln!(output, "package com.dropbear.decl")?;
         writeln!(output)?;
 
-        writeln!(output, "import com.dropbear.DropbearEngine")?;
-        writeln!(output, "import com.dropbear.EntityId")?;
-        writeln!(output, "import com.dropbear.EntityRef")?;
-        writeln!(output, "import com.dropbear.System")?;
-        writeln!(output, "import com.dropbear.ffi.NativeEngine")?;
-        writeln!(output, "import kotlinx.cinterop.COpaquePointer")?;
-        writeln!(output, "import kotlinx.cinterop.ExperimentalForeignApi")?;
-        writeln!(output, "import kotlin.experimental.ExperimentalNativeApi")?;
-        writeln!(output, "import kotlin.native.CName")?;
+        writeln!(output, r#"
+import com.dropbear.DropbearEngine
+import com.dropbear.System
+import com.dropbear.ffi.NativeEngine
+import com.dropbear.logging.Logger
+import kotlinx.cinterop.COpaquePointer
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlin.experimental.ExperimentalNativeApi
+        "#)?;
         writeln!(output)?;
 
         let mut imported_classes = Vec::new();
@@ -59,78 +60,176 @@ impl Generator for KotlinNativeGenerator {
             }
         }
 
-        writeln!(output, "private fun getScriptFactories(tag: String): List<() -> System> {{")?;
-        writeln!(output, "    return when (tag) {{")?;
-
-        for (tag, classes) in &tag_map {
-            let factories: Vec<String> = classes
-                .iter()
-                .map(|cls| format!("{{ {}() }}", cls))
-                .collect();
-            writeln!(
-                output,
-                "        \"{}\" -> listOf({})",
-                tag,
-                factories.join(", ")
-            )?;
-        }
 
-        writeln!(output, "        else -> emptyList()")?;
-        writeln!(output, "    }}")?;
-        writeln!(output, "}}")?;
-        writeln!(output)?;
+        writeln!(output, r#"
+object ScriptManager {{
+    private var dropbearEngine: DropbearEngine? = null
+    private val scriptsByTag: MutableMap<String, MutableList<System>> = mutableMapOf()
 
-        writeln!(
-            output,
-            "private fun getDropbearEngine(worldPointer: COpaquePointer?): DropbearEngine {{"
-        )?;
-        writeln!(output, "    val nativeEngine = NativeEngine()")?;
-        writeln!(output, "    nativeEngine.init(worldPointer)")?;
-        writeln!(output, "    return DropbearEngine(nativeEngine)")?;
-        writeln!(output, "}}")?;
-        writeln!(output)?;
+    fun init(worldPtr: COpaquePointer?, inputPtr: COpaquePointer?) : Int {{
+        try {{
+            val nativeEngine = NativeEngine()
+            nativeEngine.init(worldPtr, inputPtr)
+            dropbearEngine = DropbearEngine(nativeEngine)
 
-        for (func_name, method_name) in [
-            ("dropbear_load", "load"),
-            ("dropbear_update", "update"),
-            ("dropbear_destroy", "destroy"),
-        ] {
-            let param_extra = if method_name == "update" {
-                ", deltaTime: Double"
-            } else {
-                ""
-            };
-            let call_extra = if method_name == "update" {
-                ", deltaTime.toFloat()"
-            } else {
-                ""
-            };
-
-            writeln!(output, "@CName(\"{}\")", func_name)?;
-            writeln!(
-                output,
-                "fun {}ScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?{}) {{",
-                method_name,
-                param_extra
-            )?;
-            writeln!(output, "    if (tag == null) return")?;
-            writeln!(output, "    val factories = getScriptFactories(tag)")?;
-            writeln!(
-                output,
-                "    val engine = getDropbearEngine(worldPointer)"
-            )?;
-            writeln!(output, "    for (factory in factories) {{")?;
-            writeln!(output, "        val script = factory()")?;
-            if method_name == "update" {
-                writeln!(output, "        script.{}(engine{})", method_name, call_extra)?;
-            } else {
-                writeln!(output, "        script.{}(engine)", method_name)?;
+            scriptsByTag.clear()
+            Logger.debug("Native ScriptManager initialised")
+            return 0
+        }} catch (e: Exception) {{
+            Logger.error("Native ScriptManager failed to initialise: ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun loadSystemsByTag(tag: String): Int {{
+        val engine = dropbearEngine ?: return -2
+        try {{
+            val factories = getScriptFactories(tag)
+            val instances = factories.map {{ it() }}
+
+            for (instance in instances) {{
+                instance.load(engine)
+            }}
+
+            scriptsByTag.getOrPut(tag) {{ mutableListOf() }}.addAll(instances)
+            Logger.debug("Loaded ${{instances.size}} script(s) for tag: '$tag'")
+            return 0
+        }} catch (e: Exception) {{
+            Logger.error("Error loading systems for tag '$tag': ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun updateAllSystems(dt: Float): Int {{
+        val engine = dropbearEngine ?: return -2
+        try {{
+            for (instances in scriptsByTag.values) {{
+                for (instance in instances) {{
+                    instance.update(engine, dt)
+                }}
+            }}
+            return 0
+        }} catch (e: Exception) {{
+            Logger.error("Error updating all systems: ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun updateSystemsByTag(tag: String, dt: Float): Int {{
+        val engine = dropbearEngine ?: return -2
+        try {{
+            val instances = scriptsByTag[tag] ?: emptyList()
+            for (instance in instances) {{
+                instance.update(engine, dt)
+            }}
+            return 0
+        }} catch (e: Exception) {{
+            Logger.error("Error updating systems for tag '$tag': ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun destroyByTag(tag: String): Int {{
+        try {{
+            val engine = dropbearEngine ?: return -2
+            val instances = scriptsByTag[tag] ?: emptyList()
+            for (instance in instances) {{
+                instance.destroy(engine)
+            }}
+            scriptsByTag.remove(tag)
+            Logger.debug("Destroyed ${{instances.size}} script(s) for tag: '$tag'")
+            return 0
+        }} catch (e: Exception) {{
+            Logger.error("Error destroying systems for tag '$tag': ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun destroyAll(): Int {{
+        try {{
+            val engine = dropbearEngine ?: return -2
+            for (instances in scriptsByTag.values) {{
+                for (instance in instances) {{
+                    instance.destroy(engine)
+                }}
+            }}
+            scriptsByTag.clear()
+            dropbearEngine = null
+            return 0
+        }} catch (e: Exception) {{
+            Logger.error("Error destroying scripts: ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+            "#)?;
+        // getScriptFactories (generated)
+        {
+            writeln!(output, "  private fun getScriptFactories(tag: String): List<() -> System> {{")?;
+            writeln!(output, "       return when (tag) {{")?;
+
+            for (tag, classes) in &tag_map {
+                let factories: Vec<String> = classes
+                    .iter()
+                    .map(|cls| format!("{{ {}() }}", cls))
+                    .collect();
+                writeln!(
+                    output,
+                    "           \"{}\" -> listOf({})",
+                    tag,
+                    factories.join(", ")
+                )?;
             }
-            writeln!(output, "    }}")?;
-            writeln!(output, "}}")?;
+
+            writeln!(output, "           else -> emptyList()")?;
+            writeln!(output, "        }}")?;
+            writeln!(output, "  }}")?;
             writeln!(output)?;
         }
 
+        writeln!(output, "}}")?;
+
+        writeln!(output, r#"
+@CName("dropbear_init")
+fun dropbear_native_init(worldPtr: COpaquePointer?, inputStatePtr: COpaquePointer?): Int {{
+    return ScriptManager.init(worldPtr, inputStatePtr)
+}}
+
+
+@CName("dropbear_load_tagged")
+fun dropbear_load_systems_for_tag(tag: String?): Int {{
+    if (tag == null) return -1
+    return ScriptManager.loadSystemsByTag(tag)
+}}
+
+@CName("dropbear_update_all")
+fun dropbear_update_all_systems(dt: Float): Int {{
+    return ScriptManager.updateAllSystems(dt)
+}}
+
+@CName("dropbear_update_tagged")
+fun dropbear_update_systems_for_tag(tag: String?, dt: Float): Int {{
+    if (tag == null) return -1
+    return ScriptManager.updateSystemsByTag(tag, dt)
+}}
+
+@CName("dropbear_destroy_tagged")
+fun dropbear_destroy(tag: String?): Int {{
+    if (tag == null) return -1
+    return ScriptManager.destroyByTag(tag)
+}}
+
+@CName("dropbear_destroy_all")
+fun dropbear_destroy_all(): Int {{
+    return ScriptManager.destroyAll()
+}}
+        "#)?;
+
         Ok(output)
     }
 }
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index f40473a..e92b872 100644
--- a/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -1,9 +1,13 @@
 package com.dropbear
 
 import com.dropbear.ffi.NativeEngine
+import com.dropbear.input.InputState
+import com.dropbear.logging.Logger
 import com.dropbear.math.Transform
 
 class DropbearEngine(val native: NativeEngine) {
+    private var inputState: InputState? = null
+
     public fun getEntity(label: String): EntityRef? {
         val entityId = native.getEntity(label)
         val entityRef = if (entityId != null) EntityRef(EntityId(entityId)) else null
@@ -11,6 +15,14 @@ class DropbearEngine(val native: NativeEngine) {
         return entityRef
     }
 
+    public fun getInputState(): InputState {
+        if (this.inputState == null) {
+            Logger.trace("InputState not initialised, creating new one")
+            this.inputState = InputState(this)
+        }
+        return this.inputState!!
+    }
+
     internal fun getTransform(entityId: EntityId): Transform? {
         val result = native.getTransform(entityId)
         return result
diff --git a/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt b/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
index 4c89991..436a286 100644
--- a/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
+++ b/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
@@ -1,10 +1,32 @@
 package com.dropbear.ffi
 
 import com.dropbear.EntityId
+import com.dropbear.input.KeyCode
 import com.dropbear.math.Transform
 
 expect class NativeEngine {
+    /**
+     * Fetches the entity from its label, returning a [Long] (the entity ID)
+     */
     fun getEntity(label: String): Long?
+
+    /**
+     * Fetches the [Transform] component of an entity by it's ID
+     */
     fun getTransform(entityId: EntityId): Transform?
+
+    /**
+     * Sets an entities [Transform] component.
+     */
     fun setTransform(entityId: EntityId, transform: Transform)
+
+    /**
+     * Prints the input state, typically used for debugging.
+     */
+    fun printInputState()
+
+    /**
+     * Checks if a Key is pressed by its KeyCode
+     */
+    fun isKeyPressed(key: KeyCode): Boolean
 }
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/input/InputState.kt b/src/commonMain/kotlin/com/dropbear/input/InputState.kt
new file mode 100644
index 0000000..9560080
--- /dev/null
+++ b/src/commonMain/kotlin/com/dropbear/input/InputState.kt
@@ -0,0 +1,14 @@
+package com.dropbear.input
+
+import com.dropbear.DropbearEngine
+
+class InputState(private val engine: DropbearEngine) {
+
+    fun printInputState() {
+        engine.native.printInputState()
+    }
+
+    fun isKeyPressed(key: KeyCode): Boolean {
+        return engine.native.isKeyPressed(key)
+    }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/input/KeyCode.kt b/src/commonMain/kotlin/com/dropbear/input/KeyCode.kt
new file mode 100644
index 0000000..586521f
--- /dev/null
+++ b/src/commonMain/kotlin/com/dropbear/input/KeyCode.kt
@@ -0,0 +1,970 @@
+package com.dropbear.input
+
+/**
+ * Enum values representing KeyCodes. Taken from the enum `winit::keyboard::KeyCode`.
+ *
+ * Code representing the location of a physical key.
+ *
+ * This mostly conforms to the UI Events Specification's [KeyboardEvent.code](https://w3c.github.io/uievents-code/#code-value-tables)
+ * with a few exceptions:
+ *  - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and
+ *    "SuperRight" here.
+ *  - The key that the specification calls "Super" is reported as `Unidentified` here.
+ */
+@Suppress("unused")
+public enum class KeyCode {
+    /**
+     * <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
+     * This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
+     * (hankaku/zenkaku/kanji) key on Japanese keyboards
+     */
+    Backquote,
+
+    /**
+     * Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
+     * located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
+     * 104- and 106-key layouts.
+     * Labeled <kbd>#</kbd> on a UK (102) keyboard.
+     */
+    Backslash,
+
+    /**
+     * <kbd>[</kbd> on a US keyboard.
+     */
+    BracketLeft,
+
+    /**
+     * <kbd>]</kbd> on a US keyboard.
+     */
+    BracketRight,
+
+    /**
+     * <kbd>,</kbd> on a US keyboard.
+     */
+    Comma,
+
+    /**
+     * <kbd>0</kbd> on a US keyboard.
+     */
+    Digit0,
+
+    /**
+     * <kbd>1</kbd> on a US keyboard.
+     */
+    Digit1,
+
+    /**
+     * <kbd>2</kbd> on a US keyboard.
+     */
+    Digit2,
+
+    /**
+     * <kbd>3</kbd> on a US keyboard.
+     */
+    Digit3,
+
+    /**
+     * <kbd>4</kbd> on a US keyboard.
+     */
+    Digit4,
+
+    /**
+     * <kbd>5</kbd> on a US keyboard.
+     */
+    Digit5,
+
+    /**
+     * <kbd>6</kbd> on a US keyboard.
+     */
+    Digit6,
+
+    /**
+     * <kbd>7</kbd> on a US keyboard.
+     */
+    Digit7,
+
+    /**
+     * <kbd>8</kbd> on a US keyboard.
+     */
+    Digit8,
+
+    /**
+     * <kbd>9</kbd> on a US keyboard.
+     */
+    Digit9,
+
+    /**
+     * <kbd>=</kbd> on a US keyboard.
+     */
+    Equal,
+
+    /**
+     * Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
+     * Labeled <kbd>\\</kbd> on a UK keyboard.
+     */
+    IntlBackslash,
+
+    /**
+     * Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
+     * Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
+     */
+    IntlRo,
+
+    /**
+     * Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
+     * Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
+     * Russian keyboard.
+     */
+    IntlYen,
+
+    /**
+     * <kbd>a</kbd> on a US keyboard.
+     * Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
+     */
+    KeyA,
+
+    /**
+     * <kbd>b</kbd> on a US keyboard.
+     */
+    KeyB,
+
+    /**
+     * <kbd>c</kbd> on a US keyboard.
+     */
+    KeyC,
+
+    /**
+     * <kbd>d</kbd> on a US keyboard.
+     */
+    KeyD,
+
+    /**
+     * <kbd>e</kbd> on a US keyboard.
+     */
+    KeyE,
+
+    /**
+     * <kbd>f</kbd> on a US keyboard.
+     */
+    KeyF,
+
+    /**
+     * <kbd>g</kbd> on a US keyboard.
+     */
+    KeyG,
+
+    /**
+     * <kbd>h</kbd> on a US keyboard.
+     */
+    KeyH,
+
+    /**
+     * <kbd>i</kbd> on a US keyboard.
+     */
+    KeyI,
+
+    /**
+     * <kbd>j</kbd> on a US keyboard.
+     */
+    KeyJ,
+
+    /**
+     * <kbd>k</kbd> on a US keyboard.
+     */
+    KeyK,
+
+    /**
+     * <kbd>l</kbd> on a US keyboard.
+     */
+    KeyL,
+
+    /**
+     * <kbd>m</kbd> on a US keyboard.
+     */
+    KeyM,
+
+    /**
+     * <kbd>n</kbd> on a US keyboard.
+     */
+    KeyN,
+
+    /**
+     * <kbd>o</kbd> on a US keyboard.
+     */
+    KeyO,
+
+    /**
+     * <kbd>p</kbd> on a US keyboard.
+     */
+    KeyP,
+
+    /**
+     * <kbd>q</kbd> on a US keyboard.
+     * Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
+     */
+    KeyQ,
+
+    /**
+     * <kbd>r</kbd> on a US keyboard.
+     */
+    KeyR,
+
+    /**
+     * <kbd>s</kbd> on a US keyboard.
+     */
+    KeyS,
+
+    /**
+     * <kbd>t</kbd> on a US keyboard.
+     */
+    KeyT,
+
+    /**
+     * <kbd>u</kbd> on a US keyboard.
+     */
+    KeyU,
+
+    /**
+     * <kbd>v</kbd> on a US keyboard.
+     */
+    KeyV,
+
+    /**
+     * <kbd>w</kbd> on a US keyboard.
+     * Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
+     */
+    KeyW,
+
+    /**
+     * <kbd>x</kbd> on a US keyboard.
+     */
+    KeyX,
+
+    /**
+     * <kbd>y</kbd> on a US keyboard.
+     * Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
+     */
+    KeyY,
+
+    /**
+     * <kbd>z</kbd> on a US keyboard.
+     * Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
+     * QWERTZ (e.g., German) keyboard.
+     */
+    KeyZ,
+
+    /**
+     * <kbd>-</kbd> on a US keyboard.
+     */
+    Minus,
+
+    /**
+     * <kbd>.</kbd> on a US keyboard.
+     */
+    Period,
+
+    /**
+     * <kbd>'</kbd> on a US keyboard.
+     */
+    Quote,
+
+    /**
+     * <kbd>;</kbd> on a US keyboard.
+     */
+    Semicolon,
+
+    /**
+     * <kbd>/</kbd> on a US keyboard.
+     */
+    Slash,
+
+    /**
+     * <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+     */
+    AltLeft,
+
+    /**
+     * <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+     * This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
+     */
+    AltRight,
+
+    /**
+     * <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
+     * Labeled <kbd>Delete</kbd> on Apple keyboards.
+     */
+    Backspace,
+
+    /**
+     * <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
+     */
+    CapsLock,
+
+    /**
+     * The application context menu key, which is typically found between the right
+     * <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
+     */
+    ContextMenu,
+
+    /**
+     * <kbd>Control</kbd> or <kbd>⌃</kbd>
+     */
+    ControlLeft,
+
+    /**
+     * <kbd>Control</kbd> or <kbd>⌃</kbd>
+     */
+    ControlRight,
+
+    /**
+     * <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
+     */
+    Enter,
+
+    /**
+     * The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+     */
+    SuperLeft,
+
+    /**
+     * The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+     */
+    SuperRight,
+
+    /**
+     * <kbd>Shift</kbd> or <kbd>⇧</kbd>
+     */
+    ShiftLeft,
+
+    /**
+     * <kbd>Shift</kbd> or <kbd>⇧</kbd>
+     */
+    ShiftRight,
+
+    /**
+     * <kbd> </kbd> (space)
+     */
+    Space,
+
+    /**
+     * <kbd>Tab</kbd> or <kbd>⇥</kbd>
+     */
+    Tab,
+
+    /**
+     * Japanese: <kbd>変</kbd> (henkan)
+     */
+    Convert,
+
+    /**
+     * Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
+     * (katakana/hiragana/romaji)
+     */
+    KanaMode,
+
+    /**
+     * Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
+     *
+     * Japanese (Mac keyboard): <kbd>か</kbd> (kana)
+     */
+    Lang1,
+
+    /**
+     * Korean: Hanja <kbd>한</kbd> (hanja)
+     *
+     * Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
+     */
+    Lang2,
+
+    /**
+     * Japanese (word-processing keyboard): Katakana
+     */
+    Lang3,
+
+    /**
+     * Japanese (word-processing keyboard): Hiragana
+     */
+    Lang4,
+
+    /**
+     * Japanese (word-processing keyboard): Zenkaku/Hankaku
+     */
+    Lang5,
+
+    /**
+     * Japanese: <kbd>無変換</kbd> (muhenkan)
+     */
+    NonConvert,
+
+    /**
+     * <kbd>⌦</kbd>. The forward delete key.
+     * Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part of
+     * the keyboard is encoded as [Backspace].
+     */
+    Delete,
+
+    /**
+     * <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
+     */
+    End,
+
+    /**
+     * <kbd>Help</kbd>. Not present on standard PC keyboards.
+     */
+    Help,
+
+    /**
+     * <kbd>Home</kbd> or <kbd>↖</kbd>
+     */
+    Home,
+
+    /**
+     * <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
+     */
+    Insert,
+
+    /**
+     * <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
+     */
+    PageDown,
+
+    /**
+     * <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
+     */
+    PageUp,
+
+    /**
+     * <kbd>↓</kbd>
+     */
+    ArrowDown,
+
+    /**
+     * <kbd>←</kbd>
+     */
+    ArrowLeft,
+
+    /**
+     * <kbd>→</kbd>
+     */
+    ArrowRight,
+
+    /**
+     * <kbd>↑</kbd>
+     */
+    ArrowUp,
+
+    /**
+     * On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
+     */
+    NumLock,
+
+    /**
+     * <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
+     */
+    Numpad0,
+
+    /**
+     * <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or remote
+     * control
+     */
+    Numpad1,
+
+    /**
+     * <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
+     */
+    Numpad2,
+
+    /**
+     * <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
+     */
+    Numpad3,
+
+    /**
+     * <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
+     */
+    Numpad4,
+
+    /**
+     * <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
+     */
+    Numpad5,
+
+    /**
+     * <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
+     */
+    Numpad6,
+
+    /**
+     * <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
+     * or remote control
+     */
+    Numpad7,
+
+    /**
+     * <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
+     */
+    Numpad8,
+
+    /**
+     * <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
+     * or remote control
+     */
+    Numpad9,
+
+    /**
+     * <kbd>+</kbd>
+     */
+    NumpadAdd,
+
+    /**
+     * Found on the Microsoft Natural Keyboard.
+     */
+    NumpadBackspace,
+
+    /**
+     * <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
+     * <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the Mac, the
+     * numpad <kbd>Clear</kbd> key is encoded as [NumLock].
+     */
+    NumpadClear,
+
+    /**
+     * <kbd>C</kbd> (Clear Entry)
+     */
+    NumpadClearEntry,
+
+    /**
+     * <kbd>,</kbd> (thousands separator). For locales where the thousands separator
+     * is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
+     */
+    NumpadComma,
+
+    /**
+     * <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
+     * Brazil), this key may generate a <kbd>,</kbd>.
+     */
+    NumpadDecimal,
+
+    /**
+     * <kbd>/</kbd>
+     */
+    NumpadDivide,
+
+    NumpadEnter,
+
+    /**
+     * <kbd>=</kbd>
+     */
+    NumpadEqual,
+
+    /**
+     * <kbd>#</kbd> on a phone or remote control device. This key is typically found
+     * below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
+     */
+    NumpadHash,
+
+    /**
+     * <kbd>M</kbd> Add current entry to the value stored in memory.
+     */
+    NumpadMemoryAdd,
+
+    /**
+     * <kbd>M</kbd> Clear the value stored in memory.
+     */
+    NumpadMemoryClear,
+
+    /**
+     * <kbd>M</kbd> Replace the current entry with the value stored in memory.
+     */
+    NumpadMemoryRecall,
+
+    /**
+     * <kbd>M</kbd> Replace the value stored in memory with the current entry.
+     */
+    NumpadMemoryStore,
+
+    /**
+     * <kbd>M</kbd> Subtract current entry from the value stored in memory.
+     */
+    NumpadMemorySubtract,
+
+    /**
+     * <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
+     * operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
+     *
+     * Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
+     */
+    NumpadMultiply,
+
+    /**
+     * <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
+     */
+    NumpadParenLeft,
+
+    /**
+     * <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
+     */
+    NumpadParenRight,
+
+    /**
+     * <kbd>*</kbd> on a phone or remote control device.
+     *
+     * This key is typically found below the <kbd>7</kbd> key and to the left of
+     * the <kbd>0</kbd> key.
+     *
+     * Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
+     * numeric keypads.
+     */
+    NumpadStar,
+
+    /**
+     * <kbd>-</kbd>
+     */
+    NumpadSubtract,
+
+    /**
+     * <kbd>Esc</kbd> or <kbd>⎋</kbd>
+     */
+    Escape,
+
+    /**
+     * <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate code.
+     */
+    Fn,
+
+    /**
+     * <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
+     * Natural Keyboard.
+     */
+    FnLock,
+
+    /**
+     * <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
+     */
+    PrintScreen,
+
+    /**
+     * <kbd>Scroll Lock</kbd>
+     */
+    ScrollLock,
+
+    /**
+     * <kbd>Pause Break</kbd>
+     */
+    Pause,
+
+    /**
+     * Some laptops place this key to the left of the <kbd>↑</kbd> key.
+     *
+     * This also the "back" button (triangle) on Android.
+     */
+    BrowserBack,
+
+    BrowserFavorites,
+
+    /**
+     * Some laptops place this key to the right of the <kbd>↑</kbd> key.
+     */
+    BrowserForward,
+
+    /**
+     * The "home" button on Android.
+     */
+    BrowserHome,
+
+    BrowserRefresh,
+
+    BrowserSearch,
+
+    BrowserStop,
+
+    /**
+     * <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on some Apple
+     * keyboards.
+     */
+    Eject,
+
+    /**
+     * Sometimes labelled <kbd>My Computer</kbd> on the keyboard
+     */
+    LaunchApp1,
+
+    /**
+     * Sometimes labelled <kbd>Calculator</kbd> on the keyboard
+     */
+    LaunchApp2,
+
+    LaunchMail,
+
+    MediaPlayPause,
+
+    MediaSelect,
+
+    MediaStop,
+
+    MediaTrackNext,
+
+    MediaTrackPrevious,
+
+    /**
+     * This key is placed in the function section on some Apple keyboards, replacing the
+     * <kbd>Eject</kbd> key.
+     */
+    Power,
+
+    Sleep,
+
+    AudioVolumeDown,
+
+    AudioVolumeMute,
+
+    AudioVolumeUp,
+
+    WakeUp,
+
+    // Legacy modifier key. Also called "Super" in certain places.
+    Meta,
+
+    // Legacy modifier key.
+    Hyper,
+
+    Turbo,
+
+    Abort,
+
+    Resume,
+
+    Suspend,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Again,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Copy,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Cut,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Find,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Open,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Paste,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Props,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Select,
+
+    /**
+     * Found on Sun's USB keyboard.
+     */
+    Undo,
+
+    /**
+     * Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing keyboards.
+     */
+    Hiragana,
+
+    /**
+     * Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing keyboards.
+     */
+    Katakana,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F1,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F2,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F3,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F4,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F5,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F6,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F7,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F8,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F9,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F10,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F11,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F12,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F13,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F14,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F15,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F16,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F17,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F18,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F19,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F20,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F21,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F22,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F23,
+
+    /**
+     * General-purpose function key.
+     * Usually found at the top of the keyboard.
+     */
+    F24,
+
+    /** General-purpose function key. */
+    F25,
+
+    /** General-purpose function key. */
+    F26,
+
+    /** General-purpose function key. */
+    F27,
+
+    /** General-purpose function key. */
+    F28,
+
+    /** General-purpose function key. */
+    F29,
+
+    /** General-purpose function key. */
+    F30,
+
+    /** General-purpose function key. */
+    F31,
+
+    /** General-purpose function key. */
+    F32,
+
+    /** General-purpose function key. */
+    F33,
+
+    /** General-purpose function key. */
+    F34,
+
+    /** General-purpose function key. */
+    F35,
+}
\ No newline at end of file
diff --git a/src/jvmMain/java/com/dropbear/ffi/JNINative.java b/src/jvmMain/java/com/dropbear/ffi/JNINative.java
index 7968c49..9fe0f59 100644
--- a/src/jvmMain/java/com/dropbear/ffi/JNINative.java
+++ b/src/jvmMain/java/com/dropbear/ffi/JNINative.java
@@ -7,8 +7,14 @@ public class JNINative {
         System.loadLibrary("eucalyptus_core");
     }
 
+    // entity and ecs
     public static native long getEntity(long handle, String label);
 
+    // transformations
     public static native Transform getTransform(long handle, long entityHandle);
     public static native void setTransform(long worldHandle, long id, Transform transform);
+
+    // input
+    public static native void printInputState(long inputHandle);
+    public static native boolean isKeyPressed(long inputHandle, int ordinal);
 }
diff --git a/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt b/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
index 49a0101..ed7ec46 100644
--- a/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
+++ b/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
@@ -1,18 +1,21 @@
 package com.dropbear.ffi
 
 import com.dropbear.EntityId
+import com.dropbear.input.KeyCode
 import com.dropbear.math.Transform
 
 actual class NativeEngine {
     private var worldHandle: Long = 0L
+    private var inputHandle: Long = 0L
 
     actual fun getEntity(label: String): Long? {
         return JNINative.getEntity(worldHandle, label)
     }
 
     @JvmName("init")
-    fun init(handle: Long) {
-        this.worldHandle = handle
+    fun init(worldHandle: Long, inputHandle: Long) {
+        this.worldHandle = worldHandle
+        this.inputHandle = inputHandle
         if (this.worldHandle < 0L) {
             println("NativeEngine: Error - Invalid world handle received!")
             return
@@ -26,4 +29,12 @@ actual class NativeEngine {
     actual fun setTransform(entityId: EntityId, transform: Transform) {
         return JNINative.setTransform(worldHandle, entityId.id, transform)
     }
+
+    actual fun printInputState() {
+        return JNINative.printInputState(inputHandle)
+    }
+
+    actual fun isKeyPressed(key: KeyCode): Boolean {
+        return JNINative.isKeyPressed(inputHandle, key.ordinal)
+    }
 }
\ No newline at end of file
diff --git a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
index 58c2dec..7a070d5 100644
--- a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
+++ b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
@@ -5,6 +5,7 @@ package com.dropbear.ffi
 
 import com.dropbear.EntityId
 import com.dropbear.ffi.generated.*
+import com.dropbear.input.KeyCode
 import com.dropbear.logging.Logger
 import com.dropbear.math.Transform
 import kotlinx.cinterop.*
@@ -12,12 +13,17 @@ import kotlin.experimental.ExperimentalNativeApi
 
 actual class NativeEngine {
     private var worldHandle: COpaquePointer? = null
+    private var inputHandle: COpaquePointer? = null
 
-    @Suppress("unused") // called from jni
-    fun init(handle: COpaquePointer?) {
-        this.worldHandle = handle
+    @Suppress("unused")
+    fun init(worldHandle: COpaquePointer?, inputHandle: COpaquePointer?) {
+        this.worldHandle = worldHandle
+        this.inputHandle = inputHandle
         if (this.worldHandle == null) {
-            Logger.info("NativeEngine: Error - Invalid world handle received!")
+            Logger.error("NativeEngine: Error - Invalid world handle received!")
+        }
+        if (this.inputHandle == null) {
+            Logger.error("NativeEngine: Error - Invalid input handle received!")
         }
     }
 
@@ -93,4 +99,22 @@ actual class NativeEngine {
             )
         }
     }
+
+    actual fun printInputState() {
+        val input = inputHandle ?: return
+        dropbear_print_input_state(input_state_ptr = input.reinterpret())
+    }
+
+    actual fun isKeyPressed(key: KeyCode): Boolean {
+        val input = inputHandle ?: return false
+        memScoped {
+            val out = alloc<IntVar>()
+            dropbear_is_key_pressed(
+                input.reinterpret(),
+                key.ordinal,
+                out.ptr
+            )
+            return out.value != 0
+        }
+    }
 }
\ No newline at end of file