kitgit

tirbofish/dropbear · commit

2300585c9666fc6a3040c1fa503516b3cad20f66

plugin loading system and the very basics of an SDK i need to create a console and an error dock in the editor, and work on some more scripting API's. also improved gradle build times on windows by expanding the memory from 512MB to 2GB so i suppose thats also nice :)

Unverified

tk <4tkbytes@pm.me> · 2025-10-17 13:33

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 0ac2739..cb032b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ package.repository = "https://github.com/4tkbytes/dropbear"
 package.readme = "README.md"
 
 resolver = "3"
-members = [ "dropbear-engine", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "magna-carta"]
+members = [ "dropbear-engine", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "eucalyptus-sdk", "magna-carta"]
 # members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"]
 
 
@@ -52,10 +52,7 @@ winit = { version = "0.30", features = [] }
 zip = "5.1"
 walkdir = "2.3"
 rayon = "1.11"
-flate2 = "1.1"
 reqwest = { version = "0.11", features = ["stream"] }
-tar = "0.4"
-futures-util = "0.3"
 backtrace = "0.3"
 gltf = "1"
 os_info = "3.12"
@@ -63,6 +60,9 @@ rustc_version_runtime = "0.3"
 jni = { version = "0.21", features = ["invocation"] }
 tree-sitter = "0.22"
 tree-sitter-kotlin = "0.3.8"
