kitgit

tirbofish/dropbear · diff

7c471d2 · Thribhu K

feature: euca-runner now works successfully. fix: got windowing options available for the redback-runtime fix: some general fixes here and there. jarvis, run github actions. Unverified

diff --git a/Cargo.toml b/Cargo.toml
index ff19719..1aecd11 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -72,7 +72,7 @@ dyn-hash = "1.0"
 semver = { version = "1.0", features = ["serde"] }
 rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] }
 cbindgen = { version = "0.29.2" }
-postcard = { version = "1.1"}
+postcard = { version = "1.1", features = ["use-std"]}
 pollster = "0.4"
 yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] }
 yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index bcf8a9f..5c18635 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -7,7 +7,7 @@ use crate::{
 use dropbear_future_queue::FutureQueue;
 use egui::{Context, TextureId};
 use glam::{DMat4, DQuat, DVec3, Mat3};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use std::sync::Arc;
 use wgpu::*;
 use winit::window::Window;
@@ -21,6 +21,7 @@ pub struct SharedGraphicsContext {
     pub queue: Arc<Queue>,
     pub surface: Arc<Surface<'static>>,
     pub surface_format: TextureFormat,
+    pub surface_config: Arc<RwLock<SurfaceConfiguration>>,
     pub instance: Arc<wgpu::Instance>,
     pub layouts: Arc<BindGroupLayouts>,
     pub window: Arc<Window>,
@@ -77,6 +78,7 @@ impl SharedGraphicsContext {
             mipmapper: state.mipmapper.clone(),
             yakui_renderer: state.yakui_renderer.clone(),
             yakui_texture: state.yakui_texture.clone(),
+            surface_config: state.config.clone(),
         }
     }
 }
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 0028298..6da1a98 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -85,7 +85,7 @@ pub struct State {
     pub surface_format: TextureFormat,
     pub device: Arc<Device>,
     pub queue: Arc<Queue>,
-    pub config: SurfaceConfiguration,
+    pub config: Arc<RwLock<SurfaceConfiguration>>,
     pub is_surface_configured: bool,
     pub layouts: Arc<BindGroupLayouts>,
     pub egui_renderer: Arc<Mutex<EguiRenderer>>,
@@ -351,7 +351,7 @@ Hardware:
             surface_format: Texture::TEXTURE_FORMAT,
             device,
             queue,
-            config,
+            config: Arc::new(RwLock::new(config)),
             is_surface_configured,
             depth_texture,
             window,
@@ -383,16 +383,19 @@ Hardware:
     /// A helper function that changes the surface config when resized (+ depth texture).
     pub fn resize(&mut self, width: u32, height: u32) {
         if width > 0 && height > 0 {
-            self.config.width = width;
-            self.config.height = height;
-            self.surface.configure(&self.device, &self.config);
+            {
+                let mut config = self.config.write();
+                config.width = width;
+                config.height = height;
+            }
+            self.surface.configure(&self.device, &self.config.read());
             self.is_surface_configured = true;
         }
 
         self.depth_texture =
-            Texture::depth_texture(&self.config, &self.device, Some("depth texture"));
+            Texture::depth_texture(&self.config.read(), &self.device, Some("depth texture"));
         self.viewport_texture =
-            Texture::viewport(&self.config, &self.device, Some("viewport texture"));
+            Texture::viewport(&self.config.read(), &self.device, Some("viewport texture"));
         self.egui_renderer
             .lock()
             .renderer()
@@ -415,19 +418,21 @@ Hardware:
         if !self.is_surface_configured {
             return Ok(Vec::new());
         }
-
+        
+        let config = self.config.read().clone();
+        
         let output = match self.surface.get_current_texture() {
             Ok(val) => val,
             Err(e) => {
                 return match e {
                     SurfaceError::Lost => {
                         log_once::warn_once!("Surface lost, reconfiguring...");
-                        self.surface.configure(&self.device, &self.config);
+                        self.surface.configure(&self.device, &config);
                         Ok(Vec::new())
                     }
                     SurfaceError::Outdated => {
                         log_once::warn_once!("Surface outdated, reconfiguring...");
-                        self.surface.configure(&self.device, &self.config);
+                        self.surface.configure(&self.device, &config);
                         Ok(Vec::new())
                     }
                     SurfaceError::Timeout => {
@@ -446,7 +451,7 @@ Hardware:
         };
 
         let screen_descriptor = ScreenDescriptor {
-            size_in_pixels: [self.config.width, self.config.height],
+            size_in_pixels: [config.width, config.height],
             pixels_per_point: self.window.scale_factor() as f32,
         };
 
diff --git a/crates/euca-runner/Cargo.toml b/crates/euca-runner/Cargo.toml
index 5cbba10..0cd33f3 100644
--- a/crates/euca-runner/Cargo.toml
+++ b/crates/euca-runner/Cargo.toml
@@ -21,11 +21,11 @@ chrono.workspace = true
 env_logger.workspace = true
 colored.workspace = true
 ron.workspace = true
-hecs.workspace = true
-bytemuck.workspace = true
-log-once.workspace = true
 winit.workspace = true
 serde.workspace = true
-crossbeam-channel.workspace = true
-gilrs.workspace = true
-futures.workspace = true
+
+[features]
+default = ["jvm"]
+
+jvm = ["redback-runtime/jvm", "eucalyptus-core/jvm"]
+debug = ["eucalyptus-core/jvm_debug"]
diff --git a/crates/euca-runner/src/main.rs b/crates/euca-runner/src/main.rs
index b3bc66c..23ceec6 100644
--- a/crates/euca-runner/src/main.rs
+++ b/crates/euca-runner/src/main.rs
@@ -1,3 +1,5 @@
+#![windows_subsystem = "windows"]
+
 use app_dirs2::AppInfo;
 use dropbear_engine::future::FutureQueue;
 use eucalyptus_core::runtime::RuntimeProjectConfig;
@@ -8,13 +10,104 @@ use std::fs;
 use std::rc::Rc;
 use std::sync::Arc;
 use ron::ser::PrettyConfig;
-use winit::window::WindowAttributes;
+use winit::window::{Fullscreen, WindowAttributes};
 use dropbear_engine::{DropbearAppBuilder, DropbearWindowBuilder};
 use serde::{Deserialize, Serialize};
+use winit::dpi::PhysicalSize;
+use eucalyptus_core::scripting::jni::{RuntimeMode, RUNTIME_MODE};
 
 #[tokio::main]
 async fn main() {
-    env_logger::init();
+    // env_logger::init();
+
+    #[cfg(all(not(target_os = "android"), feature = "debug"))]
+    {
+        use colored::Colorize;
+        use env_logger::Builder;
+        use log::LevelFilter;
+        use std::fs::OpenOptions;
+
+        let log_dir =
+            app_dirs2::app_root(app_dirs2::AppDataType::UserData, &eucalyptus_core::APP_INFO)
+                .expect("Failed to get app data directory")
+                .join("logs");
+        fs::create_dir_all(&log_dir).expect("Failed to create log dir");
+
+        let datetime_str = chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S");
+        let log_filename = format!("{}.{}.log", "eucalyptus-editor", datetime_str);
+        let log_path = log_dir.join(log_filename);
+
+        let file = OpenOptions::new()
+            .create(true)
+            .append(true)
+            .open(&log_path)
+            .expect("Failed to open log file");
+        let file = parking_lot::Mutex::new(file);
+
+        let app_target = "eucalyptus-editor".replace('-', "_");
+        let log_config = format!("dropbear_engine=trace,{}=debug,warn", app_target);
+        unsafe { std::env::set_var("RUST_LOG", log_config) };
+
+        Builder::new()
+            .format(move |buf, record| {
+                use std::io::Write;
+
+                let ts = chrono::offset::Local::now().format("%Y-%m-%dT%H:%M:%S");
+
+                let colored_level = match record.level() {
+                    log::Level::Error => record.level().to_string().red().bold(),
+                    log::Level::Warn => record.level().to_string().yellow().bold(),
+                    log::Level::Info => record.level().to_string().green().bold(),
+                    log::Level::Debug => record.level().to_string().blue().bold(),
+                    log::Level::Trace => record.level().to_string().cyan().bold(),
+                };
+
+                let colored_timestamp = ts.to_string().bright_black();
+
+                let file_info = format!(
+                    "{}:{}",
+                    record.file().unwrap_or("unknown"),
+                    record.line().unwrap_or(0)
+                )
+                    .bright_black();
+
+                let console_line = format!(
+                    "{} {} [{}] - {}\n",
+                    file_info,
+                    colored_timestamp,
+                    colored_level,
+                    record.args()
+                );
+
+                let file_line = format!(
+                    "{}:{} {} [{}] - {}\n",
+                    record.file().unwrap_or("unknown"),
+                    record.line().unwrap_or(0),
+                    ts,
+                    record.level(),
+                    record.args()
+                );
+
+                write!(buf, "{}", console_line)?;
+
+                let mut fh = file.lock();
+                let _ = fh.write_all(file_line.as_bytes());
+
+                Ok(())
+            })
+            .filter_level(LevelFilter::Warn)
+            .filter(Some("dropbear_engine"), LevelFilter::Trace)
+            .filter(
+                Some("eucalyptus_editor"),
+                LevelFilter::Debug,
+            )
+            .filter(Some("eucalyptus_core"), LevelFilter::Debug)
+            .filter(Some("dropbear_traits"), LevelFilter::Debug)
+            .filter(Some("euca_runner"), LevelFilter::Debug)
+            .init();
+        log::info!("Initialised logger");
+    }
+
 
     dropbear_engine::panic::set_hook();
     log::debug!("Set panic hook");
@@ -65,10 +158,13 @@ async fn main() {
     log::debug!("Located scene config file: [{}] ({} bytes)", path.display(), scene_config.len());
 
     let scene_config: RuntimeProjectConfig = postcard::from_bytes(&scene_config).unwrap();
+    scene_config.populate().unwrap();
 
     log::debug!("Converted scene config file to RuntimeProjectConfig");
 
-    let runtime_scene = Rc::new(RwLock::new(PlayMode::new(None).unwrap()));
+    let _ = RUNTIME_MODE.set(RuntimeMode::Runtime);
+
+    let runtime_scene = Rc::new(RwLock::new(PlayMode::new(Some(scene_config.initial_scene)).unwrap()));
     let future_queue = Arc::new(FutureQueue::new());
 
     let authors = scene_config.authors.developer.clone();
@@ -81,11 +177,11 @@ async fn main() {
 
     let attributes = WindowAttributes::default();
 
-    match config.target_resolution {
-        WindowModes::Windowed(_, _) => {}
-        WindowModes::Maximised => {}
-        WindowModes::Fullscreen => {}
-    }
+    let attributes = match config.target_resolution {
+        WindowModes::Windowed(x, y) => {attributes.with_inner_size(PhysicalSize::new(x, y))}
+        WindowModes::Maximised => {attributes.with_maximized(true)}
+        WindowModes::Fullscreen => {attributes.with_fullscreen(Some(Fullscreen::Borderless(None)))}
+    };
 
     let window = DropbearWindowBuilder::new()
         .with_attributes(attributes)
diff --git a/crates/eucalyptus-core/src/runtime.rs b/crates/eucalyptus-core/src/runtime.rs
index 9532ce5..609525d 100644
--- a/crates/eucalyptus-core/src/runtime.rs
+++ b/crates/eucalyptus-core/src/runtime.rs
@@ -1,10 +1,12 @@
 //! Configuration and metadata information about redback-runtime based data.
 
+use crate::config::ProjectConfig;
 use crate::scene::SceneConfig;
 use crate::states::{PROJECT, SCENES};
+use crate::utils::option::HistoricalOption;
 use anyhow::Context;
+use chrono::Utc;
 use semver::Version;
-use crate::utils::option::HistoricalOption;
 
 /// The settings of a project in its runtime.
 ///
@@ -119,4 +121,44 @@ impl RuntimeProjectConfig {
 
         Ok(result)
     }
+
+    /// Populates the states (such as [PROJECT]) with all the context from the RuntimeProjectConfig. 
+    pub fn populate(&self) -> anyhow::Result<()> {
+        let exe_dir = std::env::current_exe()
+            .context("Unable to locate runtime executable")?
+            .parent()
+            .ok_or_else(|| anyhow::anyhow!("Unable to locate parent directory of runtime executable"))?
+            .to_path_buf();
+
+        let now = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
+
+        let mut runtime_settings = self.runtime_settings.clone();
+        if runtime_settings.initial_scene.is_none() {
+            runtime_settings.initial_scene = Some(self.initial_scene.clone());
+        }
+
+        let project_config = ProjectConfig {
+            project_name: self.project_name.clone(),
+            project_path: exe_dir,
+            date_created: now.clone(),
+            date_last_accessed: now,
+            project_version: self.project_version.to_string(),
+            authors: self.authors.clone(),
+            runtime_settings,
+            last_opened_scene: Some(self.initial_scene.clone()),
+        };
+
+        {
+            let mut project = PROJECT.write();
+            *project = project_config;
+        }
+
+        {
+            let mut scenes = SCENES.write();
+            scenes.clear();
+            scenes.extend(self.scenes.iter().cloned());
+        }
+
+        Ok(())
+    }
 }
diff --git a/crates/eucalyptus-core/src/scripting.rs b/crates/eucalyptus-core/src/scripting.rs
index 760b394..f4e89c9 100644
--- a/crates/eucalyptus-core/src/scripting.rs
+++ b/crates/eucalyptus-core/src/scripting.rs
@@ -119,6 +119,9 @@ impl ScriptManager {
             log::debug!("Created new JVM instance");
         }
 
+        #[cfg(not(feature = "jvm"))]
+        log::debug!("JVM feature not enabled");
+
         Ok(result)
     }
 
diff --git a/crates/eucalyptus-core/src/scripting/jni.rs b/crates/eucalyptus-core/src/scripting/jni.rs
index c311882..81cd304 100644
--- a/crates/eucalyptus-core/src/scripting/jni.rs
+++ b/crates/eucalyptus-core/src/scripting/jni.rs
@@ -144,8 +144,6 @@ 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);
@@ -163,30 +161,8 @@ impl JavaContext {
                     }
                     RuntimeMode::Editor => {
                         log::debug!("JDB is not used in the editor (as there is no need for so)");
-
-                        // let (start_port, end_port) = (6741, 6761);
-                        // let mut port = 0000;
-                        // let mut debug_arg = String::new();
-                        //
-                        // for p in start_port..end_port {
-                        //     if is_port_available(p) {
-                        //         port = p;
-                        //         debug_arg = format!("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:{}", port);
-                        //         break;
-                        //     } else {
-                        //         log::debug!("Port {} is not available", p);
-                        //     }
-                        // }
-                        //
-                        // if debug_arg.is_empty() {
-                        //     log::warn!("Could not find an available port for JDWP debugger (tried 6741-6760). Debugging will be disabled.");
-                        // } else {
-                        //     args_log.push(debug_arg.clone());
-                        //     jvm_args = jvm_args.option(debug_arg);
-                        //     log::info!("JDWP debugger enabled on port {}", port);
-                        // }
                     }
-                    RuntimeMode::PlayMode => {
+                    RuntimeMode::PlayMode | RuntimeMode::Runtime => {
                         let (start_port, end_port) = (6751, 6771);
                         let mut port = 0000;
                         let mut debug_arg = String::new();
@@ -218,9 +194,6 @@ impl JavaContext {
                             log::info!("JDWP debugger enabled on port {}", port);
                         }
                     }
-                    RuntimeMode::Runtime => {
-                        log::warn!("Runtime mode JWDB not implemented yet...");
-                    }
                 }
             }
 
@@ -288,9 +261,6 @@ impl JavaContext {
         let jvm_init_args = jvm_args.build()?;
         let jvm = JavaVM::new(jvm_init_args)?;
 
-        #[cfg(feature = "jvm_debug")]
-        crate::success!("JDB debugger enabled on localhost:6741");
-
         log::info!("Created JVM instance");
 
         Ok(Self {
diff --git a/crates/eucalyptus-editor/src/build.rs b/crates/eucalyptus-editor/src/build.rs
index e86eb37..a6ff345 100644
--- a/crates/eucalyptus-editor/src/build.rs
+++ b/crates/eucalyptus-editor/src/build.rs
@@ -76,8 +76,7 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> {
 
     // export to .eupak
     let eupak_path = build_dir.join("data.eupak");
-    const SIZE_OF_RUNTIME_PROJECT_CONFIG: usize = size_of::<RuntimeProjectConfig>();
-    let config_bytes = postcard::to_vec::<RuntimeProjectConfig, SIZE_OF_RUNTIME_PROJECT_CONFIG>(&runtime_config)?;
+    let config_bytes = postcard::to_stdvec::<RuntimeProjectConfig>(&runtime_config)?;
     fs::write(&eupak_path, config_bytes)?;
     log::debug!("Exported scene config to {:?}", eupak_path);
 
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 2ada09a..903169b 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -17,14 +17,17 @@ use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::scene::SceneCommand;
 use eucalyptus_core::input::InputState;
 use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
-use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT};
-use eucalyptus_core::scene::loading::SCENE_LOADER;
+use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script};
+use eucalyptus_core::scene::loading::{SceneLoadResult, SCENE_LOADER};
 use eucalyptus_core::traits::registry::ComponentRegistry;
 use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, WorldPtr};
 use eucalyptus_core::command::COMMAND_BUFFER;
 use eucalyptus_core::scene::loading::IsSceneLoaded;
 use std::collections::HashMap;
+use std::env::current_exe;
 use std::path::PathBuf;
+use wgpu::SurfaceConfiguration;
+use winit::window::Fullscreen;
 use yakui_winit::YakuiWinit;
 use eucalyptus_core::physics::PhysicsState;
 use eucalyptus_core::rapier3d::prelude::*;
@@ -34,8 +37,9 @@ mod scene;
 mod input;
 mod command;
 
+#[cfg(feature = "debug")]
 fn find_jvm_library_path() -> PathBuf {
-    let proj = PROJECT.read();
+    let proj = eucalyptus_core::states::PROJECT.read();
     let project_path = if !proj.project_path.is_dir() {
         proj.project_path
             .parent()
@@ -44,7 +48,7 @@ fn find_jvm_library_path() -> PathBuf {
     } else {
         proj.project_path.clone()
     }
-    .join("build/libs");
+        .join("build/libs");
 
     let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
 
@@ -73,6 +77,34 @@ fn find_jvm_library_path() -> PathBuf {
         .expect("No suitable candidate for a JVM targeted play mode session available")
 }
 
+#[cfg(not(feature = "debug"))]
+fn find_jvm_library_path() -> PathBuf {
+    let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
+
+    for entry in std::fs::read_dir(current_exe().unwrap().parent().unwrap()).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().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)),
+                    Some((_, latest_time)) if modified > latest_time => {
+                        latest_jar = Some((path.clone(), modified));
+                    }
+                    _ => {}
+                }
+            }
+        }
+    }
+
+    latest_jar
+        .map(|(path, _)| path)
+        .expect("No suitable candidate for a JVM targeted play mode session available")
+}
 pub struct PlayMode {
     scene_command: SceneCommand,
     input_state: InputState,
@@ -80,6 +112,7 @@ pub struct PlayMode {
     world: Box<World>,
     component_registry: Arc<ComponentRegistry>,
     active_camera: Option<Entity>,
+    display_settings: DisplaySettings,
     
     // rendering
     light_cube_pipeline: Option<LightCubePipeline>,
@@ -160,6 +193,14 @@ impl PlayMode {
             collision_force_event_receiver: Some(cfe_r),
             event_collector,
             yakui_winit: None,
+            display_settings: DisplaySettings {
+                window_mode: WindowMode::Windowed,
+                maintain_aspect_ratio: true,
+                vsync: false,
+                last_window_mode: WindowMode::BorderlessFullscreen,
+                last_vsync: true,
+                last_size: (0, 0),
+            },
         };
 
         log::debug!("Created new play mode instance");
@@ -216,6 +257,8 @@ impl PlayMode {
     }
 
     /// Requests an asynchronous scene load, returning immediately and loading the scene in the background.
+    ///
+    /// It will not request the scene load if the currently rendered scene is the same as the requested scene.
     pub fn request_async_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) {
         log::debug!("Requested async scene load: {}", requested_scene.requested_scene);
         let scene_name = requested_scene.requested_scene.clone();
@@ -233,8 +276,18 @@ impl PlayMode {
             if let Some(id) = progress.id {
                 let mut loader = SCENE_LOADER.lock();
                 if let Some(entry) = loader.get_entry_mut(id) {
-                    if entry.status.is_none() {
-                        entry.status = self.world_loading_progress.as_ref().cloned();
+                    if let Some(scene) = &self.current_scene && scene == &self.scene_progress.as_ref().unwrap().requested_scene {
+                        log::debug!("Load scene async request cancelled because scene name is current");
+                        entry.result = SceneLoadResult::Error("Currently rendered scene name is the same as the requested scene".to_string());
+                        self.world_loading_progress = None;
+                        self.world_receiver = None;
+                        self.physics_receiver = None;
+                        self.scene_progress = None;
+                        return
+                    } else {
+                        if entry.status.is_none() {
+                            entry.status = self.world_loading_progress.as_ref().cloned();
+                        }
                     }
                 }
             }
@@ -284,6 +337,11 @@ 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: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) {
+        if let Some(scene) = &self.current_scene && scene == &requested_scene.requested_scene {
+            log::debug!("Immediate scene load request cancelled because scene name is current");
+            return
+        }
+
         let scene_name = requested_scene.requested_scene.clone();
         log::debug!("Immediate scene load requested: {}", scene_name);
 
@@ -368,4 +426,90 @@ impl PlayMode {
             self.current_scene = Some(scene_progress.requested_scene.clone());
         }
     }
+}
+
+pub struct DisplaySettings {
+    pub window_mode: WindowMode,
+    pub maintain_aspect_ratio: bool,
+    pub vsync: bool,
+    last_window_mode: WindowMode,
+    last_vsync: bool,
+    last_size: (u32, u32),
+}
+
+impl DisplaySettings {
+    pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        let window = graphics.window.clone();
+        let size = (
+            graphics.viewport_texture.size.width,
+            graphics.viewport_texture.size.height,
+        );
+
+        let needs_update = self.window_mode != self.last_window_mode
+            || self.vsync != self.last_vsync
+            || size != self.last_size;
+
+        if !needs_update {
+            return;
+        }
+
+        match self.window_mode {
+            WindowMode::Windowed => {
+                window.set_fullscreen(None);
+                window.set_maximized(false);
+            }
+            WindowMode::Maximized => {
+                window.set_fullscreen(None);
+                window.set_maximized(true);
+            }
+            WindowMode::Fullscreen => {
+                let monitor = window.current_monitor();
+                let fullscreen = monitor
+                    .as_ref()
+                    .and_then(|m| m.video_modes().next())
+                    .map(Fullscreen::Exclusive)
+                    .or_else(|| Some(Fullscreen::Borderless(monitor)));
+
+                window.set_fullscreen(fullscreen);
+                window.set_maximized(false);
+            }
+            WindowMode::BorderlessFullscreen => {
+                let monitor = window.current_monitor();
+                window.set_fullscreen(Some(Fullscreen::Borderless(monitor)));
+                window.set_maximized(false);
+            }
+        }
+
+        let config = SurfaceConfiguration {
+            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+            format: graphics.surface_format,
+            width: graphics.viewport_texture.size.width,
+            height: graphics.viewport_texture.size.height,
+            present_mode: if self.vsync {
+                wgpu::PresentMode::AutoVsync
+            } else {
+                wgpu::PresentMode::AutoNoVsync
+            },
+            alpha_mode: wgpu::CompositeAlphaMode::Auto,
+            view_formats: vec![],
+            desired_maximum_frame_latency: 2,
+        };
+
+        {
+            let mut cfg = graphics.surface_config.write();
+            *cfg = config;
+        }
+
+        self.last_window_mode = self.window_mode;
+        self.last_vsync = self.vsync;
+        self.last_size = size;
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum WindowMode {
+    Windowed,
+    Maximized,
+    Fullscreen,
+    BorderlessFullscreen,
 }
\ No newline at end of file
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 5a2d151..8002c8f 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -64,6 +64,12 @@ impl Scene for PlayMode {
             let _ = self.script_manager.physics_update_script(self.world.as_mut(), dt as f64);
         }
 
+        let world = self.world.iter().map(|e| (e.get::<&Label>().unwrap().to_string(), e.entity())).collect::<Vec<_>>();
+        log::info!("World contents [len={}]: ", world.len());
+        for (l, e) in world {
+            log::info!("{} -> {:?}", l, e);
+        }
+
         for kcc in self.world.query::<&mut KCC>().iter() {
             kcc.collisions.clear();
         }
@@ -242,6 +248,8 @@ impl Scene for PlayMode {
         graphics.future_queue.poll();
         self.poll(graphics.clone());
 
+        self.display_settings.update(graphics.clone());
+
         {
             if let Some(fps) = PROJECT.read().runtime_settings.target_fps.get() {
                 log_once::debug_once!("setting new fps for play mode session: {}", fps);
@@ -381,6 +389,40 @@ impl Scene for PlayMode {
         #[cfg(feature = "debug")]
         egui::TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| {
             egui::MenuBar::new().ui(ui, |ui| {
+                use crate::WindowMode;
+                ui.menu_button("Window", |ui| {
+                    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() {
+                            self.display_settings.window_mode = WindowMode::Windowed;
+                            ui.close();
+                        }
+
+                        let is_maximized = matches!(self.display_settings.window_mode, WindowMode::Maximized);
+                        if ui.selectable_label(is_maximized, "Maximized").clicked() {
+                            self.display_settings.window_mode = WindowMode::Maximized;
+                            ui.close();
+                        }
+
+                        let is_fullscreen = matches!(self.display_settings.window_mode, WindowMode::Fullscreen);
+                        if ui.selectable_label(is_fullscreen, "Fullscreen").clicked() {
+                            self.display_settings.window_mode = WindowMode::Fullscreen;
+                            ui.close();
+                        }
+
+                        let is_borderless = matches!(self.display_settings.window_mode, WindowMode::BorderlessFullscreen);
+                        if ui.selectable_label(is_borderless, "Borderless Fullscreen").clicked() {
+                            self.display_settings.window_mode = WindowMode::BorderlessFullscreen;
+                            ui.close();
+                        }
+                    });
+
+                    ui.separator();
+
+                    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| {
                     ui.group(|ui| {
                         ui.add_enabled_ui(true, |ui| {
@@ -403,7 +445,6 @@ impl Scene for PlayMode {
         CentralPanel::default().show(&graphics.get_egui_context(), |ui| {
             if let Some(p) = &self.scene_progress {
                 if !p.is_everything_loaded() && p.is_first_scene {
-                    // todo: change from label to "splashscreen"
                     ui.centered_and_justified(|ui| {
                         egui_extras::install_image_loaders(&graphics.get_egui_context());
                         ui.image(egui::include_image!("../../../resources/eucalyptus-editor.png"))
@@ -425,20 +466,19 @@ impl Scene for PlayMode {
                         self.has_initial_resize_done = true;
                     }
 
-                    // if !self.display_settings.maintain_aspect_ratio {
-                    //     cam.aspect = (available_size.x / available_size.y) as f64;
-                    // }
+                    if !self.display_settings.maintain_aspect_ratio {
+                        cam.aspect = (available_size.x / available_size.y) as f64;
+                    }
                     cam.update_view_proj();
                     cam.update(graphics.clone());
 
-                    let (display_width, display_height) = (available_size.x, available_size.y);
-                    // if self.display_settings.maintain_aspect_ratio {
-                    //     let width = available_size.x;
-                    //     let height = width / cam.aspect as f32;
-                    //     (width, height)
-                    // } else {
-                        
-                    // };
+                    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;
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index 295a6ac..9ed9e3e 100644
--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -6,6 +6,7 @@ import com.dropbear.input.InputState
 import com.dropbear.logging.Logger
 import com.dropbear.scene.SceneManager
 import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.UIInstructionSet
 
 internal var exceptionOnError: Boolean = false
 var lastErrorMessage: String? = null
@@ -75,18 +76,40 @@ class DropbearEngine(val native: NativeEngine) {
     fun callExceptionOnError(toggle: Boolean) {
     }
 
-    fun renderUI(uiInstructionSet: List<UIInstruction>) {
-        Logger.trace("instructions: $uiInstructionSet")
-        renderUI(instructions = uiInstructionSet)
+    /**
+     * Renders a set of UI instructions to be displayed onto the screen.
+     *
+     * This uses the rust crate `yakui` to power the UI. You can get a [UIInstructionSet]
+     * by either doing one of two ways:
+     *
+     * ## Method 1 (recommended)
+     * ```kt
+     * val instructions: UIInstructionSet = buildUI {
+     *      label("hello world!")
+     * }
+     * engine.renderUI(instructions)
+     * ```
+     *
+     * ## Method 2 (the non-dsl way)
+     * ```kt
+     * val builder = UIBuilder()
+     * builder.add(Text.label("hello world!").toInstruction())
+     * engine.renderUI(builder.build())
+     * ```
+     */
+    fun renderUI(uiInstructionSet: UIInstructionSet?) {
+        if (uiInstructionSet != null) {
+            renderUI(instructions = uiInstructionSet)
+        }
     }
 
     /**
      * Quits the currently running app or game elegantly.
      * 
-     * This function can have different behaviours depending on where it is ran. 
+     * This function can have different behaviours depending on where it is run.
      * - 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
+     * - euca-runner - 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() {
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
new file mode 100644
index 0000000..218ee65
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
@@ -0,0 +1,50 @@
+package com.dropbear.components.camera
+
+import com.dropbear.math.Vector3d
+import com.dropbear.physics.ColliderShape
+import com.dropbear.physics.Physics
+
+/**
+ * A springy camera that is used for any camera. It uses ray casting to check if the distance from the player to the
+ * camera is obstructed by a physics object, and if so set the distance to the last "safe" spot.
+ *
+ * Inspired by Godot's SpringyCameraController (other engines probably have their own, never used them before).
+ */
+class SpringyCameraController {
+    private var currentDistance: Double = 5.0
+    private val margin = 0.3
+    private val sphereRadius = 0.2
+
+    fun getSpringyPosition(
+        playerPos: Vector3d,
+        targetPos: Vector3d,
+        deltaTime: Double
+    ): Vector3d {
+        val vectorToCam = targetPos - playerPos
+        val maxDist = vectorToCam.length()
+        val dir = vectorToCam.normalize()
+
+        val hit = Physics.shapeCast(
+            origin = playerPos,
+            shape = ColliderShape.Sphere(sphereRadius.toFloat()),
+            direction = dir,
+            maxDistance = maxDist,
+            solid = false
+        )
+
+        val targetDist = if (hit != null) {
+            (hit.distance - margin).coerceAtLeast(0.1)
+        } else {
+            maxDist
+        }
+
+        if (targetDist < currentDistance) {
+            currentDistance = targetDist
+        } else {
+            val returnSpeed = 5.0
+            currentDistance += (targetDist - currentDistance) * (returnSpeed * deltaTime).coerceIn(0.0, 1.0)
+        }
+
+        return playerPos + (dir * currentDistance)
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
index f4f5eee..2e74526 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
@@ -1,14 +1,16 @@
 package com.dropbear.ui
 
+typealias UIInstructionSet = List<UIInstruction>
+
 class UIBuilder {
     val instructions: MutableList<UIInstruction> = mutableListOf()
     private var nextId = 0
     internal fun generateId(): Int = nextId++
 
-    fun build(): List<UIInstruction> = instructions.toList()
+    fun build(): UIInstructionSet = instructions.toList()
 }
 
-inline fun buildUI(block: UIBuilder.() -> Unit): List<UIInstruction> {
+inline fun buildUI(block: UIBuilder.() -> Unit): UIInstructionSet {
     return UIBuilder().apply(block).build()
 }