kitgit

tirbofish/dropbear · commit

69aec71b868b28ec0c6ace0cea504c4f9266022a

Merge pull request #51 feature: swap on demand

Unverified

tk <4tkbytes@pm.me> · 2025-10-15 10:39

view full diff

diff --git a/build.gradle.kts b/build.gradle.kts
index 38a2db5..639a8a0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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 {
             }
         }
     }
-}
\ No newline at end of file
+}
+
+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 {}
+}
diff --git a/docs/notes.md b/docs/notes.md
index 7ebf38e..15b2c4d 100644
--- a/docs/notes.md
+++ b/docs/notes.md
@@ -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. 
+
+
diff --git a/dropbear-engine/src/graphics.rs b/dropbear-engine/src/graphics.rs
index 08e9c25..c3196db 100644
--- a/dropbear-engine/src/graphics.rs
+++ b/dropbear-engine/src/graphics.rs
@@ -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,
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index 7951578..a96626c 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -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"
diff --git a/eucalyptus-core/src/lib.rs b/eucalyptus-core/src/lib.rs
index d03d498..23afafb 100644
--- a/eucalyptus-core/src/lib.rs
+++ b/eucalyptus-core/src/lib.rs
@@ -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",
+};
diff --git a/eucalyptus-core/src/logging.rs b/eucalyptus-core/src/logging.rs
index 3af2304..a5196aa 100644
--- a/eucalyptus-core/src/logging.rs
+++ b/eucalyptus-core/src/logging.rs
@@ -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(
diff --git a/eucalyptus-core/src/ptr.rs b/eucalyptus-core/src/ptr.rs
new file mode 100644
index 0000000..f0e1593
--- /dev/null
+++ b/eucalyptus-core/src/ptr.rs
@@ -0,0 +1,3 @@
+use hecs::World;
+
+pub type WorldPtr = *mut World;
diff --git a/eucalyptus-core/src/scripting.rs b/eucalyptus-core/src/scripting.rs
index 21c5322..3db81d5 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -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)
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/jni.rs b/eucalyptus-core/src/scripting/jni.rs
index 6c46708..a6dd9b1 100644
--- a/eucalyptus-core/src/scripting/jni.rs
+++ b/eucalyptus-core/src/scripting/jni.rs
@@ -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);
diff --git a/eucalyptus-core/src/scripting/jni/hotreload.rs b/eucalyptus-core/src/scripting/jni/hotreload.rs
new file mode 100644
index 0000000..0a9b882
--- /dev/null
+++ b/eucalyptus-core/src/scripting/jni/hotreload.rs
@@ -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();
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 6a4c343..d155860 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -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,
         })
     }
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 4be71e3..29750bb 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -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(
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index 9cfc628..b819490 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -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...");
 
diff --git a/magna-carta/src/generator/jvm.rs b/magna-carta/src/generator/jvm.rs
index 74b1c89..7b37ce7 100644
--- a/magna-carta/src/generator/jvm.rs
+++ b/magna-carta/src/generator/jvm.rs
@@ -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, "}}")?;
 
diff --git a/resources/dependencies/hotswap-agent-2.0.0.jar b/resources/dependencies/hotswap-agent-2.0.0.jar
new file mode 100644
index 0000000..cfd2326
Binary files /dev/null and b/resources/dependencies/hotswap-agent-2.0.0.jar differ
diff --git a/src/commonMain/kotlin/com/dropbear/logging/LogLevel.kt b/src/commonMain/kotlin/com/dropbear/logging/LogLevel.kt
new file mode 100644
index 0000000..91fed03
--- /dev/null
+++ b/src/commonMain/kotlin/com/dropbear/logging/LogLevel.kt
@@ -0,0 +1,9 @@
+package com.dropbear.logging
+
+enum class LogLevel {
+    TRACE,
+    DEBUG,
+    INFO,
+    WARN,
+    ERROR
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/logging/LogWriter.kt b/src/commonMain/kotlin/com/dropbear/logging/LogWriter.kt
new file mode 100644
index 0000000..c196769
--- /dev/null
+++ b/src/commonMain/kotlin/com/dropbear/logging/LogWriter.kt
@@ -0,0 +1,5 @@
+package com.dropbear.logging
+
+interface LogWriter {
+    fun log(level: LogLevel, target: String, message: String, file: String?, line: Int?)
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/logging/Logger.kt b/src/commonMain/kotlin/com/dropbear/logging/Logger.kt
new file mode 100644
index 0000000..2e7340f
--- /dev/null
+++ b/src/commonMain/kotlin/com/dropbear/logging/Logger.kt
@@ -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)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt b/src/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt
new file mode 100644
index 0000000..0a4791b
--- /dev/null
+++ b/src/commonMain/kotlin/com/dropbear/logging/StdoutWriter.kt
@@ -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")
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/jvmMain/kotlin/com/dropbear/host/HotSwapUtility.kt b/src/jvmMain/kotlin/com/dropbear/host/HotSwapUtility.kt
new file mode 100644
index 0000000..a78165d
--- /dev/null
+++ b/src/jvmMain/kotlin/com/dropbear/host/HotSwapUtility.kt
@@ -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()
+    }
+}
\ No newline at end of file
diff --git a/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt b/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
new file mode 100644
index 0000000..5245299
--- /dev/null
+++ b/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
@@ -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
+    }
+}
\ No newline at end of file
diff --git a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
index dcc69ad..faadbe6 100644
--- a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
+++ b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
@@ -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!")
         }
     }