tirbofish/dropbear · diff
changing from wasmer to boa_engine and using gleam
Signature present but could not be verified.
Unverified
@@ -42,8 +42,8 @@ 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"} @@ -52,13 +52,15 @@ wgpu = "26" 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" + gltf = "1" thiserror = "2.0" @@ -77,11 +79,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 @@ -24,7 +24,7 @@ parking_lot.workspace = true ron.workspace = true serde.workspace = true winit.workspace = true -wasmer.workspace = true +# wasmer.workspace = true lazy_static.workspace = true rayon.workspace = true reqwest.workspace = true @@ -33,6 +33,7 @@ tar.workspace = true zip.workspace = true tokio.workspace = true futures-util.workspace = true +boa_engine.workspace = true [dev-dependencies] pollster = "*" @@ -0,0 +1 @@ +//! A module used for exposing functions @@ -3,10 +3,9 @@ use std::{ time::{Duration, Instant}, }; -use wasmer::{Function, FunctionEnvMut}; use winit::{event::MouseButton, keyboard::KeyCode, platform::scancode::PhysicalKeyExtScancode}; -use crate::scripting::{DropbearScriptingAPIContext, ScriptableModuleWithEnv}; +use crate::scripting::{DropbearScriptingAPIContext}; #[derive(Clone)] pub struct InputState { @@ -49,42 +48,4 @@ impl InputState { pub fn is_key_pressed(&self, key: KeyCode) -> bool { 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" - } -} +} @@ -4,3 +4,4 @@ pub mod logging; pub mod scripting; pub mod states; pub mod utils; +pub mod dropbear; @@ -1,33 +1,14 @@ -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 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 { @@ -107,20 +88,14 @@ impl DropbearScriptingAPIContext { } 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(); - + pub fn new() -> anyhow::Result<Self> { let result = Self { - store, compiled_scripts: HashMap::new(), - entity_script_data: HashMap::new(), script_context: DropbearScriptingAPIContext::new(), }; @@ -128,9 +103,8 @@ 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()) } @@ -144,31 +118,24 @@ impl ScriptManager { ) -> 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() { + if let Some(script_source) = self.compiled_scripts.get(script_name) { 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)?; + let mut context = Context::default(); + self.expose(&mut context); - 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); - } - } + context.eval(Source::from_bytes(script_source.clone().as_bytes())).map_err(|e| anyhow::anyhow!("{}", e))?; - self.sync_entity_to_memory(entity_id, world, &instance)?; + let load = context.global_object().get(PropertyKey::String(JsString::from_str("load")?), &mut context).map_err(|e| anyhow::anyhow!("{}", e))?; - if let Ok(init_func) = instance.exports.get_function("init") { - init_func.call(&mut self.store, &[])?; + 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)) } @@ -186,80 +153,31 @@ impl ScriptManager { 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.sync_memory_to_entity(entity_id, world, &instance)?; - - self.script_context.clear_context(); - - } else { - 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); - } + let mut context = Context::default(); + self.expose(&mut context); - 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(()) - } + context.eval(Source::from_bytes(module.as_bytes())).map_err(|e| anyhow::anyhow!("{}", e))?; - 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); + let update = context.global_object().get(PropertyKey::String(JsString::from_str("update")?), &mut context).map_err(|e| anyhow::anyhow!("{}", e))?; - Self::update::<Transform>(memory_offset, &memory_view, world, &entity_id)?; - } - Ok(()) - } + 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) + } - 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())?; + 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); } - - 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)?; + pub fn expose(&self, _context: &mut Context) { - Ok(imports) } } @@ -1,540 +0,0 @@ -use std::path::PathBuf; -use std::process::Command; -use app_dirs2::{AppInfo, AppDataType, app_dir}; -use tokio::fs; -use tokio::io::AsyncWriteExt; -use tokio::sync::mpsc::UnboundedSender; -use futures_util::StreamExt; - -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", -}; - -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, -} - -impl GleamScriptCompiler { - pub fn new(project_location: &PathBuf) -> Self { - GleamScriptCompiler { - project_location: project_location.clone(), - } - } - - 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<()> { - 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,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" - } -} @@ -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,544 @@ +use std::path::PathBuf; +use std::process::Command; +use app_dirs2::{AppInfo, AppDataType, app_dir}; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tokio::sync::mpsc::UnboundedSender; +use futures_util::StreamExt; + +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", +}; + +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, +} + +impl GleamScriptCompiler { + 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<()> { + 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,9 +1,10 @@ //! 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; use egui::ProgressBar; use egui::Window; -use eucalyptus_core::scripting::build::{GleamScriptCompiler, InstallStatus}; use tokio::sync::mpsc; pub struct DependencyInstaller { @@ -547,7 +547,7 @@ impl Scene for Editor { script.path.display() ); - let bytes = match tokio::fs::read(&script.path).await { + 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); @@ -601,9 +601,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..."); @@ -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); - } -}