tirbofish/dropbear · commit
7b40ac36a41ef5b41e0438602acd099bf108e098
feature: implemented my destroy() (scene exit function), and laid some groundwork for physics updates. also added some new docs and new native signatures + magna-carta updates.
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -79,6 +79,7 @@ quote = "1.0" egui_ltreeview = { version = "0.6", features = ["doc"] } dyn-hash = "1.0" semver = { version = "1.0", features = ["serde"] } +rapier3d = { version = "0.31", features = [ "simd-stable" ] } [workspace.dependencies.image] version = "0.25" @@ -17,6 +17,8 @@ pub mod shader; pub mod utils; pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new(); +pub const PHYSICS_STEP_RATE: u32 = 50; +const MAX_PHYSICS_STEPS_PER_FRAME: usize = 8; use app_dirs2::{AppDataType, AppInfo}; use bytemuck::Contiguous; @@ -76,12 +78,16 @@ pub struct State { pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, + physics_accumulator: Duration, + pub scene_manager: scene::Manager, } impl State { /// Asynchronously initialised the state and sets up the backend and surface for wgpu to render to. pub async fn new(window: Arc<Window>, instance: Arc<Instance>, future_queue: Arc<FutureQueue>) -> anyhow::Result<Self> { + let title = window.title(); + let size = window.inner_size(); let surface = instance.create_surface(window.clone())?; @@ -96,7 +102,7 @@ impl State { let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { - label: None, + label: Some(format!("{} graphics device", title).as_str()), required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), experimental_features: unsafe { ExperimentalFeatures::enabled() }, @@ -225,6 +231,7 @@ Hardware: texture_id: Arc::new(texture_id), future_queue, instance, + physics_accumulator: Duration::ZERO, scene_manager: scene::Manager::new(), }; @@ -315,14 +322,31 @@ Hardware: let mut scene_manager = std::mem::replace(&mut self.scene_manager, scene::Manager::new()); + let physics_dt = Duration::from_secs_f32(1.0 / PHYSICS_STEP_RATE as f32); + let frame_dt = Duration::from_secs_f32(previous_dt).min(Duration::from_millis(250)); + let mut physics_accumulator = self.physics_accumulator + frame_dt; + let window_commands = { let mut graphics = graphics::RenderContext::from_state(self, viewport_view, &mut encoder); + let mut steps = 0usize; + while physics_accumulator >= physics_dt && steps < MAX_PHYSICS_STEPS_PER_FRAME { + scene_manager.physics_update(physics_dt.as_secs_f32(), &mut graphics); + physics_accumulator -= physics_dt; + steps += 1; + } + + if steps == MAX_PHYSICS_STEPS_PER_FRAME && physics_accumulator >= physics_dt { + physics_accumulator = physics_accumulator.min(physics_dt); + } + let commands = scene_manager.update(previous_dt, &mut graphics, event_loop); scene_manager.render(&mut graphics); commands }; + self.physics_accumulator = physics_accumulator; + self.scene_manager = scene_manager; self.egui_renderer.lock().end_frame_and_draw( @@ -11,6 +11,7 @@ use std::{collections::HashMap, rc::Rc}; pub trait Scene { fn load(&mut self, graphics: &mut crate::graphics::RenderContext); + fn physics_update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext); fn update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext); fn render(&mut self, graphics: &mut crate::graphics::RenderContext); fn exit(&mut self, event_loop: &ActiveEventLoop); @@ -149,6 +150,18 @@ impl Manager { Vec::new() } + pub fn physics_update<'a>( + &mut self, + dt: f32, + graphics: &mut crate::graphics::RenderContext<'a>, + ) { + if let Some(scene_name) = &self.current_scene + && let Some(scene) = self.scenes.get_mut(scene_name) + { + scene.write().physics_update(dt, graphics) + } + } + pub fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>) { if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) @@ -41,6 +41,7 @@ rfd = { workspace = true, optional = true } typetag.workspace = true semver.workspace = true rustc_version_runtime.workspace = true +rapier3d.workspace = true [features] default = [] @@ -18,6 +18,7 @@ pub use dropbear_macro as macros; pub use dropbear_traits as traits; pub use egui; +pub use rapier3d; /// The appdata directory for storing any information. /// @@ -18,7 +18,7 @@ use anyhow::Context; use crossbeam_channel::Sender; use dropbear_engine::asset::ASSET_REGISTRY; use hecs::{Entity, World}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; @@ -72,6 +72,18 @@ pub struct ScriptManager { /// The path to the library. This is set if the [`ScriptTarget`] is [`ScriptTarget::Native`] or /// [`ScriptTarget::JVM`] lib_path: Option<PathBuf>, + + /// Tags that have been instantiated/loaded into the scripting runtime. + /// + /// This is intentionally independent of the current scene's entity tag database so that + /// scripts can keep state across scene switches. + loaded_tags: HashSet<String>, + + /// Tags that are currently in-scope for the active scene/world. + active_tags: HashSet<String>, + + /// True once `load_script` has successfully initialised the current target. + scripts_loaded: bool, } impl ScriptManager { @@ -86,6 +98,9 @@ impl ScriptManager { entity_tag_database: HashMap::new(), jvm_created: false, lib_path: None, + loaded_tags: HashSet::new(), + active_tags: HashSet::new(), + scripts_loaded: false, }; let jvm_args = JVM_ARGS.get().map(|v| v.clone()); @@ -116,13 +131,42 @@ impl ScriptManager { entity_tag_database: HashMap<String, Vec<Entity>>, target: ScriptTarget, ) -> anyhow::Result<()> { - self.entity_tag_database = entity_tag_database.clone(); + let previous_target = self.script_target.clone(); + let previous_path = self.lib_path.clone(); + + let next_active: HashSet<String> = entity_tag_database.keys().cloned().collect(); + + let new_path = match &target { + ScriptTarget::JVM { library_path } => Some(library_path.clone()), + ScriptTarget::Native { library_path } => Some(library_path.clone()), + ScriptTarget::None => None, + }; + + let target_kind_changed = std::mem::discriminant(&previous_target) != std::mem::discriminant(&target); + let path_changed = previous_path != new_path; + + if target_kind_changed || path_changed { + self.destroy_all().ok(); + } else if self.scripts_loaded { + let removed: Vec<String> = self + .active_tags + .difference(&next_active) + .cloned() + .collect(); + + for tag in removed { + let _ = self.destroy_in_scope_tagged(&tag); + } + } + + self.active_tags = next_active; + + self.entity_tag_database = entity_tag_database; self.script_target = target.clone(); + self.lib_path = new_path; match &target { ScriptTarget::JVM { library_path } => { - self.lib_path = Some(library_path.clone()); - if !self.jvm_created { let jvm = JavaContext::new(jvm_args)?; self.jvm = Some(jvm); @@ -134,20 +178,20 @@ impl ScriptManager { jvm.jar_path = library_path.clone(); } } - - if let Some(jvm) = &mut self.jvm { - jvm.clear_engine()?; - } } ScriptTarget::Native { library_path } => { - self.library = Some(NativeLibrary::new(library_path)?); - self.lib_path = Some(library_path.clone()); + if path_changed || self.library.is_none() { + self.library = Some(NativeLibrary::new(library_path)?); + } } ScriptTarget::None => { self.jvm = None; self.library = None; self.jvm_created = false; self.lib_path = None; + self.loaded_tags.clear(); + self.active_tags.clear(); + self.scripts_loaded = false; } } @@ -190,7 +234,9 @@ impl ScriptManager { for tag in self.entity_tag_database.keys() { log::trace!("Loading systems for tag: {}", tag); jvm.load_systems_for_tag(tag)?; + self.loaded_tags.insert(tag.clone()); } + self.scripts_loaded = true; return Ok(()); } } @@ -200,7 +246,9 @@ impl ScriptManager { for tag in self.entity_tag_database.keys() { log::trace!("Loading systems for tag: {}", tag); library.load_systems(tag.to_string())?; + self.loaded_tags.insert(tag.clone()); } + self.scripts_loaded = true; return Ok(()); } } @@ -228,7 +276,7 @@ impl ScriptManager { world: &World, dt: f32, ) -> anyhow::Result<()> { - self.rebuild_entity_tag_database(world); + self.rebuild_entity_tag_database(world)?; match self.script_target { ScriptTarget::None => Err(anyhow::anyhow!( @@ -284,6 +332,68 @@ impl ScriptManager { } } } + + pub fn physics_update_script( + &mut self, + world: &World, + dt: f32, + ) -> anyhow::Result<()> { + self.rebuild_entity_tag_database(world)?; + + match self.script_target { + ScriptTarget::None => Err(anyhow::anyhow!( + "ScriptTarget is set to None. Either set to JVM or Native" + )), + ScriptTarget::JVM { .. } => { + if let Some(jvm) = &self.jvm { + if self.entity_tag_database.is_empty() { + jvm.physics_update_all_systems(dt)?; + } else { + for (tag, entities) in &self.entity_tag_database { + let entity_ids: Vec<u64> = entities + .iter() + .map(|entity| entity.to_bits().get()) + .collect(); + + if entity_ids.is_empty() { + jvm.physics_update_systems_for_tag(tag, dt)?; + } else { + jvm.physics_update_systems_for_entities(tag, &entity_ids, dt)?; + } + } + } + return Ok(()); + } + Err(anyhow::anyhow!( + "ScriptTarget is set to JVM but JVM is None" + )) + } + ScriptTarget::Native { .. } => { + if let Some(library) = &mut self.library { + if self.entity_tag_database.is_empty() { + library.physics_update_all(dt)?; + } else { + for (tag, entities) in &self.entity_tag_database { + let entity_ids: Vec<u64> = entities + .iter() + .map(|entity| entity.to_bits().get()) + .collect(); + + if entity_ids.is_empty() { + library.physics_update_tagged(tag, dt)?; + } else { + library.physics_update_systems_for_entities(tag, entity_ids.as_slice(), dt)?; + } + } + } + return Ok(()); + } + Err(anyhow::anyhow!( + "ScriptTarget is set to Native but library is None" + )) + } + } + } /// Reloads the .jar file by unloading the previous classes and reloading them back in, /// allowing for hot reloading. @@ -301,8 +411,97 @@ impl ScriptManager { Ok(()) } + /// Destroys all scripts for the current target. + pub fn destroy_all(&mut self) -> anyhow::Result<()> { + match self.script_target { + ScriptTarget::None => Ok(()), + ScriptTarget::JVM { .. } => { + if let Some(jvm) = &self.jvm { + let _ = jvm.unload_all_systems(); + } + self.loaded_tags.clear(); + self.active_tags.clear(); + self.scripts_loaded = false; + Ok(()) + } + ScriptTarget::Native { .. } => { + if let Some(library) = &mut self.library { + library.destroy_all()?; + } + self.loaded_tags.clear(); + self.active_tags.clear(); + self.scripts_loaded = false; + Ok(()) + } + } + } + + fn destroy_tagged(&mut self, tag: &str) -> anyhow::Result<()> { + match self.script_target { + ScriptTarget::None => Ok(()), + ScriptTarget::JVM { .. } => { + if let Some(jvm) = &self.jvm { + let _ = jvm.unload_systems_for_tag(tag); + } + Ok(()) + } + ScriptTarget::Native { .. } => { + if let Some(library) = &mut self.library { + library.destroy_tagged(tag.to_string())?; + } + Ok(()) + } + } + } + + fn destroy_in_scope_tagged(&mut self, tag: &str) -> anyhow::Result<()> { + if !self.scripts_loaded { + return Ok(()); + } + + match self.script_target { + ScriptTarget::None => Ok(()), + ScriptTarget::JVM { .. } => { + if let Some(jvm) = &self.jvm { + let _ = jvm.destroy_systems_for_tag(tag); + } + Ok(()) + } + ScriptTarget::Native { .. } => { + if let Some(library) = &mut self.library { + library.destroy_in_scope_tagged(tag.to_string())?; + } + Ok(()) + } + } + } + + fn load_tagged(&mut self, tag: &str) -> anyhow::Result<()> { + self.loaded_tags.insert(tag.to_string()); + + match self.script_target { + ScriptTarget::None => Ok(()), + ScriptTarget::JVM { .. } => { + if let Some(jvm) = &mut self.jvm { + jvm.load_systems_for_tag(tag)?; + } + Ok(()) + } + ScriptTarget::Native { .. } => { + if let Some(library) = &mut self.library { + library.load_systems(tag.to_string())?; + } + Ok(()) + } + } + } + /// Rebuilds the ScriptManagers entity database by parsing a [`World`]. - fn rebuild_entity_tag_database(&mut self, world: &World) { + /// + /// If scripts are already loaded, this also: + /// - loads tags entering scope, and + /// - calls `destroy()` for tags leaving scope (without unloading instances). + fn rebuild_entity_tag_database(&mut self, world: &World) -> anyhow::Result<()> { let mut new_map: HashMap<String, Vec<Entity>> = HashMap::new(); for (entity, script) in world.query::<&Script>().iter() { @@ -311,7 +510,38 @@ impl ScriptManager { } } + if self.scripts_loaded { + let next_active: HashSet<String> = new_map.keys().cloned().collect(); + let removed: Vec<String> = self + .active_tags + .difference(&next_active) + .cloned() + .collect(); + let added: Vec<String> = next_active + .difference(&self.active_tags) + .cloned() + .collect(); + + for tag in removed { + self.destroy_in_scope_tagged(&tag)?; + } + for tag in added { + self.load_tagged(&tag)?; + } + + self.active_tags = next_active; + } else { + self.active_tags = new_map.keys().cloned().collect(); + } + self.entity_tag_database = new_map; + Ok(()) + } +} + +impl Drop for ScriptManager { + fn drop(&mut self) { + let _ = self.destroy_all(); } } @@ -44,6 +44,7 @@ fn is_port_available(port: u16) -> bool { /// Provides a context for any eucalyptus-core JNI calls and JVM hosting. pub struct JavaContext { pub(crate) jvm: JavaVM, + native_engine_instance: Option<GlobalRef>, dropbear_engine_class: Option<GlobalRef>, system_manager_instance: Option<GlobalRef>, pub(crate) jar_path: PathBuf, @@ -281,6 +282,7 @@ impl JavaContext { Ok(Self { jvm, + native_engine_instance: None, dropbear_engine_class: None, system_manager_instance: None, jar_path: PathBuf::new(), @@ -293,14 +295,6 @@ impl JavaContext { ) -> anyhow::Result<()> { let mut env = self.jvm.attach_current_thread()?; - if let Some(old_ref) = self.dropbear_engine_class.take() { - let _ = old_ref; // drop - } - - if let Some(old_ref) = self.system_manager_instance.take() { - let _ = old_ref; // drop - } - let world_handle = context.world as jlong; let input_handle = context.input as jlong; let graphics_handle = context.graphics as jlong; @@ -322,30 +316,41 @@ impl JavaContext { 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"); - let native_engine_obj = env.new_object(native_engine_class, "()V", &[])?; + + let native_engine_obj = if let Some(ref native_engine_ref) = self.native_engine_instance { + native_engine_ref.as_obj() + } else { + log::trace!("Creating new instance of NativeEngine"); + let native_engine_obj = env.new_object(native_engine_class, "()V", &[])?; + let native_engine_global_ref = env.new_global_ref(native_engine_obj)?; + self.native_engine_instance = Some(native_engine_global_ref); + self.native_engine_instance + .as_ref() + .expect("NativeEngine global ref must exist") + .as_obj() + }; log::trace!("Calling NativeEngine.init() with arg [\"com/dropbear/ffi/DropbearContext\"]"); env.call_method( - &native_engine_obj, + native_engine_obj, "init", "(Lcom/dropbear/ffi/DropbearContext;)V", - &[ - JValue::Object(&dropbear_context_obj) - ], + &[JValue::Object(&dropbear_context_obj)], )?; - 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( - dropbear_class, - "(Lcom/dropbear/ffi/NativeEngine;)V", - &[JValue::Object(&native_engine_obj)], - )?; + if self.dropbear_engine_class.is_none() { + 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( + dropbear_class, + "(Lcom/dropbear/ffi/NativeEngine;)V", + &[JValue::Object(native_engine_obj)], + )?; - log::trace!("Creating new global ref for DropbearEngine"); - let engine_global_ref = env.new_global_ref(dropbear_obj)?; - self.dropbear_engine_class = Some(engine_global_ref.clone()); + log::trace!("Creating new global ref for DropbearEngine"); + let engine_global_ref = env.new_global_ref(dropbear_obj)?; + self.dropbear_engine_class = Some(engine_global_ref); + } let jar_path_jstring = env.new_string(self.jar_path.to_string_lossy())?; let log_level_str = { LOG_LEVEL.lock().to_string() }; @@ -361,29 +366,37 @@ impl JavaContext { 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)" - ); + if self.system_manager_instance.is_none() { + let engine_ref = self + .dropbear_engine_class + .as_ref() + .expect("DropbearEngine global ref must exist") + .as_obj(); - let log_target_jstring = env.new_string("dropbear_rust_host")?; + 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 system_manager_obj = env.new_object( - system_manager_class, - "(Ljava/lang/String;Lcom/dropbear/DropbearEngine;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), - ], - )?; + let log_target_jstring = env.new_string("dropbear_rust_host")?; - 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); + let system_manager_obj = env.new_object( + system_manager_class, + "(Ljava/lang/String;Lcom/dropbear/DropbearEngine;Lcom/dropbear/logging/LogWriter;Lcom/dropbear/logging/LogLevel;Ljava/lang/String;)V", + &[ + JValue::Object(&jar_path_jstring), + JValue::Object(engine_ref), + JValue::Object(&log_writer_obj), + JValue::Object(&log_level_enum_instance), + JValue::Object(&log_target_jstring), + ], + )?; + + 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); + } Ok(()) } @@ -463,6 +476,26 @@ impl JavaContext { Ok(()) } + pub fn physics_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_once::trace_once!("Calling SystemManager.physicsUpdateAllSystems() with dt: {}", dt); + env.call_method( + manager_ref, + "physicsUpdateAllSystems", + "(F)V", + &[JValue::Float(dt)], + )?; + + Ok(()) + } else { + Err(anyhow::anyhow!( + "SystemManager not initialised when physics updating systems." + )) + } + } + 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()?; @@ -490,6 +523,32 @@ impl JavaContext { Ok(()) } + pub fn physics_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.physicsUpdateSystemsByTag() with tag: {}, dt: {}", + tag, + dt + ); + let tag_jstring = env.new_string(tag)?; + env.call_method( + manager_ref, + "physicsUpdateSystemsByTag", + "(Ljava/lang/String;F)V", + &[JValue::Object(&tag_jstring), JValue::Float(dt)], + )?; + + Ok(()) + } else { + Err(anyhow::anyhow!( + "SystemManager not initialised when physics updating systems for tag: {}", + tag + )) + } + } + pub fn update_systems_for_entities( &self, tag: &str, @@ -550,6 +609,116 @@ impl JavaContext { } } + pub fn physics_update_systems_for_entities( + &self, + tag: &str, + entity_ids: &[u64], + 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.physicsUpdateSystemsForEntities() with tag: {}, count: {}, dt: {}", + tag, + entity_ids.len(), + dt + ); + + let tag_jstring = env.new_string(tag)?; + let entity_array: JLongArray = env.new_long_array(entity_ids.len() as i32)?; + let entity_array_raw = entity_array.as_raw(); + if !entity_ids.is_empty() { + env.set_long_array_region( + entity_array, + 0, + &entity_ids.iter().map(|e| *e as i64).collect::<Vec<_>>(), + )?; + } + let entity_array_obj = + unsafe { JObject::from_raw(entity_array_raw.cast::<jni::sys::_jobject>()) }; + + env.call_method( + manager_ref, + "physicsUpdateSystemsForEntities", + "(Ljava/lang/String;[JF)V", + &[ + JValue::Object(&tag_jstring), + JValue::Object(&entity_array_obj), + JValue::Float(dt), + ], + )?; + + Ok(()) + } else { + Err(anyhow::anyhow!( + "SystemManager not initialised when physics updating systems for tag: {}", + tag + )) + } + } + + pub fn unload_systems_for_tag(&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.unloadSystemsByTag() with tag: {}", tag); + let tag_jstring = env.new_string(tag)?; + env.call_method( + manager_ref, + "unloadSystemsByTag", + "(Ljava/lang/String;)V", + &[JValue::Object(&tag_jstring)], + )?; + + Ok(()) + } else { + Err(anyhow::anyhow!( + "SystemManager not initialised when unloading systems for tag: {}", + tag + )) + } + } + + pub fn destroy_systems_for_tag(&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.destroySystemsByTag() with tag: {}", + tag + ); + let tag_jstring = env.new_string(tag)?; + env.call_method( + manager_ref, + "destroySystemsByTag", + "(Ljava/lang/String;)V", + &[JValue::Object(&tag_jstring)], + )?; + + Ok(()) + } else { + Err(anyhow::anyhow!( + "SystemManager not initialised when destroying systems for tag: {}", + tag + )) + } + } + + pub fn unload_all_systems(&self) -> anyhow::Result<()> { + if let Some(ref manager_ref) = self.system_manager_instance { + let mut env = self.jvm.attach_current_thread()?; + + log::trace!("Calling SystemManager.unloadAllSystems()"); + env.call_method(manager_ref, "unloadAllSystems", "()V", &[])?; + Ok(()) + } else { + Err(anyhow::anyhow!( + "SystemManager not initialised when unloading all systems." + )) + } + } + 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()?; @@ -610,6 +779,9 @@ impl JavaContext { } pub fn clear_engine(&mut self) -> anyhow::Result<()> { + if let Some(old_native_engine_ref) = self.native_engine_instance.take() { + let _ = old_native_engine_ref; + } if let Some(old_engine_ref) = self.dropbear_engine_class.take() { let _ = old_engine_ref; // drop } @@ -6,7 +6,19 @@ pub mod sig; pub mod utils; use crate::scripting::error::LastErrorMessage; -use crate::scripting::native::sig::{DestroyAll, DestroyTagged, Init, LoadTagged, UpdateAll, UpdateTagged, UpdateWithEntities}; +use crate::scripting::native::sig::{ + DestroyAll, + DestroyInScopeTagged, + DestroyTagged, + Init, + LoadTagged, + PhysicsUpdateAll, + PhysicsUpdateTagged, + PhysicsUpdateWithEntities, + UpdateAll, + UpdateTagged, + UpdateWithEntities, +}; use anyhow::anyhow; use libloading::{Library, Symbol}; use std::ffi::CString; @@ -22,8 +34,12 @@ pub struct NativeLibrary { update_all_fn: Symbol<'static, UpdateAll>, update_tag_fn: Symbol<'static, UpdateTagged>, update_with_entities_fn: Symbol<'static, UpdateWithEntities>, + physics_update_all_fn: Symbol<'static, PhysicsUpdateAll>, + physics_update_tag_fn: Symbol<'static, PhysicsUpdateTagged>, + physics_update_with_entities_fn: Symbol<'static, PhysicsUpdateWithEntities>, destroy_all_fn: Symbol<'static, DestroyAll>, destroy_tagged_fn: Symbol<'static, DestroyTagged>, + destroy_in_scope_tagged_fn: Symbol<'static, DestroyInScopeTagged>, // err msg #[allow(dead_code)] @@ -64,6 +80,22 @@ impl NativeLibrary { &[b"dropbear_update_with_entities\0"], "dropbear_update_with_entities", )?; + + let physics_update_all_fn = load_symbol( + &library, + &[b"dropbear_physics_update_all\0"], + "dropbear_physics_update_all", + )?; + let physics_update_tag_fn = load_symbol( + &library, + &[b"dropbear_physics_update_tagged\0"], + "dropbear_physics_update_tagged", + )?; + let physics_update_with_entities_fn = load_symbol( + &library, + &[b"dropbear_physics_update_with_entities\0"], + "dropbear_physics_update_with_entities", + )?; let destroy_all_fn = load_symbol( &library, &[b"dropbear_destroy_all\0"], @@ -74,6 +106,11 @@ impl NativeLibrary { &[b"dropbear_destroy_tagged\0"], "dropbear_destroy_tagged", )?; + let destroy_in_scope_tagged_fn = load_symbol( + &library, + &[b"dropbear_destroy_in_scope_tagged\0"], + "dropbear_destroy_in_scope_tagged", + )?; let get_last_err_msg_fn = load_symbol( &library, &[ @@ -98,8 +135,12 @@ impl NativeLibrary { update_all_fn, update_tag_fn, update_with_entities_fn, + physics_update_all_fn, + physics_update_tag_fn, + physics_update_with_entities_fn, destroy_all_fn, destroy_tagged_fn, + destroy_in_scope_tagged_fn, get_last_err_msg_fn, set_last_err_msg_fn, }) @@ -132,6 +173,13 @@ impl NativeLibrary { } } + pub fn physics_update_all(&mut self, dt: f32) -> anyhow::Result<()> { + unsafe { + let result = (self.physics_update_all_fn)(dt); + self.handle_result(result, "physics_update_all") + } + } + pub fn update_tagged(&mut self, tag: &String, dt: f32) -> anyhow::Result<()> { unsafe { let c_string: CString = CString::new(tag.clone())?; @@ -140,6 +188,14 @@ impl NativeLibrary { } } + pub fn physics_update_tagged(&mut self, tag: &String, dt: f32) -> anyhow::Result<()> { + unsafe { + let c_string: CString = CString::new(tag.clone())?; + let result = (self.physics_update_tag_fn)(c_string.as_ptr(), dt); + self.handle_result(result, "physics_update_tagged") + } + } + pub fn update_systems_for_entities( &self, tag: &str, @@ -158,6 +214,24 @@ impl NativeLibrary { } } + pub fn physics_update_systems_for_entities( + &self, + tag: &str, + entity_ids: &[u64], + dt: f32, + ) -> anyhow::Result<()> { + unsafe { + let c_string = CString::new(tag)?; + let result = (self.physics_update_with_entities_fn)( + c_string.as_ptr(), + entity_ids.as_ptr(), + entity_ids.len() as i32, + dt, + ); + self.handle_result(result, "physics_update_with_entities") + } + } + pub fn destroy_all(&mut self) -> anyhow::Result<()> { unsafe { let result = (self.destroy_all_fn)(); @@ -172,6 +246,14 @@ impl NativeLibrary { self.handle_result(result, "destroy_tagged") } } + + pub fn destroy_in_scope_tagged(&mut self, tag: String) -> anyhow::Result<()> { + unsafe { + let c_string: CString = CString::new(tag)?; + let result = (self.destroy_in_scope_tagged_fn)(c_string.as_ptr()); + self.handle_result(result, "destroy_in_scope_tagged") + } + } } impl NativeLibrary { @@ -18,8 +18,22 @@ pub type UpdateWithEntities = unsafe extern "C" fn( entity_count: i32, dt: f32 ) -> i32; + +/// CName: `dropbear_physics_update_all` +pub type PhysicsUpdateAll = unsafe extern "C" fn(dt: f32) -> i32; +/// CName: `dropbear_physics_update_tagged` +pub type PhysicsUpdateTagged = unsafe extern "C" fn(tag: *const c_char, dt: f32) -> i32; +/// CName: `dropbear_physics_update_with_entities` +pub type PhysicsUpdateWithEntities = unsafe extern "C" fn( + tag: *const c_char, + entities: *const u64, + entity_count: i32, + dt: f32 +) -> i32; /// CName: `dropbear_destroy_tagged` pub type DestroyTagged = unsafe extern "C" fn(tag: *const c_char) -> i32; +/// CName: `dropbear_destroy_in_scope_tagged` +pub type DestroyInScopeTagged = unsafe extern "C" fn(tag: *const c_char) -> i32; /// CName: `dropbear_destroy_all` pub type DestroyAll = unsafe extern "C" fn() -> i32; @@ -33,6 +33,8 @@ impl Scene for AboutWindow { self.window = Some(graphics.shared.window.id()); } + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn update(&mut self, _dt: f32, graphics: &mut RenderContext) { CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { ui.centered_and_justified(|ui| { @@ -35,6 +35,8 @@ impl Scene for DebugWindow { self.window = Some(graphics.shared.window.id()); } + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn update(&mut self, _dt: f32, graphics: &mut RenderContext) { CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { ui.label("Hello Debug Window!"); @@ -79,6 +79,8 @@ impl Scene for Editor { self.is_world_loaded.mark_scene_loaded(); } + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn update(&mut self, dt: f32, graphics: &mut RenderContext) { if let Some(rx) = &self.play_mode_exit_rx { if rx.try_recv().is_ok() { @@ -11,3 +11,4 @@ pub mod stats; pub mod utils; pub mod runtime; pub mod about; +pub mod process; @@ -239,6 +239,8 @@ impl Scene for MainMenu { log::info!("Loaded main menu scene"); } + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} fn render(&mut self, graphics: &mut RenderContext) { @@ -0,0 +1,38 @@ +//! Functions related to processes and PIDs that aren't part of the std library. + +use std::io; +use std::process::Command; + +#[cfg(unix)] +pub fn kill_process(pid: u32) -> io::Result<()> { + let status = Command::new("kill") + .arg("-15") // SIGTERM + .arg(pid.to_string()) + .status()?; + + if !status.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to kill process {}", pid) + )); + } + + Ok(()) +} + +#[cfg(windows)] +pub fn kill_process(pid: u32) -> io::Result<()> { + let status = Command::new("taskkill") + .arg("/PID") + .arg(pid.to_string()) + .status()?; + + if !status.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to kill process {}", pid) + )); + } + + Ok(()) +} @@ -217,18 +217,9 @@ impl Scene for PlayMode { if let Some(active_camera) = self.active_camera { if let Ok(cam) = self.world.query_one_mut::<&mut Camera>(active_camera) { - if !self.has_initial_resize_done && self.display_settings.maintain_aspect_ratio { - let window_size = graphics.shared.window.inner_size(); - let chrome_height = window_size.height as f32 - available_size.y; - - let new_viewport_height = available_size.x / cam.aspect as f32; - let new_window_height = new_viewport_height + chrome_height; - - let _ = graphics.shared.window.request_inner_size(winit::dpi::PhysicalSize::new( - window_size.width, - new_window_height as u32 - )); - + if !self.has_initial_resize_done { + cam.aspect = (available_size.x / available_size.y) as f64; + self.has_initial_resize_done = true; } @@ -270,11 +261,17 @@ impl Scene for PlayMode { } }); - - self.input_state.mouse_delta = None; } + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) { + if self.scripts_ready { + if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) { + panic!("Script physics update error: {}", e); + } + } + } + fn render(&mut self, graphics: &mut RenderContext) { let Some(active_camera) = self.active_camera else { return; @@ -424,11 +424,11 @@ impl SignalController for Editor { Ok(()) } Signal::StopPlaying => { - self.editor_state = EditorState::Editing; if let Some(pid) = self.play_mode_pid { - log::info!("Play mode process {} should exit soon", pid); + log::debug!("Play mode requested to be exited, killing processes [{}]", pid); + let _ = crate::process::kill_process(pid); } self.play_mode_pid = None; @@ -326,6 +326,7 @@ impl NerdStats { impl Scene for NerdStats { fn load(&mut self, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} fn update(&mut self, dt: f32, _graphics: &mut RenderContext) { self.record_stats(dt, self.entity_count); } @@ -68,6 +68,7 @@ import kotlin.experimental.ExperimentalNativeApi"# output, r#" object ScriptManager {{ + private var nativeEngine: NativeEngine? = null private var dropbearEngine: DropbearEngine? = null private val scriptsByTag: MutableMap<String, MutableList<System>> = mutableMapOf() @@ -75,12 +76,13 @@ object ScriptManager {{ try {{ val ctx = dropbearContextPtr?.pointed - val nativeEngine = NativeEngine() - nativeEngine.init(ctx) + val engine = nativeEngine ?: NativeEngine().also {{ nativeEngine = it }} + engine.init(ctx) - dropbearEngine = DropbearEngine(nativeEngine) + if (dropbearEngine == null) {{ + dropbearEngine = DropbearEngine(engine) + }} - scriptsByTag.clear() Logger.debug("Native ScriptManager initialised") return 0 }} catch (e: Exception) {{ @@ -93,10 +95,20 @@ object ScriptManager {{ fun loadSystemsByTag(tag: String): Int {{ val engine = dropbearEngine ?: return -2 try {{ + if (scriptsByTag.containsKey(tag)) {{ + Logger.trace("Systems already loaded for tag: '$tag'") + val instances = scriptsByTag[tag] ?: emptyList() + for (instance in instances) {{ + instance.attachEngine(engine) + instance.load(engine) + }} + return 0 + }} val factories = getScriptFactories(tag) val instances = factories.map {{ it() }} for (instance in instances) {{ + instance.attachEngine(engine) instance.load(engine) }} @@ -115,6 +127,8 @@ object ScriptManager {{ try {{ for (instances in scriptsByTag.values) {{ for (instance in instances) {{ + instance.attachEngine(engine) + instance.clearCurrentEntity() instance.update(engine, dt) }} }} @@ -131,6 +145,8 @@ object ScriptManager {{ try {{ val instances = scriptsByTag[tag] ?: emptyList() for (instance in instances) {{ + instance.attachEngine(engine) + instance.clearCurrentEntity() instance.update(engine, dt) }} return 0 @@ -141,7 +157,42 @@ object ScriptManager {{ }} }} -fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCount: Int, dt: Float): Int {{ + fun physicsUpdateAllSystems(dt: Float): Int {{ + val engine = dropbearEngine ?: return -2 + try {{ + for (instances in scriptsByTag.values) {{ + for (instance in instances) {{ + instance.attachEngine(engine) + instance.clearCurrentEntity() + instance.physicsUpdate(engine, dt) + }} + }} + return 0 + }} catch (e: Exception) {{ + dropbear_set_last_error("Error physics updating all systems: ${{e.message}}") + e.printStackTrace() + return -1 + }} + }} + + fun physicsUpdateSystemsByTag(tag: String, dt: Float): Int {{ + val engine = dropbearEngine ?: return -2 + try {{ + val instances = scriptsByTag[tag] ?: emptyList() + for (instance in instances) {{ + instance.attachEngine(engine) + instance.clearCurrentEntity() + instance.physicsUpdate(engine, dt) + }} + return 0 + }} catch (e: Exception) {{ + dropbear_set_last_error("Error physics updating systems for tag '$tag': ${{e.message}}") + e.printStackTrace() + return -1 + }} + }} + + fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCount: Int, dt: Float): Int {{ val engine = dropbearEngine ?: return -2 try {{ val instances = scriptsByTag[tag] ?: emptyList() @@ -188,6 +239,53 @@ fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCo }} }} + fun physicsUpdateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCount: Int, dt: Float): Int {{ + val engine = dropbearEngine ?: return -2 + try {{ + val instances = scriptsByTag[tag] ?: emptyList() + + val entityIds = LongArray(entityCount) {{ index -> + entities[index].toLong() + }} + + Logger.trace("Physics updating systems for tag: $tag with $entityCount entities") + + if (instances.isEmpty()) {{ + return 0 + }} + + if (entityIds.isEmpty()) {{ + for (instance in instances) {{ + instance.physicsUpdate(engine, dt) + }} + return 0 + }} + + for (entityId in entityIds) {{ + for (instance in instances) {{ + try {{ + instance.attachEngine(engine) + instance.setCurrentEntity(entityId) + instance.physicsUpdate(engine, dt) + }} catch (ex: Exception) {{ + Logger.error("Failed to physics update system $instance for entity $entityId: ${{ex.message}}") + }} + }} + }} + + for (instance in instances) {{ + instance.clearCurrentEntity() + }} + + Logger.debug("Physics updated ${{instances.size}} system(s) for tag '$tag' with ${{entityCount}} entities") + return 0 + }} catch (e: Exception) {{ + dropbear_set_last_error("Error physics updating systems for tag '$tag' with entities: ${{e.message}}") + e.printStackTrace() + return -1 + }} + }} + fun destroyByTag(tag: String): Int {{ try {{ val engine = dropbearEngine ?: return -2 @@ -205,6 +303,22 @@ fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCo }} }} + fun destroyInScopeByTag(tag: String): Int {{ + try {{ + val engine = dropbearEngine ?: return -2 + val instances = scriptsByTag[tag] ?: emptyList() + for (instance in instances) {{ + instance.destroy(engine) + }} + Logger.debug("Destroyed (in-scope) ${{instances.size}} script(s) for tag: '$tag'") + return 0 + }} catch (e: Exception) {{ + dropbear_set_last_error("Error destroying (in-scope) systems for tag '$tag': ${{e.message}}") + e.printStackTrace() + return -1 + }} + }} + fun destroyAll(): Int {{ try {{ val engine = dropbearEngine ?: return -2 @@ -215,6 +329,7 @@ fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCo }} scriptsByTag.clear() dropbearEngine = null + nativeEngine = null return 0 }} catch (e: Exception) {{ dropbear_set_last_error("Error destroying scripts: ${{e.message}}") @@ -294,12 +409,35 @@ fun dropbear_update_systems_for_entities(tag: String?, entities: CPointer<ULongV return ScriptManager.updateSystemsForEntities(tag, entities, entityCount, dt) }} +@CName("dropbear_physics_update_all") +fun dropbear_physics_update_all_systems(dt: Float): Int {{ + return ScriptManager.physicsUpdateAllSystems(dt) +}} + +@CName("dropbear_physics_update_tagged") +fun dropbear_physics_update_systems_for_tag(tag: String?, dt: Float): Int {{ + if (tag == null) return -1 + return ScriptManager.physicsUpdateSystemsByTag(tag, dt) +}} + +@CName("dropbear_physics_update_with_entities") +fun dropbear_physics_update_systems_for_entities(tag: String?, entities: CPointer<ULongVar>?, entityCount: Int, dt: Float): Int {{ + if (tag == null || entities == null) return -1 + return ScriptManager.physicsUpdateSystemsForEntities(tag, entities, entityCount, dt) +}} + @CName("dropbear_destroy_tagged") fun dropbear_destroy(tag: String?): Int {{ if (tag == null) return -1 return ScriptManager.destroyByTag(tag) }} +@CName("dropbear_destroy_in_scope_tagged") +fun dropbear_destroy_in_scope(tag: String?): Int {{ + if (tag == null) return -1 + return ScriptManager.destroyInScopeByTag(tag) +}} + @CName("dropbear_destroy_all") fun dropbear_destroy_all(): Int {{ return ScriptManager.destroyAll() @@ -4,26 +4,75 @@ package com.dropbear * A class that contains the basic information of a system. * * The dropbear engine follows an ECS paradigm, with logic being - * provided as Systems. - * - * The main functions you would want to look at is `load`, - * `update` and `destroy`(not impl). + * provided as Systems. */ open class System { + /** + * The current entity that is being run in the system. Most often than not, it + * will have an [EntityRef] attached (because all Script components must be part + * of an entity). + */ var currentEntity: EntityRef? = null private set private var engineRef: DropbearEngine? = null + /** + * This function is called when the script module is initialised. + * + * It is only called once during scene execution. If you re-switch back + * to this scene after running the class, it will be run again. + */ open fun load(engine: DropbearEngine) {} + + /** + * This function is called for each update. + * + * It is run once for each frame, and for every frame. Since this is synced to the frame rate, using + * the [deltaTime] variable can aid you in creating uniform player speeds (or something like that). + * + * @param deltaTime - This specifies the time elapsed since the last update. + */ open fun update(engine: DropbearEngine, deltaTime: Float) {} + + /** + * This function is called for each update that is related to physics. + * + * It can be run 0, 1, 2 or more times per frame. Updating physics is done at a constant + * rate/tick (at roughly 50Hz or 0.02), which is why it is not ran as often as a standard [update]. + * + * @param deltaTime - This specifies the time elapsed since the last frame update. Likely, it's going + * to be somewhere around 50Hz. For the most part, you might not need this. + */ + open fun physicsUpdate(engine: DropbearEngine, deltaTime: Float) {} + + /** + * This function is called at the end of the script execution. + * + * It is run at the end of execution of a scene, such as when the scene switches. It is also ran once throughout + * the lifecycle of a script class. It is best to think about it like `sceneExit()` instead of `appExit()` + * + * It would be used to clean up any memory related resources (such as `SceneLoadHandle` or any memory related items). + * + * # Note + * + * The script module does not lose state (such as variables) when destroyed. It is cached internally (within the system manager), + * therefore counters and other related stuff will not lose track. + */ open fun destroy(engine: DropbearEngine) {} + /** + * Internal: This attaches the [DropbearEngine] fascade (typically created through some external location) + * to the existing system to be used. + */ fun attachEngine(engine: DropbearEngine) { engineRef = engine currentEntity?.engine = engine } + /** + * Internal: Sets the current entity of this [System] to something. + */ fun setCurrentEntity(entity: Long) { val engine = engineRef ?: run { currentEntity = null @@ -35,6 +84,9 @@ open class System { currentEntity = reference } + /** + * Internal: Clears the current entity used. + */ fun clearCurrentEntity() { currentEntity = null } @@ -6,7 +6,6 @@ import com.dropbear.logging.LogLevel import com.dropbear.logging.LogWriter import com.dropbear.logging.Logger import com.dropbear.logging.StdoutWriter -import kotlin.collections.emptyList @Suppress("UNUSED") class SystemManager( @@ -21,6 +20,27 @@ class SystemManager( private var registryClass: Class<*>? = null private val activeSystems = mutableMapOf<String, MutableList<System>>() + private fun destroySystems(tag: String, systems: List<System>) { + if (systems.isEmpty()) return + + Logger.debug("Destroying ${systems.size} system(s) for tag: $tag") + for (system in systems) { + try { + system.attachEngine(engine) + system.clearCurrentEntity() + system.destroy(engine) + } catch (ex: Exception) { + Logger.error("Failed to destroy system ${system.javaClass.name} for tag $tag: ${ex.message}") + } finally { + try { + system.clearCurrentEntity() + } catch (_: Exception) { + // ignore + } + } + } + } + init { val writerToUse = logWriter ?: StdoutWriter() Logger.init(writerToUse, logLevel ?: LogLevel.INFO, logTarget) @@ -43,6 +63,28 @@ class SystemManager( fun loadSystemsForTag(tag: String) { Logger.debug("Loading systems for tag: $tag") + + if (activeSystems.containsKey(tag)) { + Logger.trace("Systems already loaded for tag: $tag; re-running load() on existing instances") + val systems = activeSystems[tag] ?: return + for (system in systems) { + try { + system.attachEngine(engine) + system.clearCurrentEntity() + system.load(engine) + } catch (ex: Exception) { + Logger.error("Failed to load system ${system.javaClass.name} for tag $tag: ${ex.message}") + } finally { + try { + system.clearCurrentEntity() + } catch (_: Exception) { + // ignore + } + } + } + return + } + val instantiateMethod = registryClass?.getMethod("instantiateScripts", String::class.java) val systems = instantiateMethod?.invoke(registryInstance, tag) as? List<*> @@ -81,12 +123,25 @@ class SystemManager( } } + fun physicsUpdateAllSystems(deltaTime: Float) { + Logger.trace("Physics updating all systems") + for ((tag, systems) in activeSystems) { + physicsUpdateSystemsInternal(tag, systems, deltaTime) + } + } + fun updateSystemsByTag(tag: String, deltaTime: Float) { Logger.trace("Updating systems for tag: $tag") val systems = activeSystems[tag] ?: return updateSystemsInternal(tag, systems, deltaTime) } + fun physicsUpdateSystemsByTag(tag: String, deltaTime: Float) { + Logger.trace("Physics updating systems for tag: $tag") + val systems = activeSystems[tag] ?: return + physicsUpdateSystemsInternal(tag, systems, deltaTime) + } + fun updateSystemsForEntities(tag: String, entityIds: LongArray, deltaTime: Float) { Logger.trace("Updating systems for tag: $tag with ${entityIds.size} entities") val systems = activeSystems[tag] ?: return @@ -117,6 +172,36 @@ class SystemManager( } } + fun physicsUpdateSystemsForEntities(tag: String, entityIds: LongArray, deltaTime: Float) { + Logger.trace("Physics updating systems for tag: $tag with ${entityIds.size} entities") + val systems = activeSystems[tag] ?: return + + if (systems.isEmpty()) { + return + } + + if (entityIds.isEmpty()) { + physicsUpdateSystemsInternal(tag, systems, deltaTime) + return + } + + for (entityId in entityIds) { + for (system in systems) { + try { + system.attachEngine(engine) + system.setCurrentEntity(entityId) + system.physicsUpdate(engine, deltaTime) + } catch (ex: Exception) { + Logger.error("Failed to physics update system ${system.javaClass.name} for entity $entityId: ${ex.message}") + } + } + } + + for (system in systems) { + system.clearCurrentEntity() + } + } + private fun updateSystemsInternal(tag: String, systems: List<System>, deltaTime: Float) { for (system in systems) { try { @@ -129,9 +214,23 @@ class SystemManager( } } + private fun physicsUpdateSystemsInternal(tag: String, systems: List<System>, deltaTime: Float) { + for (system in systems) { + try { + system.attachEngine(engine) + system.clearCurrentEntity() + system.physicsUpdate(engine, deltaTime) + } catch (ex: Exception) { + Logger.error("Failed to physics update system ${system.javaClass.name} for tag $tag: ${ex.message}") + } + } + } + fun reloadJar(newJarPath: String) { Logger.info("Reloading systems with new jar path: $newJarPath") - activeSystems.clear() + + unloadAllSystems() + hotSwapUtility.reloadJar(newJarPath) val (instance, clazz) = loadRegistry() @@ -144,11 +243,30 @@ class SystemManager( } fun unloadSystemsByTag(tag: String) { - activeSystems.remove(tag) + val systems = activeSystems.remove(tag) + if (systems != null) { + destroySystems(tag, systems) + } + } + + /** + * Runs `System.destroy()` for the given tag without unloading/removing the instances. + * + * This is intended for scene switches: scripts leave scope and should clean up resources, + * but the classes/instances remain cached until the application stops. + */ + fun destroySystemsByTag(tag: String) { + val systems = activeSystems[tag] ?: return + destroySystems(tag, systems) } fun unloadAllSystems() { + val snapshot = activeSystems.toMap() activeSystems.clear() + + for ((tag, systems) in snapshot) { + destroySystems(tag, systems) + } } fun getSystemCount(tag: String): Int = activeSystems[tag]?.size ?: 0