kitgit

tirbofish/dropbear · diff

a42d874 · Thribhu K

fix up some issues to do with features and add a new engine.quit() function which quits playing. jarvis, run github actions fixes #69

Unverified

diff --git a/README.md b/README.md
index fd7731c..baa19fa 100644
--- a/README.md
+++ b/README.md
@@ -62,8 +62,19 @@ cargo build
 > [!TIP]
 > It is recommended to use IntelliJ IDEA with the Rust plugin to help contribute to the engine. 
 > 
-> If you are a normal joe,
-> then just use the standard IntelliJ IDEA.
+> If you are a normal joe, then just use the standard IntelliJ IDEA.
+
+<details>
+
+<summary>For the engine developers</summary>
+
+> [!WARNING]
+> Ensure that `cargo build` is ran for each iteration of testing instead of `cargo build -p eucalyptus-editor` due to some weird ABI issue with Rust. If you do only want to build
+> a specific package, ensure you run `cargo build -p {package} -p eucalyptus-core`. 
+> 
+> If you get any FFI errors (likely a getter), you compiled the library wrong. 
+
+</details>
 
 ### Prebuilt
 
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index bd7447d..f32139e 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -44,7 +44,8 @@ rustc_version_runtime.workspace = true
 
 [features]
 default = []
-
+# internal redback-runtime configurations because some stuff keep on enabling for JVM
+runtime = []
 # editor only stuff
 editor = ["jvm", "rfd", "jvm_debug"]
 # enabled scripttarget::jvm as an option 
diff --git a/eucalyptus-core/src/ptr.rs b/eucalyptus-core/src/ptr.rs
index 2b19cb6..dcf7197 100644
--- a/eucalyptus-core/src/ptr.rs
+++ b/eucalyptus-core/src/ptr.rs
@@ -1,10 +1,10 @@
 use crate::input::InputState;
-use crate::window::GraphicsCommand;
+use crate::window::CommandBuffer;
 use crossbeam_channel::Sender;
 use dropbear_engine::asset::AssetRegistry;
 use hecs::World;
 
 pub type WorldPtr = *mut World;
 pub type InputStatePtr = *mut InputState;
-pub type GraphicsPtr = *const Sender<GraphicsCommand>;
+pub type GraphicsPtr = *const Sender<CommandBuffer>;
 pub type AssetRegistryPtr = *const AssetRegistry;
diff --git a/eucalyptus-core/src/scene.rs b/eucalyptus-core/src/scene.rs
index 97f52f8..4f59ffa 100644
--- a/eucalyptus-core/src/scene.rs
+++ b/eucalyptus-core/src/scene.rs
@@ -581,7 +581,7 @@ impl SceneConfig {
         }
 
         log::info!("Loaded {} entities from scene", self.entities.len());
