tirbofish/dropbear · commit
efa6db073ebd7b96c6b38f8d71e62972b5b1fcde
some build stuff
Signature present but could not be verified.
Unverified
@@ -1,2 +0,0 @@ -GITHUB_USERNAME=your-github-username -GITHUB_TOKEN=your-personal-access-token @@ -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"]) +// } +// } +//} @@ -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(); @@ -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"] @@ -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 } => { @@ -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 @@ -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, } @@ -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, @@ -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(()) } _ => { @@ -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 => { @@ -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(")?; @@ -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, Binary files a/resources/templates/gradle_template.tar.gz and b/resources/templates/gradle_template.tar.gz differ