kitgit

tirbofish/dropbear · diff

2bb2544 · tk

transfer to my PC, i think i have gotten most of the hot reload managers

Unverified

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/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index 7951578..20bd18a 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -34,6 +34,7 @@ interprocess = "2.2"
 serde_json = "1.0.143"
 libloading = "0.8.9"
 async-process = "2.5"
+tokio-util = "0.7"
 
 [features]
 editor = ["jvm"]
diff --git a/eucalyptus-core/src/lib.rs b/eucalyptus-core/src/lib.rs
index d03d498..69c1305 100644
--- a/eucalyptus-core/src/lib.rs
+++ b/eucalyptus-core/src/lib.rs
@@ -6,3 +6,4 @@ pub mod scripting;
 pub mod spawn;
 pub mod states;
 pub mod utils;
+pub mod ptr;
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 20a007a..f82aa8e 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -2,8 +2,8 @@ pub mod jni;
 pub mod kmp;
 
 use crate::input::InputState;
-use crate::scripting::jni::{JavaContext, WorldPtr};
-use crate::states::{EntityNode, PROJECT, SOURCE, ScriptComponent};
+use crate::scripting::jni::JavaContext;
+use crate::states::{EntityNode, ScriptComponent, PROJECT, SOURCE};
 use dropbear_engine::entity::{AdoptedEntity, Transform};
 use hecs::{Entity, World};
 use libloading::Library;
@@ -14,6 +14,7 @@ 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 +44,10 @@ pub struct ScriptManager {
     script_target: ScriptTarget,
     entity_tag_database: HashMap<String, Vec<Entity>>,
     jvm_created: bool,
+    lib_path: Option<PathBuf>,
 
     #[cfg(feature = "jvm")]
     entity_scripts: HashMap<u32, Vec<GlobalRef>>,
-    // the native version must be stateless, and doesn't have such a thing
 }
 
 impl ScriptManager {
@@ -58,6 +59,7 @@ impl ScriptManager {
             entity_tag_database: HashMap::new(),
             jvm_created: false,
             entity_scripts: Default::default(),
+            lib_path: None,
         })
     }
 
@@ -81,21 +83,7 @@ impl ScriptManager {
 
         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 gradle_cmd = get_gradle_command(project_root);
 
         let _ = status_sender.send(BuildStatus::Building(format!("Running: {}", gradle_cmd)));
 
@@ -194,6 +182,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 +191,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 +208,7 @@ impl ScriptManager {
                 self.jvm = None;
                 self.library = None;
                 self.jvm_created = false;
+                self.lib_path = None;
             }
         }
 
@@ -264,27 +258,21 @@ impl ScriptManager {
         {
             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),
-                                ],
-                            )?;
-                        }
-                    }
+            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),
+                        ],
+                    )?;
                 }
             }
             return Ok(());
@@ -292,6 +280,32 @@ impl ScriptManager {
 
         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)?
