kitgit

tirbofish/dropbear · commit

42c1d639374e9ee87f6571425d0f1dffcf2009a1

RELOAD ON DEMAND FINALLY WORKS HUZZAHHHHHHHH!!!! After so long on working on this component of scripting, I have finally gotten reloading working. Thanks to [this article](https://archive.md/WnlBC) and help from my chinese goat Qwen, I have managed to successfully get reload on demand working, as taken from things such as Unity and Unreal. This is really big news as I have had too many hours sunk into this. Ready to merge!

Unverified

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

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/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/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index 2228f33..292398d 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -512,7 +512,7 @@ impl App {
                     Some(app_name.replace('-', "_").as_str()),
                     LevelFilter::Debug,
                 )
-                .filter(Some("eucalyptus_core"), LevelFilter::Debug)
+                .filter(Some("eucalyptus_core"), LevelFilter::Trace)
                 .init();
 
             // setup panic
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index 20bd18a..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,11 +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"]
@@ -46,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 69c1305..23afafb 100644
--- a/eucalyptus-core/src/lib.rs
+++ b/eucalyptus-core/src/lib.rs
@@ -7,3 +7,8 @@ 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/scripting.rs b/eucalyptus-core/src/scripting.rs
index 634410a..3db81d5 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -7,7 +7,6 @@ use hecs::{Entity};
 use libloading::Library;
 use std::path::{Path, PathBuf};
 use std::{collections::HashMap};
-use ::jni::objects::{GlobalRef, JValue};
 use anyhow::Context;
 use crossbeam_channel::Sender;
 use tokio::io::{AsyncBufReadExt, BufReader};
@@ -43,9 +42,6 @@ pub struct ScriptManager {
     entity_tag_database: HashMap<String, Vec<Entity>>,
     jvm_created: bool,
     lib_path: Option<PathBuf>,
-
-    #[cfg(feature = "jvm")]
-    entity_scripts: HashMap<u32, Vec<GlobalRef>>,
 }
 
 impl ScriptManager {
@@ -56,120 +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 = get_gradle_command(project_root);
-
-        let _ = status_sender.send(BuildStatus::Building(format!("Running: {}", gradle_cmd)));
-
-        let mut child = Command::new(&gradle_cmd)
-            .current_dir(project_root)
-            .args(["--console=plain", "--no-watch-fs", "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>>,
@@ -216,22 +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)?;
 
-            for (tag, entities) in &self.entity_tag_database {
-                for entity in entities {
-                    let entity_id = entity.id();
-                    let scripts = jvm.load_scripts_for_entity(world, entity_id, tag)?;
-
-                    self.entity_scripts
-                        .entry(entity_id)
-                        .or_default()
-                        .extend(scripts);
-                }
+            for tag in self.entity_tag_database.keys() {
+                log::trace!("Loading systems for tag: {}", tag);
+                jvm.load_systems_for_tag(tag)?;
             }
             return Ok(());
         }
@@ -247,38 +127,21 @@ 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 (entity_id, scripts) in &self.entity_scripts {
-                log::debug!("Updating {} scripts for entity {}", scripts.len(), entity_id);
-
-                let engine_ref = jvm.create_engine_for_entity(world, *entity_id)?;
-
-                for script_ref in scripts {
-                    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 reload(&mut self, world_ptr: WorldPtr) -> anyhow::Result<()> {
         if let Some(jvm) = &mut self.jvm {
             jvm.reload(world_ptr)?
@@ -304,4 +167,113 @@ fn get_gradle_command(project_root: impl AsRef<Path>) -> String {
             "gradle".to_string()
         }
     }
+}
+
+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 = get_gradle_command(project_root);
+
+    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)
 }
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/jni.rs b/eucalyptus-core/src/scripting/jni.rs
index 9786c0c..a6dd9b1 100644
--- a/eucalyptus-core/src/scripting/jni.rs
+++ b/eucalyptus-core/src/scripting/jni.rs
@@ -3,6 +3,7 @@
 
 pub mod hotreload;
 
+use std::fs::ReadDir;
 use dropbear_engine::entity::AdoptedEntity;
 use hecs::World;
 use jni::objects::{GlobalRef, JClass, JString, JValue};
@@ -10,26 +11,38 @@ use jni::sys::jlong;
 use jni::sys::jobject;
 use jni::{InitArgsBuilder, JNIEnv, JNIVersion, JavaVM};
 use std::path::{Path, PathBuf};
+use crate::APP_INFO;
+use crate::logging::{LogLevel, LOG_LEVEL};
 use crate::ptr::WorldPtr;
 
+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,
-    // hot_reload_loader: Option<GlobalRef>,
 }
 
 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)
-            // todo: change the path to be the APPDATA part
-            .option("-javaagent:C:\\Users\\thrib\\dropbear\\resources\\dependencies\\hotswap-agent-2.0.0.jar")
-            .option("-XX:+PrintGCDetails")
-            .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");
@@ -37,34 +50,22 @@ impl JavaContext {
         Ok(Self {
             jvm,
             dropbear_engine_class: None,
+            system_manager_instance: None,
             jar_path: jar_path.as_ref().to_owned(),
-            // hot_reload_loader: None,
         })
     }
 
     pub fn init(&mut self, world: WorldPtr) -> anyhow::Result<()> {
         let mut env = self.jvm.attach_current_thread()?;
 
-        // if self.hot_reload_loader.is_none() {
-        //     log::trace!("Creating HotSwappableJarLoader for hot-reload support");
-        //
-        //     let loader_class = env.find_class("com/dropbear/reload/HotReloadManager")?;
-        //     let jar_path_str = env.new_string(self.jar_path.to_str().unwrap())?;
-        //
-        //     let loader_obj = env.new_object(
-        //         loader_class,
-        //         "(Ljava/lang/String;)V",
-        //         &[JValue::Object(&jar_path_str)],
-        //     )?;
-        //
-        //     self.hot_reload_loader = Some(env.new_global_ref(loader_obj)?);
-        // }
-
         if let Some(old_ref) = self.dropbear_engine_class.take() {
             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");
@@ -79,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(
@@ -89,178 +89,193 @@ 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),
+            ],
+        )?;
+
+        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(())
     }
 
