kitgit

tirbofish/dropbear · commit

efa6db073ebd7b96c6b38f8d71e62972b5b1fcde

some build stuff

Unverified

tk <4tkbytes@pm.me> · 2025-10-11 03:20

view full diff

diff --git a/.env-example b/.env-example
deleted file mode 100644
index 147c983..0000000
--- a/.env-example
+++ /dev/null
@@ -1,2 +0,0 @@
-GITHUB_USERNAME=your-github-username
-GITHUB_TOKEN=your-personal-access-token
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index f6783d4..6965971 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -146,33 +146,34 @@ tasks.register<JavaCompile>("generateJniHeaders") {
     dependsOn("compileKotlinJvm")
 }
 
-publishing {
-    repositories {
-        maven {
-          name = "GitHubPackages"
-            url = uri("https://maven.pkg.github.com/4tkbytes/dropbear")
-            
-            val isPublishing = gradle.startParameter.taskNames.any { 
-                it.contains("publish", ignoreCase = true) 
-            }
-            
-            if (isPublishing) {
-                val dotenv = io.github.cdimascio.dotenv.dotenv()
-                credentials {
-                  username = dotenv["GITHUB_USERNAME"]
-                  password = dotenv["GITHUB_TOKEN"]
-              }
-          }
-        }
-    }
-
-    publications {
-        create<MavenPublication>("release") {
-            groupId = group as String?
-            artifactId = rootProject.name
-            version = version
-
-            from(components["kotlin"])
-        }
-    }
-}
+// switched to jitpack now :)
+//publishing {
+//    repositories {
+//        maven {
+//          name = "GitHubPackages"
+//            url = uri("https://maven.pkg.github.com/4tkbytes/dropbear")
+//
+//            val isPublishing = gradle.startParameter.taskNames.any {
+//                it.contains("publish", ignoreCase = true)
+//            }
+//
+//            if (isPublishing) {
+//                val dotenv = io.github.cdimascio.dotenv.dotenv()
+//                credentials {
+//                  username = dotenv["GITHUB_USERNAME"]
+//                  password = dotenv["GITHUB_TOKEN"]
+//              }
+//          }
+//        }
+//    }
+//
+//    publications {
+//        create<MavenPublication>("release") {
+//            groupId = group as String?
+//            artifactId = rootProject.name
+//            version = version
+//
+//            from(components["kotlin"])
+//        }
+//    }
+//}
diff --git a/dropbear_future-queue/src/lib.rs b/dropbear_future-queue/src/lib.rs
index 99cc456..8a228f2 100644
--- a/dropbear_future-queue/src/lib.rs
+++ b/dropbear_future-queue/src/lib.rs
@@ -44,6 +44,7 @@ use std::pin::Pin;
 use std::rc::Rc;
 use std::sync::Arc;
 use tokio::sync::oneshot;
+use tokio::task::JoinHandle;
 
 /// A type used for a future.
 ///
@@ -64,6 +65,7 @@ pub enum FutureStatus {
     NotPolled,
     CurrentlyPolling,
     Completed,
+    Cancelled,
 }
 
 /// A handle to the future task
@@ -77,6 +79,7 @@ struct HandleEntry {
     receiver: Option<ResultReceiver>,
     status: FutureStatus,
     cached_result: Option<AnyResult>,
+    task_handle: Option<JoinHandle<()>>,
 }
 
 /// A queue for polling futures. It is stored in here until [`FutureQueue::poll`] is run.
@@ -118,6 +121,7 @@ impl FutureQueue {
             receiver: Some(receiver),
             status: FutureStatus::NotPolled,
             cached_result: None,
+            task_handle: None,
         };
 
         self.handle_registry.lock().insert(id, entry);
@@ -165,14 +169,21 @@ impl FutureQueue {
                 }
             }
 
-            futures_to_spawn.push(future);
+            futures_to_spawn.push((id, future));
         }
 
         drop(queue);
 
-        for future in futures_to_spawn {
+        let registry = self.handle_registry.clone();
+        for (id, future) in futures_to_spawn {
             log("Spawning future with tokio");
-            tokio::spawn(future);
+            let handle = tokio::spawn(future);
+            
+            // Store the task handle
+            let mut reg = registry.lock();
+            if let Some(entry) = reg.get_mut(&id) {
+                entry.task_handle = Some(handle);
+            }
         }
     }
 
@@ -290,6 +301,36 @@ impl FutureQueue {
         registry.get(handle).map(|entry| entry.status.clone())
     }
 
