kitgit

tirbofish/dropbear · diff

05feb50 · tk

attempted to improve model loading, required to change from rustyscript to wasm. fml

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index 7cc34e8..81b26bc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/README.md b/README.md
index 7a1359a..e879ff9 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/dropbear-engine/Cargo.toml b/dropbear-engine/Cargo.toml
index a3cbcd5..7d184cf 100644
--- a/dropbear-engine/Cargo.toml
+++ b/dropbear-engine/Cargo.toml
@@ -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
diff --git a/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index ddee3d2..cb37edd 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -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();
diff --git a/dropbear-engine/src/model.rs b/dropbear-engine/src/model.rs
index 2f6bf06..47070c1 100644
--- a/dropbear-engine/src/model.rs
+++ b/dropbear-engine/src/model.rs
@@ -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 {
             ],
         }
     }
-}
+}
\ No newline at end of file
diff --git a/dropbear-engine/src/utils.rs b/dropbear-engine/src/utils.rs
index 3c324f8..38bfea1 100644
--- a/dropbear-engine/src/utils.rs
+++ b/dropbear-engine/src/utils.rs
@@ -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]> {
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index 4c24389..33979a7 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -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 = []
diff --git a/eucalyptus-core/README.md b/eucalyptus-core/README.md
new file mode 100644
index 0000000..735d9b7
--- /dev/null
+++ b/eucalyptus-core/README.md
@@ -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.
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/mod.rs b/eucalyptus-core/src/scripting/mod.rs
index dbfb883..37ffef7 100644
--- a/eucalyptus-core/src/scripting/mod.rs
+++ b/eucalyptus-core/src/scripting/mod.rs
@@ -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(())
 }
\ No newline at end of file
diff --git a/eucalyptus-editor/Cargo.toml b/eucalyptus-editor/Cargo.toml
index 1bf1c2a..9d3729a 100644
--- a/eucalyptus-editor/Cargo.toml
+++ b/eucalyptus-editor/Cargo.toml
@@ -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
diff --git a/eucalyptus-editor/README.md b/eucalyptus-editor/README.md
new file mode 100644
index 0000000..5055465
--- /dev/null
+++ b/eucalyptus-editor/README.md
@@ -0,0 +1,3 @@
+# eucalyptus-editor
+
+The primary editor used to make games on the dropbear engine.
\ No newline at end of file
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index bc5e8ee..a68512d 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -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),
diff --git a/eucalyptus-editor/src/main.rs b/eucalyptus-editor/src/main.rs
index 7af76ce..de4144b 100644
--- a/eucalyptus-editor/src/main.rs
+++ b/eucalyptus-editor/src/main.rs
@@ -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!(),