+libloading = "0.8"
+indexmap = "2.11"
+rand = "0.9"
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/README.md b/README.md
index f2f392e..67c9f0b 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ If you might have not realised, all the crates/projects names are after Australi
 - [eucalyptus-editor](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
 - [eucalyptus-core](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other.
 - [redback-runtime](https://github.com/4tkbytes/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
+- [eucalyptus-sdk](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus-sdk) is used to develop plugins to be used with the `eucalyptus-editor`
 
 ### Related Projects
 
diff --git a/eucalyptus-core/src/lib.rs b/eucalyptus-core/src/lib.rs
index 23afafb..0c2bfe9 100644
--- a/eucalyptus-core/src/lib.rs
+++ b/eucalyptus-core/src/lib.rs
@@ -8,6 +8,8 @@ pub mod states;
 pub mod utils;
 pub mod ptr;
 
+pub use egui;
+
 pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo {
     name: "Eucalyptus",
     author: "4tkbytes",
diff --git a/eucalyptus-core/src/states.rs b/eucalyptus-core/src/states.rs
index 4f9cd85..c42519a 100644
--- a/eucalyptus-core/src/states.rs
+++ b/eucalyptus-core/src/states.rs
@@ -1355,7 +1355,6 @@ impl SceneConfig {
         );
         #[cfg(feature = "editor")]
         {
-            // Editor mode - look for debug camera, create one if none exists
             let debug_camera = {
                 world
                     .query::<(&Camera, &CameraComponent)>()
@@ -1392,7 +1391,6 @@ impl SceneConfig {
 
         #[cfg(not(feature = "editor"))]
         {
-            // Runtime mode - look for player camera, panic if none exists
             let player_camera = world
                 .query::<(&Camera, &CameraComponent)>()
                 .iter()
@@ -1460,7 +1458,12 @@ pub enum EditorTab {
     ResourceInspector, // left side,
     ModelEntityList,   // right side,
     Viewport,          // middle,
-    KotlinREPL,        // bottom side - new REPL tab
+    Plugin(usize),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct PluginInfo {
+    pub display_name: String,
 }
 
 /// An enum that describes the status of loading the world.
diff --git a/eucalyptus-editor/Cargo.toml b/eucalyptus-editor/Cargo.toml
index fee22d2..0b00577 100644
--- a/eucalyptus-editor/Cargo.toml
+++ b/eucalyptus-editor/Cargo.toml
@@ -26,7 +26,6 @@ hecs.workspace = true
 log.workspace = true
 log-once.workspace = true
 model_to_image.workspace = true
-open.workspace = true
 parking_lot.workspace = true
 transform-gizmo-egui.workspace = true
 wgpu.workspace = true
@@ -35,11 +34,9 @@ clap.workspace = true
 walkdir.workspace = true
 zip.workspace = true
 tokio.workspace = true
-reqwest.workspace = true
-tar.workspace = true
-flate2.workspace = true
 crossbeam-channel.workspace = true
-pollster = "0.4.0"
+libloading.workspace = true
+indexmap.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
@@ -51,5 +48,4 @@ editor = ["eucalyptus-core/editor"]
 [build-dependencies]
 anyhow = "1.0"
 app_dirs2 = "2.5"
-reqwest = { version = "0.12", features = ["blocking"] }
 zip = "4.3"
diff --git a/eucalyptus-editor/src/editor/dock.rs b/eucalyptus-editor/src/editor/dock.rs
index 2d338ba..b6690e1 100644
--- a/eucalyptus-editor/src/editor/dock.rs
+++ b/eucalyptus-editor/src/editor/dock.rs
@@ -2,7 +2,7 @@ use super::*;
 use crate::editor::ViewportMode;
 use std::{collections::HashSet, sync::LazyLock};
 
-use crate::APP_INFO;
+use eucalyptus_core::APP_INFO;
 use crate::editor::component::InspectableComponent;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
 use dropbear_engine::{
@@ -10,14 +10,15 @@ use dropbear_engine::{
     lighting::{Light, LightComponent},
 };
 use egui;
-use egui::CollapsingHeader;
-use egui_dock_fork::TabViewer;
+use egui::{CollapsingHeader};
+use egui_dock_fork::{TabViewer};
 use egui_extras;
 use eucalyptus_core::spawn::{PendingSpawn, push_pending_spawn};
 use eucalyptus_core::states::{File, Node, RESOURCES, ResourceType};
 use log;
 use parking_lot::Mutex;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode, math::DVec3};
+use crate::plugin::PluginRegistry;
 
 pub struct EditorTabViewer<'a> {
     pub view: egui::TextureId,
@@ -32,6 +33,9 @@ pub struct EditorTabViewer<'a> {
     pub gizmo_mode: &'a mut EnumSet<GizmoMode>,
     pub editor_mode: &'a mut EditorState,
     pub active_camera: &'a mut Arc<Mutex<Option<hecs::Entity>>>,
+    pub plugin_registry: &'a mut PluginRegistry,
+    // "wah wah its unsafe, its using raw pointers" shut the fuck up if it breaks i will know
+    pub editor: *mut Editor,
 }
 
 impl<'a> EditorTabViewer<'a> {
@@ -77,10 +81,6 @@ pub struct DraggedAsset {
 pub static TABS_GLOBAL: LazyLock<Mutex<StaticallyKept>> =
     LazyLock::new(|| Mutex::new(StaticallyKept::default()));
 
-// Separate static for REPL to avoid deadlocks
-pub static KOTLIN_REPL: LazyLock<Mutex<Option<crate::editor::repl::KotlinREPL>>> =
-    LazyLock::new(|| Mutex::new(None));
-
 /// Variables kept statically.
 ///
 /// The entire module (including the tab viewer) due to it
@@ -114,7 +114,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
             EditorTab::ModelEntityList => "Model/Entity List".into(),
             EditorTab::AssetViewer => "Asset Viewer".into(),
             EditorTab::ResourceInspector => "Resource Inspector".into(),
-            EditorTab::KotlinREPL => "Kotlin REPL".into(),
+            EditorTab::Plugin(dock_index) => {
+                if let Some((_, plugin)) = self.plugin_registry.plugins.get_index_mut(*dock_index) {
+                    plugin.display_name().into()
+                } else {
+                    "Unknown Plugin Name".into()
+                }
+            }
         }
     }
 
@@ -202,7 +208,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                 self.world.query_one::<(
                                     &Camera,
                                     &CameraComponent,
-                                    // Option<&CameraFollowTarget>,
                                 )>(active_camera)
                             {
                                 if let Some((camera, _)) = q.get() {
@@ -934,17 +939,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                         log_once::debug_once!("Unable to query entity inside resource inspector");
                     }
 
-                    // if let Some(e) = local_insert_follow_target {
-                    //     match self.world.insert_one(e, CameraFollowTarget::default()) {
-                    //         Ok(_) => {
-                    //             success!("Added CameraFollowTarget");
-                    //         }
-                    //         Err(e) => {
-                    //             warn!("Unable to add CameraFollowTarget: {}", e);
-                    //         }
-                    //     }
-                    // }
-
                     // lighting system
                     if let Ok(mut q) = self
                         .world
@@ -1022,16 +1016,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                 self.signal,
                                 &mut camera.label.clone(),
                             );
-                            // if let Some(target) = follow_target {
-                            //     target.inspect(
-                            //         entity,
-                            //         &mut cfg,
-                            //         ui,
-                            //         self.undo_stack,
-                            //         self.signal,
-                            //         &mut camera.label.clone(),
-                            //     );
-                            // }
 
                             ui.separator();
 
@@ -1075,15 +1059,18 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                     ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!");
                 }
             }
-            EditorTab::KotlinREPL => {
-                // Use separate static to avoid deadlock with TABS_GLOBAL
-                let mut repl_lock = KOTLIN_REPL.lock();
-                if repl_lock.is_none() {
-                    *repl_lock = Some(crate::editor::repl::KotlinREPL::new());
+            EditorTab::Plugin(dock_info) => {
+                if self.editor.is_null() {
+                    panic!("Editor pointer is null, unexpected behaviour");
                 }
-
-                if let Some(repl) = repl_lock.as_mut() {
-                    repl.ui(ui);
+                let editor = unsafe { &mut *self.editor };
+                if let Some((_, plugin)) = self.plugin_registry.plugins.get_index_mut(*dock_info) {
+                    plugin.ui(ui, editor);
+                } else {
+                    ui.colored_label(
+                        egui::Color32::RED,
+                        format!("Plugin at index '{}' not found", *dock_info),
+                    );
                 }
             }
         }
@@ -1136,8 +1123,20 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                 menu_action = Some(EditorTabMenuAction::ViewportOption);
                             }
                         }
-                        EditorTab::KotlinREPL => {
-                            // No context menu actions for REPL tab yet
+                        EditorTab::Plugin(dock_info) => {
+                            if self.editor.is_null() {
+                                panic!("Editor pointer is null, unexpected behaviour");
+                            }
+
+                            let editor = unsafe { &mut *self.editor };
+                            if let Some((_, plugin)) = self.plugin_registry.plugins.get_index_mut(dock_info) {
+                                plugin.context_menu(ui, editor);
+                            } else {
+                                ui.colored_label(
+                                    egui::Color32::RED,
+                                    format!("Plugin at index '{}' not found", dock_info),
+                                );
+                            }
                         }
                     }
                 })
diff --git a/eucalyptus-editor/src/editor/input.rs b/eucalyptus-editor/src/editor/input.rs
index b594342..05860f3 100644
--- a/eucalyptus-editor/src/editor/input.rs
+++ b/eucalyptus-editor/src/editor/input.rs
@@ -208,6 +208,8 @@ impl Keyboard for Editor {
                     } else {
                         self.switch_to_debug_camera();
                     }
+                } else {
+                    self.input_state.pressed_keys.insert(key);
                 }
             }
             KeyCode::KeyX => {
@@ -221,12 +223,15 @@ impl Keyboard for Editor {
             KeyCode::KeyP => {
                 if !is_playing && ctrl_pressed {
                     self.signal = Signal::Play
+                } else {
+                    self.input_state.pressed_keys.insert(key);
                 }
             }
             _ => {
                 self.input_state.pressed_keys.insert(key);
             }
         }
+        self.input_state.pressed_keys.insert(key);
     }
 
     fn key_up(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) {
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 9e040ac..f857ca1 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -1,7 +1,6 @@
 pub mod component;
 pub mod dock;
 pub mod input;
-pub mod repl;
 pub mod scene;
 
 pub(crate) use crate::editor::dock::*;
@@ -47,58 +46,59 @@ use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode};
 use wgpu::{Color, Extent3d, RenderPipeline};
 use winit::{keyboard::KeyCode, window::Window};
+use crate::plugin::PluginRegistry;
 
 pub struct Editor {
-    scene_command: SceneCommand,
+    pub scene_command: SceneCommand,
     pub world: Box<World>,
-    dock_state: DockState<EditorTab>,
-    texture_id: Option<egui::TextureId>,
-    size: Extent3d,
-    render_pipeline: Option<RenderPipeline>,
-    light_manager: LightManager,
-    color: Color,
+    pub dock_state: DockState<EditorTab>,
+    pub texture_id: Option<egui::TextureId>,
+    pub size: Extent3d,
+    pub render_pipeline: Option<RenderPipeline>,
+    pub light_manager: LightManager,
+    pub color: Color,
 
-    active_camera: Arc<Mutex<Option<hecs::Entity>>>,
+    pub active_camera: Arc<Mutex<Option<hecs::Entity>>>,
 
-    is_viewport_focused: bool,
+    pub is_viewport_focused: bool,
     // is_cursor_locked: bool,
-    window: Option<Arc<Window>>,
+    pub window: Option<Arc<Window>>,
 
-    show_new_project: bool,
-    project_name: String,
+    pub show_new_project: bool,
+    pub project_name: String,
     pub(crate) project_path: Arc<Mutex<Option<PathBuf>>>,
-    pending_scene_switch: bool,
+    pub pending_scene_switch: bool,
 
-    gizmo: Gizmo,
+    pub gizmo: Gizmo,
     pub(crate) selected_entity: Option<hecs::Entity>,
-    viewport_mode: ViewportMode,
+    pub viewport_mode: ViewportMode,
 
     pub(crate) signal: Signal,
     pub(crate) undo_stack: Vec<UndoableAction>,
     // todo: add redo (later)
     // redo_stack: Vec<UndoableAction>,
     pub(crate) editor_state: EditorState,
-    gizmo_mode: EnumSet<GizmoMode>,
+    pub gizmo_mode: EnumSet<GizmoMode>,
 
     pub(crate) script_manager: ScriptManager,
-    play_mode_backup: Option<PlayModeBackup>,
+    pub play_mode_backup: Option<PlayModeBackup>,
 
     /// State of the input
     pub(crate) input_state: InputState,
 
     // channels
     /// A threadsafe Unbounded Receiver, typically used for checking the status of the world loading
-    progress_tx: Option<UnboundedReceiver<WorldLoadingStatus>>,
+    pub progress_tx: Option<UnboundedReceiver<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
     /// egui loading windows or splash screens and such.
-    current_state: WorldLoadingStatus,
+    pub current_state: WorldLoadingStatus,
 
     // handles for futures
-    world_load_handle: Option<FutureHandle>,
+    pub world_load_handle: Option<FutureHandle>,
     pub(crate) alt_pending_spawn_queue: Vec<FutureHandle>,
-    world_receiver: Option<oneshot::Receiver<hecs::World>>,
+    pub world_receiver: Option<oneshot::Receiver<hecs::World>>,
 
     // building
     pub progress_rx: Option<Receiver<BuildStatus>>,
@@ -109,7 +109,10 @@ pub struct Editor {
     pub last_build_error: Option<String>,
     pub show_build_error_window: bool,
 
-    dock_state_shared: Option<Arc<Mutex<DockState<EditorTab>>>>,
+    // plugins
+    pub plugin_registry: PluginRegistry,
+
+    pub dock_state_shared: Option<Arc<Mutex<DockState<EditorTab>>>>,
 }
 
 impl Editor {
@@ -152,6 +155,8 @@ impl Editor {
             }
         });
 
+        let plugin_registry = PluginRegistry::new();
+
         Ok(Self {
             scene_command: SceneCommand::None,
             dock_state,
@@ -192,6 +197,7 @@ impl Editor {
             show_build_window: false,
             last_build_error: None,
             show_build_error_window: false,
+            plugin_registry,
             dock_state_shared: None,
         })
     }
@@ -612,8 +618,10 @@ impl Editor {
                     if ui_window.button("Open Viewport").clicked() {
                         self.dock_state.push_to_focused_leaf(EditorTab::Viewport);
                     }
-                    if ui_window.button("Open Kotlin REPL").clicked() {
-                        self.dock_state.push_to_focused_leaf(EditorTab::KotlinREPL);
+                    for (i, (_, plugin)) in self.plugin_registry.plugins.iter().enumerate() {
+                        if ui_window.button(format!("Open {}", plugin.display_name())).clicked() {
+                            self.dock_state.push_to_focused_leaf(EditorTab::Plugin(i));
+                        }
                     }
                 });
                 {
@@ -625,6 +633,8 @@ impl Editor {
             });
         });
 