+    /// Cancels a running future by its handle.
+    ///
+    /// This will abort the task if it's currently running, mark it as cancelled,
+    /// and clean up associated resources. Returns true if the task was cancelled,
+    /// false if the handle was not found or already completed.
+    pub fn cancel(&self, handle: &FutureHandle) -> bool {
+        let mut registry = self.handle_registry.lock();
+        
+        if let Some(entry) = registry.get_mut(handle) {
+            if matches!(entry.status, FutureStatus::Completed | FutureStatus::Cancelled) {
+                return false;
+            }
+            
+            if let Some(task_handle) = entry.task_handle.take() {
+                task_handle.abort();
+                log(format!("Aborted task for handle: {:?}", handle));
+            }
+            
+            entry.status = FutureStatus::Cancelled;
+            entry.receiver = None;
+            entry.cached_result = None;
+            
+            log(format!("Cancelled handle: {:?}", handle));
+            true
+        } else {
+            log(format!("Handle not found for cancellation: {:?}", handle));
+            false
+        }
+    }
+
     /// Cleans up any completed handles and removes them from the registry.
     ///
     /// You can do this manually, however this is typically done at the end of the frame.
@@ -298,7 +339,7 @@ impl FutureQueue {
         let completed_ids: Vec<FutureHandle> = registry
             .iter()
             .filter_map(|(&id, entry)| {
-                matches!(entry.status, FutureStatus::Completed).then_some(id)
+                matches!(entry.status, FutureStatus::Completed | FutureStatus::Cancelled).then_some(id)
             })
             .collect();
 
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index 9df9909..7951578 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -33,6 +33,7 @@ crossbeam-channel = "0.5.15"
 interprocess = "2.2"
 serde_json = "1.0.143"
 libloading = "0.8.9"
+async-process = "2.5"
 
 [features]
 editor = ["jvm"]
diff --git a/eucalyptus-core/src/scripting.rs b/eucalyptus-core/src/scripting.rs
index b3cf00a..1a0fbbd 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -7,12 +7,16 @@ use crate::states::{EntityNode, PROJECT, SOURCE, ScriptComponent};
 use dropbear_engine::entity::{AdoptedEntity, Transform};
 use hecs::{Entity, World};
 use libloading::Library;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::{collections::HashMap, fs};
+use anyhow::Context;
+use crossbeam_channel::Sender;
+use tokio::io::{AsyncBufReadExt, BufReader};
+use tokio::process::Command;
 
 pub const TEMPLATE_SCRIPT: &str = include_str!("../../resources/scripting/kotlin/Template.kt");
 
-#[derive(Default)]
+#[derive(Default, Clone)]
 pub enum ScriptTarget {
     #[default]
     None,
@@ -24,6 +28,14 @@ pub enum ScriptTarget {
     },
 }
 
+#[derive(Debug, Clone)]
+pub enum BuildStatus {
+    Started,
+    Building(String),
+    Completed,
+    Failed(String),
+}
+
 pub struct ScriptManager {
     jvm: Option<JavaContext>,
     library: Option<Library>,
@@ -41,12 +53,136 @@ impl ScriptManager {
         })
     }
 
