tirbofish/dropbear · diff
Merge pull request #46
features: optimised model loading, added world loading status, changed from WASM to boa_engine
jarvis, run github actions. i think this commit deserves a nightly action (not public enough yet lol)
Signature present but could not be verified.
Unverified
@@ -1,42 +0,0 @@ -name: Generate and Publish TypeScript Docs - -on: - push: - branches: [ main ] - workflow_dispatch: - -permissions: - contents: read - pages: write - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Cache npm - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Generate TypeScript docs - run: | - chmod +x scripts/gen_docs.sh - ./scripts/gen_docs.sh - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.PAGES_PAT }} - publish_dir: ./docs @@ -22,8 +22,8 @@ colored = "3.0" dropbear-engine = { path = "dropbear-engine" } egui = "0.32" egui-toast-fork = "0.18" -egui_wgpu_backend = { version = "0.35", git = "https://github.com/hasenbanck/egui_wgpu_backend"} -egui_winit_platform = { version = "0.27", git = "https://github.com/hasenbanck/egui_winit_platform" } +egui-wgpu = { version = "0.32" } +egui-winit = { version = "0.32" } egui_dnd = "0.13" egui_dock-fork = { version = "0.17", features = ["serde"] } egui_extras = { version = "0.32", features = ["all_loaders"] } @@ -42,22 +42,26 @@ open = "5" parking_lot = {version = "0.12", features = ["deadlock_detection"] } rfd = "0.15.4" ron = "0.11" -russimp-ng = { version = "3.2", features = ["static-link"] } -rustyscript = { version = "0.12" } +# russimp-ng = { version = "3.2", features = ["static-link"] } +# rustyscript = { version = "0.12" } serde = { version = "1.0.219", features = ["derive"] } spin_sleep = "1.3" transform-gizmo-egui = { git = "https://github.com/4tkbytes/transform-gizmo"} tokio = { version = "1", features = ["full"] } -wgpu = "26" +wgpu = "25" winit = { version = "0.30", features = [] } zip = "5.1" walkdir = "2.3" -wasmer = { version = "6.1.0-rc.3" } +# wasmer = { version = "6.1.0-rc.3" } rayon = "1.11" flate2 = "1.1" reqwest = { version = "0.11", features = ["stream"] } tar = "0.4" async-trait = "0.1" +futures-util = "0.3" +boa_engine = "0.20" +backtrace = "0.3" + gltf = "1" thiserror = "2.0" @@ -76,11 +80,11 @@ lto = false # it takes way too long to exit out panic = "abort" -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -strip = true -panic = "abort" -debug = false -incremental = false +# [profile.release] +# opt-level = 3 +# lto = true +# codegen-units = 1 +# strip = true +# panic = "abort" +# debug = false +# incremental = false @@ -14,8 +14,8 @@ app_dirs2.workspace = true bytemuck.workspace = true chrono.workspace = true colored.workspace = true -egui_wgpu_backend.workspace = true -egui_winit_platform.workspace = true +egui-wgpu.workspace = true +egui-winit.workspace = true egui.workspace = true env_logger.workspace = true futures.workspace = true @@ -36,6 +36,8 @@ gltf.workspace = true rayon.workspace = true tokio.workspace = true async-trait.workspace = true +backtrace.workspace = true +os_info = "*" [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -161,7 +161,11 @@ impl Camera { result } - pub fn create_bind_group_layout(&mut self, graphics: Arc<SharedGraphicsContext>, camera_buffer: Buffer) { + pub fn create_bind_group_layout( + &mut self, + graphics: Arc<SharedGraphicsContext>, + camera_buffer: Buffer, + ) { let camera_bind_group_layout = graphics .device @@ -179,16 +183,14 @@ impl Camera { label: Some("camera_bind_group_layout"), }); - let camera_bind_group = graphics - .device - .create_bind_group(&BindGroupDescriptor { - layout: &camera_bind_group_layout, - entries: &[BindGroupEntry { - binding: 0, - resource: camera_buffer.as_entire_binding(), - }], - label: Some("camera_bind_group"), - }); + let camera_bind_group = graphics.device.create_bind_group(&BindGroupDescriptor { + layout: &camera_bind_group_layout, + entries: &[BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + label: Some("camera_bind_group"), + }); self.layout = Some(camera_bind_group_layout); self.bind_group = Some(camera_bind_group); } @@ -1,60 +1,68 @@ -use egui::{Context, FontDefinitions}; -use egui_wgpu_backend::{RenderPass, ScreenDescriptor, wgpu}; -use egui_winit_platform::{Platform, PlatformDescriptor}; -use wgpu::{CommandEncoder, Device, Queue, TextureFormat, TextureView}; +use egui::Context; +use egui_wgpu::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView}; +use egui_wgpu::{Renderer, ScreenDescriptor, wgpu}; +use egui_winit::State; use winit::event::WindowEvent; use winit::window::Window; pub struct EguiRenderer { - state: Platform, - renderer: RenderPass, + state: State, + renderer: Renderer, frame_started: bool, } impl EguiRenderer { - pub fn context(&mut self) -> Context { - self.state.context() + pub fn context(&self) -> &Context { + self.state.egui_ctx() } - pub fn renderer(&mut self) -> &mut RenderPass { + pub fn renderer(&mut self) -> &mut Renderer { &mut self.renderer } pub fn new( device: &Device, output_color_format: TextureFormat, + output_depth_format: Option<TextureFormat>, msaa_samples: u32, window: &Window, ) -> EguiRenderer { - let size = window.inner_size(); - - let platform = Platform::new(PlatformDescriptor { - physical_width: size.width as u32, - physical_height: size.height as u32, - scale_factor: window.scale_factor(), - font_definitions: FontDefinitions::default(), - style: Default::default(), - }); - - let egui_renderer = RenderPass::new(device, output_color_format, msaa_samples); + let egui_context = Context::default(); + + let egui_state = egui_winit::State::new( + egui_context, + egui::viewport::ViewportId::ROOT, + &window, + Some(window.scale_factor() as f32), + None, + Some(2 * 1024), // default dimension is 2048 + ); + let egui_renderer = Renderer::new( + device, + output_color_format, + output_depth_format, + msaa_samples, + true, + ); EguiRenderer { - state: platform, + state: egui_state, renderer: egui_renderer, frame_started: false, } } - pub fn handle_input(&mut self, event: &WindowEvent) { - let _ = self.state.handle_event(event); + pub fn handle_input(&mut self, window: &Window, event: &WindowEvent) { + let _ = self.state.on_window_event(window, event); } pub fn ppp(&mut self, v: f32) { self.context().set_pixels_per_point(v); } - pub fn begin_frame(&mut self) { - self.state.begin_pass(); + pub fn begin_frame(&mut self, window: &Window) { + let raw_input = self.state.take_egui_input(window); + self.state.egui_ctx().begin_pass(raw_input); self.frame_started = true; } @@ -68,37 +76,46 @@ impl EguiRenderer { screen_descriptor: ScreenDescriptor, ) { if !self.frame_started { - return; + panic!("begin_frame must be called before end_frame_and_draw can be called!"); } - let full_output = self.state.end_pass(Some(window)); - let paint_jobs = self - .state - .context() - .tessellate(full_output.shapes, self.state.context().pixels_per_point()); - let textures_delta: egui::TexturesDelta = full_output.textures_delta; + self.ppp(screen_descriptor.pixels_per_point); - self.renderer - .add_textures(device, queue, &textures_delta) - .expect("add texture ok"); - self.renderer - .update_buffers(device, queue, &paint_jobs, &screen_descriptor); + let full_output = self.state.egui_ctx().end_pass(); - self.renderer - .execute( - encoder, - window_surface_view, - &paint_jobs, - &screen_descriptor, - Some(wgpu::Color::BLACK), - ) - .expect("egui execute ok"); + self.state + .handle_platform_output(window, full_output.platform_output); - // self.ppp(window.scale_factor() as f32); + let tris = self + .state + .egui_ctx() + .tessellate(full_output.shapes, self.state.egui_ctx().pixels_per_point()); + for (id, image_delta) in &full_output.textures_delta.set { + self.renderer + .update_texture(device, queue, *id, image_delta); + } + self.renderer + .update_buffers(device, queue, encoder, &tris, &screen_descriptor); + let rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: window_surface_view, + resolve_target: None, + ops: egui_wgpu::wgpu::Operations { + load: egui_wgpu::wgpu::LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + label: Some("egui main render pass"), + occlusion_query_set: None, + }); self.renderer - .remove_textures(textures_delta) - .expect("remove texture ok"); + .render(&mut rpass.forget_lifetime(), &tris, &screen_descriptor); + for x in &full_output.textures_delta.free { + self.renderer.free_texture(x) + } self.frame_started = false; } @@ -1,11 +1,11 @@ use futures::executor::block_on; use glam::{DMat4, DQuat, DVec3, Mat4}; use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, sync::Arc}; +use std::{path::{Path, PathBuf}, sync::Arc}; use wgpu::{Buffer, util::DeviceExt}; use crate::{ - graphics::{SharedGraphicsContext, Instance}, + graphics::{Instance, SharedGraphicsContext}, model::{LazyModel, LazyType, Model}, }; @@ -68,7 +68,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 = std::fs::read(path)?; + let buffer = tokio::fs::read(path).await?; Self::from_memory(buffer, label).await } @@ -79,7 +79,7 @@ impl LazyAdoptedEntity { ) -> anyhow::Result<Self> { let lazy_model = Model::lazy_load(buffer, label).await?; let label_str = label.unwrap_or("LazyAdoptedEntity").to_string(); - + Ok(Self { lazy_model, label: label_str, @@ -105,7 +105,7 @@ impl LazyType for LazyAdoptedEntity { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct AdoptedEntity { pub model: Option<Model>, pub previous_matrix: DMat4, @@ -114,8 +114,13 @@ pub struct AdoptedEntity { } impl AdoptedEntity { - pub async fn new(graphics: Arc<SharedGraphicsContext>, path: &PathBuf, label: Option<&str>) -> anyhow::Result<Self> { - let model = Model::load(graphics.clone(), path, label.clone()).await?; + pub async fn new( + graphics: Arc<SharedGraphicsContext>, + path: impl AsRef<Path>, + label: Option<&str>, + ) -> anyhow::Result<Self> { + let path = path.as_ref().to_path_buf(); + let model = Model::load(graphics.clone(), &path, label.clone()).await?; Ok(Self::adopt(graphics, model).await) } @@ -6,12 +6,19 @@ use image::GenericImageView; use parking_lot::Mutex; // use nalgebra::{Matrix4, UnitQuaternion, Vector3}; use wgpu::{ - util::{BufferInitDescriptor, DeviceExt}, BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder, CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, Queue, RenderPass, RenderPassDepthStencilAttachment, RenderPipeline, Sampler, ShaderModule, StencilState, SurfaceConfiguration, TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, VertexBufferLayout + BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder, + CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, Queue, RenderPass, + RenderPassDepthStencilAttachment, RenderPipeline, Sampler, ShaderModule, StencilState, + SurfaceConfiguration, TextureDescriptor, TextureFormat, TextureUsages, TextureView, + TextureViewDescriptor, VertexBufferLayout, + util::{BufferInitDescriptor, DeviceExt}, }; use winit::window::Window; use crate::{ - egui_renderer::EguiRenderer, model::{self, Vertex}, State + State, + egui_renderer::EguiRenderer, + model::{self, Vertex}, }; pub const NO_TEXTURE: &'static [u8] = include_bytes!("../../resources/no-texture.png"); @@ -44,7 +51,7 @@ pub struct FrameGraphicsContext<'a> { impl SharedGraphicsContext { pub fn get_egui_context(&self) -> Context { - self.egui_renderer.lock().context() + self.egui_renderer.lock().context().clone() } pub fn create_uniform<T>(&self, uniform: T, label: Option<&str>) -> Buffer @@ -83,17 +90,15 @@ impl<'a> RenderContext<'a> { encoder: &'a mut CommandEncoder, ) -> Self { let screen_size = (state.config.width as f32, state.config.height as f32); - let diffuse_sampler = Arc::new(state - .device - .create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - })); + let diffuse_sampler = Arc::new(state.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + })); Self { shared: Arc::new(SharedGraphicsContext { device: state.device.clone(), @@ -123,7 +128,8 @@ impl<'a> RenderContext<'a> { label: Option<&str>, ) -> RenderPipeline { let render_pipeline_layout = - self.shared.device + self.shared + .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(label.unwrap_or("Render Pipeline Descriptor")), bind_group_layouts: bind_group_layouts.as_slice(), @@ -131,7 +137,8 @@ impl<'a> RenderContext<'a> { }); let render_pipeline = - self.shared.device + self.shared + .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(label.unwrap_or("Render Pipeline")), layout: Some(&render_pipeline_layout), @@ -181,7 +188,8 @@ impl<'a> RenderContext<'a> { } pub fn clear_colour(&mut self, color: Color) -> RenderPass<'static> { - self.frame.encoder + self.frame + .encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -191,7 +199,6 @@ impl<'a> RenderContext<'a> { load: wgpu::LoadOp::Clear(color), store: wgpu::StoreOp::Store, }, - depth_slice: None, })], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &self.frame.depth_texture.view, @@ -208,14 +215,18 @@ impl<'a> RenderContext<'a> { } } -// A nice little struct that stored basic information about a WGPU shader. +// A nice little struct that stored basic information about a WGPU shader. pub struct Shader { pub label: String, pub module: ShaderModule, } impl Shader { - pub fn new(graphics: Arc<SharedGraphicsContext>, shader_file_contents: &str, label: Option<&str>) -> Self { + pub fn new( + graphics: Arc<SharedGraphicsContext>, + shader_file_contents: &str, + label: Option<&str>, + ) -> Self { let module = graphics .device .create_shader_module(wgpu::ShaderModuleDescriptor { @@ -236,7 +247,7 @@ impl Shader { } #[derive(Clone)] -/// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture. +/// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture. pub struct Texture { pub texture: wgpu::Texture, pub sampler: Sampler, @@ -250,10 +261,10 @@ impl Texture { /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; - /// Creates a new Texture from the bytes of an image. This function is blocking, and takes roughly 4 seconds to - /// convert from the image to RGBA, which can cause issues. There are better options, such as doing it yourself. - /// - /// Once async is implemented, this will be a better use. + /// Creates a new Texture from the bytes of an image. This function is blocking, and takes roughly 4 seconds to + /// convert from the image to RGBA, which can cause issues. There are better options, such as doing it yourself. + /// + /// Once async is implemented, this will be a better use. pub fn new(graphics: Arc<SharedGraphicsContext>, diffuse_bytes: &[u8]) -> Self { let start = Instant::now(); let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); @@ -261,7 +272,10 @@ impl Texture { let start = Instant::now(); let diffuse_rgba = diffuse_image.to_rgba8(); - println!("Converting diffuse image to rgba8 took {:?}", start.elapsed()); + println!( + "Converting diffuse image to rgba8 took {:?}", + start.elapsed() + ); let dimensions = diffuse_image.dimensions(); let texture_size = wgpu::Extent3d { @@ -271,18 +285,16 @@ impl Texture { }; let start = Instant::now(); - let diffuse_texture = graphics - .device - .create_texture(&wgpu::TextureDescriptor { - label: Some("diffuse_texture"), - size: texture_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, - view_formats: &[], - }); + let diffuse_texture = graphics.device.create_texture(&wgpu::TextureDescriptor { + label: Some("diffuse_texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); println!("Creating new diffuse texture took {:?}", start.elapsed()); let start = Instant::now(); @@ -301,41 +313,41 @@ impl Texture { }, texture_size, ); - println!("Writing texture to graphics queue took {:?}", start.elapsed()); + println!( + "Writing texture to graphics queue took {:?}", + start.elapsed() + ); let start = Instant::now(); let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); - let diffuse_sampler = graphics - .device - .create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); + let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); println!("Creating sampler took {:?}", start.elapsed()); let start = Instant::now(); - let diffuse_bind_group = - graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), - }, - ], - label: Some("texture_bind_group"), - }); + let diffuse_bind_group = graphics + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + label: Some("texture_bind_group"), + }); println!("Creating diffuse bind group took {:?}", start.elapsed()); println!("Done creating texture"); Self { @@ -348,7 +360,7 @@ impl Texture { } } - /// Creates a new depth texture. This is an internal function. + /// Creates a new depth texture. This is an internal function. pub fn create_depth_texture( config: &SurfaceConfiguration, device: &Device, @@ -443,8 +455,8 @@ impl Texture { self.bind_group.as_ref().unwrap() } - /// Alternative to [`Texture::new()`], which uses an existing rgba data buffer compared to new which synchronously - /// converts the image to RGBA form. + /// Alternative to [`Texture::new()`], which uses an existing rgba data buffer compared to new which synchronously + /// converts the image to RGBA form. pub(crate) fn from_rgba_buffer( graphics: Arc<SharedGraphicsContext>, rgba_data: &[u8], @@ -467,7 +479,10 @@ impl Texture { usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - println!("Creating new diffuse texture took {:?}", create_start.elapsed()); + println!( + "Creating new diffuse texture took {:?}", + create_start.elapsed() + ); let write_start = Instant::now(); graphics.queue.write_texture( @@ -485,7 +500,10 @@ impl Texture { }, texture_size, ); - println!("Writing texture to graphics queue took {:?}", write_start.elapsed()); + println!( + "Writing texture to graphics queue took {:?}", + write_start.elapsed() + ); let sampler_start = Instant::now(); let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { @@ -502,21 +520,26 @@ impl Texture { let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group_start = Instant::now(); - let diffuse_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), - }, - ], - label: Some("texture_bind_group"), - }); - println!("Creating diffuse bind group took {:?}", bind_group_start.elapsed()); + let diffuse_bind_group = graphics + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + label: Some("texture_bind_group"), + }); + println!( + "Creating diffuse bind group took {:?}", + bind_group_start.elapsed() + ); println!("Done creating texture"); @@ -530,7 +553,7 @@ impl Texture { } } - /// Creates a new [`Texture`] with a specified sampler (wgpu) and already converted RGBA byte buffer. + /// Creates a new [`Texture`] with a specified sampler (wgpu) and already converted RGBA byte buffer. pub fn new_with_sampler_with_rgba_buffer( graphics: Arc<SharedGraphicsContext>, rgba_data: &[u8], @@ -554,7 +577,10 @@ impl Texture { usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - println!("Creating new diffuse texture took {:?}", create_start.elapsed()); + println!( + "Creating new diffuse texture took {:?}", + create_start.elapsed() + ); let write_start = Instant::now(); graphics.queue.write_texture( @@ -572,40 +598,46 @@ impl Texture { }, texture_size, ); - println!("Writing texture to graphics queue took {:?}", write_start.elapsed()); + println!( + "Writing texture to graphics queue took {:?}", + write_start.elapsed() + ); let sampler_start = Instant::now(); - let diffuse_sampler = graphics - .device - .create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: address_mode, - address_mode_v: address_mode, - address_mode_w: address_mode, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); + let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: address_mode, + address_mode_v: address_mode, + address_mode_w: address_mode, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); println!("Creating sampler took {:?}", sampler_start.elapsed()); let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group_start = Instant::now(); - let diffuse_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), - }, - ], - label: Some("texture_bind_group"), - }); - println!("Creating diffuse bind group took {:?}", bind_group_start.elapsed()); + let diffuse_bind_group = graphics + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + label: Some("texture_bind_group"), + }); + println!( + "Creating diffuse bind group took {:?}", + bind_group_start.elapsed() + ); println!("Done creating texture"); @@ -620,9 +652,9 @@ impl Texture { } /// Creates a new [`Texture`] with a specified sampler (wgpu). - /// - /// This function decodes the image to RGBA, which can take a long time. This function is not - /// recommended to be used until you have async working. + /// + /// This function decodes the image to RGBA, which can take a long time. This function is not + /// recommended to be used until you have async working. pub fn new_with_sampler( graphics: Arc<SharedGraphicsContext>, diffuse_bytes: &[u8], @@ -638,18 +670,16 @@ impl Texture { depth_or_array_layers: 1, }; - let diffuse_texture = graphics - .device - .create_texture(&wgpu::TextureDescriptor { - label: Some("diffuse_texture"), - size: texture_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, - view_formats: &[], - }); + let diffuse_texture = graphics.device.create_texture(&wgpu::TextureDescriptor { + label: Some("diffuse_texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { @@ -668,36 +698,33 @@ impl Texture { ); let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); - let diffuse_sampler = graphics + let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: address_mode, + address_mode_v: address_mode, + address_mode_w: address_mode, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let diffuse_bind_group = graphics .device - .create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: address_mode, - address_mode_v: address_mode, - address_mode_w: address_mode, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + label: Some("texture_bind_group"), }); - let diffuse_bind_group = - graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), - }, - ], - label: Some("texture_bind_group"), - }); - Self { texture: diffuse_texture, sampler: diffuse_sampler, @@ -708,14 +735,17 @@ impl Texture { } } - /// A helper function that loads the texture from a path. Still returns the same [`Texture`]. - pub async fn load_texture(graphics: Arc<SharedGraphicsContext>, path: &PathBuf) -> anyhow::Result<Texture> { + /// A helper function that loads the texture from a path. Still returns the same [`Texture`]. + pub async fn load_texture( + graphics: Arc<SharedGraphicsContext>, + path: &PathBuf, + ) -> anyhow::Result<Texture> { let data = fs::read(path)?; Ok(Self::new(graphics.clone(), &data)) } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct Instance { pub position: DVec3, pub rotation: DQuat, @@ -17,7 +17,7 @@ use app_dirs2::{AppDataType, AppInfo}; use chrono::Local; use colored::Colorize; use egui::TextureId; -use egui_wgpu_backend::ScreenDescriptor; +use egui_wgpu::ScreenDescriptor; use env_logger::Builder; use futures::executor::block_on; use gilrs::{Gilrs, GilrsBuilder}; @@ -44,10 +44,7 @@ use winit::{ window::Window, }; -use crate::{ - egui_renderer::EguiRenderer, - graphics::Texture, -}; +use crate::{egui_renderer::EguiRenderer, graphics::Texture}; pub use gilrs; use log::LevelFilter; @@ -104,10 +101,21 @@ impl State { .await?; let info = adapter.get_info(); + let os_info = os_info::get(); log::info!( "\n==================== BACKEND INFO ==================== Backend: {} +Software: + Architecture: {:?} + Bitness: {:?} + Codename: {:?} + Edition: {:?} + Os Type: {:?} + Version: {:?} + TLDR: {} + + Hardware: Adapter Name: {} Vendor: {} @@ -118,6 +126,13 @@ Hardware: ======================================================= ", info.backend.to_string(), + os_info.architecture(), + os_info.bitness(), + os_info.codename(), + os_info.edition(), + os_info.os_type(), + os_info.version(), + os_info, info.name, info.vendor, info.device, @@ -173,13 +188,19 @@ Hardware: label: Some("texture_bind_group_layout"), }); - let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new(&device, config.format, 1, &window))); - - let texture_id = Arc::get_mut(&mut egui_renderer).unwrap().lock().renderer().egui_texture_from_wgpu_texture( + let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new( &device, - &viewport_texture.view, - wgpu::FilterMode::Linear, - ); + config.format, + None, + 1, + &window, + ))); + + let texture_id = Arc::get_mut(&mut egui_renderer) + .unwrap() + .lock() + .renderer() + .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear); let result = Self { surface, @@ -212,15 +233,15 @@ Hardware: Texture::create_depth_texture(&self.config, &self.device, Some("depth texture")); self.viewport_texture = Texture::create_viewport_texture(&self.config, &self.device, Some("viewport texture")); - self.texture_id = Arc::get_mut(&mut self - .egui_renderer).unwrap().lock() + self.egui_renderer + .lock() .renderer() - .egui_texture_from_wgpu_texture( + .update_egui_texture_from_wgpu_texture( &self.device, &self.viewport_texture.view, wgpu::FilterMode::Linear, - ) - .into(); + *self.texture_id, + ); } /// Asynchronously renders the scene and the egui renderer. I don't know what else to say. @@ -261,12 +282,9 @@ Hardware: }, }; - let size = self.window.inner_size(); - let screen_descriptor = ScreenDescriptor { - physical_width: size.width, - physical_height: size.height, - scale_factor: self.window.scale_factor() as f32, + size_in_pixels: [self.config.width, self.config.height], + pixels_per_point: self.window.scale_factor() as f32, }; let view = output @@ -280,10 +298,10 @@ Hardware: let viewport_view = { &self.viewport_texture.view.clone() }; - self.egui_renderer.lock().begin_frame(); + self.egui_renderer.lock().begin_frame(&self.window); let mut graphics = graphics::RenderContext::from_state(self, viewport_view, &mut encoder); - + scene_manager .update(previous_dt, &mut graphics, event_loop) .await; @@ -407,14 +425,16 @@ impl App { /// - 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 fn run<F>(config: WindowConfiguration, app_name: &str, setup: F) -> anyhow::Result<()> + pub async fn run<F>(config: WindowConfiguration, app_name: &str, 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"); - std::fs::create_dir_all(&log_dir).expect("Failed to create log dir"); + tokio::fs::create_dir_all(&log_dir) + .await + .expect("Failed to create log dir"); let datetime_str = Local::now().format("%Y-%m-%d_%H-%M-%S"); let log_filename = format!("{}.{}.log", app_name, datetime_str); @@ -562,7 +582,10 @@ impl ApplicationHandler for App { None => return, }; - state.egui_renderer.lock().handle_input(&event); + state + .egui_renderer + .lock() + .handle_input(&state.window, &event); match event { WindowEvent::CloseRequested => { @@ -212,13 +212,16 @@ impl LazyType for LazyLight { fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T> { let label_str = self.label.clone().unwrap_or_else(|| "Light".to_string()); - + let forward = DVec3::new(0.0, 0.0, -1.0); let direction = self.transform.rotation * forward; let uniform = LightUniform { position: dvec3_to_uniform_array(self.transform.position), - direction: dvec3_direction_to_uniform_array(direction, self.light_component.outer_cutoff_angle), + direction: dvec3_direction_to_uniform_array( + direction, + self.light_component.outer_cutoff_angle, + ), colour: dvec3_colour_to_uniform_array( self.light_component.colour * self.light_component.intensity as f64, self.light_component.light_type, @@ -232,33 +235,39 @@ impl LazyType for LazyLight { let cube_model = if let Some(lazy_model) = self.cube_lazy_model { lazy_model.poke(graphics.clone())? } else { - anyhow::bail!("The light cube LazyModel has not been initialised yet. Use Light::new(/** params */).preload_cube_model() to preload it (which is required)"); + anyhow::bail!( + "The light cube LazyModel has not been initialised yet. Use Light::new(/** params */).preload_cube_model() to preload it (which is required)" + ); }; let buffer = graphics.create_uniform(uniform, self.label.as_deref()); - let layout = graphics.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: self.label.as_deref(), - }); - - let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - label: self.label.as_deref(), - }); + let layout = graphics + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: self.label.as_deref(), + }); + + let bind_group = graphics + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + label: self.label.as_deref(), + }); let instance = Instance::new( self.transform.position, @@ -266,11 +275,14 @@ impl LazyType for LazyLight { DVec3::new(0.25, 0.25, 0.25), ); - let instance_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: self.label.as_deref().or(Some("instance buffer")), - contents: bytemuck::cast_slice(&[instance.to_raw()]), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); + let instance_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: self.label.as_deref().or(Some("instance buffer")), + contents: bytemuck::cast_slice(&[instance.to_raw()]), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); log::debug!("Created new light [{}]", label_str); @@ -286,6 +298,7 @@ impl LazyType for LazyLight { } } +#[derive(Clone)] pub struct Light { pub uniform: LightUniform, cube_model: Model, @@ -312,7 +325,8 @@ impl Light { let lazy_model = Model::lazy_load( include_bytes!("../../resources/cube.glb").to_vec(), result.label.as_deref(), - ).await?; + ) + .await?; result.cube_lazy_model = Some(lazy_model); } Ok(result) @@ -352,22 +366,21 @@ impl Light { let buffer = graphics.create_uniform(uniform, label.clone()); - let layout = - graphics - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: label.clone(), - }); + let layout = graphics + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: label.clone(), + }); let bind_group = graphics .device @@ -453,6 +466,7 @@ impl Light { } } +#[derive(Clone)] pub struct LightManager { pub pipeline: Option<RenderPipeline>, light_array_buffer: Option<Buffer>, @@ -472,22 +486,21 @@ impl LightManager { } pub fn create_light_array_resources(&mut self, graphics: Arc<SharedGraphicsContext>) { - let layout = - graphics - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("Light Array Layout"), - }); + let layout = graphics + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("Light Array Layout"), + }); let buffer = graphics.create_uniform(LightArrayUniform::default(), Some("Light Array")); @@ -505,6 +518,7 @@ impl LightManager { self.light_array_layout = Some(layout); self.light_array_buffer = Some(buffer); self.light_array_bind_group = Some(bind_group); + log::debug!("Created light array resources") } pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) { @@ -8,12 +8,12 @@ use parking_lot::Mutex; // material::{DataContent, TextureType}, // scene::{PostProcess, Scene}, // }; +use rayon::prelude::*; use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use std::{mem, ops::Range, path::PathBuf}; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt}; -use rayon::prelude::*; pub const GREY_TEXTURE_BYTES: &'static [u8] = include_bytes!("../../resources/grey.png"); @@ -86,56 +86,66 @@ impl LazyType for LazyModel { fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T> { let start = Instant::now(); - + let cache_key = self.parsed_data.label.clone(); if let Some(cached_model) = MEMORY_MODEL_CACHE.lock().get(&cache_key) { log::debug!("Model loaded from cache during poke: {:?}", cache_key); return Ok(cached_model.clone()); } - log::debug!("Creating GPU resources for model: {:?}", self.parsed_data.label); + log::debug!( + "Creating GPU resources for model: {:?}", + self.parsed_data.label + ); let mut materials = Vec::new(); for material_data in &self.parsed_data.material_data { let texture_start = Instant::now(); - + let diffuse_texture = Texture::from_rgba_buffer( - graphics.clone(), - &material_data.rgba_data, - material_data.dimensions + graphics.clone(), + &material_data.rgba_data, + material_data.dimensions, ); - + let bind_group = diffuse_texture.bind_group().to_owned(); - + materials.push(Material { name: material_data.name.clone(), diffuse_texture, bind_group, }); - - log::debug!("Created GPU texture for material '{}' in {:?}", - material_data.name, texture_start.elapsed()); + + log::debug!( + "Created GPU texture for material '{}' in {:?}", + material_data.name, + texture_start.elapsed() + ); } let mut meshes = Vec::new(); for mesh_data in &self.parsed_data.mesh_data { let buffer_start = Instant::now(); - let vertex_buffer = graphics.clone() - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{} Vertex Buffer", mesh_data.name)), - contents: bytemuck::cast_slice(&mesh_data.vertices), - usage: wgpu::BufferUsages::VERTEX, - }); + let vertex_buffer = + graphics + .clone() + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Vertex Buffer", mesh_data.name)), + contents: bytemuck::cast_slice(&mesh_data.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); - let index_buffer = graphics.clone() - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{} Index Buffer", mesh_data.name)), - contents: bytemuck::cast_slice(&mesh_data.indices), - usage: wgpu::BufferUsages::INDEX, - }); + let index_buffer = + graphics + .clone() + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Index Buffer", mesh_data.name)), + contents: bytemuck::cast_slice(&mesh_data.indices), + usage: wgpu::BufferUsages::INDEX, + }); meshes.push(Mesh { name: mesh_data.name.clone(), @@ -145,8 +155,11 @@ impl LazyType for LazyModel { material: mesh_data.material_index, }); - log::debug!("Created GPU buffers for mesh '{}' in {:?}", - mesh_data.name, buffer_start.elapsed()); + log::debug!( + "Created GPU buffers for mesh '{}' in {:?}", + mesh_data.name, + buffer_start.elapsed() + ); } let model = Model { @@ -157,25 +170,26 @@ impl LazyType for LazyModel { }; MEMORY_MODEL_CACHE.lock().insert(cache_key, model.clone()); - log::debug!("Model GPU resource creation completed in {:?}", start.elapsed()); - + log::debug!( + "Model GPU resource creation completed in {:?}", + start.elapsed() + ); + Ok(model) } } - impl Model { - - /// Creates a [`LazyModel`]. + /// Creates a [`LazyModel`]. pub async fn lazy_load( buffer: impl AsRef<[u8]>, label: Option<&str>, ) -> anyhow::Result<LazyModel> { let start = Instant::now(); let label_str = label.unwrap_or("default"); - + log::debug!("Starting lazy load for model: {:?}", label_str); - + let res_ref = ResourceReference::from_bytes(buffer.as_ref()); let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?; @@ -183,26 +197,27 @@ impl Model { for material in gltf.materials() { log::debug!("Processing material: {:?}", material.name()); let material_name = material.name().unwrap_or("Unnamed Material").to_string(); - - let image_data = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() { - let texture_info = pbr.texture(); - let image = texture_info.source(); - match image.source() { - gltf::image::Source::View { view, mime_type: _ } => { - let buffer_data = &buffers[view.buffer().index()]; - let start = view.offset(); - let end = start + view.length(); - buffer_data[start..end].to_vec() - } - gltf::image::Source::Uri { uri, mime_type: _ } => { - log::warn!("External URI textures not supported: {}", uri); - GREY_TEXTURE_BYTES.to_vec() + + let image_data = + if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() { + let texture_info = pbr.texture(); + let image = texture_info.source(); + match image.source() { + gltf::image::Source::View { view, mime_type: _ } => { + let buffer_data = &buffers[view.buffer().index()]; + let start = view.offset(); + let end = start + view.length(); + buffer_data[start..end].to_vec() + } + gltf::image::Source::Uri { uri, mime_type: _ } => { + log::warn!("External URI textures not supported: {}", uri); + GREY_TEXTURE_BYTES.to_vec() + } } - } - } else { - GREY_TEXTURE_BYTES.to_vec() - }; - + } else { + GREY_TEXTURE_BYTES.to_vec() + }; + texture_data.push((material_name, image_data)); } @@ -215,14 +230,18 @@ impl Model { .into_par_iter() .map(|(material_name, image_data)| { let material_start = Instant::now(); - - let diffuse_image = image::load_from_memory(&image_data) - .expect("Failed to load image from memory"); + + let diffuse_image = + image::load_from_memory(&image_data).expect("Failed to load image from memory"); let diffuse_rgba = diffuse_image.to_rgba8(); let dimensions = diffuse_image.dimensions(); - - log::debug!("Processed material '{}' in {:?}", material_name, material_start.elapsed()); - + + log::debug!( + "Processed material '{}' in {:?}", + material_name, + material_start.elapsed() + ); + ParsedMaterialData { name: material_name, rgba_data: diffuse_rgba.into_raw(), @@ -231,14 +250,17 @@ impl Model { }) .collect(); - log::debug!("Parallel material processing took: {:?}", parallel_start.elapsed()); + log::debug!( + "Parallel material processing took: {:?}", + parallel_start.elapsed() + ); let mut mesh_data = Vec::new(); for mesh in gltf.meshes() { log::debug!("Processing mesh: {:?}", mesh.name()); for primitive in mesh.primitives() { let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); - + let positions: Vec<[f32; 3]> = reader .read_positions() .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))? @@ -289,17 +311,20 @@ impl Model { material_data: processed_materials, }; - log::debug!("Lazy load completed for model: {:?} in {:?}", label_str, start.elapsed()); - + log::debug!( + "Lazy load completed for model: {:?} in {:?}", + label_str, + start.elapsed() + ); + Ok(LazyModel { parsed_data }) } pub async fn load_from_memory( graphics: Arc<SharedGraphicsContext>, buffer: impl AsRef<[u8]>, - label: Option<&str> + label: Option<&str>, ) -> anyhow::Result<Model> { - println!("========== Benchmarking speed of loading {:?} ==========", label); let start = Instant::now(); let cache_key = label.unwrap_or("default").to_string(); @@ -308,6 +333,10 @@ impl Model { return Ok(cached_model.clone()); } + println!( + "========== Benchmarking speed of loading {:?} ==========", + label + ); log::debug!("Loading from memory"); let res_ref = ResourceReference::from_bytes(buffer.as_ref()); @@ -318,26 +347,27 @@ impl Model { for material in gltf.materials() { log::debug!("Processing material: {:?}", material.name()); let material_name = material.name().unwrap_or("Unnamed Material").to_string(); - - let image_data = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() { - let texture_info = pbr.texture(); - let image = texture_info.source(); - match image.source() { - gltf::image::Source::View { view, mime_type: _ } => { - let buffer_data = &buffers[view.buffer().index()]; - let start = view.offset(); - let end = start + view.length(); - buffer_data[start..end].to_vec() - } - gltf::image::Source::Uri { uri, mime_type: _ } => { - log::warn!("External URI textures not supported: {}", uri); - GREY_TEXTURE_BYTES.to_vec() + + let image_data = + if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() { + let texture_info = pbr.texture(); + let image = texture_info.source(); + match image.source() { + gltf::image::Source::View { view, mime_type: _ } => { + let buffer_data = &buffers[view.buffer().index()]; + let start = view.offset(); + let end = start + view.length(); + buffer_data[start..end].to_vec() + } + gltf::image::Source::Uri { uri, mime_type: _ } => { + log::warn!("External URI textures not supported: {}", uri); + GREY_TEXTURE_BYTES.to_vec() + } } - } - } else { - GREY_TEXTURE_BYTES.to_vec() - }; - + } else { + GREY_TEXTURE_BYTES.to_vec() + }; + texture_data.push((material_name, image_data)); } @@ -350,38 +380,49 @@ impl Model { .into_par_iter() .map(|(material_name, image_data)| { let material_start = Instant::now(); - + let load_start = Instant::now(); let diffuse_image = image::load_from_memory(&image_data).unwrap(); println!("Loading image to memory: {:?}", load_start.elapsed()); - + let rgba_start = Instant::now(); let diffuse_rgba = diffuse_image.to_rgba8(); - println!("Converting diffuse image to rgba8 took {:?}", rgba_start.elapsed()); - + println!( + "Converting diffuse image to rgba8 took {:?}", + rgba_start.elapsed() + ); + let dimensions = diffuse_image.dimensions(); - - println!("Parallel processing of material '{}' took: {:?}", material_name, material_start.elapsed()); - + + println!( + "Parallel processing of material '{}' took: {:?}", + material_name, + material_start.elapsed() + ); + (material_name, diffuse_rgba.into_raw(), dimensions) }) .collect(); - println!("Total parallel image processing took: {:?}", parallel_start.elapsed()); + println!( + "Total parallel image processing took: {:?}", + parallel_start.elapsed() + ); let mut materials = Vec::new(); for (material_name, rgba_data, dimensions) in processed_textures { let start = Instant::now(); - - let diffuse_texture = Texture::from_rgba_buffer(graphics.clone(), &rgba_data, dimensions); + + let diffuse_texture = + Texture::from_rgba_buffer(graphics.clone(), &rgba_data, dimensions); let bind_group = diffuse_texture.bind_group().to_owned(); - + materials.push(Material { name: material_name, diffuse_texture, bind_group, }); - + println!("Time to create GPU texture: {:?}", start.elapsed()); } @@ -389,7 +430,7 @@ impl Model { log::debug!("Processing mesh: {:?}", mesh.name()); for primitive in mesh.primitives() { let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); - + let positions: Vec<[f32; 3]> = reader .read_positions() .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))? @@ -422,21 +463,23 @@ impl Model { .into_u32() .collect(); - let vertex_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", label)), - 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 vertex_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Vertex Buffer", label)), + 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 material_index = primitive.material().index().unwrap_or(0); @@ -481,7 +524,7 @@ impl Model { return Ok(cached_model.clone()); } - let buffer = std::fs::read(path)?; + let buffer = tokio::fs::read(path).await?; let model = Self::load_from_memory(graphics, buffer, label).await?; MODEL_CACHE.lock().insert(path_str, model.clone()); @@ -44,5 +44,10 @@ pub fn set_hook() { .set_level(MessageLevel::Error) .show(); } + + log::error!( + "Additional crash info (backtrace): \n{:#?}", + backtrace::Backtrace::new() + ); })); } @@ -74,7 +74,7 @@ impl Manager { pub async fn update<'a>( &mut self, dt: f32, - graphics: &mut crate::graphics::RenderContext<'a>, + graphics: &mut crate::graphics::RenderContext<'a>, event_loop: &ActiveEventLoop, ) { // transition scene @@ -1,14 +1,14 @@ -/// 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 crate::entity::AdoptedEntity; use crate::graphics::{SharedGraphicsContext, Texture}; use crate::model::{Material, Mesh, Model, ModelVertex}; 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), @@ -51,22 +51,20 @@ impl LazyPlaneBuilder { indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); - 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 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(), @@ -80,7 +78,7 @@ impl LazyPlaneBuilder { graphics.clone(), &self.rgba_data, self.dimensions, - AddressMode::Repeat + AddressMode::Repeat, ); let bind_group = diffuse_texture.bind_group().clone(); let material = Material { @@ -100,7 +98,7 @@ impl LazyPlaneBuilder { } } -/// Creates a plane in the form of an AdoptedEntity. +/// Creates a plane in the form of an AdoptedEntity. pub struct PlaneBuilder { width: f32, height: f32, @@ -190,22 +188,20 @@ impl PlaneBuilder { indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); - let vertex_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", label)), - 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 vertex_buffer = graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Vertex Buffer", label)), + 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(), @@ -76,7 +76,7 @@ impl ResourceReference { } /// Creates a new `ResourceReference` from bytes - pub fn from_bytes(bytes: impl AsRef<[u8]>,) -> Self { + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { Self { ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()), } @@ -8,7 +8,6 @@ readme = "README.md" [dependencies] anyhow.workspace = true -app_dirs2.workspace = true bincode.workspace = true chrono.workspace = true dropbear-engine.workspace = true @@ -24,14 +23,10 @@ parking_lot.workspace = true ron.workspace = true serde.workspace = true winit.workspace = true -wasmer.workspace = true -lazy_static.workspace = true -rayon.workspace = true -reqwest.workspace = true -flate2.workspace = true -tar.workspace = true zip.workspace = true tokio.workspace = true +boa_engine.workspace = true +rayon.workspace = true [dev-dependencies] pollster = "*" @@ -0,0 +1 @@ +//! A module used for exposing functions @@ -3,10 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use wasmer::{Function, FunctionEnvMut}; -use winit::{event::MouseButton, keyboard::KeyCode, platform::scancode::PhysicalKeyExtScancode}; - -use crate::scripting::{DropbearScriptingAPIContext, ScriptableModuleWithEnv}; +use winit::{event::MouseButton, keyboard::KeyCode}; #[derive(Clone)] pub struct InputState { @@ -50,41 +47,3 @@ impl InputState { self.pressed_keys.contains(&key) } } - -impl ScriptableModuleWithEnv for InputState { - type T = DropbearScriptingAPIContext; - - fn register(env: &wasmer::FunctionEnv<Self::T>, imports: &mut wasmer::Imports, store: &mut wasmer::Store) -> anyhow::Result<()> { - fn is_key_pressed_impl(env: FunctionEnvMut<DropbearScriptingAPIContext>, key_code: u32) -> u32 { - if let Some(input) = env.data().get_input() { - match KeyCode::from_scancode(key_code) { - winit::keyboard::PhysicalKey::Code(key_code) => { - if input.is_key_pressed(key_code) { 1 } else { 0 } - }, - winit::keyboard::PhysicalKey::Unidentified(_) => 0, - } - } else { - 0 - } - } - let is_key_pressed = Function::new_typed_with_env(store, &env, is_key_pressed_impl); - - fn get_mouse_position_impl(env: FunctionEnvMut<DropbearScriptingAPIContext>) -> (f64, f64) { - if let Some(input) = env.data().get_input() { - input.mouse_pos - } else { - (0.0, 0.0) - } - } - let get_mouse_position = Function::new_typed_with_env(store, &env, get_mouse_position_impl); - - imports.define(Self::module_name(), "getMousePosition", get_mouse_position); - imports.define(Self::module_name(), "isKeyPressed", is_key_pressed); - - Ok(()) - } - - fn module_name() -> &'static str { - "dropbear_input" - } -} @@ -1,4 +1,5 @@ pub mod camera; +pub mod dropbear; pub mod input; pub mod logging; pub mod scripting; @@ -1,33 +1,15 @@ -pub mod dropbear; -pub mod build; - use crate::input::InputState; -use crate::scripting::dropbear::DropbearAPI; use crate::states::{EntityNode, PROJECT, SOURCE, ScriptComponent, Value}; +use boa_engine::property::PropertyKey; +use boa_engine::{Context, JsString, JsValue, Source}; use dropbear_engine::entity::{AdoptedEntity, Transform}; use hecs::{Entity, World}; -use wasmer::{FunctionEnv, Imports, Instance, Module, Store, imports}; +use parking_lot::RwLock; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use std::{collections::HashMap, fs}; -/// A trait that describes a module that can be registered. -pub trait ScriptableModule { - type Data; - - fn register(data: &Self::Data, imports: &mut Imports, store: &mut Store) -> anyhow::Result<()>; - - fn module_name() -> &'static str; -} - -pub trait ScriptableModuleWithEnv { - type T; - - fn register(env: &FunctionEnv<Self::T>, imports: &mut Imports, store: &mut Store) -> anyhow::Result<()>; - - fn module_name() -> &'static str; -} - pub const TEMPLATE_SCRIPT: &'static str = include_str!("../../resources/template.ts"); pub enum ScriptAction { @@ -46,7 +28,7 @@ pub enum ScriptAction { #[derive(Clone)] pub struct DropbearScriptingAPIContext { pub current_entity: Option<Entity>, - current_world: Option<Arc<World>>, + current_world: Option<Arc<RwLock<World>>>, pub current_input: Option<InputState>, pub persistent_data: HashMap<String, Value>, pub frame_data: HashMap<String, Value>, @@ -63,7 +45,7 @@ impl DropbearScriptingAPIContext { } } - pub fn set_context(&mut self, entity: Entity, world: Arc<World>, input: &InputState) { + pub fn set_context(&mut self, entity: Entity, world: Arc<RwLock<World>>, input: &InputState) { self.current_entity = Some(entity); self.current_world = Some(world); self.current_input = Some(input.clone()); @@ -102,25 +84,20 @@ impl DropbearScriptingAPIContext { pub fn cleanup_entity_data(&mut self, entity: Entity) { let entity_prefix = format!("entity_{:?}_", entity); - self.persistent_data.retain(|k, _| !k.starts_with(&entity_prefix)); + self.persistent_data + .retain(|k, _| !k.starts_with(&entity_prefix)); } } pub struct ScriptManager { - pub store: Store, - compiled_scripts: HashMap<String, Module>, - entity_script_data: HashMap<hecs::Entity, u32>, + compiled_scripts: HashMap<String, String>, script_context: DropbearScriptingAPIContext, } impl ScriptManager { pub fn new() -> anyhow::Result<Self> { - let store = Store::default(); - let result = Self { - store, compiled_scripts: HashMap::new(), - entity_script_data: HashMap::new(), script_context: DropbearScriptingAPIContext::new(), }; @@ -128,9 +105,13 @@ impl ScriptManager { Ok(result) } - pub fn load_script(&mut self, script_name: &String, script_content: impl AsRef<[u8]>) -> anyhow::Result<String> { - let module = Module::new(self.store.engine(), script_content)?; - self.compiled_scripts.insert(script_name.clone(), module); + pub fn load_script( + &mut self, + script_name: &String, + script_content: String, + ) -> anyhow::Result<String> { + self.compiled_scripts + .insert(script_name.clone(), script_content); log::debug!("Loaded script [{}]", script_name); Ok(script_name.clone()) } @@ -139,36 +120,44 @@ impl ScriptManager { &mut self, entity_id: hecs::Entity, script_name: &str, - world: &mut Arc<World>, - input_state: &InputState + world: Arc<RwLock<World>>, + input_state: &InputState, ) -> anyhow::Result<()> { log_once::debug_once!("init_entity_script: {} for {:?}", script_name, entity_id); - if let Some(module) = self.compiled_scripts.get(script_name).cloned() { - self.script_context.set_context(entity_id, world.clone(), input_state); - - let import_obj = self.create_imports()?; - let instance = Instance::new(&mut self.store, &module, &import_obj)?; - - if let Ok(alloc_func) = instance.exports.get_function("__alloc") { - let size = std::mem::size_of::<Transform>() as i32; - let result = alloc_func.call(&mut self.store, &[size.into()])?; - if let Some(wasmer::Value::I32(ptr)) = result.get(0) { - self.entity_script_data.insert(entity_id, *ptr as u32); - } - } - - self.sync_entity_to_memory(entity_id, world, &instance)?; - - if let Ok(init_func) = instance.exports.get_function("init") { - init_func.call(&mut self.store, &[])?; + if let Some(script_source) = self.compiled_scripts.get(script_name) { + self.script_context + .set_context(entity_id, world, input_state); + + let mut context = Context::default(); + self.expose(&mut context); + + context + .eval(Source::from_bytes(script_source.clone().as_bytes())) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + let load = context + .global_object() + .get( + PropertyKey::String(JsString::from_str("load")?), + &mut context, + ) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + if load.is_callable() { + let _ = load + .as_callable() + .unwrap() + .call(&JsValue::undefined(), &[], &mut context); + } else { + log::warn!( + "Unable to call load in script {}: Load is not a callable function", + script_name + ) } - self.sync_memory_to_entity(entity_id, world, &instance)?; - self.script_context.clear_context(); - - Ok(()) + return Ok(()); } else { Err(anyhow::anyhow!("Script '{}' not found", script_name)) } @@ -178,89 +167,58 @@ impl ScriptManager { &mut self, entity_id: hecs::Entity, script_name: &str, - world: &mut Arc<World>, + world: Arc<RwLock<World>>, input_state: &InputState, dt: f32, ) -> anyhow::Result<()> { log_once::debug_once!("Update entity script name: {}", script_name); if let Some(module) = self.compiled_scripts.get(script_name).cloned() { - self.script_context.set_context(entity_id, world.clone(), input_state); - - let import_object = self.create_imports()?; - let instance = Instance::new(&mut self.store, &module, &import_object)?; - - self.sync_entity_to_memory(entity_id, world, &instance)?; - - if let Ok(update_func) = instance.exports.get_function("update") { - let dt_value = wasmer::Value::F32(dt); - update_func.call(&mut self.store, &[dt_value])?; + self.script_context + .set_context(entity_id, world.clone(), input_state); + + let mut context = Context::default(); + self.expose(&mut context); + + context + .eval(Source::from_bytes(module.as_bytes())) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + let update = context + .global_object() + .get( + PropertyKey::String(JsString::from_str("update")?), + &mut context, + ) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + if update.is_callable() { + let dt_val = JsValue::from(dt); + let _ = update.as_callable().unwrap().call( + &JsValue::undefined(), + &[dt_val], + &mut context, + ); + } else { + log::warn!( + "Unable to call update in script {}: Update is not a callable function", + script_name + ) } - - self.sync_memory_to_entity(entity_id, world, &instance)?; - + self.script_context.clear_context(); - + return Ok(()); } else { - log_once::error_once!("Unable to fetch compiled scripts for entity {:?}. Script Name: {}", entity_id, script_name); + log_once::error_once!( + "Unable to fetch compiled scripts for entity {:?}. Script Name: {}", + entity_id, + script_name + ); } Ok(()) } - pub fn remove_entity_script(&mut self, entity_id: hecs::Entity) { - self.entity_script_data.remove(&entity_id); - } - - fn sync_entity_to_memory(&self, entity_id: hecs::Entity, world: &mut Arc<World>, instance: &Instance) -> anyhow::Result<()> { - if let Some(&memory_offset) = self.entity_script_data.get(&entity_id) { - let memory = instance.exports.get_memory("memory")?; - let memory_view = memory.view(&self.store); - - if let Ok(transform) = Arc::get_mut(world).unwrap().query_one_mut::<&Transform>(entity_id) { - let transform_bytes = unsafe { std::slice::from_raw_parts( - transform as *const Transform as *const u8, - std::mem::size_of::<Transform>() - )}; - - for (i, &byte) in transform_bytes.iter().enumerate() { - memory_view.write_u8((memory_offset + i as u32).into(), byte)?; - } - } - } - Ok(()) - } - - fn sync_memory_to_entity(&self, entity_id: hecs::Entity, world: &mut Arc<World>, instance: &Instance) -> anyhow::Result<()> { - if let Some(&memory_offset) = self.entity_script_data.get(&entity_id) { - let memory = instance.exports.get_memory("memory")?; - let memory_view = memory.view(&self.store); - - Self::update::<Transform>(memory_offset, &memory_view, world, &entity_id)?; - } - Ok(()) - } - - fn update<T: Send + Sync + 'static>(memory_offset: u32, memory_view: &wasmer::MemoryView<'_>, world: &mut Arc<World>, entity_id: &hecs::Entity) -> anyhow::Result<()> { - let mut obj_bytes = vec![0u8; std::mem::size_of::<T>()]; - for (i, byte) in obj_bytes.iter_mut().enumerate() { - *byte = memory_view.read_u8((memory_offset + i as u32).into())?; - } - - let obj = unsafe { - std::ptr::read(obj_bytes.as_ptr() as *const T) - }; - - Arc::get_mut(world).unwrap().insert_one(*entity_id, obj)?; - Ok(()) - } - - fn create_imports(&mut self) -> anyhow::Result<Imports> { - let mut imports = imports! {}; - - DropbearAPI::register(&self.script_context, &mut imports, &mut self.store)?; - - Ok(imports) - } + pub fn expose(&self, _context: &mut Context) {} } pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { @@ -342,14 +300,14 @@ pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { } pub fn convert_entity_to_group( - world: &World, + world: Arc<RwLock<World>>, entity_id: hecs::Entity, ) -> anyhow::Result<EntityNode> { - if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { + if let Ok(mut query) = world.read().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.get::<&ScriptComponent>(entity_id) { + let script_node = if let Ok(script) = world.read().get::<&ScriptComponent>(entity_id) { Some(EntityNode::Script { name: script.name.clone(), path: script.path.clone(), @@ -381,12 +339,14 @@ pub fn convert_entity_to_group( } pub fn attach_script_to_entity( - world: &mut World, + world: Arc<RwLock<World>>, entity_id: hecs::Entity, script_component: ScriptComponent, ) -> anyhow::Result<()> { - if let Err(e) = world.insert_one(entity_id, script_component) { - return Err(anyhow::anyhow!("Failed to attach script to entity: {}", e)); + { + if let Err(e) = world.write().insert_one(entity_id, script_component) { + return Err(anyhow::anyhow!("Failed to attach script to entity: {}", e)); + } } log::info!("Successfully attached script to entity {:?}", entity_id); @@ -1,406 +0,0 @@ -use std::path::PathBuf; -use std::process::Command; -use app_dirs2::{AppInfo, AppDataType, app_dir}; -use tokio::fs; -use tokio::io::AsyncWriteExt; - -const GLEAM_VERSION: &'static str = "1.12.0"; -const BUN_VERSION: &'static str = "1.2.22"; -const JAVY_VERSION: &'static str = "6.0.0"; - -pub const APP_INFO: AppInfo = AppInfo { - name: "Eucalyptus", - author: "4tkbytes", -}; - -/// Compiles a gleam project into WASM through a pipeline. -pub struct GleamScriptCompiler { - #[allow(dead_code)] - project_location: PathBuf, -} - -impl GleamScriptCompiler { - pub fn new(project_location: &PathBuf) -> Self { - GleamScriptCompiler { - project_location: project_location.clone(), - } - } - - pub async fn build(self) -> anyhow::Result<()> { - Self::ensure_dependencies().await?; - Ok(()) - } - - pub async fn ensure_dependencies() -> anyhow::Result<()> { - println!("Checking dependencies..."); - - let gleam_available = Self::check_tool_in_path("gleam").await; - if gleam_available { - println!("Gleam already exists in path"); - } - let bun_available = Self::check_tool_in_path("bun").await; - if bun_available { - println!("Bun already exists in path"); - } - let javy_available = Self::check_tool_in_path("javy").await; - if javy_available { - println!("Javy already exists in path"); - } - - if gleam_available && bun_available && javy_available { - println!("All dependencies found in PATH"); - return Ok(()); - } - - if !(cfg!(target_os = "windows") || cfg!(target_os = "linux") || cfg!(target_os = "macos")) { - anyhow::bail!("The operating system is not supported for building the Gleam project") - } - - let app_dir = app_dir(AppDataType::UserData, &APP_INFO, "") - .map_err(|e| anyhow::anyhow!("Failed to get app directory: {}", e))?; - - if !gleam_available { - Self::download_gleam(&app_dir).await?; - } - - if !bun_available { - Self::download_bun(&app_dir).await?; - } - - if !javy_available { - Self::download_javy(&app_dir).await?; - } - - Ok(()) - } - - async fn check_tool_in_path(tool: &str) -> bool { - let cmd = if cfg!(target_os = "windows") { - Command::new("where").arg(tool).output() - } else { - Command::new("which").arg(tool).output() - }; - - match cmd { - Ok(output) => output.status.success(), - Err(_) => false, - } - } - - pub async fn download_gleam(app_dir: &PathBuf) -> anyhow::Result<()> { - let gleam_dir = app_dir.join("dependencies").join("gleam").join(GLEAM_VERSION); - - if gleam_dir.exists() { - println!("Gleam v{} already cached at {}", GLEAM_VERSION, app_dir.display()); - return Ok(()); - } - - println!("Downloading Gleam v{}...", GLEAM_VERSION); - - let gleam_link = Self::get_gleam_download_url()?; - Self::download_and_extract(&gleam_link, &gleam_dir, "gleam").await?; - - println!("Gleam v{} downloaded successfully", GLEAM_VERSION); - Ok(()) - } - - pub async fn download_bun(app_dir: &PathBuf) -> anyhow::Result<()> { - let bun_dir = app_dir.join("dependencies").join("bun").join(BUN_VERSION); - - if bun_dir.exists() { - println!("Bun v{} already cached at {}", BUN_VERSION, app_dir.display()); - return Ok(()); - } - - println!("Downloading Bun v{}...", BUN_VERSION); - - let bun_link = Self::get_bun_download_url()?; - Self::download_and_extract(&bun_link, &bun_dir, "bun").await?; - - println!("Bun v{} downloaded successfully", BUN_VERSION); - Ok(()) - } - - pub async fn download_javy(app_dir: &PathBuf) -> anyhow::Result<()> { - let javy_dir = app_dir.join("dependencies").join("javy").join(JAVY_VERSION); - - if javy_dir.exists() { - println!("Javy v{} already cached at {}", JAVY_VERSION, app_dir.display()); - return Ok(()); - } - - println!("Downloading Javy v{}...", JAVY_VERSION); - - let javy_link = Self::get_javy_download_url()?; - Self::download_and_extract(&javy_link, &javy_dir, "javy").await?; - - println!("Javy v{} downloaded successfully", JAVY_VERSION); - Ok(()) - } - - async fn download_and_extract(url: &str, target_dir: &PathBuf, tool_name: &str) -> anyhow::Result<()> { - fs::create_dir_all(target_dir).await?; - - let response = reqwest::get(url).await?; - let bytes = response.bytes().await?; - - let temp_file = target_dir.join(format!("{}_download", tool_name)); - let mut file = fs::File::create(&temp_file).await?; - file.write_all(&bytes).await?; - file.sync_all().await?; - drop(file); - - if url.ends_with(".zip") { - Self::extract_zip(&temp_file, target_dir).await?; - } else if url.ends_with(".tar.gz") { - Self::extract_tar_gz(&temp_file, target_dir).await?; - } else if url.ends_with(".gz") { - Self::extract_gz(&temp_file, target_dir, tool_name).await?; - } - - fs::remove_file(&temp_file).await?; - Ok(()) - } - - async fn extract_zip(archive: &PathBuf, target_dir: &PathBuf) -> 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()); - - if file.is_dir() { - fs::create_dir_all(&outpath).await?; - } else { - if let Some(p) = outpath.parent() { - fs::create_dir_all(p).await?; - } - let mut outfile = fs::File::create(&outpath).await?; - let mut buffer = Vec::new(); - std::io::Read::read_to_end(&mut file, &mut buffer)?; - outfile.write_all(&buffer).await?; - } - } - Ok(()) - } - - async fn extract_tar_gz(archive: &PathBuf, target_dir: &PathBuf) -> 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()?); - entry.unpack(path)?; - } - Ok(()) - } - - async fn extract_gz(archive: &PathBuf, target_dir: &PathBuf, tool_name: &str) -> anyhow::Result<()> { - let file = std::fs::File::open(archive)?; - let mut decoder = flate2::read::GzDecoder::new(file); - let mut buffer = Vec::new(); - std::io::Read::read_to_end(&mut decoder, &mut buffer)?; - - let exe_name = if cfg!(target_os = "windows") { - format!("{}.exe", tool_name) - } else { - tool_name.to_string() - }; - - let output_path = target_dir.join(exe_name); - let mut output_file = fs::File::create(&output_path).await?; - output_file.write_all(&buffer).await?; - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = output_file.metadata().await?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&output_path, perms).await?; - } - - Ok(()) - } - - fn get_gleam_download_url() -> anyhow::Result<String> { - let gleam_link = { - #[cfg(target_os = "windows")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-pc-windows-msvc.zip", - GLEAM_VERSION, - GLEAM_VERSION, - arch, - ) - } - - #[cfg(target_os = "linux")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-unknown-linux-musl.tar.gz", - GLEAM_VERSION, - GLEAM_VERSION, - arch, - ) - } - - #[cfg(target_os = "macos")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-apple-darwin.tar.gz", - GLEAM_VERSION, - GLEAM_VERSION, - arch, - ) - } - }; - Ok(gleam_link) - } - - fn get_bun_download_url() -> anyhow::Result<String> { - let bun_link = { - #[cfg(target_os = "windows")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-windows-{}.zip", - BUN_VERSION, - arch, - ) - } - - #[cfg(target_os = "linux")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-linux-{}.zip", - BUN_VERSION, - arch, - ) - } - - #[cfg(target_os = "macos")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-darwin-{}.zip", - BUN_VERSION, - arch, - ) - } - }; - Ok(bun_link) - } - - fn get_javy_download_url() -> anyhow::Result<String> { - let javy_link = { - #[cfg(target_os = "windows")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - anyhow::bail!("This arch is not available for prebuilt download. Please build this from source"); - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-windows-v{}.gz", - JAVY_VERSION, - arch, - JAVY_VERSION - ) - } - - #[cfg(target_os = "linux")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "arm" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-linux-v{}.gz", - JAVY_VERSION, - arch, - JAVY_VERSION - ) - } - - #[cfg(target_os = "macos")] - { - let arch = { - if cfg!(target_arch = "aarch64") { - "arm" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("This architecture is not supported for building the gleam project"); - } - }; - format!("https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-macos-v{}.gz", - JAVY_VERSION, - arch, - JAVY_VERSION - ) - } - }; - Ok(javy_link) - } -} - -#[tokio::test] -async fn check_if_dependencies_install() { - GleamScriptCompiler::ensure_dependencies().await.unwrap(); -} @@ -1,22 +0,0 @@ - -use wasmer::FunctionEnv; - -use crate::{input::InputState, scripting::{DropbearScriptingAPIContext, ScriptableModule, ScriptableModuleWithEnv}}; - -pub struct DropbearAPI; - -impl ScriptableModule for DropbearAPI { - type Data = DropbearScriptingAPIContext; - - fn register(data: &Self::Data, imports: &mut wasmer::Imports, store: &mut wasmer::Store) -> anyhow::Result<()> { - let env = FunctionEnv::new(store, data.clone()); - - InputState::register(&env, imports, store)?; - - Ok(()) - } - - fn module_name() -> &'static str { - "dropbear" - } -} @@ -15,11 +15,13 @@ use once_cell::sync::Lazy; use parking_lot::RwLock; use ron::ser::PrettyConfig; use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::UnboundedSender; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::path::PathBuf; use std::sync::Arc; use std::{fmt, fs}; +use rayon::prelude::*; pub static PROJECT: Lazy<RwLock<ProjectConfig>> = Lazy::new(|| RwLock::new(ProjectConfig::default())); @@ -716,15 +718,21 @@ impl SceneConfig { pub async fn load_into_world( &self, - world: &mut hecs::World, + world: Arc<RwLock<hecs::World>>, graphics: Arc<SharedGraphicsContext>, + progress_sender: Option<UnboundedSender<WorldLoadingStatus>> ) -> anyhow::Result<hecs::Entity> { + + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::Idle); + } + log::info!( "Loading scene [{}], clearing world with {} entities", self.scene_name, - world.len() + world.read().len() ); - world.clear(); + { world.write().clear(); } #[allow(unused_variables)] let project_config = if cfg!(feature = "editor") { @@ -735,281 +743,202 @@ impl SceneConfig { PathBuf::new() }; - log::info!("World cleared, now has {} entities", world.len()); + log::info!("World cleared, now has {} entities", world.read().len()); - for (_entity_index, entity_config) in self.entities.iter().enumerate() { + for (_i, entity_config) in self.entities.iter().cloned().enumerate() { log::debug!("Loading entity: {}", entity_config.label); - let result = match &entity_config.model_path.ref_type { - ResourceReferenceType::File(reference) => { - let path: PathBuf = { - if cfg!(feature = "editor") { - log::debug!("Using feature editor"); - entity_config - .model_path - .to_project_path(project_config.clone()) - .ok_or_else(|| { - anyhow::anyhow!( - "Unable to convert resource reference [{}] to project path", - reference - ) - })? + let entity_configs: Vec<(usize, SceneEntity)> = { + let cloned = self.entities.clone(); + cloned.into_par_iter().enumerate().map(|(i, e)| (i, e)).collect() + }; + + let total = entity_configs.len(); + + for (index, entity_config) in entity_configs { + log::debug!("Loading entity: {}", entity_config.label); + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingEntity { index, name: entity_config.label.clone(), total }); + } + + let result = match &entity_config.model_path.ref_type { + ResourceReferenceType::File(reference) => { + let path: PathBuf = { + if cfg!(feature = "editor") { + log::debug!("Using feature editor"); + entity_config + .model_path + .to_project_path(project_config.clone()) + .ok_or_else(|| { + anyhow::anyhow!( + "Unable to convert resource reference [{}] to project path", + reference + ) + })? + } else { + log::debug!("Using feature data-only"); + entity_config.model_path.to_executable_path()? + } + }; + log::debug!( + "Path for entity {} is {} from reference {}", + entity_config.label, + path.display(), + reference + ); + + let adopted = + AdoptedEntity::new(graphics.clone(), &path, Some(&entity_config.label)) + .await; + let transform = entity_config.transform; + + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + { world.write().spawn((adopted, transform, script, entity_config.properties.clone())); } } else { - log::debug!("Using feature data-only"); - entity_config.model_path.to_executable_path()? + { world.write().spawn((adopted, transform, entity_config.properties.clone())); } } - }; - log::debug!( - "Path for entity {} is {} from reference {}", - entity_config.label, - path.display(), - reference - ); - - let adopted = AdoptedEntity::new(graphics.clone(), &path, Some(&entity_config.label)).await; - - // let adopted = { - // let path_clone = path.clone(); - // let label_clone = entity_config.label.clone(); - - // let (tx, mut rx) = tokio::sync::oneshot::channel(); - - // tokio::task::spawn_local(async move { - // let entity = LazyAdoptedEntity::from_file(&path_clone, Some(&label_clone)).await; - // let _ = tx.send(entity); - // }); - - // loop { - // match rx.try_recv() { - // Ok(result) => { - // break result?.poke(graphics)?; - // }, - // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { - // std::thread::yield_now(); - // continue; - // } - // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { - // return Err(anyhow::anyhow!("Loading task was cancelled")); - // } - // } - // } - // }; - let transform = entity_config.transform; - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), - }; - world.spawn((adopted, transform, script, entity_config.properties.clone())); - } else { - world.spawn((adopted, transform, entity_config.properties.clone())); + + Ok(()) } - - Ok(()) - } - ResourceReferenceType::Bytes(bytes) => { - log::info!("Loading entity from bytes [Len: {}]", bytes.len()); - let bytes = bytes.to_owned(); - - let model = Model::load_from_memory(graphics.clone(), bytes, Some(&entity_config.label)).await?; - let adopted = AdoptedEntity::adopt(graphics.clone(), model).await; - - // let adopted = { - // let bytes_clone = bytes.clone(); - // let label_clone = entity_config.label.clone(); - - // let (tx, mut rx) = tokio::sync::oneshot::channel(); - - // tokio::task::spawn_local(async move { - // let entity = LazyAdoptedEntity::from_memory(bytes_clone, Some(label_clone.as_str())).await; - // let _ = tx.send(entity); - // }); - - // loop { - // match rx.try_recv() { - // Ok(result) => { - // break result?.poke(graphics)?; - // }, - // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { - // std::thread::yield_now(); - // continue; - // } - // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { - // return Err(anyhow::anyhow!("Loading task was cancelled")); - // } - // } - // } - // }; - let transform = entity_config.transform; - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), - }; - world.spawn((adopted, transform, script, entity_config.properties.clone())); - } else { - world.spawn((adopted, transform, entity_config.properties.clone())); + ResourceReferenceType::Bytes(bytes) => { + log::info!("Loading entity from bytes [Len: {}]", bytes.len()); + let bytes = bytes.to_owned(); + + let model = Model::load_from_memory( + graphics.clone(), + bytes, + Some(&entity_config.label), + ) + .await?; + let adopted = AdoptedEntity::adopt(graphics.clone(), model).await; + + let transform = entity_config.transform; + + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + { world.write().spawn((adopted, transform, script, entity_config.properties.clone())); } + } else { + { world.write().spawn((adopted, transform, entity_config.properties.clone())); } + } + + Ok(()) } - - Ok(()) - } - ResourceReferenceType::Plane => { - let width = entity_config - .properties - .custom_properties - .get("width") - .ok_or_else(|| anyhow::anyhow!("Entity has no width property"))?; - let width = match width { - Value::Float(width) => width, - _ => panic!("Entity has a width property that is not a float"), - }; - let height = entity_config - .properties - .custom_properties - .get("height") - .ok_or_else(|| anyhow::anyhow!("Entity has no height property"))?; - let height = match height { - Value::Float(height) => height, - _ => panic!("Entity has a height property that is not a float"), - }; - let tiles_x = entity_config - .properties - .custom_properties - .get("tiles_x") - .ok_or_else(|| anyhow::anyhow!("Entity has no tiles_x property"))?; - let tiles_x = match tiles_x { - Value::Int(tiles_x) => tiles_x, - _ => panic!("Entity has a tiles_x property that is not an int"), - }; - let tiles_z = entity_config - .properties - .custom_properties - .get("tiles_z") - .ok_or_else(|| anyhow::anyhow!("Entity has no tiles_z property"))?; - let tiles_z = match tiles_z { - Value::Int(tiles_z) => tiles_z, - _ => panic!("Entity has a tiles_z property that is not an int"), - }; - - let label_clone = entity_config.label.clone(); - let width_val = *width as f32; - let height_val = *height as f32; - let tiles_x_val = *tiles_x as u32; - let tiles_z_val = *tiles_z as u32; - - let plane = PlaneBuilder::new() - .with_size(width_val, height_val) - .with_tiles(tiles_x_val, tiles_z_val) - .build(graphics.clone(), PROTO_TEXTURE, Some(&label_clone)).await?; - - // let plane = { - // let label_clone = entity_config.label.clone(); - // let width_val = *width as f32; - // let height_val = *height as f32; - // let tiles_x_val = *tiles_x as u32; - // let tiles_z_val = *tiles_z as u32; - - // let (tx, mut rx) = tokio::sync::oneshot::channel(); - - // tokio::task::spawn_local(async move { - // let result = PlaneBuilder::new() - // .with_size(width_val, height_val) - // .with_tiles(tiles_x_val, tiles_z_val) - // .lazy_build(PROTO_TEXTURE, Some(&label_clone)).await; - // let _ = tx.send(result); - // }); - - // loop { - // match rx.try_recv() { - // Ok(result) => break result?.poke(graphics)?, - // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { - // std::thread::yield_now(); - // continue; - // } - // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { - // return Err(anyhow::anyhow!("Loading task was cancelled")); - // } - // } - // } - // }; - let transform = entity_config.transform; - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + ResourceReferenceType::Plane => { + let width = entity_config + .properties + .custom_properties + .get("width") + .ok_or_else(|| anyhow::anyhow!("Entity has no width property"))?; + let width = match width { + Value::Float(width) => width, + _ => panic!("Entity has a width property that is not a float"), + }; + let height = entity_config + .properties + .custom_properties + .get("height") + .ok_or_else(|| anyhow::anyhow!("Entity has no height property"))?; + let height = match height { + Value::Float(height) => height, + _ => panic!("Entity has a height property that is not a float"), + }; + let tiles_x = entity_config + .properties + .custom_properties + .get("tiles_x") + .ok_or_else(|| anyhow::anyhow!("Entity has no tiles_x property"))?; + let tiles_x = match tiles_x { + Value::Int(tiles_x) => tiles_x, + _ => panic!("Entity has a tiles_x property that is not an int"), + }; + let tiles_z = entity_config + .properties + .custom_properties + .get("tiles_z") + .ok_or_else(|| anyhow::anyhow!("Entity has no tiles_z property"))?; + let tiles_z = match tiles_z { + Value::Int(tiles_z) => tiles_z, + _ => panic!("Entity has a tiles_z property that is not an int"), }; - world.spawn((plane, transform, script, entity_config.properties.clone())); - } else { - world.spawn((plane, transform, entity_config.properties.clone())); - } - Ok(()) - } - ResourceReferenceType::None => { - Err(anyhow::anyhow!( + let label_clone = entity_config.label.clone(); + let width_val = *width as f32; + let height_val = *height as f32; + let tiles_x_val = *tiles_x as u32; + let tiles_z_val = *tiles_z as u32; + + let plane = PlaneBuilder::new() + .with_size(width_val, height_val) + .with_tiles(tiles_x_val, tiles_z_val) + .build(graphics.clone(), PROTO_TEXTURE, Some(&label_clone)) + .await?; + + let transform = entity_config.transform; + + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + { world.write().spawn((plane, transform, script, entity_config.properties.clone())); } + } else { + { world.write().spawn((plane, transform, entity_config.properties.clone())); } + } + + Ok(()) + } + ResourceReferenceType::None => Err(anyhow::anyhow!( "Entity has a resource reference of None, which cannot be loaded or referenced" - )) - } - }; + )), + }; - result?; - // processed_items += 1; - log::debug!("Loaded!"); + result?; + log::debug!("Loaded!"); + } } - for light_config in &self.lights { + let total = self.lights.len(); + + for (index, light_config) in self.lights.iter().enumerate() { log::debug!("Loading light: {}", light_config.label); + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingLight { index, name: light_config.label.clone(), total }); + } - let light = Light::new(graphics.clone(), &light_config.light_component, &light_config.transform, Some(&light_config.label)).await; - - // let light = { - // let light_comp_clone = light_config.light_component.clone(); - // let light_trans_clone = light_config.transform.clone(); - // let label_clone = light_config.label.clone(); - // let (tx, mut rx) = tokio::sync::oneshot::channel(); - - // tokio::task::spawn_local(async move { - // let result = Light::lazy_new( - // light_comp_clone, - // light_trans_clone, - // Some(&label_clone), - // ); - // let _ = tx.send(result); - // }); - - // loop { - // match rx.try_recv() { - // Ok(result) => break result.poke(graphics)?, - // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { - // std::thread::yield_now(); - // continue; - // } - // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { - // return Err(anyhow::anyhow!("Loading task was cancelled")); - // } - // } - // } - // }; - - world.spawn(( - light_config.light_component.clone(), - light_config.transform, - light, - ModelProperties::default(), - )); - - // processed_items += 1; + let light = Light::new( + graphics.clone(), + &light_config.light_component, + &light_config.transform, + Some(&light_config.label), + ) + .await; + { + world.write().spawn(( + light_config.light_component.clone(), + light_config.transform, + light, + ModelProperties::default(), + )); + } } - for camera_config in &self.cameras { + let total = self.cameras.len(); + for (index, camera_config) in self.cameras.iter().enumerate() { log::debug!( "Loading camera {} of type {:?}", camera_config.label, camera_config.camera_type ); + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingCamera { index, name: camera_config.label.clone(), total }); + } let camera = Camera::new( graphics.clone(), @@ -1040,64 +969,30 @@ impl SceneConfig { follow_target: target_label.clone(), offset: DVec3::from_array(*offset), }; - world.spawn((camera, component, follow_target)); + { world.write().spawn((camera, component, follow_target)); } } else { - world.spawn((camera, component)); + { world.write().spawn((camera, component)); } } - - // processed_items += 1; } - if world + 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 comp = LightComponent::directional(glam::DVec3::ONE, 1.0); let trans = Transform { - position: glam::DVec3::new(2.0, 4.0, 2.0), - ..Default::default() - }; + position: glam::DVec3::new(2.0, 4.0, 2.0), + ..Default::default() + }; let light = Light::new(graphics.clone(), &comp, &trans, Some("Default Light")).await; - // let light = { - // let light_comp_clone = LightComponent::directional(glam::DVec3::ONE, 1.0); - // let default_transform = Transform { - // position: glam::DVec3::new(2.0, 4.0, 2.0), - // ..Default::default() - // }; - // let (tx, mut rx) = tokio::sync::oneshot::channel(); - - // tokio::task::spawn_local(async move { - // let result = Light::lazy_new( - // light_comp_clone, - // default_transform, - // Some("Default Light"), - // ); - // let _ = tx.send(result); - // }); - - // loop { - // match rx.try_recv() { - // Ok(result) => break result.poke(graphics)?, - // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { - // std::thread::yield_now(); - // continue; - // } - // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { - // return Err(anyhow::anyhow!("Loading task was cancelled")); - // } - // } - // } - // }; - - world.spawn(( - comp, - trans, - light, - ModelProperties::default(), - )); + + world.write().spawn((comp, trans, light, ModelProperties::default())); } log::info!( @@ -1109,7 +1004,7 @@ impl SceneConfig { #[cfg(feature = "editor")] { // Editor mode - look for debug camera, create one if none exists - let debug_camera = world + let debug_camera = world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(entity, (_, component))| { @@ -1125,9 +1020,12 @@ impl SceneConfig { 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)); + let camera_entity = { world.write().spawn((camera, component)) }; Ok(camera_entity) } } @@ -1135,7 +1033,7 @@ impl SceneConfig { #[cfg(not(feature = "editor"))] { // Runtime mode - look for player camera, panic if none exists - let player_camera = world + let player_camera = world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(entity, (_, component))| { @@ -1203,3 +1101,27 @@ pub enum EditorTab { ModelEntityList, // right side, Viewport, // middle, } + +/// An enum that describes the status of loading the world. +/// +/// This is enum is used by [`SceneConfig::load_into_world`] heavily. This enum +/// is recommended to be used with an [`UnboundedSender`] +pub enum WorldLoadingStatus { + Idle, + LoadingEntity { + index: usize, + name: String, + total: usize, + }, + LoadingLight { + index: usize, + name: String, + total: usize, + }, + LoadingCamera { + index: usize, + name: String, + total: usize, + }, + Completed, +} @@ -33,6 +33,7 @@ pub enum ProjectProgress { Done, } +#[derive(Clone)] pub enum ViewportMode { None, CameraMove, @@ -36,6 +36,10 @@ walkdir.workspace = true zip.workspace = true tokio.workspace = true async-trait.workspace = true +reqwest.workspace = true +flate2.workspace = true +tar.workspace = true +futures-util.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -0,0 +1,604 @@ +use app_dirs2::{AppDataType, AppInfo, app_dir}; +use futures_util::StreamExt; +use std::path::PathBuf; +use std::process::Command; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tokio::sync::mpsc::UnboundedSender; + +const GLEAM_VERSION: &'static str = "1.12.0"; +const BUN_VERSION: &'static str = "1.2.22"; +const JAVY_VERSION: &'static str = "6.0.0"; + +pub const APP_INFO: AppInfo = AppInfo { + name: "Eucalyptus", + author: "4tkbytes", +}; + +#[derive(Clone)] +pub enum InstallStatus { + NotStarted, + InProgress { + tool: String, + step: String, + progress: f32, + }, + Success, + Failed(String), +} + +/// Compiles a gleam project into WASM through a pipeline. +pub struct GleamScriptCompiler { + #[allow(dead_code)] + project_location: PathBuf, +} + +#[allow(dead_code)] +impl GleamScriptCompiler { + #[allow(dead_code)] + pub fn new(project_location: &PathBuf) -> Self { + GleamScriptCompiler { + project_location: project_location.clone(), + } + } + + pub async fn evaluate(&self) -> anyhow::Result<()> { + Ok(()) + } + + pub async fn build( + &self, + sender: Option<UnboundedSender<InstallStatus>>, + ) -> anyhow::Result<()> { + Self::ensure_dependencies(sender).await?; + Ok(()) + } + + pub async fn ensure_dependencies( + sender: Option<UnboundedSender<InstallStatus>>, + ) -> anyhow::Result<()> { + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::NotStarted); + } + + println!("Checking dependencies..."); + + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::InProgress { + tool: "None".to_string(), + step: "Checking existing tools...".to_string(), + progress: 0.05, + }); + } + + let gleam_available = Self::check_tool_in_path("gleam").await; + let bun_available = Self::check_tool_in_path("bun").await; + let javy_available = Self::check_tool_in_path("javy").await; + + if gleam_available && bun_available && javy_available { + println!("All dependencies found in PATH"); + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::Success); + } + return Ok(()); + } + + if !(cfg!(target_os = "windows") || cfg!(target_os = "linux") || cfg!(target_os = "macos")) + { + anyhow::bail!("The operating system is not supported for building the Gleam project") + } + + let app_dir = app_dir(AppDataType::UserData, &APP_INFO, "") + .map_err(|e| anyhow::anyhow!("Failed to get app directory: {}", e))?; + + let tools_to_install: Vec<(&str, bool)> = vec![ + ("Gleam", gleam_available), + ("Bun", bun_available), + ("Javy", javy_available), + ]; + let total_to_install = tools_to_install.iter().filter(|(_, avail)| !avail).count(); + let mut installed_count = 0; + + if !gleam_available { + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::InProgress { + tool: "Gleam".to_string(), + step: "Downloading Gleam...".to_string(), + progress: 0.2 + (installed_count as f32 / total_to_install as f32) * 0.6, + }); + } + Self::download_gleam(&app_dir, sender.clone()).await?; + installed_count += 1; + } else { + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::InProgress { + tool: "Gleam".to_string(), + step: "Done!".to_string(), + progress: 1.0, + }); + } + installed_count += 1; + } + + if !bun_available { + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::InProgress { + tool: "Bun".to_string(), + step: "Downloading Bun...".to_string(), + progress: 0.2 + (installed_count as f32 / total_to_install as f32) * 0.6, + }); + } + Self::download_bun(&app_dir, sender.clone()).await?; + installed_count += 1; + } else { + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::InProgress { + tool: "Bun".to_string(), + step: "Done!".to_string(), + progress: 1.0, + }); + } + installed_count += 1; + } + + if !javy_available { + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::InProgress { + tool: "Javy".to_string(), + step: "Downloading Javy...".to_string(), + progress: 0.2 + (installed_count as f32 / total_to_install as f32) * 0.6, + }); + } + Self::download_javy(&app_dir, sender.clone()).await?; + installed_count += 1; + } else { + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::InProgress { + tool: "Javy".to_string(), + step: "Done!".to_string(), + progress: 1.0, + }); + } + installed_count += 1; + } + + if let Some(ref s) = sender.clone() { + let _ = s.send(InstallStatus::Success); + } + + println!( + "All {} dependencies installed successfully", + installed_count + ); + Ok(()) + } + + async fn check_tool_in_path(tool: &str) -> bool { + let cmd = if cfg!(target_os = "windows") { + Command::new("where").arg(tool).output() + } else { + Command::new("which").arg(tool).output() + }; + + match cmd { + Ok(output) => output.status.success(), + Err(_) => false, + } + } + + pub async fn download_gleam( + app_dir: &PathBuf, + sender: Option<UnboundedSender<InstallStatus>>, + ) -> anyhow::Result<()> { + let gleam_dir = app_dir + .join("dependencies") + .join("gleam") + .join(GLEAM_VERSION); + + if gleam_dir.exists() { + println!( + "Gleam v{} already cached at {}", + GLEAM_VERSION, + app_dir.display() + ); + return Ok(()); + } + + println!("Downloading Gleam v{}...", GLEAM_VERSION); + + let gleam_link = Self::get_gleam_download_url()?; + Self::download_and_extract(&gleam_link, &gleam_dir, "gleam", sender).await?; + + println!("Gleam v{} downloaded successfully", GLEAM_VERSION); + Ok(()) + } + + pub async fn download_bun( + app_dir: &PathBuf, + sender: Option<UnboundedSender<InstallStatus>>, + ) -> anyhow::Result<()> { + let bun_dir = app_dir.join("dependencies").join("bun").join(BUN_VERSION); + + if bun_dir.exists() { + println!( + "Bun v{} already cached at {}", + BUN_VERSION, + app_dir.display() + ); + return Ok(()); + } + + println!("Downloading Bun v{}...", BUN_VERSION); + + let bun_link = Self::get_bun_download_url()?; + Self::download_and_extract(&bun_link, &bun_dir, "bun", sender).await?; + + println!("Bun v{} downloaded successfully", BUN_VERSION); + Ok(()) + } + + pub async fn download_javy( + app_dir: &PathBuf, + sender: Option<UnboundedSender<InstallStatus>>, + ) -> anyhow::Result<()> { + let javy_dir = app_dir.join("dependencies").join("javy").join(JAVY_VERSION); + + if javy_dir.exists() { + println!( + "Javy v{} already cached at {}", + JAVY_VERSION, + app_dir.display() + ); + return Ok(()); + } + + println!("Downloading Javy v{}...", JAVY_VERSION); + + let javy_link = Self::get_javy_download_url()?; + Self::download_and_extract(&javy_link, &javy_dir, "javy", sender).await?; + + println!("Javy v{} downloaded successfully", JAVY_VERSION); + Ok(()) + } + + async fn download_and_extract( + url: &str, + target_dir: &PathBuf, + tool_name: &str, + sender: Option<UnboundedSender<InstallStatus>>, + ) -> anyhow::Result<()> { + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::InProgress { + step: format!("Creating directories for {}...", tool_name), + progress: 0.0, + tool: tool_name.to_string(), + }); + } + + fs::create_dir_all(target_dir).await?; + + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::InProgress { + step: format!("Downloading {}...", tool_name), + progress: 0.3, + tool: tool_name.to_string(), + }); + } + + let response = reqwest::get(url).await?; + let total_size = response.content_length().unwrap_or(0); + let mut downloaded = 0; + let mut stream = response.bytes_stream(); + + let temp_file = target_dir.join(format!("{}_download", tool_name)); + let mut file = fs::File::create(&temp_file).await?; + + while let Some(item) = stream.next().await { + let bytes = item?; + file.write_all(&bytes).await?; + downloaded += bytes.len() as u64; + + if let Some(ref s) = sender { + let progress = 0.3 + (downloaded as f32 / total_size as f32) * 0.4; + let _ = s.send(InstallStatus::InProgress { + step: format!("Downloading {}...", tool_name), + progress, + tool: tool_name.to_string(), + }); + } + } + + file.sync_all().await?; + drop(file); + + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::InProgress { + step: format!("Extracting {}...", tool_name), + progress: 0.7, + tool: tool_name.to_string(), + }); + } + + if url.ends_with(".zip") { + Self::extract_zip(&temp_file, target_dir).await?; + } else if url.ends_with(".tar.gz") { + Self::extract_tar_gz(&temp_file, target_dir).await?; + } else if url.ends_with(".gz") { + Self::extract_gz(&temp_file, target_dir, tool_name).await?; + } + + fs::remove_file(&temp_file).await?; + + if let Some(ref s) = sender { + let _ = s.send(InstallStatus::InProgress { + step: format!("{} installation complete", tool_name), + progress: 0.9, + tool: tool_name.to_string(), + }); + } + + Ok(()) + } + + async fn extract_zip(archive: &PathBuf, target_dir: &PathBuf) -> 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()); + + if file.is_dir() { + fs::create_dir_all(&outpath).await?; + } else { + if let Some(p) = outpath.parent() { + fs::create_dir_all(p).await?; + } + let mut outfile = fs::File::create(&outpath).await?; + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut file, &mut buffer)?; + outfile.write_all(&buffer).await?; + } + } + Ok(()) + } + + async fn extract_tar_gz(archive: &PathBuf, target_dir: &PathBuf) -> 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()?); + entry.unpack(path)?; + } + Ok(()) + } + + async fn extract_gz( + archive: &PathBuf, + target_dir: &PathBuf, + tool_name: &str, + ) -> anyhow::Result<()> { + let file = std::fs::File::open(archive)?; + let mut decoder = flate2::read::GzDecoder::new(file); + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut decoder, &mut buffer)?; + + let exe_name = if cfg!(target_os = "windows") { + format!("{}.exe", tool_name) + } else { + tool_name.to_string() + }; + + let output_path = target_dir.join(exe_name); + let mut output_file = fs::File::create(&output_path).await?; + output_file.write_all(&buffer).await?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = output_file.metadata().await?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&output_path, perms).await?; + } + + Ok(()) + } + + fn get_gleam_download_url() -> anyhow::Result<String> { + let gleam_link = { + #[cfg(target_os = "windows")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-pc-windows-msvc.zip", + GLEAM_VERSION, GLEAM_VERSION, arch, + ) + } + + #[cfg(target_os = "linux")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-unknown-linux-musl.tar.gz", + GLEAM_VERSION, GLEAM_VERSION, arch, + ) + } + + #[cfg(target_os = "macos")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-apple-darwin.tar.gz", + GLEAM_VERSION, GLEAM_VERSION, arch, + ) + } + }; + Ok(gleam_link) + } + + fn get_bun_download_url() -> anyhow::Result<String> { + let bun_link = { + #[cfg(target_os = "windows")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-windows-{}.zip", + BUN_VERSION, arch, + ) + } + + #[cfg(target_os = "linux")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-linux-{}.zip", + BUN_VERSION, arch, + ) + } + + #[cfg(target_os = "macos")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-darwin-{}.zip", + BUN_VERSION, arch, + ) + } + }; + Ok(bun_link) + } + + fn get_javy_download_url() -> anyhow::Result<String> { + let javy_link = { + #[cfg(target_os = "windows")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + anyhow::bail!( + "This arch is not available for prebuilt download. Please build this from source" + ); + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-windows-v{}.gz", + JAVY_VERSION, arch, JAVY_VERSION + ) + } + + #[cfg(target_os = "linux")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "arm" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-linux-v{}.gz", + JAVY_VERSION, arch, JAVY_VERSION + ) + } + + #[cfg(target_os = "macos")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "arm" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!( + "This architecture is not supported for building the gleam project" + ); + } + }; + format!( + "https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-macos-v{}.gz", + JAVY_VERSION, arch, JAVY_VERSION + ) + } + }; + Ok(javy_link) + } +} + +#[tokio::test] +async fn check_if_dependencies_install() { + GleamScriptCompiler::ensure_dependencies(None) + .await + .unwrap(); +} @@ -1,3 +1,5 @@ +pub mod gleam; + use std::{collections::HashMap, fs, path::PathBuf, process::Command}; use clap::ArgMatches; @@ -1,4 +1,4 @@ -use crate::editor::component::Component; +use crate::editor::component::InspectableComponent; use crate::editor::{EntityType, Signal, StaticallyKept, UndoableAction}; use dropbear_engine::camera::Camera; use egui::{CollapsingHeader, Ui}; @@ -6,7 +6,7 @@ use eucalyptus_core::camera::{CameraAction, CameraComponent, CameraFollowTarget, use hecs::Entity; use std::time::Instant; -impl Component for Camera { +impl InspectableComponent for Camera { fn inspect( &mut self, entity: &mut Entity, @@ -81,7 +81,7 @@ pub enum UndoableCameraAction { Type(Entity, CameraType), } -impl Component for CameraComponent { +impl InspectableComponent for CameraComponent { fn inspect( &mut self, _entity: &mut Entity, @@ -159,7 +159,7 @@ impl Component for CameraComponent { } } -impl Component for CameraFollowTarget { +impl InspectableComponent for CameraFollowTarget { fn inspect( &mut self, _entity: &mut Entity, @@ -1,249 +1,195 @@ //! Used to aid with debugging any issues with the editor. +use crate::build::gleam::GleamScriptCompiler; +use crate::build::gleam::InstallStatus; use crate::editor::Signal; -use egui::{Ui, Window, ProgressBar}; -use eucalyptus_core::scripting::build::GleamScriptCompiler; +use egui::ProgressBar; +use egui::Ui; +use egui::Window; use tokio::sync::mpsc; -#[derive(Debug, Clone)] -pub enum DependencyProgress { - Starting, - CheckingPath(String), - Downloading(String), - Extracting(String), - Completed(String), - Error(String), - Finished, -} - pub struct DependencyInstaller { - pub progress_receiver: Option<mpsc::UnboundedReceiver<DependencyProgress>>, - pub current_progress: Vec<String>, + pub progress_receiver: Option<mpsc::UnboundedReceiver<InstallStatus>>, pub is_installing: bool, - pub progress_value: f32, + + pub gleam_progress: f32, + pub bun_progress: f32, + pub javy_progress: f32, + + pub gleam_status: String, + pub bun_status: String, + pub javy_status: String, } impl Default for DependencyInstaller { fn default() -> Self { Self { progress_receiver: None, - current_progress: Vec::new(), is_installing: false, - progress_value: 0.0, + gleam_progress: 0.0, + bun_progress: 0.0, + javy_progress: 0.0, + gleam_status: "Not started".to_string(), + bun_status: "Not started".to_string(), + javy_status: "Not started".to_string(), } } } -/// Show a menu bar for debug. A new "Debug" menu button will show up on the editors menu bar. -pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal, dependency_installer: &mut DependencyInstaller) { - ui.menu_button("Debug", |ui_debug| { - if ui_debug.button("Panic").clicked() { - log::warn!("Panic caused on purpose from Menu Button Click"); - panic!("Testing out panicking with new panic module, this is a test") - } - - if ui_debug.button("Show Entities Loaded").clicked() { - log::info!("Show Entities Loaded under Debug Menu is clicked"); - *signal = Signal::LogEntities; - } - - ui_debug.add_enabled_ui(!dependency_installer.is_installing, |ui| { - if ui.button("Ensure dependencies").clicked() { - log::info!("Clicked ensure dependencies from debug menu"); - start_dependency_installation(dependency_installer); - } - }); - - if ui_debug.button("Evaluate script").clicked() { - log::info!("Evaluating script"); - } - }); -} - -pub(crate) fn show_dependency_progress_window( - ctx: &egui::Context, - dependency_installer: &mut DependencyInstaller, -) { - if let Some(receiver) = &mut dependency_installer.progress_receiver { - while let Ok(progress) = receiver.try_recv() { - match &progress { - DependencyProgress::Starting => { - dependency_installer.current_progress.clear(); - dependency_installer.current_progress.push("Starting dependency check...".to_string()); - dependency_installer.progress_value = 0.0; - } - DependencyProgress::CheckingPath(tool) => { - dependency_installer.current_progress.push(format!("Checking if {} is in PATH...", tool)); - dependency_installer.progress_value = 0.1; - } - DependencyProgress::Downloading(tool) => { - dependency_installer.current_progress.push(format!("Downloading {}...", tool)); - dependency_installer.progress_value += 0.25; - } - DependencyProgress::Extracting(tool) => { - dependency_installer.current_progress.push(format!("Extracting {}...", tool)); - dependency_installer.progress_value += 0.1; - } - DependencyProgress::Completed(tool) => { - dependency_installer.current_progress.push(format!("✓ {} ready", tool)); - dependency_installer.progress_value += 0.1; - } - DependencyProgress::Error(error) => { - dependency_installer.current_progress.push(format!("❌ Error: {}", error)); - } - DependencyProgress::Finished => { - dependency_installer.current_progress.push("✓ All dependencies ready!".to_string()); - dependency_installer.progress_value = 1.0; - dependency_installer.is_installing = false; - // dependency_installer.progress_receiver = None; +impl DependencyInstaller { + pub fn update_progress(&mut self) { + let mut local_prog_rec = false; + let mut update_tool_status: (bool, String, f32, String) = Default::default(); + if let Some(receiver) = &mut self.progress_receiver { + while let Ok(status) = receiver.try_recv() { + match status { + InstallStatus::NotStarted => { + self.is_installing = false; + } + InstallStatus::InProgress { + tool, + step, + progress, + } => { + self.is_installing = true; + update_tool_status = + (true, String::from(&tool), progress, String::from(&step)); + } + InstallStatus::Success => { + self.is_installing = false; + local_prog_rec = true; + self.gleam_status = "Complete".to_string(); + self.gleam_progress = 1.0; + self.bun_status = "Complete".to_string(); + self.bun_progress = 1.0; + self.javy_status = "Complete".to_string(); + self.javy_progress = 1.0; + } + InstallStatus::Failed(msg) => { + self.is_installing = false; + log::error!("Installation error: {}", msg); + self.gleam_status = format!("Error: {}", msg); + self.bun_status = format!("Error: {}", msg); + self.javy_status = format!("Error: {}", msg); + } } } } - } - - if dependency_installer.is_installing || !dependency_installer.current_progress.is_empty() { - Window::new("Dependency Installation") - .collapsible(false) - .resizable(false) - .show(ctx, |ui| { - ui.vertical(|ui| { - ui.add( - ProgressBar::new(dependency_installer.progress_value) - .show_percentage() - .animate(dependency_installer.is_installing) - ); - - ui.separator(); - - egui::ScrollArea::vertical() - .max_height(200.0) - .show(ui, |ui| { - for message in &dependency_installer.current_progress { - ui.label(message); - } - }); - - if !dependency_installer.is_installing && ui.button("Close").clicked() { - dependency_installer.current_progress.clear(); - } - }); - }); - } -} - -fn start_dependency_installation(dependency_installer: &mut DependencyInstaller) { - let (sender, receiver) = mpsc::unbounded_channel(); - dependency_installer.progress_receiver = Some(receiver); - dependency_installer.is_installing = true; - dependency_installer.progress_value = 0.0; - - tokio::spawn(async move { - let _ = sender.send(DependencyProgress::Starting); - - match ensure_dependencies_with_progress(sender.clone()).await { - Ok(_) => { - let _ = sender.send(DependencyProgress::Finished); - } - Err(e) => { - let _ = sender.send(DependencyProgress::Error(e.to_string())); - let _ = sender.send(DependencyProgress::Finished); - } + if local_prog_rec { + self.progress_receiver = None; } - }); -} - -async fn ensure_dependencies_with_progress( - progress_sender: mpsc::UnboundedSender<DependencyProgress>, -) -> anyhow::Result<()> { - let tools = vec![ - ("gleam", "Gleam"), - ("bun", "Bun"), - ("javy", "Javy"), - ]; - - let mut tools_to_download = Vec::new(); - - for (tool_cmd, tool_name) in &tools { - let _ = progress_sender.send(DependencyProgress::CheckingPath(tool_name.to_string())); - - let available = check_tool_in_path(tool_cmd).await; - if !available { - tools_to_download.push((*tool_cmd, *tool_name)); - } else { - let _ = progress_sender.send(DependencyProgress::Completed(format!("{} (found in PATH)", tool_name))); + if update_tool_status.0 { + self.update_tool_status( + update_tool_status.1.as_str(), + update_tool_status.2, + update_tool_status.3.as_str(), + ); } } - - if tools_to_download.is_empty() { - return Ok(()); - } - - let app_dir = app_dirs2::app_dir(app_dirs2::AppDataType::UserData, &eucalyptus_core::scripting::build::APP_INFO, "") - .map_err(|e| anyhow::anyhow!("Failed to get app directory: {}", e))?; - - for (tool_cmd, tool_name) in tools_to_download { - let _ = progress_sender.send(DependencyProgress::Downloading(tool_name.to_string())); - - match tool_cmd { + + fn update_tool_status(&mut self, tool: &str, progress: f32, status: &str) { + match tool.to_lowercase().as_str() { "gleam" => { - if let Err(e) = download_gleam_with_progress(&app_dir, progress_sender.clone()).await { - let _ = progress_sender.send(DependencyProgress::Error(format!("Failed to download Gleam: {}", e))); - return Err(e); - } + self.gleam_progress = progress; + self.gleam_status = status.to_string(); } "bun" => { - if let Err(e) = download_bun_with_progress(&app_dir, progress_sender.clone()).await { - let _ = progress_sender.send(DependencyProgress::Error(format!("Failed to download Bun: {}", e))); - return Err(e); - } + self.bun_progress = progress; + self.bun_status = status.to_string(); } "javy" => { - if let Err(e) = download_javy_with_progress(&app_dir, progress_sender.clone()).await { - let _ = progress_sender.send(DependencyProgress::Error(format!("Failed to download Javy: {}", e))); - return Err(e); - } + self.javy_progress = progress; + self.javy_status = status.to_string(); } _ => {} } - - let _ = progress_sender.send(DependencyProgress::Completed(tool_name.to_string())); } - - Ok(()) -} -async fn check_tool_in_path(tool: &str) -> bool { - let cmd = if cfg!(target_os = "windows") { - std::process::Command::new("where").arg(tool).output() - } else { - std::process::Command::new("which").arg(tool).output() - }; + pub fn show_installation_window(&mut self, ctx: &egui::Context) { + Window::new("Installing Dependencies") + .resizable(true) + .collapsible(false) + .default_width(400.0) + .show(ctx, |ui| { + ui.heading("Installing Dependencies"); + + ui.separator(); + + ui.label("Gleam:"); + ui.label(&self.gleam_status); + ui.add(ProgressBar::new(self.gleam_progress).show_percentage()); + ui.separator(); + + ui.label("Bun:"); + ui.label(&self.bun_status); + ui.add(ProgressBar::new(self.bun_progress).show_percentage()); + ui.separator(); + + ui.label("Javy:"); + ui.label(&self.javy_status); + ui.add(ProgressBar::new(self.javy_progress).show_percentage()); + ui.separator(); + + let overall_progress = + (self.gleam_progress + self.bun_progress + self.javy_progress) / 3.0; + ui.label("Overall Progress:"); + ui.add(ProgressBar::new(overall_progress).show_percentage()); - match cmd { - Ok(output) => output.status.success(), - Err(_) => false, + ui.separator(); + + if ui.button("Cancel").clicked() { + self.is_installing = false; + self.progress_receiver = None; + } + }); } } -async fn download_gleam_with_progress( - app_dir: &std::path::PathBuf, - progress_sender: mpsc::UnboundedSender<DependencyProgress>, -) -> anyhow::Result<()> { - let _ = progress_sender.send(DependencyProgress::Extracting("Gleam".to_string())); - GleamScriptCompiler::download_gleam(app_dir).await -} +pub(crate) fn show_menu_bar( + ui: &mut Ui, + signal: &mut Signal, + dependency_installer: &mut DependencyInstaller, +) { + ui.menu_button("Debug", |ui_debug| { + if ui_debug.button("Panic").clicked() { + log::warn!("Panic caused on purpose from Menu Button Click"); + panic!("Testing out panicking with new panic module, this is a test") + } -async fn download_bun_with_progress( - app_dir: &std::path::PathBuf, - progress_sender: mpsc::UnboundedSender<DependencyProgress>, -) -> anyhow::Result<()> { - let _ = progress_sender.send(DependencyProgress::Extracting("Bun".to_string())); - GleamScriptCompiler::download_bun(app_dir).await -} + if ui_debug.button("Show Entities Loaded").clicked() { + log::info!("Show Entities Loaded under Debug Menu is clicked"); + *signal = Signal::LogEntities; + } + + ui_debug.add_enabled_ui(!dependency_installer.is_installing, |ui| { + if ui.button("Ensure dependencies").clicked() { + log::info!("Clicked ensure dependencies from debug menu"); + + let (sender, receiver) = mpsc::unbounded_channel(); + dependency_installer.progress_receiver = Some(receiver); + dependency_installer.is_installing = true; + + dependency_installer.gleam_progress = 0.0; + dependency_installer.bun_progress = 0.0; + dependency_installer.javy_progress = 0.0; + dependency_installer.gleam_status = "Starting...".to_string(); + dependency_installer.bun_status = "Starting...".to_string(); + dependency_installer.javy_status = "Starting...".to_string(); + + tokio::task::spawn(async move { + match GleamScriptCompiler::ensure_dependencies(Some(sender.clone())).await { + Ok(_) => { + let _ = sender.send(InstallStatus::Success); + } + Err(e) => { + let _ = sender.send(InstallStatus::Failed(e.to_string())); + } + } + }); + } + }); -async fn download_javy_with_progress( - app_dir: &std::path::PathBuf, - progress_sender: mpsc::UnboundedSender<DependencyProgress>, -) -> anyhow::Result<()> { - let _ = progress_sender.send(DependencyProgress::Extracting("Javy".to_string())); - GleamScriptCompiler::download_javy(app_dir).await -} + if ui_debug.button("Evaluate script").clicked() { + log::info!("Evaluating script"); + } + }); +} @@ -12,7 +12,7 @@ use glam::Vec3; use hecs::Entity; use std::time::Instant; -pub trait Component { +pub trait InspectableComponent { fn inspect( &mut self, entity: &mut hecs::Entity, @@ -24,7 +24,7 @@ pub trait Component { ); } -impl Component for Transform { +impl InspectableComponent for Transform { fn inspect( &mut self, entity: &mut Entity, @@ -405,7 +405,7 @@ impl Component for Transform { } } -impl Component for ScriptComponent { +impl InspectableComponent for ScriptComponent { fn inspect( &mut self, _entity: &mut Entity, @@ -506,7 +506,7 @@ impl Component for ScriptComponent { } } -impl Component for AdoptedEntity { +impl InspectableComponent for AdoptedEntity { fn inspect( &mut self, entity: &mut Entity, @@ -552,7 +552,7 @@ impl Component for AdoptedEntity { } } -impl Component for Light { +impl InspectableComponent for Light { fn inspect( &mut self, entity: &mut Entity, @@ -597,7 +597,7 @@ impl Component for Light { } } -impl Component for LightComponent { +impl InspectableComponent for LightComponent { fn inspect( &mut self, _entity: &mut Entity, @@ -3,7 +3,7 @@ use crate::editor::ViewportMode; use std::{collections::HashSet, sync::LazyLock}; use crate::APP_INFO; -use crate::editor::component::Component; +use crate::editor::component::InspectableComponent; use crate::editor::scene::PENDING_SPAWNS; use dropbear_engine::{ entity::Transform, @@ -16,24 +16,21 @@ 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::{DMat4, DVec3}, -}; +use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode, math::DVec3}; pub struct EditorTabViewer<'a> { pub view: egui::TextureId, pub nodes: Vec<EntityNode>, pub tex_size: Extent3d, pub gizmo: &'a mut Gizmo, - pub world: &'a mut Arc<World>, + pub world: Arc<RwLock<World>>, pub selected_entity: &'a mut Option<hecs::Entity>, pub viewport_mode: &'a mut ViewportMode, pub undo_stack: &'a mut Vec<UndoableAction>, pub signal: &'a mut Signal, pub gizmo_mode: &'a mut EnumSet<GizmoMode>, pub editor_mode: &'a mut EditorState, - pub active_camera: &'a mut Option<hecs::Entity>, + pub active_camera: &'a mut Arc<Mutex<Option<hecs::Entity>>>, } impl<'a> EditorTabViewer<'a> { @@ -69,7 +66,6 @@ impl<'a> EditorTabViewer<'a> { } } - #[derive(Clone, Debug)] pub struct DraggedAsset { pub name: String, @@ -134,9 +130,10 @@ impl<'a> TabViewer for EditorTabViewer<'a> { match tab { EditorTab::Viewport => { + 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.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { + // if let Ok(mut q) = self.world.read().query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { // if let Some((camera, component, follow_target)) = q.get() { // } else { @@ -194,151 +191,156 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ); if image_response.clicked() { - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<( - &Camera, - &CameraComponent, - Option<&CameraFollowTarget>, - )>(*active_camera) + let active_cam = self.active_camera.lock(); + if let Some(active_camera) = *active_cam { { - if let Some((camera, _, _)) = q.get() { - if let Some(click_pos) = - ui.ctx().input(|i| i.pointer.interact_pos()) - { - let viewport_rect = image_response.rect; - let local_pos = click_pos - viewport_rect.min; + if let Ok(mut q) = self.world.read().query_one::<( + &Camera, + &CameraComponent, + Option<&CameraFollowTarget>, + )>(active_camera) + { + if let Some((camera, _, _)) = q.get() { + if let Some(click_pos) = + ui.ctx().input(|i| i.pointer.interact_pos()) + { + let viewport_rect = image_response.rect; + let local_pos = click_pos - viewport_rect.min; - let ndc_x = (2.0 * local_pos.x / viewport_rect.width()) - 1.0; - let ndc_y = 1.0 - (2.0 * local_pos.y / viewport_rect.height()); + let ndc_x = (2.0 * local_pos.x / viewport_rect.width()) - 1.0; + let ndc_y = 1.0 - (2.0 * local_pos.y / viewport_rect.height()); - let view_matrix = glam::DMat4::look_at_lh( - camera.eye, - camera.target, - camera.up, - ); + let view_matrix = glam::DMat4::look_at_lh( + camera.eye, + camera.target, + camera.up, + ); - let proj_matrix = glam::DMat4::perspective_lh( - camera.fov_y.to_radians(), - camera.aspect, - camera.znear, - camera.zfar, - ); + let proj_matrix = glam::DMat4::perspective_lh( + camera.fov_y.to_radians(), + camera.aspect, + camera.znear, + camera.zfar, + ); - if !view_matrix.is_finite() { - log::error!("Invalid view matrix"); - return; - } - if !proj_matrix.is_finite() { - log::error!("Invalid projection matrix"); - return; - } + if !view_matrix.is_finite() { + log::error!("Invalid view matrix"); + return; + } + if !proj_matrix.is_finite() { + log::error!("Invalid projection matrix"); + return; + } - let view_proj = proj_matrix * view_matrix; - let inv_view_proj = view_proj.inverse(); + let view_proj = proj_matrix * view_matrix; + let inv_view_proj = view_proj.inverse(); - if !inv_view_proj.is_finite() { - log::error!("Cannot invert view-projection matrix"); - return; - } + if !inv_view_proj.is_finite() { + log::error!("Cannot invert view-projection matrix"); + return; + } - let ray_start_ndc = - glam::DVec4::new(ndc_x as f64, ndc_y as f64, 0.0, 1.0); - let ray_end_ndc = - glam::DVec4::new(ndc_x as f64, ndc_y as f64, 1.0, 1.0); + let ray_start_ndc = + glam::DVec4::new(ndc_x as f64, ndc_y as f64, 0.0, 1.0); + let ray_end_ndc = + glam::DVec4::new(ndc_x as f64, ndc_y as f64, 1.0, 1.0); - let ray_start_world = inv_view_proj * ray_start_ndc; - let ray_end_world = inv_view_proj * ray_end_ndc; + let ray_start_world = inv_view_proj * ray_start_ndc; + let ray_end_world = inv_view_proj * ray_end_ndc; - if ray_start_world.w == 0.0 || ray_end_world.w == 0.0 { - log::error!("Invalid homogeneous coordinates"); - return; - } + if ray_start_world.w == 0.0 || ray_end_world.w == 0.0 { + log::error!("Invalid homogeneous coordinates"); + return; + } - let ray_start = ray_start_world.truncate() / ray_start_world.w; - let ray_end = ray_end_world.truncate() / ray_end_world.w; + let ray_start = ray_start_world.truncate() / ray_start_world.w; + let ray_end = ray_end_world.truncate() / ray_end_world.w; - if !ray_start.is_finite() || !ray_end.is_finite() { - log::error!( + if !ray_start.is_finite() || !ray_end.is_finite() { + log::error!( "Invalid ray points - start: {:?}, end: {:?}", ray_start, ray_end ); - return; - } + return; + } - let ray_direction = (ray_end - ray_start).normalize(); + let ray_direction = (ray_end - ray_start).normalize(); - if !ray_direction.is_finite() { - log::error!("Invalid ray direction: {:?}", ray_direction); - return; - } + if !ray_direction.is_finite() { + log::error!("Invalid ray direction: {:?}", ray_direction); + return; + } - let mut closest_distance = f64::INFINITY; - let mut selected_entity_id: Option<hecs::Entity> = None; - let mut entity_count = 0; + let (selected_entity_id, entity_count) = { + let mut closest_distance = f64::INFINITY; + let mut selected_entity_id: Option<hecs::Entity> = None; + let mut entity_count = 0; - for (entity_id, (transform, _)) in - self.world.query::<(&Transform, &AdoptedEntity)>().iter() - { - entity_count += 1; - let entity_pos = transform.position; - let sphere_radius = transform.scale.max_element() * 1.5; - - let to_sphere = entity_pos - ray_start; - let projection = to_sphere.dot(ray_direction); - - if projection > 0.0 { - let closest_point = - ray_start + ray_direction * projection; - let distance_to_sphere = - (closest_point - entity_pos).length(); - - if distance_to_sphere <= sphere_radius { - let discriminant = sphere_radius * sphere_radius - - distance_to_sphere * distance_to_sphere; - if discriminant >= 0.0 { - let intersection_distance = - projection - discriminant.sqrt(); - - if intersection_distance < closest_distance { - closest_distance = intersection_distance; - selected_entity_id = Some(entity_id); + { + let world_read_guard = self.world.read(); + for (entity_id, (transform, _)) in + world_read_guard.query::<(&Transform, &AdoptedEntity)>().iter() + { + entity_count += 1; + let entity_pos = transform.position; + let sphere_radius = transform.scale.max_element() * 1.5; + let to_sphere = entity_pos - ray_start; + let projection = to_sphere.dot(ray_direction); + if projection > 0.0 { + let closest_point = + ray_start + ray_direction * projection; + let distance_to_sphere = + (closest_point - entity_pos).length(); + if distance_to_sphere <= sphere_radius { + let discriminant = sphere_radius * sphere_radius + - distance_to_sphere * distance_to_sphere; + if discriminant >= 0.0 { + let intersection_distance = + projection - discriminant.sqrt(); + if intersection_distance < closest_distance { + closest_distance = intersection_distance; + selected_entity_id = Some(entity_id); + } + } + } } } } - } - } - log::debug!("Total entities checked: {}", entity_count); + (selected_entity_id, entity_count) + }; - if !matches!(self.editor_mode, EditorState::Playing) { - if let Some(entity_id) = selected_entity_id { - *self.selected_entity = Some(entity_id); - log::debug!("Selected entity: {:?}", entity_id); - } else { - // *self.selected_entity = None; - if entity_count == 0 { - log::debug!("No entities in world to select"); + log::debug!("Total entities checked: {}", entity_count); + + if !matches!(self.editor_mode, EditorState::Playing) { + if let Some(entity_id) = selected_entity_id { + *self.selected_entity = Some(entity_id); + log::debug!("Selected entity: {:?}", entity_id); } else { - log::debug!( + if entity_count == 0 { + log::debug!("No entities in world.read() to select"); + } else { + log::debug!( "No entity hit by ray (checked {} entities)", entity_count ); + } } } } - } - } else { - log_once::warn_once!( + } else { + log_once::warn_once!( "Unable to fetch the query result of camera: {:?}", active_camera ) - } - } else { - log_once::warn_once!( + } + } else { + log_once::warn_once!( "Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera ); + } } } else { log_once::warn_once!("No active camera found"); @@ -350,106 +352,82 @@ impl<'a> TabViewer for EditorTabViewer<'a> { // Note to self: fuck you >:( // Note to self: ok wow thats pretty rude im trying my best >﹏< // Note to self: finally holy shit i got it working - - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = - self.world - .query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>( - *active_camera, - ) - { - if let Some((camera, _, _)) = q.get() { - self.gizmo.update_config(GizmoConfig { - view_matrix: DMat4::look_at_lh( - DVec3::new( - camera.eye.x as f64, - camera.eye.y as f64, - camera.eye.z as f64, - ), - DVec3::new( - camera.target.x as f64, - camera.target.y as f64, - camera.target.z as f64, - ), - DVec3::new( - camera.up.x as f64, - camera.up.y as f64, - camera.up.z as f64, - ), - ) - .into(), - projection_matrix: DMat4::perspective_infinite_reverse_lh( - camera.fov_y as f64, - display_width as f64 / display_height as f64, - camera.znear as f64, - ) - .into(), - viewport: image_rect, - modes: *self.gizmo_mode, - orientation: transform_gizmo_egui::GizmoOrientation::Global, - snapping, - snap_distance: 1.0, - ..Default::default() - }); + 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) { + let val = q.get(); + if let Some(val) = val { + Some(val.0.clone()) + } else { + log::warn!("Queried camera but unable to get value"); + None + } } else { - log_once::warn_once!( - "Unable to fetch the query result of camera: {:?}", - active_camera - ) + log::warn!("Unable to query camera"); + None } - } else { - log_once::warn_once!( - "Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", - active_camera - ); + }; + + if let Some(camera) = camera_data { + self.gizmo.update_config(GizmoConfig { + view_matrix: camera.view_mat.into(), + projection_matrix: camera.proj_mat.into(), + viewport: image_rect, + modes: *self.gizmo_mode, + orientation: transform_gizmo_egui::GizmoOrientation::Global, + snapping, + snap_distance: 1.0, + ..Default::default() + }); } - } else { - log_once::warn_once!("No active camera found"); } - if !matches!(self.viewport_mode, ViewportMode::None) { if let Some(entity_id) = self.selected_entity { - if let Ok(transform) = - Arc::get_mut(&mut self.world).unwrap().query_one_mut::<&mut Transform>(*entity_id) { - let was_focused = cfg.is_focused; - cfg.is_focused = self.gizmo.is_focused(); + if let Ok(transform) = self.world.write() + .query_one_mut::<&mut Transform>(*entity_id) + { + let was_focused = cfg.is_focused; + cfg.is_focused = self.gizmo.is_focused(); - if cfg.is_focused && !was_focused { - cfg.old_pos = *transform; - } + if cfg.is_focused && !was_focused { + cfg.old_pos = *transform; + } - let gizmo_transform = - transform_gizmo_egui::math::Transform::from_scale_rotation_translation( - transform.scale, - transform.rotation, - transform.position, - ); + let gizmo_transform = + transform_gizmo_egui::math::Transform::from_scale_rotation_translation( + transform.scale, + transform.rotation, + transform.position, + ); - if let Some((_result, new_transforms)) = - self.gizmo.interact(ui, &[gizmo_transform]) - { - if 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 let Some((_result, new_transforms)) = + self.gizmo.interact(ui, &[gizmo_transform]) + { + if 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 - || cfg.old_pos.rotation != transform.rotation - || cfg.old_pos.scale != transform.scale; - - if transform_changed { - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Transform( - entity_id.clone(), - cfg.old_pos.clone(), - ), - ); - log::debug!("Pushed transform action to stack"); + if was_focused && !cfg.is_focused { + let transform_changed = cfg.old_pos.position != transform.position + || cfg.old_pos.rotation != transform.rotation + || cfg.old_pos.scale != transform.scale; + + if transform_changed { + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::Transform( + entity_id.clone(), + cfg.old_pos.clone(), + ), + ); + log::debug!("Pushed transform action to stack"); + } } } } @@ -644,18 +622,21 @@ impl<'a> TabViewer for EditorTabViewer<'a> { if is_d_clicked { if matches!(asset_type, ResourceType::Model) { let mut spawn_position = DVec3::default(); - if let Some(active_camera) = self.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; + { + 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 Some((camera, _, _)) = q.get() { + spawn_position = camera.eye; + } else { + log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + } } else { - log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); } } else { - log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + log_once::warn_once!("No active camera found"); } - } else { - log_once::warn_once!("No active camera found"); } let asset = DraggedAsset { @@ -714,207 +695,216 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } EditorTab::ResourceInspector => { if let Some(entity) = self.selected_entity { - if let Ok(( - e, - transform, - _props, - script, - camera, - camera_component, - follow_target, - )) = Arc::get_mut(&mut self.world).unwrap().query_one_mut::<( - &mut AdoptedEntity, - Option<&mut Transform>, - Option<&ModelProperties>, - Option<&mut ScriptComponent>, - Option<&mut Camera>, - Option<&mut CameraComponent>, - Option<&mut CameraFollowTarget>, - )>(*entity) { - e.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - if let Some(t) = transform { - t.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - e.label_mut(), - ); - } - - if let (Some(camera), Some(camera_component)) = (camera, camera_component) { - ui.separator(); - ui.label("Camera Components:"); - camera.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - camera_component.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut camera.label.clone(), - ); + 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::<( + &mut AdoptedEntity, + Option<&mut Transform>, + Option<&ModelProperties>, + Option<&mut ScriptComponent>, + Option<&mut Camera>, + Option<&mut CameraComponent>, + Option<&mut CameraFollowTarget>, + )>(*entity) + { + if let Some(( + e, + transform, + _props, + script, + camera, + camera_component, + follow_target, + )) = q.get() { + e.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut String::new(), + ); + if let Some(t) = transform { + t.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + e.label_mut(), + ); + } - if let Some(target) = follow_target { - target.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut camera.label.clone(), - ); - } - } + if let (Some(camera), Some(camera_component)) = (camera, camera_component) { + ui.separator(); + ui.label("Camera Components:"); + camera.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut String::new(), + ); + camera_component.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut camera.label.clone(), + ); - // if let Some(props) = _props { - // props.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); - // } - - if let Some(script) = script { - script.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - e.label_mut(), - ); - } + if let Some(target) = follow_target { + target.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut camera.label.clone(), + ); + } + } - 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() { - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Label(ent, orig, EntityType::Entity), + // if let Some(props) = _props { + // props.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); + // } + + if let Some(script) = script { + script.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + e.label_mut(), ); - log::debug!( + } + + 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() { + UndoableAction::push_to_undo( + &mut 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; + } } } - cfg.label_last_edit = None; + } else { + log_once::debug_once!("Unable to query entity inside resource inspector"); } - } - } else { - log_once::debug_once!("Unable to query entity inside resource inspector"); - } - if let Ok((light, transform, props)) = - Arc::get_mut(&mut self.world).unwrap() - .query_one_mut::<(&mut Light, &mut Transform, &mut LightComponent)>( - *entity, - ) - { - light.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - transform.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut light.label, - ); - props.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - 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() { - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Label(ent, orig, EntityType::Light), - ); - log::debug!( + if let Ok(mut q) = world + .query_one::<(&mut Light, &mut Transform, &mut LightComponent)>(*entity) + { + if let Some((light, transform, props)) = q.get() { + light.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut String::new(), + ); + transform.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut light.label, + ); + props.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + 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() { + UndoableAction::push_to_undo( + &mut 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; + } } } - cfg.label_last_edit = None; + } else { + log_once::debug_once!("Unable to query light inside resource inspector"); } - } - } else { - log_once::debug_once!("Unable to query light inside resource inspector"); - } - if let Ok((camera, camera_component, follow_target)) = - Arc::get_mut(&mut self.world).unwrap().query_one_mut::<( - &mut Camera, - &mut CameraComponent, - Option<&mut CameraFollowTarget>, - )>(*entity) - { - camera.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - camera_component.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut camera.label.clone(), - ); - if let Some(target) = follow_target { - target.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut camera.label.clone(), - ); - } + if let Ok(mut q) = + world.query_one::<( + &mut Camera, + &mut CameraComponent, + Option<&mut CameraFollowTarget>, + )>(*entity) + { + if let Some((camera, camera_component, follow_target)) = q.get() { + camera.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut String::new(), + ); + camera_component.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut camera.label.clone(), + ); + if let Some(target) = follow_target { + target.inspect( + entity, + &mut cfg, + ui, + self.undo_stack, + self.signal, + &mut camera.label.clone(), + ); + } - ui.separator(); - ui.label("Camera Controls:"); - if ui.button("Set as Active Camera").clicked() { - *self.active_camera = Some(*entity); - log::info!("Set camera '{}' as active", camera.label); - } + ui.separator(); + 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(); + log::info!("Set camera '{}' as active", camera.label); + } - if matches!(self.editor_mode, EditorState::Editing) { - if ui.button("Switch to This Camera").clicked() { - *self.active_camera = Some(*entity); - log::info!("Switched to camera '{}'", camera.label); + if matches!(self.editor_mode, EditorState::Editing) { + if ui.button("Switch to This Camera").clicked() { + *active_camera = Some(*entity).into(); + 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!"); @@ -1027,16 +1017,23 @@ impl<'a> TabViewer for EditorTabViewer<'a> { EditorTabMenuAction::AddComponent => { log::debug!("Add Component clicked"); if let Some(entity) = self.selected_entity { - if let Ok(..) = Arc::get_mut(&mut self.world).unwrap().query_one_mut::<&AdoptedEntity>(*entity) { - log::debug!("Queried selected entity, it is an entity"); - *self.signal = - Signal::AddComponent(*entity, EntityType::Entity); + if let Ok(..) = self.world.write() + .query_one_mut::<&AdoptedEntity>(*entity) + { + log::debug!("Queried selected entity, it is an entity"); + *self.signal = + Signal::AddComponent(*entity, EntityType::Entity); + } } - if let Ok(..) = Arc::get_mut(&mut self.world).unwrap().query_one_mut::<&Light>(*entity) { - log::debug!("Queried selected entity, it is a light"); - *self.signal = Signal::AddComponent(*entity, EntityType::Light); + { + if let Ok(..) = self.world.write() + .query_one_mut::<&Light>(*entity) + { + log::debug!("Queried selected entity, it is a light"); + *self.signal = Signal::AddComponent(*entity, EntityType::Light); + } } } else { warn!( @@ -1057,8 +1054,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> { EditorTabMenuAction::RemoveComponent => { log::debug!("Remove Component clicked"); if let Some(entity) = self.selected_entity { - if let Ok(script) = - Arc::get_mut(&mut self.world).unwrap().query_one_mut::<&ScriptComponent>(*entity) + if let Ok(script) = self.world.write() + .query_one_mut::<&ScriptComponent>(*entity) { log::debug!( "Queried selected entity, it has a script component" @@ -114,8 +114,8 @@ impl Keyboard for Editor { KeyCode::KeyC => { if ctrl_pressed && !is_playing { if let Some(entity) = &self.selected_entity { - let query = self - .world + let world = self.world.read(); + let query = world .query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); if let Ok(mut q) = query { if let Some((e, t, props)) = q.get() { @@ -240,14 +240,20 @@ impl Mouse for Editor { // this impl doesn't have the mouse being recentered back to the center (to be fixed), this works as a usable alternative fn mouse_move(&mut self, position: PhysicalPosition<f64>) { if (self.is_viewport_focused && matches!(self.viewport_mode, ViewportMode::CameraMove)) - || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked) + || (matches!(self.editor_state, EditorState::Playing) + && !self.input_state.is_cursor_locked) { if let Some(last_pos) = self.input_state.last_mouse_pos { let dx = position.x - last_pos.0; let dy = position.y - last_pos.1; - - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { + + if let Some(active_camera) = *self.active_camera.lock() { + if let Ok(mut q) = self.world.read().query_one::<( + &mut Camera, + &CameraComponent, + Option<&CameraFollowTarget>, + )>(active_camera) + { if let Some((camera, _, _)) = q.get() { camera.track_mouse_delta(dx, dy); } @@ -256,7 +262,7 @@ impl Mouse for Editor { } self.input_state.last_mouse_pos = Some((position.x, position.y)); } - + self.input_state.mouse_pos = (position.x, position.y); } @@ -264,33 +270,41 @@ impl Mouse for Editor { // for some reason, this doesn't work on linux but works on windows (not tested on anywhere else) fn mouse_move(&mut self, position: PhysicalPosition<f64>) { if (self.is_viewport_focused && matches!(self.viewport_mode, ViewportMode::CameraMove)) - || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked) + || (matches!(self.editor_state, EditorState::Playing) + && !self.input_state.is_cursor_locked) { if let Some(window) = &self.window { let size = window.inner_size(); - let center = PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0); - - let distance_from_center = ((position.x - center.x).powi(2) + (position.y - center.y).powi(2)).sqrt(); - + let center = + PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0); + + let distance_from_center = + ((position.x - center.x).powi(2) + (position.y - center.y).powi(2)).sqrt(); + if distance_from_center > 5.0 { let dx = position.x - center.x; let dy = position.y - center.y; - + if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { + if let Ok(mut q) = self.world.query_one::<( + &mut Camera, + &CameraComponent, + Option<&CameraFollowTarget>, + )>(active_camera) + { if let Some((camera, _, _)) = q.get() { camera.track_mouse_delta(dx, dy); } } } - + let _ = window.set_cursor_position(center); } - + window.set_cursor_visible(false); } } - + self.input_state.mouse_pos = (position.x, position.y); } @@ -12,9 +12,9 @@ use std::{ time::{Duration, Instant}, }; -use crate::{build::build, debug::DependencyInstaller}; use crate::camera::UndoableCameraAction; use crate::debug; +use crate::{build::build, debug::DependencyInstaller}; use dropbear_engine::{ camera::Camera, entity::{AdoptedEntity, Transform}, @@ -26,7 +26,7 @@ use egui::{self, Context}; use egui_dock_fork::{DockArea, DockState, NodeIndex, Style}; use eucalyptus_core::{camera::{ CameraAction, CameraComponent, CameraFollowTarget, CameraType, DebugCamera, -}}; +}, states::WorldLoadingStatus}; use eucalyptus_core::input::InputState; use eucalyptus_core::scripting::{ScriptAction, ScriptManager}; use eucalyptus_core::states::{ @@ -37,22 +37,24 @@ use eucalyptus_core::utils::ViewportMode; use eucalyptus_core::{fatal, info, states, success, warn}; use hecs::World; use log; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode}; use wgpu::{Color, Extent3d, RenderPipeline}; use winit::{keyboard::KeyCode, window::Window}; +use dropbear_engine::graphics::{RenderContext, Shader}; pub struct Editor { scene_command: SceneCommand, - world: Arc<World>, - dock_state: DockState<EditorTab>, + world: Arc<RwLock<World>>, + dock_state: Arc<Mutex<DockState<EditorTab>>>, texture_id: Option<egui::TextureId>, size: Extent3d, render_pipeline: Option<RenderPipeline>, light_manager: LightManager, color: Color, - active_camera: Option<hecs::Entity>, + active_camera: Arc<Mutex<Option<hecs::Entity>>>, is_viewport_focused: bool, // is_cursor_locked: bool, @@ -60,7 +62,7 @@ pub struct Editor { show_new_project: bool, project_name: String, - project_path: Option<PathBuf>, + project_path: Arc<Mutex<Option<PathBuf>>>, pending_scene_switch: bool, gizmo: Gizmo, @@ -77,10 +79,21 @@ pub struct Editor { script_manager: ScriptManager, play_mode_backup: Option<PlayModeBackup>, + /// State of the input input_state: InputState, // channels + /// A channel for installing dependencies. dep_installer: DependencyInstaller, + /// A threadsafe Unbounded Receiver, typically used for checking the status of the world loading + progress_tx: Option<UnboundedReceiver<WorldLoadingStatus>>, + /// Unused: A threadsafe Unbounded Sender + _progress_rx: Option<UnboundedSender<WorldLoadingStatus>>, + /// Used to check if the world has been loaded in + is_world_loaded: IsWorldLoadedYet, + /// 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, } impl Editor { @@ -96,7 +109,7 @@ impl Editor { let [_old, _] = surface.split_below(right, 0.5, vec![EditorTab::AssetViewer]); // this shit doesnt work :( - // nvm it works (sorta) + // nvm it works std::thread::spawn(move || { loop { std::thread::sleep(Duration::from_secs(1)); @@ -112,13 +125,16 @@ impl Editor { log::error!("{:#?}", t.backtrace()); } } - panic!("Fatal: {} deadlocks detected, unable to continue on normal process", deadlocks.len()); + panic!( + "Fatal: {} deadlocks detected, unable to continue on normal process", + deadlocks.len() + ); } }); Self { scene_command: SceneCommand::None, - dock_state, + dock_state: Arc::new(Mutex::new(dock_state)), texture_id: None, size: Extent3d::default(), render_pipeline: None, @@ -126,10 +142,10 @@ impl Editor { is_viewport_focused: false, // is_cursor_locked: false, window: None, - world: Arc::new(World::new()), + world: Arc::new(RwLock::new(World::new())), show_new_project: false, project_name: String::new(), - project_path: None, + project_path: Arc::new(Mutex::new(None)), pending_scene_switch: false, gizmo: Gizmo::default(), selected_entity: None, @@ -142,10 +158,12 @@ impl Editor { play_mode_backup: None, input_state: InputState::new(), light_manager: LightManager::new(), - active_camera: None, - dep_installer: DependencyInstaller::default() - // ..Default::default() - // note to self: DO NOT USE ..DEFAULT::DEFAULT(), IT WILL CAUSE OVERFLOW + active_camera: Arc::new(Mutex::new(None)), + dep_installer: DependencyInstaller::default(), + _progress_rx: None, + progress_tx: None, + is_world_loaded: IsWorldLoadedYet::new(), + current_state: WorldLoadingStatus::Idle, } } @@ -181,7 +199,7 @@ impl Editor { scene.cameras.clear(); for (id, (adopted, transform, properties, script)) in self - .world + .world.read() .query::<( &AdoptedEntity, Option<&Transform>, @@ -206,7 +224,7 @@ impl Editor { } for (id, (light_component, transform, light)) in self - .world + .world.read() .query::<( &dropbear_engine::lighting::LightComponent, &Transform, @@ -227,7 +245,7 @@ impl Editor { } for (_id, (camera, component, follow_target)) in self - .world + .world.read() .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() .iter() { @@ -250,7 +268,8 @@ impl Editor { { let mut config = PROJECT.write(); - config.dock_layout = Some(self.dock_state.clone()); + let dock_state = self.dock_state.lock(); + config.dock_layout = Some(dock_state.clone()); } { @@ -269,14 +288,92 @@ impl Editor { Ok(()) } - pub async fn load_project_config(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { + /// The window when loading a project or a scene or anything that uses [`WorldLoadingStatus`] + fn show_project_loading_window(&mut self, ctx: &egui::Context) { + if let Some(ref mut rx) = self.progress_tx { + match rx.try_recv() { + Ok(status) => { + match status { + WorldLoadingStatus::LoadingEntity { index, name, total } => { + log::debug!("Loading entity: {} ({}/{})", name, index + 1, total); + self.current_state = WorldLoadingStatus::LoadingEntity { index, name, total }; + } + WorldLoadingStatus::LoadingLight { index, name, total } => { + log::debug!("Loading light: {} ({}/{})", name, index + 1, total); + self.current_state = WorldLoadingStatus::LoadingLight { index, name, total }; + } + WorldLoadingStatus::LoadingCamera { index, name, total } => { + log::debug!("Loading camera: {} ({}/{})", name, index + 1, total); + self.current_state = WorldLoadingStatus::LoadingCamera { index, name, total }; + } + WorldLoadingStatus::Completed => { + log::debug!("Received WorldLoadingStatus::Completed - project loading finished"); + self.is_world_loaded.mark_project_loaded(); + self.current_state = WorldLoadingStatus::Completed; + self.progress_tx = None; + println!("Returning back"); + return; + } + WorldLoadingStatus::Idle => { + log::debug!("Project loading is idle"); + } + } + } + Err(_) => { + // log::debug!("Unable to receive the progress: {}", e); + } + } + } else { + log::debug!("No progress receiver available"); + } + + egui::Window::new("Loading Project") + .collapsible(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) + .fixed_size([300.0, 100.0]) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Loading..."); + }); + // ui.add_space(5.0); + // ui.add(egui::ProgressBar::new(progress).text(format!("{:.0}%", progress * 100.0))); + match &self.current_state { + WorldLoadingStatus::Idle => {ui.label("Initialising...");} + WorldLoadingStatus::LoadingEntity { name, ..} => {ui.label(format!("Loading entity: {}", name));} + WorldLoadingStatus::LoadingLight { name, .. } => {ui.label(format!("Loading light: {}", name));} + WorldLoadingStatus::LoadingCamera { name, .. } => {ui.label(format!("Loading camera: {}", name));} + WorldLoadingStatus::Completed => {ui.label("Done!");} + } + }); + }); + } + + /// Loads the project config. + /// + /// It uses an unbounded sender to send messages back to the receiver so it can + /// be used within threads. + pub async fn load_project_config( + // &mut self, + graphics: Arc<SharedGraphicsContext>, + sender: Option<UnboundedSender<WorldLoadingStatus>>, + world: Arc<RwLock<World>>, + active_camera: Arc<Mutex<Option<hecs::Entity>>>, + project_path: Arc<Mutex<Option<PathBuf>>>, + dock_state: Arc<Mutex<DockState<EditorTab>>>, + ) -> anyhow::Result<()> { { let config = PROJECT.read(); - - self.project_path = Some(config.project_path.clone()); + let mut path = project_path.lock(); + *path = Some(config.project_path.clone()); if let Some(layout) = &config.dock_layout { - self.dock_state = layout.clone(); + let mut dock = dock_state.lock(); + let layout = layout.clone(); + *dock = layout.clone(); } } @@ -287,8 +384,11 @@ impl Editor { { if let Some(first_scene) = first_scene_opt { - let cam = first_scene.load_into_world(Arc::get_mut(&mut self.world).unwrap(), graphics).await?; - self.active_camera = Some(cam); + let cam = first_scene + .load_into_world(world, graphics, sender.clone()) + .await?; + let mut a_c = active_camera.lock(); + *a_c = Some(cam).into(); log::info!( "Successfully loaded scene with {} entities and {} camera configs", @@ -296,8 +396,7 @@ impl Editor { first_scene.cameras.len(), ); } else { - let existing_debug_camera = self - .world + let existing_debug_camera = world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(entity, (_, component))| { @@ -310,23 +409,31 @@ impl Editor { if let Some(camera_entity) = existing_debug_camera { log::info!("Using existing debug camera"); - self.active_camera = Some(camera_entity); + let mut a_c = active_camera.lock(); + *a_c = Some(camera_entity).into(); } else { log::info!("No scenes found, creating default debug camera"); let debug_camera = Camera::predetermined(graphics, Some("Debug Camera")); let component = DebugCamera::new(); - let e = Arc::get_mut(&mut self.world).unwrap().spawn((debug_camera, component)); - self.active_camera = Some(e); + { + let e = world.write() + .spawn((debug_camera, component)); + let mut a_c = active_camera.lock(); + *a_c = Some(e).into(); + } } } } + if let Some(ref s) = sender.clone() { + let _ = s.send(WorldLoadingStatus::Completed); + } Ok(()) } - pub fn show_ui(&mut self, ctx: &Context) { + pub async 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| { @@ -388,7 +495,8 @@ impl Editor { ui.menu_button("Edit", |ui| { if ui.button("Copy").clicked() { if let Some(entity) = &self.selected_entity { - let query = self.world.query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); + let world = self.world.read(); + let query = 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 { @@ -411,6 +519,7 @@ impl Editor { } else { warn!("Unable to copy entity: None selected"); } + } if ui.button("Paste").clicked() { @@ -431,19 +540,20 @@ impl Editor { }); ui.menu_button("Window", |ui_window| { + let mut dock_state = self.dock_state.lock(); if ui_window.button("Open Asset Viewer").clicked() { - self.dock_state.push_to_focused_leaf(EditorTab::AssetViewer); + dock_state.push_to_focused_leaf(EditorTab::AssetViewer); } if ui_window.button("Open Resource Inspector").clicked() { - self.dock_state + dock_state .push_to_focused_leaf(EditorTab::ResourceInspector); } if ui_window.button("Open Entity List").clicked() { - self.dock_state + dock_state .push_to_focused_leaf(EditorTab::ModelEntityList); } if ui_window.button("Open Viewport").clicked() { - self.dock_state.push_to_focused_leaf(EditorTab::Viewport); + dock_state.push_to_focused_leaf(EditorTab::Viewport); } }); { @@ -456,16 +566,18 @@ impl Editor { }); egui::CentralPanel::default().show(&ctx, |ui| { - DockArea::new(&mut self.dock_state) + let mut dock_state = self.dock_state.lock(); + + DockArea::new(&mut 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), + nodes: EntityNode::from_world(&self.world.clone().read()), gizmo: &mut self.gizmo, tex_size: self.size, - world: &mut self.world, + world: self.world.clone(), selected_entity: &mut self.selected_entity, viewport_mode: &mut self.viewport_mode, undo_stack: &mut self.undo_stack, @@ -477,11 +589,12 @@ impl Editor { ); }); + let mut project_path = self.project_path.lock(); crate::utils::show_new_project_window( ctx, &mut self.show_new_project, &mut self.project_name, - &mut self.project_path, + &mut project_path, |name, path| { crate::utils::start_project_creation(name.to_string(), Some(path.clone())); self.pending_scene_switch = true; @@ -494,9 +607,139 @@ impl Editor { } } + /// Restores transform components back to its original state before PlayMode. + pub fn restore(&mut self) -> anyhow::Result<()> { + if let Some(backup) = &self.play_mode_backup { + 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) { + *transform = *original_transform; + } + + if let Ok(mut properties) = self.world.read().get::<&mut ModelProperties>(*entity_id) { + *properties = original_properties.clone(); + } + + let has_script = self.world.read().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) + { + *script = original.clone(); + } + } + (true, None) => { + { + let _ = self.world.write() + .remove_one::<ScriptComponent>(*entity_id); + } + } + (false, Some(original)) => { + { + let _ = self.world.write() + .insert_one(*entity_id, original.clone()); + } + } + (false, None) => { + } + } + } + + 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) { + *camera = original_camera.clone(); + } + + if let Ok(mut component) = self.world.read().get::<&mut CameraComponent>(*entity_id) { + *component = original_component.clone(); + } + + let has_follow_target = self.world.read().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) + { + *follow_target = original.clone(); + } + } + (true, None) => { + { + let _ = self.world.write() + .remove_one::<CameraFollowTarget>(*entity_id); + } + } + (false, Some(original)) => { + { + let _ = self.world.write() + .insert_one(*entity_id, original.clone()); + } + } + (false, None) => { + // No change needed + } + } + } + + log::info!("Restored scene from play mode backup"); + + self.play_mode_backup = None; + Ok(()) + } else { + Err(anyhow::anyhow!("No play mode backup found to restore")) + } + } + + pub fn create_backup(&mut self) -> anyhow::Result<()> { + let mut entities = Vec::new(); + + for (entity_id, (_, transform, properties)) in self + .world.read() + .query::<(&AdoptedEntity, &Transform, &ModelProperties)>() + .iter() + { + let script = self + .world.read() + .query_one::<&ScriptComponent>(entity_id) + .ok() + .and_then(|mut s| s.get().map(|script| script.clone())); + 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() + .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() + .iter() + { + camera_data.push(( + entity_id, + camera.clone(), + component.clone(), + follow_target.cloned(), + )); + } + + self.play_mode_backup = Some(PlayModeBackup { + entities, + camera_data, + }); + + log::info!( + "Created play mode backup with {} entities and {} cameras", + self.play_mode_backup.as_ref().unwrap().entities.len(), + self.play_mode_backup.as_ref().unwrap().camera_data.len() + ); + Ok(()) + } + pub fn switch_to_debug_camera(&mut self) { let debug_camera = self - .world + .world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(e, (_cam, comp))| { @@ -508,7 +751,8 @@ impl Editor { }); if let Some(camera_entity) = debug_camera { - self.active_camera = Some(camera_entity); + let mut active_camera = self.active_camera.lock(); + *active_camera = Some(camera_entity); info!("Switched to debug camera"); } else { warn!("No debug camera found in the world"); @@ -517,7 +761,7 @@ impl Editor { pub fn switch_to_player_camera(&mut self) { let player_camera = self - .world + .world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(e, (_cam, comp))| { @@ -529,7 +773,8 @@ impl Editor { }); if let Some(camera_entity) = player_camera { - self.active_camera = Some(camera_entity); + let mut active_camera = self.active_camera.lock(); + *active_camera = Some(camera_entity); info!("Switched to player camera"); } else { warn!("No player camera found in the world"); @@ -537,9 +782,10 @@ impl Editor { } pub fn is_using_debug_camera(&self) -> bool { - if let Some(active_camera_entity) = self.active_camera { + let active_camera = self.active_camera.lock(); + if let Some(active_camera_entity) = *active_camera { if let Ok(mut query) = self - .world + .world.read() .query_one::<&CameraComponent>(active_camera_entity) { if let Some(component) = query.get() { @@ -549,6 +795,63 @@ impl Editor { } false } + + /// Loads all the wgpu resources such as renderer. + /// + /// **Note**: To be ran AFTER [`Editor::load_project_config`] + pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) { + let shader = Shader::new( + graphics.shared.clone(), + include_str!("../../../resources/shaders/shader.wgsl"), + Some("viewport_shader"), + ); + + self.light_manager.create_light_array_resources(graphics.shared.clone()); + + if let Some(active_camera) = *self.active_camera.lock() { + if let Ok(mut q) = self + .world.read() + .query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>( + active_camera, + ) + { + if let Some((camera, _component, _follow_target)) = q.get() { + let pipeline = graphics.create_render_pipline( + &shader, + vec![ + &graphics.shared.texture_bind_layout.clone(), + camera.layout(), + self.light_manager.layout(), + ], + None, + ); + self.render_pipeline = Some(pipeline); + + self.light_manager.create_render_pipeline( + graphics.shared.clone(), + include_str!("../../../resources/shaders/light.wgsl"), + camera, + Some("Light Pipeline"), + ); + } else { + log_once::warn_once!( + "Unable to fetch the query result of camera: {:?}", + active_camera + ) + } + } else { + log_once::warn_once!( + "Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", + active_camera + ); + } + } else { + log_once::warn_once!("No active camera found"); + } + + self.window = Some(graphics.shared.window.clone()); + self.is_world_loaded.mark_rendering_loaded(); + } } pub static LOGGED: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(|| Mutex::new(HashSet::new())); @@ -643,9 +946,13 @@ fn show_entity_tree( /// Describes an action that is undoable #[derive(Debug)] pub enum UndoableAction { + /// A change in transform. The entity + the old transform. Undoing will revert the transform Transform(hecs::Entity, Transform), + /// 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), #[allow(dead_code)] CameraAction(UndoableCameraAction), @@ -665,105 +972,117 @@ impl UndoableAction { // log::debug!("Undo Stack contents: {:#?}", undo_stack); } - pub fn undo(&self, world: &mut hecs::World) -> anyhow::Result<()> { + pub fn undo(&self, world: Arc<RwLock<World>>) -> anyhow::Result<()> { match self { UndoableAction::Transform(entity, transform) => { - 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"); - Ok(()) + { + if let Ok(mut q) = world.write().query_one::<&mut Transform>(*entity) { + if let Some(e_t) = q.get() { + *e_t = *transform; + log::debug!("Reverted transform"); + Ok(()) + } else { + Err(anyhow::anyhow!("Unable to query the entity")) + } } else { - Err(anyhow::anyhow!("Unable to query the entity")) + Err(anyhow::anyhow!("Could not find an entity to query")) } - } else { - Err(anyhow::anyhow!("Could not find an entity to query")) } } UndoableAction::Spawn(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)) + { + 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)) + } } } UndoableAction::Label(entity, original_label, entity_type) => match entity_type { EntityType::Entity => { - if let Ok(mut q) = world.query_one::<&mut AdoptedEntity>(*entity) { - if let Some(adopted) = q.get() { - adopted.model_mut().label = original_label.clone(); - log::debug!( + { + if let Ok(mut q) = world.write().query_one::<&mut AdoptedEntity>(*entity) { + if let Some(adopted) = q.get() { + adopted.model_mut().label = original_label.clone(); + log::debug!( "Reverted label for entity {:?} to '{}'", entity, original_label ); - Ok(()) - } else { - Err(anyhow::anyhow!( + Ok(()) + } else { + Err(anyhow::anyhow!( "Unable to query the entity for label revert" )) - } - } else { - Err(anyhow::anyhow!( + } + } else { + Err(anyhow::anyhow!( "Could not find an entity to query for label revert" )) + } } } EntityType::Light => { - if let Ok(mut q) = world.query_one::<&mut Light>(*entity) { - if let Some(adopted) = q.get() { - adopted.label = original_label.clone(); - log::debug!( + { + if let Ok(mut q) = world.write().query_one::<&mut Light>(*entity) { + if let Some(adopted) = q.get() { + adopted.label = original_label.clone(); + log::debug!( "Reverted label for light {:?} to '{}'", entity, original_label ); - Ok(()) - } else { - Err(anyhow::anyhow!( + Ok(()) + } else { + Err(anyhow::anyhow!( "Unable to query the light for label revert" )) - } - } else { - Err(anyhow::anyhow!( + } + } else { + Err(anyhow::anyhow!( "Could not find a light to query for label revert" )) + } } } EntityType::Camera => { - if let Ok(mut q) = world.query_one::<&mut Camera>(*entity) { - if let Some(adopted) = q.get() { - adopted.label = original_label.clone(); - log::debug!( + { + if let Ok(mut q) = world.write().query_one::<&mut Camera>(*entity) { + if let Some(adopted) = q.get() { + adopted.label = original_label.clone(); + log::debug!( "Reverted label for camera {:?} to '{}'", entity, original_label ); - Ok(()) - } else { - Err(anyhow::anyhow!( + Ok(()) + } else { + Err(anyhow::anyhow!( "Unable to query the camera for label revert" )) - } - } else { - Err(anyhow::anyhow!( + } + } else { + Err(anyhow::anyhow!( "Could not find a camera to query for label revert" )) + } } } }, UndoableAction::RemoveComponent(entity, c_type) => { match c_type { ComponentType::Script(component) => { - world.insert_one(*entity, component.clone())?; + { world.write().insert_one(*entity, component.clone())?; } } ComponentType::Camera(camera, component, follow) => { if let Some(f) = follow { - world - .insert(*entity, (camera.clone(), component.clone(), f.clone()))?; + { + world.write() + .insert(*entity, (camera.clone(), component.clone(), f.clone()))?; + } } else { - world.insert(*entity, (camera.clone(), component.clone()))?; + { world.write().insert(*entity, (camera.clone(), component.clone()))?; } } } } @@ -772,35 +1091,43 @@ impl UndoableAction { UndoableAction::CameraAction(action) => { match action { UndoableCameraAction::Speed(entity, speed) => { - if let Ok((cam, comp)) = - world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.speed = *speed; - comp.update(cam); + if let Ok((cam, comp)) = + world.write().query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) + { + comp.speed = *speed; + comp.update(cam); + } } } UndoableCameraAction::Sensitivity(entity, sensitivity) => { - if let Ok((cam, comp)) = - world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.sensitivity = *sensitivity; - comp.update(cam); + if let Ok((cam, comp)) = + world.write().query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) + { + comp.sensitivity = *sensitivity; + comp.update(cam); + } } } UndoableCameraAction::FOV(entity, fov) => { - if let Ok((cam, comp)) = - world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.fov_y = *fov; - comp.update(cam); + if let Ok((cam, comp)) = + world.write().query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) + { + comp.fov_y = *fov; + comp.update(cam); + } } } UndoableCameraAction::Type(entity, camera_type) => { - if let Ok((cam, comp)) = - world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.camera_type = *camera_type; - comp.update(cam); + if let Ok((cam, comp)) = + world.write().query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) + { + comp.camera_type = *camera_type; + comp.update(cam); + } } } }; @@ -853,136 +1180,60 @@ pub struct PlayModeBackup { )>, } -impl PlayModeBackup { - pub fn create_backup(editor: &mut Editor) -> anyhow::Result<()> { - let mut entities = Vec::new(); - - for (entity_id, (_, transform, properties)) in editor - .world - .query::<(&AdoptedEntity, &Transform, &ModelProperties)>() - .iter() - { - let script = editor - .world - .query_one::<&ScriptComponent>(entity_id) - .ok() - .and_then(|mut s| s.get().map(|script| script.clone())); - entities.push((entity_id, *transform, properties.clone(), script)); - } +pub enum EditorState { + Editing, + Playing, +} - let mut camera_data = Vec::new(); +pub enum PendingSpawn2 { + CreateLight, + CreatePlane, + CreateCube, + CreateCamera, +} - for (entity_id, (camera, component, follow_target)) in editor - .world - .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() - .iter() - { - camera_data.push(( - entity_id, - camera.clone(), - component.clone(), - follow_target.cloned(), - )); - } - editor.play_mode_backup = Some(PlayModeBackup { - entities, - camera_data, - }); +pub(crate) struct IsWorldLoadedYet { + /// Whether the project configuration and world data has been loaded + pub project_loaded: bool, + /// Whether the scene rendering and UI setup is complete + pub scene_loaded: bool, + /// Checks if the wgpu rendering contexts have been initialised for rendering + pub rendering_loaded: bool, +} - log::info!( - "Created play mode backup with {} entities and {} cameras", - editor.play_mode_backup.as_ref().unwrap().entities.len(), - editor.play_mode_backup.as_ref().unwrap().camera_data.len() - ); - Ok(()) +impl IsWorldLoadedYet { + pub fn new() -> Self { + Self { + project_loaded: false, + scene_loaded: false, + rendering_loaded: false, + } } - pub fn restore(editor: &mut Editor) -> anyhow::Result<()> { - if let Some(backup) = &editor.play_mode_backup { - // Restore entity states - for (entity_id, original_transform, original_properties, original_script) in - &backup.entities - { - if let Ok(mut transform) = editor.world.get::<&mut Transform>(*entity_id) { - *transform = *original_transform; - } - - if let Ok(mut properties) = editor.world.get::<&mut ModelProperties>(*entity_id) { - *properties = original_properties.clone(); - } - - let has_script = editor.world.get::<&ScriptComponent>(*entity_id).is_ok(); - match (has_script, original_script) { - (true, Some(original)) => { - if let Ok(mut script) = editor.world.get::<&mut ScriptComponent>(*entity_id) - { - *script = original.clone(); - } - } - (true, None) => { - let _ = Arc::get_mut(&mut editor.world).unwrap().remove_one::<ScriptComponent>(*entity_id); - } - (false, Some(original)) => { - let _ = Arc::get_mut(&mut editor.world).unwrap().insert_one(*entity_id, original.clone()); - } - (false, None) => { - // No change needed - } - } - } - - // Restore camera states - for (entity_id, original_camera, original_component, original_follow_target) in - &backup.camera_data - { - if let Ok(mut camera) = editor.world.get::<&mut Camera>(*entity_id) { - *camera = original_camera.clone(); - } - - if let Ok(mut component) = editor.world.get::<&mut CameraComponent>(*entity_id) { - *component = original_component.clone(); - } + pub fn is_fully_loaded(&self) -> bool { + self.project_loaded && self.scene_loaded + } - let has_follow_target = editor.world.get::<&CameraFollowTarget>(*entity_id).is_ok(); - match (has_follow_target, original_follow_target) { - (true, Some(original)) => { - if let Ok(mut follow_target) = - editor.world.get::<&mut CameraFollowTarget>(*entity_id) - { - *follow_target = original.clone(); - } - } - (true, None) => { - let _ = Arc::get_mut(&mut editor.world).unwrap().remove_one::<CameraFollowTarget>(*entity_id); - } - (false, Some(original)) => { - let _ = Arc::get_mut(&mut editor.world).unwrap().insert_one(*entity_id, original.clone()); - } - (false, None) => { - // No change needed - } - } - } + pub fn is_project_ready(&self) -> bool { + self.project_loaded + } - log::info!("Restored scene from play mode backup"); + pub fn mark_project_loaded(&mut self) { + self.project_loaded = true; + } - editor.play_mode_backup = None; - Ok(()) - } else { - Err(anyhow::anyhow!("No play mode backup found to restore")) - } + pub fn mark_scene_loaded(&mut self) { + self.scene_loaded = true; } -} -pub enum EditorState { - Editing, - Playing, + pub fn mark_rendering_loaded(&mut self) { + self.rendering_loaded = true; + } } -pub enum PendingSpawn2 { - CreateLight, - CreatePlane, - CreateCube, - CreateCamera, -} +impl Default for IsWorldLoadedYet { + fn default() -> Self { + Self::new() + } +} @@ -1,24 +1,27 @@ + use super::*; use dropbear_engine::graphics::{InstanceRaw, RenderContext}; use dropbear_engine::model::Model; use dropbear_engine::starter::plane::PlaneBuilder; use dropbear_engine::{ entity::{AdoptedEntity, Transform}, - graphics::Shader, lighting::{Light, LightComponent}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; use egui::{Align2, Image}; use eucalyptus_core::camera::PlayerCamera; -use eucalyptus_core::states::Value; +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 hecs::Entity; use log; use parking_lot::Mutex; +use tokio::sync::mpsc::unbounded_channel; use wgpu::Color; use wgpu::util::DeviceExt; use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; +use std::sync::LazyLock; pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = LazyLock::new(|| Mutex::new(Vec::new())); @@ -26,64 +29,52 @@ pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = #[async_trait::async_trait] impl Scene for Editor { async fn load<'a>(&mut self, graphics: &mut RenderContext<'a>) { - if self.active_camera.is_none() { - self.load_project_config(graphics.shared.clone()).await.unwrap(); - } - - let shader = Shader::new( - graphics.shared.clone(), - include_str!("../../../resources/shaders/shader.wgsl"), - Some("viewport_shader"), - ); + let (tx, rx) = unbounded_channel::<WorldLoadingStatus>(); + self.progress_tx = Some(rx); - self.light_manager.create_light_array_resources(graphics.shared.clone()); - - if let Some(active_camera) = self.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() { - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.shared.texture_bind_layout.clone(), - camera.layout(), - self.light_manager.layout(), - ], - None, - ); - self.render_pipeline = Some(pipeline); + 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(); - self.light_manager.create_render_pipeline( - graphics.shared.clone(), - include_str!("../../../resources/shaders/light.wgsl"), - camera, - Some("Light Pipeline"), - ); - } else { - log_once::warn_once!( - "Unable to fetch the query result of camera: {:?}", - active_camera - ) + 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); + } } - } else { - log_once::warn_once!( - "Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", - active_camera - ); - } - } else { - log_once::warn_once!("No active camera found"); - } + }) + }); 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 let Some((_, tab)) = self.dock_state.find_active_focused() { + if !self.is_world_loaded.is_project_ready() { + log_once::debug_once!("Project is not loaded yet"); + self.show_project_loading_window(&graphics.shared.get_egui_context()); + return; + } else { + log_once::debug_once!("Project has loaded successfully"); + } + + if !self.is_world_loaded.is_fully_loaded() { + log::debug!("Scene is not fully loaded, initializing..."); + // self.load(graphics).await; + return; + } else { + log_once::debug_once!("Scene has fully loaded"); + } + + if !self.is_world_loaded.rendering_loaded && self.is_world_loaded.is_fully_loaded() { + self.load_wgpu_nerdy_stuff(graphics); + return; + } + + if let Some((_, tab)) = self.dock_state.lock().find_active_focused() { self.is_viewport_focused = matches!(tab, EditorTab::Viewport); } else { self.is_viewport_focused = false; @@ -95,11 +86,22 @@ impl Scene for Editor { }; for spawn in spawns_to_process { - match AdoptedEntity::new(graphics.shared.clone(), &spawn.asset_path, Some(&spawn.asset_name)).await { + match AdoptedEntity::new( + graphics.shared.clone(), + &spawn.asset_path, + Some(&spawn.asset_name), + ) + .await + { Ok(adopted) => { - let entity_id = Arc::get_mut(&mut self.world).unwrap() - .spawn((adopted, spawn.transform, spawn.properties)); - + let entity_id = { + self.world.write().spawn(( + adopted, + spawn.transform, + spawn.properties, + )) + }; + self.selected_entity = Some(entity_id); UndoableAction::push_to_undo( @@ -125,20 +127,25 @@ impl Scene for Editor { } let mut script_entities = Vec::new(); - for (entity_id, script) in Arc::get_mut(&mut self.world).unwrap().query::<&mut ScriptComponent>().iter() { - log_once::debug_once!( - "Script Entity -> id: {:?}, component: {:?}", - entity_id, - script - ); - script.name = script - .path - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_string(); - script_entities.push((entity_id, script.name.clone())); + { + for (entity_id, script) in self.world.read() + .query::<&mut ScriptComponent>() + .iter() + { + log_once::debug_once!( + "Script Entity -> id: {:?}, component: {:?}", + entity_id, + script + ); + script.name = script + .path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + script_entities.push((entity_id, script.name.clone())); + } } if script_entities.is_empty() { @@ -149,7 +156,7 @@ impl Scene for Editor { if let Err(e) = self.script_manager.update_entity_script( entity_id, &script_name, - &mut self.world, + self.world.clone(), &self.input_state, dt, ) { @@ -184,14 +191,13 @@ impl Scene for Editor { .copied() .collect(); - // Handle camera input through ECS - if let Some(active_camera) = self.active_camera { - if let Ok(mut query) = self - .world + 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, &CameraComponent)>(active_camera) { if let Some((camera, component)) = query.get() { - // Handle keyboard input based on camera type match component.camera_type { CameraType::Debug => { DebugCamera::handle_keyboard_input(camera, &movement_keys); @@ -210,7 +216,6 @@ impl Scene for Editor { ); } CameraType::Normal => { - // Handle normal camera input if needed DebugCamera::handle_keyboard_input(camera, &movement_keys); DebugCamera::handle_mouse_input( camera, @@ -222,29 +227,46 @@ 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) => { - match &scene_entity.model_path.to_project_path(self.project_path.clone().unwrap()) { + 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) => { - match AdoptedEntity::new( - graphics.shared.clone(), - v, - Some(&scene_entity.label), - ).await { + let entity = { + AdoptedEntity::new( + graphics.shared.clone(), + v, + Some(&scene_entity.label), + ).await + }; + + match entity + { Ok(adopted) => { - let entity_id = Arc::get_mut(&mut self.world).unwrap().spawn(( - adopted, - scene_entity.transform, - ModelProperties::default(), - )); + 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 {:?}", @@ -259,52 +281,67 @@ impl Scene for Editor { 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; - }, - }; - }, - 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); + fatal!( + "Unable to convert resource reference [{}] to project related path", + reference + ); self.signal = Signal::None; - return; - }, + } }; - let adopted = AdoptedEntity::adopt( - graphics.shared.clone(), - model, - ).await; - let entity_id = Arc::get_mut(&mut self.world).unwrap().spawn(( + } 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 - ); + )) + }; + + 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; - } + 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) = Arc::get_mut(&mut self.world).unwrap().query_one::<&CameraComponent>(*sel_e) { + { + 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 @@ -317,38 +354,42 @@ impl Scene for Editor { } else { false }; - if is_viewport_cam { - warn!("You can't delete the viewport camera"); - self.signal = Signal::None; - } else { - match Arc::get_mut(&mut self.world).unwrap().despawn(*sel_e) { - Ok(_) => { - info!("Decimated entity"); - self.signal = Signal::None; - } - Err(e) => { - warn!("Failed to delete entity: {}", e); - self.signal = Signal::None; + 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(&mut Arc::get_mut(&mut self.world).unwrap()) { - Ok(_) => { - info!("Undid action"); - } - Err(e) => { - warn!("Failed to undo action: {}", e); + { + 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"); } - } else { - warn_without_console!("Nothing to undo"); - log::debug!("No undoable actions in stack"); + self.signal = Signal::None; } - self.signal = Signal::None; } Signal::None => {} Signal::Copy(_) => {} @@ -365,19 +406,25 @@ impl Scene for Editor { path: moved_path.clone(), }; - let replaced = if let Ok(mut sc) = - Arc::get_mut(&mut self.world).unwrap().get::<&mut ScriptComponent>(selected_entity) - { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { + 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( - &mut Arc::get_mut(&mut self.world).unwrap(), + self.world.clone(), selected_entity, new_script.clone(), ) { - Ok(_) => false, + Ok(_) => { + } Err(e) => { fatal!( "Failed to attach script to entity {:?}: {}", @@ -388,12 +435,15 @@ impl Scene for Editor { return; } } - }; + } - if let Err(e) = - scripting::convert_entity_to_group(&Arc::get_mut(&mut self.world).unwrap(), selected_entity) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + 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!( @@ -424,31 +474,40 @@ impl Scene for Editor { path: script_path.clone(), }; - let replaced = if let Ok(mut sc) = - Arc::get_mut(&mut self.world).unwrap().get::<&mut ScriptComponent>(selected_entity) - { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { + 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( - Arc::get_mut(&mut self.world).unwrap(), + self.world.clone(), selected_entity, new_script.clone(), ) { - Ok(_) => false, + Ok(_) => { + } Err(e) => { fatal!("Failed to attach new script: {}", e); self.signal = Signal::None; return; } } - }; + } - if let Err(e) = - scripting::convert_entity_to_group(&Arc::get_mut(&mut self.world).unwrap(), selected_entity) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + 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!( @@ -466,14 +525,25 @@ impl Scene for Editor { } ScriptAction::RemoveScript => { if let Some(selected_entity) = self.selected_entity { - if let Ok(script) = - Arc::get_mut(&mut self.world).unwrap().remove_one::<ScriptComponent>(selected_entity) + let mut success = false; + let mut comp = ScriptComponent::default(); { - success!("Removed script from entity {:?}", selected_entity); - - if let Err(e) = - scripting::convert_entity_to_group(&Arc::get_mut(&mut self.world).unwrap(), selected_entity) + 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"); @@ -481,11 +551,9 @@ impl Scene for Editor { &mut self.undo_stack, UndoableAction::RemoveComponent( selected_entity, - ComponentType::Script(script), + ComponentType::Script(comp), ), ); - } else { - warn!("No script component found on entity {:?}", selected_entity); } } else { warn!("No entity selected to remove script from"); @@ -495,16 +563,22 @@ impl Scene for Editor { } ScriptAction::EditScript => { if let Some(selected_entity) = self.selected_entity { - if let Ok(mut q) = Arc::get_mut(&mut self.world).unwrap().query_one::<&ScriptComponent>(selected_entity) - { - if let Some(script) = q.get() { - match open::that(script.path.clone()) { - Ok(()) => { - success!("Opened {}", script.name) - } - Err(e) => { - warn!("Error while opening {}: {}", script.name, e); - } + 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 { @@ -517,15 +591,14 @@ impl Scene for Editor { } }, Signal::Play => { - // Check if a player camera target exists let has_player_camera_target = self - .world + .world.read() .query::<(&Camera, &CameraComponent, &CameraFollowTarget)>() .iter() .any(|(_, (_, comp, _))| matches!(comp.camera_type, CameraType::Player)); if has_player_camera_target { - if let Err(e) = PlayModeBackup::create_backup(self) { + if let Err(e) = self.create_backup() { fatal!("Failed to create play mode backup: {}", e); self.signal = Signal::None; return; @@ -536,8 +609,13 @@ impl Scene for Editor { self.switch_to_player_camera(); let mut script_entities = Vec::new(); - for (entity_id, script) in Arc::get_mut(&mut self.world).unwrap().query::<&ScriptComponent>().iter() { - script_entities.push((entity_id, script.clone())); + { + 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 { @@ -547,21 +625,33 @@ impl Scene for Editor { script.path.display() ); - let bytes = match std::fs::read(&script.path) { + 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); + 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) { + + 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.world.clone(), &self.input_state, ) { log::warn!( @@ -592,7 +682,7 @@ impl Scene for Editor { self.signal = Signal::None; } Signal::StopPlaying => { - if let Err(e) = PlayModeBackup::restore(self) { + if let Err(e) = self.restore() { warn!("Failed to restore from play mode backup: {}", e); log::warn!("Failed to restore scene state: {}", e); } @@ -601,9 +691,10 @@ impl Scene for Editor { self.switch_to_debug_camera(); - for (entity_id, _) in Arc::get_mut(&mut self.world).unwrap().query::<&ScriptComponent>().iter() { - self.script_manager.remove_entity_script(entity_id); - } + // 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..."); @@ -612,9 +703,8 @@ impl Scene for Editor { } Signal::CameraAction(action) => match action { CameraAction::SetPlayerTarget { entity, offset } => { - // Find player camera and add/update CameraFollowTarget component let player_camera = self - .world + .world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(e, (_, comp))| { @@ -627,8 +717,9 @@ impl Scene for Editor { if let Some(camera_entity) = player_camera { let mut follow_target = (false, CameraFollowTarget::default()); - // Find the target entity label - if let Ok(mut query) = Arc::get_mut(&mut self.world).unwrap().query_one::<&AdoptedEntity>(*entity) { + if let Ok(mut query) = self.world.read() + .query_one::<&AdoptedEntity>(*entity) + { if let Some(adopted) = query.get() { follow_target = ( true, @@ -640,16 +731,19 @@ impl Scene for Editor { } } - if follow_target.0 { - let _ = Arc::get_mut(&mut self.world).unwrap().insert_one(camera_entity, follow_target); - info!("Set player camera target to entity {:?}", entity); + { + 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 + .world.read() .query::<(&Camera, &CameraComponent)>() .iter() .find_map(|(e, (_, comp))| { @@ -661,7 +755,10 @@ impl Scene for Editor { }); if let Some(camera_entity) = player_camera { - let _ = Arc::get_mut(&mut self.world).unwrap().remove_one::<CameraFollowTarget>(camera_entity); + { + let _ = self.world.write() + .remove_one::<CameraFollowTarget>(camera_entity); + } } info!("Cleared player camera target"); self.signal = Signal::None; @@ -670,7 +767,9 @@ impl Scene for Editor { Signal::AddComponent(entity, e_type) => { match e_type { EntityType::Entity => { - if let Ok(e) = Arc::get_mut(&mut self.world).unwrap().query_one_mut::<&AdoptedEntity>(*entity) { + if let Ok(e) = self.world.write() + .query_one_mut::<&AdoptedEntity>(*entity) + { let mut local_signal: Option<Signal> = None; let label = e.label().clone(); let mut show = true; @@ -692,15 +791,17 @@ impl Scene for Editor { "Adding scripting component to entity [{}]", label ); - if let Err(e) = Arc::get_mut(&mut self.world).unwrap() - .insert_one(*entity, ScriptComponent::default()) { - warn!( + 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"); + } else { + success!("Added the scripting component"); + } } local_signal = Some(Signal::None); } @@ -716,8 +817,7 @@ impl Scene for Editor { label ); - let has_camera = self - .world + let has_camera = self.world.read() .query_one::<(&Camera, &CameraComponent)>(*entity) .is_ok(); @@ -733,15 +833,17 @@ impl Scene for Editor { ); let component = CameraComponent::new(); - if let Err(e) = - Arc::get_mut(&mut self.world).unwrap().insert(*entity, (camera, component)) { - warn!( + 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"); + } else { + success!("Added the camera component"); + } } } local_signal = Some(Signal::None); @@ -760,83 +862,92 @@ impl Scene for Editor { } } EntityType::Light => { - if let Ok(light) = Arc::get_mut(&mut self.world).unwrap().query_one_mut::<&Light>(*entity) { - 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!( + { + if let Ok(light) = self.world.write() + .query_one_mut::<&Light>(*entity) + { + 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!( + success!( "Added the scripting component to light [{}]", light.label ); - self.signal = Signal::None; - } - }); - if !show { - self.signal = Signal::None; - } - } else { - log_once::warn_once!( + 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((cam, _comp)) = Arc::get_mut(&mut self.world).unwrap() - .query_one_mut::<(&Camera, &CameraComponent)>(*entity) { - 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", - 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!( + if let Ok((cam, _comp)) = self.world.write() + .query_one_mut::<(&Camera, &CameraComponent)>(*entity) + { + 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 { + Signal::RemoveComponent(entity, c_type) => + {match c_type { ComponentType::Script(_) => { - match Arc::get_mut(&mut self.world).unwrap().remove_one::<ScriptComponent>(*entity) { + match self.world.write() + .remove_one::<ScriptComponent>(*entity) + { Ok(component) => { success!("Removed script component from entity {:?}", entity); UndoableAction::push_to_undo( @@ -855,9 +966,13 @@ impl Scene for Editor { } ComponentType::Camera(_, _, follow) => { if let Some(_) = follow { - match Arc::get_mut(&mut self.world).unwrap() - .remove::<(Camera, CameraComponent, CameraFollowTarget)>(*entity) - { + match self.world.write().remove::<( + Camera, + CameraComponent, + CameraFollowTarget, + )>( + *entity + ) { Ok(component) => { success!("Removed camera component from entity {:?}", entity); UndoableAction::push_to_undo( @@ -877,7 +992,9 @@ impl Scene for Editor { } }; } else { - match Arc::get_mut(&mut self.world).unwrap().remove::<(Camera, CameraComponent)>(*entity) { + match self.world.write() + .remove::<(Camera, CameraComponent)>(*entity) + { Ok(component) => { success!("Removed camera component from entity {:?}", entity); UndoableAction::push_to_undo( @@ -894,9 +1011,8 @@ impl Scene for Editor { }; } } - }, + }}, Signal::CreateEntity => { - // self.show_add_entity_window = true; let mut show = true; egui::Window::new("Add Entity") .scroll([false, true]) @@ -937,8 +1053,9 @@ impl Scene for Editor { } } Signal::LogEntities => { - log::info!("===================="); - for entity in Arc::get_mut(&mut self.world).unwrap().iter() { + log::debug!("===================="); + let mut counter = 0; + for entity in self.world.read().iter() { if let Some(entity) = entity.get::<&AdoptedEntity>() { log::info!("Model: {:?}", entity.label()); } @@ -946,64 +1063,99 @@ impl Scene for Editor { if let Some(entity) = entity.get::<&Light>() { log::info!("Light: {:?}", entity.label()); } + + if let Some(_) = entity.get::<&Camera>() { + log::info!("Camera"); + } + counter += 1; } - log::info!("===================="); + 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; - Arc::get_mut(&mut self.world).unwrap().spawn((light, component, transform)); + 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(); + .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)); - Arc::get_mut(&mut self.world).unwrap().spawn((plane, transform, props)); + 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; + Some("Cube"), + ) + .await; match model { Ok(model) => { let cube = AdoptedEntity::adopt( graphics.shared.clone(), model, // Some("Cube") - ).await; - Arc::get_mut(&mut self.world).unwrap().spawn((cube, Transform::new(), ModelProperties::new())); + ) + .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(); - Arc::get_mut(&mut self.world).unwrap().spawn((camera, component)); + { + self.world.write() + .spawn((camera, component)); + } success!("Created new camera"); - }, + } } self.signal = Signal::None; } @@ -1013,56 +1165,114 @@ impl Scene for Editor { self.size = current_size; let new_aspect = current_size.width as f64 / current_size.height as f64; - if let Some(active_camera) = self.active_camera { - if let Ok(mut query) = Arc::get_mut(&mut self.world).unwrap().query_one::<&mut Camera>(active_camera) { - if let Some(camera) = query.get() { - camera.aspect = new_aspect; + + { + 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() { + camera.aspect = new_aspect; + } } } + } - for (_entity_id, (camera, _component, follow_target)) in self - .world - .query::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>() - .iter() - { - if let Some(target) = follow_target { - for (_target_entity_id, (adopted, transform)) in - self.world.query::<(&AdoptedEntity, &Transform)>().iter() - { - if adopted.label() == &target.follow_target { - let target_pos = transform.position; - camera.eye = target_pos + target.offset; - camera.target = target_pos; - break; + let camera_follow_data: Vec<(Entity, String, glam::Vec3)> = { + let world = self.world.read(); + world + .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() + .iter() + .filter_map(|(entity_id, (_, _, follow_target))| { + follow_target.map(|target| { + ( + entity_id, + target.follow_target.clone(), + target.offset.as_vec3() + ) + }) + }) + .collect() + }; + + + for (camera_entity, target_label, offset) in camera_follow_data { + let target_position = { + let world = self.world.read(); + world + .query::<(&AdoptedEntity, &Transform)>() + .iter() + .find_map(|(_, (adopted, transform))| { + if adopted.label() == &target_label { + Some(transform.position) + } else { + None + } + }) + }; + + + 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() { + camera.eye = pos + offset.as_dvec3(); + camera.target = pos; } } } + } - for (_entity_id, (camera, component)) in self - .world - .query::<(&mut Camera, &mut CameraComponent)>() - .iter() { - component.update(camera); - camera.update(graphics.shared.clone()); + let world = self.world.read(); + for (_entity_id, (camera, component)) in world + .query::<(&mut Camera, &mut CameraComponent)>() + .iter() + { + component.update(camera); + camera.update(graphics.shared.clone()); + } } + + { + { + let mut world = self.world.write(); + let query = world + .query_mut::<(&mut AdoptedEntity, &Transform)>(); + for (_, (entity, transform)) in query { + entity.update(graphics.shared.clone(), transform); + } + } + - let query = Arc::get_mut(&mut self.world).unwrap().query_mut::<(&mut AdoptedEntity, &Transform)>(); - for (_, (entity, transform)) in query { - entity.update(graphics.shared.clone(), transform); + { + let mut world = self.world.write(); + let light_query = + world + .query_mut::<(&mut LightComponent, &Transform, &mut Light)>(); + for (_, (light_component, transform, light)) in light_query { + light.update(light_component, transform); + } + } + } - let light_query = Arc::get_mut(&mut self.world).unwrap() - .query_mut::<(&mut LightComponent, &Transform, &mut Light)>(); - for (_, (light_component, transform, light)) in light_query { - light.update(light_component, transform); + { + let mut world = self.world.read(); + self.light_manager.update( + graphics.shared.clone(), + &mut world, + ); } - - self.light_manager.update(graphics.shared.clone(), &Arc::get_mut(&mut self.world).unwrap()); - crate::debug::show_dependency_progress_window(&graphics.shared.get_egui_context(), &mut self.dep_installer); + + if self.dep_installer.is_installing { + self.dep_installer + .show_installation_window(&mut graphics.shared.get_egui_context()); + } + self.dep_installer.update_progress(); } async fn render<'a>(&mut self, graphics: &mut RenderContext<'a>) { @@ -1077,73 +1287,105 @@ impl Scene for Editor { self.color = color.clone(); self.size = graphics.shared.viewport_texture.size.clone(); self.texture_id = Some(*graphics.shared.texture_id.clone()); - self.show_ui(&graphics.shared.get_egui_context()); + { self.show_ui(&graphics.shared.get_egui_context()).await; } self.window = Some(graphics.shared.window.clone()); logging::render(&graphics.shared.get_egui_context()); if let Some(pipeline) = &self.render_pipeline { - if let Some(active_camera) = self.active_camera { - if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { - if let Some(camera) = query.get() { - let mut light_query = self.world.query::<(&Light, &LightComponent)>(); - let mut entity_query = self.world.query::<(&AdoptedEntity, &Transform)>(); - { - let mut render_pass = graphics.clear_colour(color); - - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); - for (_, (light, component)) in light_query.iter() { - if component.enabled { - render_pass.set_vertex_buffer( - 1, - light.instance_buffer.as_ref().unwrap().slice(..), - ); - render_pass.draw_light_model( - light.model(), - camera.bind_group(), - light.bind_group(), - ); - } - } - } - - let mut model_batches: HashMap<*const Model, Vec<InstanceRaw>> = - HashMap::new(); - - for (_, (entity, _)) in entity_query.iter() { - let model_ptr = entity.model() as *const Model; - let instance_raw = entity.instance.to_raw(); - model_batches - .entry(model_ptr) - .or_insert(Vec::new()) - .push(instance_raw); - } - - render_pass.set_pipeline(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 + } + } else { + None + } + }; + + + 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)>(); + for (_, (light, comp)) in light_query.iter() { + lights.push((light.clone(), comp.clone())); + } + lights + }; + - for (model_ptr, instances) in model_batches { - let model = unsafe { &*model_ptr }; + let entities = { + let world = self.world.read(); + let mut entities = Vec::new(); + let mut entity_query = world.query::<(&AdoptedEntity, &Transform)>(); + for (_, (entity, transform)) in entity_query.iter() { + entities.push((entity.clone(), transform.clone())); + } + entities + }; + - let instance_buffer = graphics.shared.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Batched Instance Buffer"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::VERTEX, - }, + { + let mut render_pass = graphics.clear_colour(color); + if let Some(light_pipeline) = &self.light_manager.pipeline { + render_pass.set_pipeline(light_pipeline); + for (light, _component) in &lights { + render_pass.set_vertex_buffer( + 1, + light.instance_buffer.as_ref().unwrap().slice(..), ); - - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.draw_model_instanced( - model, - 0..instances.len() as u32, + render_pass.draw_light_model( + light.model(), camera.bind_group(), - self.light_manager.bind_group(), + light.bind_group(), ); } } + + + let mut model_batches: HashMap<*const Model, Vec<InstanceRaw>> = + HashMap::new(); + for (entity, _) in &entities { + let model_ptr = entity.model() as *const Model; + let instance_raw = entity.instance.to_raw(); + model_batches + .entry(model_ptr) + .or_insert(Vec::new()) + .push(instance_raw); + } + + render_pass.set_pipeline(pipeline); + for (model_ptr, instances) in model_batches { + let model = unsafe { &*model_ptr }; + let instance_buffer = graphics.shared.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("Batched Instance Buffer"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX, + }, + ); + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.draw_model_instanced( + model, + 0..instances.len() as u32, + camera.bind_group(), + self.light_manager.bind_group(), + ); + } } + } else { + log_once::error_once!("Camera returned None"); } + } else { + log_once::error_once!("No active camera found"); } + } else { + log_once::error_once!("No render pipeline exists"); } } @@ -136,6 +136,7 @@ async fn main() -> anyhow::Result<()> { (scene_manager, input_manager) }) + .await .unwrap(); } _ => unreachable!(), @@ -5,7 +5,9 @@ use std::{ use anyhow::anyhow; use dropbear_engine::{ - graphics::RenderContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand} + graphics::RenderContext, + input::{Controller, Keyboard, Mouse}, + scene::{Scene, SceneCommand}, }; use egui::{self, FontId, Frame, RichText}; use egui_toast_fork::{ToastOptions, Toasts}; @@ -160,83 +162,90 @@ impl Scene for MainMenu { graphics.shared.window.inner_size().height as f32 - 100.0, ); let egui_ctx = graphics.shared.get_egui_context(); - + let mut local_open_project = false; egui::CentralPanel::default() - .frame(Frame::new()) - .show(&egui_ctx, |ui| { - ui.vertical_centered(|ui| { - ui.add_space(64.0); - ui.label(RichText::new("Eucalyptus").font(FontId::proportional(32.0))); - ui.add_space(40.0); - - let button_size = egui::vec2(300.0, 60.0); - - if ui - .add_sized(button_size, egui::Button::new("New Project")) - .clicked() - { - log::debug!("Creating new project"); - self.show_new_project = true; - } - ui.add_space(20.0); - - if ui - .add_sized(button_size, egui::Button::new("Open Project")) - .clicked() - { - log::debug!("Opening project"); - self.is_in_file_dialogue = true; - if let Some(path) = rfd::FileDialog::new() - .add_filter("Eucalyptus Configuration Files", &["eucp"]) - .pick_file() - { - match ProjectConfig::read_from(&path) { - Ok(config) => { - log::info!("Loaded project!"); - let mut global = PROJECT.write(); - *global = config; - // println!("Loaded config info: {:#?}", global); - self.scene_command = - SceneCommand::SwitchScene(String::from("editor")); - } - Err(e) => if e.to_string().contains("missing field") { - 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(), - options: ToastOptions::default() - .duration_in_seconds(10.0) - .show_progress(true), - ..Default::default() - }); - log::error!("Failed to load project: {}", e); - } - }; - } else { - log::warn!("File dialog returned \"None\""); - } - self.is_in_file_dialogue = false; - } - ui.add_space(20.0); + .frame(Frame::new()) + .show(&egui_ctx, |ui| { + ui.vertical_centered(|ui| { + ui.add_space(64.0); + ui.label(RichText::new("Eucalyptus").font(FontId::proportional(32.0))); + ui.add_space(40.0); - if ui - .add_sized(button_size, egui::Button::new("Settings")) - .clicked() - { - log::debug!("Settings (not implemented)"); - } - ui.add_space(20.0); + let button_size = egui::vec2(300.0, 60.0); - if ui - .add_sized(button_size, egui::Button::new("Quit")) - .clicked() - { - self.scene_command = SceneCommand::Quit - } - ui.add_space(20.0); - }); + if ui + .add_sized(button_size, egui::Button::new("New Project")) + .clicked() + { + log::debug!("Creating new project"); + self.show_new_project = true; + } + ui.add_space(20.0); + + if ui + .add_sized(button_size, egui::Button::new("Open Project")) + .clicked() + { + local_open_project = true; + } + ui.add_space(20.0); + + if ui + .add_sized(button_size, egui::Button::new("Settings")) + .clicked() + { + log::debug!("Settings (not implemented)"); + } + ui.add_space(20.0); + + if ui + .add_sized(button_size, egui::Button::new("Quit")) + .clicked() + { + self.scene_command = SceneCommand::Quit + } + ui.add_space(20.0); }); + }); + + if local_open_project { + log::debug!("Opening project"); + self.is_in_file_dialogue = true; + if let Some(path) = rfd::AsyncFileDialog::new() + .add_filter("Eucalyptus Configuration Files", &["eucp"]) + .pick_file() + .await + { + match ProjectConfig::read_from(&path.into()) { + Ok(config) => { + log::info!("Loaded project!"); + let mut global = PROJECT.write(); + *global = config; + // println!("Loaded config info: {:#?}", global); + self.scene_command = SceneCommand::SwitchScene(String::from("editor")); + } + Err(e) => { + if e.to_string().contains("missing field") { + 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(), + options: ToastOptions::default() + .duration_in_seconds(10.0) + .show_progress(true), + ..Default::default() + }); + log::error!("Failed to load project: {}", e); + } + } + }; + } else { + log::warn!("File dialog returned \"None\""); + } + 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") .open(&mut show_new_project) .resizable(true) @@ -260,16 +269,7 @@ impl Scene for MainMenu { ui.add_space(5.0); if ui.button("Choose Location").clicked() { - self.is_in_file_dialogue = true; - if let Some(path) = rfd::FileDialog::new() - .set_title("Save Project") - .set_file_name(&self.project_name) - .pick_folder() - { - self.project_path = Some(path); - log::debug!("Project will be saved at: {:?}", self.project_path); - } - self.is_in_file_dialogue = false; + local_select_project = true; } let can_create = self.project_path.is_some() && !self.project_name.is_empty(); @@ -285,6 +285,20 @@ impl Scene for MainMenu { }); self.show_new_project = show_new_project; + if local_select_project { + self.is_in_file_dialogue = true; + if let Some(path) = rfd::AsyncFileDialog::new() + .set_title("Save Project") + .set_file_name(&self.project_name) + .pick_folder() + .await + { + self.project_path = Some(path.into()); + log::debug!("Project will be saved at: {:?}", self.project_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 { @@ -1 +1 @@ -Subproject commit ba32a2319d7b302c0d294311a9ad3135e9eb8503 +Subproject commit 5cb98c0d508c7f8fd7d6a268d101d6e0fde0f41f @@ -0,0 +1,3 @@ +/// Queries the attached entity. Returns a serialized string to be decoded. +@external(javascript, "dropbear", "queryAttachedEntity") +fn raw_query_attached_entity() -> String @@ -1,2 +0,0 @@ -@external(javascript, "dropbear_input", "is_key_pressed") -fn is_key_pressed(key_code: Int) -> Int @@ -1 +0,0 @@ -//// Internal library for WASM memory sharing between the dropbear game engine. @@ -1,14 +0,0 @@ -/// Javascript library for managing memory in WebAssembly for the dropbear engine Gleam interface. - -class WasmMemoryManager { - constructor(memory) { - this.memory = memory; - this.nextFreeOffset = 1024; - this.allocations = new Map(); - } - - /// Fetches the new view of the memory after the memory is refreshed - getView() { - return new DataView(this.memory.buffer); - } -} @@ -1 +0,0 @@ -# Hello World