+        let editor_ptr = self as *mut Editor;
+
         egui::CentralPanel::default().show(ctx, |ui| {
             DockArea::new(&mut self.dock_state)
                 .style(Style::from_egui(ui.style().as_ref()))
@@ -643,6 +653,8 @@ impl Editor {
                         active_camera: &mut self.active_camera,
                         gizmo_mode: &mut self.gizmo_mode,
                         editor_mode: &mut self.editor_state,
+                        plugin_registry: &mut self.plugin_registry,
+                        editor: editor_ptr,
                     },
                 );
         });
diff --git a/eucalyptus-editor/src/editor/repl.rs b/eucalyptus-editor/src/editor/repl.rs
deleted file mode 100644
index 27e01f6..0000000
--- a/eucalyptus-editor/src/editor/repl.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-use egui::{Color32, FontId, RichText, ScrollArea, TextEdit};
-
-/// A simple Kotlin REPL interface for testing scripts
-pub struct KotlinREPL {
-    input: String,
-    output: Vec<ReplOutputLine>,
-    history: Vec<String>,
-    history_index: Option<usize>,
-}
-
-#[derive(Clone)]
-pub struct ReplOutputLine {
-    pub text: String,
-    pub is_error: bool,
-    pub is_input: bool,
-}
-
-impl Default for KotlinREPL {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl KotlinREPL {
-    pub fn new() -> Self {
-        let mut repl = Self {
-            input: String::new(),
-            output: Vec::new(),
-            history: Vec::new(),
-            history_index: None,
-        };
-
-        repl.add_output("Kotlin REPL - Ready", false, false);
-        repl.add_output(
-            "Type Kotlin expressions to test script functionality",
-            false,
-            false,
-        );
-        repl.add_output("Example: Input.isKeyPressed(KeyCode.W)", false, false);
-        repl.add_output("", false, false);
-
-        repl
-    }
-
-    fn add_output(&mut self, text: &str, is_error: bool, is_input: bool) {
-        self.output.push(ReplOutputLine {
-            text: text.to_string(),
-            is_error,
-            is_input,
-        });
-    }
-
-    fn execute(&mut self, code: &str) {
-        // Add input to output
-        self.add_output(&format!("> {}", code), false, true);
-
-        // Add to history
-        if !code.trim().is_empty() {
-            self.history.push(code.to_string());
-            self.history_index = None;
-        }
-
-        // Execute the code
-        match self.execute_kotlin_code(code) {
-            Ok(result) => {
-                if !result.is_empty() {
-                    self.add_output(&result, false, false);
-                }
-            }
-            Err(e) => {
-                self.add_output(&format!("Error: {}", e), true, false);
-            }
-        }
-
-        self.add_output("", false, false); // blank line
-    }
-
-    fn execute_kotlin_code(&self, code: &str) -> anyhow::Result<String> {
-        // For now, we'll provide a simplified evaluation
-        // In the future, this could compile and run actual Kotlin code via JNI
-
-        // Check for common test commands
-        if code.trim().starts_with("Input.isKeyPressed") {
-            Ok("boolean (check console for actual value)".to_string())
-        } else if code.trim().starts_with("Input.getMouseX")
-            || code.trim().starts_with("Input.getMouseY")
-        {
-            Ok("double (check console for actual value)".to_string())
-        } else if code.contains("Transform") {
-            Ok("Transform manipulation (check entity in viewport)".to_string())
-        } else if code.trim() == "help" {
-            Ok(r#"Available APIs:
-- Input.isKeyPressed(KeyCode.W) - Check if key is pressed
-- Input.getMouseX() - Get mouse X position
-- Input.getMouseY() - Get mouse Y position
-- engine.getTransform() - Get current entity transform
-- transform.position - Get/set position (Vector3D)
-- transform.rotation - Get/set rotation (Quaternion)
-- transform.scale - Get/set scale (Vector3D)
-
-Note: This is a simplified REPL. Full script execution requires 
-attaching a script to an entity and running the game."#
-                .to_string())
-        } else if code.trim() == "clear" {
-            Err(anyhow::anyhow!("CLEAR_SCREEN"))
-        } else {
-            Ok(format!(
-                "Command received: {}\n(Full Kotlin evaluation not yet implemented)",
-                code
-            ))
-        }
-    }
-
-    pub fn ui(&mut self, ui: &mut egui::Ui) {
-        ui.vertical(|ui| {
-            // Title bar
-            ui.horizontal(|ui| {
-                ui.heading("Kotlin REPL");
-                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
-                    if ui.button("Clear").clicked() {
-                        self.output.clear();
-                        self.add_output("Kotlin REPL - Ready", false, false);
-                    }
-                    if ui.button("Help").clicked() {
-                        self.execute("help");
-                    }
-                });
-            });
-            
-            ui.separator();
-            
-            // Output area
-            ScrollArea::vertical()
-                .auto_shrink([false, false])
-                .stick_to_bottom(true)
-                .show(ui, |ui| {
-                    ui.set_min_height(ui.available_height() - 80.0);
-                    
-                    for line in &self.output {
-                        let color = if line.is_error {
-                            Color32::from_rgb(255, 100, 100)
-                        } else if line.is_input {
-                            Color32::from_rgb(100, 200, 255)
-                        } else {
-                            Color32::from_rgb(200, 200, 200)
-                        };
-                        
-                        ui.label(
-                            RichText::new(&line.text)
-                                .color(color)
-                                .font(FontId::monospace(14.0))
-                        );
-                    }
-                });
-            
-            ui.separator();
-            
-            // Input area
-            ui.horizontal(|ui| {
-                ui.label(">>>");
-                
-                let response = ui.add(
-                    TextEdit::singleline(&mut self.input)
-                        .desired_width(f32::INFINITY)
-                        .font(FontId::monospace(14.0))
-                );
-                
-                // Handle keyboard input
-                if response.has_focus() {
-                    if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
-                        let code = self.input.clone();
-                        self.input.clear();
-                        
-                        if code.trim() == "clear" {
-                            self.output.clear();
-                            self.add_output("Kotlin REPL - Ready", false, false);
-                        } else {
-                            self.execute(&code);
-                        }
-                    }
-                    
-                    // History navigation
-                    if ui.input(|i| i.key_pressed(egui::Key::ArrowUp))
-                        && !self.history.is_empty() {
-                            if let Some(idx) = self.history_index {
-                                if idx > 0 {
-                                    self.history_index = Some(idx - 1);
-                                    self.input = self.history[idx - 1].clone();
-                                }
-                            } else {
-                                self.history_index = Some(self.history.len() - 1);
-                                self.input = self.history[self.history.len() - 1].clone();
-                            }
-                        }
-                    
-                    if ui.input(|i| i.key_pressed(egui::Key::ArrowDown))
-                        && let Some(idx) = self.history_index {
-                            if idx < self.history.len() - 1 {
-                                self.history_index = Some(idx + 1);
-                                self.input = self.history[idx + 1].clone();
-                            } else {
-                                self.history_index = None;
-                                self.input.clear();
-                            }
-                        }
-                }
-                
-                if ui.button("Execute").clicked() && !self.input.is_empty() {
-                    let code = self.input.clone();
-                    self.input.clear();
-                    self.execute(&code);
-                }
-            });
-            
-            // Tips
-            ui.horizontal(|ui| {
-                ui.label(
-                    RichText::new("Tip: Type 'help' for available commands, 'clear' to clear output. Use ↑↓ for history.")
-                        .size(10.0)
-                        .color(Color32::from_rgb(150, 150, 150))
-                );
-            });
-        });
-    }
-}
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 3773412..ca00543 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -100,6 +100,14 @@ impl Scene for Editor {
                 fatal!("{}", e);
             }
         }
