tirbofish/dropbear · diff
Merge pull request #47 from 4tkbytes/big-refactor
fix: remove a threadsafe world, instead make it singlethreaded and add async jobs for model loading.
Signature present but could not be verified.
Unverified
@@ -24,6 +24,8 @@ dropbear-engine/src/resources/textures/Autism.png .vscode docs +dropbear_future-queue/test.log + # gleam *.beam *.ez @@ -2,11 +2,11 @@ package.version = "0.1.2" package.edition = "2024" package.license-file = "LICENSE.md" -package.repository = "https://github.com/4tkbytes/dropbear-engine" +package.repository = "https://github.com/4tkbytes/dropbear" package.readme = "README.md" resolver = "3" -members = [ "dropbear-engine", "eucalyptus-core", "eucalyptus-editor"] +members = [ "dropbear-engine", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor"] # members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"] @@ -12,7 +12,7 @@ If you might have not realised, all the crates/projects names are after Australi - [redback-runtime](https://github.com/4tkbytes/redback-runtime) is the runtime used to load .eupak files and run the games loaded on them. ### Related Projects - +- [dropbear_future-queue](https://github.com/4tkbytes/dropbear/tree/main/dropbear_future-queue) is a handy library for dealing with async in a sync context - [model_to_image](https://github.com/4tkbytes/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` ## Build @@ -54,21 +54,14 @@ If you do not want to build it locally, you are able to download the latest acti ## Usage -~~Depsite it looking like a dependency for `eucalyptus`, it can serve as a framework too. Looking through the `docs.rs` will you find related documentation onhow to use it and for rendering your own projects.~~ - -dropbear cannot be used as a framework (yet), but is best compatible with the eucalyptus editor when making games. For -scripting, eucalyptus uses `typescript`. The main philosophy of scripting is to be able to have **type safety**, which can aid developers with their code, which is the reason as to why I didn't use `javascript` for scripting instead. - -The typescript used in the scripting is ran with the deno runtime. - -Documentation is hosted on GitHub Pages with typedoc. The website is at this link: [4tkbytes.github.io/dropbear][https://4tkbytes.github.io/dropbear/] +todo ## Compability -| | Windows | macOS | Linux | Web | Android | iOS | -|------------|---------|-------|-------|-----|---------|-----| -| eucalyptus | ✅ | ✅ | ✅ | ❌<sup>1</sup> | ❌<sup>1</sup> | ❌<sup>1</sup> | -| redback | ✅ | ✅ | ✅ | ❌<sup>2</sup> | ❌<sup>2</sup> | ❌ | +| | Windows | macOS | Linux | Web | Android | iOS | +|------------|---------|-------|-------|---------------|---------------|---------------| +| eucalyptus | ✅ | ✅ | ✅ | ❌<sup>1</sup> | ❌<sup>1</sup> | ❌<sup>1</sup> | +| redback | ✅ | ✅ | ✅ | ❌<sup>2</sup> | ❌<sup>2</sup> | ❌ | <sup>1</sup> Will never be implemented; not intended for that platform. @@ -9,6 +9,7 @@ repository.workspace = true readme.workspace = true [dependencies] +dropbear_future-queue = { path = "../dropbear_future-queue" } anyhow.workspace = true app_dirs2.workspace = true bytemuck.workspace = true @@ -32,8 +33,6 @@ parking_lot.workspace = true lazy_static.workspace = true gltf.workspace = true rayon.workspace = true -tokio.workspace = true -async-trait.workspace = true backtrace.workspace = true os_info.workspace = true rustc_version_runtime.workspace = true @@ -42,8 +41,8 @@ rustc_version_runtime.workspace = true rfd.workspace = true arboard.workspace = true -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.15" +# [target.'cfg(target_os = "android")'.dependencies] +# android_logger = "0.15" [dependencies.image] version = "0.25" @@ -1,3 +1,7 @@ +//! The attenuation values of light. +//! +//! See https://en.wikipedia.org/wiki/Attenuation + #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub struct Attenuation { pub range: f32, @@ -1,3 +1,5 @@ +//! Vertices and different buffers used for wgpu + #[repr(C)] #[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vertex { @@ -1,3 +1,5 @@ +//! Camera and components related to cameras. + use std::sync::Arc; use glam::{DMat4, DQuat, DVec3, Mat4}; @@ -8,6 +10,7 @@ use wgpu::{ use crate::graphics::SharedGraphicsContext; +/// Matrix that converts OpenGL (from [`glam`]) to [`wgpu`] values #[rustfmt::skip] pub const OPENGL_TO_WGPU_MATRIX: [[f64; 4]; 4] = [ [1.0, 0.0, 0.0, 0.0], @@ -16,65 +19,86 @@ pub const OPENGL_TO_WGPU_MATRIX: [[f64; 4]; 4] = [ [0.0, 0.0, 0.5, 1.0], ]; +/// The basic values of a Camera. #[derive(Default, Debug, Clone)] pub struct Camera { + /// The name of the camera pub label: String, + /// Eye of camera / Position pub eye: DVec3, + /// Target of camera / Looking at pub target: DVec3, + /// Up pub up: DVec3, + /// Aspect ratio pub aspect: f64, + /// FOV of camera pub fov_y: f64, + /// Near buffer? pub znear: f64, + /// Far buffer? pub zfar: f64, + /// Yaw (rotation) pub yaw: f64, + /// Pitch (rotation) pub pitch: f64, + /// Uniform/interface for Rust and the GPU pub uniform: CameraUniform, buffer: Option<Buffer>, layout: Option<BindGroupLayout>, bind_group: Option<BindGroup>, + /// Speed of the camera pub speed: f64, + /// Sensitivity of the mouse for the camera pub sensitivity: f64, + /// View matrix pub view_mat: DMat4, + /// Projection Matrix pub proj_mat: DMat4, } +/// A simple builder/struct that allows you to build a [`Camera`] +pub struct CameraBuilder { + pub eye: DVec3, + pub target: DVec3, + pub up: DVec3, + pub aspect: f64, + pub fov_y: f64, + pub znear: f64, + pub zfar: f64, + pub speed: f64, + pub sensitivity: f64, +} + impl Camera { /// Creates a new camera pub fn new( graphics: Arc<SharedGraphicsContext>, - eye: DVec3, - target: DVec3, - up: DVec3, - aspect: f64, - fov_y: f64, - znear: f64, - zfar: f64, - speed: f64, - sensitivity: f64, + builder: CameraBuilder, label: Option<&str>, ) -> Self { let uniform = CameraUniform::new(); let mut camera = Self { - eye, - target, - up, - aspect, - fov_y, - znear, - zfar, + eye: builder.eye, + target: builder.target, + up: builder.up, + aspect: builder.aspect, + fov_y: builder.fov_y, + znear: builder.znear, + zfar: builder.zfar, uniform, buffer: None, layout: None, bind_group: None, - speed, + speed: builder.speed, yaw: 0.0, pitch: 0.0, - sensitivity, + sensitivity: builder.sensitivity, label: if let Some(l) = label { l.to_string() } else { @@ -94,19 +118,21 @@ impl Camera { pub fn predetermined(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self { Self::new( graphics.clone(), - DVec3::new(0.0, 1.0, 2.0), - DVec3::new(0.0, 0.0, 0.0), - DVec3::Y, - (graphics.screen_size.0 / graphics.screen_size.1).into(), - 45.0, - 0.1, - 100.0, - 1.0, - 0.002, + CameraBuilder { + eye: DVec3::new(0.0, 1.0, 2.0), + target: DVec3::new(0.0, 0.0, 0.0), + up: DVec3::Y, + aspect: (graphics.screen_size.0 / graphics.screen_size.1).into(), + fov_y: 45.0, + znear: 0.1, + zfar: 100.0, + speed: 1.0, + sensitivity: 0.002, + }, label, ) } - + pub fn rotation(&self) -> DQuat { let yaw = DQuat::from_axis_angle(DVec3::Y, self.yaw); let pitch = DQuat::from_axis_angle(DVec3::X, self.pitch); @@ -133,6 +159,7 @@ impl Camera { self.eye } + /// Prints out the values of the camera. pub fn debug_camera_state(&self) { let camera = self; log::debug!("Camera state:"); @@ -154,11 +181,10 @@ impl Camera { self.znear, ); - self.view_mat = view.clone(); - self.proj_mat = proj.clone(); + self.view_mat = view; + self.proj_mat = proj; - let result = DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * proj * view; - result + DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * proj * view } pub fn create_bind_group_layout( @@ -198,7 +224,7 @@ impl Camera { pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) { self.update_view_proj(); graphics.queue.write_buffer( - &self.buffer.as_ref().unwrap(), + self.buffer.as_ref().unwrap(), 0, bytemuck::cast_slice(&[self.uniform]), ); @@ -87,7 +87,7 @@ pub struct LazyAdoptedEntity { impl LazyAdoptedEntity { /// Create a LazyAdoptedEntity from a file path (can be run on background thread) pub async fn from_file(path: &PathBuf, label: Option<&str>) -> anyhow::Result<Self> { - let buffer = tokio::fs::read(path).await?; + let buffer = std::fs::read(path)?; Self::from_memory(buffer, label).await } @@ -141,7 +141,7 @@ impl AdoptedEntity { label: Option<&str>, ) -> anyhow::Result<Self> { let path = path.as_ref().to_path_buf(); - let model = Model::load(graphics.clone(), &path, label.clone()).await?; + let model = Model::load(graphics.clone(), &path, label).await?; Ok(Self::adopt(graphics, model).await) } @@ -14,15 +14,15 @@ use wgpu::{ util::{BufferInitDescriptor, DeviceExt}, }; use winit::window::Window; - +use dropbear_future_queue::FutureQueue; use crate::{ State, egui_renderer::EguiRenderer, model::{self, Vertex}, }; -pub const NO_TEXTURE: &'static [u8] = include_bytes!("../../resources/no-texture.png"); -pub const NO_MODEL: &'static [u8] = include_bytes!("../../resources/error.glb"); +pub const NO_TEXTURE: &[u8] = include_bytes!("../../resources/no-texture.png"); +pub const NO_MODEL: &[u8] = include_bytes!("../../resources/error.glb"); pub struct RenderContext<'a> { pub shared: Arc<SharedGraphicsContext>, @@ -40,6 +40,7 @@ pub struct SharedGraphicsContext { pub diffuse_sampler: Arc<Sampler>, pub screen_size: (f32, f32), pub texture_id: Arc<TextureId>, + pub future_queue: Arc<FutureQueue>, } pub struct FrameGraphicsContext<'a> { @@ -101,6 +102,7 @@ impl<'a> RenderContext<'a> { })); Self { shared: Arc::new(SharedGraphicsContext { + future_queue: state.future_queue.clone(), device: state.device.clone(), queue: state.queue.clone(), instance: Arc::new(state.instance.clone()), @@ -193,7 +195,7 @@ impl<'a> RenderContext<'a> { .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.frame.view, + view: self.frame.view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(color), @@ -220,7 +222,7 @@ impl<'a> RenderContext<'a> { .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.frame.view, + view: self.frame.view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, @@ -400,7 +402,7 @@ impl Texture { }; let desc = TextureDescriptor { - label: label, + label, size, mip_level_count: 1, sample_count: 1, @@ -1,6 +1,6 @@ use gilrs::{Axis, EventType, Gilrs}; +use parking_lot::RwLock; use std::{ - cell::RefCell, collections::{HashMap, HashSet}, rc::Rc, }; @@ -8,9 +8,9 @@ use winit::{ dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode, }; -pub type KeyboardImpl = Rc<RefCell<dyn Keyboard>>; -pub type MouseImpl = Rc<RefCell<dyn Mouse>>; -pub type ControllerImpl = Rc<RefCell<dyn Controller>>; +pub type KeyboardImpl = Rc<RwLock<dyn Keyboard>>; +pub type MouseImpl = Rc<RwLock<dyn Mouse>>; +pub type ControllerImpl = Rc<RwLock<dyn Controller>>; pub trait Keyboard { fn key_down(&mut self, key: KeyCode, event_loop: &ActiveEventLoop); @@ -51,6 +51,12 @@ pub struct Manager { active_handlers: HashSet<String>, } +impl Default for Manager { + fn default() -> Self { + Self::new() + } +} + impl Manager { pub fn new() -> Self { Self { @@ -89,7 +95,7 @@ impl Manager { self.just_pressed_keys.insert(key); for (name, handler) in self.keyboard_handlers.iter_mut() { if self.active_handlers.contains(name) { - handler.borrow_mut().key_down(key, event_loop); + handler.write().key_down(key, event_loop); } } } @@ -99,7 +105,7 @@ impl Manager { self.just_released_keys.insert(key); for (name, handler) in self.keyboard_handlers.iter_mut() { if self.active_handlers.contains(name) { - handler.borrow_mut().key_up(key, event_loop); + handler.write().key_up(key, event_loop); } } } @@ -113,7 +119,7 @@ impl Manager { self.just_pressed_mouse_buttons.insert(button); for (name, handler) in self.mouse_handlers.iter_mut() { if self.active_handlers.contains(name) { - handler.borrow_mut().mouse_down(button); + handler.write().mouse_down(button); } } } @@ -123,7 +129,7 @@ impl Manager { self.just_released_mouse_buttons.insert(button); for (name, handler) in self.mouse_handlers.iter_mut() { if self.active_handlers.contains(name) { - handler.borrow_mut().mouse_up(button); + handler.write().mouse_up(button); } } } @@ -135,7 +141,7 @@ impl Manager { self.mouse_position = position; for (name, handler) in self.mouse_handlers.iter_mut() { if self.active_handlers.contains(name) { - handler.borrow_mut().mouse_move(position); + handler.write().mouse_move(position); } } } @@ -185,28 +191,28 @@ impl Manager { if self.active_handlers.contains(name) { match event.event { EventType::ButtonPressed(button, _) => { - handler.borrow_mut().button_down(button, event.id); + handler.write().button_down(button, event.id); } EventType::ButtonReleased(button, _) => { - handler.borrow_mut().button_up(button, event.id); + handler.write().button_up(button, event.id); } EventType::AxisChanged(Axis::LeftStickX, x, _) => { - handler.borrow_mut().left_stick_changed(x, 0.0, event.id); + handler.write().left_stick_changed(x, 0.0, event.id); } EventType::AxisChanged(Axis::LeftStickY, y, _) => { - handler.borrow_mut().left_stick_changed(0.0, y, event.id); + handler.write().left_stick_changed(0.0, y, event.id); } EventType::AxisChanged(Axis::RightStickX, x, _) => { - handler.borrow_mut().right_stick_changed(x, 0.0, event.id); + handler.write().right_stick_changed(x, 0.0, event.id); } EventType::AxisChanged(Axis::RightStickY, y, _) => { - handler.borrow_mut().right_stick_changed(0.0, y, event.id); + handler.write().right_stick_changed(0.0, y, event.id); } EventType::Connected => { - handler.borrow_mut().on_connect(event.id); + handler.write().on_connect(event.id); } EventType::Disconnected => { - handler.borrow_mut().on_disconnect(event.id); + handler.write().on_disconnect(event.id); } _ => {} } @@ -214,7 +220,7 @@ impl Manager { } } - pub fn poll_controllers(&mut self, gilrs: &mut gilrs::Gilrs) { + pub fn poll_controllers(&mut self, gilrs: &mut Gilrs) { while let Some(event) = gilrs.next_event() { self.handle_controller_event(event); } @@ -10,7 +10,7 @@ pub mod model; pub mod panic; pub mod resources; pub mod scene; -pub mod starter; +pub mod procedural; pub mod utils; use app_dirs2::{AppDataType, AppInfo}; @@ -29,8 +29,8 @@ use std::{ fmt::{self, Display, Formatter}, sync::Arc, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, - u32, }; +use bytemuck::Contiguous; use wgpu::{ BindGroupLayout, Device, Instance, Queue, Surface, SurfaceConfiguration, SurfaceError, TextureFormat, @@ -50,6 +50,8 @@ pub use gilrs; use log::LevelFilter; pub use wgpu; pub use winit; +pub use dropbear_future_queue as future; +use dropbear_future_queue::FutureQueue; /// The backend information, such as the device, queue, config, surface, renderer, window and more. pub struct State { @@ -64,23 +66,24 @@ pub struct State { pub instance: Instance, pub viewport_texture: Texture, pub texture_id: Arc<TextureId>, + pub future_queue: Arc<FutureQueue>, pub window: Arc<Window>, } impl State { /// Asynchronously initialised the state and sets up the backend and surface for wgpu to render to. - pub async fn new(window: Arc<Window>) -> anyhow::Result<Self> { + pub async fn new(window: Arc<Window>, future_queue: Arc<FutureQueue>) -> anyhow::Result<Self> { let size = window.inner_size(); // create backend - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + let instance = Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, // flags: wgpu::InstanceFlags::empty(), ..Default::default() }); - let surface = instance.create_surface(window.clone()).unwrap(); + let surface = instance.create_surface(window.clone())?; let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { @@ -125,7 +128,7 @@ Hardware: Driver Info: {} ======================================================= ", - info.backend.to_string(), + info.backend, os_info.architecture(), os_info.bitness(), os_info.codename(), @@ -148,7 +151,7 @@ Hardware: .find(|f| f.is_srgb()) .copied() .unwrap_or(TextureFormat::Rgba8Unorm); - let config = wgpu::SurfaceConfiguration { + let config = SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, width: size.width, @@ -215,6 +218,7 @@ Hardware: egui_renderer, viewport_texture, texture_id: Arc::new(texture_id), + future_queue }; Ok(result) @@ -245,7 +249,7 @@ Hardware: } /// Asynchronously renders the scene and the egui renderer. I don't know what else to say. - async fn render( + fn render( &mut self, scene_manager: &mut scene::Manager, previous_dt: f32, @@ -257,27 +261,27 @@ Hardware: let output = match self.surface.get_current_texture() { Ok(val) => val, - Err(e) => match e { + Err(e) => return match e { SurfaceError::Lost => { log_once::warn_once!("Surface lost, reconfiguring..."); self.surface.configure(&self.device, &self.config); - return Ok(()); + Ok(()) } SurfaceError::Outdated => { log_once::warn_once!("Surface outdated, reconfiguring..."); self.surface.configure(&self.device, &self.config); - return Ok(()); + Ok(()) } SurfaceError::Timeout => { log_once::warn_once!("Surface timeout, skipping frame"); - return Ok(()); + Ok(()) } SurfaceError::OutOfMemory => { - return Err(anyhow::anyhow!("Surface out of memory: {:?}", e)); + Err(anyhow::anyhow!("Surface out of memory: {:?}", e)) } SurfaceError::Other => { log_once::warn_once!("Surface error (Other): {:?}, skipping frame", e); - return Ok(()); + Ok(()) } }, }; @@ -303,9 +307,8 @@ Hardware: let mut graphics = graphics::RenderContext::from_state(self, viewport_view, &mut encoder); scene_manager - .update(previous_dt, &mut graphics, event_loop) - .await; - scene_manager.render(&mut graphics).await; + .update(previous_dt, &mut graphics, event_loop); + scene_manager.render(&mut graphics); self.egui_renderer.lock().end_frame_and_draw( &self.device, @@ -346,8 +349,7 @@ Hardware: pub fn get_current_time_as_ns() -> u128 { let now = SystemTime::now(); let duration_since_epoch = now.duration_since(UNIX_EPOCH).unwrap(); - let timestamp_ns = duration_since_epoch.as_nanos(); - timestamp_ns + duration_since_epoch.as_nanos() } /// A struct storing the information about the application/game that is using the engine. @@ -371,25 +373,27 @@ pub struct App { target_fps: u32, /// The library used for polling controllers, specifically the instance of that. gilrs: Gilrs, - // /// A task pool used for background async work - // runtime: Runtime, + /// A queue that polls through futures for asynchronous functions + /// + /// Winit doesn't use async, so this is the next best alternative. + future_queue: Arc<FutureQueue>, } impl App { /// Creates a new instance of the application. It only sets the default for the struct + the /// window config. - fn new(config: WindowConfiguration) -> Self { + fn new(config: WindowConfiguration, future_queue: Option<Arc<FutureQueue>>) -> Self { let result = Self { state: None, config: config.clone(), scene_manager: scene::Manager::new(), input_manager: input::Manager::new(), - delta_time: (1.0 / 60.0), + delta_time: 1.0 / 60.0, next_frame_time: None, target_fps: config.max_fps, // default settings for now gilrs: GilrsBuilder::new().build().unwrap(), - // runtime, + future_queue: future_queue.unwrap_or_else(|| Arc::new(FutureQueue::new())), }; log::debug!("Created new instance of app"); result @@ -397,7 +401,7 @@ impl App { /// A constant that lets you not have any fps count. /// It is just the max value of an unsigned 32 bit number lol. - pub const NO_FPS_CAP: u32 = u32::MAX; + pub const NO_FPS_CAP: u32 = u32::MAX_VALUE; /// Helper function that sets the target frames per second. Can be used mid game to increase FPS. pub fn set_target_fps(&mut self, fps: u32) { @@ -423,17 +427,17 @@ impl App { /// - config: The window configuration, such as the title, and window dimensions. /// - app_name: A string to the app name for debugging. /// - setup: A closure that can initialise the first scenes, such as a menu or the game itself. + /// /// It takes an input of a scene manager and an input manager, and expects you to return back the changed /// managers. - pub async fn run<F>(config: WindowConfiguration, app_name: &str, setup: F) -> anyhow::Result<()> + pub async fn run<F>(config: WindowConfiguration, app_name: &str, future_queue: Option<Arc<FutureQueue>>, setup: F) -> anyhow::Result<()> where F: FnOnce(scene::Manager, input::Manager) -> (scene::Manager, input::Manager), { let log_dir = app_dirs2::app_root(AppDataType::UserData, &config.app_info) .expect("Failed to get app data directory") .join("logs"); - tokio::fs::create_dir_all(&log_dir) - .await + std::fs::create_dir_all(&log_dir) .expect("Failed to create log dir"); let datetime_str = Local::now().format("%Y-%m-%d_%H-%M-%S"); @@ -521,14 +525,11 @@ impl App { } log::info!("dropbear-engine running..."); let ad = app_dirs2::get_app_root(AppDataType::UserData, &config.app_info); - match ad { - Ok(path) => {log::info!("App data is stored at {}", path.display())}, - Err(_) => {} - }; + if let Ok(path) = ad {log::info!("App data is stored at {}", path.display())}; log::debug!("Additional nerdy stuff: {:#?}", rustc_version_runtime::version_meta()); let event_loop = EventLoop::with_user_event().build()?; log::debug!("Created new event loop"); - let mut app = Box::new(App::new(config)); + let mut app = Box::new(App::new(config, future_queue)); log::debug!("Configured app with details: {}", app.config); log::debug!("Running through setup"); @@ -550,10 +551,14 @@ impl App { /// It is crucial to run with this macro instead of the latter is for debugging purposes (and to make life /// easier by not having to guess your package name if it changes). /// -/// See also the docs for a further run down on the parameters of how it is run: [`App::run()`] +/// # Parameters +/// * config - [`WindowConfiguration`]: The configuration/settings of the window. +/// * queue - [`Option<Throwable<FutureQueue>>`]: An optional value for a [`FutureQueue`] +/// * setup - [`FnOnce`]: A function that sets up all the scenes. It shouldn't be loaded +/// but instead be set as an [`Arc<Mutex<T>>>`]. macro_rules! run_app { - ($config:expr, $setup:expr) => { - $crate::App::run($config, env!("CARGO_PKG_NAME"), $setup) + ($config:expr, $queue:expr, $setup:expr) => { + $crate::App::run($config, env!("CARGO_PKG_NAME"), $queue, $setup) }; } @@ -576,7 +581,7 @@ impl ApplicationHandler for App { let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); - self.state = Some(block_on(State::new(window)).unwrap()); + self.state = Some(block_on(State::new(window, self.future_queue.clone())).unwrap()); if let Some(state) = &mut self.state { let size = state.window.inner_size(); @@ -609,6 +614,8 @@ impl ApplicationHandler for App { } WindowEvent::Resized(size) => state.resize(size.width, size.height), WindowEvent::RedrawRequested => { + self.future_queue.poll(); + let frame_start = Instant::now(); let active_handlers = self.scene_manager.get_active_input_handlers(); @@ -616,8 +623,7 @@ impl ApplicationHandler for App { self.input_manager.update(&mut self.gilrs); - let render_result = - block_on(state.render(&mut self.scene_manager, self.delta_time, event_loop)); + let render_result = state.render(&mut self.scene_manager, self.delta_time, event_loop); if let Err(e) = render_result { log::error!("Render failed: {:?}", e); @@ -640,6 +646,7 @@ impl ApplicationHandler for App { } state.window.request_redraw(); + self.future_queue.cleanup(); } WindowEvent::KeyboardInput { event: @@ -650,8 +657,8 @@ impl ApplicationHandler for App { }, .. } => { - if code == KeyCode::F11 && key_state.is_pressed() { - if let Some(state) = &self.state { + if code == KeyCode::F11 && key_state.is_pressed() + && let Some(state) = &self.state { match self.config.windowed_mode { WindowedModes::Windowed(_, _) => { if state.window.fullscreen().is_some() { @@ -686,7 +693,6 @@ impl ApplicationHandler for App { } } } - } self.input_manager .handle_key_input(code, key_state.is_pressed(), event_loop); } @@ -709,7 +715,7 @@ impl ApplicationHandler for App { /// The window configuration of the app/game. /// /// This struct is primitive but has purpose in the way that it sets the initial specs of the window. -/// Thats all it does. And it can also display. But thats about it. +/// That's all it does. And it can also display. But that's about it. #[derive(Debug, Clone)] pub struct WindowConfiguration { pub windowed_mode: WindowedModes, @@ -109,9 +109,9 @@ impl Display for LightType { } } -impl Into<u32> for LightType { - fn into(self) -> u32 { - match self { +impl From<LightType> for u32 { + fn from(val: LightType) -> Self { + match val { LightType::Directional => 0, LightType::Point => 1, LightType::Spot => 2, @@ -316,8 +316,8 @@ impl Light { label: Option<&str>, ) -> anyhow::Result<LazyLight> { let mut result = LazyLight { - light_component: light_component, - transform: transform, + light_component, + transform, label: label.map(|s| s.to_string()), cube_lazy_model: None, }; @@ -334,8 +334,8 @@ impl Light { pub async fn new( graphics: Arc<SharedGraphicsContext>, - light: &LightComponent, - transform: &Transform, + light: LightComponent, + transform: Transform, label: Option<&str>, ) -> Self { let forward = DVec3::new(0.0, 0.0, -1.0); @@ -357,14 +357,14 @@ impl Light { let cube_model = Arc::new(Model::load_from_memory( graphics.clone(), include_bytes!("../../resources/cube.glb").to_vec(), - label.clone(), + label, ) .await .unwrap()); - let label_str = label.clone().unwrap_or("Light").to_string(); + let label_str = label.unwrap_or("Light").to_string(); - let buffer = graphics.create_uniform(uniform, label.clone()); + let buffer = graphics.create_uniform(uniform, label); let layout = graphics .device @@ -379,7 +379,7 @@ impl Light { }, count: None, }], - label: label.clone(), + label, }); let bind_group = graphics @@ -390,7 +390,7 @@ impl Light { binding: 0, resource: buffer.as_entire_binding(), }], - label: label.clone(), + label, }); let instance = Instance::new( @@ -462,7 +462,7 @@ impl Light { } pub fn buffer(&self) -> &Buffer { - &self.buffer.as_ref().unwrap() + self.buffer.as_ref().unwrap() } } @@ -474,6 +474,12 @@ pub struct LightManager { light_array_layout: Option<BindGroupLayout>, } +impl Default for LightManager { + fn default() -> Self { + Self::new() + } +} + impl LightManager { pub fn new() -> Self { log::info!("Initialised lighting"); @@ -543,7 +549,7 @@ impl LightManager { } if light_component.enabled && light_index < MAX_LIGHTS { - light_array.lights[light_index] = light.uniform().clone(); + light_array.lights[light_index] = *light.uniform(); light_index += 1; } } @@ -576,13 +582,13 @@ impl LightManager { ) { use crate::graphics::Shader; - let shader = Shader::new(graphics.clone(), shader_contents, label.clone()); + let shader = Shader::new(graphics.clone(), shader_contents, label); let pipeline = Self::create_render_pipeline_for_lighting( graphics, &shader, vec![camera.layout(), self.light_array_layout.as_ref().unwrap()], - label.clone(), + label, ); self.pipeline = Some(pipeline); @@ -16,7 +16,7 @@ use std::{mem, ops::Range, path::PathBuf}; use std::hash::{DefaultHasher, Hash, Hasher}; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt}; -pub const GREY_TEXTURE_BYTES: &'static [u8] = include_bytes!("../../resources/grey.png"); +pub const GREY_TEXTURE_BYTES: &[u8] = include_bytes!("../../resources/grey.png"); lazy_static! { pub static ref MODEL_CACHE: Mutex<HashMap<String, Model>> = Mutex::new(HashMap::new()); @@ -87,7 +87,7 @@ pub trait LazyType { /// This can be ran after the initial thread loading is completed. /// # Parameters /// * [`Arc<SharedGraphicsContext>`] - The graphics context. It can be shared between threads - /// if required. + /// if required. /// /// # Returns /// * [`Self::T`] - The data type of the item @@ -366,7 +366,7 @@ impl Model { return Ok(cached_model.clone()); } - println!( + log::trace!( "========== Benchmarking speed of loading {:?} ==========", label ); @@ -416,18 +416,18 @@ impl Model { let load_start = Instant::now(); let diffuse_image = image::load_from_memory(&image_data).unwrap(); - println!("Loading image to memory: {:?}", load_start.elapsed()); + log::trace!("Loading image to memory: {:?}", load_start.elapsed()); let rgba_start = Instant::now(); let diffuse_rgba = diffuse_image.to_rgba8(); - println!( + log::trace!( "Converting diffuse image to rgba8 took {:?}", rgba_start.elapsed() ); let dimensions = diffuse_image.dimensions(); - println!( + log::trace!( "Parallel processing of material '{}' took: {:?}", material_name, material_start.elapsed() @@ -437,7 +437,7 @@ impl Model { }) .collect(); - println!( + log::trace!( "Total parallel image processing took: {:?}", parallel_start.elapsed() ); @@ -456,7 +456,7 @@ impl Model { bind_group, }); - println!("Time to create GPU texture: {:?}", start.elapsed()); + log::trace!("Time to create GPU texture: {:?}", start.elapsed()); } for mesh in gltf.meshes() { @@ -542,10 +542,10 @@ impl Model { }; MODEL_CACHE.lock().insert(cache_key, model.clone()); - println!("==================== DONE ===================="); + log::trace!("==================== DONE ===================="); log::debug!("Model cached from memory: {:?}", label); log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label); - println!("=============================================="); + log::trace!("=============================================="); Ok(model) } @@ -559,12 +559,16 @@ impl Model { let path_str = path.to_string_lossy().to_string(); + log::debug!("Checking if model exists in cache"); if let Some(cached_model) = MODEL_CACHE.lock().get(&path_str) { log::debug!("Model loaded from cache: {:?}", path_str); return Ok(cached_model.clone()); } + log::debug!("Model does not exist in cache, loading memory..."); - let buffer = tokio::fs::read(path).await?; + log::debug!("Path of model: {}", path.display()); + + let buffer = std::fs::read(path)?; let model = Self::load_from_memory(graphics, buffer, label).await?; MODEL_CACHE.lock().insert(path_str, model.clone()); @@ -0,0 +1,4 @@ +//! Starter objects like planes and primitive objects are that made during runtime with +//! vertices rather than from a model. + +pub mod plane; @@ -0,0 +1,272 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; +use crate::entity::AdoptedEntity; +use crate::graphics::{SharedGraphicsContext, Texture}; +use crate::model::{LazyType, Material, Mesh, Model, ModelId, ModelVertex, MODEL_CACHE}; +use crate::utils::{ResourceReference, ResourceReferenceType}; +use futures::executor::block_on; +use image::GenericImageView; +/// A straight plane (and some components). Thats it. +/// +/// Inspiration taken from `https://github.com/4tkbytes/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, +/// my old game engine made in C sharp, where this is the plane "algorithm". +use std::sync::Arc; +use wgpu::{AddressMode, util::DeviceExt}; + +/// Lazily creates a new Plane. This can only be accessed through the Default trait (which you shouldn't use), +/// or the [`PlaneBuilder::lazy_build()`] (also taken from [`PlaneBuilder::new()`]). +#[derive(Default)] +pub struct LazyPlaneBuilder { + rgba_data: Vec<u8>, + dimensions: (u32, u32), + width: f32, + height: f32, + tiles_x: u32, + tiles_z: u32, + label: Option<String>, +} + +impl LazyType for LazyPlaneBuilder { + type T = AdoptedEntity; + + fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T> { + let mut hasher = DefaultHasher::new(); + + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + for z in 0..=1 { + for x in 0..=1 { + let position = [ + (x as f32 - 0.5) * self.width, + 0.0, + (z as f32 - 0.5) * self.height, + ]; + let normal = [0.0, 1.0, 0.0]; + let tex_coords = [ + x as f32 * self.tiles_x as f32, + z as f32 * self.tiles_z as f32, + ]; + + let _ = position.iter().map(|v| (*v as i32).hash(&mut hasher)); + let _ = normal.iter().map(|v| (*v as i32).hash(&mut hasher)); + let _ = tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher)); + + vertices.push(ModelVertex { + position, + tex_coords, + normal, + }); + } + } + + indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); + indices.hash(&mut hasher); + + let vertex_buffer = graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Vertex Buffer", self.label.as_deref())), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Index Buffer", self.label.as_deref())), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); + + let mesh = Mesh { + name: "plane".to_string(), + vertex_buffer, + index_buffer, + num_elements: indices.len() as u32, + material: 0, + }; + + let diffuse_texture = Texture::new_with_sampler_with_rgba_buffer( + graphics.clone(), + &self.rgba_data, + self.dimensions, + AddressMode::Repeat, + ); + let bind_group = diffuse_texture.bind_group().clone(); + let material = Material { + name: "plane_material".to_string(), + diffuse_texture, + bind_group, + }; + + let model = Model { + label: self.label.as_deref().unwrap_or("Plane").to_string(), + path: ResourceReference::from_reference(ResourceReferenceType::Plane), + meshes: vec![mesh], + materials: vec![material], + id: ModelId(hasher.finish()) + }; + + Ok(block_on(AdoptedEntity::adopt(graphics, model))) + } +} + +/// Creates a plane in the form of an AdoptedEntity. +pub struct PlaneBuilder { + width: f32, + height: f32, + tiles_x: u32, + tiles_z: u32, +} + +impl Default for PlaneBuilder { + fn default() -> Self { + Self::new() + } +} + +impl PlaneBuilder { + pub fn new() -> Self { + Self { + width: 10.0, + height: 10.0, + tiles_x: 0, + tiles_z: 0, + } + } + + pub fn with_size(mut self, width: f32, height: f32) -> Self { + self.width = width; + self.height = height; + self + } + + pub fn with_tiles(mut self, tiles_x: u32, tiles_z: u32) -> Self { + self.tiles_x = tiles_x; + self.tiles_z = tiles_z; + self + } + + pub async fn lazy_build( + mut self, + texture_bytes: &[u8], + label: Option<&str>, + ) -> anyhow::Result<LazyPlaneBuilder> { + if self.tiles_x == 0 && self.tiles_z == 0 { + self.tiles_x = self.width as u32; + self.tiles_z = self.height as u32; + } + + let img = image::load_from_memory(texture_bytes)?; + let rgba = img.to_rgba8(); + let dimensions = img.dimensions(); + + Ok(LazyPlaneBuilder { + rgba_data: rgba.into_raw(), + dimensions, + width: self.width, + height: self.height, + tiles_x: self.tiles_x, + tiles_z: self.tiles_z, + label: label.map(|s| s.to_string()), + }) + } + + pub async fn build( + mut self, + graphics: Arc<SharedGraphicsContext>, + texture_bytes: &[u8], + label: Option<&str>, + ) -> anyhow::Result<AdoptedEntity> { + let label = if let Some(label) = label {label.to_string()} else {format!("{}*{}_tx{}xtz{}_plane", self.width, self.height, self.tiles_x, self.tiles_z)}; + let mut hasher = DefaultHasher::new(); + if self.tiles_x == 0 && self.tiles_z == 0 { + self.tiles_x = self.width as u32; + self.tiles_z = self.height as u32; + } + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + for z in 0..=1 { + for x in 0..=1 { + let position = [ + (x as f32 - 0.5) * self.width, + 0.0, + (z as f32 - 0.5) * self.height, + ]; + let normal = [0.0, 1.0, 0.0]; + let tex_coords = [ + x as f32 * self.tiles_x as f32, + z as f32 * self.tiles_z as f32, + ]; + let _ = position.iter().map(|v| (*v as i32).hash(&mut hasher)); + let _ = normal.iter().map(|v| (*v as i32).hash(&mut hasher)); + let _ = tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher)); + + vertices.push(ModelVertex { + position, + tex_coords, + normal, + }); + } + } + + indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); + indices.hash(&mut hasher); + + let hash = hasher.finish(); + + let model = if let Some(cached_model) = MODEL_CACHE.lock().get(&label.clone()) { + log::debug!("Model loaded from cache: {:?}", label.clone()); + Some(cached_model.clone()) + } else {None}; + + let vertex_buffer = graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Vertex Buffer", label.clone())), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Index Buffer", label)), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); + + let mesh = Mesh { + name: "plane".to_string(), + vertex_buffer, + index_buffer, + num_elements: indices.len() as u32, + material: 0, + }; + + let diffuse_texture = + Texture::new_with_sampler(graphics.clone(), texture_bytes, AddressMode::Repeat); + let bind_group = diffuse_texture.bind_group().clone(); + let material = Material { + name: "plane_material".to_string(), + diffuse_texture, + bind_group, + }; + + let model = if let Some(m) = model { + m + } else { + let m = Model { + label: label.to_string(), + path: ResourceReference::from_reference(ResourceReferenceType::Plane), + meshes: vec![mesh], + materials: vec![material], + id: ModelId(hash) + }; + MODEL_CACHE.lock().insert(label, m.clone()); + m + }; + + + Ok(AdoptedEntity::adopt(graphics, model).await) + } +} @@ -1,13 +1,17 @@ +// yeah this lint is required to be allowed because winit doesn't use async +// logically, it wouldn't be possible to deadlock +#![allow(clippy::await_holding_lock)] + use winit::event_loop::ActiveEventLoop; use crate::input; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{collections::HashMap, rc::Rc}; +use parking_lot::RwLock; -#[async_trait::async_trait] pub trait Scene { - async fn load<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>); - async fn update<'a>(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext<'a>); - async fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>); + fn load(&mut self, graphics: &mut crate::graphics::RenderContext); + fn update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext); + fn render(&mut self, graphics: &mut crate::graphics::RenderContext); fn exit(&mut self, event_loop: &ActiveEventLoop); /// By far a mess of a trait however it works. /// @@ -33,7 +37,7 @@ impl Default for SceneCommand { } } -pub type SceneImpl = Rc<RefCell<dyn Scene>>; +pub type SceneImpl = Rc<RwLock<dyn Scene>>; #[derive(Clone)] pub struct Manager { @@ -43,6 +47,12 @@ pub struct Manager { scene_input_map: HashMap<String, String>, } +impl Default for Manager { + fn default() -> Self { + Self::new() + } +} + impl Manager { pub fn new() -> Self { Self { @@ -62,7 +72,7 @@ impl Manager { } } - pub fn add(&mut self, name: &str, scene: Rc<RefCell<dyn Scene>>) { + pub fn add(&mut self, name: &str, scene: SceneImpl) { self.scenes.insert(name.to_string(), scene); } @@ -71,7 +81,7 @@ impl Manager { .insert(scene_name.to_string(), input_name.to_string()); } - pub async fn update<'a>( + pub fn update<'a>( &mut self, dt: f32, graphics: &mut crate::graphics::RenderContext<'a>, @@ -79,30 +89,33 @@ impl Manager { ) { // transition scene if let Some(next_scene_name) = self.next_scene.take() { - if let Some(current_scene_name) = &self.current_scene { - if let Some(scene) = self.scenes.get_mut(current_scene_name) { - scene.borrow_mut().exit(event_loop); + if let Some(current_scene_name) = &self.current_scene + && let Some(scene) = self.scenes.get_mut(current_scene_name) { + { scene.write().exit(event_loop); } } - } if let Some(scene) = self.scenes.get_mut(&next_scene_name) { - scene.borrow_mut().load(graphics).await; + { scene.write().load(graphics); } } self.current_scene = Some(next_scene_name); } // update scene - if let Some(scene_name) = &self.current_scene { - if let Some(scene) = self.scenes.get_mut(scene_name) { - scene.borrow_mut().update(dt, graphics).await; - let command = scene.borrow_mut().run_command(); + if let Some(scene_name) = &self.current_scene + && let Some(scene) = self.scenes.get_mut(scene_name) { + { + scene.write() + .update(dt, graphics); + } + let command = scene.write().run_command(); match command { SceneCommand::SwitchScene(target) => { if let Some(current) = &self.current_scene { if current == &target { // reload the scene if let Some(scene) = self.scenes.get_mut(current) { - scene.borrow_mut().exit(event_loop); - scene.borrow_mut().load(graphics).await; + scene.write().exit(event_loop); + scene.write().load(graphics); + log::debug!("Reloaded scene: {}", current); } } else { @@ -120,14 +133,13 @@ impl Manager { SceneCommand::DebugMessage(msg) => log::debug!("{}", msg), } } - } } - pub async fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>) { - if let Some(scene_name) = &self.current_scene { - if let Some(scene) = self.scenes.get_mut(scene_name) { - scene.borrow_mut().render(graphics).await; - } + pub fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>) { + if let Some(scene_name) = &self.current_scene + && let Some(scene) = self.scenes.get_mut(scene_name) + { + scene.write().render(graphics) } } @@ -167,7 +179,7 @@ pub fn add_scene_with_input< >( scene_manager: &mut Manager, input_manager: &mut input::Manager, - scene: Rc<RefCell<S>>, + scene: Rc<RwLock<S>>, scene_name: &str, ) { scene_manager.add(scene_name, scene.clone()); @@ -1,3 +0,0 @@ -//! Starter objects like planes, players and more - -pub mod plane; @@ -1,265 +0,0 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; -use crate::entity::AdoptedEntity; -use crate::graphics::{SharedGraphicsContext, Texture}; -use crate::model::{Material, Mesh, Model, ModelId, ModelVertex, MODEL_CACHE}; -use crate::utils::{ResourceReference, ResourceReferenceType}; -use futures::executor::block_on; -use image::GenericImageView; -/// A straight plane (and some components). Thats it. -/// -/// Inspiration taken from `https://github.com/4tkbytes/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, -/// my old game engine made in C sharp, where this is the plane "algorithm". -use std::sync::Arc; -use wgpu::{AddressMode, util::DeviceExt}; - -/// Lazily creates a new Plane. This can only be accessed through the Default trait (which you shouldn't use), -/// or the [`PlaneBuilder::lazy_build()`] (also taken from [`PlaneBuilder::new()`]). -#[derive(Default)] -pub struct LazyPlaneBuilder { - rgba_data: Vec<u8>, - dimensions: (u32, u32), - width: f32, - height: f32, - tiles_x: u32, - tiles_z: u32, - label: Option<String>, -} - -impl LazyPlaneBuilder { - pub fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<AdoptedEntity> { - let mut hasher = DefaultHasher::new(); - - let mut vertices = Vec::new(); - let mut indices = Vec::new(); - - for z in 0..=1 { - for x in 0..=1 { - let position = [ - (x as f32 - 0.5) * self.width, - 0.0, - (z as f32 - 0.5) * self.height, - ]; - let normal = [0.0, 1.0, 0.0]; - let tex_coords = [ - x as f32 * self.tiles_x as f32, - z as f32 * self.tiles_z as f32, - ]; - - let _ = position.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = normal.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher)); - - vertices.push(ModelVertex { - position, - tex_coords, - normal, - }); - } - } - - indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); - indices.hash(&mut hasher); - - let vertex_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", self.label.as_deref())), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - let index_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Index Buffer", self.label.as_deref())), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); - - let mesh = Mesh { - name: "plane".to_string(), - vertex_buffer, - index_buffer, - num_elements: indices.len() as u32, - material: 0, - }; - - let diffuse_texture = Texture::new_with_sampler_with_rgba_buffer( - graphics.clone(), - &self.rgba_data, - self.dimensions, - AddressMode::Repeat, - ); - let bind_group = diffuse_texture.bind_group().clone(); - let material = Material { - name: "plane_material".to_string(), - diffuse_texture, - bind_group, - }; - - let model = Model { - label: self.label.as_deref().unwrap_or("Plane").to_string(), - path: ResourceReference::from_reference(ResourceReferenceType::Plane), - meshes: vec![mesh], - materials: vec![material], - id: ModelId(hasher.finish()) - }; - - Ok(block_on(AdoptedEntity::adopt(graphics, model))) - } -} - -/// Creates a plane in the form of an AdoptedEntity. -pub struct PlaneBuilder { - width: f32, - height: f32, - tiles_x: u32, - tiles_z: u32, -} - -impl PlaneBuilder { - pub fn new() -> Self { - Self { - width: 10.0, - height: 10.0, - tiles_x: 0, - tiles_z: 0, - } - } - - pub fn with_size(mut self, width: f32, height: f32) -> Self { - self.width = width; - self.height = height; - self - } - - pub fn with_tiles(mut self, tiles_x: u32, tiles_z: u32) -> Self { - self.tiles_x = tiles_x; - self.tiles_z = tiles_z; - self - } - - pub async fn lazy_build( - mut self, - texture_bytes: &[u8], - label: Option<&str>, - ) -> anyhow::Result<LazyPlaneBuilder> { - if self.tiles_x == 0 && self.tiles_z == 0 { - self.tiles_x = self.width as u32; - self.tiles_z = self.height as u32; - } - - let img = image::load_from_memory(texture_bytes)?; - let rgba = img.to_rgba8(); - let dimensions = img.dimensions(); - - Ok(LazyPlaneBuilder { - rgba_data: rgba.into_raw(), - dimensions, - width: self.width, - height: self.height, - tiles_x: self.tiles_x, - tiles_z: self.tiles_z, - label: label.map(|s| s.to_string()), - }) - } - - pub async fn build( - mut self, - graphics: Arc<SharedGraphicsContext>, - texture_bytes: &[u8], - label: Option<&str>, - ) -> anyhow::Result<AdoptedEntity> { - let label = if let Some(label) = label {label.to_string()} else {format!("{}*{}_tx{}xtz{}_plane", self.width, self.height, self.tiles_x, self.tiles_z)}; - let mut hasher = DefaultHasher::new(); - if self.tiles_x == 0 && self.tiles_z == 0 { - self.tiles_x = self.width as u32; - self.tiles_z = self.height as u32; - } - let mut vertices = Vec::new(); - let mut indices = Vec::new(); - - for z in 0..=1 { - for x in 0..=1 { - let position = [ - (x as f32 - 0.5) * self.width, - 0.0, - (z as f32 - 0.5) * self.height, - ]; - let normal = [0.0, 1.0, 0.0]; - let tex_coords = [ - x as f32 * self.tiles_x as f32, - z as f32 * self.tiles_z as f32, - ]; - let _ = position.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = normal.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher)); - - vertices.push(ModelVertex { - position, - tex_coords, - normal, - }); - } - } - - indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); - indices.hash(&mut hasher); - - let hash = hasher.finish(); - - let model = if let Some(cached_model) = MODEL_CACHE.lock().get(&label.clone()) { - log::debug!("Model loaded from cache: {:?}", label.clone()); - Some(cached_model.clone()) - } else {None}; - - let vertex_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", label.clone())), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - let index_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Index Buffer", label)), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); - - let mesh = Mesh { - name: "plane".to_string(), - vertex_buffer, - index_buffer, - num_elements: indices.len() as u32, - material: 0, - }; - - let diffuse_texture = - Texture::new_with_sampler(graphics.clone(), texture_bytes, AddressMode::Repeat); - let bind_group = diffuse_texture.bind_group().clone(); - let material = Material { - name: "plane_material".to_string(), - diffuse_texture, - bind_group, - }; - - let model = if model.is_none() { - let m = Model { - label: label.to_string(), - path: ResourceReference::from_reference(ResourceReferenceType::Plane), - meshes: vec![mesh], - materials: vec![material], - id: ModelId(hash) - }; - MODEL_CACHE.lock().insert(label, m.clone()); - m - } else { - // safe to do - model.unwrap() - }; - - - Ok(AdoptedEntity::adopt(graphics, model).await) - } -} @@ -31,7 +31,7 @@ pub enum ResourceReferenceType { /// A simple plane. Some of the types in [`ResourceReferenceType`] can be simple, just as a signal /// to load that entity as a plane or another type. /// - /// In specifics, the plane (as from [`crate::starter::plane::PlaneBuilder`]) is a model that + /// In specifics, the plane (as from [`crate::procedural::plane::PlaneBuilder`]) is a model that /// has meshes and a textured material, but is created "in house" (during runtime instead of loaded). Plane, } @@ -50,12 +50,11 @@ impl Default for ResourceReferenceType { /// /// The resource reference will be `models/cube.obj`. /// -/// - If ran in the editor, it translates to -/// `/home/tk/project/resources/models/cube.obj`. +/// - If ran in the editor, it translates to `/home/tk/project/resources/models/cube.obj`. /// /// - In the runtime (with redback-runtime), it -/// translates to `/home/tk/Downloads/Maze/resources/models/cube.obj` -/// _(assuming the executable is at `/home/tk/Downloads/Maze/maze_runner.exe`)_. +/// translates to `/home/tk/Downloads/Maze/resources/models/cube.obj` +/// _(assuming the executable is at `/home/tk/Downloads/Maze/maze_runner.exe`)_. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ResourceReference { pub ref_type: ResourceReferenceType, @@ -104,8 +103,8 @@ impl ResourceReference { let components: Vec<_> = path.components().collect(); for (i, component) in components.iter().enumerate() { - if let std::path::Component::Normal(name) = component { - if *name == "resources" { + if let std::path::Component::Normal(name) = component + && *name == "resources" { let remaining_components = &components[i + 1..]; if remaining_components.is_empty() { anyhow::bail!("Unable to locate any remaining components"); @@ -124,7 +123,6 @@ impl ResourceReference { ref_type: ResourceReferenceType::File(resource_path), }); } - } } anyhow::bail!("Nothing here") @@ -184,11 +182,10 @@ impl ResourceReference { anyhow::bail!("Cannot resolve bytes") } ResourceReferenceType::File(path) => { - if let Ok(exe_path) = self.to_executable_path() { - if exe_path.exists() { + if let Ok(exe_path) = self.to_executable_path() + && exe_path.exists() { return Ok(exe_path); } - } Ok(std::env::current_dir()? .join("resources") @@ -0,0 +1,18 @@ +[package] +name = "dropbear_future-queue" + +version.workspace = true +edition.workspace = true +repository.workspace = true +license = "MIT" +readme = "README.md" +description = "A queue for polling futures in a synchronous context" + +[dependencies] +ahash = "0.8" +parking_lot.workspace = true +tokio = { version = "1", features = ["rt", "sync", "time", "rt-multi-thread"] } + +[dev-dependencies] +tokio = { version = "1", features = ["rt", "sync", "time", "rt-multi-thread", "macros"] } +tokio-test = "0.4" @@ -0,0 +1,31 @@ +# dropbear-futurequeue + +A helper queue for polling futures in single threaded systems such as in winit. + +## Example + +```rust +// create new queue +let queue = FutureQueue::new(); + +// create a new handle to keep for reference +let handle = queue.push(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + 67 + 41 +}); + +// check initial status +assert!(matches!(queue.get_status(&handle), Some(FutureStatus::NotPolled))); + +// execute the futures +queue.poll(); + +// wait for the task to do its job (this can be simulated with an update loop) +tokio::time::sleep(tokio::time::Duration::from_millis(20)).await; + +// check the result +if let Some(result) = queue.exchange_as::<i32>(&handle) { + println!("67 + 41 = {}", result); + assert_eq!(result, 108); +} +``` @@ -0,0 +1,417 @@ +//! Enabling multithreading for functions and apps that are purely single threaded. +//! +//! This was originally a module in my [dropbear](https://github.com/4tkbytes/dropbear) game engine, +//! however I thought there were barely any libraries that had future queuing. It takes inspiration +//! from Unity and how they handle their events. +//! +//! # Example +//! ```rust +//! use dropbear_future_queue::{FutureQueue, FutureStatus}; +//! +//! # tokio_test::block_on(async { +//! // create new queue +//! let queue = FutureQueue::new(); +//! +//! // create a new handle to keep for reference +//! let handle = queue.push(async move { +//! tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; +//! 67 + 41 +//! }); +//! +//! // Check initial status +//! assert!(matches!(queue.get_status(&handle), Some(FutureStatus::NotPolled))); +//! +//! // execute the futures +//! queue.poll(); +//! +//! // Wait a bit for completion and check result +//! tokio::time::sleep(tokio::time::Duration::from_millis(20)).await; +//! +//! if let Some(result) = queue.exchange_as::<i32>(&handle) { +//! println!("67 + 41 = {}", result); +//! assert_eq!(result, 108); +//! } +//! # }); +//! ``` + +use std::any::Any; +use std::cell::RefCell; +use std::collections::VecDeque; +use std::pin::Pin; +use std::sync::Arc; +use ahash::{HashMap, HashMapExt}; +use parking_lot::Mutex; +use tokio::sync::oneshot; +use std::future::Future; +use std::rc::Rc; + +/// A type used for a future. +/// +/// It must include a [`Send`] trait to be usable for the [`FutureQueue`] +pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>; +/// A clonable generic result. It can/preferred to be downcasted to your specific type. +pub type AnyResult = Arc<dyn Any + Send + Sync>; +/// Internal function: A result receiver +type ResultReceiver = oneshot::Receiver<AnyResult>; +/// A type for storing the queue. It uses a [`VecDeque`] to store [`FutureHandle`]'s and [`BoxFuture`] +pub type FutureStorage = Arc<Mutex<VecDeque<(FutureHandle, BoxFuture<()>)>>>; +/// A type recommended to be used by [`FutureQueue`] to allow being thrown around in your app +pub type Throwable<T> = Rc<RefCell<T>>; + +/// A status showing the future, used by the [`ResultReceiver`] and [`ResultSender`] +#[derive(Clone, Debug)] +pub enum FutureStatus { + NotPolled, + CurrentlyPolling, + Completed, +} + +/// A handle to the future task +#[derive(Default, Copy, Clone, Eq, Hash, PartialEq, Debug)] +pub struct FutureHandle { + pub id: u64, +} + +/// Internal storage per handle — separate from FutureHandle +struct HandleEntry { + receiver: Option<ResultReceiver>, + status: FutureStatus, + cached_result: Option<AnyResult>, +} + +/// A queue for polling futures. It is stored in here until [`FutureQueue::poll`] is run. +pub struct FutureQueue { + /// The queue for the futures. + queued: FutureStorage, + /// A place to store all handle data + handle_registry: Arc<Mutex<HashMap<FutureHandle, HandleEntry>>>, + /// Next id to be processed + next_id: Arc<Mutex<u64>>, +} + +impl FutureQueue { + /// Creates a new [`Arc<FutureQueue>`]. + pub fn new() -> Self { + Self { + queued: Arc::new(Mutex::new(VecDeque::new())), + handle_registry: Arc::new(Mutex::new(HashMap::new())), + next_id: Arc::new(Mutex::new(0)), + } + } + + /// Pushes a future to the FutureQueue. It will sit and wait + /// to be processed until [`FutureQueue::poll`] is called. + pub fn push<F, T>(&self, future: F) -> FutureHandle + where + F: Future<Output = T> + Send + 'static, + T: Send + Sync + 'static, + { + let mut next_id = self.next_id.lock(); + let id = *next_id; + *next_id += 1; + + let id = FutureHandle { id }; + + let (sender, receiver) = oneshot::channel::<AnyResult>(); + + let entry = HandleEntry { + receiver: Some(receiver), + status: FutureStatus::NotPolled, + cached_result: None, + }; + + self.handle_registry.lock().insert(id, entry); + + let wrapped_future: Pin<Box<dyn Future<Output = ()> + Send>> = Box::pin(async move { + log("Starting future execution"); + let result = future.await; + let boxed_result: AnyResult = Arc::new(result); + log("Future completed, sending result"); + + let _ = sender.send(boxed_result); + + // Don't update status here - let the exchange method handle it + log("Result sent via channel"); + }); + + self.queued.lock().push_back((id, wrapped_future)); + + id + } + + /// Polls all the futures in the future queue and resolves the handles. + /// + /// This function spawns a new async thread for each item inside the thread and + /// sends updates to the Handle's receiver. + pub fn poll(&self) { + let mut queue = self.queued.lock(); + log("Locked queue for polling"); + + if queue.is_empty() { + log("Queue is empty, nothing to poll"); + return; + } + + let mut futures_to_spawn = Vec::new(); + + while let Some((id, future)) = queue.pop_front() { + log(format!("Processing future with id: {:?}", id)); + + { + let mut registry = self.handle_registry.lock(); + if let Some(entry) = registry.get_mut(&id) { + entry.status = FutureStatus::CurrentlyPolling; + log("Updated status to CurrentlyPolling"); + } + } + + futures_to_spawn.push(future); + } + + drop(queue); + + for future in futures_to_spawn { + log("Spawning future with tokio"); + tokio::spawn(future); + } + } + + /// Exchanges the future for the result. + /// + /// When the handle is not successful, it will return nothing. When the handle is successful, + /// it will return the result. The result is cached and can be retrieved multiple times. + pub fn exchange(&self, handle: &FutureHandle) -> Option<AnyResult> { + let mut registry = self.handle_registry.lock(); + if let Some(entry) = registry.get_mut(handle) { + match &entry.status { + FutureStatus::Completed => { + log("FutureStatus::Completed - returning cached result"); + entry.cached_result.clone() + } + _ => { + log("Future not completed yet, checking receiver"); + if let Some(receiver) = entry.receiver.as_mut() { + match receiver.try_recv() { + Ok(result) => { + log("Received result from channel"); + entry.status = FutureStatus::Completed; + entry.cached_result = Some(result.clone()); + entry.receiver = None; // Remove receiver as it's no longer needed + Some(result) + } + Err(oneshot::error::TryRecvError::Empty) => { + log("Channel is empty - future still running"); + None + } + Err(oneshot::error::TryRecvError::Closed) => { + log("Channel is closed - future may have panicked"); + None + } + } + } else { + log("No receiver available"); + None + } + } + } + } else { + log("Handle not found in registry"); + None + } + } + + /// Exchanges the future for the result, taking ownership and consuming the cached result. + /// + /// When the handle is not successful, it will return nothing. When the handle is successful, + /// it will return the result and remove it from the cache, allowing Arc::try_unwrap to succeed. + /// This method can only be called once per completed future. + pub fn exchange_owned(&self, handle: &FutureHandle) -> Option<AnyResult> { + let mut registry = self.handle_registry.lock(); + if let Some(entry) = registry.get_mut(handle) { + match &entry.status { + FutureStatus::Completed => { + log("FutureStatus::Completed - taking ownership of cached result"); + entry.cached_result.take() + } + _ => { + log("Future not completed yet, checking receiver"); + if let Some(receiver) = entry.receiver.as_mut() { + match receiver.try_recv() { + Ok(result) => { + log("Received result from channel"); + entry.status = FutureStatus::Completed; + entry.receiver = None; // Remove receiver as it's no longer needed + Some(result) + } + Err(oneshot::error::TryRecvError::Empty) => { + log("Channel is empty - future still running"); + None + } + Err(oneshot::error::TryRecvError::Closed) => { + log("Channel is closed - future may have panicked"); + None + } + } + } else { + log("No receiver available"); + None + } + } + } + } else { + log("Handle not found in registry"); + None + } + } + + /// Exchanges the handle and safely downcasts it into a specific type. + pub fn exchange_as<T: Any + Send + Sync + 'static>(&self, handle: &FutureHandle) -> Option<T> { + self.exchange(handle)? + .downcast::<T>() + .ok() + .and_then(|arc| Arc::try_unwrap(arc).ok()) + } + + /// Exchanges the handle taking ownership and safely downcasts it into a specific type. + /// This method consumes the cached result, allowing Arc::try_unwrap to succeed. + pub fn exchange_owned_as<T: Any + Send + Sync + 'static>(&self, handle: &FutureHandle) -> Option<T> { + self.exchange_owned(handle)? + .downcast::<T>() + .ok() + .and_then(|arc| Arc::try_unwrap(arc).ok()) + } + + /// Get status of a handle + pub fn get_status(&self, handle: &FutureHandle) -> Option<FutureStatus> { + let registry = self.handle_registry.lock(); + registry.get(handle).map(|entry| entry.status.clone()) + } + + /// Cleans up any completed handles and removes them from the registry. + /// + /// You can do this manually, however this is typically done at the end of the frame. + pub fn cleanup(&self) { + let mut registry = self.handle_registry.lock(); + let completed_ids: Vec<FutureHandle> = registry + .iter() + .filter_map(|(&id, entry)| { + matches!(entry.status, FutureStatus::Completed).then_some(id) + }) + .collect(); + + for id in completed_ids { + registry.remove(&id); + } + } +} + + +/// Internal function for logging to a file for tests (when stdout is not available). +/// +/// Only logs if the [`LOG_TO_FILE`] constant is set to true. +#[cfg(test)] +fn log(msg: impl ToString) { + use std::io::Write; + + let mut file = std::fs::OpenOptions::new().append(true).create(true).open("test.log").unwrap(); + file.write_all(format!("{}\n", msg.to_string()).as_bytes()).unwrap(); +} + +#[cfg(not(test))] +fn log(_msg: impl ToString) { + +} + +impl Default for FutureQueue { + fn default() -> Self { + Self::new() + } +} + +#[test] +fn test_future_queue() { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let queue = FutureQueue::new(); + log("Created new queue"); + + let handle = queue.push(async move { + log("Inside the pushed future - starting work"); + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + log("Inside the pushed future - work completed"); + 67 + 41 + }); + log("Created new handle"); + + queue.poll(); + log("Initial poll completed"); + + let mut attempts = 0; + let max_attempts = 100; + let start_time = std::time::Instant::now(); + + loop { + attempts += 1; + log(format!("Attempt {}: Checking for result", attempts)); + log(format!("Time since start: {} ms", start_time.elapsed().as_millis())); + + if let Some(result) = queue.exchange(&handle) { + let result = result.downcast::<i32>().unwrap(); + log(format!("Success! 67 + 41 = {}", result)); + assert_eq!(*result, 108); + break; + } + + if attempts >= max_attempts { + log("Max attempts reached - test failed"); + panic!("Future never completed"); + } + + tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; + } + + log("Test completed successfully"); + }); +} + +// #[tokio::test] +#[test] +fn test_exchange_owned_as() { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let queue = FutureQueue::new(); + + let handle = queue.push(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + 67 + 41 + }); + + queue.poll(); + + let mut attempts = 0; + let max_attempts = 100; + + loop { + attempts += 1; + + if let Some(result) = queue.exchange_owned_as::<i32>(&handle) { + assert_eq!(result, 108); + break; + } + + if attempts >= max_attempts { + panic!("Future never completed"); + } + + tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; + } + + assert!(queue.exchange_owned_as::<i32>(&handle).is_none()); + }); +} @@ -23,7 +23,7 @@ parking_lot.workspace = true ron.workspace = true serde.workspace = true winit.workspace = true -zip.workspace = true +# zip.workspace = true tokio.workspace = true boa_engine.workspace = true rayon.workspace = true @@ -31,8 +31,8 @@ rayon.workspace = true [features] editor = [] -[target.'cfg(not(target_os = "android"))'.dependencies] -rfd.workspace = true +# [target.'cfg(not(target_os = "android"))'.dependencies] +# rfd.workspace = true [build-dependencies] anyhow = "1.0" @@ -12,6 +12,12 @@ pub struct CameraComponent { pub camera_type: CameraType, } +impl Default for CameraComponent { + fn default() -> Self { + Self::new() + } +} + impl CameraComponent { pub fn new() -> Self { Self { @@ -35,6 +41,7 @@ impl CameraComponent { pub struct PlayerCamera; impl PlayerCamera { + #[allow(clippy::new_ret_no_self)] pub fn new() -> CameraComponent { CameraComponent { camera_type: CameraType::Player, @@ -70,6 +77,7 @@ impl PlayerCamera { pub struct DebugCamera; impl DebugCamera { + #[allow(clippy::new_ret_no_self)] pub fn new() -> CameraComponent { CameraComponent { camera_type: CameraType::Debug, @@ -5,3 +5,4 @@ pub mod logging; pub mod scripting; pub mod states; pub mod utils; +pub mod spawn; @@ -4,36 +4,28 @@ use boa_engine::property::PropertyKey; use boa_engine::{Context, JsString, JsValue, Source}; use dropbear_engine::entity::{AdoptedEntity, Transform}; use hecs::{Entity, World}; -use parking_lot::RwLock; use std::path::PathBuf; use std::str::FromStr; -use std::sync::Arc; use std::{collections::HashMap, fs}; -pub const TEMPLATE_SCRIPT: &'static str = include_str!("../../resources/template.ts"); - -pub enum ScriptAction { - AttachScript { - script_path: PathBuf, - script_name: String, - }, - CreateAndAttachScript { - script_path: PathBuf, - script_name: String, - }, - RemoveScript, - EditScript, -} +pub const TEMPLATE_SCRIPT: &str = include_str!("../../resources/template.ts"); #[derive(Clone)] pub struct DropbearScriptingAPIContext { pub current_entity: Option<Entity>, - current_world: Option<Arc<RwLock<World>>>, + // fyi: im pretty sure this is safe because I can just null with [`Option::None`] + current_world: Option<*const World>, pub current_input: Option<InputState>, pub persistent_data: HashMap<String, Value>, pub frame_data: HashMap<String, Value>, } +impl Default for DropbearScriptingAPIContext { + fn default() -> Self { + Self::new() + } +} + impl DropbearScriptingAPIContext { pub fn new() -> Self { Self { @@ -45,9 +37,9 @@ impl DropbearScriptingAPIContext { } } - pub fn set_context(&mut self, entity: Entity, world: Arc<RwLock<World>>, input: &InputState) { + pub fn set_context(&mut self, entity: Entity, world: &mut World, input: &InputState) { self.current_entity = Some(entity); - self.current_world = Some(world); + self.current_world = Some(world as *mut World); self.current_input = Some(input.clone()); } @@ -120,7 +112,7 @@ impl ScriptManager { &mut self, entity_id: hecs::Entity, script_name: &str, - world: Arc<RwLock<World>>, + world: &mut World, input_state: &InputState, ) -> anyhow::Result<()> { log_once::debug_once!("init_entity_script: {} for {:?}", script_name, entity_id); @@ -157,7 +149,7 @@ impl ScriptManager { } self.script_context.clear_context(); - return Ok(()); + Ok(()) } else { Err(anyhow::anyhow!("Script '{}' not found", script_name)) } @@ -167,7 +159,7 @@ impl ScriptManager { &mut self, entity_id: hecs::Entity, script_name: &str, - world: Arc<RwLock<World>>, + world: &mut World, input_state: &InputState, dt: f32, ) -> anyhow::Result<()> { @@ -175,7 +167,7 @@ impl ScriptManager { if let Some(module) = self.compiled_scripts.get(script_name).cloned() { self.script_context - .set_context(entity_id, world.clone(), input_state); + .set_context(entity_id, world, input_state); let mut context = Context::default(); self.expose(&mut context); @@ -300,14 +292,14 @@ pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { } pub fn convert_entity_to_group( - world: Arc<RwLock<World>>, + world: &mut World, entity_id: hecs::Entity, ) -> anyhow::Result<EntityNode> { - if let Ok(mut query) = world.read().query_one::<(&AdoptedEntity, &Transform)>(entity_id) { + if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { if let Some((adopted, _transform)) = query.get() { let entity_name = adopted.model.label.clone(); - let script_node = if let Ok(script) = world.read().get::<&ScriptComponent>(entity_id) { + let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { Some(EntityNode::Script { name: script.name.clone(), path: script.path.clone(), @@ -339,12 +331,12 @@ pub fn convert_entity_to_group( } pub fn attach_script_to_entity( - world: Arc<RwLock<World>>, + world: &mut World, entity_id: hecs::Entity, script_component: ScriptComponent, ) -> anyhow::Result<()> { { - if let Err(e) = world.write().insert_one(entity_id, script_component) { + if let Err(e) = world.insert_one(entity_id, script_component) { return Err(anyhow::anyhow!("Failed to attach script to entity: {}", e)); } } @@ -352,3 +344,16 @@ pub fn attach_script_to_entity( log::info!("Successfully attached script to entity {:?}", entity_id); Ok(()) } + +pub enum ScriptAction { + AttachScript { + script_path: PathBuf, + script_name: String, + }, + CreateAndAttachScript { + script_path: PathBuf, + script_name: String, + }, + RemoveScript, + EditScript, +} @@ -0,0 +1,46 @@ +use std::sync::{Arc, LazyLock}; +use parking_lot::Mutex; +use dropbear_engine::entity::{Transform}; +use dropbear_engine::future::{FutureHandle, FutureQueue}; +use dropbear_engine::graphics::SharedGraphicsContext; +use dropbear_engine::utils::ResourceReference; +use crate::states::ModelProperties; + +/// All spawns that are waiting to be spawned in. +pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = + LazyLock::new(|| Mutex::new(Vec::new())); + +/// A spawn that's waiting to be added into the world. +#[derive(Clone, Debug)] +pub struct PendingSpawn { + /// A [`ResourceReference`] to the asset + pub asset_path: ResourceReference, + /// The name/label of the asset + pub asset_name: String, + /// The [`Transform`] properties (position) + pub transform: Transform, + /// The properties of a model, as specified in [`ModelProperties`] + pub properties: ModelProperties, + /// An optional future handle to an object. + /// + /// If one is specified, it is assumed that the returned object is an [`AdoptedEntity`](dropbear_engine::entity::AdoptedEntity). + /// + /// If one is NOT specified, it will be created based off the information provided. It is **recommended** to set it to [`None`]. + pub handle: Option<FutureHandle>, +} + +/// Extension trait for checking the editor (or anything else) for any spawns +/// that are waiting to be polled and added +pub trait PendingSpawnController { + /// Checks up on the spawn list and spawns them into the world after it has been + /// asynchronously loaded. + /// + /// This is expected to be run on the update loop. + fn check_up(&mut self, graphics: Arc<SharedGraphicsContext>, queue: Arc<FutureQueue>) -> anyhow::Result<()>; +} + +/// Helper function to spawn a [`PendingSpawn`] +pub fn push_pending_spawn(spawn: PendingSpawn) { + log::debug!("Pushing spawn"); + PENDING_SPAWNS.lock().push(spawn); +} @@ -2,12 +2,12 @@ use crate::camera::DebugCamera; use crate::camera::{CameraComponent, CameraFollowTarget, CameraType}; use crate::utils::PROTO_TEXTURE; use chrono::Utc; -use dropbear_engine::camera::Camera; +use dropbear_engine::camera::{Camera, CameraBuilder}; use dropbear_engine::entity::{AdoptedEntity, Transform}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::model::Model; -use dropbear_engine::starter::plane::PlaneBuilder; +use dropbear_engine::procedural::plane::PlaneBuilder; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use egui_dock_fork::DockState; use glam::DVec3; @@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::UnboundedSender; use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{fmt, fs}; use rayon::prelude::*; @@ -52,24 +52,13 @@ pub struct ProjectConfig { impl ProjectConfig { /// Creates a new instance of the ProjectConfig. This function is typically used when creating /// a new project, with it creating new defaults for everything. - pub fn new(project_name: String, project_path: &PathBuf) -> Self { + pub fn new(project_name: String, project_path: impl AsRef<Path>) -> Self { let date_created = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); let date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); - // #[cfg(not(feature = "editor"))] - // { - // let mut result = Self { - // project_name, - // project_path: project_path.to_path_buf(), - // date_created, - // date_last_accessed, - // }; - // let _ = result.load_config_to_memory(); // TODO: Deal with later... - // result - // } let mut result = Self { project_name, - project_path: project_path.to_path_buf(), + project_path: project_path.as_ref().to_path_buf(), date_created, date_last_accessed, editor_settings: Default::default(), @@ -84,15 +73,14 @@ impl ProjectConfig { /// /// # Parameters /// * path - The root **folder** of the project. - - pub fn write_to(&mut self, path: &PathBuf) -> anyhow::Result<()> { + pub fn write_to(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> { self.load_config_to_memory()?; self.date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); // self.assets = Assets::walk(path); let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let config_path = path.join(format!("{}.eucp", self.project_name.clone().to_lowercase())); - self.project_path = path.to_path_buf(); + let config_path = path.as_ref().join(format!("{}.eucp", self.project_name.clone().to_lowercase())); + self.project_path = path.as_ref().to_path_buf(); fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; Ok(()) @@ -103,10 +91,10 @@ impl ProjectConfig { /// /// # Parameters /// * path - The root config **file** for the project - pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { - let ron_str = fs::read_to_string(path)?; - let mut config: ProjectConfig = ron::de::from_str(&ron_str.as_str())?; - config.project_path = path.parent().unwrap().to_path_buf(); + pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { + let ron_str = fs::read_to_string(path.as_ref())?; + let mut config: ProjectConfig = ron::de::from_str(ron_str.as_str())?; + config.project_path = path.as_ref().parent().unwrap().to_path_buf(); log::info!("Loaded project!"); log::debug!("Loaded config info"); log::debug!("Updating with new content"); @@ -222,7 +210,7 @@ impl ProjectConfig { /// # Parameters /// * path - The root folder of the project pub fn write_to_all(&mut self) -> anyhow::Result<()> { - let path = PathBuf::from(self.project_path.clone()); + let path = self.project_path.clone(); { let resources_config = RESOURCES.read(); @@ -307,15 +295,15 @@ impl ResourceConfig { /// # Parameters /// - path: The root **folder** of the project - pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> { - let resource_dir = path.join("resources"); + pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> { + let resource_dir = path.as_ref().join("resources"); let updated_config = ResourceConfig { path: resource_dir.clone(), - nodes: collect_nodes(&resource_dir, path, vec!["thumbnails"].as_slice()), + nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["thumbnails"].as_slice()), }; let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let config_path = path.join("resources").join("resources.eucc"); + let config_path = path.as_ref().join("resources").join("resources.eucc"); fs::create_dir_all(config_path.parent().unwrap())?; fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; Ok(()) @@ -334,8 +322,8 @@ impl ResourceConfig { /// # Parameters /// - path: The location to the **resources.eucc** file - pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { - let config_path = path.join("resources").join("resources.eucc"); + pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { + let config_path = path.as_ref().join("resources").join("resources.eucc"); let ron_str = fs::read_to_string(&config_path)?; let config: ResourceConfig = ron::de::from_str(&ron_str) .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; @@ -360,16 +348,16 @@ impl SourceConfig { /// # Parameters /// - path: The root **folder** of the project - pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> { - let resource_dir = path.join("src"); + pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> { + let resource_dir = path.as_ref().join("src"); let updated_config = SourceConfig { path: resource_dir.clone(), - nodes: collect_nodes(&resource_dir, path, vec!["scripts"].as_slice()), + nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["scripts"].as_slice()), }; let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?; - let config_path = path.join("src").join("source.eucc"); + let config_path = path.as_ref().join("src").join("source.eucc"); fs::create_dir_all(config_path.parent().unwrap())?; fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; Ok(()) @@ -377,8 +365,8 @@ impl SourceConfig { /// # Parameters /// - path: The location to the **source.eucc** file - pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { - let config_path = path.join("src").join("source.eucc"); + pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { + let config_path = path.as_ref().join("src").join("source.eucc"); let ron_str = fs::read_to_string(&config_path)?; let config: SourceConfig = ron::de::from_str(&ron_str) .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; @@ -386,7 +374,7 @@ impl SourceConfig { } } -fn collect_nodes(dir: &PathBuf, project_path: &PathBuf, exclude_list: &[&str]) -> Vec<Node> { +fn collect_nodes(dir: impl AsRef<Path>, project_path: impl AsRef<Path>, exclude_list: &[&str]) -> Vec<Node> { let mut nodes = Vec::new(); if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { @@ -397,13 +385,13 @@ fn collect_nodes(dir: &PathBuf, project_path: &PathBuf, exclude_list: &[&str]) - .to_string_lossy() .to_string(); - if entry_path.is_dir() && exclude_list.iter().any(|ex| &ex.to_string() == &name) { + if entry_path.is_dir() && exclude_list.iter().any(|ex| ex.to_string() == *name) { log::debug!("Skipped past folder {:?}", name); continue; } if entry_path.is_dir() { - let folder_nodes = collect_nodes(&entry_path, project_path, exclude_list); + let folder_nodes = collect_nodes(&entry_path, project_path.as_ref(), exclude_list); nodes.push(Node::Folder(Folder { name, path: entry_path.clone(), @@ -426,9 +414,15 @@ fn collect_nodes(dir: &PathBuf, project_path: &PathBuf, exclude_list: &[&str]) - Some(ResourceType::Unknown) }; + // Store relative path from the project root instead of absolute path + let relative_path = entry_path + .strip_prefix(project_path.as_ref()) + .unwrap_or(&entry_path) + .to_path_buf(); + nodes.push(Node::File(File { name, - path: entry_path.clone(), + path: relative_path, resource_type, })); } @@ -606,16 +600,8 @@ impl CameraConfig { far: camera.zfar as f32, speed: component.speed as f32, sensitivity: component.sensitivity as f32, - follow_target_entity_label: if let Some(target) = follow_target { - Some(target.follow_target.clone()) - } else { - None - }, - follow_offset: if let Some(target) = follow_target { - Some(target.offset.to_array()) - } else { - None - }, + follow_target_entity_label: follow_target.map(|target| target.follow_target.clone()), + follow_offset: follow_target.map(|target| target.offset.to_array()), } } } @@ -683,10 +669,10 @@ pub struct SceneConfig { impl SceneConfig { /// Creates a new instance of the scene config - pub fn new(scene_name: String, path: PathBuf) -> Self { + pub fn new(scene_name: String, path: impl AsRef<Path>) -> Self { Self { scene_name, - path, + path: path.as_ref().to_path_buf(), entities: Vec::new(), cameras: Vec::new(), lights: Vec::new(), @@ -694,11 +680,11 @@ impl SceneConfig { } /// Write the scene config to a .eucs file - pub fn write_to(&self, project_path: &PathBuf) -> anyhow::Result<()> { + pub fn write_to(&self, project_path: impl AsRef<Path>) -> anyhow::Result<()> { let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let scenes_dir = project_path.join("scenes"); + let scenes_dir = project_path.as_ref().join("scenes"); fs::create_dir_all(&scenes_dir)?; let config_path = scenes_dir.join(format!("{}.eucs", self.scene_name)); @@ -707,18 +693,18 @@ impl SceneConfig { } /// Read a scene config from a .eucs file - pub fn read_from(scene_path: &PathBuf) -> anyhow::Result<Self> { - let ron_str = fs::read_to_string(scene_path)?; + pub fn read_from(scene_path: impl AsRef<Path>) -> anyhow::Result<Self> { + let ron_str = fs::read_to_string(scene_path.as_ref())?; let mut config: SceneConfig = ron::de::from_str(&ron_str) .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; - config.path = scene_path.clone(); + config.path = scene_path.as_ref().to_path_buf(); Ok(config) } pub async fn load_into_world( &self, - world: Arc<RwLock<hecs::World>>, + world: &mut hecs::World, graphics: Arc<SharedGraphicsContext>, progress_sender: Option<UnboundedSender<WorldLoadingStatus>> ) -> anyhow::Result<hecs::Entity> { @@ -730,9 +716,9 @@ impl SceneConfig { log::info!( "Loading scene [{}], clearing world with {} entities", self.scene_name, - world.read().len() + world.len() ); - { world.write().clear(); } + { world.clear(); } #[allow(unused_variables)] let project_config = if cfg!(feature = "editor") { @@ -743,7 +729,7 @@ impl SceneConfig { PathBuf::new() }; - log::info!("World cleared, now has {} entities", world.read().len()); + log::info!("World cleared, now has {} entities", world.len()); let entity_configs: Vec<(usize, SceneEntity)> = { let cloned = self.entities.clone(); @@ -795,9 +781,9 @@ impl SceneConfig { name: script_config.name.clone(), path: script_config.path.clone(), }; - { world.write().spawn((adopted, transform, script, entity_config.properties.clone())); } + { world.spawn((adopted, transform, script, entity_config.properties.clone())); } } else { - { world.write().spawn((adopted, transform, entity_config.properties.clone())); } + { world.spawn((adopted, transform, entity_config.properties.clone())); } } Ok(()) @@ -821,9 +807,9 @@ impl SceneConfig { name: script_config.name.clone(), path: script_config.path.clone(), }; - { world.write().spawn((adopted, transform, script, entity_config.properties.clone())); } + { world.spawn((adopted, transform, script, entity_config.properties.clone())); } } else { - { world.write().spawn((adopted, transform, entity_config.properties.clone())); } + { world.spawn((adopted, transform, entity_config.properties.clone())); } } Ok(()) @@ -885,9 +871,9 @@ impl SceneConfig { name: script_config.name.clone(), path: script_config.path.clone(), }; - { world.write().spawn((plane, transform, script, entity_config.properties.clone())); } + { world.spawn((plane, transform, script, entity_config.properties.clone())); } } else { - { world.write().spawn((plane, transform, entity_config.properties.clone())); } + { world.spawn((plane, transform, entity_config.properties.clone())); } } Ok(()) @@ -911,13 +897,13 @@ impl SceneConfig { let light = Light::new( graphics.clone(), - &light_config.light_component, - &light_config.transform, + light_config.light_component.clone(), + light_config.transform.clone(), Some(&light_config.label), ) .await; { - world.write().spawn(( + world.spawn(( light_config.light_component.clone(), light_config.transform, light, @@ -939,15 +925,17 @@ impl SceneConfig { let camera = Camera::new( graphics.clone(), - DVec3::from_array(camera_config.position), - DVec3::from_array(camera_config.target), - DVec3::from_array(camera_config.up), - camera_config.aspect, - camera_config.fov as f64, - camera_config.near as f64, - camera_config.far as f64, - camera_config.speed as f64, - camera_config.sensitivity as f64, + CameraBuilder { + eye: DVec3::from_array(camera_config.position), + target: DVec3::from_array(camera_config.target), + up: DVec3::from_array(camera_config.up), + aspect: camera_config.aspect, + fov_y: camera_config.fov as f64, + znear: camera_config.near as f64, + zfar: camera_config.far as f64, + speed: camera_config.speed as f64, + sensitivity: camera_config.sensitivity as f64, + }, Some(&camera_config.label), ); @@ -966,30 +954,37 @@ impl SceneConfig { follow_target: target_label.clone(), offset: DVec3::from_array(*offset), }; - { world.write().spawn((camera, component, follow_target)); } + { world.spawn((camera, component, follow_target)); } } else { - { world.write().spawn((camera, component)); } + { world.spawn((camera, component)); } } } - if world.read() - .query::<(&LightComponent, &Light)>() - .iter() - .next() - .is_none() { - log::info!("No lights in scene, spawning default light"); - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingLight { index: 0, name: String::from("Default Light"), total: 1 }); + let mut is_none = false; + if world + .query::<(&LightComponent, &Light)>() + .iter() + .next() + .is_none() + { + log::info!("No lights in scene, spawning default light"); + is_none = true; } - let comp = LightComponent::directional(glam::DVec3::ONE, 1.0); - let trans = Transform { - position: glam::DVec3::new(2.0, 4.0, 2.0), - ..Default::default() - }; - let light = Light::new(graphics.clone(), &comp, &trans, Some("Default Light")).await; + + if is_none { + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingLight { index: 0, name: String::from("Default Light"), total: 1 }); + } + let comp = LightComponent::directional(glam::DVec3::ONE, 1.0); + let trans = Transform { + position: glam::DVec3::new(2.0, 4.0, 2.0), + ..Default::default() + }; + let light = Light::new(graphics.clone(), comp.clone(), trans, Some("Default Light")).await; - world.write().spawn((comp, trans, light, ModelProperties::default())); + { world.spawn((comp, trans, light, ModelProperties::default())); } + } } log::info!( @@ -1001,36 +996,40 @@ impl SceneConfig { #[cfg(feature = "editor")] { // Editor mode - look for debug camera, create one if none exists - let debug_camera = world.read() - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Debug) { - Some(entity) - } else { - None - } - }); + let debug_camera = { + world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Debug) { + Some(entity) + } else { + None + } + }) + }; - if let Some(camera_entity) = debug_camera { - log::info!("Using existing debug camera for editor"); - Ok(camera_entity) - } else { - log::info!("No debug camera found, creating viewport camera for editor"); - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingCamera { index: 0, name: String::from("Viewport Camera"), total: 1 }); + { + if let Some(camera_entity) = debug_camera { + log::info!("Using existing debug camera for editor"); + Ok(camera_entity) + } else { + log::info!("No debug camera found, creating viewport camera for editor"); + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingCamera { index: 0, name: String::from("Viewport Camera"), total: 1 }); + } + let camera = Camera::predetermined(graphics.clone(), Some("Viewport Camera")); + let component = DebugCamera::new(); + let camera_entity = { world.spawn((camera, component)) }; + Ok(camera_entity) } - let camera = Camera::predetermined(graphics.clone(), Some("Viewport Camera")); - let component = DebugCamera::new(); - let camera_entity = { world.write().spawn((camera, component)) }; - Ok(camera_entity) } } #[cfg(not(feature = "editor"))] { // Runtime mode - look for player camera, panic if none exists - let player_camera = world.read() + let player_camera = world .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(entity, (_, component))| { @@ -1,7 +1,5 @@ -use dropbear_engine::entity::Transform; -use std::path::PathBuf; -use crate::states::{ModelProperties, Node}; +use crate::states::Node; pub const PROTO_TEXTURE: &[u8] = include_bytes!("../../resources/proto.png"); @@ -39,12 +37,3 @@ pub enum ViewportMode { CameraMove, Gizmo, } - -#[derive(Clone, Debug)] -pub struct PendingSpawn { - pub asset_path: PathBuf, - pub asset_name: String, - pub transform: Transform, - pub properties: ModelProperties, - pub handle_id: Option<u64>, -} @@ -35,7 +35,6 @@ clap.workspace = true walkdir.workspace = true zip.workspace = true tokio.workspace = true -async-trait.workspace = true reqwest.workspace = true flate2.workspace = true tar.workspace = true @@ -52,4 +51,4 @@ editor = ["eucalyptus-core/editor"] anyhow = "1.0" app_dirs2 = "2.5" reqwest = { version = "0.12", features = ["blocking"] } -zip = "4.3" +zip = "4.3" @@ -1,15 +1,15 @@ use app_dirs2::{AppDataType, app_dir}; use futures_util::StreamExt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use tokio::fs; use tokio::io::AsyncWriteExt; use tokio::sync::mpsc::UnboundedSender; use crate::APP_INFO; -const GLEAM_VERSION: &'static str = "1.12.0"; -const BUN_VERSION: &'static str = "1.2.22"; -const JAVY_VERSION: &'static str = "6.0.0"; +const GLEAM_VERSION: &str = "1.12.0"; +const BUN_VERSION: &str = "1.2.22"; +const JAVY_VERSION: &str = "6.0.0"; #[derive(Clone)] pub enum InstallStatus { @@ -32,9 +32,9 @@ pub struct GleamScriptCompiler { #[allow(dead_code)] impl GleamScriptCompiler { #[allow(dead_code)] - pub fn new(project_location: &PathBuf) -> Self { + pub fn new(project_location: impl AsRef<Path>) -> Self { GleamScriptCompiler { - project_location: project_location.clone(), + project_location: project_location.as_ref().to_path_buf(), } } @@ -183,10 +183,10 @@ impl GleamScriptCompiler { } pub async fn download_gleam( - app_dir: &PathBuf, + app_dir: impl AsRef<Path>, sender: Option<UnboundedSender<InstallStatus>>, ) -> anyhow::Result<()> { - let gleam_dir = app_dir + let gleam_dir = app_dir.as_ref() .join("dependencies") .join("gleam") .join(GLEAM_VERSION); @@ -195,7 +195,7 @@ impl GleamScriptCompiler { log::info!( "Gleam v{} already cached at {}", GLEAM_VERSION, - app_dir.display() + app_dir.as_ref().display() ); return Ok(()); } @@ -210,16 +210,16 @@ impl GleamScriptCompiler { } pub async fn download_bun( - app_dir: &PathBuf, + app_dir: impl AsRef<Path>, sender: Option<UnboundedSender<InstallStatus>>, ) -> anyhow::Result<()> { - let bun_dir = app_dir.join("dependencies").join("bun").join(BUN_VERSION); + let bun_dir = app_dir.as_ref().join("dependencies").join("bun").join(BUN_VERSION); if bun_dir.exists() { log::info!( "Bun v{} already cached at {}", BUN_VERSION, - app_dir.display() + app_dir.as_ref().display() ); return Ok(()); } @@ -234,16 +234,16 @@ impl GleamScriptCompiler { } pub async fn download_javy( - app_dir: &PathBuf, + app_dir: impl AsRef<Path>, sender: Option<UnboundedSender<InstallStatus>>, ) -> anyhow::Result<()> { - let javy_dir = app_dir.join("dependencies").join("javy").join(JAVY_VERSION); + let javy_dir = app_dir.as_ref().join("dependencies").join("javy").join(JAVY_VERSION); if javy_dir.exists() { log::info!( "Javy v{} already cached at {}", JAVY_VERSION, - app_dir.display() + app_dir.as_ref().display() ); return Ok(()); } @@ -259,7 +259,7 @@ impl GleamScriptCompiler { async fn download_and_extract( url: &str, - target_dir: &PathBuf, + target_dir: impl AsRef<Path>, tool_name: &str, sender: Option<UnboundedSender<InstallStatus>>, ) -> anyhow::Result<()> { @@ -271,7 +271,7 @@ impl GleamScriptCompiler { }); } - fs::create_dir_all(target_dir).await?; + fs::create_dir_all(target_dir.as_ref()).await?; if let Some(ref s) = sender { let _ = s.send(InstallStatus::InProgress { @@ -286,7 +286,7 @@ impl GleamScriptCompiler { let mut downloaded = 0; let mut stream = response.bytes_stream(); - let temp_file = target_dir.join(format!("{}_download", tool_name)); + let temp_file = target_dir.as_ref().join(format!("{}_download", tool_name)); let mut file = fs::File::create(&temp_file).await?; while let Some(item) = stream.next().await { @@ -336,13 +336,13 @@ impl GleamScriptCompiler { Ok(()) } - async fn extract_zip(archive: &PathBuf, target_dir: &PathBuf) -> anyhow::Result<()> { + async fn extract_zip(archive: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> anyhow::Result<()> { let file = std::fs::File::open(archive)?; let mut archive = zip::ZipArchive::new(file)?; for i in 0..archive.len() { let mut file = archive.by_index(i)?; - let outpath = target_dir.join(file.name()); + let outpath = target_dir.as_ref().join(file.name()); if file.is_dir() { fs::create_dir_all(&outpath).await?; @@ -359,22 +359,22 @@ impl GleamScriptCompiler { Ok(()) } - async fn extract_tar_gz(archive: &PathBuf, target_dir: &PathBuf) -> anyhow::Result<()> { + async fn extract_tar_gz(archive: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> anyhow::Result<()> { let file = std::fs::File::open(archive)?; let tar = flate2::read::GzDecoder::new(file); let mut archive = tar::Archive::new(tar); for entry in archive.entries()? { let mut entry = entry?; - let path = target_dir.join(entry.path()?); + let path = target_dir.as_ref().join(entry.path()?); entry.unpack(path)?; } Ok(()) } async fn extract_gz( - archive: &PathBuf, - target_dir: &PathBuf, + archive: impl AsRef<Path>, + target_dir: impl AsRef<Path>, tool_name: &str, ) -> anyhow::Result<()> { let file = std::fs::File::open(archive)?; @@ -388,7 +388,7 @@ impl GleamScriptCompiler { tool_name.to_string() }; - let output_path = target_dir.join(exe_name); + let output_path = target_dir.as_ref().join(exe_name); let mut output_file = fs::File::create(&output_path).await?; output_file.write_all(&buffer).await?; @@ -1,7 +1,7 @@ pub mod gleam; use std::{collections::HashMap, fs, path::PathBuf, process::Command}; - +use std::path::Path; use clap::ArgMatches; use zip::write::SimpleFileOptions; @@ -62,7 +62,7 @@ pub fn package(project_path: PathBuf, _sub_matches: &ArgMatches) -> anyhow::Resu println!("Building {} for release", project_name); let mut cargo_build = Command::new("cargo") - .args(&["build", "--release"]) + .args(["build", "--release"]) .current_dir(&runtime_dir) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) @@ -168,16 +168,16 @@ pub fn package(project_path: PathBuf, _sub_matches: &ArgMatches) -> anyhow::Resu Ok(()) } -fn clone_repository(build_dir: &PathBuf) -> anyhow::Result<()> { +fn clone_repository(build_dir: impl AsRef<Path>) -> anyhow::Result<()> { git2::build::RepoBuilder::new().clone( "https://github.com/4tkbytes/redback-runtime", - &build_dir.join("redback-runtime"), + &build_dir.as_ref().join("redback-runtime"), )?; println!("Repository cloned successfully!"); Ok(()) } -fn should_update_repository(repo_dir: &PathBuf) -> anyhow::Result<bool> { +fn should_update_repository(repo_dir: impl AsRef<Path>) -> anyhow::Result<bool> { let repo = match git2::Repository::open(repo_dir) { Ok(repo) => repo, Err(_) => { @@ -214,8 +214,8 @@ fn should_update_repository(repo_dir: &PathBuf) -> anyhow::Result<bool> { Ok(head != remote_commit) } -fn copy_resources_folder(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { - std::fs::create_dir_all(dest)?; +fn copy_resources_folder(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> anyhow::Result<()> { + fs::create_dir_all(dest.as_ref())?; for entry in fs::read_dir(src)? { let entry = entry?; @@ -226,7 +226,7 @@ fn copy_resources_folder(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { continue; } - let dest_path = dest.join(&file_name); + let dest_path = dest.as_ref().join(&file_name); if src_path.is_dir() { copy_resources_folder(&src_path, &dest_path)?; @@ -238,7 +238,7 @@ fn copy_resources_folder(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { Ok(()) } -fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> { +fn copy_system_libraries(output_dir: impl AsRef<Path>) -> anyhow::Result<()> { #[cfg(target_os = "windows")] { let dll_paths = vec![ @@ -251,7 +251,7 @@ fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> { for dll_path in dll_paths { if std::path::Path::new(dll_path).exists() { let dll_name = std::path::Path::new(dll_path).file_name().unwrap(); - let dest = output_dir.join(dll_name); + let dest = output_dir.as_ref().join(dll_name); std::fs::copy(dll_path, dest)?; println!("Copied system library: {}", dll_name.to_string_lossy()); break; @@ -261,7 +261,7 @@ fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> { #[cfg(any(target_os = "linux", target_os = "macos"))] { - let lib_dir = output_dir.join("lib"); + let lib_dir = output_dir.as_ref().join("lib"); std::fs::create_dir_all(&lib_dir)?; let lib_paths = vec![ @@ -287,28 +287,28 @@ fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> { Ok(()) } -fn create_zip_package(source_dir: &PathBuf, zip_path: &PathBuf) -> anyhow::Result<()> { - let file = fs::File::create(zip_path)?; +fn create_zip_package(source_dir: impl AsRef<Path>, zip_path: impl AsRef<Path>) -> anyhow::Result<()> { + let file = fs::File::create(zip_path.as_ref())?; let mut zip = zip::ZipWriter::new(file); - let walkdir = walkdir::WalkDir::new(source_dir); + let walkdir = walkdir::WalkDir::new(source_dir.as_ref()); for entry in walkdir { let entry = entry?; let path = entry.path(); if path.is_file() { - let relative_path = path.strip_prefix(source_dir)?; + let relative_path = path.strip_prefix(source_dir.as_ref())?; let name = relative_path.to_string_lossy(); let options: SimpleFileOptions = Default::default(); - zip.start_file(name, options.into())?; + zip.start_file(name, options)?; let mut file = std::fs::File::open(path)?; std::io::copy(&mut file, &mut zip)?; } } zip.finish()?; - println!("Created zip package: {}", zip_path.display()); + println!("Created zip package: {}", zip_path.as_ref().display()); Ok(()) } @@ -362,14 +362,13 @@ pub fn build( for entry in fs::read_dir(&script_dir)? { let entry = entry?; let path = entry.path(); - if let Some(ext) = path.extension() { - if ext == "rhai" { + if let Some(ext) = path.extension() + && ext == "rhai" { let name = path.file_name().unwrap().to_string_lossy().to_string(); let contents = fs::read_to_string(&path)?; println!(" > Copied script info from [{}]", name); scripts.insert(name, contents); } - } } } @@ -496,24 +495,21 @@ fn check_assimp_availability() -> bool { } if let Ok(output) = Command::new("pkg-config") - .args(&["--exists", "assimp"]) + .args(["--exists", "assimp"]) .output() - { - if output.status.success() { + && output.status.success() { return true; } - } #[cfg(target_os = "linux")] { - if let Ok(output) = Command::new("ldconfig").args(&["-p"]).output() { - if output.status.success() { + if let Ok(output) = Command::new("ldconfig").args(["-p"]).output() + && output.status.success() { let output_str = String::from_utf8_lossy(&output.stdout); if output_str.contains("libassimp") { return true; } } - } } } @@ -27,7 +27,7 @@ impl InspectableComponent for Camera { if resp.changed() { if cfg.old_label_entity.is_none() { - cfg.old_label_entity = Some(entity.clone()); + cfg.old_label_entity = Some(*entity); cfg.label_original = Some(self.label.clone()); } cfg.label_last_edit = Some(Instant::now()); @@ -77,7 +77,7 @@ impl InspectableComponent for Camera { pub enum UndoableCameraAction { Speed(Entity, f64), Sensitivity(Entity, f64), - FOV(Entity, f64), + Fov(Entity, f64), Type(Entity, CameraType), } @@ -53,21 +53,20 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed X transform change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -80,21 +79,20 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed Y transform change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -108,21 +106,20 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed Z transform change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -158,8 +155,8 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } @@ -168,15 +165,14 @@ impl InspectableComponent for Transform { } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed X rotation change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -199,8 +195,8 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } @@ -209,15 +205,14 @@ impl InspectableComponent for Transform { } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed Y rotation change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -240,8 +235,8 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } @@ -250,15 +245,14 @@ impl InspectableComponent for Transform { } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed Z rotation change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -298,8 +292,8 @@ impl InspectableComponent for Transform { ); if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } @@ -313,15 +307,14 @@ impl InspectableComponent for Transform { } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed X scale change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -335,8 +328,8 @@ impl InspectableComponent for Transform { let response = ui.add_enabled(!cfg.scale_locked, y_slider); if response.drag_started() && !cfg.scale_locked { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } @@ -345,15 +338,14 @@ impl InspectableComponent for Transform { } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed Y scale change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -367,8 +359,8 @@ impl InspectableComponent for Transform { let response = ui.add_enabled(!cfg.scale_locked, z_slider); if response.drag_started() && !cfg.scale_locked { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_old_entity = Some(*entity); + cfg.transform_original_transform = Some(*self); cfg.transform_in_progress = true; } @@ -377,15 +369,14 @@ impl InspectableComponent for Transform { } if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { + if let Some(ent) = cfg.transform_old_entity.take() + && let Some(orig) = cfg.transform_original_transform.take() { UndoableAction::push_to_undo( undo_stack, UndoableAction::Transform(ent, orig), ); log::debug!("Pushed Z scale change to undo stack"); } - } cfg.transform_in_progress = false; } }); @@ -424,8 +415,8 @@ impl InspectableComponent for ScriptComponent { .default_open(true) .show(ui, |ui| { ui.horizontal(|ui| { - if ui.button("Browse").clicked() { - if let Some(script_file) = rfd::FileDialog::new() + if ui.button("Browse").clicked() + && let Some(script_file) = rfd::FileDialog::new() .add_filter("Typescript", &["ts"]) .pick_file() { @@ -435,7 +426,7 @@ impl InspectableComponent for ScriptComponent { .to_string_lossy() .to_string(); let lib_path = &script_file.clone().parent().unwrap().join("dropbear.ts"); - if let Err(_) = std::fs::read(lib_path) { + if std::fs::read(lib_path).is_err() { log::warn!("dropbear.ts library does not exist in project source directory, copying..."); if let Err(e) = std::fs::write(lib_path, include_str!("../../../resources/dropbear.ts")) { log::error!("Non-fatal error: Creating library file failed: {}", e); @@ -448,10 +439,9 @@ impl InspectableComponent for ScriptComponent { script_name, }); } - } - if ui.button("New").clicked() { - if let Some(script_path) = rfd::FileDialog::new() + if ui.button("New").clicked() + && let Some(script_path) = rfd::FileDialog::new() .add_filter("TypeScript", &["ts"]) .set_file_name(format!("{}_script.ts", label)) .save_file() @@ -459,7 +449,7 @@ impl InspectableComponent for ScriptComponent { // check if dropbear module exists // todo: change this to an %APPDATA% file instead of to memory. let lib_path = &script_path.clone().parent().unwrap().join("dropbear.ts"); - if let Err(_) = std::fs::read(lib_path) { + if std::fs::read(lib_path).is_err() { log::warn!("dropbear.ts library does not exist in project source directory, copying..."); if let Err(e) = std::fs::write(lib_path, include_str!("../../../resources/dropbear.ts")) { log::error!("Non-fatal error: Creating library file failed: {}", e); @@ -484,7 +474,6 @@ impl InspectableComponent for ScriptComponent { }, } } - } }); ui.separator(); @@ -527,7 +516,7 @@ impl InspectableComponent for AdoptedEntity { if resp.changed() { if cfg.old_label_entity.is_none() { - cfg.old_label_entity = Some(entity.clone()); + cfg.old_label_entity = Some(*entity); cfg.label_original = Some(self.model.label.clone()); } cfg.label_last_edit = Some(Instant::now()); @@ -572,7 +561,7 @@ impl InspectableComponent for Light { if resp.changed() { if cfg.old_label_entity.is_none() { - cfg.old_label_entity = Some(entity.clone()); + cfg.old_label_entity = Some(*entity); cfg.label_original = Some(self.label.clone().to_string()); } cfg.label_last_edit = Some(Instant::now()); @@ -658,10 +647,10 @@ impl InspectableComponent for LightComponent { ui.horizontal(|ui| { ComboBox::new("Range", "Range") // .width(ui.available_width()) - .selected_text(format!("Range {}", self.attenuation.range.to_string())) + .selected_text(format!("Range {}", self.attenuation.range)) .show_ui(ui, |ui| { for (preset, label) in ATTENUATION_PRESETS { - ui.selectable_value(&mut self.attenuation, preset.clone(), *label); + ui.selectable_value(&mut self.attenuation, *preset, *label); } }); }); @@ -4,7 +4,6 @@ use std::{collections::HashSet, sync::LazyLock}; use crate::APP_INFO; use crate::editor::component::InspectableComponent; -use crate::editor::scene::PENDING_SPAWNS; use dropbear_engine::{ entity::Transform, lighting::{Light, LightComponent}, @@ -13,17 +12,18 @@ use egui; use egui_dock_fork::TabViewer; use egui_extras; use eucalyptus_core::states::{Node, RESOURCES, ResourceType}; -use eucalyptus_core::utils::PendingSpawn; use log; use parking_lot::Mutex; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode, math::DVec3}; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use eucalyptus_core::spawn::{push_pending_spawn, PendingSpawn}; pub struct EditorTabViewer<'a> { pub view: egui::TextureId, pub nodes: Vec<EntityNode>, pub tex_size: Extent3d, pub gizmo: &'a mut Gizmo, - pub world: Arc<RwLock<World>>, + pub world: &'a mut World, pub selected_entity: &'a mut Option<hecs::Entity>, pub viewport_mode: &'a mut ViewportMode, pub undo_stack: &'a mut Vec<UndoableAction>, @@ -40,25 +40,23 @@ impl<'a> EditorTabViewer<'a> { position: DVec3, properties: Option<ModelProperties>, ) -> anyhow::Result<()> { - let mut transform = Transform::default(); - transform.position = position; + let transform = Transform { position, ..Default::default() }; { - let mut pending_spawns = PENDING_SPAWNS.lock(); if let Some(props) = properties { - pending_spawns.push(PendingSpawn { + push_pending_spawn(PendingSpawn { asset_path: asset.path.clone(), asset_name: asset.name.clone(), transform, properties: props, - handle_id: None, + handle: None, }); } else { - pending_spawns.push(PendingSpawn { + push_pending_spawn(PendingSpawn { asset_path: asset.path.clone(), asset_name: asset.name.clone(), transform, properties: ModelProperties::default(), - handle_id: None, + handle: None, }); } Ok(()) @@ -69,7 +67,7 @@ impl<'a> EditorTabViewer<'a> { #[derive(Clone, Debug)] pub struct DraggedAsset { pub name: String, - pub path: PathBuf, + pub path: ResourceReference, } pub static TABS_GLOBAL: LazyLock<Mutex<StaticallyKept>> = @@ -117,15 +115,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let mut cfg = TABS_GLOBAL.lock(); ui.ctx().input(|i| { - if i.pointer.button_pressed(egui::PointerButton::Secondary) { - if let Some(pos) = i.pointer.hover_pos() { - if ui.available_rect_before_wrap().contains(pos) { + if i.pointer.button_pressed(egui::PointerButton::Secondary) + && let Some(pos) = i.pointer.hover_pos() + && ui.available_rect_before_wrap().contains(pos) { cfg.show_context_menu = true; cfg.context_menu_pos = pos; cfg.context_menu_tab = Some(tab.clone()); } - } - } }); match tab { @@ -133,7 +129,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { log_once::debug_once!("Viewport focused"); // ------------------- Template for querying active camera ----------------- // if let Some(active_camera) = self.active_camera { - // if let Ok(mut q) = self.world.read().query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { + // if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { // if let Some((camera, component, follow_target)) = q.get() { // } else { @@ -194,7 +190,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let active_cam = self.active_camera.lock(); if let Some(active_camera) = *active_cam { { - if let Ok(mut q) = self.world.read().query_one::<( + if let Ok(mut q) = self.world.query_one::<( &Camera, &CameraComponent, Option<&CameraFollowTarget>, @@ -278,9 +274,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let mut entity_count = 0; { - let world_read_guard = self.world.read(); for (entity_id, (transform, _)) in - world_read_guard.query::<(&Transform, &AdoptedEntity)>().iter() + self.world.query::<(&Transform, &AdoptedEntity)>().iter() { entity_count += 1; let entity_pos = transform.position; @@ -317,15 +312,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> { if let Some(entity_id) = selected_entity_id { *self.selected_entity = Some(entity_id); log::debug!("Selected entity: {:?}", entity_id); + } else if entity_count == 0 { + log::debug!("No entities in world to select"); } else { - if entity_count == 0 { - log::debug!("No entities in world.read() to select"); - } else { - log::debug!( + log::debug!( "No entity hit by ray (checked {} entities)", entity_count ); - } } } } @@ -355,8 +348,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let active_cam = self.active_camera.lock(); if let Some(active_camera) = *active_cam { let camera_data = { - let world = self.world.read(); - if let Ok(mut q) = world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { + if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { let val = q.get(); if let Some(val) = val { Some(val.0.clone()) @@ -383,13 +375,12 @@ impl<'a> TabViewer for EditorTabViewer<'a> { }); } } - if !matches!(self.viewport_mode, ViewportMode::None) { - if let Some(entity_id) = self.selected_entity { + if !matches!(self.viewport_mode, ViewportMode::None) + && let Some(entity_id) = self.selected_entity { { - if let Ok(mut q) = self.world.read() + if let Ok(mut q) = self.world .query_one::<&mut Transform>(*entity_id) - { - if let Some(transform) = q.get() { + && let Some(transform) = q.get() { let was_focused = cfg.is_focused; cfg.is_focused = self.gizmo.is_focused(); @@ -406,13 +397,11 @@ impl<'a> TabViewer for EditorTabViewer<'a> { if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[gizmo_transform]) - { - if let Some(new_transform) = new_transforms.first() { + && let Some(new_transform) = new_transforms.first() { transform.position = new_transform.translation.into(); transform.rotation = new_transform.rotation.into(); transform.scale = new_transform.scale.into(); } - } if was_focused && !cfg.is_focused { let transform_changed = cfg.old_pos.position != transform.position @@ -421,27 +410,25 @@ impl<'a> TabViewer for EditorTabViewer<'a> { if transform_changed { UndoableAction::push_to_undo( - &mut self.undo_stack, + self.undo_stack, UndoableAction::Transform( - entity_id.clone(), - cfg.old_pos.clone(), + *entity_id, + cfg.old_pos, ), ); log::debug!("Pushed transform action to stack"); } } } - } } } - } } EditorTab::ModelEntityList => { ui.label("Model/Entity List"); show_entity_tree( ui, &mut self.nodes, - &mut self.selected_entity, + self.selected_entity, "Model Entity Asset List", ); } @@ -520,7 +507,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ResourceType::Texture => assets.push(( format!( "file://{}", - file.path.to_string_lossy().to_string() + file.path.to_string_lossy() ), file.name.clone(), file.path.clone(), @@ -621,13 +608,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let is_hovered = card_response.hovered() || image_response.hovered() || state.dragged; let is_d_clicked = card_response.double_clicked() || image_response.double_clicked(); - if is_d_clicked { - if matches!(asset_type, ResourceType::Model) { + if is_d_clicked + && matches!(asset_type, ResourceType::Model) { let mut spawn_position = DVec3::default(); { let active_cam = self.active_camera.lock(); if let Some(active_camera) = *active_cam { - if let Ok(mut q) = self.world.read().query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { + if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { if let Some((camera, _, _)) = q.get() { spawn_position = camera.eye; } else { @@ -643,7 +630,23 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let asset = DraggedAsset { name: asset_name.clone(), - path: asset_path.clone(), + path: if asset_path.starts_with("resources/") { + // Path is already relative from project root, extract the part after "resources/" + ResourceReference { + ref_type: ResourceReferenceType::File( + asset_path.strip_prefix("resources/") + .unwrap_or(&asset_path) + .to_string_lossy() + .to_string() + ), + } + } else { + // Fallback to the original method for absolute paths + ResourceReference::from_path(asset_path.clone()).unwrap_or_else(|_e| { + log::warn!("Unable to create ResourceReference from path: {:?}", asset_path); + Default::default() + }) + }, // asset_type: asset_type.clone(), }; @@ -666,7 +669,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } } } - } if is_hovered || state.dragged { ui.painter().rect_filled( @@ -698,10 +700,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { EditorTab::ResourceInspector => { if let Some(entity) = self.selected_entity { { - let world = self.world.read(); - // Note: Use self.world.read() and query_one instead of self.world.write() and query_one_mut as - // that causes a deadlock - if let Ok(mut q) = world.query_one::<( + if let Ok(mut q) = self.world.query_one::<( &mut AdoptedEntity, Option<&mut Transform>, Option<&ModelProperties>, @@ -786,28 +785,26 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ); } - if let Some(t) = cfg.label_last_edit { - if t.elapsed() >= Duration::from_millis(500) { - if let Some(ent) = cfg.old_label_entity.take() { - if let Some(orig) = cfg.label_original.take() { + 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() { UndoableAction::push_to_undo( - &mut self.undo_stack, + self.undo_stack, UndoableAction::Label(ent, orig, EntityType::Entity), ); 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"); } - if let Ok(mut q) = world + if let Ok(mut q) = self.world .query_one::<(&mut Light, &mut Transform, &mut LightComponent)>(*entity) { if let Some((light, transform, props)) = q.get() { @@ -835,35 +832,32 @@ impl<'a> TabViewer for EditorTabViewer<'a> { self.signal, &mut light.label, ); - if let Some(t) = cfg.label_last_edit { - if t.elapsed() >= Duration::from_millis(500) { - if let Some(ent) = cfg.old_label_entity.take() { - if let Some(orig) = cfg.label_original.take() { + 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() { UndoableAction::push_to_undo( - &mut self.undo_stack, + self.undo_stack, UndoableAction::Label(ent, orig, EntityType::Light), ); 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 light inside resource inspector"); } if let Ok(mut q) = - world.query_one::<( + self.world.query_one::<( &mut Camera, &mut CameraComponent, Option<&mut CameraFollowTarget>, )>(*entity) - { - if let Some((camera, camera_component, follow_target)) = q.get() { + && let Some((camera, camera_component, follow_target)) = q.get() { camera.inspect( entity, &mut cfg, @@ -895,18 +889,16 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.label("Camera Controls:"); let mut active_camera = self.active_camera.lock(); if ui.button("Set as Active Camera").clicked() { - *active_camera = Some(*entity).into(); + *active_camera = Some(*entity); log::info!("Set camera '{}' as active", camera.label); } - if matches!(self.editor_mode, EditorState::Editing) { - if ui.button("Switch to This Camera").clicked() { - *active_camera = Some(*entity).into(); + if matches!(self.editor_mode, EditorState::Editing) + && ui.button("Switch to This Camera").clicked() { + *active_camera = Some(*entity); log::info!("Switched to camera '{}'", camera.label); } - } } - } } } else { ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!"); @@ -966,8 +958,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> { }) }); - if let Some(action) = menu_action { - if Some(tab.clone()) == cfg.context_menu_tab { + if let Some(action) = menu_action + && Some(tab.clone()) == cfg.context_menu_tab { match action { EditorTabMenuAction::ImportResource => { log::debug!("Import Resource clicked"); @@ -1020,26 +1012,22 @@ impl<'a> TabViewer for EditorTabViewer<'a> { log::debug!("Add Component clicked"); if let Some(entity) = self.selected_entity { { - if let Ok(mut q) = self.world.read() + if let Ok(mut q) = self.world .query_one::<&AdoptedEntity>(*entity) - { - if let Some(..) = q.get() { + && q.get().is_some() { log::debug!("Queried selected entity, it is an entity"); *self.signal = Signal::AddComponent(*entity, EntityType::Entity); } - } } { - if let Ok(mut q) = self.world.read() + if let Ok(mut q) = self.world .query_one::<&Light>(*entity) - { - if let Some(..) = q.get() { + && q.get().is_some() { log::debug!("Queried selected entity, it is a light"); *self.signal = Signal::AddComponent(*entity, EntityType::Light); } - } } } else { warn!( @@ -1060,7 +1048,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { EditorTabMenuAction::RemoveComponent => { log::debug!("Remove Component clicked"); if let Some(entity) = self.selected_entity { - if let Ok(mut q) = self.world.write() + if let Ok(mut q) = self.world .query_one::<&ScriptComponent>(*entity) { if let Some(script) = q.get() { @@ -1069,7 +1057,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ); *self.signal = Signal::RemoveComponent( *entity, - ComponentType::Script(script.clone()), + Box::new(ComponentType::Script(script.clone())), ); } } else { @@ -1089,23 +1077,17 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } } } - } - if let Some(rect) = popup_rect { - if cfg.show_context_menu && Some(tab.clone()) == cfg.context_menu_tab { - if ui + if let Some(rect) = popup_rect + && cfg.show_context_menu && Some(tab.clone()) == cfg.context_menu_tab + && ui .ctx() .input(|i| i.pointer.button_clicked(egui::PointerButton::Primary)) - { - if let Some(pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { - if !rect.contains(pos) { + && let Some(pos) = ui.ctx().input(|i| i.pointer.interact_pos()) + && !rect.contains(pos) { cfg.show_context_menu = false; cfg.context_menu_tab = None; } - } - } - } - } } } } @@ -1128,7 +1110,7 @@ pub(crate) fn import_object() -> anyhow::Result<()> { // copy over to models folder { let project = PROJECT.read(); - let models_dir = PathBuf::from(project.project_path.clone()) + let models_dir = project.project_path.clone() .join("resources") .join("models"); if !models_dir.exists() { @@ -1146,7 +1128,7 @@ pub(crate) fn import_object() -> anyhow::Result<()> { // copy over to textures folder { let project = PROJECT.read(); - let textures_dir = PathBuf::from(project.project_path.clone()) + let textures_dir = project.project_path.clone() .join("resources") .join("textures"); if !textures_dir.exists() { @@ -1165,7 +1147,7 @@ pub(crate) fn import_object() -> anyhow::Result<()> { let project = PROJECT.read(); // everything else copies over to resources root dir let resources_dir = - PathBuf::from(project.project_path.clone()).join("resources"); + project.project_path.clone().join("resources"); if !resources_dir.exists() { std::fs::create_dir_all(&resources_dir)?; } @@ -1180,7 +1162,7 @@ pub(crate) fn import_object() -> anyhow::Result<()> { proj.write_to_all()?; Ok(()) } else { - return Err(anyhow::anyhow!("File dialogue returned None")); + Err(anyhow::anyhow!("File dialogue returned None")) } } @@ -69,7 +69,7 @@ impl Keyboard for Editor { } KeyCode::Delete => { if !is_playing { - if let Some(_) = &self.selected_entity { + if self.selected_entity.is_some() { self.signal = Signal::Delete; } else { warn!("Failed to delete: No entity selected"); @@ -80,7 +80,7 @@ impl Keyboard for Editor { } KeyCode::Escape => { if is_double_press { - if let Some(_) = &self.selected_entity { + if self.selected_entity.is_some() { self.selected_entity = None; log::debug!("Deselected entity"); } @@ -114,8 +114,7 @@ impl Keyboard for Editor { KeyCode::KeyC => { if ctrl_pressed && !is_playing { if let Some(entity) = &self.selected_entity { - let world = self.world.read(); - let query = world + let query = self.world .query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); if let Ok(mut q) = query { if let Some((e, t, props)) = q.get() { @@ -218,11 +217,10 @@ impl Keyboard for Editor { } } KeyCode::KeyP => { - if !is_playing { - if ctrl_pressed { + if !is_playing + && ctrl_pressed { self.signal = Signal::Play } - } } _ => { self.input_state.pressed_keys.insert(key); @@ -247,18 +245,15 @@ impl Mouse for Editor { let dx = position.x - last_pos.0; let dy = position.y - last_pos.1; - if let Some(active_camera) = *self.active_camera.lock() { - if let Ok(mut q) = self.world.read().query_one::<( + if let Some(active_camera) = *self.active_camera.lock() + && let Ok(mut q) = self.world.query_one::<( &mut Camera, &CameraComponent, Option<&CameraFollowTarget>, )>(active_camera) - { - if let Some((camera, _, _)) = q.get() { + && let Some((camera, _, _)) = q.get() { camera.track_mouse_delta(dx, dy); } - } - } } self.input_state.last_mouse_pos = Some((position.x, position.y)); } @@ -309,10 +304,8 @@ impl Mouse for Editor { } fn mouse_down(&mut self, button: MouseButton) { - match button { - _ => { - self.input_state.mouse_button.insert(button); - } + { + self.input_state.mouse_button.insert(button); } } @@ -33,22 +33,23 @@ use eucalyptus_core::states::{ CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, PROJECT, SCENES, SceneEntity, ScriptComponent, }; -use eucalyptus_core::utils::ViewportMode; +use eucalyptus_core::utils::{ViewportMode}; use eucalyptus_core::{fatal, info, states, success, warn}; use hecs::World; -use log; -use parking_lot::{Mutex, RwLock}; +use parking_lot::Mutex; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::oneshot; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode}; use wgpu::{Color, Extent3d, RenderPipeline}; use winit::{keyboard::KeyCode, window::Window}; +use dropbear_engine::future::FutureHandle; use dropbear_engine::graphics::{RenderContext, Shader}; use dropbear_engine::model::{ModelId}; pub struct Editor { scene_command: SceneCommand, - world: Arc<RwLock<World>>, - dock_state: Arc<Mutex<DockState<EditorTab>>>, + pub world: World, + dock_state: DockState<EditorTab>, texture_id: Option<egui::TextureId>, size: Extent3d, render_pipeline: Option<RenderPipeline>, @@ -63,25 +64,25 @@ pub struct Editor { show_new_project: bool, project_name: String, - project_path: Arc<Mutex<Option<PathBuf>>>, + pub(crate) project_path: Arc<Mutex<Option<PathBuf>>>, pending_scene_switch: bool, gizmo: Gizmo, - selected_entity: Option<hecs::Entity>, + pub(crate) selected_entity: Option<hecs::Entity>, viewport_mode: ViewportMode, - signal: Signal, - undo_stack: Vec<UndoableAction>, + pub(crate) signal: Signal, + pub(crate) undo_stack: Vec<UndoableAction>, // todo: add redo (later) // redo_stack: Vec<UndoableAction>, - editor_state: EditorState, + pub(crate) editor_state: EditorState, gizmo_mode: EnumSet<GizmoMode>, - script_manager: ScriptManager, + pub(crate) script_manager: ScriptManager, play_mode_backup: Option<PlayModeBackup>, /// State of the input - input_state: InputState, + pub(crate) input_state: InputState, // channels /// A channel for installing dependencies. @@ -95,6 +96,11 @@ pub struct Editor { /// Used to fetch the current status of the loading, so it can be used for different /// egui loading windows or splash screens and such. current_state: WorldLoadingStatus, + + // handles for futures + world_load_handle: Option<FutureHandle>, + pub(crate) alt_pending_spawn_queue: Vec<FutureHandle>, + world_receiver: Option<oneshot::Receiver<hecs::World>>, } impl Editor { @@ -109,7 +115,7 @@ impl Editor { surface.split_left(NodeIndex::root(), 0.20, vec![EditorTab::ResourceInspector]); let [_old, _] = surface.split_below(right, 0.5, vec![EditorTab::AssetViewer]); - // this shit doesnt work :( + // this shit doesn't work :( // nvm it works std::thread::spawn(move || { loop { @@ -135,7 +141,7 @@ impl Editor { Self { scene_command: SceneCommand::None, - dock_state: Arc::new(Mutex::new(dock_state)), + dock_state, texture_id: None, size: Extent3d::default(), render_pipeline: None, @@ -143,7 +149,7 @@ impl Editor { is_viewport_focused: false, // is_cursor_locked: false, window: None, - world: Arc::new(RwLock::new(World::new())), + world: World::new(), show_new_project: false, project_name: String::new(), project_path: Arc::new(Mutex::new(None)), @@ -165,6 +171,9 @@ impl Editor { progress_tx: None, is_world_loaded: IsWorldLoadedYet::new(), current_state: WorldLoadingStatus::Idle, + world_load_handle: None, + alt_pending_spawn_queue: vec![], + world_receiver: None, } } @@ -200,7 +209,7 @@ impl Editor { scene.cameras.clear(); for (id, (adopted, transform, properties, script)) in self - .world.read() + .world .query::<( &AdoptedEntity, Option<&Transform>, @@ -209,7 +218,7 @@ impl Editor { )>() .iter() { - let transform = transform.unwrap_or(&Transform::default()).clone(); + let transform = *transform.unwrap_or(&Transform::default()); let scene_entity = SceneEntity { model_path: adopted.model.path.clone(), @@ -225,7 +234,7 @@ impl Editor { } for (id, (light_component, transform, light)) in self - .world.read() + .world .query::<( &dropbear_engine::lighting::LightComponent, &Transform, @@ -246,7 +255,7 @@ impl Editor { } for (_id, (camera, component, follow_target)) in self - .world.read() + .world .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() .iter() { @@ -269,8 +278,8 @@ impl Editor { { let mut config = PROJECT.write(); - let dock_state = self.dock_state.lock(); - config.dock_layout = Some(dock_state.clone()); + let dock_state = self.dock_state.clone(); + config.dock_layout = Some(dock_state); } { @@ -361,7 +370,8 @@ impl Editor { // &mut self, graphics: Arc<SharedGraphicsContext>, sender: Option<UnboundedSender<WorldLoadingStatus>>, - world: Arc<RwLock<World>>, + world: &mut World, + world_sender: Option<oneshot::Sender<hecs::World>>, active_camera: Arc<Mutex<Option<hecs::Entity>>>, project_path: Arc<Mutex<Option<PathBuf>>>, dock_state: Arc<Mutex<DockState<EditorTab>>>, @@ -389,7 +399,7 @@ impl Editor { .load_into_world(world, graphics, sender.clone()) .await?; let mut a_c = active_camera.lock(); - *a_c = Some(cam).into(); + *a_c = Some(cam); log::info!( "Successfully loaded scene with {} entities and {} camera configs", @@ -397,21 +407,23 @@ impl Editor { first_scene.cameras.len(), ); } else { - let existing_debug_camera = world.read() - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Debug) { - Some(entity) - } else { - None - } - }); + let existing_debug_camera = { + world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Debug) { + Some(entity) + } else { + None + } + }) + }; if let Some(camera_entity) = existing_debug_camera { log::info!("Using existing debug camera"); let mut a_c = active_camera.lock(); - *a_c = Some(camera_entity).into(); + *a_c = Some(camera_entity); } else { log::info!("No scenes found, creating default debug camera"); @@ -419,10 +431,10 @@ impl Editor { let component = DebugCamera::new(); { - let e = world.write() + let e = world .spawn((debug_camera, component)); let mut a_c = active_camera.lock(); - *a_c = Some(e).into(); + *a_c = Some(e); } } } @@ -431,10 +443,15 @@ impl Editor { if let Some(ref s) = sender.clone() { let _ = s.send(WorldLoadingStatus::Completed); } + + if let Some(ws) = world_sender { + let _ = ws.send(std::mem::take(world)); + } + Ok(()) } - pub async fn show_ui(&mut self, ctx: &Context) { + pub fn show_ui(&mut self, ctx: &Context) { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { @@ -459,10 +476,8 @@ impl Editor { if ui.button("Stop").clicked() { self.signal = Signal::StopPlaying; } - } else { - if ui.button("Play").clicked() { - self.signal = Signal::Play; - } + } else if ui.button("Play").clicked() { + self.signal = Signal::Play; } ui.menu_button("Export", |ui| { // todo: create a window for better build menu @@ -496,8 +511,7 @@ impl Editor { ui.menu_button("Edit", |ui| { if ui.button("Copy").clicked() { if let Some(entity) = &self.selected_entity { - let world = self.world.read(); - let query = world.query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); + let query = self.world.query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); if let Ok(mut q) = query { if let Some((e, t, props)) = q.get() { let s_entity = states::SceneEntity { @@ -541,20 +555,19 @@ impl Editor { }); ui.menu_button("Window", |ui_window| { - let mut dock_state = self.dock_state.lock(); if ui_window.button("Open Asset Viewer").clicked() { - dock_state.push_to_focused_leaf(EditorTab::AssetViewer); + self.dock_state.push_to_focused_leaf(EditorTab::AssetViewer); } if ui_window.button("Open Resource Inspector").clicked() { - dock_state + self.dock_state .push_to_focused_leaf(EditorTab::ResourceInspector); } if ui_window.button("Open Entity List").clicked() { - dock_state + self.dock_state .push_to_focused_leaf(EditorTab::ModelEntityList); } if ui_window.button("Open Viewport").clicked() { - dock_state.push_to_focused_leaf(EditorTab::Viewport); + self.dock_state.push_to_focused_leaf(EditorTab::Viewport); } }); { @@ -566,19 +579,18 @@ impl Editor { }); }); - egui::CentralPanel::default().show(&ctx, |ui| { - let mut dock_state = self.dock_state.lock(); - - DockArea::new(&mut dock_state) + egui::CentralPanel::default().show(ctx, |ui| { + + DockArea::new(&mut self.dock_state) .style(Style::from_egui(ui.style().as_ref())) .show_inside( ui, &mut EditorTabViewer { view: self.texture_id.unwrap(), - nodes: EntityNode::from_world(&self.world.clone().read()), + nodes: EntityNode::from_world(&self.world), gizmo: &mut self.gizmo, tex_size: self.size, - world: self.world.clone(), + world: &mut self.world, selected_entity: &mut self.selected_entity, viewport_mode: &mut self.viewport_mode, undo_stack: &mut self.undo_stack, @@ -614,31 +626,31 @@ impl Editor { for (entity_id, original_transform, original_properties, original_script) in &backup.entities { - if let Ok(mut transform) = self.world.read().get::<&mut Transform>(*entity_id) { + if let Ok(mut transform) = self.world.get::<&mut Transform>(*entity_id) { *transform = *original_transform; } - if let Ok(mut properties) = self.world.read().get::<&mut ModelProperties>(*entity_id) { + if let Ok(mut properties) = self.world.get::<&mut ModelProperties>(*entity_id) { *properties = original_properties.clone(); } - let has_script = self.world.read().get::<&ScriptComponent>(*entity_id).is_ok(); + let has_script = self.world.get::<&ScriptComponent>(*entity_id).is_ok(); match (has_script, original_script) { (true, Some(original)) => { - if let Ok(mut script) = self.world.read().get::<&mut ScriptComponent>(*entity_id) + if let Ok(mut script) = self.world.get::<&mut ScriptComponent>(*entity_id) { *script = original.clone(); } } (true, None) => { { - let _ = self.world.write() + let _ = self.world .remove_one::<ScriptComponent>(*entity_id); } } (false, Some(original)) => { { - let _ = self.world.write() + let _ = self.world .insert_one(*entity_id, original.clone()); } } @@ -650,32 +662,32 @@ impl Editor { for (entity_id, original_camera, original_component, original_follow_target) in &backup.camera_data { - if let Ok(mut camera) = self.world.read().get::<&mut Camera>(*entity_id) { + if let Ok(mut camera) = self.world.get::<&mut Camera>(*entity_id) { *camera = original_camera.clone(); } - if let Ok(mut component) = self.world.read().get::<&mut CameraComponent>(*entity_id) { + if let Ok(mut component) = self.world.get::<&mut CameraComponent>(*entity_id) { *component = original_component.clone(); } - let has_follow_target = self.world.read().get::<&CameraFollowTarget>(*entity_id).is_ok(); + let has_follow_target = self.world.get::<&CameraFollowTarget>(*entity_id).is_ok(); match (has_follow_target, original_follow_target) { (true, Some(original)) => { if let Ok(mut follow_target) = - self.world.read().get::<&mut CameraFollowTarget>(*entity_id) + self.world.get::<&mut CameraFollowTarget>(*entity_id) { *follow_target = original.clone(); } } (true, None) => { { - let _ = self.world.write() + let _ = self.world .remove_one::<CameraFollowTarget>(*entity_id); } } (false, Some(original)) => { { - let _ = self.world.write() + let _ = self.world .insert_one(*entity_id, original.clone()); } } @@ -698,22 +710,22 @@ impl Editor { let mut entities = Vec::new(); for (entity_id, (_, transform, properties)) in self - .world.read() + .world .query::<(&AdoptedEntity, &Transform, &ModelProperties)>() .iter() { let script = self - .world.read() + .world .query_one::<&ScriptComponent>(entity_id) .ok() - .and_then(|mut s| s.get().map(|script| script.clone())); + .and_then(|mut s| s.get().cloned()); entities.push((entity_id, *transform, properties.clone(), script)); } let mut camera_data = Vec::new(); for (entity_id, (camera, component, follow_target)) in self - .world.read() + .world .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() .iter() { @@ -740,7 +752,7 @@ impl Editor { pub fn switch_to_debug_camera(&mut self) { let debug_camera = self - .world.read() + .world .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(e, (_cam, comp))| { @@ -762,7 +774,7 @@ impl Editor { pub fn switch_to_player_camera(&mut self) { let player_camera = self - .world.read() + .world .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(e, (_cam, comp))| { @@ -784,16 +796,13 @@ impl Editor { pub fn is_using_debug_camera(&self) -> bool { let active_camera = self.active_camera.lock(); - if let Some(active_camera_entity) = *active_camera { - if let Ok(mut query) = self - .world.read() + if let Some(active_camera_entity) = *active_camera + && let Ok(mut query) = self + .world .query_one::<&CameraComponent>(active_camera_entity) - { - if let Some(component) = query.get() { + && let Some(component) = query.get() { return matches!(component.camera_type, CameraType::Debug); } - } - } false } @@ -811,7 +820,7 @@ impl Editor { if let Some(active_camera) = *self.active_camera.lock() { if let Ok(mut q) = self - .world.read() + .world .query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>( active_camera, ) @@ -859,7 +868,7 @@ pub static LOGGED: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(|| Mutex::ne fn show_entity_tree( ui: &mut egui::Ui, - nodes: &mut Vec<EntityNode>, + nodes: &mut [EntityNode], selected: &mut Option<hecs::Entity>, id_source: &str, ) { @@ -881,7 +890,7 @@ fn show_entity_tree( handle.ui(ui, |ui| { ui.label("📜"); }); - ui.label(format!("{name}")); + ui.label(name.to_string()); }); } EntityNode::Group { @@ -896,7 +905,7 @@ fn show_entity_tree( .show(ui, |ui| { show_entity_tree(ui, children, selected, name); }); - *collapsed = !header.body_returned.is_some(); + *collapsed = header.body_returned.is_none(); }); }); } @@ -949,12 +958,13 @@ fn show_entity_tree( pub enum UndoableAction { /// A change in transform. The entity + the old transform. Undoing will revert the transform Transform(hecs::Entity, Transform), + #[allow(dead_code)] // don't know why its considered dead code, todo: check the cause /// A spawn of the entity. Undoing will delete the entity Spawn(hecs::Entity), /// A change of label of the entity. Undoing will revert its label Label(hecs::Entity, String, EntityType), /// Removing a component. Undoing will add back the component. - RemoveComponent(hecs::Entity, ComponentType), + RemoveComponent(hecs::Entity, Box<ComponentType>), #[allow(dead_code)] CameraAction(UndoableCameraAction), } @@ -973,11 +983,11 @@ impl UndoableAction { // log::debug!("Undo Stack contents: {:#?}", undo_stack); } - pub fn undo(&self, world: Arc<RwLock<World>>) -> anyhow::Result<()> { + pub fn undo(&self, world: &mut World) -> anyhow::Result<()> { match self { UndoableAction::Transform(entity, transform) => { { - if let Ok(mut q) = world.write().query_one::<&mut Transform>(*entity) { + if let Ok(mut q) = world.query_one::<&mut Transform>(*entity) { if let Some(e_t) = q.get() { *e_t = *transform; log::debug!("Reverted transform"); @@ -991,19 +1001,17 @@ impl UndoableAction { } } UndoableAction::Spawn(entity) => { - { - if world.write().despawn(*entity).is_ok() { - log::debug!("Undid spawn by despawning entity {:?}", entity); - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to despawn entity {:?}", entity)) - } + if world.despawn(*entity).is_ok() { + log::debug!("Undid spawn by despawning entity {:?}", entity); + Ok(()) + } else { + Err(anyhow::anyhow!("Failed to despawn entity {:?}", entity)) } } UndoableAction::Label(entity, original_label, entity_type) => match entity_type { EntityType::Entity => { { - if let Ok(mut q) = world.write().query_one::<&mut AdoptedEntity>(*entity) { + if let Ok(mut q) = world.query_one::<&mut AdoptedEntity>(*entity) { if let Some(adopted) = q.get() { Arc::make_mut(&mut adopted.model).label = original_label.clone(); log::debug!( @@ -1026,7 +1034,7 @@ impl UndoableAction { } EntityType::Light => { { - if let Ok(mut q) = world.write().query_one::<&mut Light>(*entity) { + if let Ok(mut q) = world.query_one::<&mut Light>(*entity) { if let Some(adopted) = q.get() { adopted.label = original_label.clone(); log::debug!( @@ -1049,7 +1057,7 @@ impl UndoableAction { } EntityType::Camera => { { - if let Ok(mut q) = world.write().query_one::<&mut Camera>(*entity) { + if let Ok(mut q) = world.query_one::<&mut Camera>(*entity) { if let Some(adopted) = q.get() { adopted.label = original_label.clone(); log::debug!( @@ -1072,18 +1080,18 @@ impl UndoableAction { } }, UndoableAction::RemoveComponent(entity, c_type) => { - match c_type { + match &**c_type { ComponentType::Script(component) => { - { world.write().insert_one(*entity, component.clone())?; } + world.insert_one(*entity, component.clone())?; } ComponentType::Camera(camera, component, follow) => { if let Some(f) = follow { { - world.write() + world .insert(*entity, (camera.clone(), component.clone(), f.clone()))?; } } else { - { world.write().insert(*entity, (camera.clone(), component.clone()))?; } + world.insert(*entity, (camera.clone(), component.clone()))?; } } } @@ -1094,49 +1102,41 @@ impl UndoableAction { UndoableCameraAction::Speed(entity, speed) => { { if let Ok(mut q) = - world.read().query_one::<(&mut Camera, &mut CameraComponent)>(*entity) - { - if let Some((cam, comp)) = q.get() { + world.query_one::<(&mut Camera, &mut CameraComponent)>(*entity) + && let Some((cam, comp)) = q.get() { comp.speed = *speed; comp.update(cam); } - } } } UndoableCameraAction::Sensitivity(entity, sensitivity) => { { if let Ok(mut q) = - world.read().query_one::<(&mut Camera, &mut CameraComponent)>(*entity) - { - if let Some((cam, comp)) = q.get() { + world.query_one::<(&mut Camera, &mut CameraComponent)>(*entity) + && let Some((cam, comp)) = q.get() { comp.sensitivity = *sensitivity; comp.update(cam); } - } } } - UndoableCameraAction::FOV(entity, fov) => { + UndoableCameraAction::Fov(entity, fov) => { { if let Ok(mut q) = - world.read().query_one::<(&mut Camera, &mut CameraComponent)>(*entity) - { - if let Some((cam, comp)) = q.get() { + world.query_one::<(&mut Camera, &mut CameraComponent)>(*entity) + && let Some((cam, comp)) = q.get() { comp.fov_y = *fov; comp.update(cam); } - } } } UndoableCameraAction::Type(entity, camera_type) => { { if let Ok(mut q) = - world.read().query_one::<(&mut Camera, &mut CameraComponent)>(*entity) - { - if let Some((cam, comp)) = q.get() { + world.query_one::<(&mut Camera, &mut CameraComponent)>(*entity) + && let Some((cam, comp)) = q.get() { comp.camera_type = *camera_type; comp.update(cam); } - } } } }; @@ -1159,7 +1159,7 @@ pub enum Signal { Play, StopPlaying, AddComponent(hecs::Entity, EntityType), - RemoveComponent(hecs::Entity, ComponentType), + RemoveComponent(hecs::Entity, Box<ComponentType>), CreateEntity, LogEntities, Spawn(PendingSpawn2), @@ -1170,7 +1170,7 @@ pub enum Signal { // todo: deal with the Camera and create an implementation pub enum ComponentType { Script(ScriptComponent), - Camera(Camera, CameraComponent, Option<CameraFollowTarget>), + Camera(Box<Camera>, CameraComponent, Option<CameraFollowTarget>), } #[derive(Clone)] @@ -1195,10 +1195,10 @@ pub enum EditorState { } pub enum PendingSpawn2 { - CreateLight, - CreatePlane, - CreateCube, - CreateCamera, + Light, + Plane, + Cube, + Camera, } @@ -1224,6 +1224,9 @@ impl IsWorldLoadedYet { self.project_loaded && self.scene_loaded } + // I don't know whether this should be kept or removed, but + // im adding dead code just in case. + #[allow(dead_code)] pub fn is_project_ready(&self) -> bool { self.project_loaded } @@ -1,19 +1,16 @@ use super::*; use dropbear_engine::graphics::{InstanceRaw, RenderContext}; -use dropbear_engine::model::{Model, MODEL_CACHE}; -use dropbear_engine::starter::plane::PlaneBuilder; +use dropbear_engine::model::MODEL_CACHE; use dropbear_engine::{ entity::{AdoptedEntity, Transform}, lighting::{Light, LightComponent}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; -use egui::{Align2, Image}; use eucalyptus_core::camera::PlayerCamera; -use eucalyptus_core::states::{Value, WorldLoadingStatus}; -use eucalyptus_core::utils::{PROTO_TEXTURE, PendingSpawn}; -use eucalyptus_core::{logging, scripting, success_without_console, warn_without_console}; +use eucalyptus_core::states::{WorldLoadingStatus}; +use eucalyptus_core::{logging}; use hecs::Entity; use log; use parking_lot::Mutex; @@ -21,44 +18,45 @@ use tokio::sync::mpsc::unbounded_channel; use wgpu::Color; use wgpu::util::DeviceExt; use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; -use std::sync::LazyLock; +use crate::signal::SignalController; +use crate::spawn::PendingSpawnController; -pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = - LazyLock::new(|| Mutex::new(Vec::new())); - -#[async_trait::async_trait] impl Scene for Editor { - async fn load<'a>(&mut self, graphics: &mut RenderContext<'a>) { + fn load(&mut self, graphics: &mut RenderContext) { let (tx, rx) = unbounded_channel::<WorldLoadingStatus>(); + let (tx2, rx2) = oneshot::channel::<World>(); self.progress_tx = Some(rx); + self.world_receiver = Some(rx2); let graphics_shared = graphics.shared.clone(); - let world_clone = self.world.clone(); let active_camera_clone = self.active_camera.clone(); let project_path_clone = self.project_path.clone(); - let dock_state_clone = self.dock_state.clone(); + let dock_state_clone = Arc::new(Mutex::new(self.dock_state.clone())); - tokio::task::spawn_blocking(move || { - tokio::runtime::Handle::current().block_on(async { - { - if let Err(e) = Self::load_project_config(graphics_shared, Some(tx), world_clone, active_camera_clone, project_path_clone, dock_state_clone).await { - log::error!("Failed to load project config: {}", e); - } - } - }) + let handle = graphics.shared.future_queue.push(async move { + let mut temp_world = World::new(); + if let Err(e) = Self::load_project_config(graphics_shared, Some(tx), &mut temp_world, Some(tx2), active_camera_clone, project_path_clone, dock_state_clone).await { + log::error!("Failed to load project config: {}", e); + } }); + self.world_load_handle = Some(handle); + self.window = Some(graphics.shared.window.clone()); self.is_world_loaded.mark_scene_loaded(); } - async fn update<'a>(&mut self, dt: f32, graphics: &mut RenderContext<'a>) { - if !self.is_world_loaded.is_project_ready() { - log_once::debug_once!("Project is not loaded yet"); + fn update(&mut self, dt: f32, graphics: &mut RenderContext) { + if let Some(mut receiver) = self.world_receiver.take() { self.show_project_loading_window(&graphics.shared.get_egui_context()); - return; - } else { - log_once::debug_once!("Project has loaded successfully"); + if let Ok(loaded_world) = receiver.try_recv() { + self.world = loaded_world; + self.is_world_loaded.mark_project_loaded(); + log::info!("World received"); + } else { + self.world_receiver = Some(receiver); + return; + } } if !self.is_world_loaded.is_fully_loaded() { @@ -74,53 +72,19 @@ impl Scene for Editor { return; } - if let Some((_, tab)) = self.dock_state.lock().find_active_focused() { + match self.check_up(graphics.shared.clone(), graphics.shared.future_queue.clone()) { + Ok(_) => {} + Err(e) => { + fatal!("{}", e); + } + } + + if let Some((_, tab)) = self.dock_state.find_active_focused() { self.is_viewport_focused = matches!(tab, EditorTab::Viewport); } else { self.is_viewport_focused = false; } - let spawns_to_process = { - let mut pending_spawns = PENDING_SPAWNS.lock(); - std::mem::take(&mut *pending_spawns) - }; - - for spawn in spawns_to_process { - match AdoptedEntity::new( - graphics.shared.clone(), - &spawn.asset_path, - Some(&spawn.asset_name), - ) - .await - { - Ok(adopted) => { - let entity_id = { - self.world.write().spawn(( - adopted, - spawn.transform, - spawn.properties, - )) - }; - - self.selected_entity = Some(entity_id); - - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Spawn(entity_id), - ); - - log::info!( - "Successfully spawned {} with ID {:?}", - spawn.asset_name, - entity_id - ); - } - Err(e) => { - log::error!("Failed to spawn {}: {}", spawn.asset_name, e); - } - } - } - if matches!(self.editor_state, EditorState::Playing) { if self.input_state.pressed_keys.contains(&KeyCode::Escape) { self.signal = Signal::StopPlaying; @@ -128,7 +92,7 @@ impl Scene for Editor { let mut script_entities = Vec::new(); { - for (entity_id, script) in self.world.read() + for (entity_id, script) in self.world .query::<&mut ScriptComponent>() .iter() { @@ -156,7 +120,7 @@ impl Scene for Editor { if let Err(e) = self.script_manager.update_entity_script( entity_id, &script_name, - self.world.clone(), + &mut self.world, &self.input_state, dt, ) { @@ -193,11 +157,9 @@ impl Scene for Editor { let active_cam = self.active_camera.lock(); if let Some(active_camera) = *active_cam { - let world = self.world.read(); - if let Ok(mut query) = world + if let Ok(mut query) = self.world .query_one::<(&mut Camera, &CameraComponent)>(active_camera) - { - if let Some((camera, component)) = query.get() { + && let Some((camera, component)) = query.get() { match component.camera_type { CameraType::Debug => { DebugCamera::handle_keyboard_input(camera, &movement_keys); @@ -225,947 +187,14 @@ impl Scene for Editor { } } } - } } } - match &self.signal { - Signal::Paste(scene_entity) => match &scene_entity.model_path.ref_type { - dropbear_engine::utils::ResourceReferenceType::None => { - warn!("Resource has a reference type of None"); - self.signal = Signal::None; - } - dropbear_engine::utils::ResourceReferenceType::File(reference) => { - let cloned = { - let project_path = self.project_path.lock(); - project_path.clone() - }; - if let Some(proj_root) = cloned { - match &scene_entity - .model_path - .to_project_path(proj_root) - { - Some(v) => { - let entity = { - AdoptedEntity::new( - graphics.shared.clone(), - v, - Some(&scene_entity.label), - ).await - }; - - match entity - { - Ok(adopted) => { - let entity_id = { - self.world.write().spawn(( - adopted, - scene_entity.transform, - ModelProperties::default(), - )) - }; - - - self.selected_entity = Some(entity_id); - log::debug!( - "Successfully paste-spawned {} with ID {:?}", - scene_entity.label, - entity_id - ); - - success_without_console!("Paste!"); - self.signal = Signal::Copy(scene_entity.clone()); - } - Err(e) => { - warn!("Failed to paste-spawn {}: {}", scene_entity.label, e); - } - } - } - None => { - fatal!( - "Unable to convert resource reference [{}] to project related path", - reference - ); - self.signal = Signal::None; - } - }; - } else { - fatal!("Project path is not set"); - self.signal = Signal::None; - } - } - dropbear_engine::utils::ResourceReferenceType::Bytes(bytes) => { - let model = match Model::load_from_memory( - graphics.shared.clone(), - bytes, - Some(&scene_entity.label), - ) - .await - { - Ok(v) => v, - Err(e) => { - fatal!("Unable to load from memory: {}", e); - self.signal = Signal::None; - return; - } - }; - let adopted = AdoptedEntity::adopt(graphics.shared.clone(), model).await; - - let entity_id = { - self.world.write().spawn(( - adopted, - scene_entity.transform, - ModelProperties::default(), - )) - }; - - self.selected_entity = Some(entity_id); - log::debug!( - "Successfully paste-spawned {} with ID {:?}", - scene_entity.label, - entity_id - ); - - success_without_console!("Paste!"); - self.signal = Signal::Copy(scene_entity.clone()); - } - _ => { - warn!("Unable to copy, not of bytes or path"); - self.signal = Signal::None; - return; - } - }, - Signal::Delete => { - if let Some(sel_e) = &self.selected_entity { - { - let is_viewport_cam = if let Ok(mut q) = self.world.read() - .query_one::<&CameraComponent>(*sel_e) - { - if let Some(c) = q.get() { - if matches!(c.camera_type, CameraType::Debug) { - true - } else { - false - } - } else { - false - } - } else { - false - }; - if is_viewport_cam { - warn!("You can't delete the viewport camera"); - self.signal = Signal::None; - } else { - match self.world.write().despawn(*sel_e) { - Ok(_) => { - info!("Decimated entity"); - self.signal = Signal::None; - } - Err(e) => { - warn!("Failed to delete entity: {}", e); - self.signal = Signal::None; - } - } - // println!("is world still locked here [{}]: {}", file!(), self.world.is_locked_exclusive()) - } - } - } - } - Signal::Undo => { - { - if let Some(action) = self.undo_stack.pop() { - match action.undo(self.world.clone()) { - Ok(_) => { - info!("Undid action"); - } - Err(e) => { - warn!("Failed to undo action: {}", e); - } - } - } else { - warn_without_console!("Nothing to undo"); - log::debug!("No undoable actions in stack"); - } - self.signal = Signal::None; - } - } - Signal::None => {} - Signal::Copy(_) => {} - Signal::ScriptAction(action) => match action { - ScriptAction::AttachScript { - script_path, - script_name, - } => { - if let Some(selected_entity) = self.selected_entity { - match scripting::move_script_to_src(script_path) { - Ok(moved_path) => { - let new_script = ScriptComponent { - name: script_name.clone(), - path: moved_path.clone(), - }; - - let replaced = { - let world = self.world.read(); - if let Ok(mut sc) = world.get::<&mut ScriptComponent>(selected_entity) { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { - false - } - }; - - if !replaced { - match scripting::attach_script_to_entity( - self.world.clone(), - selected_entity, - new_script.clone(), - ) { - Ok(_) => { - } - Err(e) => { - fatal!( - "Failed to attach script to entity {:?}: {}", - selected_entity, - e - ); - self.signal = Signal::None; - return; - } - } - } - - { - if let Err(e) = scripting::convert_entity_to_group( - self.world.clone(), - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - } - - success!( - "{} script '{}' at {} to entity {:?}", - if replaced { "Reattached" } else { "Attached" }, - script_name, - moved_path.display(), - selected_entity - ); - } - Err(e) => { - fatal!("Move failed: {}", e); - } - } - } else { - fatal!("AttachScript requested but no entity is selected"); - } - - self.signal = Signal::None; - } - ScriptAction::CreateAndAttachScript { - script_path, - script_name, - } => { - if let Some(selected_entity) = self.selected_entity { - let new_script = ScriptComponent { - name: script_name.clone(), - path: script_path.clone(), - }; - - let replaced = { - let world = self.world.read(); - if let Ok(mut sc) = world.get::<&mut ScriptComponent>(selected_entity) { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { - false - } - }; - - if !replaced { - match scripting::attach_script_to_entity( - self.world.clone(), - selected_entity, - new_script.clone(), - ) { - Ok(_) => { - } - Err(e) => { - fatal!("Failed to attach new script: {}", e); - self.signal = Signal::None; - return; - } - } - } - - { - if let Err(e) = scripting::convert_entity_to_group( - self.world.clone(), - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - } - - success!( - "{} new script '{}' at {} to entity {:?}", - if replaced { "Replaced" } else { "Attached" }, - script_name, - script_path.display(), - selected_entity - ); - } else { - warn_without_console!("No selected entity to attach new script"); - log::warn!("CreateAndAttachScript requested but no entity is selected"); - } - self.signal = Signal::None; - } - ScriptAction::RemoveScript => { - if let Some(selected_entity) = self.selected_entity { - let mut success = false; - let mut comp = ScriptComponent::default(); - { - if let Ok(script) = self.world.write() - .remove_one::<ScriptComponent>(selected_entity) - { - success!("Removed script from entity {:?}", selected_entity); - success = true; - comp = script.clone(); - } else { - warn!("No script component found on entity {:?}", selected_entity); - } - } - - if success { - if let Err(e) = scripting::convert_entity_to_group( - self.world.clone(), - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - log::debug!("Pushing remove component to undo stack"); - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::RemoveComponent( - selected_entity, - ComponentType::Script(comp), - ), - ); - } - } else { - warn!("No entity selected to remove script from"); - } - - self.signal = Signal::None; - } - ScriptAction::EditScript => { - if let Some(selected_entity) = self.selected_entity { - let script_opt = { - let world = self.world.read(); - if let Ok(mut q) = world.query_one::<&ScriptComponent>(selected_entity) { - q.get().cloned() - } else { - None - } - }; - - if let Some(script) = script_opt { - match open::that(script.path.clone()) { - Ok(()) => { - success!("Opened {}", script.name) - } - Err(e) => { - warn!("Error while opening {}: {}", script.name, e); - } - } - } else { - warn!("No script component found on entity {:?}", selected_entity); - } - } else { - warn!("No entity selected to edit script"); - } - self.signal = Signal::None; - } - }, - Signal::Play => { - let has_player_camera_target = self - .world.read() - .query::<(&Camera, &CameraComponent, &CameraFollowTarget)>() - .iter() - .any(|(_, (_, comp, _))| matches!(comp.camera_type, CameraType::Player)); - - if has_player_camera_target { - if let Err(e) = self.create_backup() { - fatal!("Failed to create play mode backup: {}", e); - self.signal = Signal::None; - return; - } - - self.editor_state = EditorState::Playing; - - self.switch_to_player_camera(); - - let mut script_entities = Vec::new(); - { - for (entity_id, script) in self.world.read() - .query::<&ScriptComponent>() - .iter() - { - script_entities.push((entity_id, script.clone())); - } - } - - for (entity_id, script) in script_entities { - log::debug!( - "Initialising entity script [{}] from path: {}", - script.name, - script.path.display() - ); - - let bytes = match tokio::fs::read_to_string(&script.path).await { - Ok(val) => val, - Err(e) => { - fatal!( - "Unable to read script {} to bytes because {}", - &script.path.display(), - e - ); - self.signal = Signal::None; - return; - } - }; - - match self.script_manager.load_script( - &script - .path - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - bytes, - ) { - Ok(script_name) => { - if let Err(e) = self.script_manager.init_entity_script( - entity_id, - &script_name, - self.world.clone(), - &self.input_state, - ) { - log::warn!( - "Failed to initialise script '{}' for entity {:?}: {}", - script.name, - entity_id, - e - ); - self.signal = Signal::StopPlaying; - } else { - success_without_console!( - "You are in play mode now! Press Escape to exit" - ); - log::info!("You are in play mode now! Press Escape to exit"); - } - } - Err(e) => { - // todo: proper error menu - fatal!("Failed to load script '{}': {}", script.name, e); - self.signal = Signal::StopPlaying; - } - } - } - } else { - fatal!("Unable to build: Player camera not attached to an entity"); - } - - self.signal = Signal::None; - } - 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); - } - - self.editor_state = EditorState::Editing; - - self.switch_to_debug_camera(); - - // already kills itself - // for (entity_id, _) in Arc::get_mut(&mut self.world).unwrap().query::<&ScriptComponent>().iter() { - // self.script_manager.remove_entity_script(entity_id); - // } - - success!("Exited play mode"); - log::info!("Back to the editor you go..."); - - self.signal = Signal::None; - } - Signal::CameraAction(action) => match action { - CameraAction::SetPlayerTarget { entity, offset } => { - let player_camera = self - .world.read() - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(e, (_, comp))| { - if matches!(comp.camera_type, CameraType::Player) { - Some(e) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - let mut follow_target = (false, CameraFollowTarget::default()); - if let Ok(mut query) = self.world.read() - .query_one::<&AdoptedEntity>(*entity) - { - if let Some(adopted) = query.get() { - follow_target = ( - true, - CameraFollowTarget { - follow_target: adopted.model.label.to_string(), - offset: *offset, - }, - ); - } - } - - { - if follow_target.0 { - let _ = self.world.write() - .insert_one(camera_entity, follow_target); - info!("Set player camera target to entity {:?}", entity); - } - } - } - self.signal = Signal::None; - } - CameraAction::ClearPlayerTarget => { - let player_camera = self - .world.read() - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(e, (_, comp))| { - if matches!(comp.camera_type, CameraType::Player) { - Some(e) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - { - let _ = self.world.write() - .remove_one::<CameraFollowTarget>(camera_entity); - } - } - info!("Cleared player camera target"); - self.signal = Signal::None; - } - }, - Signal::AddComponent(entity, e_type) => { - match e_type { - EntityType::Entity => { - if let Ok(mut q) = self.world.read() - .query_one::<&AdoptedEntity>(*entity) - { - if let Some(e) = q.get() { - let mut local_signal: Option<Signal> = None; - let label = e.model.label.clone(); - let mut show = true; - egui::Window::new(format!("Add component for {}", label)) - .title_bar(true) - .open(&mut show) - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .show(&graphics.shared.get_egui_context(), |ui| { - if ui - .add_sized( - [ui.available_width(), 30.0], - egui::Button::new("Scripting"), - ) - .clicked() - { - log::debug!( - "Adding scripting component to entity [{}]", - label - ); - { - if let Err(e) = self.world.write() - .insert_one(*entity, ScriptComponent::default()) - { - warn!( - "Failed to add scripting component to entity: {}", - e - ); - } else { - success!("Added the scripting component"); - } - } - local_signal = Some(Signal::None); - } - if ui - .add_sized( - [ui.available_width(), 30.0], - egui::Button::new("Camera"), - ) - .clicked() - { - log::debug!( - "Adding camera component to entity [{}]", - label - ); - - let has_camera = self.world.read() - .query_one::<(&Camera, &CameraComponent)>(*entity) - .is_ok(); - - if has_camera { - warn!( - "Entity [{}] already has a camera component", - label - ); - } else { - let camera = Camera::predetermined( - graphics.shared.clone(), - Some(&format!("{} Camera", label)), - ); - let component = CameraComponent::new(); - - { - if let Err(e) = self.world.write() - .insert(*entity, (camera, component)) - { - warn!( - "Failed to add camera component to entity: {}", - e - ); - } else { - success!("Added the camera component"); - } - } - } - local_signal = Some(Signal::None); - } - }); - if !show { - self.signal = Signal::None; - } - if let Some(signal) = local_signal { - self.signal = signal - } - } - } else { - log_once::warn_once!( - "Failed to add component to entity: no entity component found" - ); - } - } - EntityType::Light => { - { - if let Ok(mut q) = self.world.read() - .query_one::<&Light>(*entity) - { - if let Some(light) = q.get() { - let mut show = true; - egui::Window::new(format!("Add component for {}", light.label)) - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .open(&mut show) - .title_bar(true) - .show(&graphics.shared.get_egui_context(), |ui| { - if ui - .add_sized( - [ui.available_width(), 30.0], - egui::Button::new("Scripting"), - ) - .clicked() - { - log::debug!( - "Adding scripting component to light [{}]", - light.label - ); - - success!( - "Added the scripting component to light [{}]", - light.label - ); - self.signal = Signal::None; - } - }); - if !show { - self.signal = Signal::None; - } - } - } else { - log_once::warn_once!( - "Failed to add component to light: no light component found" - ); - } - } - } - EntityType::Camera => { - { - if let Ok(mut q) = self.world.write() - .query_one::<(&Camera, &CameraComponent)>(*entity) - { - if let Some((cam, _comp)) = q.get() { - let mut show = true; - egui::Window::new(format!("Add component for {}", cam.label)) - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .open(&mut show) - .title_bar(true) - .show(&graphics.shared.get_egui_context(), |ui| { - egui_extras::install_image_loaders(ui.ctx()); - ui.add(Image::from_bytes( - "bytes://theres_nothing.jpg", - include_bytes!("../../../resources/theres_nothing.jpg"), - )); - ui.label("Theres nothing..."); - // // scripting - // if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { - // log::debug!("Adding scripting component to camera [{}]", cam.label); - - // success!("Added the scripting component to camera [{}]", cam.label); - // self.signal = Signal::None; - // } - }); - if !show { - self.signal = Signal::None; - } - } - } else { - log_once::warn_once!( - "Failed to add component to light: no light component found" - ); - } - } - } - } - } - Signal::RemoveComponent(entity, c_type) => - {match c_type { - ComponentType::Script(_) => { - match self.world.write() - .remove_one::<ScriptComponent>(*entity) - { - Ok(component) => { - success!("Removed script component from entity {:?}", entity); - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::RemoveComponent( - *entity, - ComponentType::Script(component), - ), - ); - } - Err(e) => { - warn!("Failed to remove script component from entity: {}", e); - } - }; - self.signal = Signal::None; - } - ComponentType::Camera(_, _, follow) => { - if let Some(_) = follow { - match self.world.write().remove::<( - Camera, - CameraComponent, - CameraFollowTarget, - )>( - *entity - ) { - Ok(component) => { - success!("Removed camera component from entity {:?}", entity); - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::RemoveComponent( - *entity, - ComponentType::Camera( - component.0, - component.1, - Some(component.2), - ), - ), - ); - } - Err(e) => { - warn!("Failed to remove camera component from entity: {}", e); - } - }; - } else { - match self.world.write() - .remove::<(Camera, CameraComponent)>(*entity) - { - Ok(component) => { - success!("Removed camera component from entity {:?}", entity); - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::RemoveComponent( - *entity, - ComponentType::Camera(component.0, component.1, None), - ), - ); - } - Err(e) => { - warn!("Failed to remove script component from entity: {}", e); - } - }; - } - } - }}, - Signal::CreateEntity => { - let mut show = true; - egui::Window::new("Add Entity") - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .open(&mut show) - .title_bar(true) - .show(&graphics.shared.get_egui_context(), |ui| { - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Model")).clicked() { - log::debug!("Creating new model"); - warn!("Instead of using the `Add Entity` window, double click on the imported model in the asset \n\ - viewer to import a new model, then tweak the settings to how you wish after!"); - self.signal = Signal::None; - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Light")).clicked() { - log::debug!("Creating new lighting"); - self.signal = Signal::Spawn(PendingSpawn2::CreateLight); - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() { - log::debug!("Creating new plane"); - self.signal = Signal::Spawn(PendingSpawn2::CreatePlane); - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { - log::debug!("Creating new cube"); - self.signal = Signal::Spawn(PendingSpawn2::CreateCube); - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { - log::debug!("Creating new cube"); - self.signal = Signal::Spawn(PendingSpawn2::CreateCamera); - } - }); - if !show { - self.signal = Signal::None; - } - } - Signal::LogEntities => { - log::debug!("===================="); - let mut counter = 0; - for entity in self.world.read().iter() { - if let Some(entity) = entity.get::<&AdoptedEntity>() { - log::info!("Model: {:?}", entity.model.label); - log::info!(" |-> Using model: {:?}", entity.model.id); - } - - if let Some(entity) = entity.get::<&Light>() { - log::info!("Light: {:?}", entity.cube_model.label); - log::info!(" |-> Using model: {:?}", entity.cube_model.id); - } - - if let Some(_) = entity.get::<&Camera>() { - log::info!("Camera"); - } - counter += 1; - } - log::debug!("===================="); - info!("Total entity count: {}", counter); - self.signal = Signal::None; - } - Signal::Spawn(entity_type) => { - match entity_type { - crate::editor::PendingSpawn2::CreateLight => { - let transform = Transform::new(); - let component = LightComponent::default(); - let light = Light::new( - graphics.shared.clone(), - &component, - &transform, - Some("Light"), - ) - .await; - { - self.world.write() - .spawn((light, component, transform)); - } - success!("Created new light"); - } - crate::editor::PendingSpawn2::CreatePlane => { - let plane = PlaneBuilder::new() - .with_size(500.0, 200.0) - .build(graphics.shared.clone(), PROTO_TEXTURE, Some("Plane")) - .await - .unwrap(); - - let transform = Transform::new(); - let mut props = ModelProperties::new(); - props - .custom_properties - .insert("width".to_string(), Value::Float(500.0)); - props - .custom_properties - .insert("height".to_string(), Value::Float(200.0)); - props - .custom_properties - .insert("tiles_x".to_string(), Value::Int(500)); - props - .custom_properties - .insert("tiles_z".to_string(), Value::Int(200)); - { - self.world.write() - .spawn((plane, transform, props)); - } - success!("Created new plane"); - } - crate::editor::PendingSpawn2::CreateCube => { - let model = Model::load_from_memory( - graphics.shared.clone(), - include_bytes!("../../../resources/cube.glb"), - Some("Cube"), - ) - .await; - match model { - Ok(model) => { - let cube = AdoptedEntity::adopt( - graphics.shared.clone(), - model, - // Some("Cube") - ) - .await; - { - self.world.write().spawn(( - cube, - Transform::new(), - ModelProperties::new(), - )); - } - } - Err(e) => { - fatal!("Failed to load cube model: {}", e); - } - } - success!("Created new cube"); - } - crate::editor::PendingSpawn2::CreateCamera => { - let camera = Camera::predetermined(graphics.shared.clone(), None); - let component = CameraComponent::new(); - { - self.world.write() - .spawn((camera, component)); - } - success!("Created new camera"); - } - } - self.signal = Signal::None; + match self.run_signal(graphics.shared.clone()) { + Ok(_) => {} + Err(e) => { + fatal!("{}", e); } } @@ -1177,19 +206,16 @@ impl Scene for Editor { { let active_cam = self.active_camera.lock(); if let Some(active_camera) = *active_cam { - let world = self.world.read(); - if let Ok(mut query) = world.query_one::<&mut Camera>(active_camera) { - if let Some(camera) = query.get() { + if let Ok(mut query) = self.world.query_one::<&mut Camera>(active_camera) + && let Some(camera) = query.get() { camera.aspect = new_aspect; } - } } } let camera_follow_data: Vec<(Entity, String, glam::Vec3)> = { - let world = self.world.read(); - world + self.world .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() .iter() .filter_map(|(entity_id, (_, _, follow_target))| { @@ -1207,8 +233,7 @@ impl Scene for Editor { for (camera_entity, target_label, offset) in camera_follow_data { let target_position = { - let world = self.world.read(); - world + self.world .query::<(&AdoptedEntity, &Transform)>() .iter() .find_map(|(_, (adopted, transform))| { @@ -1222,20 +247,17 @@ impl Scene for Editor { if let Some(pos) = target_position { - let world = self.world.read(); - if let Ok(mut query) = world.query_one::<&mut Camera>(camera_entity) { - if let Some(camera) = query.get() { + if let Ok(mut query) = self.world.query_one::<&mut Camera>(camera_entity) + && let Some(camera) = query.get() { camera.eye = pos + offset.as_dvec3(); camera.target = pos; } - } } } { - let world = self.world.read(); - for (_entity_id, (camera, component)) in world + for (_entity_id, (camera, component)) in self.world .query::<(&mut Camera, &mut CameraComponent)>() .iter() { @@ -1246,8 +268,7 @@ impl Scene for Editor { { { - let mut world = self.world.write(); - let query = world + let query = self.world .query_mut::<(&mut AdoptedEntity, &Transform)>(); for (_, (entity, transform)) in query { entity.update(graphics.shared.clone(), transform); @@ -1256,9 +277,8 @@ impl Scene for Editor { { - let mut world = self.world.write(); let light_query = - world + self.world .query_mut::<(&mut LightComponent, &Transform, &mut Light)>(); for (_, (light_component, transform, light)) in light_query { light.update(light_component, transform); @@ -1268,22 +288,20 @@ impl Scene for Editor { } { - let mut world = self.world.read(); self.light_manager.update( graphics.shared.clone(), - &mut world, + &self.world, ); } - if self.dep_installer.is_installing { self.dep_installer - .show_installation_window(&mut graphics.shared.get_egui_context()); + .show_installation_window(&graphics.shared.get_egui_context()); } self.dep_installer.update_progress(); } - async fn render<'a>(&mut self, graphics: &mut RenderContext<'a>) { + fn render(&mut self, graphics: &mut RenderContext) { // cornflower blue let color = Color { r: 100.0 / 255.0, @@ -1292,10 +310,10 @@ impl Scene for Editor { a: 1.0, }; - self.color = color.clone(); - self.size = graphics.shared.viewport_texture.size.clone(); + self.color = color; + self.size = graphics.shared.viewport_texture.size; self.texture_id = Some(*graphics.shared.texture_id.clone()); - { self.show_ui(&graphics.shared.get_egui_context()).await; } + { self.show_ui(&graphics.shared.get_egui_context()); } self.window = Some(graphics.shared.window.clone()); logging::render(&graphics.shared.get_egui_context()); @@ -1303,13 +321,8 @@ impl Scene for Editor { log_once::debug_once!("Found render pipeline"); if let Some(active_camera) = *self.active_camera.lock() { let cam = { - let world = self.world.read(); - if let Ok(mut query) = world.query_one::<&Camera>(active_camera) { - if let Some(camera) = query.get() { - Some(camera.clone()) - } else { - None - } + if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { + query.get().cloned() } else { None } @@ -1317,9 +330,8 @@ impl Scene for Editor { if let Some(camera) = cam { let lights = { - let world = self.world.read(); let mut lights = Vec::new(); - let mut light_query = world.query::<(&Light, &LightComponent)>(); + let mut light_query = self.world.query::<(&Light, &LightComponent)>(); for (_, (light, comp)) in light_query.iter() { lights.push((light.clone(), comp.clone())); } @@ -1328,9 +340,8 @@ impl Scene for Editor { let entities = { - let world = self.world.read(); let mut entities = Vec::new(); - let mut entity_query = world.query::<&AdoptedEntity>(); + let mut entity_query = self.world.query::<&AdoptedEntity>(); for (_, entity) in entity_query.iter() { entities.push(entity.clone()); } @@ -1363,7 +374,7 @@ impl Scene for Editor { let instance_raw = entity.instance.to_raw(); model_batches .entry(model_ptr) - .or_insert(Vec::new()) + .or_default() .push(instance_raw); } @@ -5,11 +5,15 @@ mod debug; mod editor; mod menu; mod utils; +mod spawn; +mod signal; use clap::{Arg, Command}; -use std::{cell::RefCell, fs, path::PathBuf, rc::Rc}; - -use dropbear_engine::{WindowConfiguration, scene}; +use dropbear_engine::{scene, WindowConfiguration}; +use std::{fs, path::PathBuf, rc::Rc}; +use std::sync::Arc; +use parking_lot::RwLock; +use dropbear_engine::future::FutureQueue; pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { name: "Eucalyptus", @@ -114,11 +118,13 @@ async fn main() -> anyhow::Result<()> { max_fps: dropbear_engine::App::NO_FPS_CAP, app_info: APP_INFO, }; + + let future_queue = Arc::new(FutureQueue::new()); - let main_menu = Rc::new(RefCell::new(crate::menu::MainMenu::new())); - let editor = Rc::new(RefCell::new(crate::editor::Editor::new())); + let main_menu = Rc::new(RwLock::new(menu::MainMenu::new())); + let editor = Rc::new(RwLock::new(editor::Editor::new())); - let _app = dropbear_engine::run_app!(config, |mut scene_manager, mut input_manager| { + dropbear_engine::run_app!(config, Some(future_queue), |mut scene_manager, mut input_manager| { scene::add_scene_with_input( &mut scene_manager, &mut input_manager, @@ -136,8 +142,7 @@ async fn main() -> anyhow::Result<()> { (scene_manager, input_manager) }) - .await - .unwrap(); + .await?; } _ => unreachable!(), } @@ -152,13 +157,11 @@ fn find_eucp_file() -> Result<PathBuf, String> { let mut eucp_files = Vec::new(); for entry in entries { - if let Ok(entry) = entry { - if let Some(file_name) = entry.file_name().to_str() { - if file_name.ends_with(".eucp") { + if let Ok(entry) = entry + && let Some(file_name) = entry.file_name().to_str() + && file_name.ends_with(".eucp") { eucp_files.push(entry.path()); } - } - } } match eucp_files.len() { @@ -1,52 +1,56 @@ use std::{ fs, - sync::mpsc::{self, Receiver}, + path::PathBuf, }; - +use std::sync::Arc; use anyhow::anyhow; use dropbear_engine::{ graphics::RenderContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}, + future::{FutureHandle, FutureQueue}, }; use egui::{self, FontId, Frame, RichText}; use egui_toast_fork::{ToastOptions, Toasts}; -use gilrs; use git2::Repository; use log::{self, debug}; +use rfd::FileDialog; // ← Sync version — no async needed use winit::{ dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode, }; - use eucalyptus_core::states::{PROJECT, ProjectConfig}; +use tokio::sync::watch; + +#[derive(Debug, Clone)] +pub enum ProjectProgress { + Step { + progress: f32, + message: String, + }, + Error(String), + Done, +} #[derive(Default)] pub struct MainMenu { scene_command: SceneCommand, show_new_project: bool, project_name: String, - project_path: Option<std::path::PathBuf>, + project_path: Option<PathBuf>, project_error: Option<Vec<String>>, - progress_rx: Option<Receiver<ProjectProgress>>, - + project_progress_rx: Option<watch::Receiver<ProjectProgress>>, show_progress: bool, progress: f32, progress_message: String, + + // ❌ REMOVED: file_dialog: Option<FutureHandle>, + project_creation_handle: Option<FutureHandle>, // ← Keep — project creation is async + toast: Toasts, is_in_file_dialogue: bool, } -pub enum ProjectProgress { - Step { - progress: f32, - message: String, - }, - #[allow(dead_code)] - Error(String), - Done, -} - impl MainMenu { pub fn new() -> Self { Self { @@ -58,111 +62,150 @@ impl MainMenu { } } - fn start_project_creation(&mut self) { - let (tx, rx) = mpsc::channel(); + fn start_project_creation(&mut self, queue: Arc<FutureQueue>) { let project_name = self.project_name.clone(); let project_path = self.project_path.clone(); - self.progress_rx = Some(rx); + let (progress_tx, progress_rx) = watch::channel(ProjectProgress::Step { + progress: 0.0, + message: "Starting project creation...".to_string(), + }); + + self.project_progress_rx = Some(progress_rx); self.show_progress = true; self.progress = 0.0; - self.progress_message = "Starting project creation...".to_string(); - std::thread::spawn(move || { + let handle = queue.push(async move { let mut errors = Vec::new(); let folders = [ - ("git", 0.1, "Creating a git folder..."), + ("git", 0.1, "Initializing git repository..."), ("src", 0.2, "Creating src folder..."), ("resources/models", 0.3, "Creating models folder..."), - ("resources/shaders", 0.4, "Creating shader folder..."), + ("resources/shaders", 0.4, "Creating shaders folder..."), ("resources/textures", 0.5, "Creating textures folder..."), - ("src2", 0.6, "Creating project config file..."), - ("scenes", 0.7, "Creating the scenes folder"), + ("src2", 0.6, "Generating project config..."), + ("scenes", 0.7, "Creating scenes folder..."), ]; if let Some(path) = &project_path { for (folder, progress, message) in folders { - tx.send(ProjectProgress::Step { + let _ = progress_tx.send(ProjectProgress::Step { progress, message: message.to_string(), - }) - .ok(); + }); let full_path = path.join(folder); - let result: anyhow::Result<()> = if folder == "src" { - if !full_path.exists() { - fs::create_dir(&full_path) - .map_err(|e| anyhow::anyhow!(e)) - .map(|_| ()) - } else { - Ok(()) - } - } else if folder == "git" { - match Repository::init(path) { - Ok(_) => Ok(()), - Err(e) => { - if matches!(e.code(), git2::ErrorCode::Exists) { - log::warn!("Git repository already exists"); - Ok(()) - } else { - Err(anyhow!(e)) + let result: anyhow::Result<()> = match folder { + "git" => { + match Repository::init(path) { + Ok(_) => Ok(()), + Err(e) => { + if matches!(e.code(), git2::ErrorCode::Exists) { + log::warn!("Git repository already exists"); + Ok(()) + } else { + Err(anyhow!(e)) + } } } } - } else if folder == "src2" { - if let Some(path) = &project_path { - let mut config = ProjectConfig::new(project_name.clone(), &path); + "src2" => { + let mut config = ProjectConfig::new(project_name.clone(), path); let _ = config.write_to_all(); let mut global = PROJECT.write(); *global = config; Ok(()) - } else { - Err(anyhow!("Project path not found")) } - } else { - if !full_path.exists() { - fs::create_dir_all(&full_path) - .map_err(|e| anyhow!(e)) - .map(|_| ()) - } else { - log::warn!("{:?} already exists", full_path); - Ok(()) + _ => { + if !full_path.exists() { + fs::create_dir_all(&full_path) + .map_err(|e| anyhow::anyhow!(e)) + .map(|_| ()) + } else { + log::warn!("{:?} already exists", full_path); + Ok(()) + } } }; + if let Err(e) = result { - tx.send(ProjectProgress::Error(e.to_string())).ok(); + let _ = progress_tx.send(ProjectProgress::Error(e.to_string())); errors.push(e); } } - tx.send(ProjectProgress::Step { + + let _ = progress_tx.send(ProjectProgress::Step { progress: 1.0, - message: "Project creation complete!".to_string(), - }) - .ok(); + message: "Finalizing project...".to_string(), + }); - tx.send(ProjectProgress::Done).ok(); + if errors.is_empty() { + let _ = progress_tx.send(ProjectProgress::Done); + Ok(()) // Success + } else { + Err(anyhow!("Project creation failed with {} errors", errors.len())) + } + } else { + let _ = progress_tx.send(ProjectProgress::Error("Project path not set".to_string())); + Err(anyhow!("Project path not set")) } }); - log::debug!("Starting project creation"); + self.project_creation_handle = Some(handle); + queue.poll(); + debug!("Starting project creation"); } } -#[async_trait::async_trait] impl Scene for MainMenu { - async fn load<'a>(&mut self, _graphics: &mut RenderContext<'a>) { - log::info!("Loaded menu scene"); + fn load(&mut self, _graphics: &mut RenderContext) { + log::info!("Loaded main menu scene"); } - async fn update<'a>(&mut self, _dt: f32, _graphics: &mut RenderContext<'a>) {} + fn update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + + fn render(&mut self, graphics: &mut RenderContext) { + #[allow(clippy::collapsible_if)] + if let Some(handle) = self.project_creation_handle.as_ref() { + if let Some(result) = graphics.shared.future_queue.exchange_owned_as::<anyhow::Result<()>>(handle) { + self.project_creation_handle = None; + + if result.is_ok() { + log::info!("Project created successfully!"); + self.show_new_project = false; + self.show_progress = false; + self.scene_command = SceneCommand::SwitchScene("editor".to_string()); + } else { + log::error!("Project creation failed"); + } + } + } + + #[allow(clippy::collapsible_if)] + if let Some(rx) = self.project_progress_rx.as_ref() { + if let Ok(true) = rx.has_changed() { + let progress = rx.borrow().clone(); + match progress { + ProjectProgress::Step { progress, message } => { + self.progress = progress; + self.progress_message = message; + } + ProjectProgress::Error(err) => { + self.project_error.get_or_insert_with(Vec::new).push(err); + } + ProjectProgress::Done => {} + } + } + } - async fn render<'a>(&mut self, graphics: &mut RenderContext<'a>) { let screen_size: (f32, f32) = ( graphics.shared.window.inner_size().width as f32 - 100.0, graphics.shared.window.inner_size().height as f32 - 100.0, ); let egui_ctx = graphics.shared.get_egui_context(); let mut local_open_project = false; + let mut local_select_project = false; + egui::CentralPanel::default() .frame(Frame::new()) .show(&egui_ctx, |ui| { @@ -172,9 +215,10 @@ impl Scene for MainMenu { ui.add_space(40.0); let button_size = egui::vec2(300.0, 60.0); + let is_busy = self.is_in_file_dialogue || self.project_creation_handle.is_some(); if ui - .add_sized(button_size, egui::Button::new("New Project")) + .add_enabled(!is_busy, egui::Button::new("New Project").min_size(button_size)) .clicked() { log::debug!("Creating new project"); @@ -183,7 +227,7 @@ impl Scene for MainMenu { ui.add_space(20.0); if ui - .add_sized(button_size, egui::Button::new("Open Project")) + .add_enabled(!is_busy, egui::Button::new("Open Project").min_size(button_size)) .clicked() { local_open_project = true; @@ -191,7 +235,7 @@ impl Scene for MainMenu { ui.add_space(20.0); if ui - .add_sized(button_size, egui::Button::new("Settings")) + .add_enabled(!is_busy, egui::Button::new("Settings").min_size(button_size)) .clicked() { log::debug!("Settings (not implemented)"); @@ -199,54 +243,57 @@ impl Scene for MainMenu { ui.add_space(20.0); if ui - .add_sized(button_size, egui::Button::new("Quit")) + .add_enabled(!is_busy, egui::Button::new("Quit").min_size(button_size)) .clicked() { - self.scene_command = SceneCommand::Quit + self.scene_command = SceneCommand::Quit; } ui.add_space(20.0); }); }); if local_open_project { - log::debug!("Opening project"); + debug!("Opening project dialog"); self.is_in_file_dialogue = true; - if let Some(path) = rfd::AsyncFileDialog::new() + + if let Some(path) = FileDialog::new() .add_filter("Eucalyptus Configuration Files", &["eucp"]) .pick_file() - .await { - match ProjectConfig::read_from(&path.into()) { + match ProjectConfig::read_from(&path) { Ok(config) => { - log::info!("Loaded project!"); + log::info!("Loaded project: {:?}", path); let mut global = PROJECT.write(); *global = config; - // println!("Loaded config info: {:#?}", global); - self.scene_command = SceneCommand::SwitchScene(String::from("editor")); + self.scene_command = SceneCommand::SwitchScene("editor".to_string()); } Err(e) => { - if e.to_string().contains("missing field") { - self.toast.add(egui_toast_fork::Toast { + let error_msg = if e.to_string().contains("missing field") { + "Project version is outdated. Please update your .eucp file." + } else { + &e.to_string() + }; + + self.toast.add(egui_toast_fork::Toast { kind: egui_toast_fork::ToastKind::Error, - text: format!("Your project version is not up to date with the current project version").into(), + text: error_msg.to_string().into(), options: ToastOptions::default() - .duration_in_seconds(10.0) + .duration_in_seconds(8.0) .show_progress(true), ..Default::default() }); - log::error!("Failed to load project: {}", e); - } + log::error!("Failed to load project: {}", e); } - }; + } } else { - log::warn!("File dialog returned \"None\""); + log::info!("User cancelled file dialog"); } + self.is_in_file_dialogue = false; } let mut show_new_project = self.show_new_project; - let mut local_select_project = false; - egui::Window::new("Create new project") + egui::Window::new("Create New Project") .open(&mut show_new_project) .resizable(true) .collapsible(false) @@ -255,11 +302,10 @@ impl Scene for MainMenu { ui.vertical(|ui| { ui.heading("Project Name:"); ui.add_space(5.0); - ui.text_edit_singleline(&mut self.project_name); ui.add_space(10.0); - ui.heading("Project Location: "); + ui.heading("Project Location:"); ui.add_space(5.0); if let Some(ref path) = self.project_path { @@ -267,71 +313,54 @@ impl Scene for MainMenu { ui.add_space(5.0); } - ui.add_space(5.0); if ui.button("Choose Location").clicked() { local_select_project = true; } + ui.add_space(10.0); let can_create = self.project_path.is_some() && !self.project_name.is_empty(); if ui - .add_enabled(can_create, egui::Button::new("Create Project")) + .add_enabled(can_create && !self.project_creation_handle.is_some(), + egui::Button::new("Create Project")) .clicked() { log::info!("Creating new project at {:?}", self.project_path); - self.start_project_creation(); - ui.ctx().request_repaint(); + self.start_project_creation(graphics.shared.future_queue.clone()); } }); }); self.show_new_project = show_new_project; if local_select_project { + log::debug!("Opening folder picker"); self.is_in_file_dialogue = true; - if let Some(path) = rfd::AsyncFileDialog::new() - .set_title("Save Project") - .set_file_name(&self.project_name) + + let name = self.project_name.clone(); + if let Some(path) = FileDialog::new() + .set_title("Select Project Folder") + .set_file_name(&name) .pick_folder() - .await { - self.project_path = Some(path.into()); - log::debug!("Project will be saved at: {:?}", self.project_path); + self.project_path = Some(path.clone()); + log::debug!("Selected project location: {:?}", path); } - self.is_in_file_dialogue = false; - } - if let Some(rx) = self.progress_rx.as_mut() { - while let Ok(progress) = rx.try_recv() { - match progress { - ProjectProgress::Step { progress, message } => { - self.progress = progress; - self.progress_message = message; - } - ProjectProgress::Error(err) => { - self.project_error.get_or_insert_with(Vec::new).push(err); - } - ProjectProgress::Done if self.project_error.is_none() => { - self.is_in_file_dialogue = false; - self.show_new_project = false; - self.show_progress = false; - self.scene_command = SceneCommand::SwitchScene("editor".to_string()); - } - ProjectProgress::Done => {} - } - } + self.is_in_file_dialogue = false; } if self.show_progress { egui::Window::new("Creating Project...") - .collapsible(true) + .collapsible(false) .resizable(false) - .fixed_size([400.0, 120.0]) + .fixed_size([400.0, 150.0]) .show(&egui_ctx, |ui| { ui.label(&self.progress_message); ui.add_space(10.0); - ui.add(egui::ProgressBar::new(self.progress).show_percentage()); + if let Some(errors) = &self.project_error { - ui.colored_label(egui::Color32::RED, "Errors:"); + ui.add_space(10.0); + ui.colored_label(egui::Color32::RED, "Errors encountered:"); for err in errors { ui.label(err); } @@ -339,11 +368,11 @@ impl Scene for MainMenu { }); } - self.toast.show(&graphics.shared.get_egui_context()); + self.toast.show(&egui_ctx); } fn exit(&mut self, _event_loop: &ActiveEventLoop) { - log::info!("Exiting menu scene"); + log::info!("Exiting main menu scene"); } fn run_command(&mut self) -> SceneCommand { @@ -353,10 +382,9 @@ impl Scene for MainMenu { impl Keyboard for MainMenu { fn key_down(&mut self, key: KeyCode, event_loop: &ActiveEventLoop) { - if key == KeyCode::Escape { - if !self.show_new_project && !self.is_in_file_dialogue { - event_loop.exit(); - } + if key == KeyCode::Escape + && !self.show_new_project && !self.is_in_file_dialogue { + event_loop.exit(); } } @@ -365,9 +393,7 @@ impl Keyboard for MainMenu { impl Mouse for MainMenu { fn mouse_move(&mut self, _position: PhysicalPosition<f64>) {} - fn mouse_down(&mut self, _button: MouseButton) {} - fn mouse_up(&mut self, _button: MouseButton) {} } @@ -395,4 +421,4 @@ impl Controller for MainMenu { fn on_disconnect(&mut self, id: gilrs::GamepadId) { debug!("Controller disconnected [{}]", id); } -} +} @@ -0,0 +1,850 @@ +use std::sync::Arc; +use egui::{Align2, Image}; +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{AdoptedEntity, Transform}; +use dropbear_engine::graphics::SharedGraphicsContext; +use dropbear_engine::lighting::{Light, LightComponent}; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use eucalyptus_core::states::{ModelProperties, ScriptComponent, Value}; +use eucalyptus_core::{info, scripting, success, success_without_console, warn, warn_without_console}; +use eucalyptus_core::camera::{CameraAction, CameraComponent, CameraFollowTarget, CameraType}; +use eucalyptus_core::scripting::ScriptAction; +use eucalyptus_core::spawn::{push_pending_spawn, PendingSpawn}; +use crate::editor::{ComponentType, Editor, EditorState, EntityType, PendingSpawn2, Signal, UndoableAction}; + +pub trait SignalController { + fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>; +} + +impl SignalController for Editor { + fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { + let mut local_insert_script = false; + let mut local_insert_camera = (false, String::new()); + let mut local_signal: Option<Signal> = None; + let mut show = true; + + match &self.signal { + Signal::None => { + Ok::<(), anyhow::Error>(()) + } + Signal::Copy(_) => {Ok(())} + Signal::Paste(scene_entity) => { + let spawn = PendingSpawn { + asset_path: scene_entity.model_path.clone(), + asset_name: scene_entity.label.clone(), + transform: scene_entity.transform, + properties: scene_entity.properties.clone(), + handle: None, + }; + push_pending_spawn(spawn); + self.signal = Signal::Copy(scene_entity.clone()); + Ok(()) + }, + Signal::Delete => { + if let Some(sel_e) = &self.selected_entity { + let is_viewport_cam = if let Ok(mut q) = self.world + .query_one::<&CameraComponent>(*sel_e) + { + if let Some(c) = q.get() { + matches!(c.camera_type, CameraType::Debug) + } else { + false + } + } else { + false + }; + if is_viewport_cam { + warn!("You can't delete the viewport camera"); + self.signal = Signal::None; + Ok(()) + } else { + match self.world.despawn(*sel_e) { + Ok(_) => { + info!("Decimated entity"); + self.signal = Signal::None; + Ok(()) + } + Err(e) => { + self.signal = Signal::None; + anyhow::bail!("Failed to delete entity: {}", e); + } + } + } + } else { + // no entity has been selected, so all good + Ok(()) + } + } + Signal::Undo => { + if let Some(action) = self.undo_stack.pop() { + match action.undo(&mut self.world) { + Ok(_) => { + info!("Undid action"); + } + Err(e) => { + warn!("Failed to undo action: {}", e); + } + } + } else { + warn_without_console!("Nothing to undo"); + log::debug!("No undoable actions in stack"); + } + self.signal = Signal::None; + Ok(()) + } + Signal::ScriptAction(action) => match action { + ScriptAction::AttachScript { + script_path, + script_name, + } => { + if let Some(selected_entity) = self.selected_entity { + match scripting::move_script_to_src(script_path) { + Ok(moved_path) => { + let new_script = ScriptComponent { + name: script_name.clone(), + path: moved_path.clone(), + }; + + let replaced = { + if let Ok(mut sc) = self.world.get::<&mut ScriptComponent>(selected_entity) { + sc.name = new_script.name.clone(); + sc.path = new_script.path.clone(); + true + } else { + false + } + }; + + if !replaced { + match scripting::attach_script_to_entity( + &mut self.world, + selected_entity, + new_script.clone(), + ) { + Ok(_) => { + } + Err(e) => { + self.signal = Signal::None; + anyhow::bail!("Failed to attach script to entity {:?}: {}", + selected_entity, + e); + } + } + } + + { + if let Err(e) = scripting::convert_entity_to_group( + &mut self.world, + selected_entity, + ) { + log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + } + } + + success!( + "{} script '{}' at {} to entity {:?}", + if replaced { "Reattached" } else { "Attached" }, + script_name, + moved_path.display(), + selected_entity + ); + } + Err(e) => { + anyhow::bail!("Move failed: {}", e); + } + } + } else { + anyhow::bail!("AttachScript requested but no entity is selected"); + } + + self.signal = Signal::None; + Ok(()) + } + ScriptAction::CreateAndAttachScript { + script_path, + script_name, + } => { + if let Some(selected_entity) = self.selected_entity { + let new_script = ScriptComponent { + name: script_name.clone(), + path: script_path.clone(), + }; + + let replaced = { + if let Ok(mut sc) = self.world.get::<&mut ScriptComponent>(selected_entity) { + sc.name = new_script.name.clone(); + sc.path = new_script.path.clone(); + true + } else { + false + } + }; + + if !replaced { + match scripting::attach_script_to_entity( + &mut self.world, + selected_entity, + new_script.clone(), + ) { + Ok(_) => { + } + Err(e) => { + self.signal = Signal::None; + anyhow::bail!("Failed to attach new script: {}", e); + } + } + } + + { + if let Err(e) = scripting::convert_entity_to_group( + &mut self.world, + selected_entity, + ) { + log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + } + } + + success!( + "{} new script '{}' at {} to entity {:?}", + if replaced { "Replaced" } else { "Attached" }, + script_name, + script_path.display(), + selected_entity + ); + } else { + warn_without_console!("No selected entity to attach new script"); + log::warn!("CreateAndAttachScript requested but no entity is selected"); + } + self.signal = Signal::None; + Ok(()) + } + ScriptAction::RemoveScript => { + if let Some(selected_entity) = self.selected_entity { + let mut success = false; + let mut comp = ScriptComponent::default(); + { + if let Ok(script) = self.world + .remove_one::<ScriptComponent>(selected_entity) + { + success!("Removed script from entity {:?}", selected_entity); + success = true; + comp = script.clone(); + } else { + warn!("No script component found on entity {:?}", selected_entity); + } + } + + if success { + if let Err(e) = scripting::convert_entity_to_group( + &mut self.world, + selected_entity, + ) { + log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + } + log::debug!("Pushing remove component to undo stack"); + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::RemoveComponent( + selected_entity, + Box::new(ComponentType::Script(comp)), + ), + ); + } + } else { + warn!("No entity selected to remove script from"); + } + + self.signal = Signal::None; + Ok(()) + } + ScriptAction::EditScript => { + if let Some(selected_entity) = self.selected_entity { + let script_opt = { + if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(selected_entity) { + q.get().cloned() + } else { + None + } + }; + + if let Some(script) = script_opt { + match open::that(script.path.clone()) { + Ok(()) => { + success!("Opened {}", script.name) + } + Err(e) => { + warn!("Error while opening {}: {}", script.name, e); + } + } + } else { + warn!("No script component found on entity {:?}", selected_entity); + } + } else { + warn!("No entity selected to edit script"); + } + self.signal = Signal::None; + Ok(()) + } + }, + Signal::Play => { + let has_player_camera_target = self + .world + .query::<(&Camera, &CameraComponent, &CameraFollowTarget)>() + .iter() + .any(|(_, (_, comp, _))| matches!(comp.camera_type, CameraType::Player)); + + if has_player_camera_target { + if let Err(e) = self.create_backup() { + self.signal = Signal::None; + anyhow::bail!("Failed to create play mode backup: {}", e); + } + + self.editor_state = EditorState::Playing; + + self.switch_to_player_camera(); + + let mut script_entities = Vec::new(); + { + for (entity_id, script) in self.world + .query::<&ScriptComponent>() + .iter() + { + script_entities.push((entity_id, script.clone())); + } + } + + for (entity_id, script) in script_entities { + log::debug!( + "Initialising entity script [{}] from path: {}", + script.name, + script.path.display() + ); + + let bytes = match std::fs::read_to_string(&script.path) { + Ok(val) => val, + Err(e) => { + self.signal = Signal::None; + anyhow::bail!( + "Unable to read script {} to bytes because {}", + &script.path.display(), + e + ); + } + }; + + match self.script_manager.load_script( + &script + .path + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + bytes, + ) { + Ok(script_name) => { + if let Err(e) = self.script_manager.init_entity_script( + entity_id, + &script_name, + &mut self.world, + &self.input_state, + ) { + log::warn!( + "Failed to initialise script '{}' for entity {:?}: {}", + script.name, + entity_id, + e + ); + self.signal = Signal::StopPlaying; + } else { + success_without_console!( + "You are in play mode now! Press Escape to exit" + ); + log::info!("You are in play mode now! Press Escape to exit"); + } + } + Err(e) => { + // todo: proper error menu + self.signal = Signal::StopPlaying; + anyhow::bail!("Failed to load script '{}': {}", script.name, e); + } + } + } + } else { + anyhow::bail!("Unable to build: Player camera not attached to an entity"); + } + + self.signal = Signal::None; + 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); + } + + self.editor_state = EditorState::Editing; + + self.switch_to_debug_camera(); + + // already kills itself + // for (entity_id, _) in Arc::get_mut(&mut self.world).unwrap().query::<&ScriptComponent>().iter() { + // self.script_manager.remove_entity_script(entity_id); + // } + + success!("Exited play mode"); + log::info!("Back to the editor you go..."); + + self.signal = Signal::None; + Ok(()) + } + Signal::CameraAction(action) => match action { + CameraAction::SetPlayerTarget { entity, offset } => { + let player_camera = self + .world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(e, (_, comp))| { + if matches!(comp.camera_type, CameraType::Player) { + Some(e) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + let mut follow_target = (false, CameraFollowTarget::default()); + if let Ok(mut query) = self.world + .query_one::<&AdoptedEntity>(*entity) + && let Some(adopted) = query.get() { + follow_target = ( + true, + CameraFollowTarget { + follow_target: adopted.model.label.to_string(), + offset: *offset, + }, + ); + } + + { + if follow_target.0 { + let _ = self.world + .insert_one(camera_entity, follow_target); + info!("Set player camera target to entity {:?}", entity); + } + } + } + self.signal = Signal::None; + Ok(()) + } + CameraAction::ClearPlayerTarget => { + let player_camera = self + .world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(e, (_, comp))| { + if matches!(comp.camera_type, CameraType::Player) { + Some(e) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + { + let _ = self.world + .remove_one::<CameraFollowTarget>(camera_entity); + } + } + info!("Cleared player camera target"); + self.signal = Signal::None; + Ok(()) + } + }, + Signal::AddComponent(entity, e_type) => { + match e_type { + EntityType::Entity => { + if let Ok(mut q) = self.world + .query_one::<&AdoptedEntity>(*entity) + { + if let Some(e) = q.get() { + let label = e.model.label.clone(); + egui::Window::new(format!("Add component for {}", label)) + .title_bar(true) + .open(&mut show) + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .show(&graphics.get_egui_context(), |ui| { + if ui + .add_sized( + [ui.available_width(), 30.0], + egui::Button::new("Scripting"), + ) + .clicked() + { + log::debug!( + "Adding scripting component to entity [{}]", + label + ); + local_insert_script = true; + local_signal = Some(Signal::None); + } + if ui + .add_sized( + [ui.available_width(), 30.0], + egui::Button::new("Camera"), + ) + .clicked() + { + log::debug!( + "Adding camera component to entity [{}]", + label + ); + + let has_camera = self.world + .query_one::<(&Camera, &CameraComponent)>(*entity) + .is_ok(); + + if has_camera { + warn!( + "Entity [{}] already has a camera component", + label + ); + } else { + local_insert_camera = (true, label.clone()); + } + local_signal = Some(Signal::None); + } + }); + + } + } else { + log_once::warn_once!( + "Failed to add component to entity: no entity component found" + ); + } + if local_insert_script { + if let Err(e) = self.world + .insert_one(entity.clone(), ScriptComponent::default()) + { + warn!( + "Failed to add scripting component to entity: {}", + e + ); + } else { + success!("Added the scripting component"); + } + } + + if local_insert_camera.0 { + let camera = Camera::predetermined( + graphics.clone(), + Some(&format!("{} Camera", local_insert_camera.1)), + ); + let component = CameraComponent::new(); + if let Err(e) = self.world + .insert(*entity, (camera, component)) + { + warn!( + "Failed to add camera component to entity: {}", + e + ); + } else { + success!("Added the camera component"); + } + } + Ok(()) + } + EntityType::Light => { + { + if let Ok(mut q) = self.world + .query_one::<&Light>(*entity) + { + if let Some(light) = q.get() { + let mut show = true; + egui::Window::new(format!("Add component for {}", light.label)) + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .open(&mut show) + .title_bar(true) + .show(&graphics.get_egui_context(), |ui| { + if ui + .add_sized( + [ui.available_width(), 30.0], + egui::Button::new("Scripting"), + ) + .clicked() + { + log::debug!( + "Adding scripting component to light [{}]", + light.label + ); + + success!( + "Added the scripting component to light [{}]", + light.label + ); + self.signal = Signal::None; + } + }); + if !show { + self.signal = Signal::None; + } + } + Ok(()) + } else { + log_once::warn_once!( + "Failed to add component to light: no light component found" + ); + Ok(()) + } + } + } + EntityType::Camera => { + { + if let Ok(mut q) = self.world + .query_one::<(&Camera, &CameraComponent)>(*entity) + { + if let Some((cam, _comp)) = q.get() { + let mut show = true; + egui::Window::new(format!("Add component for {}", cam.label)) + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .open(&mut show) + .title_bar(true) + .show(&graphics.get_egui_context(), |ui| { + egui_extras::install_image_loaders(ui.ctx()); + ui.add(Image::from_bytes( + "bytes://theres_nothing.jpg", + include_bytes!("../../resources/theres_nothing.jpg"), + )); + ui.label("Theres nothing..."); + // scripting could be planned??? + // if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { + // log::debug!("Adding scripting component to camera [{}]", cam.label); + + // success!("Added the scripting component to camera [{}]", cam.label); + // self.signal = Signal::None; + // } + }); + if !show { + self.signal = Signal::None; + } + } + Ok(()) + } else { + log_once::warn_once!( + "Failed to add component to light: no light component found" + ); + Ok(()) + } + } + } + } + } + Signal::RemoveComponent(entity, c_type) => + {match &**c_type { + ComponentType::Script(_) => { + match self.world + .remove_one::<ScriptComponent>(*entity) + { + Ok(component) => { + success!("Removed script component from entity {:?}", entity); + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::RemoveComponent( + *entity, + Box::new(ComponentType::Script(component)), + ), + ); + } + Err(e) => { + warn!("Failed to remove script component from entity: {}", e); + } + }; + self.signal = Signal::None; + Ok(()) + } + ComponentType::Camera(_, _, follow) => { + if follow.is_some() { + match self.world.remove::<( + Camera, + CameraComponent, + CameraFollowTarget, + )>( + *entity + ) { + Ok(component) => { + success!("Removed camera component from entity {:?}", entity); + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::RemoveComponent( + *entity, + Box::new(ComponentType::Camera( + Box::new(component.0), + component.1, + Some(component.2), + )), + ), + ); + } + Err(e) => { + warn!("Failed to remove camera component from entity: {}", e); + } + }; + Ok(()) + } else { + match self.world + .remove::<(Camera, CameraComponent)>(*entity) + { + Ok(component) => { + success!("Removed camera component from entity {:?}", entity); + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::RemoveComponent( + *entity, + Box::new(ComponentType::Camera(Box::new(component.0), component.1, None)), + ), + ); + } + Err(e) => { + warn!("Failed to remove script component from entity: {}", e); + } + }; + Ok(()) + } + } + }}, + Signal::CreateEntity => { + let mut show = true; + egui::Window::new("Add Entity") + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .open(&mut show) + .title_bar(true) + .show(&graphics.get_egui_context(), |ui| { + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Model")).clicked() { + log::debug!("Creating new model"); + warn!("Instead of using the `Add Entity` window, double click on the imported model in the asset \n\ + viewer to import a new model, then tweak the settings to how you wish after!"); + self.signal = Signal::None; + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Light")).clicked() { + log::debug!("Creating new lighting"); + self.signal = Signal::Spawn(PendingSpawn2::Light); + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() { + log::debug!("Creating new plane"); + self.signal = Signal::Spawn(PendingSpawn2::Plane); + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { + log::debug!("Creating new cube"); + self.signal = Signal::Spawn(PendingSpawn2::Cube); + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { + log::debug!("Creating new cube"); + self.signal = Signal::Spawn(PendingSpawn2::Camera); + } + }); + if !show { + self.signal = Signal::None; + } + Ok(()) + } + Signal::LogEntities => { + log::debug!("===================="); + let mut counter = 0; + for entity in self.world.iter() { + if let Some(entity) = entity.get::<&AdoptedEntity>() { + log::info!("Model: {:?}", entity.model.label); + log::info!(" |-> Using model: {:?}", entity.model.id); + } + + if let Some(entity) = entity.get::<&Light>() { + log::info!("Light: {:?}", entity.cube_model.label); + log::info!(" |-> Using model: {:?}", entity.cube_model.id); + } + + if entity.get::<&Camera>().is_some() { + log::info!("Camera"); + } + counter += 1; + } + log::debug!("===================="); + info!("Total entity count: {}", counter); + self.signal = Signal::None; + Ok(()) + } + Signal::Spawn(entity_type) => { + match entity_type { + crate::editor::PendingSpawn2::Light => { + let light = Light::new(graphics.clone(), LightComponent::default(), Transform::new(), Some("Default Light")); + let handle = graphics.future_queue.push(light); + self.alt_pending_spawn_queue.push(handle); + success!("Pushed light to queue"); + } + crate::editor::PendingSpawn2::Plane => { + let transform = Transform::new(); + let mut props = ModelProperties::new(); + props + .custom_properties + .insert("width".to_string(), Value::Float(500.0)); + props + .custom_properties + .insert("height".to_string(), Value::Float(200.0)); + props + .custom_properties + .insert("tiles_x".to_string(), Value::Int(500)); + props + .custom_properties + .insert("tiles_z".to_string(), Value::Int(200)); + push_pending_spawn(PendingSpawn { + asset_path: ResourceReference::from_reference(ResourceReferenceType::Plane), + asset_name: "DefaultPlane".to_string(), + transform, + properties: props, + handle: None, + }); + success!("Pushed plane to queue"); + } + PendingSpawn2::Cube => { + let pending = PendingSpawn { + asset_path: ResourceReference::from_bytes(include_bytes!("../../resources/cube.glb")), + asset_name: "Default Cube".to_string(), + transform: Default::default(), + properties: Default::default(), + handle: None, + }; + push_pending_spawn(pending); + success!("Pushed cube to queue"); + } + PendingSpawn2::Camera => { + let camera = Camera::predetermined(graphics.clone(), None); + let component = CameraComponent::new(); + { + self.world + .spawn((camera, component)); + } + success!("Pushed camera to queue"); + } + } + self.signal = Signal::None; + return Ok(()); + } + }?; + if !show { + self.signal = Signal::None; + } + if let Some(signal) = local_signal { + self.signal = signal; + } + Ok(()) + } +} @@ -0,0 +1,131 @@ +use std::sync::Arc; +use dropbear_engine::entity::AdoptedEntity; +use dropbear_engine::future::FutureQueue; +use dropbear_engine::graphics::SharedGraphicsContext; +use dropbear_engine::model::Model; +use dropbear_engine::procedural::plane::PlaneBuilder; +use dropbear_engine::utils::ResourceReferenceType; +pub(crate) use eucalyptus_core::spawn::{PendingSpawnController, PENDING_SPAWNS}; +use eucalyptus_core::states::{Value, PROJECT}; +use eucalyptus_core::utils::PROTO_TEXTURE; +use crate::editor::Editor; + +impl PendingSpawnController for Editor { + fn check_up(&mut self, graphics: Arc<SharedGraphicsContext>, queue: Arc<FutureQueue>) -> anyhow::Result<()> { + queue.poll(); + let mut spawn_list = PENDING_SPAWNS.lock(); + + let mut completed = Vec::new(); + + for (i, spawn) in spawn_list.iter_mut().enumerate() { + log_once::debug_once!("Caught pending spawn! Info: {} of type {}", spawn.asset_name, spawn.asset_path); + if spawn.handle.is_none() { + log_once::debug_once!("Pending spawn does NOT have a handle, creating new one now"); + let graphics_clone = graphics.clone(); + let asset_name = spawn.asset_name.clone(); + let asset_path = spawn.asset_path.ref_type.clone(); + let properties = spawn.properties.clone(); + + let func = async move { + match asset_path { + ResourceReferenceType::None => { + Err(anyhow::anyhow!("No asset path available")) + } + ResourceReferenceType::File(file) => { + let path = { + let _guard = PROJECT.read(); + _guard.project_path.clone() + }; + let resource = path.join("resources").join(file); + AdoptedEntity::new(graphics_clone, resource, Some(&asset_name)).await + } + ResourceReferenceType::Bytes(bytes) => { + let model = Model::load_from_memory(graphics_clone.clone(), &bytes, Some(&asset_name)).await?; + Ok(AdoptedEntity::adopt(graphics_clone, model).await) + } + ResourceReferenceType::Plane => { + let get_float = |key: &str| -> anyhow::Result<f32> { + let val = properties.custom_properties + .get(key) + .ok_or_else(|| anyhow::anyhow!("Entity has no {} property", key))?; + match val { + Value::Float(f) => Ok(*f as f32), + _ => Err(anyhow::anyhow!("{} is not a float", key)), + } + }; + + let get_int = |key: &str| -> anyhow::Result<u32> { + let val = properties.custom_properties + .get(key) + .ok_or_else(|| anyhow::anyhow!("Entity has no {} property", key))?; + match val { + Value::Int(i) => Ok(*i as u32), + _ => Err(anyhow::anyhow!("{} is not an int", key)), + } + }; + + let width = get_float("width")?; + let height = get_float("height")?; + let tiles_x = get_int("tiles_x")?; + let tiles_z = get_int("tiles_z")?; + + PlaneBuilder::new() + .with_size(width, height) + .with_tiles(tiles_x, tiles_z) + .build(graphics_clone, PROTO_TEXTURE, Some(&asset_name)) + .await + } + } + }; + + let handle = queue.push(Box::pin(func)); + spawn.handle = Some(handle); + } else { + log_once::debug_once!("Spawn does have handle, using that one"); + } + + if let Some(handle) = &spawn.handle { + log_once::debug_once!("Handle located"); + if let Some(result) = queue.exchange_owned(handle) { + log_once::debug_once!("Loading done, located result"); + if let Ok(r) = result + .downcast::<anyhow::Result<AdoptedEntity>>() { + log_once::debug_once!("Result has been successfully downcasted"); + match Arc::try_unwrap(r) { + Ok(entity) => { + match entity { + Ok(entity) => { + log::debug!("Entity loaded"); + self.world.spawn((entity, spawn.transform, spawn.properties.clone())); + completed.push(i); + } + Err(e) => { + log_once::error_once!("Unable to load model: {}", e); + completed.push(i); + } + } + + } + Err(_) => return { + log_once::warn_once!("Cannot unwrap Arc result"); + completed.push(i); + Ok(()) + }, + } + } + } else { + log_once::debug_once!("Handle exchanging failed, probably not ready yet"); + } + } else { + log_once::debug_once!("Spawn has no handle"); + } + } + + for &i in completed.iter().rev() { + log_once::debug_once!("Removing item {} from pending spawn list", i); + spawn_list.remove(i); + } + + Ok(()) + } +} @@ -45,15 +45,14 @@ pub fn show_new_project_window<F>( } ui.add_space(5.0); - if ui.button("Choose Location").clicked() { - if let Some(path) = rfd::FileDialog::new() + if ui.button("Choose Location").clicked() + && let Some(path) = rfd::FileDialog::new() .set_title("Save Project") .set_file_name(project_name.clone()) .pick_folder() { *project_path = Some(path); } - } let can_create = project_path.is_some() && !project_name.is_empty(); if ui @@ -150,7 +149,7 @@ pub fn start_project_creation( } } else if folder == "src2" { if let Some(path) = &project_path { - let mut config = ProjectConfig::new(project_name.clone(), &path); + let mut config = ProjectConfig::new(project_name.clone(), path); let _ = config.write_to_all(); let mut global = PROJECT.write(); *global = config; @@ -158,14 +157,12 @@ pub fn start_project_creation( } else { Err(anyhow!("Project path not found")) } + } else if !full_path.exists() { + fs::create_dir_all(&full_path) + .map_err(|e| anyhow!(e)) + .map(|_| ()) } else { - if !full_path.exists() { - fs::create_dir_all(&full_path) - .map_err(|e| anyhow!(e)) - .map(|_| ()) - } else { - Ok(()) - } + Ok(()) }; if let Err(e) = result { tx.send(ProjectProgress::Error(e.to_string())).ok(); @@ -1,3 +1,60 @@ -/// Queries the attached entity. Returns a serialized string to be decoded. -@external(javascript, "dropbear", "queryAttachedEntity") -fn raw_query_attached_entity() -> String +//// The bridge used for connecting between the dropbear-engine and the +//// gleam WASM interface. +//// +//// This can be ran on it's own, but it is pretty useless without attaching to an entity. +//// +//// # Compile Pipeline +//// In the rust interface, it compiles Gleam using a Gleam -> JavaScript -> WASM (using javy). Despite using javy +//// creating large amounts of overhead, it is best choice for compiling JavaScript. +//// +//// # Targets +//// Currently, Erlang targets do not do anything (unless you can compile to WASM, which I'm pretty sure you can't. +//// There is also no library available for a Gleam <-> Rust connection, nor is there a Erlang <-> Rust **practical** +//// connection. Also, using Erlang for a game engine is extremely impractical and stupid (and a pain to setup). +//// +//// My other thoughts were to use OCaml, but OCaml has a high learning curve (as if Gleam doesn't already), and is +//// hard to setup up, including dealing with FFI. +//// +//// Gleam just seemed like the best language: niche, rising to popularity, can be compiled and integrates with Rust, +//// and most importantly: Type safety. + +import gleam/dict +import math +import gleam/option +import entity + +/// The id returned during a query, which allows you to query for other features +/// +/// A very primitive type. +pub type QueryId { + QueryId(id: Int) +} + +/// Queries the current entity the script is attached to. +/// +/// Returns None is the script is not attached to anything (i.e. a library), or Some if it is attached, +/// along with the entity information as shown in `dropbear/entity.Entity`. +pub fn query_current_entity() -> option.Option(entity.Entity) { + option.Some(entity.Entity(0, transform: math.new_transform(), properties: dict.new(), dirty: False, before_dirty_entity: entity.BeforeSyncedEntity(id: 0, transform: math.new_transform(), properties: dict.new()))) + +} + +/// A command that syncs/pushes the data back to the game engine interface. +/// +/// It is typically automatically triggered at the end of a function (such as at the end of a move function), but can +/// be triggered manually by running the command. +/// +/// The sync command takes an input of entity (so perfect for chaining), and then returning an entity. It also updates +/// the BeforeSyncedEntity to its current iteration. +pub fn sync(entity: entity.Entity) -> entity.Entity { + case entity.dirty { + True -> { + // mock sync/push + entity.Entity(..entity, before_dirty_entity: entity.new_before_synced_entity_from_existing_entity(entity)) + } + False -> { + // just return back the entity + entity + } + } +} @@ -0,0 +1,65 @@ +//// A module for storing the properties of entities, as well as the manipulation of entities. + +import gleam/dict +import types +import math + +/// A standard type for an entity. +pub type Entity { + Entity( + /// The id/reference + id: Int, + /// The position, rotation and scale of the entity + transform: math.Transform, + /// The properties of the entity, stored in a gleam dictionary as a String key and + /// Value type. + properties: dict.Dict(String, types.Value), + /// An internal value that checks if the entity is "dirty" and needs to be synced up. + /// + /// It can be manually synced up using the `dropbear.sync()` command, but in the case + /// that it is not used, this flag pushes the changes at the end of the update function. + dirty: Bool, + before_dirty_entity: BeforeSyncedEntity, + ) +} + +/// Creates a "dummy"/placeholder value for an Entity. +/// +/// It's ID is always set to -1 so it cannot be used in any queries. +pub fn dummy() -> Entity { + Entity(id: -1, transform: math.new_transform(), properties: dict.new(), dirty: False, before_dirty_entity: dummy_before()) +} + +/// A type used to checked the latest synced change. This is mainly internal, however you can use it +/// to check which values are dirty or not. +pub type BeforeSyncedEntity { + BeforeSyncedEntity( + /// The id/reference + id: Int, + /// The position, rotation and scale of the entity + transform: math.Transform, + /// The properties of the entity, stored in a gleam dictionary as a String key and + /// Value type. + properties: dict.Dict(String, types.Value), + ) +} + +/// Creates a new dummy value for a BeforeSyncedEntity. +/// +/// It's ID is always set to -1 so it cannot be used in any queries. +pub fn dummy_before() -> BeforeSyncedEntity { + BeforeSyncedEntity(id: -1, transform: math.new_transform(), properties: dict.new()) +} + +/// A hella long name, creates a new BeforeSyncedEntity from an existing Entity. +/// +/// It mainly exists as an internal helper, thats all... +pub fn new_before_synced_entity_from_existing_entity(entity: Entity) -> BeforeSyncedEntity { + BeforeSyncedEntity(id: entity.id, transform: entity.transform, properties: entity.properties) +} + +/// Sets the position of the entity +pub fn set_position(entity: Entity, position: math.Vector3(Float)) -> Entity { + let transform = math.Transform(..entity.transform, position: position) + Entity(..entity, transform:transform, dirty: True) +} @@ -0,0 +1,44 @@ +//// A dummy file, do not use. I mean, use it if you want, its public for a reason... + +import gleam/io +import gleam/string +import gleam/option.{Some} +import math +import entity +import dropbear + +pub fn main() { + load() + update(0.016) +} + +pub fn load() -> Nil { + let _ = case dropbear.query_current_entity() { + Some(entity) -> { + entity.set_position(entity, math.Vector3(1.0, 1.0, 1.0)) + |> dropbear.sync() + } + option.None -> { + entity.dummy() + } + } + + io.println("Loaded!") +} + +pub fn update(dt: Float) -> Nil { + io.println("Updating...") + let _ = case dropbear.query_current_entity() { + Some(entity) -> { + io.println("Successfully queried entity!") + entity.set_position(entity, math.Vector3(1.0, 1.0, 1.0)) + |> dropbear.sync() + } + option.None -> { + io.println("Could not query entity, creating a dummy entity") + entity.dummy() + } + } + io.println("Deltatime is " <> string.inspect(dt)) + update(dt) +} @@ -0,0 +1,2 @@ +//// A module for input management and input detection from different IO devices +//// such as keyboards, mice, and game controllers (for now). @@ -1,57 +1,59 @@ -/// The generic type of an entity, containing a position, rotation and scale. +//// A module for the basic types of math that the gleam standard library didn't include. + +/// The generic type of an entity, containing a position, rotation and scale. pub type Transform { - Transform ( - position: Vector3(Float), - rotation: Quaternion(Float), - scale: Vector3(Float), - ) + Transform ( + position: Vector3(Float), + rotation: Quaternion(Float), + scale: Vector3(Float), + ) } /// Creates a new transform pub fn new_transform() -> Transform { - Transform( - position: zero_vector3f(), - rotation: identity_quatf(), - scale: zero_vector3f(), - ) + Transform( + position: zero_vector3f(), + rotation: identity_quatf(), + scale: zero_vector3f(), + ) } -/// A type used to show 3 instances of a value. +/// A type used to show 3 instances of a value. pub type Vector3(a) { - Vector3( - /// X value - x: a, - /// Y value - y: a, - /// Z value - z: a, - ) + Vector3( + /// X value + x: a, + /// Y value + y: a, + /// Z value + z: a, + ) } -/// Creates a new Vector3(Float) of all 0.0. +/// Creates a new Vector3(Float) of all 0.0. pub fn zero_vector3f() -> Vector3(Float) { - Vector3( - x: 0.0, - y: 0.0, - z: 0.0, - ) + Vector3( + x: 0.0, + y: 0.0, + z: 0.0, + ) } pub type Quaternion(a) { - Quaternion( - w: a, - x: a, - y: a, - z: a, - ) + Quaternion( + w: a, + x: a, + y: a, + z: a, + ) } -/// Creates a new quaternion with 1.0 as the scale and 0.0 for the x, y and z. +/// Creates a new quaternion with 1.0 as the scale and 0.0 for the x, y and z. pub fn identity_quatf() -> Quaternion(Float) { - Quaternion( - w: 1.0, - x: 0.0, - y: 0.0, - z: 0.0, - ) + Quaternion( + w: 1.0, + x: 0.0, + y: 0.0, + z: 0.0, + ) } @@ -0,0 +1,13 @@ +//// Different types and values available to be used. + +/// A generic value. It is used in the entity properties. +pub type Value { + /// Integer + Int(Int) + /// Float + Float(Float) + /// String + String(String) + /// Boolean + Bool(Bool) +}