-        #[cfg(feature = "editor")]
+        #[cfg(all(feature = "editor", not(feature = "runtime")))]
         {
             log::debug!("editor feature enabled");
             use crate::camera::CameraType;
@@ -602,7 +602,7 @@ impl SceneConfig {
             {
                 if let Some(camera_entity) = debug_camera {
                     log::info!("Using existing debug camera for editor");
-                    Ok(camera_entity)
+                    return Ok(camera_entity);
                 } else {
                     log::info!("No debug camera found, creating viewport camera for editor");
 
@@ -644,12 +644,12 @@ impl SceneConfig {
                     let component = crate::camera::DebugCamera::new();
                     let label = Label::new("Viewport Camera");
                     let camera_entity = { world.spawn((label, camera, component)) };
-                    Ok(camera_entity)
+                    return Ok(camera_entity);
                 }
             }
         }
 
-        #[cfg(not(feature = "editor"))]
+        #[cfg(any(not(feature = "editor"), feature = "runtime"))]
         {
             log::debug!("loading player camera without editor feature");
             let player_camera = world
@@ -667,7 +667,7 @@ impl SceneConfig {
                 log::info!("Using player camera for runtime");
                 log::debug!("Checkpoint 1: Entity id: {:?}", e);
                 cam.debug_camera_state();
-                Ok(e)
+                return Ok(e);
             } else {
                 panic!("Runtime mode requires an initial camera, but none was found in the scene");
                 // todo: get a better way of rendering something without a camera.
diff --git a/eucalyptus-core/src/scripting/jni/exports.rs b/eucalyptus-core/src/scripting/jni/exports.rs
index fb0ac27..058a3dc 100644
--- a/eucalyptus-core/src/scripting/jni/exports.rs
+++ b/eucalyptus-core/src/scripting/jni/exports.rs
@@ -8,7 +8,7 @@ use crate::scripting::jni::utils::{
 };
 use crate::states::{Label, ModelProperties, Value};
 use crate::utils::keycode_from_ordinal;
-use crate::window::{GraphicsCommand, WindowCommand};
+use crate::window::{CommandBuffer, WindowCommand};
 use crate::{convert_jlong_to_entity, convert_jstring, convert_ptr, ffi_error_return};
 use dropbear_engine::asset::PointerKind::Const;
 use dropbear_engine::asset::{ASSET_REGISTRY, AssetHandle, AssetRegistry};
@@ -604,7 +604,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorLocked(
     _env: JNIEnv,
     _class: JClass,
     input_handle: jlong,
-    graphics_handle: jlong,
+    command_handle: jlong,
     locked: jboolean,
 ) {
     let input = input_handle as InputStatePtr;
@@ -616,7 +616,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorLocked(
         return;
     }
 
-    let graphics = graphics_handle as GraphicsPtr;
+    let graphics = command_handle as GraphicsPtr;
 
     if graphics.is_null() {
         eprintln!(
@@ -630,7 +630,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorLocked(
 
     let is_locked = locked != 0;
 
-    if let Err(e) = graphics.send(GraphicsCommand::WindowCommand(WindowCommand::WindowGrab(
+    if let Err(e) = graphics.send(CommandBuffer::WindowCommand(WindowCommand::WindowGrab(
         is_locked,
     ))) {
         eprintln!(
@@ -1859,7 +1859,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorHidden(
     _env: JNIEnv,
     _class: JClass,
     input_handle: jlong,
-    graphics_handle: jlong,
+    command_handle: jlong,
     hide: jboolean,
 ) {
     let input = input_handle as InputStatePtr;
@@ -1871,7 +1871,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorHidden(
     }
     let input = unsafe { &mut *input };
 
-    let graphics = graphics_handle as GraphicsPtr;
+    let graphics = command_handle as GraphicsPtr;
     if graphics.is_null() {
         println!(
             "[Java_com_dropbear_ffi_JNINative_setCursorHidden] [ERROR] Input state pointer is null"
@@ -1882,7 +1882,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorHidden(
 
     let hide = hide != JNI_FALSE;
 
-    if let Err(e) = graphics.send(GraphicsCommand::WindowCommand(WindowCommand::HideCursor(
+    if let Err(e) = graphics.send(CommandBuffer::WindowCommand(WindowCommand::HideCursor(
         hide,
     ))) {
         println!(
@@ -2609,3 +2609,27 @@ pub fn Java_com_dropbear_ffi_JNINative_getParent(
         crate::ffi_error_return!("No entity exists")
     }
 }
+
+/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_quit
+///   (JNIEnv *, jclass, jlong);`
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_quit(
+    _env: JNIEnv,
+    _class: JClass,
+    command_handle: jlong,
+) {
+    let graphics = command_handle as GraphicsPtr;
+
+    if graphics.is_null() {
+        eprintln!(
+            "[Java_com_dropbear_ffi_JNINative_quit] [ERROR] Graphics pointer is null"
+        );
+        panic!("NullHandle while quitting GraphicsCommand, better off to shoot with gun than to nicely ask...")
+    }
+
+    let graphics = unsafe { &*graphics };
+
+    if let Err(e) = graphics.send(CommandBuffer::Quit) {
+        panic!("Unable to send window command while quitting GraphicsCommand, better off to shoot with gun than to nicely ask... \n Error: {}", e);
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/native/exports.rs b/eucalyptus-core/src/scripting/native/exports.rs
index 91ba1ae..f540200 100644
--- a/eucalyptus-core/src/scripting/native/exports.rs
+++ b/eucalyptus-core/src/scripting/native/exports.rs
@@ -12,7 +12,7 @@ use crate::scripting::native::types::{
 };
 use crate::states::{Label, ModelProperties, Value};
 use crate::utils::keycode_from_ordinal;
-use crate::window::{GraphicsCommand, WindowCommand};
+use crate::window::{CommandBuffer, WindowCommand};
 use dropbear_engine::asset::{PointerKind::Const, ASSET_REGISTRY, AssetHandle};
 use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
@@ -955,7 +955,7 @@ pub unsafe extern "C" fn dropbear_set_cursor_locked(
     input.is_cursor_locked = locked != 0;
 
     if graphics
-        .send(GraphicsCommand::WindowCommand(WindowCommand::WindowGrab(
+        .send(CommandBuffer::WindowCommand(WindowCommand::WindowGrab(
             input.is_cursor_locked,
         )))
         .is_err()
@@ -1223,7 +1223,7 @@ pub unsafe extern "C" fn dropbear_set_cursor_hidden(
     input.is_cursor_hidden = hidden != 0;
 
     if graphics
-        .send(GraphicsCommand::WindowCommand(WindowCommand::HideCursor(
+        .send(CommandBuffer::WindowCommand(WindowCommand::HideCursor(
             input.is_cursor_hidden,
         )))
         .is_err()
@@ -1922,3 +1922,21 @@ pub unsafe extern "C" fn dropbear_get_parent(
         }
     }
 }
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn dropbear_quit(
+    command_ptr: GraphicsPtr,
+) {
+    if command_ptr.is_null() {
+        panic!("NullPointer (-1) while quitting GraphicsCommand, better off to shoot with gun than to nicely ask...")
+    }
+
+    let graphics = unsafe { &*(command_ptr as GraphicsPtr) };
+
+    if graphics
+        .send(CommandBuffer::Quit)
+        .is_err()
+    {
+        panic!("SendError (-7) while quitting GraphicsCommand, better off to shoot with gun than to nicely ask...")
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/window.rs b/eucalyptus-core/src/window.rs
index e18580d..f630c4c 100644
--- a/eucalyptus-core/src/window.rs
+++ b/eucalyptus-core/src/window.rs
@@ -1,23 +1,23 @@
 use crossbeam_channel::{Receiver, Sender, unbounded};
+use dropbear_engine::graphics::RenderContext;
 use once_cell::sync::Lazy;
 use parking_lot::RwLock;
-use std::sync::{Arc, OnceLock};
-use winit::window::{CursorGrabMode, Window};
+use std::sync::{OnceLock};
 
-pub static GRAPHICS_COMMAND: Lazy<(Box<Sender<GraphicsCommand>>, Receiver<GraphicsCommand>)> =
+pub static GRAPHICS_COMMAND: Lazy<(Box<Sender<CommandBuffer>>, Receiver<CommandBuffer>)> =
     Lazy::new(|| {
-        let (tx, rx) = unbounded::<GraphicsCommand>();
+        let (tx, rx) = unbounded::<CommandBuffer>();
         (Box::new(tx), rx)
     });
 static PREVIOUS_CONFIG: OnceLock<RwLock<CommandCache>> = OnceLock::new();
 
-fn get_config() -> &'static RwLock<CommandCache> {
+pub fn get_config() -> &'static RwLock<CommandCache> {
     PREVIOUS_CONFIG.get_or_init(|| RwLock::new(CommandCache::new()))
 }
 
-struct CommandCache {
-    is_locked: bool,
-    is_hidden: bool,
+pub struct CommandCache {
+    pub is_locked: bool,
+    pub is_hidden: bool,
 }
 
 impl CommandCache {
@@ -30,8 +30,9 @@ impl CommandCache {
 }
 
 #[derive(Debug)]
-pub enum GraphicsCommand {
+pub enum CommandBuffer {
     WindowCommand(WindowCommand),
+    Quit,
 }
 
 #[derive(Debug)]
@@ -40,43 +41,7 @@ pub enum WindowCommand {
     HideCursor(bool),
 }
 
-pub fn poll(window: Arc<Window>) {
-    while let Ok(cmd) = GRAPHICS_COMMAND.1.try_recv() {
-        log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd);
-        match cmd {
-            GraphicsCommand::WindowCommand(w_cmd) => match w_cmd {
-                WindowCommand::WindowGrab(is_locked) => {
-                    let mut cfg = get_config().write();
-                    if cfg.is_locked != is_locked {
-                        if is_locked {
-                            if let Err(e) = window
-                                .set_cursor_grab(CursorGrabMode::Confined)
-                                .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked))
-                            {
-                                log_once::warn_once!("Failed to grab cursor: {:?}", e);
-                            } else {
-                                log_once::info_once!("Grabbed cursor");
-                                cfg.is_locked = true;
-                            }
-                        } else if let Err(e) = window.set_cursor_grab(CursorGrabMode::None) {
-                            log_once::warn_once!("Failed to release cursor: {:?}", e);
-                        } else {
-                            log_once::info_once!("Released cursor");
-                            cfg.is_locked = false;
-                        }
-                    }
-                }
-                WindowCommand::HideCursor(should_hide) => {
-                    let cfg = get_config().write();
-                    if cfg.is_hidden != should_hide {
-                        if should_hide {
-                            window.set_cursor_visible(false);
-                        } else {
-                            window.set_cursor_visible(true);
-                        }
-                    }
-                }
-            },
-        }
-    }
-}
+/// Command buffer that is used for oneway communication between Kotlin to Rust.  
+pub trait CommandBufferPoller {
+    fn poll(&mut self, graphics: &RenderContext);
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/build.rs b/eucalyptus-editor/src/build.rs
index 44a5a1e..df5f4a9 100644
--- a/eucalyptus-editor/src/build.rs
+++ b/eucalyptus-editor/src/build.rs
@@ -326,6 +326,7 @@ pub async fn package(
                 .file_name()
                 .ok_or_else(|| anyhow::anyhow!("Import library missing filename: {}", import_lib.display()))?
                 .to_owned();
+            if !format!("{}", file_name.display()).contains("eucalyptus_core") { continue; }
             let libs_target = libs_dir.join(&file_name);
             tokio_fs::copy(&import_lib, &libs_target).await?;
             log::info!(
diff --git a/eucalyptus-editor/src/command.rs b/eucalyptus-editor/src/command.rs
new file mode 100644
index 0000000..14e4cfe
--- /dev/null
+++ b/eucalyptus-editor/src/command.rs
@@ -0,0 +1,52 @@
+use dropbear_engine::graphics::RenderContext;
+use eucalyptus_core::window::{CommandBufferPoller, GRAPHICS_COMMAND, CommandBuffer, WindowCommand, get_config};
+use winit::window::CursorGrabMode;
+
+use crate::editor::Editor;
+
+impl CommandBufferPoller for Editor {
+    fn poll(&mut self, graphics: &RenderContext) {
+        while let Ok(cmd) = GRAPHICS_COMMAND.1.try_recv() {
+            log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd);
+            match cmd {
+                CommandBuffer::WindowCommand(w_cmd) => match w_cmd {
+                    WindowCommand::WindowGrab(is_locked) => {
+                        let mut cfg = get_config().write();
+                        if cfg.is_locked != is_locked {
+                            if is_locked {
+                                if let Err(e) = graphics.shared.window
+                                    .set_cursor_grab(CursorGrabMode::Confined)
+                                    .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked))
+                                {
+                                    log_once::warn_once!("Failed to grab cursor: {:?}", e);
+                                } else {
+                                    log_once::info_once!("Grabbed cursor");
+                                    cfg.is_locked = true;
+                                }
+                            } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) {
+                                log_once::warn_once!("Failed to release cursor: {:?}", e);
+                            } else {
+                                log_once::info_once!("Released cursor");
+                                cfg.is_locked = false;
+                            }
+                        }
+                    }
+                    WindowCommand::HideCursor(should_hide) => {
+                        let cfg = get_config().write();
+                        if cfg.is_hidden != should_hide {
+                            if should_hide {
+                                graphics.shared.window.set_cursor_visible(false);
+                            } else {
+                                graphics.shared.window.set_cursor_visible(true);
+                            }
+                        }
+                    }
+                },
+                CommandBuffer::Quit => {
+                    log::info!("Quit command received in editor, stopping play mode");
+                    self.signal = crate::editor::Signal::StopPlaying;
+                },
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/editor/input.rs b/eucalyptus-editor/src/editor/input.rs
index ed61a6f..8c09b66 100644
--- a/eucalyptus-editor/src/editor/input.rs
+++ b/eucalyptus-editor/src/editor/input.rs
@@ -91,19 +91,13 @@ impl Keyboard for Editor {
                 }
             }
             KeyCode::Escape => {
-                if is_double_press {
+                if is_playing {
+                } else if is_double_press {
                     if self.selected_entity.is_some() {
                         self.selected_entity = None;
                         log::debug!("Deselected entity");
                     }
-                } else if matches!(self.editor_state, EditorState::Playing) && shift_pressed {
-                    self.viewport_mode = ViewportMode::None;
-                    info!("Switched to Viewport::None");
-                    if let Some(window) = &self.window {
-                        window.set_cursor_visible(true);
-                        let _ = window.set_cursor_grab(CursorGrabMode::None);
-                    }
-                } else if self.is_viewport_focused && !is_playing {
+                } else if self.is_viewport_focused {
                     self.viewport_mode = ViewportMode::None;
                     info!("Switched to Viewport::None");
                     if let Some(window) = &self.window {
@@ -252,6 +246,14 @@ impl Keyboard for Editor {
                     self.input_state.pressed_keys.insert(key);
                 }
             }
+            KeyCode::F12 => {
+                if is_playing {
+                    self.signal = Signal::StopPlaying;
+                    info!("Stopping play mode");
+                } else {
+                    self.input_state.pressed_keys.insert(key);
+                }
+            }
             KeyCode::KeyL => {
                 if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing {
                     info!("GizmoOrientation set to Local");
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 5f74c1c..567c756 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -13,7 +13,7 @@ use dropbear_engine::{
 use eucalyptus_core::hierarchy::EntityTransformExt;
 use eucalyptus_core::logging;
 use eucalyptus_core::states::{Label, WorldLoadingStatus};
-use eucalyptus_core::window::poll;
+use eucalyptus_core::window::{CommandBufferPoller};
 use log;
 use parking_lot::Mutex;
 use tokio::sync::mpsc::unbounded_channel;
@@ -149,7 +149,7 @@ impl Scene for Editor {
             graphics.shared.window.set_title(&title);
         }
 
-        poll(graphics.shared.window.clone());
+        self.poll(graphics);
 
         {
             // basic futurequeue spawn queue management.
diff --git a/eucalyptus-editor/src/lib.rs b/eucalyptus-editor/src/lib.rs
index bdcf5d6..4c14130 100644
--- a/eucalyptus-editor/src/lib.rs
+++ b/eucalyptus-editor/src/lib.rs
@@ -9,3 +9,4 @@ pub mod signal;
 pub mod spawn;
 pub mod stats;
 pub mod utils;
+pub mod command;
diff --git a/headers/dropbear.h b/headers/dropbear.h
index 9741726..099df54 100644
--- a/headers/dropbear.h
+++ b/headers/dropbear.h
@@ -158,6 +158,9 @@ DROPBEAR_NATIVE dropbear_get_last_mouse_pos(const InputState* input_ptr, float* 
 DROPBEAR_NATIVE dropbear_is_cursor_hidden(const InputState* input_ptr, BOOL* out_hidden);
 DROPBEAR_NATIVE dropbear_set_cursor_hidden(InputState* input_ptr, GraphicsCommandQueue* graphics_ptr, BOOL hidden);
 
+// editor
+void dropbear_quit(const GraphicsCommandQueue* command_ptr);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif // __cplusplus
diff --git a/redback-runtime/Cargo.toml b/redback-runtime/Cargo.toml
index c0515e0..3129ca6 100644
--- a/redback-runtime/Cargo.toml
+++ b/redback-runtime/Cargo.toml
@@ -8,7 +8,7 @@ readme = "README.md"
 
 [dependencies]
 dropbear-engine = { path = "../dropbear-engine" }
-eucalyptus-core = { path = "../eucalyptus-core", features = ["default"] }
+eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false }
 
 anyhow.workspace = true
 log.workspace = true
@@ -25,6 +25,7 @@ bytemuck.workspace = true
 log-once.workspace = true
 egui-wgpu.workspace = true
 rustc_version_runtime.workspace = true
+winit.workspace = true
 
 [features]
 # enable this to allow for JVM support
diff --git a/redback-runtime/src/command.rs b/redback-runtime/src/command.rs
new file mode 100644
index 0000000..a29d202
--- /dev/null
+++ b/redback-runtime/src/command.rs
@@ -0,0 +1,51 @@
+use dropbear_engine::graphics::RenderContext;
+use eucalyptus_core::window::{CommandBufferPoller, GRAPHICS_COMMAND, CommandBuffer, WindowCommand, get_config};
+use winit::window::CursorGrabMode;
+
+use crate::scene::RuntimeScene;
+
+impl CommandBufferPoller for RuntimeScene {
+    fn poll(&mut self, graphics: &RenderContext) {
+        while let Ok(cmd) = GRAPHICS_COMMAND.1.try_recv() {
+            log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd);
+            match cmd {
+                CommandBuffer::WindowCommand(w_cmd) => match w_cmd {
+                    WindowCommand::WindowGrab(is_locked) => {
+                        let mut cfg = get_config().write();
+                        if cfg.is_locked != is_locked {
+                            if is_locked {
+                                if let Err(e) = graphics.shared.window
+                                    .set_cursor_grab(CursorGrabMode::Confined)
+                                    .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked))
+                                {
+                                    log_once::warn_once!("Failed to grab cursor: {:?}", e);
+                                } else {
+                                    log_once::info_once!("Grabbed cursor");
+                                    cfg.is_locked = true;
+                                }
+                            } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) {
+                                log_once::warn_once!("Failed to release cursor: {:?}", e);
+                            } else {
+                                log_once::info_once!("Released cursor");
+                                cfg.is_locked = false;
+                            }
+                        }
+                    }
+                    WindowCommand::HideCursor(should_hide) => {
+                        let cfg = get_config().write();
+                        if cfg.is_hidden != should_hide {
+                            if should_hide {
+                                graphics.shared.window.set_cursor_visible(false);
+                            } else {
+                                graphics.shared.window.set_cursor_visible(true);
+                            }
+                        }
+                    }
+                },
+                CommandBuffer::Quit => {
+                    self.scene_command = dropbear_engine::scene::SceneCommand::Quit;
+                },
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/redback-runtime/src/main.rs b/redback-runtime/src/main.rs
index 0f73b7f..1b7ada2 100644
--- a/redback-runtime/src/main.rs
+++ b/redback-runtime/src/main.rs
@@ -2,6 +2,7 @@ mod input;
 mod scene;
 mod debug;
 mod utils;
+mod command;
 
 use crate::scene::RuntimeScene;
 use app_dirs2::AppInfo;
diff --git a/redback-runtime/src/scene.rs b/redback-runtime/src/scene.rs
index 3a818f2..1545710 100644
--- a/redback-runtime/src/scene.rs
+++ b/redback-runtime/src/scene.rs
@@ -16,7 +16,7 @@ use dropbear_engine::wgpu::{self, Color, RenderPipeline};
 use dropbear_engine::winit::event_loop::ActiveEventLoop;
 use dropbear_engine::winit::window::Window;
 use eucalyptus_core::camera::CameraComponent;
-use eucalyptus_core::egui::{self, CentralPanel, UiBuilder};
+use eucalyptus_core::egui::{self, CentralPanel, Frame, UiBuilder};
 use eucalyptus_core::hierarchy::EntityTransformExt;
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::ptr::{GraphicsPtr, InputStatePtr, WorldPtr};
@@ -25,7 +25,7 @@ use eucalyptus_core::scene::SceneConfig;
 use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
 use eucalyptus_core::states::{Camera3D, ConfigFile, Light as LightConfig, ModelProperties, Script, SerializedMeshRenderer};
 use eucalyptus_core::traits::registry::ComponentRegistry;
-use eucalyptus_core::window::GRAPHICS_COMMAND;
+use eucalyptus_core::window::{CommandBufferPoller, GRAPHICS_COMMAND};
 use hecs::{Entity, World};
 use parking_lot::Mutex;
 use tokio::sync::oneshot;
@@ -48,7 +48,7 @@ pub(crate) struct RuntimeScene {
     script_manager: ScriptManager,
     script_target: Option<ScriptTarget>,
     scripts_ready: bool,
-    scene_command: SceneCommand,
+    pub scene_command: SceneCommand,
 
     current_scene: Option<String>,
     world_receiver: Option<oneshot::Receiver<World>>,
@@ -434,9 +434,9 @@ impl Scene for RuntimeScene {
     fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
         graphics.shared.future_queue.poll();
 
-        self.poll_scene_loading(graphics);
-
-        CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| {
+        self.poll_scene_loading(graphics);        
+        self.poll(graphics);
+        CentralPanel::default().frame(Frame::new()).show(&graphics.shared.get_egui_context(), |ui| {
             if self.render_pipeline.is_none() {
                 ui.label("Loading scene...");
             }
@@ -447,6 +447,14 @@ impl Scene for RuntimeScene {
 
             self.update_world_state(graphics);
 
+            let egui_ctx = graphics.shared.get_egui_context();
+            let egui_wants_input = egui_ctx.wants_pointer_input() || egui_ctx.wants_keyboard_input();
+            
+            if egui_wants_input {
+                self.input_state.pressed_keys.clear();
+                self.input_state.mouse_button.clear();
+            }
+
             if self.scripts_ready {
                 let world_ptr = self.world.as_mut() as WorldPtr;
                 if let Err(err) = unsafe { self.script_manager.update_script(world_ptr, &self.input_state, dt) } {
@@ -457,28 +465,34 @@ impl Scene for RuntimeScene {
             let texture_id = *graphics.shared.texture_id;
             let available_size = ui.available_rect_before_wrap().size();
             
+            let is_fullscreen = self.window_config.window_configuration.windowed_mode.is_fullscreen();
+            
             let viewport_aspect = self.viewport_resolution.0 as f32 / self.viewport_resolution.1 as f32;
+            let available_aspect = available_size.x / available_size.y;
             
             let active_camera: Option<Entity> = *self.active_camera.lock();
             if let Some(cam_ent) = active_camera {
                 if let Ok(mut q) = self.world.query_one::<&mut Camera>(cam_ent) {
                     if let Some(camera) = q.get() {
-                        camera.aspect = viewport_aspect as f64;
+                        if is_fullscreen {
+                            camera.aspect = viewport_aspect as f64;
+                        } else {
+                            camera.aspect = available_aspect as f64;
+                        }
                         camera.update_view_proj();
                         camera.update(graphics.shared.clone());
                     }
                 }
             }
             
-            let available_aspect = available_size.x / available_size.y;
-            let (display_width, display_height) = if available_aspect > viewport_aspect {
-                let height = available_size.y;
-                let width = height * viewport_aspect;
-                (width, height)
+            let (display_width, display_height) = if is_fullscreen {
+                if available_aspect > viewport_aspect {
+                    (available_size.x, available_size.x / viewport_aspect)
+                } else {
+                    (available_size.y * viewport_aspect, available_size.y)
+                }
             } else {
-                let width = available_size.x;
-                let height = width / viewport_aspect;
-                (width, height)
+                (available_size.x, available_size.y)
             };
             
             let rect = ui.available_rect_before_wrap();
@@ -497,6 +511,7 @@ impl Scene for RuntimeScene {
             });
 
             self.input_state.window = self.window.clone();
+            self.input_state.mouse_delta = None;
         });
     }
 
diff --git a/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index 23ac4bc..bb2d830 100644
--- a/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/src/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -82,4 +82,17 @@ class DropbearEngine(val native: NativeEngine) {
      * This can be run in your update loop without consequences.
      */
     fun callExceptionOnError(toggle: Boolean) = DropbearEngine.callExceptionOnError(toggle)
+
+    /**
+     * Quits the currently running app or game.
+     * 
+     * This function can have different behaviours depending on where it is ran. 
+     * - eucalyptus-editor - When called, this exits your Play Mode session and returns you back to
+     *                       `EditorState::Editing`
+     * - redback-runtime - When called, this will exit your current process and kill the app as is. It will
+     *                     also drop any pointers and do any additional cleanup.
+     */
+    fun quit() {
+        native.quit()
+    }
 }
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt b/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
index 5d680bd..63ffc9c 100644
--- a/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
+++ b/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
@@ -84,4 +84,6 @@ expect class NativeEngine {
 //    fun getConnectedGamepads(): List<Gamepad>
 
     // -------------------------------------------------------------------
+
+    fun quit()
 }
diff --git a/src/jvmMain/java/com/dropbear/ffi/JNINative.java b/src/jvmMain/java/com/dropbear/ffi/JNINative.java
index 0b74c7d..a17c878 100644
--- a/src/jvmMain/java/com/dropbear/ffi/JNINative.java
+++ b/src/jvmMain/java/com/dropbear/ffi/JNINative.java
@@ -76,4 +76,7 @@ public class JNINative {
     public static native boolean isCursorHidden(long inputHandle);
     public static native void setCursorHidden(long inputHandle, long graphicsHandle, boolean hidden);
     public static native String[] getAllTextures(long worldHandle, long entityHandle);
+
+    // engine
+    public static native void quit(long graphicsHandle);
 }
diff --git a/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt b/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
index 11428a7..5dc8979 100644
--- a/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
+++ b/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
@@ -383,4 +383,8 @@ actual class NativeEngine {
             EntityRef(EntityId(result))
         }
     }
+
+    actual fun quit() {
+        JNINative.quit(graphicsHandle)
+    }
 }
\ 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 7c1c653..fea9282 100644
--- a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
+++ b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
@@ -1172,4 +1172,9 @@ actual class NativeEngine {
             return if (result == 0) EntityRef(EntityId(outParent.value)) else if (exceptionOnError) throw DropbearNativeException("getParent failed with code: $result") else null
         }
     }
+
+    actual fun quit() {
+        val command = graphicsHandle ?: if (exceptionOnError) throw DropbearNativeException("Unable to quit: graphicsHandle does not exist") else return
+        dropbear_quit(command.reinterpret())
+    }
 }
\ No newline at end of file