+        
+        if !self.plugin_registry.plugins_loaded {
+            if let Err(e) = self.plugin_registry.load_plugins() {
+                fatal!("Failed to load plugins: {}", e);
+            } else {
+                log::info!("Plugins loaded");
+            }
+        }
 
         if let Some((_, tab)) = self.dock_state.find_active_focused() {
             self.is_viewport_focused = matches!(tab, EditorTab::Viewport);
diff --git a/eucalyptus-editor/src/lib.rs b/eucalyptus-editor/src/lib.rs
new file mode 100644
index 0000000..8bc482b
--- /dev/null
+++ b/eucalyptus-editor/src/lib.rs
@@ -0,0 +1,9 @@
+pub mod build;
+pub mod camera;
+pub mod debug;
+pub mod editor;
+pub mod menu;
+pub mod signal;
+pub mod spawn;
+pub mod utils;
+pub mod plugin;
\ No newline at end of file
diff --git a/eucalyptus-editor/src/main.rs b/eucalyptus-editor/src/main.rs
index a1344a6..cd93c8a 100644
--- a/eucalyptus-editor/src/main.rs
+++ b/eucalyptus-editor/src/main.rs
@@ -1,24 +1,13 @@
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-mod build;
-mod camera;
-mod debug;
-mod editor;
-mod menu;
-mod signal;
-mod spawn;
-mod utils;
 
 use clap::{Arg, Command};
 use dropbear_engine::future::FutureQueue;
-use dropbear_engine::{WindowConfiguration, scene};
+use dropbear_engine::{scene, WindowConfiguration};
 use parking_lot::RwLock;
 use std::sync::Arc;
 use std::{fs, path::PathBuf, rc::Rc};
-
-pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo {
-    name: "Eucalyptus",
-    author: "4tkbytes",
-};
+use eucalyptus_editor::{build, editor, menu};
+use eucalyptus_core::APP_INFO;
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
@@ -89,7 +78,7 @@ async fn main() -> anyhow::Result<()> {
                 },
             };
 
