tirbofish/dropbear · diff
Merge pull request #51
feature: swap on demand
Signature present but could not be verified.
Unverified
@@ -92,7 +92,8 @@ kotlin { sourceSets { commonMain { dependencies { - api("co.touchlab:kermit:2.0.4") +// api("co.touchlab:kermit:2.0.4") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") } } nativeMain { @@ -114,6 +115,10 @@ kotlin { compileTaskProvider.configure { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") + // reminding kotlin that this is a library and not an executable + freeCompilerArgs.add("-Xsuppress-warning=UNUSED_PARAMETER") + freeCompilerArgs.add("-Xsuppress-warning=UNUSED_VARIABLE") + freeCompilerArgs.add("-Xsuppress-warning=UNUSED_PRIVATE_MEMBER") } } } @@ -173,4 +178,21 @@ publishing { } } } -} +} + +tasks.register<Jar>("fatJar") { + archiveClassifier.set("all") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + from(kotlin.jvm().compilations["main"].output) + + configurations.named("jvmRuntimeClasspath").get().forEach { file -> + if (file.name.endsWith(".jar")) { + from(zipTree(file)) + } else { + from(file) + } + } + + manifest {} +} @@ -2,16 +2,23 @@ ## hybrid approach: -- during dev builds, interpret using the JVM -- on production build, build to native. +* during dev builds, interpret using the JVM +* on production build, build to native. to check if its prod or dev, we use a config to build. workflow: -- user creates a new project, which loads the project -- use jvm to watch (because editor is always on desktop) -- on production build, create native build for specific OS + +* user creates a new project, which loads the project +* use jvm to watch (because editor is always on desktop) +* on production build, create native build for specific OS ## required dependencies -- jdk 21 +* jdk 21 + + +## Kotlin REPL +get hot reloading working, then the kotlin repl acts as a way to evaluate .kts files. + + @@ -297,11 +297,11 @@ impl Texture { pub fn new(graphics: Arc<SharedGraphicsContext>, diffuse_bytes: &[u8]) -> Self { let start = Instant::now(); let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); - println!("Loading image to memory: {:?}", start.elapsed()); + log::trace!("Loading image to memory: {:?}", start.elapsed()); let start = Instant::now(); let diffuse_rgba = diffuse_image.to_rgba8(); - println!( + log::trace!( "Converting diffuse image to rgba8 took {:?}", start.elapsed() ); @@ -324,7 +324,7 @@ impl Texture { usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }); - println!("Creating new diffuse texture took {:?}", start.elapsed()); + log::trace!("Creating new diffuse texture took {:?}", start.elapsed()); let start = Instant::now(); graphics.queue.write_texture( @@ -342,7 +342,7 @@ impl Texture { }, texture_size, ); - println!( + log::trace!( "Writing texture to graphics queue took {:?}", start.elapsed() ); @@ -358,7 +358,7 @@ impl Texture { mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); - println!("Creating sampler took {:?}", start.elapsed()); + log::trace!("Creating sampler took {:?}", start.elapsed()); let start = Instant::now(); let diffuse_bind_group = graphics @@ -377,8 +377,8 @@ impl Texture { ], label: Some("texture_bind_group"), }); - println!("Creating diffuse bind group took {:?}", start.elapsed()); - println!("Done creating texture"); + log::trace!("Creating diffuse bind group took {:?}", start.elapsed()); + log::trace!("Done creating texture"); Self { bind_group: Some(diffuse_bind_group), layout: Some(graphics.texture_bind_layout.clone()), @@ -508,7 +508,7 @@ impl Texture { usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - println!( + log::trace!( "Creating new diffuse texture took {:?}", create_start.elapsed() ); @@ -529,7 +529,7 @@ impl Texture { }, texture_size, ); - println!( + log::trace!( "Writing texture to graphics queue took {:?}", write_start.elapsed() ); @@ -544,7 +544,7 @@ impl Texture { mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); - println!("Creating sampler took {:?}", sampler_start.elapsed()); + log::trace!("Creating sampler took {:?}", sampler_start.elapsed()); let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); @@ -565,12 +565,12 @@ impl Texture { ], label: Some("texture_bind_group"), }); - println!( + log::trace!( "Creating diffuse bind group took {:?}", bind_group_start.elapsed() ); - println!("Done creating texture"); + log::trace!("Done creating texture"); Texture { texture: diffuse_texture, @@ -606,7 +606,7 @@ impl Texture { usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - println!( + log::trace!( "Creating new diffuse texture took {:?}", create_start.elapsed() ); @@ -627,7 +627,7 @@ impl Texture { }, texture_size, ); - println!( + log::trace!( "Writing texture to graphics queue took {:?}", write_start.elapsed() ); @@ -642,7 +642,7 @@ impl Texture { mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); - println!("Creating sampler took {:?}", sampler_start.elapsed()); + log::trace!("Creating sampler took {:?}", sampler_start.elapsed()); let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); @@ -663,12 +663,12 @@ impl Texture { ], label: Some("texture_bind_group"), }); - println!( + log::trace!( "Creating diffuse bind group took {:?}", bind_group_start.elapsed() ); - println!("Done creating texture"); + log::trace!("Done creating texture"); Texture { texture: diffuse_texture, @@ -20,7 +20,6 @@ egui_dock-fork.workspace = true glam.workspace = true hecs.workspace = true log.workspace = true -log-once.workspace = true once_cell.workspace = true parking_lot.workspace = true ron.workspace = true @@ -30,10 +29,9 @@ tokio.workspace = true rayon.workspace = true jni.workspace = true crossbeam-channel = "0.5.15" -interprocess = "2.2" -serde_json = "1.0.143" libloading = "0.8.9" -async-process = "2.5" +tokio-util = "0.7" +app_dirs2.workspace = true [features] editor = ["jvm"] @@ -45,5 +43,3 @@ jvm = [] [build-dependencies] anyhow = "1.0" app_dirs2 = "2.5" -reqwest = { version = "0.12", features = ["blocking"] } -zip = "4.3" @@ -6,3 +6,9 @@ pub mod scripting; pub mod spawn; pub mod states; pub mod utils; +pub mod ptr; + +pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { + name: "Eucalyptus", + author: "4tkbytes", +}; @@ -7,6 +7,7 @@ #[cfg(feature = "editor")] use egui::Context; +use std::fmt::{Display, Formatter}; #[cfg(feature = "editor")] use egui_toast_fork::Toasts; @@ -16,6 +17,33 @@ use once_cell::sync::Lazy; #[cfg(feature = "editor")] use parking_lot::Mutex; +pub static LOG_LEVEL: Lazy<Mutex<LogLevel>> = Lazy::new(|| { + Mutex::new(LogLevel::default()) +}); + +#[derive(Default)] +/// LogLevel as shown in LogLevel.kt in the dropbear engine jar library +pub enum LogLevel { + TRACE, + DEBUG, + #[default] + INFO, + WARN, + ERROR +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + LogLevel::TRACE => write!(f, "TRACE"), + LogLevel::DEBUG => write!(f, "DEBUG"), + LogLevel::INFO => write!(f, "INFO"), + LogLevel::WARN => write!(f, "WARN"), + LogLevel::ERROR => write!(f, "ERROR"), + } + } +} + #[cfg(feature = "editor")] pub static GLOBAL_TOASTS: Lazy<Mutex<Toasts>> = Lazy::new(|| { Mutex::new( @@ -0,0 +1,3 @@ +use hecs::World; + +pub type WorldPtr = *mut World; @@ -2,18 +2,16 @@ pub mod jni; pub mod kmp; use crate::input::InputState; -use crate::scripting::jni::{JavaContext, WorldPtr}; -use crate::states::{EntityNode, PROJECT, SOURCE, ScriptComponent}; -use dropbear_engine::entity::{AdoptedEntity, Transform}; -use hecs::{Entity, World}; +use crate::scripting::jni::JavaContext; +use hecs::{Entity}; use libloading::Library; use std::path::{Path, PathBuf}; -use std::{collections::HashMap, fs}; -use ::jni::objects::{GlobalRef, JValue}; +use std::{collections::HashMap}; use anyhow::Context; use crossbeam_channel::Sender; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; +use crate::ptr::WorldPtr; pub const TEMPLATE_SCRIPT: &str = include_str!("../../resources/scripting/kotlin/Template.kt"); @@ -43,10 +41,7 @@ pub struct ScriptManager { script_target: ScriptTarget, entity_tag_database: HashMap<String, Vec<Entity>>, jvm_created: bool, - - #[cfg(feature = "jvm")] - entity_scripts: HashMap<u32, Vec<GlobalRef>>, - // the native version must be stateless, and doesn't have such a thing + lib_path: Option<PathBuf>, } impl ScriptManager { @@ -57,133 +52,10 @@ impl ScriptManager { script_target: Default::default(), entity_tag_database: HashMap::new(), jvm_created: false, - entity_scripts: Default::default(), + lib_path: None, }) } - pub async fn build_jvm( - 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); - - let gradle_cmd = if cfg!(target_os = "windows") { - let gradlew = project_root.join("gradlew.bat"); - if gradlew.exists() { - gradlew.to_string_lossy().to_string() - } else { - "gradle.bat".to_string() - } - } else { - 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", "fatJar"]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - .context(format!("Failed to spawn `{} fatJar`", 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 { - 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 { - 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'".to_string(); - let _ = status_sender.send(BuildStatus::Failed(err.clone())); - return Err(anyhow::anyhow!(err)); - } - - let fat_jar = jar_files - .iter() - .find(|path| { - path.file_name() - .and_then(|n| n.to_str()) - .map_or(false, |name| name.contains("-all")) - }); - - let jar_path = if let Some(fat) = fat_jar { - fat.clone() - } else { - jar_files - .into_iter() - .max_by_key(|path| { - std::fs::metadata(path).map(|m| 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>>, @@ -194,6 +66,8 @@ impl ScriptManager { match &target { ScriptTarget::JVM { library_path } => { + self.lib_path = Some(library_path.clone()); + if !self.jvm_created { let jvm = JavaContext::new(library_path)?; self.jvm = Some(jvm); @@ -201,6 +75,9 @@ impl ScriptManager { log::debug!("Created new JVM instance"); } else { log::debug!("Reusing existing JVM instance"); + if let Some(jvm) = &mut self.jvm { + jvm.jar_path = library_path.clone(); + } } if let Some(jvm) = &mut self.jvm { @@ -215,6 +92,7 @@ impl ScriptManager { self.jvm = None; self.library = None; self.jvm_created = false; + self.lib_path = None; } } @@ -224,20 +102,16 @@ impl ScriptManager { pub fn load_script( &mut self, world: WorldPtr, + _input_state: &InputState, ) -> anyhow::Result<()> { if matches!(self.script_target, ScriptTarget::JVM { .. }) && let Some(jvm) = &mut self.jvm { jvm.init(world)?; - log::debug!("Initialised JVM engine"); - - for (tag, entities) in &self.entity_tag_database { - for entity in entities { - log::debug!("Loading scripts with tag {} for entity {}", tag, entity.id()); - let entity_id = entity.id(); - let scripts = jvm.load_scripts_for_entity(world, entity_id, tag)?; - self.entity_scripts.insert(entity_id, scripts); - } + + for tag in self.entity_tag_database.keys() { + log::trace!("Loading systems for tag: {}", tag); + jvm.load_systems_for_tag(tag)?; } return Ok(()); } @@ -253,193 +127,153 @@ impl ScriptManager { pub fn update_script( &mut self, - world: WorldPtr, + _world: WorldPtr, _input_state: &InputState, dt: f32, ) -> anyhow::Result<()> { if matches!(self.script_target, ScriptTarget::JVM { .. }) && let Some(jvm) = &self.jvm { - let mut env = jvm.jvm.attach_current_thread()?; - - for (tag, entities) in &self.entity_tag_database { - for entity in entities { - let entity_id = entity.id(); - log::debug!("Updating scripts with tag {} for entity {}", tag, entity_id); - - if let Some(scripts) = self.entity_scripts.get(&entity_id) { - let engine_ref = jvm.create_engine_for_entity(world, entity_id)?; - - for script_ref in scripts { - log::debug!("Calling method update"); - env.call_method( - script_ref, - "update", - "(Lcom/dropbear/DropbearEngine;F)V", - &[ - JValue::Object(engine_ref.as_obj()), - JValue::Float(dt), - ], - )?; - } - } - } - } + jvm.update_all_systems(dt)?; return Ok(()); } Err(anyhow::anyhow!("Native implementation not implemented yet")) } - pub fn kill(&mut self) -> anyhow::Result<()> { - log::debug!("Killing JVM"); - self.jvm = None; - self.jvm_created = false; - // self.library.take(); + + pub fn reload(&mut self, world_ptr: WorldPtr) -> anyhow::Result<()> { + if let Some(jvm) = &mut self.jvm { + jvm.reload(world_ptr)? + } Ok(()) } } -pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { - let project_path = { - let project = PROJECT.read(); - project.project_path.clone() - }; +fn get_gradle_command(project_root: impl AsRef<Path>) -> String { + let project_root = project_root.as_ref().to_owned(); + if cfg!(target_os = "windows") { + let gradlew = project_root.join("gradlew.bat"); + if gradlew.exists() { + gradlew.to_string_lossy().to_string() + } else { + "gradle.bat".to_string() + } + } else { + let gradlew = project_root.join("gradlew"); + if gradlew.exists() { + "./gradlew".to_string() + } else { + "gradle".to_string() + } + } +} - let src_path = { - let source_config = SOURCE.read(); - source_config.path.clone() - }; +pub async fn build_jvm( + project_root: impl AsRef<Path>, + status_sender: Sender<BuildStatus> +) -> anyhow::Result<PathBuf> { + let project_root = project_root.as_ref(); - let scripts_dir = src_path; - fs::create_dir_all(&scripts_dir)?; + 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)); + } - let filename = script_path - .file_name() - .ok_or_else(|| anyhow::anyhow!("Invalid script path: no filename"))?; + 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 dest_path = scripts_dir.join(filename); + let _ = status_sender.send(BuildStatus::Started); - if dest_path.exists() { - log::info!( - "Script file already exists at {:?}, returning existing path", - dest_path - ); - return Ok(dest_path); - } + let gradle_cmd = get_gradle_command(project_root); - const MAX_RETRIES: usize = 5; - const RETRY_DELAY_MS: u64 = 60; + let _ = status_sender.send(BuildStatus::Building(format!("Running: {}", gradle_cmd))); - let mut last_err: Option<std::io::Error> = None; - for attempt in 0..=MAX_RETRIES { - match fs::copy(script_path, &dest_path) { - Ok(_) => { - log::info!("Copied script from {:?} to {:?}", script_path, dest_path); - last_err = None; - break; - } - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { - log::warn!( - "Script file already exists at {:?}, continuing anyway", - dest_path - ); - last_err = None; - break; - } - Err(e) => { - if e.raw_os_error() == Some(32) && attempt < MAX_RETRIES { - log::warn!( - "Sharing violation copying script (attempt {}/{}). Retrying in {}ms: {}", - attempt + 1, - MAX_RETRIES, - RETRY_DELAY_MS, - e - ); - std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); - last_err = Some(e); - continue; - } else { - return Err(e.into()); - } - } + let mut child = Command::new(&gradle_cmd) + .current_dir(project_root) + .args(["--console=plain", "fatJar"]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .context(format!("Failed to spawn `{} fatJar`", 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 { + let _ = tx_out.send(BuildStatus::Building(line)); } - } - if let Some(e) = last_err { - return Err(e.into()); + }); + + 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 { + 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 source_config = SOURCE.read(); - source_config.write_to(&project_path)?; + 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)); } - log::info!("Moved script from {:?} to {:?}", script_path, dest_path); - Ok(dest_path) -} + 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(); -pub fn convert_entity_to_group( - world: &mut World, - entity_id: hecs::Entity, -) -> anyhow::Result<EntityNode> { - if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { - if let Some((adopted, _transform)) = query.get() { - let entity_name = adopted.model.label.clone(); - - let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { - Some(EntityNode::Script { - tags: script.tags.clone(), - }) - } else { - None - }; - - let entity_node = EntityNode::Entity { - id: entity_id, - name: entity_name.clone(), - }; - - if let Some(script_node) = script_node { - Ok(EntityNode::Group { - name: entity_name, - children: vec![entity_node, script_node], - collapsed: false, - }) - } else { - Ok(entity_node) - } - } else { - Err(anyhow::anyhow!("Failed to get entity components")) - } - } else { - Err(anyhow::anyhow!("Failed to query entity {:?}", entity_id)) + if jar_files.is_empty() { + let err = "No JAR artifact found in 'build/libs'".to_string(); + let _ = status_sender.send(BuildStatus::Failed(err.clone())); + return Err(anyhow::anyhow!(err)); } -} -pub fn attach_script_to_entity( - world: &mut World, - entity_id: hecs::Entity, - script_component: ScriptComponent, -) -> anyhow::Result<()> { - { - if let Err(e) = world.insert_one(entity_id, script_component) { - return Err(anyhow::anyhow!("Failed to attach script to entity: {}", e)); - } - } + let fat_jar = jar_files + .iter() + .find(|path| { + path.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |name| name.contains("-all")) + }); - log::info!("Successfully attached script to entity {:?}", entity_id); - Ok(()) -} + let jar_path = if let Some(fat) = fat_jar { + fat.clone() + } else { + jar_files + .into_iter() + .max_by_key(|path| { + std::fs::metadata(path).map(|m| m.len()) + .unwrap_or(0) + }) + .unwrap() + }; -// pub enum ScriptAction { -// AttachScript { -// script_path: PathBuf, -// script_name: String, -// }, -// CreateAndAttachScript { -// script_path: PathBuf, -// script_name: String, -// }, -// RemoveScript, -// EditScript, -// } + let _ = status_sender.send(BuildStatus::Completed); + Ok(jar_path) +} @@ -1,29 +1,48 @@ #![allow(non_snake_case)] //! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate +pub mod hotreload; + +use std::fs::ReadDir; use dropbear_engine::entity::AdoptedEntity; use hecs::World; use jni::objects::{GlobalRef, JClass, JString, JValue}; use jni::sys::jlong; use jni::sys::jobject; use jni::{InitArgsBuilder, JNIEnv, JNIVersion, JavaVM}; -use std::path::{Path}; +use std::path::{Path, PathBuf}; +use crate::APP_INFO; +use crate::logging::{LogLevel, LOG_LEVEL}; +use crate::ptr::WorldPtr; -pub type WorldPtr = *mut World; +const LIBRARY_PATH: &[u8] = include_bytes!("../../../build/libs/dropbear-1.0-SNAPSHOT-all.jar"); /// Provides a context for any eucalyptus-core JNI calls and JVM hosting. pub struct JavaContext { pub(crate) jvm: JavaVM, dropbear_engine_class: Option<GlobalRef>, + system_manager_instance: Option<GlobalRef>, + pub(crate) jar_path: PathBuf, } impl JavaContext { pub fn new(jar_path: impl AsRef<Path>) -> anyhow::Result<Self> { + let root = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &APP_INFO)?; + let deps = root.join("dependencies"); + let host_jar_filename = "dropbear-1.0-SNAPSHOT-all.jar"; + let host_jar_path = deps.join(host_jar_filename); + + std::fs::create_dir_all(&deps)?; + std::fs::write(&host_jar_path, LIBRARY_PATH)?; + log::info!("Host library JAR written to {:?}", host_jar_path); + let jvm_args = InitArgsBuilder::new() .version(JNIVersion::V8) - .option(format!("-Djava.class.path={}", jar_path.as_ref().display())) + .option(format!( + "-Djava.class.path={}", + host_jar_path.display() + )) .build()?; - let jvm = JavaVM::new(jvm_args)?; log::info!("Created JVM instance"); @@ -31,6 +50,8 @@ impl JavaContext { Ok(Self { jvm, dropbear_engine_class: None, + system_manager_instance: None, + jar_path: jar_path.as_ref().to_owned(), }) } @@ -41,7 +62,10 @@ impl JavaContext { let _ = old_ref; // drop } - // create native engine first + if let Some(old_ref) = self.system_manager_instance.take() { + let _ = old_ref; // drop + } + log::trace!("Locating \"com/dropbear/ffi/NativeEngine\" class"); let native_engine_class: JClass = env.find_class("com/dropbear/ffi/NativeEngine")?; log::trace!("Creating new instance of NativeEngine"); @@ -56,7 +80,6 @@ impl JavaContext { &[JValue::Long(world_handle)], )?; - // create dropbear engine after let dropbear_class: JClass = env.find_class("com/dropbear/DropbearEngine")?; log::trace!("Creating DropbearEngine constructor with arg (NativeEngine_object)"); let dropbear_obj = env.new_object( @@ -66,152 +89,194 @@ impl JavaContext { )?; log::trace!("Creating new global ref for DropbearEngine"); - let global_ref = env.new_global_ref(dropbear_obj)?; - self.dropbear_engine_class = Some(global_ref); + let engine_global_ref = env.new_global_ref(dropbear_obj)?; + self.dropbear_engine_class = Some(engine_global_ref.clone()); + + let jar_path_jstring = env.new_string(self.jar_path.to_string_lossy())?; + let log_level_str = {LOG_LEVEL.lock().to_string()}; + let log_level_enum_class = env.find_class("com/dropbear/logging/LogLevel")?; + let log_level_enum_instance = env.get_static_field( + log_level_enum_class, + log_level_str, + "Lcom/dropbear/logging/LogLevel;" + )?.l()?; + + let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?; + let log_writer_obj = env.new_object(std_out_writer_class, "()V", &[])?; + + log::trace!("Locating \"com/dropbear/host/SystemManager\" class"); + let system_manager_class: JClass = env.find_class("com/dropbear/host/SystemManager")?; + log::trace!("Creating SystemManager constructor with args (jar_path_string, dropbear_engine_object, log_writer_object, log_level_enum, log_target_string)"); + + let log_target_jstring = env.new_string("dropbear_rust_host")?; + + let system_manager_obj = env.new_object( + system_manager_class, + "(Ljava/lang/String;Ljava/lang/Object;Lcom/dropbear/logging/LogWriter;Lcom/dropbear/logging/LogLevel;Ljava/lang/String;)V", + &[ + JValue::Object(&jar_path_jstring), + JValue::Object(engine_global_ref.as_obj()), + JValue::Object(&log_writer_obj), + JValue::Object(&log_level_enum_instance), + JValue::Object(&log_target_jstring), + ], + )?; - Ok(()) - } + log::trace!("Creating new global ref for SystemManager"); + let manager_global_ref = env.new_global_ref(system_manager_obj)?; + self.system_manager_instance = Some(manager_global_ref); - pub fn clear_engine(&mut self) -> anyhow::Result<()> { - if let Some(old_ref) = self.dropbear_engine_class.take() { - let _ = old_ref; // drop - } - self.dropbear_engine_class = None; Ok(()) } - pub fn load_scripts_for_entity( - &mut self, - world: WorldPtr, - entity_id: u32, - tag: &str, - ) -> anyhow::Result<Vec<GlobalRef>> { - log::trace!("Loading scripts for entity {} with tag {}", entity_id, tag); - let mut env = self.jvm.attach_current_thread()?; - - let registry_class = env.find_class("com/dropbear/decl/RunnableRegistry")?; - log::trace!("Getting RunnableRegistry instance"); - let registry_instance = env.get_static_field( - registry_class, - "INSTANCE", - "Lcom/dropbear/decl/RunnableRegistry;", - )?; + pub fn reload(&mut self, _world: WorldPtr) -> anyhow::Result<()> { + log::info!("Reloading JAR using SystemManager: {}", self.jar_path.display()); + + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.reloadJar()"); + let jar_path_jstring = env.new_string(self.jar_path.to_string_lossy())?; + env.call_method( + manager_ref, + "reloadJar", + "(Ljava/lang/String;)V", + &[JValue::Object(&jar_path_jstring)], + )?; + } else { + log::warn!("SystemManager instance not found during reload."); + // self.init(world)?; + return Err(anyhow::anyhow!("SystemManager not initialised for reload.")); + } - let tag_jstring = env.new_string(tag)?; + log::info!("Reload complete via SystemManager!"); - log::trace!("Calling RunnableRegistry.instantiateScripts() with arg [{} as Object]", tag); - let scripts_list = env.call_method( - registry_instance.l()?, - "instantiateScripts", - "(Ljava/lang/String;)Ljava/util/List;", - &[JValue::Object(&tag_jstring)], - )?; + Ok(()) + } - log::trace!("Getting List instance"); - let scripts_list = scripts_list.l()?; - log::trace!("Getting List"); - let scripts = env.get_list(&scripts_list)?; - - let mut script_instances = Vec::new(); - - for i in 0..scripts.size(&mut env)? { - log::trace!("Getting script instance at index {}", i); - let script_obj = scripts.get(&mut env, i)?; - - if let Some(script_system) = script_obj { - log::trace!("Locating class \"com/dropbear/EntityRef\""); - let entity_id_class = env.find_class("com/dropbear/EntityId")?; - log::trace!("Initialising new EntityId with arg [{} as JValue::Long]", entity_id as i64); - let entity_id_obj = env.new_object( - entity_id_class, - "(J)V", - &[JValue::Long(entity_id as i64)], - )?; - - log::trace!("Locating class \"com/dropbear/EntityRef\""); - let entity_ref_class = env.find_class("com/dropbear/EntityRef")?; - log::trace!("Initialising new EntityRef with arg [entity_id as Object]"); - let entity_ref_obj = env.new_object( - entity_ref_class, - "(Lcom/dropbear/EntityId;)V", - &[JValue::Object(&entity_id_obj)], - )?; - - log::trace!("Setting field \"currentEntity\" on script system with arg [entity_ref as Object]"); - env.set_field( - &script_system, - "currentEntity", - "Lcom/dropbear/EntityRef;", - JValue::Object(&entity_ref_obj), - )?; - - log::trace!("Creating engine for entity"); - let engine_ref = self.create_engine_for_entity(world, entity_id)?; - - log::trace!("Calling script's load method with arg [engine as Object]"); - env.call_method( - &script_system, - "load", - "(Lcom/dropbear/DropbearEngine;)V", - &[JValue::Object(engine_ref.as_obj())], - )?; - - log::trace!("Creating global ref for script"); - script_instances.push(env.new_global_ref(script_system)?); - } + pub fn load_systems_for_tag(&mut self, tag: &str) -> anyhow::Result<()> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.loadSystemsForTag() with tag: {}", tag); + let tag_jstring = env.new_string(tag)?; + env.call_method( + manager_ref, + "loadSystemsForTag", + "(Ljava/lang/String;)V", + &[JValue::Object(&tag_jstring)], + )?; + + log::debug!("Loaded systems for tag: {}", tag); + } else { + return Err(anyhow::anyhow!("SystemManager not initialised when loading systems for tag: {}", tag)); } - - Ok(script_instances) + Ok(()) } - pub fn create_engine_for_entity(&self, world: WorldPtr, entity_id: u32) -> anyhow::Result<GlobalRef> { - let mut env = self.jvm.attach_current_thread()?; - - log::trace!("Locating \"com/dropbear/ffi/NativeEngine\" class"); - let native_engine_class = env.find_class("com/dropbear/ffi/NativeEngine")?; - log::trace!("Creating new instance of NativeEngine"); - let native_engine_obj = env.new_object(native_engine_class, "()V", &[])?; + pub fn update_all_systems(&self, dt: f32) -> anyhow::Result<()> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.updateAllSystems() with dt: {}", dt); + env.call_method( + manager_ref, + "updateAllSystems", + "(F)V", + &[JValue::Float(dt)], + )?; + + log::debug!("Updated all systems with dt: {}", dt); + } else { + return Err(anyhow::anyhow!("SystemManager not initialised when updating systems.")); + } + Ok(()) + } - log::trace!("Calling NativeEngine.init() with arg {}", world as jlong); - env.call_method( - &native_engine_obj, - "init", - "(J)V", - &[JValue::Long(world as jlong)], - )?; + pub fn update_systems_for_tag(&self, tag: &str, dt: f32) -> anyhow::Result<()> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.updateSystemsByTag() with tag: {}, dt: {}", tag, dt); + let tag_jstring = env.new_string(tag)?; + env.call_method( + manager_ref, + "updateSystemsByTag", + "(Ljava/lang/String;F)V", + &[JValue::Object(&tag_jstring), JValue::Float(dt)], + )?; + + log::debug!("Updated systems for tag: {} with dt: {}", tag, dt); + } else { + return Err(anyhow::anyhow!("SystemManager not initialised when updating systems for tag: {}", tag)); + } + Ok(()) + } - log::trace!("Locating \"com/dropbear/EntityId\""); - let entity_id_class = env.find_class("com/dropbear/EntityId")?; - log::trace!("Creating new EntityId with arg {}", entity_id as i64); - let entity_id_obj = env.new_object( - entity_id_class, - "(J)V", - &[JValue::Long(entity_id as i64)], - )?; + pub fn get_system_count_for_tag(&self, tag: &str) -> anyhow::Result<i32> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.getSystemCount() for tag: {}", tag); + let tag_jstring = env.new_string(tag)?; + let result = env.call_method( + manager_ref, + "getSystemCount", + "(Ljava/lang/String;)I", + &[JValue::Object(&tag_jstring)], + )?; + + Ok(result.i()?) + } else { + Err(anyhow::anyhow!("SystemManager not initialised when getting system count for tag: {}", tag)) + } + } - log::trace!("Locating \"com/dropbear/EntityRef\""); - let entity_ref_class = env.find_class("com/dropbear/EntityRef")?; - log::trace!("Creating new EntityRef with arg [entity_id as Object]"); - env.new_object( - entity_ref_class, - "(Lcom/dropbear/EntityId;)V", - &[JValue::Object(&entity_id_obj)], - )?; + pub fn has_systems_for_tag(&self, tag: &str) -> anyhow::Result<bool> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.hasSystemsForTag() for tag: {}", tag); + let tag_jstring = env.new_string(tag)?; + let result = env.call_method( + manager_ref, + "hasSystemsForTag", + "(Ljava/lang/String;)Z", + &[JValue::Object(&tag_jstring)], + )?; + + Ok(result.z()?) + } else { + Err(anyhow::anyhow!("SystemManager not initialised when checking for systems for tag: {}", tag)) + } + } - log::trace!("Locating class \"com/dropbear/DropbearEngine\""); - let dropbear_class = env.find_class("com/dropbear/DropbearEngine")?; - log::trace!("Creating DropbearEngine constructor with arg (NativeEngine_object)"); - let dropbear_obj = env.new_object( - dropbear_class, - "(Lcom/dropbear/ffi/NativeEngine;)V", - &[ - JValue::Object(&native_engine_obj), - // JValue::Object(&entity_ref_obj), - ], - )?; + pub fn get_total_system_count(&self) -> anyhow::Result<i32> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.getTotalSystemCount()"); + let result = env.call_method( + manager_ref, + "getTotalSystemCount", + "()I", + &[], + )?; + + Ok(result.i()?) + } else { + Err(anyhow::anyhow!("SystemManager not initialised when getting total system count.")) + } + } - log::trace!("Creating new global ref for DropbearEngine and returning"); - Ok(env.new_global_ref(dropbear_obj)?) + pub fn clear_engine(&mut self) -> anyhow::Result<()> { + if let Some(old_engine_ref) = self.dropbear_engine_class.take() { + let _ = old_engine_ref; // drop + } + if let Some(old_manager_ref) = self.system_manager_instance.take() { + let _ = old_manager_ref; // drop + } + Ok(()) } } @@ -220,8 +285,12 @@ impl Drop for JavaContext { if let Some(ref global_ref) = self.dropbear_engine_class { let _ = global_ref; } + if let Some(old_ref) = self.system_manager_instance.take() { + let _ = old_ref; + } } } + #[unsafe(no_mangle)] // JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getEntity // (JNIEnv *, jobject, jlong, jstring); @@ -0,0 +1,202 @@ +use std::path::Path; +use crossbeam_channel::Sender; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use crate::scripting::get_gradle_command; + +pub enum HotReloadEvent { + SuccessBuild, + FailedBuild(String), +} + +pub struct HotReloader { + cancellation_token: CancellationToken, + handle: Option<JoinHandle<()>>, +} + +impl HotReloader { + pub fn new() -> Self { + Self { + cancellation_token: CancellationToken::new(), + handle: None, + } + } + + pub fn start( + &mut self, + project_root: impl AsRef<Path>, + sender: Sender<HotReloadEvent>, + ) { + if self.is_running() { + self.stop(); + } + + let project_root = project_root.as_ref().to_path_buf(); + let token = self.cancellation_token.clone(); + + let handle = tokio::spawn(async move { + let gradle_cmd = get_gradle_command(project_root.clone()); + + loop { + if token.is_cancelled() { + log::info!("Hot reloader cancelled, stopping..."); + break; + } + + let mut child = match Command::new(&gradle_cmd) + .current_dir(&project_root) + .args(["--continuous", "--console=plain", "jvmJar"]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn() + { + Ok(child) => child, + Err(e) => { + log::error!("Failed to start continuous build: {}", e); + + tokio::select! { + _ = token.cancelled() => break, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => continue, + } + } + }; + + let stdout = child.stdout.take().expect("Stdout was piped"); + let stderr = child.stderr.take().expect("Stderr was piped"); + + let stdout_handle = tokio::spawn({ + let sender = sender.clone(); + let token = token.clone(); + + async move { + let mut reader = BufReader::new(stdout).lines(); + + loop { + tokio::select! { + _ = token.cancelled() => { + log::debug!("Stdout reader cancelled"); + break; + } + result = reader.next_line() => { + match result { + Ok(Some(line)) => { + log::debug!("[Gradle] {}", line); + + if line.contains("BUILD SUCCESSFUL") { + log::info!("Build completed, triggering hot-reload..."); + let _ = sender.send(HotReloadEvent::SuccessBuild); + } else if line.contains("BUILD FAILED") { + log::error!("Build failed, waiting for next change..."); + let _ = sender.send(HotReloadEvent::FailedBuild(line.clone())); + } + } + Ok(None) => break, // EOF + Err(e) => { + log::error!("Error reading stdout: {}", e); + break; + } + } + } + } + } + } + }); + + let stderr_handle = tokio::spawn({ + let token = token.clone(); + + async move { + let mut reader = BufReader::new(stderr).lines(); + + loop { + tokio::select! { + _ = token.cancelled() => { + log::debug!("Stderr reader cancelled"); + break; + } + result = reader.next_line() => { + match result { + Ok(Some(line)) => { + log::warn!("[Gradle Error] {}", line); + } + Ok(None) => break, // EOF + Err(e) => { + log::error!("Error reading stderr: {}", e); + break; + } + } + } + } + } + } + }); + + tokio::select! { + _ = token.cancelled() => { + log::info!("Hot reloader cancelled, cleaning up..."); + let _ = child.kill().await; + + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + stdout_handle.abort(); + stderr_handle.abort(); + let _ = tokio::join!(stdout_handle, stderr_handle); + break; + } + status = child.wait() => { + match status { + Ok(exit_status) => { + log::warn!("Gradle process exited with status: {}, restarting...", exit_status); + } + Err(e) => { + log::error!("Error waiting for gradle process: {}", e); + } + } + let _ = tokio::join!(stdout_handle, stderr_handle); + } + } + + tokio::select! { + _ = token.cancelled() => break, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => {} + } + } + + log::info!("Hot reloader task finished"); + }); + + self.handle = Some(handle); + } + + pub fn stop(&mut self) { + log::info!("Stopping hot reloader..."); + + self.cancellation_token.cancel(); + + if let Some(handle) = self.handle.take() { + handle.abort(); + } + + self.cancellation_token = CancellationToken::new(); + } + + pub fn is_running(&self) -> bool { + self.handle.as_ref().map_or(false, |h| !h.is_finished()) + && !self.cancellation_token.is_cancelled() + } +} + +impl Default for HotReloader { + fn default() -> Self { + Self::new() + } +} + +impl Drop for HotReloader { + fn drop(&mut self) { + self.stop(); + } +} @@ -12,7 +12,7 @@ use std::{ sync::{Arc, LazyLock}, time::{Duration, Instant}, }; - +use crossbeam_channel::Receiver; use crate::build::build; use crate::camera::UndoableCameraAction; use crate::debug; @@ -47,6 +47,7 @@ use tokio::sync::oneshot; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode}; use wgpu::{Color, Extent3d, RenderPipeline}; use winit::{keyboard::KeyCode, window::Window}; +use eucalyptus_core::scripting::jni::hotreload::{HotReloadEvent, HotReloader}; pub struct Editor { scene_command: SceneCommand, @@ -89,8 +90,6 @@ pub struct Editor { // channels /// A threadsafe Unbounded Receiver, typically used for checking the status of the world loading progress_tx: Option<UnboundedReceiver<WorldLoadingStatus>>, - /// Unused: A threadsafe Unbounded Sender - _progress_rx: Option<UnboundedSender<WorldLoadingStatus>>, /// Used to check if the world has been loaded in is_world_loaded: IsWorldLoadedYet, /// Used to fetch the current status of the loading, so it can be used for different @@ -103,7 +102,7 @@ pub struct Editor { world_receiver: Option<oneshot::Receiver<hecs::World>>, // building - pub progress_rx: Option<crossbeam_channel::Receiver<BuildStatus>>, + pub progress_rx: Option<Receiver<BuildStatus>>, pub handle_created: Option<FutureHandle>, pub build_logs: Vec<String>, pub build_progress: f32, @@ -111,6 +110,11 @@ pub struct Editor { pub last_build_error: Option<String>, pub show_build_error_window: bool, + // hot reloading + pub hot_reloader: HotReloader, + pub hot_reload_rx: Option<Receiver<HotReloadEvent>>, + + dock_state_shared: Option<Arc<Mutex<DockState<EditorTab>>>>, } @@ -127,7 +131,7 @@ impl Editor { let [_old, _] = surface.split_below( right, 0.5, - vec![EditorTab::AssetViewer, EditorTab::KotlinREPL], + vec![EditorTab::AssetViewer], ); // this shit doesn't work :( @@ -181,7 +185,6 @@ impl Editor { input_state: InputState::new(), light_manager: LightManager::new(), active_camera: Arc::new(Mutex::new(None)), - _progress_rx: None, progress_tx: None, is_world_loaded: IsWorldLoadedYet::new(), current_state: WorldLoadingStatus::Idle, @@ -195,6 +198,8 @@ impl Editor { show_build_window: false, last_build_error: None, show_build_error_window: false, + hot_reloader: HotReloader::new(), + hot_reload_rx: None, dock_state_shared: None, }) } @@ -112,6 +112,32 @@ impl Scene for Editor { self.signal = Signal::StopPlaying; } + if let Some(rx) = &self.hot_reload_rx { + match rx.try_recv() { + Ok(val) => { + match val { + HotReloadEvent::SuccessBuild => { + let world_ptr = self.world.as_mut() as *mut World; + match self.script_manager.reload(world_ptr) { + Ok(_) => { + log::info!("Reloaded script"); + } + Err(e) => { + log::error!("Failed to reload script: {}", e); + } + } + } + HotReloadEvent::FailedBuild(e) => { + log_once::warn_once!("Failed to build: {}", e); + } + } + } + Err(e) => { + log_once::warn_once!("Failed to receive hot reload signal: {}", e); + } + } + } + let world_ptr = self.world.as_mut() as *mut World; if let Err(e) = self.script_manager.update_script( @@ -8,7 +8,7 @@ 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::{BuildStatus, ScriptManager, ScriptTarget}; +use eucalyptus_core::scripting::{build_jvm, BuildStatus, ScriptTarget}; use eucalyptus_core::spawn::{PendingSpawn, push_pending_spawn}; use eucalyptus_core::states::{ModelProperties, ScriptComponent, Value, PROJECT}; use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console}; @@ -124,7 +124,7 @@ impl SignalController for Editor { let status_tx = tx.clone(); let handle = graphics.future_queue.push(async move { - ScriptManager::build_jvm(project_root, status_tx).await + build_jvm(project_root, status_tx).await }); log::debug!("Pushed future to future_queue, received handle: {:?}", handle); @@ -392,13 +392,9 @@ impl SignalController for Editor { self.show_build_error_window = false; } } - - // self.signal = Signal::None; Ok(()) } Signal::StopPlaying => { - let _ = self.script_manager.kill(); - if let Err(e) = self.restore() { warn!("Failed to restore from play mode backup: {}", e); log::warn!("Failed to restore scene state: {}", e); @@ -408,11 +404,6 @@ impl SignalController for Editor { self.switch_to_debug_camera(); - // 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..."); @@ -20,8 +20,8 @@ impl Generator for KotlinJVMGenerator { writeln!(output)?; writeln!(output, "package com.dropbear.decl")?; - writeln!(output)?; + let mut imported_packages = std::collections::HashSet::new(); for item in manifest.items() { if let Some(last_dot) = item.fqcn().rfind('.') { @@ -47,31 +47,40 @@ impl Generator for KotlinJVMGenerator { } writeln!(output, "object RunnableRegistry {{")?; - writeln!(output, " private val TAG_REGISTRY = mapOf(")?; - - let tag_entries: Vec<String> = tag_map - .iter() - .map(|(tag, classes)| { - let factories: Vec<String> = classes - .iter() - .map(|cls| format!("::{}", cls)) - .collect(); - format!(" \"{}\" to listOf({})", tag, factories.join(", ")) - }) - .collect(); - - if !tag_entries.is_empty() { - writeln!(output, "{}", tag_entries.join(",\n"))?; + writeln!(output, " private val tagRegistry = mutableMapOf<String, MutableList<() -> com.dropbear.System>>()")?; + writeln!(output)?; + + writeln!(output, " init {{")?; + writeln!(output, " registerStaticScripts()")?; + writeln!(output, " }}")?; + writeln!(output)?; + + writeln!(output, " private fun registerStaticScripts() {{")?; + for (tag, classes) in &tag_map { + writeln!(output, " // Tag: {}", tag)?; + for class in classes { + writeln!( + output, + " tagRegistry.computeIfAbsent(\"{}\") {{ mutableListOf() }}.add(::{})", + tag, class + )?; + } } + writeln!(output, " }}")?; + writeln!(output)?; - writeln!(output, " )")?; + writeln!(output, " @Synchronized")?; + writeln!(output, " fun reload() {{")?; + writeln!(output, " tagRegistry.clear()")?; + writeln!(output, " registerStaticScripts()")?; + writeln!(output, " }}")?; writeln!(output)?; writeln!( output, " fun getScriptFactories(tag: String): List<() -> com.dropbear.System> {{" )?; - writeln!(output, " return TAG_REGISTRY[tag] ?: emptyList()")?; + writeln!(output, " return tagRegistry[tag]?.toList() ?: emptyList()")?; writeln!(output, " }}")?; writeln!(output)?; @@ -84,6 +93,11 @@ impl Generator for KotlinJVMGenerator { " return getScriptFactories(tag).map {{ it() }}" )?; writeln!(output, " }}")?; + writeln!(output)?; + + writeln!(output, " fun getAllTags(): Set<String> {{")?; + writeln!(output, " return tagRegistry.keys.toSet()")?; + writeln!(output, " }}")?; writeln!(output, "}}")?; Binary files /dev/null and b/resources/dependencies/hotswap-agent-2.0.0.jar differ @@ -0,0 +1,9 @@ +package com.dropbear.logging + +enum class LogLevel { + TRACE, + DEBUG, + INFO, + WARN, + ERROR +} @@ -0,0 +1,5 @@ +package com.dropbear.logging + +interface LogWriter { + fun log(level: LogLevel, target: String, message: String, file: String?, line: Int?) +} @@ -0,0 +1,64 @@ +package com.dropbear.logging + +@Suppress("unused") +object Logger { + private var writer: LogWriter = StdoutWriter() + private var minLevel: LogLevel = LogLevel.INFO + private var defaultTarget: String = "dropbear" + + internal fun init(writer: LogWriter, minLevel: LogLevel = LogLevel.INFO, defaultTarget: String = "dropbear") { + this.writer = writer + this.minLevel = minLevel + this.defaultTarget = defaultTarget + println("Log: Initialised with writer: $writer, minLevel: $minLevel, defaultTarget: $defaultTarget") + } + + fun setLogLevel(level: LogLevel) { + this.minLevel = level + } + + private fun logInternal(level: LogLevel, message: String, target: String, file: String?, line: Int?) { + if (level.ordinal >= minLevel.ordinal) { + writer.log(level, target, message, file, line) + } + } + + fun trace(message: String, target: String = defaultTarget, file: String? = null, line: Int? = null) = + logInternal(LogLevel.TRACE, message, target, file, line) + fun debug(message: String, target: String = defaultTarget, file: String? = null, line: Int? = null) = + logInternal(LogLevel.DEBUG, message, target, file, line) + fun info(message: String, target: String = defaultTarget, file: String? = null, line: Int? = null) = + logInternal(LogLevel.INFO, message, target, file, line) + fun warn(message: String, target: String = defaultTarget, file: String? = null, line: Int? = null) = + logInternal(LogLevel.WARN, message, target, file, line) + fun error(message: String, target: String = defaultTarget, file: String? = null, line: Int? = null) = + logInternal(LogLevel.ERROR, message, target, file, line) + + // --- + + fun trace(message: () -> String, target: String = defaultTarget, file: String? = null, line: Int? = null) { + if (LogLevel.TRACE.ordinal >= minLevel.ordinal) { + logInternal(LogLevel.TRACE, message(), target, file, line) + } + } + fun debug(message: () -> String, target: String = defaultTarget, file: String? = null, line: Int? = null) { + if (LogLevel.DEBUG.ordinal >= minLevel.ordinal) { + logInternal(LogLevel.DEBUG, message(), target, file, line) + } + } + fun info(message: () -> String, target: String = defaultTarget, file: String? = null, line: Int? = null) { + if (LogLevel.INFO.ordinal >= minLevel.ordinal) { + logInternal(LogLevel.INFO, message(), target, file, line) + } + } + fun warn(message: () -> String, target: String = defaultTarget, file: String? = null, line: Int? = null) { + if (LogLevel.WARN.ordinal >= minLevel.ordinal) { + logInternal(LogLevel.WARN, message(), target, file, line) + } + } + fun error(message: () -> String, target: String = defaultTarget, file: String? = null, line: Int? = null) { + if (LogLevel.ERROR.ordinal >= minLevel.ordinal) { + logInternal(LogLevel.ERROR, message(), target, file, line) + } + } +} @@ -0,0 +1,24 @@ +package com.dropbear.logging + +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +class StdoutWriter: LogWriter { + override fun log( + level: LogLevel, + target: String, + message: String, + file: String?, + line: Int? + ) { + val now = Clock.System.now() + val timeZone = TimeZone.currentSystemDefault() + val timestamp = now.toLocalDateTime(timeZone) + val location = if (file != null && line != null) "[$file:$line] " else "" + when (level) { + LogLevel.ERROR, LogLevel.WARN -> error("[$timestamp] [$level] $location[$target] $message") + else -> println("[$timestamp] [$level] $location[$target] $message") + } + } +} @@ -0,0 +1,58 @@ +package com.dropbear.host + +import com.dropbear.logging.Logger +import java.lang.reflect.InvocationTargetException +import java.net.URLClassLoader +import java.nio.file.Path + +class HotSwapUtility( + private var jarFilePath: String, + private var className: String +) { + private lateinit var classLoader: URLClassLoader + + init { + initialiseClassLoader() + } + + private fun initialiseClassLoader() { + try { + val urls = arrayOf(Path.of(jarFilePath).toUri().toURL()) + classLoader = URLClassLoader(urls, HotSwapUtility::class.java.classLoader) + } catch (e: Exception) { + Logger.error("Failed to initialise class loader: ${e.message}") + e.printStackTrace() + } + } + + @Throws( + ClassNotFoundException::class, + NoSuchMethodException::class, + IllegalAccessException::class, + InvocationTargetException::class, + InstantiationException::class + ) + fun getInstance(parameterTypes: Array<Class<*>>, args: Array<out Any?>): Any { + val clazz = classLoader.loadClass(className) + if (clazz.isAnnotationPresent(Metadata::class.java)) { + try { + val instanceField = clazz.getDeclaredField("INSTANCE") + return instanceField.get(null) + } catch (e: NoSuchFieldException) { + Logger.error("Failed to get instance of class: ${e.message}") + } + } + val constructor = clazz.getConstructor(*parameterTypes) + return constructor.newInstance(*args) + } + + fun reloadJar(newJarFilePath: String) { + try { + classLoader.close() + } catch (e: Exception) { + e.printStackTrace() + } + jarFilePath = newJarFilePath + initialiseClassLoader() + } +} @@ -0,0 +1,130 @@ +package com.dropbear.host + +import com.dropbear.logging.Logger +import com.dropbear.logging.LogLevel +import com.dropbear.logging.LogWriter +import com.dropbear.logging.StdoutWriter + +@Suppress("UNUSED") +class SystemManager( + jarPath: String, + private val engine: Any, + logWriter: LogWriter? = null, + logLevel: LogLevel? = null, + logTarget: String = "unset" +) { + private val hotSwapUtility = HotSwapUtility(jarPath, "com.dropbear.decl.RunnableRegistry") + private var registryInstance: Any? = null + private var registryClass: Class<*>? = null + private val activeSystems = mutableMapOf<String, MutableList<Any>>() + + init { + val writerToUse = logWriter ?: StdoutWriter() + Logger.init(writerToUse, logLevel ?: LogLevel.INFO, logTarget) + Logger.info("SystemManager: Initialised with jarPath: $jarPath, " + + "logWriter: $writerToUse, " + + "logLevel: $logLevel, " + + "logTarget: $logTarget") + + val (instance, clazz) = loadRegistry() + registryInstance = instance + registryClass = clazz + } + + private fun loadRegistry(): Pair<Any, Class<*>> { + Logger.debug("Loading RunnableRegistry instance...") + val instance = hotSwapUtility.getInstance(emptyArray(), emptyArray()) + Logger.debug("RunnableRegistry instance loaded successfully.") + return instance to instance.javaClass + } + + fun loadSystemsForTag(tag: String) { + Logger.debug("Loading systems for tag: $tag") + val instantiateMethod = registryClass?.getMethod("instantiateScripts", String::class.java) + val systems = instantiateMethod?.invoke(registryInstance, tag) as List<*> + + val loadedSystems = mutableListOf<Any>() + val engineClass = engine.javaClass + + for (system in systems) { + system?.let { + val loadMethod = it.javaClass.getMethod("load", engineClass) + loadMethod.invoke(it, engine) + loadedSystems.add(it) + Logger.trace("Loaded system: ${it.javaClass.name} for tag: $tag") + } + } + + activeSystems[tag] = loadedSystems + Logger.debug("Loaded ${loadedSystems.size} systems for tag: $tag") + } + + fun updateAllSystems(deltaTime: Float) { + Logger.trace("Updating all systems") + val engineClass = engine.javaClass + + for ((_, systems) in activeSystems) { + for (system in systems) { + val updateMethod = system.javaClass.getMethod( + "update", + engineClass, + Float::class.javaPrimitiveType + ) + updateMethod.invoke(system, engine, deltaTime) + } + } + } + + fun updateSystemsByTag(tag: String, deltaTime: Float) { + Logger.trace("Updating systems for tag: $tag") + val systems = activeSystems[tag] ?: return + val engineClass = engine.javaClass + + for (system in systems) { + val updateMethod = system.javaClass.getMethod( + "update", + engineClass, + Float::class.javaPrimitiveType + ) + updateMethod.invoke(system, engine, deltaTime) + } + } + + fun reloadJar(newJarPath: String) { + Logger.info("Reloading systems with new jar path: $newJarPath") + activeSystems.clear() + hotSwapUtility.reloadJar(newJarPath) + + val (instance, clazz) = loadRegistry() + registryInstance = instance + registryClass = clazz + + val reloadMethod = registryClass?.getMethod("reload") + reloadMethod?.invoke(registryInstance) + Logger.info("JAR loaded successfully.") + } + + fun unloadSystemsByTag(tag: String) { + activeSystems.remove(tag) + } + + fun unloadAllSystems() { + activeSystems.clear() + } + + fun getSystemCount(tag: String): Int { + return activeSystems[tag]?.size ?: 0 + } + + fun getTotalSystemCount(): Int { + return activeSystems.values.sumOf { it.size } + } + + fun getActiveTags(): Set<String> { + return activeSystems.keys.toSet() + } + + fun hasSystemsForTag(tag: String): Boolean { + return activeSystems.containsKey(tag) && activeSystems[tag]?.isNotEmpty() == true + } +} @@ -3,18 +3,19 @@ package com.dropbear.ffi -import co.touchlab.kermit.Logger import com.dropbear.ffi.generated.dropbear_get_entity +import com.dropbear.logging.Logger import kotlinx.cinterop.* import kotlin.experimental.ExperimentalNativeApi actual class NativeEngine { private var worldHandle: COpaquePointer? = null + @Suppress("unused") // called from jni fun init(handle: COpaquePointer?) { this.worldHandle = handle if (this.worldHandle == null) { - Logger.i("NativeEngine: Error - Invalid world handle received!") + Logger.info("NativeEngine: Error - Invalid world handle received!") } }