tirbofish/dropbear · diff
attempted to improve model loading, required to change from rustyscript to wasm. fml
Signature present but could not be verified.
Unverified
@@ -38,7 +38,6 @@ model_to_image = "0.1.1" once_cell = "1.21" open = "5" parking_lot = {version = "0.12", features = ["deadlock_detection"] } -pollster = "0.4" rfd = "0.15.4" ron = "0.11" russimp-ng = { version = "3.2", features = ["static-link"] } @@ -46,10 +45,12 @@ 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" winit = { version = "0.30", features = [] } zip = "4.6" walkdir = "2.3" +wasmtime = "36.0" [workspace.dependencies.image] version = "0.25" @@ -6,8 +6,9 @@ If you might have not realised, all the crates/projects names are after Australi ## Related projects -- [eucalyptus](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus) is the visual editor used to create games visually, taking inspiration from Unity, Roblox Studio and other engines. -- [redback](https://github.com/4tkbytes/redback-runtime) is the runtime used to load .eupak files and run the games loaded on them. +- [eucalyptus-editor](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Roblox Studio and other engines. +- [eucalyptus-core](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other. +- [redback-runtime](https://github.com/4tkbytes/redback-runtime) is the runtime used to load .eupak files and run the games loaded on them. ## Build @@ -16,12 +17,21 @@ To build, ensure build requirements, clone the repository, then build it. It wil With Unix systems (macOS not tested), you will have to download a couple dependencies if building locally: <!-- If you have a macOS system, please create a PR and add your own implementation. I know you need to use brew, but I don't know what dependencies to install. --> +--- + +### Ubuntu ```bash -# ubuntu, adapt to your own OS sudo apt install libudev-dev pkg-config libssl-dev clang cmake meson assimp-utils ``` +### Arch + +```bash +sudo pacman -Syu base-devel systemd pkgconf openssl clang cmake meson assimp +``` +--- + After downloading the requirements, you are free to build it using cargo. ```bash @@ -24,7 +24,6 @@ glam.workspace = true log.workspace = true log-once.workspace = true russimp-ng.workspace = true -pollster.workspace = true serde.workspace = true spin_sleep.workspace = true wgpu.workspace = true @@ -33,6 +32,7 @@ hecs.workspace = true once_cell.workspace = true parking_lot.workspace = true lazy_static.workspace = true +tokio.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -1,4 +1,3 @@ -use std::io::Write; pub mod lighting; pub mod buffer; pub mod camera; @@ -14,12 +13,13 @@ pub mod panic; pub mod starter; pub mod utils; +use std::io::Write; use chrono::Local; use egui::TextureId; use egui_wgpu_backend::ScreenDescriptor; -use futures::FutureExt; use gilrs::{Gilrs, GilrsBuilder}; use spin_sleep::SpinSleeper; +use tokio::task::LocalSet; use std::{ fmt::{self, Display, Formatter}, sync::Arc, @@ -350,14 +350,15 @@ pub struct App { target_fps: u32, /// The library used for polling controllers, specifically the instance of that. gilrs: Gilrs, + // /// A task pool used for background async work + // runtime: Runtime, } impl App { /// Creates a new instance of the application. It only sets the default for the struct + the /// window config. fn new(config: WindowConfiguration) -> Self { - log::debug!("Created new instance of app"); - Self { + let result = Self { state: None, config: config.clone(), scene_manager: scene::Manager::new(), @@ -367,10 +368,12 @@ impl App { target_fps: config.max_fps, // default settings for now gilrs: GilrsBuilder::new().build().unwrap(), - } + // runtime, + }; + log::debug!("Created new instance of app"); + result } - #[allow(dead_code)] /// A constant that lets you not have any fps count. /// It is just the max value of an unsigned 32 bit number lol. pub const NO_FPS_CAP: u32 = u32::MAX; @@ -380,6 +383,14 @@ impl App { self.target_fps = fps.max(1); } + // /// Spawns a new task + // pub fn spawn_task<F>(&self, fut: F) + // where + // F: std::future::Future<Output = ()> + Send + 'static, + // { + // let _ = self.runtime.spawn(fut); + // } + /// The run function. This function runs the app into gear. /// /// ## Warning @@ -393,7 +404,7 @@ 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), { @@ -531,7 +542,7 @@ impl ApplicationHandler for App { let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); - self.state = Some(pollster::block_on(State::new(window)).unwrap()); + self.state = Some(tokio::runtime::Handle::current().block_on(State::new(window)).unwrap()); if let Some(state) = &mut self.state { let size = state.window.inner_size(); @@ -555,7 +566,10 @@ impl ApplicationHandler for App { state.egui_renderer.handle_input(&event); match event { - WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::CloseRequested => { + log::info!("Exiting app"); + event_loop.exit(); + }, WindowEvent::Resized(size) => state.resize(size.width, size.height), WindowEvent::RedrawRequested => { let frame_start = Instant::now(); @@ -564,14 +578,14 @@ impl ApplicationHandler for App { self.input_manager.set_active_handlers(active_handlers); self.input_manager.update(&mut self.gilrs); - if let Some(result) = state - .render(&mut self.scene_manager, self.delta_time, event_loop) - .now_or_never() - { - match result { - Ok(_) => {} - Err(_) => {} - } + + let ls = LocalSet::new(); + let render_result = tokio::runtime::Handle::current().block_on(ls.run_until(async { + state.render(&mut self.scene_manager, self.delta_time, event_loop).await + })); + + if let Err(e) = render_result { + log::error!("Render failed: {:?}", e); } let frame_elapsed = frame_start.elapsed(); @@ -8,6 +8,7 @@ use russimp_ng::{ use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt}; use lazy_static::lazy_static; use parking_lot::Mutex; +use tokio::task; use crate::graphics::{Graphics, NO_MODEL, Texture}; use crate::utils::ResourceReference; @@ -42,8 +43,20 @@ pub struct Mesh { pub material: usize, } +struct ProcessedMesh { + name: String, + vertices: Vec<ModelVertex>, + indices: Vec<u32>, + material_index: usize, +} + +struct ProcessedMaterial { + name: String, + texture_bytes: Option<Vec<u8>>, +} + impl Model { - pub fn load_from_memory( + pub async fn load_from_memory_async( graphics: &Graphics<'_>, buffer: Vec<u8>, label: Option<&str>, @@ -56,9 +69,97 @@ impl Model { return Ok(cached_model.clone()); } - log::debug!("Loading from memory"); + log::debug!("Loading from memory asynchronously"); let res_ref = ResourceReference::from_bytes(buffer.clone()); + let label_owned = label.clone().unwrap_or_default().to_string(); + + let (processed_materials, processed_meshes) = task::spawn_blocking(move || { + Self::process_scene_from_buffer(buffer, Some(label_owned.as_str())) + }).await??; + + let model = Self::create_gpu_model( + graphics, + processed_materials, + processed_meshes, + cache_key.clone(), + res_ref, + ).await?; + + MEMORY_MODEL_CACHE.lock().insert(cache_key, model.clone()); + log::debug!("Model cached from memory: {:?}", label); + Ok(model) + } + + pub async fn load_async( + graphics: &Graphics<'_>, + path: &PathBuf, + label: Option<&str>, + ) -> anyhow::Result<Model> { + let file_name = path.file_name(); + log::debug!("Loading model asynchronously [{:?}]", file_name); + + let path_str = path.to_string_lossy().to_string(); + + if let Some(cached_model) = MODEL_CACHE.lock().get(&path_str) { + log::debug!("Model loaded from cache: {:?}", path_str); + return Ok(cached_model.clone()); + } + + let path_clone = path.clone(); + + let (processed_materials, processed_meshes) = task::spawn_blocking(move || { + Self::process_scene_from_file(&path_clone) + }).await??; + + let model_label = if let Some(l) = label { + l.to_string() + } else { + file_name.unwrap().to_str().unwrap().split('.').next().unwrap().to_string() + }; + + let resource_ref = ResourceReference::from_path(path)?; + + let model = Self::create_gpu_model( + graphics, + processed_materials, + processed_meshes, + model_label, + resource_ref, + ).await?; + + MODEL_CACHE.lock().insert(path_str, model.clone()); + log::debug!("Model cached and loaded: {:?}", file_name); + Ok(model) + } + + pub fn load_from_memory( + graphics: &Graphics<'_>, + buffer: Vec<u8>, + label: Option<&str>, + ) -> anyhow::Result<Model> { + let rt = tokio::runtime::Handle::try_current() + .or_else(|_| { + tokio::runtime::Runtime::new().map(|rt| rt.handle().clone()) + })?; + + rt.block_on(Self::load_from_memory_async(graphics, buffer, label)) + } + + pub fn load( + graphics: &Graphics<'_>, + path: &PathBuf, + label: Option<&str>, + ) -> anyhow::Result<Model> { + let rt = tokio::runtime::Handle::current(); + + rt.block_on(Self::load_async(graphics, path, label)) + } + + fn process_scene_from_buffer( + buffer: Vec<u8>, + label: Option<&str>, + ) -> anyhow::Result<(Vec<ProcessedMaterial>, Vec<ProcessedMesh>)> { let scene = match Scene::from_buffer( buffer.as_slice(), vec![ @@ -83,137 +184,12 @@ impl Model { }, }; - let mut materials = Vec::new(); - for m in &scene.materials { - let mut name = String::new(); - let diffuse_bytes_opt = m - .textures - .iter() - .find(|(t_type, _)| **t_type == TextureType::Diffuse) - .and_then(|(_, tex)| { - name = tex.borrow().filename.clone(); - match &tex.borrow().data { - DataContent::Bytes(b) => Some(b.clone()), - DataContent::Texel(_) => { - log::warn!("Skipping texel-based texture for material '{}'", &name); - None - } - } - }); - - let diffuse_texture = if let Some(bytes) = diffuse_bytes_opt { - Texture::new(graphics, &bytes) - } else { - if !name.is_empty() { - log::warn!( - "Error loading material {}, using default missing texture", - name - ); - } else { - log::warn!("Error loading material, using default missing texture"); - } - Texture::new(graphics, GREY_TEXTURE_BYTES) - }; - - let bind_group = diffuse_texture.bind_group().to_owned(); - materials.push(Material { - name: name, - diffuse_texture, - bind_group, - }); - } - - let mut meshes = Vec::new(); - for mesh in &scene.meshes { - let vertices: Vec<ModelVertex> = mesh - .vertices - .iter() - .enumerate() - .map(|(i, v)| { - let normal = mesh.normals.get(i).copied().unwrap_or(Vector3D { - x: 0.0, - y: 1.0, - z: 0.0, - }); - let tex_coords = mesh - .texture_coords - .get(0) - .and_then(|coords| coords.as_ref().and_then(|vec| vec.get(i))) - .map(|tc| [tc.x, tc.y]) - .unwrap_or([0.0, 0.0]); - ModelVertex { - position: [v.x, v.y, v.z], - tex_coords, - normal: [normal.x, normal.y, normal.z], - } - }) - .collect(); - - let indices: Vec<u32> = mesh - .faces - .iter() - .flat_map(|f| f.0.iter().copied()) - .collect(); - - let vertex_buffer = - graphics - .state - .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 - .state - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Index Buffer", label)), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); - - meshes.push(Mesh { - name: mesh.name.clone(), - vertex_buffer, - index_buffer, - num_elements: indices.len() as u32, - material: mesh.material_index as usize, - }); - } - log::debug!("Successfully loaded model [{:?}]", label); - let model = Model { - meshes, - materials, - label: if let Some(l) = label { - l.to_string() - } else { - String::from("No named model") - }, - path: res_ref, - }; - - MEMORY_MODEL_CACHE.lock().insert(cache_key, model.clone()); - log::debug!("Model cached from memory: {:?}", label); - Ok(model) + Self::process_scene(scene) } - pub fn load( - graphics: &Graphics<'_>, + fn process_scene_from_file( path: &PathBuf, - label: Option<&str>, - ) -> anyhow::Result<Model> { - let file_name = path.file_name(); - log::debug!("Loading model [{:?}]", file_name); - - let path_str = path.to_string_lossy().to_string(); - - if let Some(cached_model) = MODEL_CACHE.lock().get(&path_str) { - log::debug!("Model loaded from cache: {:?}", path_str); - return Ok(cached_model.clone()); - } - + ) -> anyhow::Result<(Vec<ProcessedMaterial>, Vec<ProcessedMesh>)> { let scene = match Scene::from_file( path.to_str().unwrap(), vec![ @@ -237,120 +213,155 @@ impl Model { }, }; - let mut materials = Vec::new(); - for m in &scene.materials { - let mut name = String::new(); - let diffuse_bytes_opt = m - .textures - .iter() - .find(|(t_type, _)| **t_type == TextureType::Diffuse) - .and_then(|(_, tex)| { - name = tex.borrow().filename.clone(); - match &tex.borrow().data { - DataContent::Bytes(b) => Some(b.clone()), - DataContent::Texel(_) => { - log::warn!("Skipping texel-based texture for material '{}'", &name); - None + Self::process_scene(scene) + } + + fn process_scene(scene: Scene) -> anyhow::Result<(Vec<ProcessedMaterial>, Vec<ProcessedMesh>)> { + let processed_materials: Vec<ProcessedMaterial> = scene + .materials + .into_iter() + .map(|m| { + let mut name = String::new(); + let texture_bytes = m + .textures + .iter() + .find(|(t_type, _)| **t_type == TextureType::Diffuse) + .and_then(|(_, tex)| { + name = tex.borrow().filename.clone(); + match &tex.borrow().data { + DataContent::Bytes(b) => Some(b.clone()), + DataContent::Texel(_) => { + log::warn!("Skipping texel-based texture for material '{}'", &name); + None + } } - } - }); - - let diffuse_texture = if let Some(bytes) = diffuse_bytes_opt { - Texture::new(graphics, &bytes) - } else { - if !name.is_empty() { - log::warn!( - "Error loading material {}, using default missing texture", - name - ); - } else { - log::warn!("Error loading material, using default missing texture"); + }); + + ProcessedMaterial { + name, + texture_bytes, } - Texture::new(graphics, GREY_TEXTURE_BYTES) - }; - - let bind_group = diffuse_texture.bind_group().to_owned(); - materials.push(Material { - name: name, - diffuse_texture, - bind_group, - }); - } + }) + .collect(); + + let processed_meshes: Vec<ProcessedMesh> = scene + .meshes + .into_iter() + .map(|mesh| { + let vertices: Vec<ModelVertex> = mesh + .vertices + .iter() + .enumerate() + .map(|(i, v)| { + let normal = mesh.normals.get(i).copied().unwrap_or(Vector3D { + x: 0.0, + y: 1.0, + z: 0.0, + }); + let tex_coords = mesh + .texture_coords + .get(0) + .and_then(|coords| coords.as_ref().and_then(|vec| vec.get(i))) + .map(|tc| [tc.x, tc.y]) + .unwrap_or([0.0, 0.0]); + ModelVertex { + position: [v.x, v.y, v.z], + tex_coords, + normal: [normal.x, normal.y, normal.z], + } + }) + .collect(); + + let indices: Vec<u32> = mesh + .faces + .iter() + .flat_map(|f| f.0.iter().copied()) + .collect(); + + ProcessedMesh { + name: mesh.name, + vertices, + indices, + material_index: mesh.material_index as usize, + } + }) + .collect(); - let mut meshes = Vec::new(); - for mesh in &scene.meshes { - let vertices: Vec<ModelVertex> = mesh - .vertices - .iter() - .enumerate() - .map(|(i, v)| { - let normal = mesh.normals.get(i).copied().unwrap_or(Vector3D { - x: 0.0, - y: 1.0, - z: 0.0, - }); - let tex_coords = mesh - .texture_coords - .get(0) - .and_then(|coords| coords.as_ref().and_then(|vec| vec.get(i))) - .map(|tc| [tc.x, tc.y]) - .unwrap_or([0.0, 0.0]); - ModelVertex { - position: [v.x, v.y, v.z], - tex_coords, - normal: [normal.x, normal.y, normal.z], - } - }) - .collect(); + Ok((processed_materials, processed_meshes)) + } - let indices: Vec<u32> = mesh - .faces - .iter() - .flat_map(|f| f.0.iter().copied()) - .collect(); + async fn create_gpu_model( + graphics: &Graphics<'_>, + processed_materials: Vec<ProcessedMaterial>, + processed_meshes: Vec<ProcessedMesh>, + label: String, + path: ResourceReference, + ) -> anyhow::Result<Model> { + // Create materials with textures + let materials: Vec<Material> = processed_materials + .into_iter() + .map(|pm| { + let diffuse_texture = if let Some(bytes) = pm.texture_bytes { + Texture::new(graphics, &bytes) + } else { + if !pm.name.is_empty() { + log::warn!( + "Error loading material {}, using default missing texture", + pm.name + ); + } else { + log::warn!("Error loading material, using default missing texture"); + } + Texture::new(graphics, GREY_TEXTURE_BYTES) + }; + + let bind_group = diffuse_texture.bind_group().to_owned(); + Material { + name: pm.name, + diffuse_texture, + bind_group, + } + }) + .collect(); - let vertex_buffer = - graphics + let meshes: Vec<Mesh> = processed_meshes + .into_iter() + .map(|pm| { + let vertex_buffer = graphics .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", file_name)), - contents: bytemuck::cast_slice(&vertices), + label: Some(&format!("{} Vertex Buffer", label)), + contents: bytemuck::cast_slice(&pm.vertices), usage: wgpu::BufferUsages::VERTEX, }); - let index_buffer = - graphics + + let index_buffer = graphics .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Index Buffer", file_name)), - contents: bytemuck::cast_slice(&indices), + label: Some(&format!("{} Index Buffer", label)), + contents: bytemuck::cast_slice(&pm.indices), usage: wgpu::BufferUsages::INDEX, }); - meshes.push(Mesh { - name: mesh.name.clone(), - vertex_buffer, - index_buffer, - num_elements: indices.len() as u32, - material: mesh.material_index as usize, - }); - } - log::debug!("Successfully loaded model [{:?}]", file_name); - let model = Model { + Mesh { + name: pm.name, + vertex_buffer, + index_buffer, + num_elements: pm.indices.len() as u32, + material: pm.material_index, + } + }) + .collect(); + + log::debug!("Successfully loaded model [{}]", label); + Ok(Model { meshes, materials, - label: if let Some(l) = label { - l.to_string() - } else { - String::from(file_name.unwrap().to_str().unwrap().split(".").into_iter().next().unwrap()) - }, - path: ResourceReference::from_path(path).unwrap(), - }; - - MODEL_CACHE.lock().insert(path_str, model.clone()); - log::debug!("Model cached and loaded: {:?}", file_name); - Ok(model) + label, + path, + }) } } @@ -538,4 +549,4 @@ impl Vertex for ModelVertex { ], } } -} +} @@ -100,7 +100,7 @@ impl ResourceReference { /// ``` /// /// Returns `None` if the path doesn't contain "resources" or if the path after resources is empty. - pub fn from_path(full_path: impl AsRef<Path>) -> Option<Self> { + pub fn from_path(full_path: impl AsRef<Path>) -> anyhow::Result<Self> { let path = full_path.as_ref(); let components: Vec<_> = path.components().collect(); @@ -110,7 +110,7 @@ impl ResourceReference { if *name == "resources" { let remaining_components = &components[i + 1..]; if remaining_components.is_empty() { - return None; + anyhow::bail!("Unable to locate any remaining components"); } let resource_path = remaining_components @@ -122,14 +122,14 @@ impl ResourceReference { .collect::<Vec<_>>() .join("/"); - return Some(Self { + return Ok(Self { ref_type: ResourceReferenceType::File(resource_path), }); } } } - None + anyhow::bail!("Nothing here") } pub fn as_bytes(&self) -> Option<&[u8]> { @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true license-file.workspace = true repository.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] anyhow.workspace = true @@ -26,6 +26,9 @@ serde.workspace = true winit.workspace = true zip.workspace = true rustyscript.workspace = true +tokio.workspace = true + +wasmtime.workspace = true [features] editor = [] @@ -0,0 +1,5 @@ +# eucalyptus-core + +The core libraries of the eucalyptus editor. Great for embedding into `redback-runtime` and `eucalyptus-editor` as one big change instead of a bunch of features. + +This is a library, so if tools are wished to be made, this is the perfect library for you. @@ -41,136 +41,6 @@ pub enum ScriptAction { EditScript, } -pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { - let project_path = { - let project = PROJECT.read(); - project.project_path.clone() - }; - - let src_path = { - let source_config = SOURCE.read(); - source_config.path.clone() - }; - - let scripts_dir = src_path; - fs::create_dir_all(&scripts_dir)?; - - let filename = script_path - .file_name() - .ok_or_else(|| anyhow::anyhow!("Invalid script path: no filename"))?; - - let dest_path = scripts_dir.join(filename); - - if dest_path.exists() { - log::info!( - "Script file already exists at {:?}, returning existing path", - dest_path - ); - return Ok(dest_path); - } - - const MAX_RETRIES: usize = 5; - const RETRY_DELAY_MS: u64 = 60; - - let mut last_err: Option<std::io::Error> = None; - for attempt in 0..=MAX_RETRIES { - match fs::copy(script_path, &dest_path) { - Ok(_) => { - log::info!("Copied script from {:?} to {:?}", script_path, dest_path); - last_err = None; - break; - } - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { - log::warn!( - "Script file already exists at {:?}, continuing anyway", - dest_path - ); - last_err = None; - break; - } - Err(e) => { - if e.raw_os_error() == Some(32) && attempt < MAX_RETRIES { - log::warn!( - "Sharing violation copying script (attempt {}/{}). Retrying in {}ms: {}", - attempt + 1, - MAX_RETRIES, - RETRY_DELAY_MS, - e - ); - std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); - last_err = Some(e); - continue; - } else { - return Err(e.into()); - } - } - } - } - if let Some(e) = last_err { - return Err(e.into()); - } - - { - let source_config = SOURCE.read(); - source_config.write_to(&project_path)?; - } - - log::info!("Moved script from {:?} to {:?}", script_path, dest_path); - Ok(dest_path) -} - -pub fn convert_entity_to_group( - world: &World, - entity_id: hecs::Entity, -) -> anyhow::Result<EntityNode> { - if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { - if let Some((adopted, _transform)) = query.get() { - let entity_name = adopted.model().label.clone(); - - let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { - Some(EntityNode::Script { - name: script.name.clone(), - path: script.path.clone(), - }) - } else { - None - }; - - let entity_node = EntityNode::Entity { - id: entity_id, - name: entity_name.clone(), - }; - - if let Some(script_node) = script_node { - Ok(EntityNode::Group { - name: entity_name, - children: vec![entity_node, script_node], - collapsed: false, - }) - } else { - Ok(entity_node) - } - } else { - Err(anyhow::anyhow!("Failed to get entity components")) - } - } else { - Err(anyhow::anyhow!("Failed to query entity {:?}", entity_id)) - } -} - -pub fn attach_script_to_entity( - world: &mut 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)); - } - - log::info!("Successfully attached script to entity {:?}", entity_id); - Ok(()) -} - pub struct ScriptManager { pub runtime: Runtime, compiled_scripts: HashMap<String, ModuleHandle>, @@ -178,8 +48,9 @@ pub struct ScriptManager { } impl ScriptManager { - pub fn new() -> anyhow::Result<Self> { - let mut runtime = Runtime::new(RuntimeOptions::default())?; + pub async fn new() -> anyhow::Result<Self> { + // let mut runtime = Runtime::new(RuntimeOptions::default())?; + let mut runtime = Runtime::with_tokio_runtime_handle(RuntimeOptions::default(), tokio::runtime::Handle::try_current()?)?; let dropbear_content = include_str!("../../../resources/dropbear.ts"); let dropbear_module = Module::new("./dropbear.ts", dropbear_content); runtime.load_module(&dropbear_module)?; @@ -830,4 +701,134 @@ impl ScriptManager { } Ok(()) } +} + +pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { + let project_path = { + let project = PROJECT.read(); + project.project_path.clone() + }; + + let src_path = { + let source_config = SOURCE.read(); + source_config.path.clone() + }; + + let scripts_dir = src_path; + fs::create_dir_all(&scripts_dir)?; + + let filename = script_path + .file_name() + .ok_or_else(|| anyhow::anyhow!("Invalid script path: no filename"))?; + + let dest_path = scripts_dir.join(filename); + + if dest_path.exists() { + log::info!( + "Script file already exists at {:?}, returning existing path", + dest_path + ); + return Ok(dest_path); + } + + const MAX_RETRIES: usize = 5; + const RETRY_DELAY_MS: u64 = 60; + + let mut last_err: Option<std::io::Error> = None; + for attempt in 0..=MAX_RETRIES { + match fs::copy(script_path, &dest_path) { + Ok(_) => { + log::info!("Copied script from {:?} to {:?}", script_path, dest_path); + last_err = None; + break; + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + log::warn!( + "Script file already exists at {:?}, continuing anyway", + dest_path + ); + last_err = None; + break; + } + Err(e) => { + if e.raw_os_error() == Some(32) && attempt < MAX_RETRIES { + log::warn!( + "Sharing violation copying script (attempt {}/{}). Retrying in {}ms: {}", + attempt + 1, + MAX_RETRIES, + RETRY_DELAY_MS, + e + ); + std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); + last_err = Some(e); + continue; + } else { + return Err(e.into()); + } + } + } + } + if let Some(e) = last_err { + return Err(e.into()); + } + + { + let source_config = SOURCE.read(); + source_config.write_to(&project_path)?; + } + + log::info!("Moved script from {:?} to {:?}", script_path, dest_path); + Ok(dest_path) +} + +pub fn convert_entity_to_group( + world: &World, + entity_id: hecs::Entity, +) -> anyhow::Result<EntityNode> { + if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { + if let Some((adopted, _transform)) = query.get() { + let entity_name = adopted.model().label.clone(); + + let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { + Some(EntityNode::Script { + name: script.name.clone(), + path: script.path.clone(), + }) + } else { + None + }; + + let entity_node = EntityNode::Entity { + id: entity_id, + name: entity_name.clone(), + }; + + if let Some(script_node) = script_node { + Ok(EntityNode::Group { + name: entity_name, + children: vec![entity_node, script_node], + collapsed: false, + }) + } else { + Ok(entity_node) + } + } else { + Err(anyhow::anyhow!("Failed to get entity components")) + } + } else { + Err(anyhow::anyhow!("Failed to query entity {:?}", entity_id)) + } +} + +pub fn attach_script_to_entity( + world: &mut 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)); + } + + log::info!("Successfully attached script to entity {:?}", entity_id); + Ok(()) } @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true license-file.workspace = true repository.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] eucalyptus-core = { path = "../eucalyptus-core", features = ["editor"] } @@ -33,6 +33,7 @@ winit.workspace = true clap.workspace = true walkdir.workspace = true zip.workspace = true +tokio.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -0,0 +1,3 @@ +# eucalyptus-editor + +The primary editor used to make games on the dropbear engine. @@ -73,7 +73,7 @@ pub struct Editor { } impl Editor { - pub fn new() -> Self { + pub async fn new() -> Self { let tabs = vec![EditorTab::Viewport]; let mut dock_state = DockState::new(tabs); @@ -125,7 +125,7 @@ impl Editor { viewport_mode: ViewportMode::None, signal: Signal::None, undo_stack: Vec::new(), - script_manager: ScriptManager::new().unwrap(), + script_manager: ScriptManager::new().await.unwrap(), editor_state: EditorState::Editing, gizmo_mode: EnumSet::empty(), play_mode_backup: None, @@ -761,12 +761,6 @@ pub enum Signal { LogEntities, } -impl Default for Editor { - fn default() -> Self { - Editor::new() - } -} - #[derive(Debug)] pub enum ComponentType { Script(ScriptComponent), @@ -16,7 +16,8 @@ pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { author: "4tkbytes", }; -fn main() -> anyhow::Result<()> { +#[tokio::main] +async fn main() -> anyhow::Result<()> { #[cfg(target_os = "android")] compile_error!("The `editor` feature is not supported on Android. If you are attempting\ to use the Eucalyptus editor on Android, please don't. Instead, use the `data-only` feature\ @@ -112,9 +113,11 @@ fn main() -> anyhow::Result<()> { app_info: APP_INFO, }; + let main_menu = Rc::new(RefCell::new(crate::menu::MainMenu::new())); + let editor = Rc::new(RefCell::new(crate::editor::Editor::new().await)); + let _app = dropbear_engine::run_app!(config, |mut scene_manager, mut input_manager| { - let main_menu = Rc::new(RefCell::new(crate::menu::MainMenu::new())); - let editor = Rc::new(RefCell::new(crate::editor::Editor::new())); + scene::add_scene_with_input( &mut scene_manager, @@ -133,6 +136,7 @@ fn main() -> anyhow::Result<()> { (scene_manager, input_manager) }) + .await .unwrap(); } _ => unreachable!(),