-            crate::build::build(project_path)?;
+            build::build(project_path)?;
         }
         Some(("package", sub_matches)) => {
             let project_path = match sub_matches.get_one::<String>("project") {
@@ -103,7 +92,7 @@ async fn main() -> anyhow::Result<()> {
                 },
             };
 
-            crate::build::package(project_path, sub_matches)?;
+            build::package(project_path, sub_matches)?;
         }
         Some(("health", _)) => {
             build::health()?;
diff --git a/eucalyptus-editor/src/plugin.rs b/eucalyptus-editor/src/plugin.rs
new file mode 100644
index 0000000..244c8e7
--- /dev/null
+++ b/eucalyptus-editor/src/plugin.rs
@@ -0,0 +1,141 @@
+use std::fs::ReadDir;
+use std::path::PathBuf;
+use std::time::Instant;
+use app_dirs2::AppDataType;
+use egui::Ui;
+use indexmap::IndexMap;
+use eucalyptus_core::{APP_INFO};
+use libloading as lib;
+use eucalyptus_core::states::PluginInfo;
+use crate::editor::Editor;
+
+pub trait EditorPlugin: Send + Sync {
+    fn id(&self) -> &str;
+    fn display_name(&self) -> &str;
+    fn ui(&mut self, ui: &mut Ui, editor: &mut Editor);
+    fn tab_title(&self) -> &str;
+    fn context_menu(&mut self, _ui: &mut Ui, _editor: &mut Editor) { /* No context */ }
+}
+
+pub type PluginConstructor = fn() -> Box<dyn EditorPlugin>;
+
+pub struct PluginRegistry {
+    pub plugins: IndexMap<String, Box<dyn EditorPlugin>>,
+    loaded_libraries: Vec<lib::Library>,
+    pub(crate) plugins_loaded: bool,
+}
+
+impl Default for PluginRegistry {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl PluginRegistry {
+    pub fn new() -> Self {
+        Self {
+            plugins: IndexMap::new(),
+            loaded_libraries: Vec::new(),
+            plugins_loaded: false,
+        }
+    }
+
+    pub fn register(&mut self, plugin: Box<dyn EditorPlugin>) {
+        let id = plugin.id().to_string();
+        self.plugins.insert(id, plugin);
+    }
+
+    #[allow(clippy::borrowed_box)]
+    pub fn get(&self, id: &str) -> Option<&Box<dyn EditorPlugin>> {
+        self.plugins.get(id)
+    }
+
+    pub fn get_mut(&mut self, id: &str) -> Option<&mut Box<dyn EditorPlugin>> {
+        self.plugins.get_mut(id)
+    }
+
+    pub fn list_plugins(&self) -> Vec<PluginInfo> {
+        self.plugins
+            .values()
+            .map(|p| PluginInfo {
+                display_name: p.display_name().to_string()
+            })
+            .collect()
+    }
+
+    pub fn load_plugins(&mut self) -> anyhow::Result<()> {
+        let appdir: PathBuf = app_dirs2::app_root(AppDataType::UserData, &APP_INFO)?;
+        let plugins_folder = appdir.join("plugins");
+
+        std::fs::create_dir_all(&plugins_folder)?;
+
+        let contents: ReadDir = std::fs::read_dir(&plugins_folder)?;
+
+        log::info!("Loading plugins from {}", plugins_folder.display());
+        let mut index: i32 = -1;
+        for (i, entry) in contents.enumerate() {
+            match entry {
+                Ok(e) => {
+                    index = i as i32;
+                    let now = Instant::now();
+                    let path = e.path();
+                    if let Some(ext) = path.extension() {
+                        let ext_str = ext.to_str().unwrap_or("");
+
+                        if !self.is_valid_extension_for_platform(ext_str) {
+                            log::warn!("Skipping plugin {} - incompatible extension for this platform",
+                                     path.display());
+                            continue;
+                        }
+
+                        match self.load_plugin_from_file(&path) {
+                            Ok(plugin) => {
+                                log::info!("Successfully loaded plugin: {}", path.display());
+                                log::debug!("Plugin {} loaded in {:?}", path.display(), now.elapsed());
+                                self.register(plugin);
+                            }
+                            Err(e) => {
+                                log::error!("Failed to load plugin {}: {}", path.display(), e);
+                                continue;
+                            }
+                        }
+                    }
+                }
+                Err(err) => {
+                    log::warn!("Failed to read directory entry: {}", err);
+                    continue;
+                }
+            }
+        }
+
+        if index == -1 {
+            log::info!("No plugins found");
+        }
+
+        self.plugins_loaded = true;
+        Ok(())
+    }
+
+    fn is_valid_extension_for_platform(&self, ext: &str) -> bool {
+        match ext {
+            "dll" => cfg!(windows),
+            "so" => cfg!(unix) && !cfg!(target_os = "macos"),
+            "dylib" => cfg!(target_os = "macos"),
+            _ => false,
+        }
+    }
+
+    fn load_plugin_from_file(&mut self, path: &PathBuf) -> anyhow::Result<Box<dyn EditorPlugin>> {
+        let library = unsafe { lib::Library::new(path)? };
+
+        let constructor: lib::Symbol<PluginConstructor> = unsafe {
+            library.get(b"create_plugin")?
+        };
+
+        let plugin = constructor();
+
+        self.loaded_libraries.push(library);
+
+        Ok(plugin)
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index b819490..caaf980 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -16,6 +16,7 @@ use hecs::{Entity, World};
 use std::collections::HashMap;
 use std::path::PathBuf;
 use std::sync::Arc;
+use winit::keyboard::KeyCode;
 
 pub trait SignalController {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>;
@@ -136,6 +137,152 @@ impl SignalController for Editor {
                 }
 
                 if matches!(self.editor_state, EditorState::Building) {
+                    #[cfg(not(target_os = "macos"))]
+                    let ctrl_pressed = self
+                        .input_state
+                        .pressed_keys
+                        .contains(&KeyCode::ControlLeft)
+                        || self
+                        .input_state
+                        .pressed_keys
+                        .contains(&KeyCode::ControlRight);
+                    #[cfg(target_os = "macos")]
+                    let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft)
+                        || self.input_state.pressed_keys.contains(&KeyCode::SuperRight);
+
+                    let alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft)
+                        || self.input_state.pressed_keys.contains(&KeyCode::AltRight);
+
+                    // Ctrl+Alt+P skips build process and starts running, such as if using cached jar
+                    if ctrl_pressed && alt_pressed && self.input_state.pressed_keys.contains(&KeyCode::KeyP) {
+
+                        if let Some(handle) = self.handle_created {
+                            log::debug!("Cancelling build task due to manual intervention");
+                            graphics.future_queue.cancel(&handle);
+                        } else {
+                            log::warn!("No handle was created during this time. Weird...")
+                        }
+
+                        let project_root = {
+                            let cfg = PROJECT.read();
+                            cfg.project_path.clone()
+                        };
+                        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();
+                            return Err(anyhow::anyhow!(err));
+                        }
+
+                        let jar_files: Vec<PathBuf> = std::fs::read_dir(&libs_dir)?
+                            .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();
+                            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()
+                        };
+
+                        info!("Using cached JAR: {}", jar_path.display());
+
+                        self.show_build_window = false;
+
+                        let has_player_camera_target = self
+                            .world
+                            .query::<(&Camera, &CameraComponent)>()
+                            .iter()
+                            .any(|(_, (_, comp))| comp.starting_camera);
+
+                        if has_player_camera_target {
+                            if let Err(e) = self.create_backup() {
+                                self.signal = Signal::None;
+                                fatal!("Failed to create play mode backup: {}", e);
+                            }
+
+                            self.editor_state = EditorState::Playing;
+
+                            self.switch_to_player_camera();
+
+                            let mut script_entities = Vec::new();
+                            {
+                                for (entity_id, script) in self.world.query::<&ScriptComponent>().iter() {
+                                    script_entities.push((entity_id, script.clone()));
+                                }
+                            }
+
+                            let mut etag: HashMap<String, Vec<Entity>> = HashMap::new();
+                            for (entity_id, script) in script_entities {
+                                for tag in script.tags {
+                                    if etag.contains_key(&tag) {
+                                        etag.get_mut(&tag).unwrap().push(entity_id);
+                                    } else {
+                                        etag.insert(tag.clone(), vec![entity_id]);
+                                    }
+                                }
+                            }
+
+                            let etag_clone = etag.clone();
+
+                            if let Err(e) = self
+                                .script_manager
+                                .init_script(etag_clone, ScriptTarget::JVM { library_path: jar_path })
+                            {
+                                fatal!("Failed to ready script manager interface because {}", e);
+                                self.signal = Signal::StopPlaying;
+                                return Err(anyhow::anyhow!(e));
+                            }
+
+                            let world_ptr = self.world.as_mut() as *mut World;
+
+                            if let Err(e) = self.script_manager
+                                .load_script(
+                                    world_ptr,
+                                    &self.input_state,
+                                ) {
+                                fatal!(
+                                    "Failed to initialise script because {}",
+                                    e
+                                );
+                                self.signal = Signal::StopPlaying;
+                                return Err(anyhow::anyhow!(e));
+                            } else {
+                                success_without_console!(
+                                    "You are in play mode now! Press Escape to exit"
+                                );
+                                log::info!("You are in play mode now! Press Escape to exit");
+                            }
+
+                            self.signal = Signal::None;
+                        } else {
+                            self.signal = Signal::None;
+                            fatal!("Unable to build: No initial camera set");
+                        }
+                    }
+
                     let mut local_handle_exchanged: Option<anyhow::Result<PathBuf>> = None;
                     if let Some(rx) = &self.progress_rx {
                         while let Ok(status) = rx.try_recv() {
@@ -227,6 +374,7 @@ impl SignalController for Editor {
                                                     .italics()
                                                     .color(egui::Color32::GRAY)
                                                 );
+                                                ui.label("Tip: Press Ctrl+Alt+P to skip build and start running");
                                             }
                                         });
                                     
diff --git a/eucalyptus-sdk/Cargo.toml b/eucalyptus-sdk/Cargo.toml
new file mode 100644
index 0000000..8ffeb2e
--- /dev/null
+++ b/eucalyptus-sdk/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "eucalyptus-sdk"
+version.workspace = true
+edition.workspace = true
+license-file.workspace = true
+repository.workspace = true
+readme = "README.md"
+
+[dependencies]
+eucalyptus-core = { path = "../eucalyptus-core" }
+eucalyptus-editor = { path = "../eucalyptus-editor" }
+dropbear-engine = { path = "../dropbear-engine" }
\ No newline at end of file
diff --git a/eucalyptus-sdk/README.md b/eucalyptus-sdk/README.md
new file mode 100644
index 0000000..71972c0
--- /dev/null
+++ b/eucalyptus-sdk/README.md
@@ -0,0 +1,10 @@
+# eucalyptus-sdk
+
+This is the SDK that deals with plugins and its management, as well as user defined plugins. 
+
+To use this crate, check out the related docs.rs (TODO ADD WEBSITE) to define your own plugin. 
+
+Note: This SDK is only used in the eucalyptus-editor, and not in any games exported by it. If you wish to 
+add your own dependencies to help other developers with scripting, you should create your own Kotlin Multiplatform 
+library, or fork the dropbear-engine and add your own FFI interop functions. (Then recontribute it back to the 
+repository.) 
\ No newline at end of file
diff --git a/eucalyptus-sdk/src/lib.rs b/eucalyptus-sdk/src/lib.rs
new file mode 100644
index 0000000..b66440b
--- /dev/null
+++ b/eucalyptus-sdk/src/lib.rs
@@ -0,0 +1,3 @@
+pub use dropbear_engine;
+pub use eucalyptus_core;
+pub use eucalyptus_editor;
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 9b79a16..97ff880 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,8 @@
 kotlin.code.style=official
+
 org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
-org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
\ No newline at end of file
+org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
+
+org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxMetaspaceSize=512m
+org.gradle.daemon=true
+org.gradle.caching=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 249e583..9bbc975 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 3adfd49..37f853b 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Tue Sep 30 12:55:55 AEST 2025
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 1b6c787..faf9300 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+# SPDX-License-Identifier: Apache-2.0
+#
 
 ##############################################################################
 #
@@ -55,7 +57,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -133,22 +133,29 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +216,12 @@ set -- \
         org.gradle.wrapper.GradleWrapperMain \
         "$@"
 
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
 # Use "xargs" to parse quoted args.
 #
 # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
diff --git a/gradlew.bat b/gradlew.bat
index 107acd3..9d21a21 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,8 +13,10 @@
 @rem See the License for the specific language governing permissions and
 @rem limitations under the License.
 @rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
 
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
 @rem ##########################################################################
 @rem
 @rem  Gradle startup script for Windows
@@ -25,7 +27,8 @@
 if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
 
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
 if exist "%JAVA_EXE%" goto execute
 
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
 :end
 @rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
 
 :fail
 rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
 rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
 
 :mainEnd
 if "%OS%"=="Windows_NT" endlocal
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6c22cfe..b22da72 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -9,4 +9,10 @@ pluginManagement {
 plugins {
     id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
 }
-include("magna-carta-plugin")
\ No newline at end of file
+include("magna-carta-plugin")
+
+buildCache {
+    local {
+        isEnabled = true
+    }
+}
\ No newline at end of file