+    pub async fn build_jvm(
+        &mut self,
+        project_root: impl AsRef<Path>,
+        status_sender: Sender<BuildStatus>
+    ) -> anyhow::Result<PathBuf> {
+        let project_root = project_root.as_ref();
+
+        if !project_root.exists() {
+            let err = format!("Project root does not exist: {:?}", project_root);
+            let _ = status_sender.send(BuildStatus::Failed(err.clone()));
+            return Err(anyhow::anyhow!(err));
+        }
+
+        if !(project_root.join("build.gradle").exists() || project_root.join("build.gradle.kts").exists()) {
+            let err = format!("No Gradle build script found in: {:?}", project_root);
+            let _ = status_sender.send(BuildStatus::Failed(err.clone()));
+            return Err(anyhow::anyhow!(err));
+        }
+
+        let _ = status_sender.send(BuildStatus::Started);
+
+        // Determine the gradle command to use
+        let gradle_cmd = if cfg!(target_os = "windows") {
+            // On Windows, prefer gradlew.bat if it exists, otherwise use gradle
+            let gradlew = project_root.join("gradlew.bat");
+            if gradlew.exists() {
+                gradlew.to_string_lossy().to_string()
+            } else {
+                "gradle.bat".to_string()
+            }
+        } else {
+            // On Unix-like systems, prefer ./gradlew if it exists
+            let gradlew = project_root.join("gradlew");
+            if gradlew.exists() {
+                "./gradlew".to_string()
+            } else {
+                "gradle".to_string()
+            }
+        };
+
+        let _ = status_sender.send(BuildStatus::Building(format!("Running: {}", gradle_cmd)));
+
+        let mut child = Command::new(&gradle_cmd)
+            .current_dir(project_root)
+            .args(&["--console=plain", "assemble"])
+            .stdout(std::process::Stdio::piped())
+            .stderr(std::process::Stdio::piped())
+            .spawn()
+            .context(format!("Failed to spawn `{} assemble`", gradle_cmd))?;
+
+        let stdout = child.stdout.take().expect("Stdout was piped");
+        let stderr = child.stderr.take().expect("Stderr was piped");
+
+        let tx_out = status_sender.clone();
+        let stdout_task = tokio::spawn(async move {
+            let mut reader = BufReader::new(stdout).lines();
+            while let Ok(Some(line)) = reader.next_line().await {
+                // if let Ok(line) = line {
+                println!("stdout: {}", line);
+                    let _ = tx_out.send(BuildStatus::Building(line));
+                // }
+            }
+        });
+
+        let tx_err = status_sender.clone();
+        let stderr_task = tokio::spawn(async move {
+            let mut reader = BufReader::new(stderr).lines();
+            while let Ok(Some(line)) = reader.next_line().await {
+                // if let Ok(line) = line {
+                println!("stderr: {}", line);
+                    let _ = tx_err.send(BuildStatus::Building(line));
+                // }
+            }
+        });
+
+        let status = child.wait().await.context("Failed to wait for gradle process")?;
+
+        let _ = tokio::join!(stdout_task, stderr_task);
+
+        if !status.success() {
+            let code = status.code().unwrap_or(-1);
+            let err_msg = format!("Gradle build failed with exit code {}", code);
+            let _ = status_sender.send(BuildStatus::Failed(err_msg.clone()));
+            return Err(anyhow::anyhow!(err_msg));
+        }
+
+        let libs_dir = project_root.join("build").join("libs");
+        if !libs_dir.exists() {
+            let err = "Build succeeded but 'build/libs' directory is missing".to_string();
+            let _ = status_sender.send(BuildStatus::Failed(err.clone()));
+            return Err(anyhow::anyhow!(err));
+        }
+
+        let jar_files: Vec<PathBuf> = std::fs::read_dir(&libs_dir)
+            .context("Failed to read 'build/libs'")?
+            .filter_map(|entry| entry.ok().map(|e| e.path()))
+            .filter(|path| {
+                path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar"))
+                    && !path.file_name().unwrap_or_default().to_string_lossy().contains("-sources")
+                    && !path.file_name().unwrap_or_default().to_string_lossy().contains("-javadoc")
+            })
+            .collect();
+
+        if jar_files.is_empty() {
+            let err = "No JAR artifact found in 'build/libs' (looked for non-source, non-javadoc JARs)".to_string();
+            let _ = status_sender.send(BuildStatus::Failed(err.clone()));
+            return Err(anyhow::anyhow!(err));
+        }
+
+        let jar_path = jar_files
+            .into_iter()
+            .max_by_key(|path| {
+                std::fs::metadata(path)
+                    .and_then(|m| Ok(m.len()))
+                    .unwrap_or(0)
+            })
+            .unwrap();
+
+        let _ = status_sender.send(BuildStatus::Completed);
+        Ok(jar_path)
+    }
+
     pub fn init_script(
         &mut self,
         entity_tag_database: HashMap<String, Vec<Entity>>,
         target: ScriptTarget,
     ) -> anyhow::Result<()> {
         self.entity_tag_database = entity_tag_database;
+        let target_clone = target.clone();
+        self.script_target = target_clone;
 
         match target {
             ScriptTarget::JVM { library_path } => {
diff --git a/eucalyptus-editor/Cargo.toml b/eucalyptus-editor/Cargo.toml
index 7af3909..fee22d2 100644
--- a/eucalyptus-editor/Cargo.toml
+++ b/eucalyptus-editor/Cargo.toml
@@ -38,6 +38,8 @@ tokio.workspace = true
 reqwest.workspace = true
 tar.workspace = true
 flate2.workspace = true
+crossbeam-channel.workspace = true
+pollster = "0.4.0"
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 71c7194..f43ef2a 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -29,7 +29,7 @@ use dropbear_engine::{
 use egui::{self, Context};
 use egui_dock_fork::{DockArea, DockState, NodeIndex, Style};
 use eucalyptus_core::input::InputState;
-use eucalyptus_core::scripting::ScriptManager;
+use eucalyptus_core::scripting::{BuildStatus, ScriptManager};
 use eucalyptus_core::states::{
     CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, PROJECT, SCENES,
     SceneEntity, ScriptComponent,
@@ -80,7 +80,7 @@ pub struct Editor {
     pub(crate) editor_state: EditorState,
     gizmo_mode: EnumSet<GizmoMode>,
 
-    pub(crate) script_manager: ScriptManager,
+    pub(crate) script_manager: Arc<tokio::sync::Mutex<ScriptManager>>,
     play_mode_backup: Option<PlayModeBackup>,
 
     /// State of the input
@@ -102,6 +102,15 @@ pub struct Editor {
     pub(crate) alt_pending_spawn_queue: Vec<FutureHandle>,
     world_receiver: Option<oneshot::Receiver<hecs::World>>,
 
+    // building
+    pub progress_rx: Option<crossbeam_channel::Receiver<BuildStatus>>,
+    pub handle_created: Option<FutureHandle>,
+    pub build_logs: Vec<String>,
+    pub build_progress: f32,
+    pub show_build_window: bool,
+    pub last_build_error: Option<String>,
+    pub show_build_error_window: bool,
+
     dock_state_shared: Option<Arc<Mutex<DockState<EditorTab>>>>,
 }
 
@@ -165,7 +174,7 @@ impl Editor {
             viewport_mode: ViewportMode::None,
             signal: Signal::None,
             undo_stack: Vec::new(),
-            script_manager: ScriptManager::new()?,
+            script_manager: Arc::new(tokio::sync::Mutex::new(ScriptManager::new()?)),
             editor_state: EditorState::Editing,
             gizmo_mode: EnumSet::empty(),
             play_mode_backup: None,
@@ -179,6 +188,13 @@ impl Editor {
             world_load_handle: None,
             alt_pending_spawn_queue: vec![],
             world_receiver: None,
+            progress_rx: None,
+            handle_created: None,
+            build_logs: Vec::new(),
+            build_progress: 0.0,
+            show_build_window: false,
+            last_build_error: None,
+            show_build_error_window: false,
             dock_state_shared: None,
         })
     }
@@ -1200,6 +1216,7 @@ pub struct PlayModeBackup {
 
 pub enum EditorState {
     Editing,
+    Building,
     Playing,
 }
 
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 61959e1..c7fe70a 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -128,8 +128,8 @@ impl Scene for Editor {
                 log_once::warn_once!("Script entities is empty");
             }
 
-            for (entity_id, script_name) in script_entities {
-                if let Err(e) = self.script_manager.update_script(
+            for (entity_id, _) in script_entities {
+                if let Err(e) = pollster::block_on(self.script_manager.lock()).update_script(
                     entity_id,
                     &mut self.world,
                     &self.input_state,
diff --git a/eucalyptus-editor/src/menu.rs b/eucalyptus-editor/src/menu.rs
index cf4c140..b4638aa 100644
--- a/eucalyptus-editor/src/menu.rs
+++ b/eucalyptus-editor/src/menu.rs
@@ -1,4 +1,4 @@
-use anyhow::anyhow;
+use anyhow::{anyhow, Context};
 use dropbear_engine::{
     future::{FutureHandle, FutureQueue},
     graphics::RenderContext,
@@ -8,15 +8,11 @@ use dropbear_engine::{
 use egui::{self, FontId, Frame, RichText};
 use egui_toast_fork::{ToastOptions, Toasts};
 use eucalyptus_core::states::{PROJECT, ProjectConfig};
-use flate2::read::GzDecoder;
 use git2::Repository;
 use log::{self, debug};
 use rfd::FileDialog;
-use std::io::Cursor;
-use std::path::Path;
 use std::sync::Arc;
 use std::{fs, path::PathBuf};
-use tar::Archive;
 use tokio::sync::watch;
 use winit::{
     dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode,
@@ -77,9 +73,9 @@ impl MainMenu {
         let handle = queue.push(async move {
             let mut errors = Vec::new();
             let folders = [
-                ("git", 0.1, "Initialising git repository..."),
-                ("gradle", 0.2, "Unpacking gradle template..."),
-                ("setting_config", 0.25, "Setting gradle config..."),
+                ("gradle", 0.1, "Unpacking gradle template..."),
+                ("setting_config", 0.2, "Setting gradle config..."),
+                ("git", 0.3, "Initialising git repository..."),
                 ("resources/models", 0.3, "Creating models folder..."),
                 ("resources/shaders", 0.4, "Creating shaders folder..."),
                 ("resources/textures", 0.5, "Creating textures folder..."),
@@ -143,47 +139,36 @@ impl MainMenu {
 
                             let updated_gradle_content = gradle_content
                                 .replace("domain", project_domain.clone().as_str())
-                                .replace("project", &project_name.to_lowercase());
+                                .replace("projectExample", &project_name.to_lowercase());
 
                             fs::write(&build_gradle_path, updated_gradle_content)?;
 
                             Ok(())
                         }
                         "gradle" => {
-                            log::debug!("Unpacking gradle template");
-                            let gradle_template =
-                                include_bytes!("../../resources/templates/gradle_template.tar.gz");
-
-                            let temp_extract_dir = path.join("temp_extract_dir");
-
-                            let cursor = Cursor::new(gradle_template);
-                            let gz_decoder = GzDecoder::new(cursor);
-                            let mut archive = Archive::new(gz_decoder);
-                            archive.unpack(&temp_extract_dir)?;
-
-                            let temp_path = Path::new(&temp_extract_dir);
-                            let mut entries = fs::read_dir(temp_path)?;
-                            let top_dir = entries
-                                .next()
-                                .ok_or("No entries found in archive")
-                                .map_err(|e| anyhow::anyhow!(e))??;
-
-                            if !top_dir.file_type()?.is_dir() {
-                                return Err(anyhow::anyhow!("Top-level entry is not a directory"));
-                            }
+                            log::debug!("Cloning gradle template from GitHub");
+                            let url = "https://github.com/4tkbytes/eucalyptus-gradle-template";
+
+                            fs::create_dir_all(path).context("Failed to create project directory")?;
 
-                            let top_dir_path = top_dir.path();
-                            println!("Found top-level directory: {:?}", top_dir_path);
+                            let temp_clone_path = path.with_file_name(format!("{}.clone_tmp", path.file_name().unwrap_or_default().to_str().unwrap_or_default()));
 
-                            fs::create_dir_all(path)?;
-                            for entry in fs::read_dir(top_dir_path)? {
+                            Repository::clone(url, &temp_clone_path)?;
+
+                            for entry in fs::read_dir(&temp_clone_path)? {
                                 let entry = entry?;
-                                let dest_path = Path::new(path).join(entry.file_name());
+                                let file_name = entry.file_name();
+                                if file_name == ".git" {
+                                    continue;
+                                }
+                                let dest_path = path.join(file_name);
                                 fs::rename(entry.path(), dest_path)?;
                             }
 
-                            fs::remove_dir_all(&temp_extract_dir)?;
+                            fs::remove_dir_all(&temp_clone_path)
+                                .context("Failed to remove temporary clone directory")?;
 
+                            log::debug!("Template cloned and .git removed successfully");
                             Ok(())
                         }
                         _ => {
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index 2cb9ac6..834ba4a 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -8,12 +8,13 @@ use dropbear_engine::lighting::{Light, LightComponent};
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
 use egui::{Align2, Image};
 use eucalyptus_core::camera::{CameraComponent, CameraType};
-use eucalyptus_core::scripting::ScriptTarget;
+use eucalyptus_core::scripting::{BuildStatus, ScriptTarget};
 use eucalyptus_core::spawn::{PendingSpawn, push_pending_spawn};
-use eucalyptus_core::states::{ModelProperties, ScriptComponent, Value};
+use eucalyptus_core::states::{ModelProperties, ScriptComponent, Value, PROJECT};
 use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console};
 use hecs::Entity;
 use std::collections::HashMap;
+use std::path::PathBuf;
 use std::sync::Arc;
 
 pub trait SignalController {
@@ -103,87 +104,326 @@ impl SignalController for Editor {
                     self.signal = Signal::None;
                     return Err(anyhow::anyhow!("Unable to play: already in playing mode"));
                 }
-                let has_player_camera_target = self
-                    .world
-                    .query::<(&Camera, &CameraComponent)>()
-                    .iter()
-                    .any(|(_, (_, comp))| comp.starting_camera);
-
-                if has_player_camera_target {
-                    if let Err(e) = self.create_backup() {
-                        self.signal = Signal::None;
-                        fatal!("Failed to create play mode backup: {}", e);
-                    }
 
-                    self.editor_state = EditorState::Playing;
+                if matches!(self.editor_state, EditorState::Editing) {
+                    log::debug!("Starting build process");
+                    let (tx, rx) = crossbeam_channel::unbounded();
+                    self.progress_rx = Some(rx);
+                    
+                    // Clear previous build logs and reset progress
+                    self.build_logs.clear();
+                    self.build_progress = 0.0;
+                    self.show_build_window = true;
+                    self.last_build_error = None; // Clear previous error
+
+                    let project_root = {
+                        let cfg = PROJECT.read();
+                        cfg.project_path.clone()
+                    };
+
+                    let script_manager = self.script_manager.clone();
+                    let project_root = project_root.to_path_buf();
+                    let status_tx = tx.clone();
+
+                    let handle = graphics.future_queue.push(async move {
+                        let mut manager = script_manager.lock().await;
+                        manager.build_jvm(project_root, status_tx).await
+                    });
+
+                    log::debug!("Pushed future to future_queue, received handle: {:?}", handle);
 
-                    self.switch_to_player_camera();
+                    self.handle_created = Some(handle);
 
-                    let mut script_entities = Vec::new();
-                    {
-                        for (entity_id, script) in self.world.query::<&ScriptComponent>().iter() {
-                            script_entities.push((entity_id, script.clone()));
+                    self.editor_state = EditorState::Building;
+                    log::debug!("Set editor state to EditorState::Building");
+                }
+
+                if matches!(self.editor_state, EditorState::Building) {
+                    let mut should_stop_building = false;
+                    
+                    if let Some(rx) = &self.progress_rx {
+                        while let Ok(status) = rx.try_recv() {
+                            match status {
+                                BuildStatus::Started => {
+                                    self.build_logs.push("Build started...".to_string());
+                                    self.build_progress = 0.1;
+                                    log::info!("Build started");
+                                }
+                                BuildStatus::Building(msg) => {
+                                    log::info!("[BUILD] {}", msg);
+                                    self.build_logs.push(msg.clone());
+                                    self.build_progress = (self.build_progress + 0.01).min(0.9);
+                                }
+                                BuildStatus::Completed => {
+                                    self.build_logs.push("Build completed successfully!".to_string());
+                                    self.build_progress = 1.0;
+                                    log::info!("Build completed");
+                                }
+                                BuildStatus::Failed(e) => {
+                                    let error_msg = format!("Build failed: {}", e);
+                                    self.build_logs.push(error_msg.clone());
+                                    
+                                    self.build_progress = 0.0;
+                                    fatal!("Failed to build gradle: {}", e);
+                                    
+                                    should_stop_building = true;
+                                }
+                            }
                         }
                     }
+                    
+                    // Handle build failure after the borrow is done
+                    if should_stop_building {
+                        self.last_build_error = Some(self.build_logs.join("\n"));
+                        self.signal = Signal::None;
+                        self.show_build_window = false;
+                        self.show_build_error_window = true;
+                        self.handle_created = None;
+                        self.progress_rx = None;
+                        self.editor_state = EditorState::Editing;
+                    }
 
-                    let mut etag: HashMap<String, Vec<Entity>> = HashMap::new();
-                    for (entity_id, script) in script_entities {
-                        for tag in script.tags {
-                            if etag.contains_key(&tag) {
-                                etag.get_mut(&tag).unwrap().push(entity_id);
-                            } else {
-                                etag.insert(tag.clone(), vec![entity_id]);
+                    if self.show_build_window {
+                        let mut window_open = true;
+                        let mut cancel_clicked = false;
+                        
+                        egui::Window::new("Building Project")
+                            .collapsible(false)
+                            .resizable(true)
+                            .default_width(600.0)
+                            .default_height(400.0)
+                            .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
+                            .open(&mut window_open)
+                            .show(&graphics.get_egui_context(), |ui| {
+                                ui.vertical_centered(|ui| {
+                                    ui.heading("Gradle Build Progress");
+                                    ui.add_space(10.0);
+                                    
+                                    let progress_bar = egui::ProgressBar::new(self.build_progress)
+                                        .show_percentage()
+                                        .animate(true);
+                                    ui.add(progress_bar);
+                                    
+                                    ui.add_space(15.0);
+                                    ui.separator();
+                                    ui.add_space(5.0);
+                                    
+                                    ui.heading("Build Log");
+                                    ui.add_space(5.0);
+                                    
+                                    egui::ScrollArea::vertical()
+                                        .auto_shrink([false, false])
+                                        .stick_to_bottom(true)
+                                        .show(ui, |ui| {
+                                            for log_line in &self.build_logs {
+                                                ui.label(
+                                                    egui::RichText::new(log_line)
+                                                        .family(egui::FontFamily::Monospace)
+                                                        .size(12.0)
+                                                );
+                                            }
+                                            
+                                            if !self.build_logs.is_empty() {
+                                                ui.add_space(10.0);
+                                                ui.label(
+                                                    egui::RichText::new(
+                                                        format!("Total log entries: {}", self.build_logs.len())
+                                                    )
+                                                    .italics()
+                                                    .color(egui::Color32::GRAY)
+                                                );
+                                            }
+                                        });
+                                    
+                                    ui.add_space(10.0);
+                                    
+                                    if ui.button("Cancel Build").clicked() {
+                                        cancel_clicked = true;
+                                    }
+                                });
+                            });
+                        
+                        if !window_open || cancel_clicked {
+                            if let Some(handle) = self.handle_created {
+                                log::info!("Cancelling build task due to window close");
+                                graphics.future_queue.cancel(&handle);
                             }
+                            
+                            self.show_build_window = false;
+                            self.handle_created = None;
+                            self.progress_rx = None;
+                            self.editor_state = EditorState::Editing;
+                            self.signal = Signal::None;
                         }
                     }
 
-                    let etag_clone = etag.clone();
+                    if let Some(handle) = self.handle_created {
+                        if let Some(result) = graphics
+                            .future_queue
+                            .exchange_as::<anyhow::Result<PathBuf>>(&handle)
+                        {
+                            log::debug!("Build future completed, processing result");
+                            self.handle_created = None;
+                            self.progress_rx = None;
+                            
+                            match result {
+                                Ok(path) => {
+                                    log::debug!("Path is valid, JAR location as {}", path.display());
+                                    success!("Build completed successfully!");
+                                    self.show_build_window = false; // Close the build window
+                                    
+                                    let has_player_camera_target = self
+                                        .world
+                                        .query::<(&Camera, &CameraComponent)>()
+                                        .iter()
+                                        .any(|(_, (_, comp))| comp.starting_camera);
 
-                    // todo: get the library name working
-                    if let Err(e) = self
-                        .script_manager
-                        .init_script(etag_clone, ScriptTarget::None)
-                    {
-                        fatal!("Failed to ready script manager interface because {}", e);
-                        self.signal = Signal::StopPlaying;
-                        return Err(anyhow::anyhow!(e));
-                    }
+                                    if has_player_camera_target {
+                                        if let Err(e) = self.create_backup() {
+                                            self.signal = Signal::None;
+                                            fatal!("Failed to create play mode backup: {}", e);
+                                        }
 
-                    for (tag, entities) in &etag {
-                        log::debug!(
+                                        self.editor_state = EditorState::Playing;
+
+                                        self.switch_to_player_camera();
+
+                                        let mut script_entities = Vec::new();
+                                        {
+                                            for (entity_id, script) in self.world.query::<&ScriptComponent>().iter() {
+                                                script_entities.push((entity_id, script.clone()));
+                                            }
+                                        }
+
+                                        let mut etag: HashMap<String, Vec<Entity>> = HashMap::new();
+                                        for (entity_id, script) in script_entities {
+                                            for tag in script.tags {
+                                                if etag.contains_key(&tag) {
+                                                    etag.get_mut(&tag).unwrap().push(entity_id);
+                                                } else {
+                                                    etag.insert(tag.clone(), vec![entity_id]);
+                                                }
+                                            }
+                                        }
+
+                                        let etag_clone = etag.clone();
+
+                                        if let Err(e) = pollster::block_on(self
+                                            .script_manager
+                                            .lock())
+                                            .init_script(etag_clone, ScriptTarget::JVM { library_path: path })
+                                        {
+                                            fatal!("Failed to ready script manager interface because {}", e);
+                                            self.signal = Signal::StopPlaying;
+                                            return Err(anyhow::anyhow!(e));
+                                        }
+
+                                        for (tag, entities) in &etag {
+                                            log::debug!(
                             "Initialising script for tag {:?} with entities: {:?}",
                             tag,
                             entities
                         );
 
-                        for e in entities {
-                            if let Err(e) = self.script_manager.load_script(
-                                *e,
-                                tag.clone(),
-                                &mut self.world,
-                                &self.input_state,
-                            ) {
-                                fatal!(
+                                            for e in entities {
+                                                if let Err(e) = pollster::block_on(self.script_manager.lock())
+                                                    .load_script(
+                                                        *e,
+                                                        tag.clone(),
+                                                        &mut self.world,
+                                                        &self.input_state,
+                                                    ) {
+                                                    fatal!(
                                     "Failed to initialise script for tag {:?} because {}",
                                     tag,
                                     e
                                 );
-                                self.signal = Signal::StopPlaying;
-                                return Err(anyhow::anyhow!(e));
-                            } else {
-                                success_without_console!(
+                                                    self.signal = Signal::StopPlaying;
+                                                    return Err(anyhow::anyhow!(e));
+                                                } else {
+                                                    success_without_console!(
                                     "You are in play mode now! Press Escape to exit"
                                 );
-                                log::info!("You are in play mode now! Press Escape to exit");
+                                                    log::info!("You are in play mode now! Press Escape to exit");
+                                                }
+                                            }
+                                        }
+                                    } else {
+                                        self.signal = Signal::None;
+                                        fatal!("Unable to build: No initial camera set");
+                                    }
+                                }
+                                Err(e) => {
+                                    let error_msg = format!("Build process error: {}", e);
+                                    self.build_logs.push(error_msg.clone());
+                                    self.last_build_error = Some(self.build_logs.join("\n"));
+                                    
+                                    fatal!("Failed to ready script manager interface because {}", e);
+                                    self.signal = Signal::None;
+                                    self.show_build_window = false;
+                                    self.show_build_error_window = true;
+                                    self.editor_state = EditorState::Editing;
+                                }
                             }
                         }
+                    } else {
+                        log::warn!("Handle has not been created, must be a bug");
+                        self.signal = Signal::None;
+                        self.show_build_window = false;
+                        self.editor_state = EditorState::Editing;
                     }
-                } else {
-                    self.signal = Signal::None;
-                    fatal!("Unable to build: No initial camera set");
                 }
 
-                self.signal = Signal::None;
+                if self.show_build_error_window {
+                    if let Some(error_log) = &self.last_build_error {
+                        let mut window_open = true;
+                        let mut close_clicked = false;
+                        
+                        egui::Window::new("Build Error")
+                            .collapsible(true)
+                            .resizable(true)
+                            .default_width(700.0)
+                            .default_height(500.0)
+                            .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
+                            .open(&mut window_open)
+                            .show(&graphics.get_egui_context(), |ui| {
+                                ui.vertical(|ui| {
+                                    ui.heading("Build Failed");
+                                    ui.add_space(5.0);
+                                    ui.label("The Gradle build failed. See the error log below:");
+                                    ui.add_space(10.0);
+                                    ui.separator();
+                                    ui.add_space(10.0);
+                                    
+                                    // Scrollable error log with code styling
+                                    egui::ScrollArea::both()
+                                        .auto_shrink([false, false])
+                                        .show(ui, |ui| {
+                                            ui.add(
+                                                egui::TextEdit::multiline(&mut error_log.as_str())
+                                                    .font(egui::TextStyle::Monospace)
+                                                    .desired_width(f32::INFINITY)
+                                                    .desired_rows(20)
+                                            );
+                                        });
+                                    
+                                    ui.add_space(10.0);
+                                    
+                                    if ui.button("Close").clicked() {
+                                        close_clicked = true;
+                                    }
+                                });
+                            });
+                        
+                        if !window_open || close_clicked {
+                            self.show_build_error_window = false;
+                        }
+                    } else {
+                        self.show_build_error_window = false;
+                    }
+                }
+
+                // self.signal = Signal::None;
                 Ok(())
             }
             Signal::StopPlaying => {
diff --git a/magna-carta/src/generator/jvm.rs b/magna-carta/src/generator/jvm.rs
index d1ca1f8..35f632e 100644
--- a/magna-carta/src/generator/jvm.rs
+++ b/magna-carta/src/generator/jvm.rs
@@ -14,7 +14,6 @@ impl Generator for KotlinJVMGenerator {
         )?;
         writeln!(output)?;
 
-        // Import all discovered classes
         let mut imported_packages = std::collections::HashSet::new();
         for item in manifest.items() {
             if let Some(last_dot) = item.fqcn().rfind('.') {
@@ -28,7 +27,6 @@ impl Generator for KotlinJVMGenerator {
         }
         writeln!(output)?;
 
-        // Simple registry class for JVM
         writeln!(output, "object RunnableRegistry {{")?;
         writeln!(output, "    val SCRIPTS = listOf(")?;
 
diff --git a/magna-carta/src/generator/native.rs b/magna-carta/src/generator/native.rs
index affb52d..a4f2b3e 100644
--- a/magna-carta/src/generator/native.rs
+++ b/magna-carta/src/generator/native.rs
@@ -8,7 +8,6 @@ impl Generator for KotlinNativeGenerator {
     fn generate(&self, manifest: &ScriptManifest) -> anyhow::Result<String> {
         let mut output = String::new();
 
-        // Header
         writeln!(
             output,
             "// Auto-generated by dropbear-engine with the magna-carta parser"
@@ -19,7 +18,6 @@ impl Generator for KotlinNativeGenerator {
         )?;
         writeln!(output)?;
 
-        // Imports
         writeln!(output, "import com.dropbear.DropbearEngine")?;
         writeln!(output, "import com.dropbear.EntityId")?;
         writeln!(output, "import com.dropbear.EntityRef")?;
@@ -32,23 +30,19 @@ impl Generator for KotlinNativeGenerator {
         writeln!(output, "import kotlin.native.CName")?;
         writeln!(output)?;
 
-        // Import all discovered classes
         let mut imported_classes = std::collections::HashSet::new();
         for item in manifest.items() {
-            // Extract package from FQCN to create import
             if let Some(last_dot) = item.fqcn().rfind('.') {
                 let package = &item.fqcn()[..last_dot];
                 let simple_name = &item.fqcn()[last_dot + 1..];
                 writeln!(output, "import {}.{}", package, simple_name)?;
                 imported_classes.insert(simple_name.to_string());
             } else {
-                // No package, just the class name
                 imported_classes.insert(item.simple_name().to_string());
             }
         }
         writeln!(output)?;
 
-        // Script instances registry
         writeln!(
             output,
             "private val scriptInstances: List<ScriptRegistration> by lazy {{"
@@ -57,7 +51,6 @@ impl Generator for KotlinNativeGenerator {
         writeln!(output, "}}")?;
         writeln!(output)?;
 
-        // Metadata class
         writeln!(output, "class Metadata: ProjectScriptingMetadata {{")?;
         writeln!(
             output,
@@ -70,7 +63,6 @@ impl Generator for KotlinNativeGenerator {
                 writeln!(output, ",")?;
             }
 
-            // Format tags
             let tags_str = if item.tags().is_empty() {
                 "listOf()".to_string()
             } else {
@@ -98,7 +90,6 @@ impl Generator for KotlinNativeGenerator {
         writeln!(output, "}}")?;
         writeln!(output)?;
 
-        // Engine factory function
         writeln!(
             output,
             "fun getDropbearEngine(worldPointer: COpaquePointer?, currentEntity: Long?): DropbearEngine {{"
@@ -113,7 +104,6 @@ impl Generator for KotlinNativeGenerator {
         writeln!(output, "}}")?;
         writeln!(output)?;
 
-        // Load function
         writeln!(output, "@CName(\"dropbear_load\")")?;
         writeln!(
             output,
@@ -134,7 +124,6 @@ impl Generator for KotlinNativeGenerator {
         writeln!(output, "}}")?;
         writeln!(output)?;
 
-        // Update function
         writeln!(output, "@CName(\"dropbear_update\")")?;
         writeln!(
             output,
@@ -158,7 +147,6 @@ impl Generator for KotlinNativeGenerator {
         writeln!(output, "}}")?;
         writeln!(output)?;
 
-        // Destroy function
         writeln!(output, "@CName(\"dropbear_destroy\")")?;
         writeln!(
             output,
diff --git a/resources/templates/gradle_template.tar.gz b/resources/templates/gradle_template.tar.gz
index 34148e6..7a4d84c 100644
Binary files a/resources/templates/gradle_template.tar.gz and b/resources/templates/gradle_template.tar.gz differ