kitgit

tirbofish/dropbear · commit

827a107b136f26d5960399f97b59e90bdbb5fa5a

wip: got most of the runtime working, camera doesnt seem to update to my positions at all for some reason :(

Unverified

Thribhu K <4tkbytes@pm.me> · 2025-12-23 16:18

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index a6f5375..f99ffd6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,7 +15,7 @@ members = [
     "eucalyptus-core",
     "eucalyptus-editor",
     "magna-carta",
-    "redback-runtime"
+#    "redback-runtime"
 ]
 
 [workspace.dependencies]
diff --git a/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index 21a10e3..e31a9d3 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -30,12 +30,10 @@ use futures::executor::block_on;
 use gilrs::{Gilrs, GilrsBuilder};
 use log::LevelFilter;
 use parking_lot::{Mutex, RwLock};
-use serde::{Serialize};
 use spin_sleep::SpinSleeper;
 use std::fs::OpenOptions;
-use std::io::Write;
 use std::sync::OnceLock;
-use std::{fs, sync::Arc, time::{Duration, Instant, SystemTime, UNIX_EPOCH}};
+use std::{fs, sync::Arc, time::{Duration, Instant}};
 use std::collections::HashMap;
 use std::rc::Rc;
 use wgpu::{
@@ -629,6 +627,7 @@ impl DropbearWindowBuilder {
 
 /// A struct storing the information about the application/game that is using the engine.
 pub struct App {
+    #[allow(dead_code)]
     app_data: AppInfo,
     /// The input manager, manages any inputs and their actions
     input_manager: input::Manager,
@@ -867,7 +866,7 @@ impl ApplicationHandler for App {
                         scene::SceneCommand::CloseWindow(target_window_id) => {
                             log::info!("Scene requested closing window: {:?}", target_window_id);
                             if Some(target_window_id) == self.root_window_id {
-                                log::warn!("Cannot close root window via CloseWindow command, use Quit instead");
+                                event_loop.exit();
                             } else {
                                 self.windows.remove(&target_window_id);
                             }
diff --git a/dropbear_future-queue/src/lib.rs b/dropbear_future-queue/src/lib.rs
index fb345e1..4a334c5 100644
--- a/dropbear_future-queue/src/lib.rs
+++ b/dropbear_future-queue/src/lib.rs
@@ -40,7 +40,6 @@ use std::any::Any;
 use std::cell::RefCell;
 use std::collections::VecDeque;
 use std::future::Future;
-use std::marker::PhantomData;
 use std::pin::Pin;
 use std::rc::Rc;
 use std::sync::Arc;
diff --git a/eucalyptus-core/src/config.rs b/eucalyptus-core/src/config.rs
index d1330d1..0e92d11 100644
--- a/eucalyptus-core/src/config.rs
+++ b/eucalyptus-core/src/config.rs
@@ -100,7 +100,7 @@ impl ProjectConfig {
         log::debug!("Updating with new content");
         config.load_config_to_memory()?;
         config.write_to_all()?;
-        log::debug!("Successfully updated!");
+        log::debug!("Project config successfully updated!");
         Ok(config)
     }
 
@@ -243,7 +243,7 @@ impl ProjectConfig {
             if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("eucs") {
                 match SceneConfig::read_from(&path) {
                     Ok(scene) => {
-                        log::debug!("Loaded scene: {}", scene.scene_name);
+                        log::debug!("Loaded scene config: {}", scene.scene_name);
                         scene_configs.push(scene);
                     }
                     Err(e) => {
diff --git a/eucalyptus-core/src/scene.rs b/eucalyptus-core/src/scene.rs
index cf170e9..3f8bb6e 100644
--- a/eucalyptus-core/src/scene.rs
+++ b/eucalyptus-core/src/scene.rs
@@ -24,7 +24,6 @@ use std::fs;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use crossbeam_channel::Sender;
-use tokio::sync::mpsc::UnboundedSender;
 
 #[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct SceneEntity {
@@ -608,6 +607,7 @@ impl SceneConfig {
                     .iter()
                     .find_map(|(entity, (_, component))| {
                         if component.starting_camera {
+                            log::debug!("Found starting camera: {:?}", entity);
                             Some(entity)
                         } else {
                             None
@@ -627,6 +627,7 @@ impl SceneConfig {
                         .iter()
                         .find_map(|(entity, (_, component))| {
                             if matches!(component.camera_type, CameraType::Debug) {
+                                log::debug!("Found debug camera: {:?}", entity);
                                 Some(entity)
                             } else {
                                 None
@@ -646,6 +647,7 @@ impl SceneConfig {
                             .iter()
                             .filter_map(|(entity, label)| {
                                 if label.as_str() == "Viewport Camera" {
+                                    log::debug!("Found 'Viewport Camera' entity: {:?}", entity);
                                     Some(entity)
                                 } else {
                                     None
diff --git a/eucalyptus-core/src/scripting.rs b/eucalyptus-core/src/scripting.rs
index babadca..48d9607 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -10,7 +10,6 @@ pub mod utils;
 pub static JVM_ARGS: OnceLock<String> = OnceLock::new();
 
 use std::sync::OnceLock;
-use crate::input::InputState;
 use crate::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr};
 use crate::scripting::jni::JavaContext;
 use crate::scripting::native::NativeLibrary;
@@ -26,7 +25,7 @@ use tokio::process::Command;
 use magna_carta::Target;
 
 /// The target of the script. This can be either a JVM or a native library.
-#[derive(Default, Clone)]
+#[derive(Default, Clone, Debug)]
 pub enum ScriptTarget {
     #[default]
     /// The default target. Using this will always return an error.
diff --git a/eucalyptus-core/src/scripting/jni.rs b/eucalyptus-core/src/scripting/jni.rs
index bdb127a..284582e 100644
--- a/eucalyptus-core/src/scripting/jni.rs
+++ b/eucalyptus-core/src/scripting/jni.rs
@@ -13,11 +13,19 @@ use jni::sys::jlong;
 use jni::{InitArgsBuilder, JNIVersion, JavaVM};
 use sha2::{Digest, Sha256};
 use std::fs;
+use std::net::TcpListener;
 use std::path::PathBuf;
 use crate::scripting::JVM_ARGS;
 
 const LIBRARY_PATH: &[u8] = include_bytes!("../../../build/libs/dropbear-1.0-SNAPSHOT-all.jar");
 
+fn is_port_available(port: u16) -> bool {
+    match TcpListener::bind(("127.0.0.1", port)) {
+        Ok(_) => true,
+        Err(_) => false,
+    }
+}
+
 /// Provides a context for any eucalyptus-core JNI calls and JVM hosting.
 pub struct JavaContext {
     pub(crate) jvm: JavaVM,
@@ -116,6 +124,8 @@ impl JavaContext {
         
         let mut args_log = Vec::new();
 
+
+
         if let Some(args) = external_vm_args {
             args_log.push(args.clone());
             jvm_args = jvm_args.option(args);
@@ -126,7 +136,12 @@ impl JavaContext {
 
             #[cfg(feature = "jvm_debug")]
             {
-                let debug_arg = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:6741";
+                let port = 6741;
+                let debug_arg = if is_port_available(port) {
+                    "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:6741"
+                } else {
+                    "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:6742"
+                };
                 args_log.push(debug_arg.to_string());
                 jvm_args = jvm_args.option(debug_arg);
             }
@@ -140,7 +155,7 @@ impl JavaContext {
                     .parent()
                     .ok_or_else(|| anyhow::anyhow!("Unable to locate parent"))?;
 
-                println!("Libs folder at {}", path.display());
+                log::debug!("Libs folder at {}", path.display());
                 if !path.exists() {
                     log::warn!(
                     "Libs folder ({}) does not exist; native libraries may fail to load",
diff --git a/eucalyptus-core/src/scripting/native/exports/dropbear_input.rs b/eucalyptus-core/src/scripting/native/exports/dropbear_input.rs
index 918ed01..f2d262c 100644
--- a/eucalyptus-core/src/scripting/native/exports/dropbear_input.rs
+++ b/eucalyptus-core/src/scripting/native/exports/dropbear_input.rs
@@ -89,13 +89,22 @@ pub unsafe extern "C" fn dropbear_is_mouse_button_pressed(
 
     let input = unsafe { &*input_state_ptr };
 
-    match keycode_from_ordinal(button_code) {
+    let button = match button_code {
+        0 => Some(winit::event::MouseButton::Left),
+        1 => Some(winit::event::MouseButton::Right),
+        2 => Some(winit::event::MouseButton::Middle),
+        3 => Some(winit::event::MouseButton::Back),
+        4 => Some(winit::event::MouseButton::Forward),
+        _ => None,
+    };
+
+    match button {
         None => {
             eprintln!("[dropbear_is_mouse_button_pressed] [WARN] Invalid button code");
             unsafe { *out_pressed = 0 };
         }
-        Some(key) => {
-            if input.pressed_keys.contains(&key) {
+        Some(btn) => {
+            if input.mouse_button.contains(&btn) {
                 unsafe { *out_pressed = 1 };
             } else {
                 unsafe { *out_pressed = 0 };
@@ -119,7 +128,7 @@ pub unsafe extern "C" fn dropbear_get_mouse_delta(
 
     let input = unsafe { &mut *(input_state_ptr as InputStatePtr) };
 
-    if let Some(pos) = input.mouse_delta.take() {
+    if let Some(pos) = input.mouse_delta {
         unsafe {
             *out_delta_x = pos.0 as f32;
             *out_delta_y = pos.1 as f32;
diff --git a/eucalyptus-editor/src/command.rs b/eucalyptus-editor/src/command.rs
deleted file mode 100644
index 58d2c77..0000000
--- a/eucalyptus-editor/src/command.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use dropbear_engine::graphics::RenderContext;
-use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand};
-use winit::window::CursorGrabMode;
-
-use crate::editor::Editor;
-
-impl CommandBufferPoller for Editor {
-    fn poll(&mut self, graphics: &RenderContext) {
-        while let Ok(cmd) = COMMAND_BUFFER.1.try_recv() {
-            log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd);
-            match cmd {
-                CommandBuffer::WindowCommand(w_cmd) => match w_cmd {
-                    WindowCommand::WindowGrab(lock) => {
-                        if lock {
-                            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");
-                            }
-                        } 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");
-                        }
-                    }
-                    WindowCommand::HideCursor(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;
-                },
-                CommandBuffer::SwitchScene(scene_name) => {
-                    if matches!(self.editor_state, crate::editor::EditorState::Playing) {
-                        log::info!(
-                            "SwitchScene requested during play mode; switching play-world scene to '{}'",
-                            scene_name
-                        );
-                        self.pending_play_scene_load = Some(scene_name);
-                    } else if let Err(err) = self.queue_scene_load_by_name(&scene_name) {
-                        log::error!(
-                            "Failed to queue scene load for '{}': {}",
-                            scene_name,
-                            err
-                        );
-                    }
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/editor/dock.rs b/eucalyptus-editor/src/editor/dock.rs
index 88a1586..225ca99 100644
--- a/eucalyptus-editor/src/editor/dock.rs
+++ b/eucalyptus-editor/src/editor/dock.rs
@@ -644,57 +644,31 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                 let local_scene_settings = cfg.root_node_selected;
                 
                 if let Some(entity) = self.selected_entity {
-                    let is_playing = matches!(*self.editor_mode, EditorState::Playing);
-                    
-                    let author_entity = *entity;
-                    let selected_label = self
-                        .world
-                        .get::<&Label>(author_entity)
-                        .ok()
-                        .map(|l| l.as_str().to_string());
-
-                    let mut inspect_play_world = false;
-                    let mut play_entity: Option<Entity> = None;
-
-                    if is_playing && !self.editor.is_null() {
-                        let editor = unsafe { &mut *self.editor };
-                        if let (Some(label), Some(play_world)) =
-                            (selected_label.as_deref(), editor.play_world.as_deref())
-                        {
-                            play_entity = play_world
-                                .query::<&Label>()
-                                .iter()
-                                .find_map(|(e, l)| (l.as_str() == label).then_some(e));
-                            inspect_play_world = play_entity.is_some();
-                        }
-                    }
-
-                    if inspect_play_world {
-                        ui.label("Play Mode: showing live values (read-only)");
-                        ui.separator();
-                    }
-
                     let mut local_set_initial_camera = false;
-                    let mut inspect_entity = author_entity;
-                    let world_ptr: *mut World = if inspect_play_world {
-                        let editor = unsafe { &mut *self.editor };
-                        let play_world = editor
-                            .play_world
-                            .as_deref_mut()
-                            .expect("inspect_play_world implies play_world exists");
-                        inspect_entity = play_entity.expect("inspect_play_world implies play_entity exists");
-                        play_world as *mut World
-                    } else {
-                        self.world as *mut World
-                    };
+                    let mut inspect_entity = *entity;
+                    let world = &self.world;
+
+                    if !cfg.root_node_selected {
+                        if let Ok(mut q) = world.query_one::<(&mut Label,)>(inspect_entity) {
+                            if let Some((label, )) = q.get() {
+                                label.inspect(
+                                    &mut inspect_entity,
+                                    &mut cfg,
+                                    ui,
+                                    self.undo_stack,
+                                    self.signal,
+                                    &mut String::new(),
+                                );
 
-                    let world = unsafe { &mut *world_ptr };
+                                ui.label(format!("Entity ID: {}", inspect_entity.id()));
 
-                    ui.add_enabled_ui(!inspect_play_world, |ui| {
-                        if !cfg.root_node_selected {
-                            if let Ok(mut q) = world.query_one::<(&mut Label,)>(inspect_entity) {
-                                if let Some((label, )) = q.get() {
-                                    label.inspect(
+                                ui.separator();
+
+                                if let Ok(mut q) = world.query_one::<&mut MeshRenderer>(inspect_entity)
+                                    && let Some(e) = q.get()
+                                {
+                                    // entity
+                                    e.inspect(
                                         &mut inspect_entity,
                                         &mut cfg,
                                         ui,
@@ -702,16 +676,46 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                         self.signal,
                                         &mut String::new(),
                                     );
+                                }
 
-                                    ui.label(format!("Entity ID: {}", inspect_entity.id()));
+                                if let Ok(mut q) = world.query_one::<&mut EntityTransform>(inspect_entity)
+                                    && let Some(t) = q.get()
+                                {
+                                    CollapsingHeader::new("Transform").default_open(true).show(ui, |ui| {
+                                        t.inspect(
+                                            &mut inspect_entity,
+                                            &mut cfg,
+                                            ui,
+                                            self.undo_stack,
+                                            self.signal,
+                                            &mut String::new(),
+                                        );
+                                    });
+                                    ui.separator();
+                                }
 
+                                if let Ok(mut q) = world.query_one::<&mut CustomProperties>(inspect_entity)
+                                    && let Some(props) = q.get()
+                                {
+                                    CollapsingHeader::new("Custom Properties").default_open(true).show(ui, |ui| {
+                                        props.inspect(
+                                            &mut inspect_entity,
+                                            &mut cfg,
+                                            ui,
+                                            self.undo_stack,
+                                            self.signal,
+                                            label.as_mut_string(),
+                                        );
+                                    });
                                     ui.separator();
+                                }
 
-                                    if let Ok(mut q) = world.query_one::<&mut MeshRenderer>(inspect_entity)
-                                        && let Some(e) = q.get()
-                                    {
-                                        // entity
-                                        e.inspect(
+                                if let Ok(mut q) = world
+                                    .query_one::<(&mut Camera, &mut CameraComponent)>(inspect_entity)
+                                    && let Some((camera, camera_component)) = q.get()
+                                {
+                                    CollapsingHeader::new("Camera").default_open(true).show(ui, |ui| {
+                                        camera.inspect(
                                             &mut inspect_entity,
                                             &mut cfg,
                                             ui,
@@ -719,161 +723,113 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                             self.signal,
                                             &mut String::new(),
                                         );
-                                    }
 
-                                    if let Ok(mut q) = world.query_one::<&mut EntityTransform>(inspect_entity)
-                                        && let Some(t) = q.get()
-                                    {
-                                        CollapsingHeader::new("Transform").default_open(true).show(ui, |ui| {
-                                            t.inspect(
-                                                &mut inspect_entity,
-                                                &mut cfg,
-                                                ui,
-                                                self.undo_stack,
-                                                self.signal,
-                                                &mut String::new(),
-                                            );
-                                        });
                                         ui.separator();
-                                    }
 
-                                    if let Ok(mut q) = world.query_one::<&mut CustomProperties>(inspect_entity)
-                                        && let Some(props) = q.get()
-                                    {
-                                        CollapsingHeader::new("Custom Properties").default_open(true).show(ui, |ui| {
-                                            props.inspect(
-                                                &mut inspect_entity,
-                                                &mut cfg,
-                                                ui,
-                                                self.undo_stack,
-                                                self.signal,
-                                                label.as_mut_string(),
-                                            );
-                                        });
-                                        ui.separator();
-                                    }
-
-                                    if let Ok(mut q) = world
-                                        .query_one::<(&mut Camera, &mut CameraComponent)>(inspect_entity)
-                                        && let Some((camera, camera_component)) = q.get()
-                                    {
-                                        CollapsingHeader::new("Camera").default_open(true).show(ui, |ui| {
-                                            camera.inspect(
-                                                &mut inspect_entity,
-                                                &mut cfg,
-                                                ui,
-                                                self.undo_stack,
-                                                self.signal,
-                                                &mut String::new(),
-                                            );
+                                        camera_component.inspect(
+                                            &mut inspect_entity,
+                                            &mut cfg,
+                                            ui,
+                                            self.undo_stack,
+                                            self.signal,
+                                            &mut camera.label.clone(),
+                                        );
 
-                                            ui.separator();
+                                        ui.separator();
 
-                                            camera_component.inspect(
-                                                &mut inspect_entity,
-                                                &mut cfg,
-                                                ui,
-                                                self.undo_stack,
-                                                self.signal,
-                                                &mut camera.label.clone(),
-                                            );
+                                        // camera controller
+                                        let mut active_camera = self.active_camera.lock();
 
-                                            ui.separator();
+                                        if active_camera.equivalent(&Some(*entity)) {
+                                            ui.label("Status: Currently viewing through camera");
+                                        } else {
+                                            ui.label("Status: Not viewing through this camera");
+                                        }
 
-                                            // camera controller
-                                            let mut active_camera = self.active_camera.lock();
+                                        if ui.button("Set as active camera").clicked() {
+                                            *active_camera = Some(*entity);
+                                            log::info!(
+                                            "Currently viewing from camera angle '{}'",
+                                            camera.label
+                                        );
+                                        }
 
-                                            if active_camera.equivalent(&Some(*entity)) {
-                                                ui.label("Status: Currently viewing through camera");
-                                            } else {
-                                                ui.label("Status: Not viewing through this camera");
+                                        if camera_component.starting_camera {
+                                            if ui.button("Stop making camera initial").clicked() {
+                                                log::debug!("'Stop making camera initial' button clicked");
+                                                camera_component.starting_camera = false;
+                                                success!("Removed {} from starting camera", camera.label);
                                             }
-
-                                            if ui.button("Set as active camera").clicked() {
-                                                *active_camera = Some(*entity);
-                                                log::info!(
-                                                "Currently viewing from camera angle '{}'",
-                                                camera.label
+                                        } else if ui.button("Set as initial camera").clicked() {
+                                            log::debug!("'Set as initial camera' button clicked");
+                                            if matches!(camera_component.camera_type, CameraType::Debug) {
+                                                warn!(
+                                                "Cannot set any cameras of type 'Debug' to initial camera"
                                             );
+                                            } else {
+                                                local_set_initial_camera = true
                                             }
+                                        }
+                                    });
+                                    ui.separator();
+                                }
 
-                                            if camera_component.starting_camera {
-                                                if ui.button("Stop making camera initial").clicked() {
-                                                    log::debug!("'Stop making camera initial' button clicked");
-                                                    camera_component.starting_camera = false;
-                                                    success!("Removed {} from starting camera", camera.label);
-                                                }
-                                            } else if ui.button("Set as initial camera").clicked() {
-                                                log::debug!("'Set as initial camera' button clicked");
-                                                if matches!(camera_component.camera_type, CameraType::Debug) {
-                                                    warn!(
-                                                    "Cannot set any cameras of type 'Debug' to initial camera"
-                                                );
-                                                } else {
-                                                    local_set_initial_camera = true
-                                                }
-                                            }
-                                        });
-                                        ui.separator();
-                                    }
+                                if let Ok(mut q) = world.query_one::<(&mut Light, &mut LightComponent, &mut Transform)>(inspect_entity)
+                                    && let Some((light, comp, transform)) = q.get()
+                                {
+                                    light.inspect(
+                                        entity,
+                                        &mut cfg,
+                                        ui,
+                                        self.undo_stack,
+                                        self.signal,
+                                        &mut String::new(),
+                                    );
 
-                                    if let Ok(mut q) = world.query_one::<(&mut Light, &mut LightComponent, &mut Transform)>(inspect_entity)
-                                        && let Some((light, comp, transform)) = q.get()
-                                    {
-                                        light.inspect(
+                                    *transform = light.transform;
+                                    *comp = light.light_component.clone();
+                                    ui.separator();
+                                }
+
+                                if let Ok(mut q) = self.world.query_one::<&mut Script>(*entity)
+                                    && let Some(script) = q.get()
+                                {
+                                    CollapsingHeader::new("Script").default_open(true).show(ui, |ui| {
+                                        script.inspect(
                                             entity,
                                             &mut cfg,
                                             ui,
                                             self.undo_stack,
                                             self.signal,
-                                            &mut String::new(),
+                                            label.as_mut_string(),
                                         );
+                                    });
+                                    ui.separator();
+                                }
 
-                                        *transform = light.transform;
-                                        *comp = light.light_component.clone();
-                                        ui.separator();
-                                    }
-
-                                    if let Ok(mut q) = self.world.query_one::<&mut Script>(*entity)
-                                        && let Some(script) = q.get()
-                                    {
-                                        CollapsingHeader::new("Script").default_open(true).show(ui, |ui| {
-                                            script.inspect(
-                                                entity,
-                                                &mut cfg,
-                                                ui,
-                                                self.undo_stack,
-                                                self.signal,
-                                                label.as_mut_string(),
-                                            );
-                                        });
-                                        ui.separator();
-                                    }
-
-                                    if let Some(t) = cfg.label_last_edit
-                                        && t.elapsed() >= Duration::from_millis(500)
+                                if let Some(t) = cfg.label_last_edit
+                                    && t.elapsed() >= Duration::from_millis(500)
+                                {
+                                    if let Some(ent) = cfg.old_label_entity.take()
+                                        && let Some(orig) = cfg.label_original.take()
                                     {
-                                        if let Some(ent) = cfg.old_label_entity.take()
-                                            && let Some(orig) = cfg.label_original.take()
-                                        {
-                                            UndoableAction::push_to_undo(
-                                                self.undo_stack,
-                                                UndoableAction::Label(ent, orig),
-                                            );
-                                            log::debug!(
-                                                "Pushed label change to undo stack after 500ms debounce period"
-                                            );
-                                        }
-                                        cfg.label_last_edit = None;
+                                        UndoableAction::push_to_undo(
+                                            self.undo_stack,
+                                            UndoableAction::Label(ent, orig),
+                                        );
+                                        log::debug!(
+                                            "Pushed label change to undo stack after 500ms debounce period"
+                                        );
                                     }
+                                    cfg.label_last_edit = None;
                                 }
-                            } else {
-                                log_once::debug_once!("Unable to query entity inside resource inspector");
                             }
+                        } else {
+                            log_once::debug_once!("Unable to query entity inside resource inspector");
                         }
-                    });
+                    }
 
-                    if local_set_initial_camera && !inspect_play_world {
+                    if local_set_initial_camera {
                         for (id, comp) in self.world.query::<&mut CameraComponent>().iter() {
                             comp.starting_camera = false;
                             self.undo_stack
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 1c3b060..4cfec34 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -3,7 +3,7 @@ pub mod console_error;
 pub mod dock;
 pub mod input;
 pub mod scene;
-mod settings;
+pub mod settings;
 
 pub(crate) use crate::editor::dock::*;
 
@@ -37,8 +37,7 @@ use eucalyptus_core::{
     camera::{CameraComponent, CameraType, DebugCamera},
     fatal, info,
     input::InputState,
-    ptr::{CommandBufferPtr, InputStatePtr, WorldPtr},
-    scripting::{BuildStatus, ScriptManager, ScriptTarget},
+    scripting::{BuildStatus, ScriptManager},
     states,
     states::{
         Camera3D, EditorTab, Light, PROJECT, SCENES, Script, WorldLoadingStatus,
@@ -46,12 +45,10 @@ use eucalyptus_core::{
     success,
     utils::ViewportMode,
     warn,
-    command::COMMAND_BUFFER,
 };
 use hecs::{Entity, World};
 use parking_lot::Mutex;
 use rfd::FileDialog;
-use std::path::Path;
 use std::{
     collections::HashMap,
     fs,
@@ -59,7 +56,6 @@ use std::{
     sync::Arc,
     time::{Duration, Instant},
 };
-use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
 use wgpu::{Color, Extent3d, RenderPipeline};
@@ -101,19 +97,10 @@ pub struct Editor {
     pub gizmo_mode: EnumSet<GizmoMode>,
     pub gizmo_orientation: GizmoOrientation,
 
+    #[allow(unused)] // unused to allow for JVM to startup
     pub(crate) script_manager: ScriptManager,
     pub play_mode_backup: Option<PlayModeBackup>,
 
-    /// Separate ECS world used only for play mode simulation/rendering.
-    /// The editor UI (entity list / inspector) remains bound to `world`.
-    pub(crate) play_world: Option<Box<World>>,
-    pub(crate) play_world_receiver: Option<oneshot::Receiver<Result<hecs::World, String>>>,
-    pub(crate) play_world_load_handle: Option<FutureHandle>,
-    pub(crate) pending_play_scene_load: Option<String>,
-    pub(crate) play_active_camera: Arc<Mutex<Option<Entity>>>,
-    pub(crate) play_script_jar: Option<PathBuf>,
-    pub(crate) play_scripts_loaded: bool,
-
     /// State of the input
     pub(crate) input_state: Box<InputState>,
 
@@ -159,6 +146,11 @@ pub struct Editor {
 
     // component registry
     component_registry: Arc<ComponentRegistry>,
+
+    // play mode process tracking
+    pub(crate) play_mode_process: Option<std::process::Child>,
+    pub(crate) play_mode_pid: Option<u32>,
+    pub(crate) play_mode_exit_rx: Option<std::sync::mpsc::Receiver<()>>,
 }
 
 impl Editor {
@@ -264,13 +256,6 @@ impl Editor {
             gizmo_mode: EnumSet::empty(),
             gizmo_orientation: GizmoOrientation::Global,
             play_mode_backup: None,
-            play_world: None,
-            play_world_receiver: None,
-            play_world_load_handle: None,
-            pending_play_scene_load: None,
-            play_active_camera: Arc::new(Mutex::new(None)),
-            play_script_jar: None,
-            play_scripts_loaded: false,
             input_state: Box::new(InputState::new()),
             light_manager: LightManager::new(),
             active_camera: Arc::new(Mutex::new(None)),
@@ -299,121 +284,12 @@ impl Editor {
             show_about: false,
             nerd_stats: NerdStats::default(),
             component_registry,
+            play_mode_process: None,
+            play_mode_pid: None,
+            play_mode_exit_rx: None,
         })
     }
 
-    fn ensure_play_world(&mut self) {
-        if self.play_world.is_none() {
-            self.play_world = Some(Box::new(World::new()));
-        }
-    }
-
-    fn start_async_play_scene_load_by_name(
-        &mut self,
-        scene_name: &str,
-        graphics_shared: Arc<SharedGraphicsContext>,
-    ) -> anyhow::Result<()> {
-        if scene_name.trim().is_empty() {
-            return Err(anyhow::anyhow!("Scene name cannot be empty"));
-        }
-
-        // Check if a scene load is already in progress
-        if self.play_world_receiver.is_some() {
-            log::warn!(
-                "Scene load already in progress, ignoring request to load '{}'",
-                scene_name
-            );
-            return Ok(());
-        }
-
-        self.ensure_play_world();
-        self.play_scripts_loaded = false;
-
-        if let Some(handle) = self.play_world_load_handle.take() {
-            graphics_shared.future_queue.cancel(&handle);
-        }
-
-        let scene = states::load_scene(scene_name)?;
-
-        let (world_sender, world_receiver) = oneshot::channel();
-        self.play_world_receiver = Some(world_receiver);
-
-        let graphics_shared_for_task = graphics_shared.clone();
-        let play_active_camera = self.play_active_camera.clone();
-        let scene_name_owned = scene.scene_name.clone();
-        let component_registry_clone = self.component_registry.clone();
-
-        let handle = graphics_shared.future_queue.push(async move {
-            let mut temp_world = World::new();
-
-            let load_result = scene
-                .load_into_world(
-                    &mut temp_world,
-                    graphics_shared_for_task.clone(),
-                    Some(component_registry_clone.as_ref()),
-                    None,
-                    true,
-                )
-                .await;
-
-            let result = match load_result {
-                Ok(active_entity) => {
-                    let mut camera_lock = play_active_camera.lock();
-                    *camera_lock = Some(active_entity);
-                    Ok(temp_world)
-                }
-                Err(err) => {
-                    let error_msg = format!("Failed to load play scene '{}': {}", scene_name_owned, err);
-                    log::error!("{}", error_msg);
-                    Err(error_msg)
-                }
-            };
-
-            if let Err(_) = world_sender.send(result) {
-                log::error!(
-                    "Failed to deliver play world result for scene '{}' (receiver dropped)",
-                    scene_name_owned
-                );
-            }
-        });
-
-        self.play_world_load_handle = Some(handle);
-        Ok(())
-    }
-
-    fn reload_play_scripts(&mut self) -> anyhow::Result<()> {
-        let Some(jar_path) = self.play_script_jar.clone() else {
-            return Ok(());
-        };
-
-        let Some(play_world) = self.play_world.as_mut() else {
-            return Ok(());
-        };
-
-        let mut etag: HashMap<String, Vec<Entity>> = HashMap::new();
-        for (entity_id, script) in play_world.query::<&Script>().iter() {
-            for tag in &script.tags {
-                etag.entry(tag.clone()).or_default().push(entity_id);
-            }
-        }
-
-        self.script_manager.init_script(
-            None,
-            etag,
-            ScriptTarget::JVM {
-                library_path: jar_path,
-            },
-        )?;
-
-        let world_ptr = play_world.as_mut() as WorldPtr;
-        let input_ptr = self.input_state.as_mut() as InputStatePtr;
-        let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr;
-
-        self.script_manager.load_script(world_ptr, input_ptr, graphics_ptr)?;
-        self.play_scripts_loaded = true;
-        Ok(())
-    }
-
     fn double_key_pressed(&mut self, key: KeyCode) -> bool {
         let now = Instant::now();
 
@@ -1502,52 +1378,69 @@ impl Editor {
         self.is_world_loaded.mark_rendering_loaded();
     }
 
-    pub fn load_play_mode(
-        &mut self,
-        path: impl AsRef<Path>,
-        graphics_shared: Arc<SharedGraphicsContext>,
-    ) -> anyhow::Result<()> {
-        let path = path.as_ref();
-        let has_player_camera_target = self
-            .world
-            .query::<(&Camera, &CameraComponent)>()
-            .iter()
-            .any(|(_, (_, comp))| comp.starting_camera);
+    /// Initialises another eucalyptus-editor play mode app as a separate process and monitors it in a separate thread.
+    pub fn load_play_mode(&mut self) -> anyhow::Result<()> {
+        use std::process::{Command, Stdio};
+        use std::sync::mpsc::channel;
+        use std::thread;
 
-        if has_player_camera_target {
-            self.save_current_scene()?;
-            success!("Saved scene");
+        let current_exe = std::env::current_exe()
+            .map_err(|e| anyhow::anyhow!("Failed to get current executable path: {}", e))?;
 
-            if let Err(e) = self.create_backup() {
-                self.signal = Signal::None;
-                fatal!("Failed to create play mode backup: {}", e);
-            }
+        let project_dir = {
+            let cfg = PROJECT.read();
+            cfg.project_path.clone()
+        };
 
-            self.editor_state = EditorState::Playing;
+        let current_scene = self.current_scene_name.clone().ok_or_else(|| {
+            anyhow::anyhow!("No current scene loaded; cannot launch play mode")
+        })?;
 
-            self.play_script_jar = Some(path.to_path_buf());
-            self.ensure_play_world();
+        log::info!("Launching play mode: {} play --project {:?}",
+            current_exe.display(), project_dir);
 
-            let scene_name = self
-                .current_scene_name
-                .clone()
-                .or_else(|| PROJECT.read().last_opened_scene.clone())
-                .ok_or_else(|| anyhow::anyhow!("No active scene available for play mode"))?;
+        let mut child = Command::new(&current_exe)
+            .arg("play")
+            .arg(&project_dir)
+            .arg(&current_scene)
+            .stdin(Stdio::null())
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .spawn()
+            .map_err(|e| anyhow::anyhow!("Failed to spawn play mode process: {}", e))?;
 
-            self.start_async_play_scene_load_by_name(&scene_name, graphics_shared)?;
-            info!(
-                "Entering play mode (loading play-world scene '{}')",
-                scene_name
-            );
+        let pid = child.id();
+        log::info!("Play mode process started with PID: {}", pid);
+        success!("Play mode launched (PID: {})", pid);
 
-            self.signal = Signal::None;
-            Ok(())
-        } else {
-            self.signal = Signal::None;
-            self.editor_state = EditorState::Editing;
-            fatal!("Unable to build: No initial camera set");
-            Err(anyhow::anyhow!("Unable to build: No initial camera set"))
-        }
+        let (tx, rx) = channel();
+
+        thread::spawn(move || {
+            log::debug!("Watch thread started for play mode process {}", pid);
+
+            match child.wait() {
+                Ok(status) => {
+                    log::info!("Play mode process {} exited with status: {}", pid, status);
+
+                    if let Err(e) = tx.send(()) {
+                        log::error!("Failed to send play mode exit notification: {}", e);
+                    }
+                }
+                Err(e) => {
+                    log::error!("Error waiting for play mode process {}: {}", pid, e);
+                    let _ = tx.send(());
+                }
+            }
+
+            log::debug!("Watch thread for play mode process {} terminated", pid);
+        });
+
+        self.play_mode_process = None;
+        self.play_mode_pid = Some(pid);
+        self.play_mode_exit_rx = Some(rx);
+        self.editor_state = EditorState::Playing;
+
+        Ok(())
     }
 }
 
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 1fe296b..4796f24 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -14,10 +14,8 @@ use dropbear_engine::{
 use eucalyptus_core::hierarchy::EntityTransformExt;
 use eucalyptus_core::logging;
 use eucalyptus_core::states::{CustomProperties, Label, WorldLoadingStatus};
-use eucalyptus_core::command::{CommandBufferPoller};
 use log;
 use parking_lot::Mutex;
-use tokio::sync::mpsc::unbounded_channel;
 use wgpu::Color;
 use wgpu::util::DeviceExt;
 use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode};
@@ -38,6 +36,8 @@ impl Scene for Editor {
             }
         };
 
+        log::debug!("Current scene name = {:?}", self.current_scene_name);
+
         let (tx, rx) = unbounded::<WorldLoadingStatus>();
         let (tx2, rx2) = oneshot::channel::<World>();
         self.progress_tx = Some(rx);
@@ -80,47 +80,20 @@ impl Scene for Editor {
     }
 
     fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
+        if let Some(rx) = &self.play_mode_exit_rx {
+            if rx.try_recv().is_ok() {
+                log::info!("Play mode process has exited, returning to editing mode");
+                success!("Exited play mode");
+                self.editor_state = EditorState::Editing;
+                self.play_mode_exit_rx = None;
+                self.play_mode_pid = None;
+            }
+        }
+
         if let Some(request) = self.pending_scene_load.take() {
             self.start_async_scene_load(request.scene, graphics);
         }
 
-        if matches!(self.editor_state, EditorState::Playing) {
-            if let Some(scene_name) = self.pending_play_scene_load.take() {
-                if let Err(e) = self
-                    .start_async_play_scene_load_by_name(&scene_name, graphics.shared.clone())
-                {
-                    fatal!("Failed to start play-world scene load: {}", e);
-                    self.signal = Signal::StopPlaying;
-                }
-            }
-
-            if let Some(mut receiver) = self.play_world_receiver.take() {
-                if let Ok(result) = receiver.try_recv() {
-                    match result {
-                        Ok(loaded_world) => {
-                            self.ensure_play_world();
-                            if let Some(play_world) = self.play_world.as_mut() {
-                                **play_world = loaded_world;
-                            }
-
-                            if let Err(e) = self.reload_play_scripts() {
-                                fatal!("Failed to initialise play-mode scripts: {}", e);
-                                self.signal = Signal::StopPlaying;
-                            } else {
-                                success!("You are in play mode now! Press Escape (or F12) to exit");
-                                log::info!("Play-mode scripts initialised");
-                            }
-                        }
-                        Err(error_msg) => {
-                            fatal!("Scene loading failed: {}", error_msg);
-                            self.signal = Signal::StopPlaying;
-                        }
-                    }
-                } else {
-                    self.play_world_receiver = Some(receiver);
-                }
-            }
-        }
 
         if let Some(mut receiver) = self.world_receiver.take() {
             self.show_project_loading_window(&graphics.shared.get_egui_context());
@@ -188,8 +161,6 @@ impl Scene for Editor {
             graphics.shared.window.set_title(&title);
         }
 
-        self.poll(graphics);
-
         {
             // basic futurequeue spawn queue management.
             let mut completed = Vec::new();
@@ -231,18 +202,6 @@ impl Scene for Editor {
             if self.input_state.pressed_keys.contains(&KeyCode::Escape) {
                 self.signal = Signal::StopPlaying;
             }
-
-            if self.play_scripts_loaded {
-                if let Some(play_world) = self.play_world.as_mut() {
-                    let world_ptr = play_world.as_mut() as *mut World;
-
-                    if let Err(e) = self.script_manager.update_script(&self.world, dt) 
-                    {
-                        fatal!("Failed to update script: {}", e);
-                        self.signal = Signal::StopPlaying;
-                    }
-                }
-            }
         }
 
         if self.is_viewport_focused
@@ -273,53 +232,19 @@ impl Scene for Editor {
 
         let _ = self.run_signal(graphics.shared.clone());
         
-        if matches!(self.editor_state, EditorState::Playing) {
-            if let Some(play_world) = self.play_world.as_mut() {
-                let resolve_play_entity = |author_entity: Entity, author_world: &World, play_world: &World| {
-                    let label = author_world
-                        .get::<&Label>(author_entity)
-                        .ok()
-                        .map(|l| l.as_str().to_string())?;
-                    play_world
-                        .query::<&Label>()
-                        .iter()
-                        .find_map(|(e, l)| (l.as_str() == label).then_some(e))
-                };
 
-                if let Some(prev) = self.previously_selected_entity {
-                    if let Some(prev_play) = resolve_play_entity(prev, self.world.as_ref(), play_world) {
-                        if let Ok(mut q) = play_world.query_one::<&mut MeshRenderer>(prev_play)
-                            && let Some(entity) = q.get()
-                        {
-                            entity.is_selected = false;
-                        }
-                    }
-                }
-
-                if let Some(sel) = self.selected_entity {
-                    if let Some(sel_play) = resolve_play_entity(sel, self.world.as_ref(), play_world) {
-                        if let Ok(mut q) = play_world.query_one::<&mut MeshRenderer>(sel_play)
-                            && let Some(entity) = q.get()
-                        {
-                            entity.is_selected = true;
-                        }
-                    }
-                }
-            }
-        } else {
-            if let Some(e) = self.previously_selected_entity
-                && let Ok(mut q) = self.world.query_one::<&mut MeshRenderer>(e)
-                && let Some(entity) = q.get()
-            {
-                entity.is_selected = false
-            }
+        if let Some(e) = self.previously_selected_entity
+            && let Ok(mut q) = self.world.query_one::<&mut MeshRenderer>(e)
+            && let Some(entity) = q.get()
+        {
+            entity.is_selected = false
+        }
 
-            if let Some(e) = self.selected_entity
-                && let Ok(mut q) = self.world.query_one::<&mut MeshRenderer>(e)
-                && let Some(entity) = q.get()
-            {
-                entity.is_selected = true
-            }
+        if let Some(e) = self.selected_entity
+            && let Ok(mut q) = self.world.query_one::<&mut MeshRenderer>(e)
+            && let Some(entity) = q.get()
+        {
+            entity.is_selected = true
         }
 
         let current_size = graphics.shared.viewport_texture.size;
@@ -328,16 +253,11 @@ impl Scene for Editor {
         let new_aspect = current_size.width as f64 / current_size.height as f64;
 
         {
-            let (active_camera, world_for_aspect): (Option<Entity>, Option<&mut World>) = if matches!(
-                self.editor_state,
-                EditorState::Playing
-            ) {
-                (*self.play_active_camera.lock(), self.play_world.as_deref_mut())
-            } else {
-                (*self.active_camera.lock(), Some(self.world.as_mut()))
-            };
+            // Play mode runs in separate process, always use editor world
+            let active_camera = *self.active_camera.lock();
+            let world = self.world.as_mut();
 
-            if let (Some(active_camera), Some(world)) = (active_camera, world_for_aspect)
+            if let Some(active_camera) = active_camera
                 && let Ok(mut query) = world.query_one::<&mut Camera>(active_camera)
                 && let Some(camera) = query.get()
             {
@@ -346,13 +266,7 @@ impl Scene for Editor {
         }
 
         {
-            let sim_world: &mut World = if matches!(self.editor_state, EditorState::Playing) {
-                self.play_world
-                    .as_deref_mut()
-                    .unwrap_or_else(|| self.world.as_mut())
-            } else {
-                self.world.as_mut()
-            };
+            let sim_world = self.world.as_mut();
 
             for (_entity_id, (camera, component)) in sim_world
                 .query::<(&mut Camera, &mut CameraComponent)>()
@@ -364,13 +278,7 @@ impl Scene for Editor {
         }
 
         {
-            let sim_world: &mut World = if matches!(self.editor_state, EditorState::Playing) {
-                self.play_world
-                    .as_deref_mut()
-                    .unwrap_or_else(|| self.world.as_mut())
-            } else {
-                self.world.as_mut()
-            };
+            let sim_world = self.world.as_mut();
 
             {
                 let query = sim_world.query_mut::<(&mut MeshRenderer, &Transform)>();
@@ -403,11 +311,7 @@ impl Scene for Editor {
         }
 
         {
-            let sim_world: &World = if matches!(self.editor_state, EditorState::Playing) {
-                self.play_world.as_deref().unwrap_or_else(|| self.world.as_ref())
-            } else {
-                self.world.as_ref()
-            };
+            let sim_world = self.world.as_ref();
 
             self.light_manager
                 .update(graphics.shared.clone(), sim_world);
@@ -417,6 +321,7 @@ impl Scene for Editor {
 
         self.input_state.window = self.window.clone();
         self.previously_selected_entity = self.selected_entity;
+        self.input_state.mouse_delta = None;
     }
 
     fn render(&mut self, graphics: &mut RenderContext) {
@@ -440,18 +345,8 @@ impl Scene for Editor {
         logging::render(&graphics.shared.get_egui_context());
         if let Some(pipeline) = &self.render_pipeline {
             log_once::debug_once!("Found render pipeline");
-            let (active_camera, world): (Option<Entity>, &World) = if matches!(
-                self.editor_state,
-                EditorState::Playing
-            ) {
-                let world = self
-                    .play_world
-                    .as_deref()
-                    .unwrap_or_else(|| self.world.as_ref());
-                (*self.play_active_camera.lock(), world)
-            } else {
-                (*self.active_camera.lock(), self.world.as_ref())
-            };
+            let (active_camera, world): (Option<Entity>, &World) =
+                (*self.active_camera.lock(), self.world.as_ref());
 
             if let Some(active_camera) = active_camera {
                 let cam = {
diff --git a/eucalyptus-editor/src/lib.rs b/eucalyptus-editor/src/lib.rs
index 6070924..e5c6693 100644
--- a/eucalyptus-editor/src/lib.rs
+++ b/eucalyptus-editor/src/lib.rs
@@ -9,5 +9,4 @@ pub mod signal;
 pub mod spawn;
 pub mod stats;
 pub mod utils;
-pub mod command;
 pub mod runtime;
diff --git a/eucalyptus-editor/src/main.rs b/eucalyptus-editor/src/main.rs
index e3eb001..4933d84 100644
--- a/eucalyptus-editor/src/main.rs
+++ b/eucalyptus-editor/src/main.rs
@@ -204,7 +204,20 @@ async fn main() -> anyhow::Result<()> {
                 path = find_eucp_in_dir(path.as_path())?;
             }
 
-            let config = ProjectConfig::read_from(path)?;
+            let config = ProjectConfig::read_from(path.clone())?;
+
+            {
+                let mut project = eucalyptus_core::states::PROJECT.write();
+                *project = config.clone();
+            }
+
+            let scene_to_load = initial_scene
+                .as_ref()
+                .or(config.runtime_settings.initial_scene.as_ref())
+                .ok_or_else(|| anyhow::anyhow!("No initial scene specified and no default scene in project config"))?;
+
+            eucalyptus_core::states::load_scene_into_memory(scene_to_load)?;
+            log::info!("Loaded initial scene '{}' for play mode", scene_to_load);
 
             let future_queue = Arc::new(FutureQueue::new());
 
diff --git a/eucalyptus-editor/src/runtime/command.rs b/eucalyptus-editor/src/runtime/command.rs
index 6b88b93..dbd1adc 100644
--- a/eucalyptus-editor/src/runtime/command.rs
+++ b/eucalyptus-editor/src/runtime/command.rs
@@ -1,6 +1,5 @@
 use dropbear_engine::graphics::RenderContext;
-use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand, get_config};
-use hecs::World;
+use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand};
 use winit::window::CursorGrabMode;
 use crate::runtime::PlayMode;
 
diff --git a/eucalyptus-editor/src/runtime/input.rs b/eucalyptus-editor/src/runtime/input.rs
index 50d1b6b..77566c2 100644
--- a/eucalyptus-editor/src/runtime/input.rs
+++ b/eucalyptus-editor/src/runtime/input.rs
@@ -18,9 +18,19 @@ impl Keyboard for PlayMode {
 
 impl Mouse for PlayMode {
     fn mouse_move(&mut self, position: PhysicalPosition<f64>, delta: Option<(f64, f64)>) {
-        self.input_state.last_mouse_pos = Some(<(f64, f64)>::from(position));
+        let delta = if delta.is_none() {
+            if let Some(last_pos) = self.input_state.last_mouse_pos {
+                Some((position.x - last_pos.0, position.y - last_pos.1))
+            } else {
+                None
+            }
+        } else {
+            delta
+        };
+
         self.input_state.mouse_delta = delta;
         self.input_state.mouse_pos = (position.x, position.y);
+        self.input_state.last_mouse_pos = Some(<(f64, f64)>::from(position));
     }
 
     fn mouse_down(&mut self, button: MouseButton) {
diff --git a/eucalyptus-editor/src/runtime/mod.rs b/eucalyptus-editor/src/runtime/mod.rs
index 4fc2a45..7fdf2f6 100644
--- a/eucalyptus-editor/src/runtime/mod.rs
+++ b/eucalyptus-editor/src/runtime/mod.rs
@@ -2,7 +2,6 @@
 
 use std::sync::Arc;
 use crossbeam_channel::{unbounded, Receiver};
-use egui::layers::GraphicLayers;
 use futures::executor;
 use hecs::{Entity, World};
 use wgpu::RenderPipeline;
@@ -15,10 +14,8 @@ use dropbear_engine::shader::Shader;
 use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::scripting::ScriptManager;
-use eucalyptus_core::states::{WorldLoadingStatus, PROJECT, SCENES};
+use eucalyptus_core::states::{WorldLoadingStatus, SCENES};
 use eucalyptus_core::traits::registry::ComponentRegistry;
-use crate::editor::IsWorldLoadedYet;
-use crate::graphics::OutlineShader;
 use crate::runtime::scene::IsSceneLoaded;
 
 mod scene;
@@ -68,13 +65,13 @@ impl PlayMode {
             scripts_ready: false,
             display_settings: DisplaySettings {
                 window_mode: WindowMode::Windowed,
-                render_resolution: (1280, 720),
-                display_resolution: (1280, 720),
                 maintain_aspect_ratio: true,
                 vsync: true,
             },
         };
 
+        log::debug!("Created new play mode instance");
+
         Ok(result)
     }
 
@@ -130,6 +127,7 @@ impl PlayMode {
 
     /// Requests an asynchronous scene load, returning immediately and loading the scene in the background.
     pub fn request_async_scene_load(&mut self, graphics: &mut RenderContext, requested_scene: IsSceneLoaded) {
+        log::debug!("Requested async scene load: {}", requested_scene.requested_scene);
         let scene_name = requested_scene.requested_scene.clone();
         self.scene_progress = Some(requested_scene);
 
@@ -168,6 +166,8 @@ impl PlayMode {
             }
         });
 
+        log::debug!("Created future handle for scene loading: {:?}", handle);
+
         self.scene_loading_handle = Some(handle);
         if let Some(ref mut progress) = self.scene_progress {
             progress.scene_handle_requested = true;
@@ -177,7 +177,7 @@ impl PlayMode {
     /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded.
     pub fn request_immediate_scene_load(&mut self, graphics: &mut RenderContext, requested_scene: IsSceneLoaded) {
         let scene_name = requested_scene.requested_scene.clone();
-        log::info!("Immediate scene load requested: {}", scene_name);
+        log::debug!("Immediate scene load requested: {}", scene_name);
 
         self.world = Box::new(World::new());
         self.active_camera = None;
@@ -230,12 +230,13 @@ impl PlayMode {
         progress.camera_received = true;
         self.scene_progress = Some(progress);
 
-        log::info!("Scene '{}' loaded immediately", scene_name);
+        log::debug!("Scene '{}' loaded immediately", scene_name);
     }
 
     /// Switches to a new scene, clearing the current world and preparing to load the new scene.
     pub fn switch_to(&mut self, scene_progress: IsSceneLoaded, _future_queue: Arc<FutureQueue>) {
-        log::info!("Switching to scene: {}", scene_progress.requested_scene);
+        // todo: fix this
+        log::debug!("Switching to new scene requested: {}", scene_progress.requested_scene);
 
         self.world = Box::new(World::new());
         self.active_camera = None;
@@ -253,8 +254,6 @@ impl PlayMode {
 
 pub struct DisplaySettings {
     pub window_mode: WindowMode,
-    pub render_resolution: (u32, u32),
-    pub display_resolution: (u32, u32),
     pub maintain_aspect_ratio: bool,
     pub vsync: bool,
 }
diff --git a/eucalyptus-editor/src/runtime/scene.rs b/eucalyptus-editor/src/runtime/scene.rs
index c152f18..b51a0a6 100644
--- a/eucalyptus-editor/src/runtime/scene.rs
+++ b/eucalyptus-editor/src/runtime/scene.rs
@@ -1,8 +1,7 @@
 use std::collections::HashMap;
 use std::path::PathBuf;
-use crossbeam_channel::unbounded;
 use egui::{CentralPanel, MenuBar, TopBottomPanel};
-use hecs::{Entity, World};
+use hecs::{Entity};
 use wgpu::Color;
 use wgpu::util::DeviceExt;
 use winit::event_loop::ActiveEventLoop;
@@ -13,11 +12,9 @@ use dropbear_engine::lighting::{Light, LightComponent};
 use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE};
 use dropbear_engine::scene::{Scene, SceneCommand};
 use eucalyptus_core::command::{COMMAND_BUFFER, CommandBufferPoller};
-use eucalyptus_core::input::InputState;
-use eucalyptus_core::logging;
+use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr};
 use eucalyptus_core::scripting::ScriptTarget;
-use eucalyptus_core::states::{Script, WorldLoadingStatus, PROJECT, SCENES};
-use crate::editor::Signal;
+use eucalyptus_core::states::{Script, PROJECT};
 use crate::runtime::{PlayMode, WindowMode};
 
 impl Scene for PlayMode {
@@ -27,9 +24,11 @@ impl Scene for PlayMode {
                 s.clone()
             } else {
                 let proj = PROJECT.read();
-                proj.runtime_settings.initial_scene.clone().unwrap()
+                proj.runtime_settings.initial_scene.clone().expect("No initial scene set in project settings")
             };
 
+            log::debug!("Loading initial scene: {}", initial_scene);
+
             let first_time = IsSceneLoaded::new_first_time(initial_scene);
 
             self.request_async_scene_load(graphics, first_time);
@@ -58,7 +57,6 @@ impl Scene for PlayMode {
                 }
             } else {
                 self.world_receiver = Some(receiver);
-                return;
             }
         }
 
@@ -74,7 +72,7 @@ impl Scene for PlayMode {
                 self.scene_loading_handle = Some(handle)
             }
         }
-        
+
         if let Some(ref progress) = self.scene_progress {
             if progress.is_everything_loaded() {
                 if progress.is_first_scene() && !self.scripts_ready {
@@ -90,21 +88,21 @@ impl Scene for PlayMode {
                     fn find_jvm_library_path() -> PathBuf {
                         let proj = PROJECT.read();
                         let project_path = if !proj.project_path.is_dir() {
-                            proj.project_path.parent().unwrap().to_path_buf()
+                            proj.project_path.parent().expect("Unable to locate parent of project").to_path_buf()
                         } else {
                             proj.project_path.clone()
                         }.join("build/libs");
 
                         let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
 
-                        for entry in std::fs::read_dir(&project_path).unwrap() {
-                            let entry = entry.unwrap();
+                        for entry in std::fs::read_dir(&project_path).expect("Unable to read directory") {
+                            let entry = entry.expect("Unable to get directory entry");
                             let path = entry.path();
 
                             if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
                                 if filename.ends_with("-all.jar") {
-                                    let metadata = entry.metadata().unwrap();
-                                    let modified = metadata.modified().unwrap();
+                                    let metadata = entry.metadata().expect("Unable to get file metadata");
+                                    let modified = metadata.modified().expect("Unable to get file modified time");
 
                                     match latest_jar {
                                         None => latest_jar = Some((path.clone(), modified)),
@@ -122,6 +120,7 @@ impl Scene for PlayMode {
 
                     let target = ScriptTarget::JVM { library_path: find_jvm_library_path() };
 
+                    log::debug!("Awaiting script initialisation with target: {:?}", target);
                     if let Err(e) = self.script_manager.init_script(
                         None,
                         entity_tag_map.clone(),
@@ -129,9 +128,9 @@ impl Scene for PlayMode {
                     ) {
                         log::error!("Failed to initialise scripts: {}", e);
                     } else {
-                        let world_ptr = self.world.as_mut() as *mut World;
-                        let input_ptr = &mut self.input_state as *mut InputState;
-                        let graphics_ptr = &COMMAND_BUFFER.0 as *const _ as *const _;
+                        let world_ptr = self.world.as_mut() as WorldPtr;
+                        let input_ptr = &mut self.input_state as InputStatePtr;
+                        let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr;
 
                         if let Err(e) = self.script_manager.load_script(world_ptr, input_ptr, graphics_ptr) {
                             log::error!("Failed to load scripts: {}", e);
@@ -143,8 +142,8 @@ impl Scene for PlayMode {
                 }
 
                 if self.scripts_ready {
-                    if let Err(e) = unsafe { self.script_manager.update_script(&self.world, dt) } {
-                        log::error!("Script update error: {}", e);
+                    if let Err(e) = self.script_manager.update_script(&self.world, dt) {
+                        panic!("Script update error: {}", e);
                     }
                 }
             }
@@ -153,26 +152,6 @@ impl Scene for PlayMode {
         TopBottomPanel::top("menu_bar").show(&graphics.shared.get_egui_context(), |ui| {
             MenuBar::new().ui(ui, |ui| {
                 ui.menu_button("Window", |ui| {
-                    ui.menu_button("Resolution", |ui| {
-                        let resolutions = [
-                            (1280, 720, "1280×720 (HD)"),
-                            (1600, 900, "1600×900"),
-                            (1920, 1080, "1920×1080 (Full HD)"),
-                            (2560, 1440, "2560×1440 (QHD)"),
-                            (3840, 2160, "3840×2160 (4K)"),
-                        ];
-
-                        for (width, height, label) in resolutions {
-                            let is_current = self.display_settings.render_resolution == (width, height);
-                            if ui.selectable_label(is_current, label).clicked() {
-                                self.display_settings.render_resolution = (width, height);
-                                ui.close();
-                            }
-                        }
-                    });
-
-                    ui.separator();
-
                     ui.menu_button("Window Mode", |ui| {
                         let is_windowed = matches!(self.display_settings.window_mode, WindowMode::Windowed);
                         if ui.selectable_label(is_windowed, "Windowed").clicked() {
@@ -201,13 +180,8 @@ impl Scene for PlayMode {
 
                     ui.separator();
 
-                    if ui.checkbox(&mut self.display_settings.maintain_aspect_ratio, "Maintain aspect ratio").clicked() {
-                        self.display_settings.maintain_aspect_ratio = !self.display_settings.maintain_aspect_ratio;
-                    }
-
-                    if ui.checkbox(&mut self.display_settings.vsync, "VSync").clicked() {
-                        self.display_settings.vsync = !self.display_settings.vsync;
-                    }
+                    ui.checkbox(&mut self.display_settings.maintain_aspect_ratio, "Maintain aspect ratio");
+                    ui.checkbox(&mut self.display_settings.vsync, "VSync").clicked();
                 });
 
                 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
@@ -241,170 +215,160 @@ impl Scene for PlayMode {
             }
 
             let texture_id = *graphics.shared.texture_id;
-            
-            let (render_width, render_height) = self.display_settings.render_resolution;
-            let render_aspect = render_width as f32 / render_height as f32;
 
             let available_size = ui.available_size();
             let available_rect = ui.available_rect_before_wrap();
 
-            let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio {
-                if available_size.x / available_size.y > render_aspect {
-                    let height = available_size.y;
-                    let width = height * render_aspect;
-                    (width, height)
-                } else {
-                    let width = available_size.x;
-                    let height = width / render_aspect;
-                    (width, height)
-                }
-            } else {
-                (available_size.x, available_size.y)
-            };
+            if let Some(active_camera) = self.active_camera {
+                if let Ok(cam) = self.world.query_one_mut::<&Camera>(active_camera) {
+                    let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio {
+                        let width = available_size.x;
+                        let height = width / cam.aspect as f32;
+                        (width, height)
+                    } else {
+                        (available_size.x, available_size.y)
+                    };
 
-            let center_x = available_rect.center().x;
-            let center_y = available_rect.center().y;
+                    let center_x = available_rect.center().x;
+                    let center_y = available_rect.center().y;
 
-            let image_rect = egui::Rect::from_center_size(
-                egui::pos2(center_x, center_y),
-                egui::vec2(display_width, display_height),
-            );
+                    let image_rect = egui::Rect::from_center_size(
+                        egui::pos2(center_x, center_y),
+                        egui::vec2(display_width, display_height),
+                    );
 
-            ui.allocate_exact_size(available_size, egui::Sense::hover());
+                    ui.allocate_exact_size(available_size, egui::Sense::hover());
 
-            ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| {
-                ui.add_sized(
-                    [display_width, display_height],
-                    egui::Image::new((texture_id, [display_width, display_height].into()))
-                        .fit_to_exact_size([display_width, display_height].into()),
-                )
-            });
+                    ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| {
+                        ui.add_sized(
+                            [display_width, display_height],
+                            egui::Image::new((texture_id, [display_width, display_height].into()))
+                                .fit_to_exact_size([display_width, display_height].into()),
+                        )
+                    });
+                }
+            }
         });
+
+        self.input_state.mouse_delta = None;
     }
 
     fn render(&mut self, graphics: &mut RenderContext) {
-        // cornflower blue
-        let color = Color {
+        let Some(active_camera) = self.active_camera else {
+            return;
+        };
+        log_once::debug_once!("Active camera found: {:?}", active_camera);
+
+        let q = if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) {
+            query.get().cloned()
+        } else {
+            None
+        };
+
+        let Some(camera) = q else {
+            return;
+        };
+        log_once::debug_once!("Camera ready");
+        log_once::debug_once!("Camera currently being viewed: {}", camera.label);
+
+        // camera.debug_camera_state();
+        // println!("{:#?}", self.project_config);
+
+        let Some(pipeline) = &self.render_pipeline else {
+            log_once::warn_once!("Render pipeline not ready");
+            return;
+        };
+        log_once::debug_once!("Pipeline ready");
+
+        let clear_color = Color {
             r: 100.0 / 255.0,
             g: 149.0 / 255.0,
             b: 237.0 / 255.0,
             a: 1.0,
         };
 
-        if let Some(pipeline) = &self.render_pipeline {
-            log_once::debug_once!("Found render pipeline");
+        let lights = {
+            let mut lights = Vec::new();
+            let mut query = self.world.query::<(&Light, &LightComponent)>();
+            for (_, (light, comp)) in query.iter() {
+                lights.push((light.clone(), comp.clone()));
+            }
+            lights
+        };
 
-            if let Some(active_camera) = self.active_camera {
-                let cam = {
-                    if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) {
-                        query.get().cloned()
-                    } else {
-                        None
-                    }
-                };
-
-                if let Some(camera) = cam {
-                    let lights = {
-                        let mut lights = Vec::new();
-                        let mut light_query = self.world.query::<(&Light, &LightComponent)>();
-                        for (_, (light, comp)) in light_query.iter() {
-                            lights.push((light.clone(), comp.clone()));
-                        }
-                        lights
-                    };
+        let renderers = {
+            let mut renderers = Vec::new();
+            let mut query = self.world.query::<&MeshRenderer>();
+            for (_, renderer) in query.iter() {
+                renderers.push(renderer.clone());
+            }
+            renderers
+        };
 
-                    let entities = {
-                        let mut entities = Vec::new();
-                        let mut entity_query = self.world.query::<&MeshRenderer>();
-                        for (_, renderer) in entity_query.iter() {
-                            entities.push(renderer.clone());
-                        }
-                        entities
-                    };
+        {
+            let mut query = self.world.query::<(&mut LightComponent, &dropbear_engine::entity::Transform, &mut Light)>();
+            for (_, (light_component, transform, light)) in query.iter() {
+                light.update(light_component, transform);
+            }
+        }
 
-                    {
-                        // light cube rendering
-                        let mut render_pass = graphics.clear_colour(color);
-                        if let Some(light_pipeline) = &self.light_manager.pipeline {
-                            render_pass.set_pipeline(light_pipeline);
-                            for (light, _component) in &lights {
-                                render_pass.set_vertex_buffer(
-                                    1,
-                                    light.instance_buffer.as_ref().unwrap().slice(..),
-                                );
-                                if _component.visible {
-                                    render_pass.draw_light_model(
-                                        &light.cube_model,
-                                        camera.bind_group(),
-                                        light.bind_group(),
-                                    );
-                                }
-                            }
+        self.light_manager.update(graphics.shared.clone(), &self.world);
+
+        {
+            let mut render_pass = graphics.clear_colour(clear_color);
+            if let Some(light_pipeline) = &self.light_manager.pipeline {
+                render_pass.set_pipeline(light_pipeline);
+                for (light, component) in &lights {
+                    if let Some(buffer) = &light.instance_buffer {
+                        render_pass.set_vertex_buffer(1, buffer.slice(..));
+                        if component.visible {
+                            render_pass.draw_light_model(
+                                &light.cube_model,
+                                camera.bind_group(),
+                                light.bind_group(),
+                            );
                         }
                     }
+                }
+            }
+        }
 
-                    let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new();
-                    for renderer in &entities {
-                        let model_ptr = renderer.model_id();
-                        let instance_raw = renderer.instance.to_raw();
-                        model_batches
-                            .entry(model_ptr)
-                            .or_default()
-                            .push(instance_raw);
-                    }
+        let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new();
+        for renderer in &renderers {
+            model_batches
+                .entry(renderer.model_id())
+                .or_default()
+                .push(renderer.instance.to_raw());
+        }
 
-                    for (model_ptr, instances) in model_batches {
-                        {
-                            let model_opt = {
-                                let cache = MODEL_CACHE.lock();
-                                cache.values().find(|m| m.id == model_ptr).cloned()
-                            };
-
-                            if let Some(model) = model_opt {
-                                let instance_buffer = graphics.shared.device.create_buffer_init(
-                                    &wgpu::util::BufferInitDescriptor {
-                                        label: Some("Batched Instance Buffer"),
-                                        contents: bytemuck::cast_slice(&instances),
-                                        usage: wgpu::BufferUsages::VERTEX,
-                                    },
-                                );
-
-                                {
-                                    // normal model rendering
-                                    let mut render_pass = graphics.continue_pass();
-                                    render_pass.set_pipeline(pipeline);
-
-                                    render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
-                                    render_pass.draw_model_instanced(
-                                        &model,
-                                        0..instances.len() as u32,
-                                        camera.bind_group(),
-                                        self.light_manager.bind_group(),
-                                    );
-                                }
+        for (model_id, instances) in model_batches {
+            let model_opt = {
+                let cache = MODEL_CACHE.lock();
+                cache.values().find(|model| model.id == model_id).cloned()
+            };
 
-                                log_once::debug_once!("Rendered {:?}", model_ptr);
-                            } else {
-                                log_once::error_once!("No such MODEL as {:?}", model_ptr);
-                            }
-                        }
-                    }
-                } else {
-                    log_once::error_once!("Camera returned None");
-                }
-            } else {
-                log_once::error_once!("No active camera found");
-            }
-        } else {
-            if let Some(p) = &self.scene_progress {
-                if p.is_everything_loaded() {
-                    log_once::warn_once!("No render pipeline exists");
-                } else {
-                    log_once::debug_once!("No render pipeline exists, but world not loaded yet");
-                }
-            } else {
-                log_once::debug_once!("No render pipeline exists");
-            }
+            let Some(model) = model_opt else {
+                log_once::error_once!("Missing model {:?} in cache", model_id);
+                continue;
+            };
+
+            let instance_buffer = graphics.shared.device.create_buffer_init(
+                &wgpu::util::BufferInitDescriptor {
+                    label: Some("Runtime Instance Buffer"),
+                    contents: bytemuck::cast_slice(&instances),
+                    usage: wgpu::BufferUsages::VERTEX,
+                },
+            );
+
+            let mut render_pass = graphics.continue_pass();
+            render_pass.set_pipeline(pipeline);
+            render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+            render_pass.draw_model_instanced(
+                &model,
+                0..instances.len() as u32,
+                camera.bind_group(),
+                self.light_manager.bind_group(),
+            );
         }
     }
 
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index bfe6f50..d2902da 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -219,7 +219,7 @@ impl SignalController for Editor {
 
                         self.show_build_window = false;
 
-                        self.load_play_mode(jar_path, graphics.clone())?;
+                        self.load_play_mode()?;
                         return Ok(());
                     }
 
@@ -357,7 +357,7 @@ impl SignalController for Editor {
                                 success!("Build completed successfully!");
                                 self.show_build_window = false;
 
-                                self.load_play_mode(path, graphics.clone())?;
+                                self.load_play_mode()?;
                             }
                             Err(e) => {
                                 let error_msg = format!("Build process error: {}", e);
@@ -424,24 +424,16 @@ impl SignalController for Editor {
                 Ok(())
             }
             Signal::StopPlaying => {
-                if let Err(e) = self.restore() {
-                    warn!("Failed to restore from play mode backup: {}", e);
-                    log::warn!("Failed to restore scene state: {}", e);
-                }
-
-                // Drop play-mode-only state; authoring world remains intact.
-                self.play_world = None;
-                self.play_world_receiver = None;
-                self.play_world_load_handle = None;
-                self.pending_play_scene_load = None;
-                self.play_active_camera.lock().take();
-                self.play_script_jar = None;
-                self.play_scripts_loaded = false;
 
                 self.editor_state = EditorState::Editing;
-
-                self.switch_to_debug_camera();
-
+                
+                if let Some(pid) = self.play_mode_pid {
+                    log::info!("Play mode process {} should exit soon", pid);
+                }
+                
+                self.play_mode_pid = None;
+                self.play_mode_exit_rx = None;
+                
                 success!("Exited play mode");
                 log::info!("Back to the editor you go...");
 
diff --git a/redback-runtime/src/scene.rs b/redback-runtime/src/scene.rs
index af759af..0e32b83 100644
--- a/redback-runtime/src/scene.rs
+++ b/redback-runtime/src/scene.rs
@@ -88,7 +88,7 @@ impl RuntimeScene {
             current_scene: None,
             pending_scene_switch: None,
             component_registry,
-            script_manager: ScriptManager::new(window_config.jvm_args)?,
+            script_manager: ScriptManager::new()?,
             script_target,
             scripts_ready: false,
             scene_command: Default::default(),