+        }
+        Ok(())
+    }
+}
+
+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()
+        }
+    }
 }
 
 pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> {
@@ -342,7 +356,8 @@ pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> {
                 break;
             }
             Err(e) => {
-                if e.raw_os_error() == Some(32) && attempt < MAX_RETRIES {
+                if let Some(e_int) = e.raw_os_error() && e_int == 32
+                && attempt < MAX_RETRIES {
                     log::warn!(
                         "Sharing violation copying script (attempt {}/{}). Retrying in {}ms: {}",
                         attempt + 1,
diff --git a/eucalyptus-core/src/scripting/jni.rs b/eucalyptus-core/src/scripting/jni.rs
index 6c46708..98514f0 100644
--- a/eucalyptus-core/src/scripting/jni.rs
+++ b/eucalyptus-core/src/scripting/jni.rs
@@ -1,20 +1,23 @@
 #![allow(non_snake_case)]
 //! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate
 
+pub mod hotreload;
+
 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};
-
-pub type WorldPtr = *mut World;
+use std::path::{Path, PathBuf};
+use crate::ptr::WorldPtr;
 
 /// Provides a context for any eucalyptus-core JNI calls and JVM hosting.
 pub struct JavaContext {
     pub(crate) jvm: JavaVM,
     dropbear_engine_class: Option<GlobalRef>,
+    pub(crate) jar_path: PathBuf,
+    hot_reload_loader: Option<GlobalRef>,
 }
 
 impl JavaContext {
@@ -31,12 +34,29 @@ impl JavaContext {
         Ok(Self {
             jvm,
             dropbear_engine_class: 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
         }
@@ -72,11 +92,29 @@ impl JavaContext {
         Ok(())
     }
 
-    pub fn clear_engine(&mut self) -> anyhow::Result<()> {
-        if let Some(old_ref) = self.dropbear_engine_class.take() {
-            let _ = old_ref; // drop
+    pub fn reload(&mut self, world: WorldPtr) -> anyhow::Result<()> {
+        log::info!("Hot-reloading JAR: {}", self.jar_path.display());
+
+        {
+            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");
         }
-        self.dropbear_engine_class = None;
+
+        self.clear_engine()?;
+        self.init(world)?;
+
+        log::info!("Hot-reload complete!");
+
         Ok(())
     }
 
@@ -206,13 +244,22 @@ impl JavaContext {
             "(Lcom/dropbear/ffi/NativeEngine;)V",
             &[
                 JValue::Object(&native_engine_obj),
-                // JValue::Object(&entity_ref_obj),
             ],
         )?;
 
         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_ref) = self.dropbear_engine_class.take() {
+            let _ = old_ref; // drop
+        }
+        if let Some(ref loader) = self.hot_reload_loader {
+            let _ = loader;
+        }
+        Ok(())
+    }
 }
 
 impl Drop for JavaContext {
@@ -222,6 +269,7 @@ impl Drop for JavaContext {
         }
     }
 }
+
 #[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..3d59704
--- /dev/null
+++ b/eucalyptus-core/src/scripting/jni/hotreload.rs
@@ -0,0 +1,186 @@
+use std::path::Path;
+use std::sync::Arc;
+use crossbeam_channel::Sender;
+use tokio::io::{AsyncBufReadExt, BufReader};
+use tokio::process::Command;
+use tokio_util::sync::CancellationToken;
+use dropbear_engine::future::{FutureHandle, FutureQueue};
+use crate::scripting::{get_gradle_command};
+
+pub enum HotReloadEvent {
+    SuccessBuild,
+    FailedBuild(String),
+}
+
+pub struct HotReloader {
+    cancellation_token: CancellationToken,
+    handle: Option<FutureHandle>,
+}
+
+impl HotReloader {
+    pub fn new() -> Self {
+        Self {
+            cancellation_token: CancellationToken::new(),
+            handle: None,
+        }
+    }
+
+    pub fn start(
+        &mut self,
+        project_root: impl AsRef<Path>,
+        future_queue: Arc<FutureQueue>,
+        sender: Sender<HotReloadEvent>,
+    ) {
+        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 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", "fatJar"])
+                    .stdout(std::process::Stdio::piped())
+                    .stderr(std::process::Stdio::piped())
+                    .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");
+
+                // Spawn child tasks with cancellation
+                let stdout_handle = tokio::spawn({
+                    let sender = sender_clone.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;
+                        stdout_handle.abort();
+                        stderr_handle.abort();
+                        break;
+                    }
+                    _ = child.wait() => {
+                        log::warn!("Gradle process exited unexpectedly, restarting...");
+                        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.clone());
+    }
+
+    pub fn stop(&mut self, future_queue: Arc<FutureQueue>) {
+        self.cancellation_token.cancel();
+
+        if let Some(handle) = self.handle.take() {
+            future_queue.cancel(&handle);
+            log::info!("Hot reloader stopped");
+        }
+    }
+
+    pub fn is_running(&self) -> bool {
+        self.handle.is_some() && !self.cancellation_token.is_cancelled()
+    }
+}
+
+impl Default for HotReloader {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Drop for HotReloader {
+    fn drop(&mut self) {
+        self.cancellation_token.cancel();
+    }
+}
\ 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..1e0f098 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -1,3 +1,4 @@
+use crossbeam_channel::TryRecvError;
 use super::*;
 use crate::signal::SignalController;
 use crate::spawn::PendingSpawnController;
@@ -112,6 +113,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 6d8e3af..7cdb173 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -304,6 +304,16 @@ 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
@@ -397,6 +407,8 @@ impl SignalController for Editor {
                 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);
@@ -406,11 +418,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/src/jvmMain/kotlin/com/dropbear/reload/HotReloadManager.kt b/src/jvmMain/kotlin/com/dropbear/reload/HotReloadManager.kt
new file mode 100644
index 0000000..b218779
--- /dev/null
+++ b/src/jvmMain/kotlin/com/dropbear/reload/HotReloadManager.kt
@@ -0,0 +1,44 @@
+package com.dropbear.reload
+
+import java.net.URLClassLoader
+import java.nio.file.Paths
+
+class HotReloadManager(private val jarPath: String) : AutoCloseable {
+    @Volatile
+    private var currentLoader: URLClassLoader? = null
+
+    init {
+        reload()
+    }
+
+    @Synchronized
+    fun reload() {
+        currentLoader?.close()
+
+        val jarUrl = Paths.get(jarPath).toUri().toURL()
+
+        currentLoader = URLClassLoader(
+            arrayOf(jarUrl),
+            ClassLoader.getSystemClassLoader().parent
+        )
+    }
+
+    fun loadClass(className: String): Class<*> {
+        return currentLoader?.loadClass(className)
+            ?: throw IllegalStateException("ClassLoader not initialised")
+    }
+
+    fun createInstance(className: String): Any {
+        val clazz = loadClass(className)
+        return clazz.getDeclaredConstructor().newInstance()
+    }
+
+    fun getCurrentLoader(): ClassLoader {
+        return currentLoader ?: throw IllegalStateException("ClassLoader not initialised")
+    }
+
+    override fun close() {
+        currentLoader?.close()
+        currentLoader = null
+    }
+}
\ No newline at end of file