-    pub fn reload(&mut self, world: WorldPtr) -> anyhow::Result<()> {
-        log::info!("Hot-reloading JAR: {}", self.jar_path.display());
+    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()?;
 
-            // if let Some(loader) = &self.hot_reload_loader {
-            //     log::trace!("Calling HotSwappableJarLoader.reload()");
-            //     env.call_method(loader.as_obj(), "reload", "()V", &[])?;
-            // }
-
-            log::trace!("Calling RunnableRegistry.reload()");
-            let registry_class = env.find_class("com/dropbear/decl/RunnableRegistry")?;
-            env.call_static_method(registry_class, "reload", "()V", &[])?;
-
-            log::trace!("Re-initialising engine with new classes");
+            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."));
         }
 
-        self.clear_engine()?;
-        self.init(world)?;
-
-        log::info!("Hot-reload complete!");
+        log::info!("Reload complete via SystemManager!");
 
         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 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()?;
 
-        let tag_jstring = env.new_string(tag)?;
+            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(())
+    }
 
-        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)],
-        )?;
+    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!("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)?);
-            }
+            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(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()?;
+    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!("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", &[])?;
+            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!("Calling NativeEngine.init() with arg {}", world as jlong);
-        env.call_method(
-            &native_engine_obj,
-            "init",
-            "(J)V",
-            &[JValue::Long(world as jlong)],
-        )?;
+    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!("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)],
-        )?;
+            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!("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),
-            ],
-        )?;
+            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!("Creating new global ref for DropbearEngine and returning");
-        Ok(env.new_global_ref(dropbear_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."))
+        }
     }
 
     pub fn clear_engine(&mut self) -> anyhow::Result<()> {
-        if let Some(old_ref) = self.dropbear_engine_class.take() {
-            let _ = old_ref; // drop
+        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
         }
-        // if let Some(ref loader) = self.hot_reload_loader {
-        //     let _ = loader;
-        // }
         Ok(())
     }
 }
@@ -270,6 +285,9 @@ 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;
+        }
     }
 }
 
diff --git a/eucalyptus-core/src/scripting/jni/hotreload.rs b/eucalyptus-core/src/scripting/jni/hotreload.rs
index 3d59704..0a9b882 100644
--- a/eucalyptus-core/src/scripting/jni/hotreload.rs
+++ b/eucalyptus-core/src/scripting/jni/hotreload.rs
@@ -1,11 +1,10 @@
 use std::path::Path;
-use std::sync::Arc;
 use crossbeam_channel::Sender;
 use tokio::io::{AsyncBufReadExt, BufReader};
 use tokio::process::Command;
+use tokio::task::JoinHandle;
 use tokio_util::sync::CancellationToken;
-use dropbear_engine::future::{FutureHandle, FutureQueue};
-use crate::scripting::{get_gradle_command};
+use crate::scripting::get_gradle_command;
 
 pub enum HotReloadEvent {
     SuccessBuild,
@@ -14,7 +13,7 @@ pub enum HotReloadEvent {
 
 pub struct HotReloader {
     cancellation_token: CancellationToken,
-    handle: Option<FutureHandle>,
+    handle: Option<JoinHandle<()>>,
 }
 
 impl HotReloader {
@@ -28,14 +27,16 @@ impl HotReloader {
     pub fn start(
         &mut self,
         project_root: impl AsRef<Path>,
-        future_queue: Arc<FutureQueue>,
         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 sender_clone = sender.clone();
 
-        let handle = future_queue.push(async move {
+        let handle = tokio::spawn(async move {
             let gradle_cmd = get_gradle_command(project_root.clone());
 
             loop {
@@ -46,9 +47,10 @@ impl HotReloader {
 
                 let mut child = match Command::new(&gradle_cmd)
                     .current_dir(&project_root)
-                    .args(["--continuous", "--console=plain", "fatJar"])
+                    .args(["--continuous", "--console=plain", "jvmJar"])
                     .stdout(std::process::Stdio::piped())
                     .stderr(std::process::Stdio::piped())
+                    .kill_on_drop(true)
                     .spawn()
                 {
                     Ok(child) => child,
@@ -65,9 +67,8 @@ impl HotReloader {
                 let stdout = child.stdout.take().expect("Stdout was piped");
                 let stderr = child.stderr.take().expect("Stderr was piped");
 
-                // Spawn child tasks with cancellation
                 let stdout_handle = tokio::spawn({
-                    let sender = sender_clone.clone();
+                    let sender = sender.clone();
                     let token = token.clone();
 
                     async move {
@@ -137,12 +138,23 @@ impl HotReloader {
                     _ = 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;
                     }
-                    _ = child.wait() => {
-                        log::warn!("Gradle process exited unexpectedly, restarting...");
+                    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);
                     }
                 }
@@ -156,20 +168,24 @@ impl HotReloader {
             log::info!("Hot reloader task finished");
         });
 
-        self.handle = Some(handle.clone());
+        self.handle = Some(handle);
     }
 
-    pub fn stop(&mut self, future_queue: Arc<FutureQueue>) {
+    pub fn stop(&mut self) {
+        log::info!("Stopping hot reloader...");
+
         self.cancellation_token.cancel();
 
         if let Some(handle) = self.handle.take() {
-            future_queue.cancel(&handle);
-            log::info!("Hot reloader stopped");
+            handle.abort();
         }
+
+        self.cancellation_token = CancellationToken::new();
     }
 
     pub fn is_running(&self) -> bool {
-        self.handle.is_some() && !self.cancellation_token.is_cancelled()
+        self.handle.as_ref().map_or(false, |h| !h.is_finished())
+            && !self.cancellation_token.is_cancelled()
     }
 }
 
@@ -181,6 +197,6 @@ impl Default for HotReloader {
 
 impl Drop for HotReloader {
     fn drop(&mut self) {
-        self.cancellation_token.cancel();
+        self.stop();
     }
 }
\ No newline at end of file
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index b3dcc58..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);
@@ -304,16 +304,6 @@ impl SignalController for Editor {
                                         return Err(anyhow::anyhow!(e));
                                     }
 
-                                    let project_path = {
-                                        PROJECT.read().project_path.clone()
-                                    };
-
-                                    let (tx, rx) = crossbeam_channel::unbounded();
-
-                                    self.hot_reload_rx = Some(rx);
-
-                                    // self.hot_reloader.start(project_path, graphics.future_queue.clone(), tx);
-
                                     let world_ptr = self.world.as_mut() as *mut World;
 
                                     if let Err(e) = self.script_manager
@@ -402,13 +392,9 @@ impl SignalController for Editor {
                         self.show_build_error_window = false;
                     }
                 }
-
-                // self.signal = Signal::None;
                 Ok(())
             }
             Signal::StopPlaying => {
-                // self.hot_reloader.stop(graphics.future_queue.clone());
-
                 if let Err(e) = self.restore() {
                     warn!("Failed to restore from play mode backup: {}", e);
                     log::warn!("Failed to restore scene state: {}", e);
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!")
         }
     }