tirbofish/dropbear · commit
b44209ff0ae4a2ca729b96a43743651ba9d5134c
Merge branch 'main' into sockets
# Conflicts:
# build.gradle.kts
# eucalyptus-core/Cargo.toml
# eucalyptus-core/build.rs
# eucalyptus-core/src/scripting.rs
# eucalyptus-core/src/scripting/java.rs
# eucalyptus-editor/build.rs
# eucalyptus-editor/src/editor/component.rs
# src/commonMain/kotlin/com/dropbear/EntityRef.kt
# src/main/kotlin/com/dropbear/DropbearEngine.kt
# src/main/kotlin/com/dropbear/Example.kt
# src/main/kotlin/com/dropbear/ffi/NativeEngine.java
Signature present but could not be verified.
Unverified
@@ -0,0 +1,35 @@ +name: Build and Upload JAR + +on: + push: + branches: + - '**' + workflow_dispatch: + +jobs: + build: + if: contains(github.event.head_commit.message, 'jarvis, run github actions') + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build + + - name: Upload JAR artifact + uses: actions/upload-artifact@v4 + with: + name: application-jar + path: build/libs/*.jar + retention-days: 30 @@ -23,14 +23,14 @@ If you might have not realised, all the crates/projects names are after Australi To build, ensure build requirements, clone the repository, then build it. It will build in debug mode, and use a lot of packages, so if your CPU is not fast enough for building you should brew a cup of coffee during the build time. -With Unix systems (macOS not tested), you will have to download a couple dependencies if building locally: +With Unix systems (macOS not tested), you will have to download a couple of dependencies if building locally: <!-- If you have a macOS system, please create a PR and add your own implementation. I know you need to use brew, but I don't know what dependencies to install. --> ```bash # ubuntu -sudo apt install libudev-dev pkg-config libssl-dev clang cmake meson assimp-utils +sudo apt install libudev-dev pkg-config libssl-dev clang cmake meson assimp-utils openjdk-21-jdk # i use arch btw sudo pacman -Syu base-devel systemd pkgconf openssl clang cmake meson assimp jdk21-openjdk @@ -55,6 +55,12 @@ cargo build [//]: # (git submodule update) +> [!TIP] +> During the development of your game, you should download JetBrains IntelliJ (for kotlin) for the best support. +> +> For a development build of the eucalyptus editor, it is recommended that you use JetBrains IntelliJ (for jvm) and +> JetBrains RustRover (for rust) + ### Prebuilt If you do not want to build it locally, you are able to download the latest action build (if no releases have been made). @@ -0,0 +1,17 @@ +# notes to self + +## hybrid approach: + +- 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 + +## required dependencies + +- jdk 21 @@ -1356,6 +1356,7 @@ pub enum EditorTab { ResourceInspector, // left side, ModelEntityList, // right side, Viewport, // middle, + KotlinREPL, // bottom side - new REPL tab } /// An enum that describes the status of loading the world. @@ -74,6 +74,10 @@ 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 @@ -107,6 +111,7 @@ 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(), } } @@ -1024,6 +1029,17 @@ 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()); + } + + if let Some(repl) = repl_lock.as_mut() { + repl.ui(ui); + } + } } let mut menu_action: Option<EditorTabMenuAction> = None; @@ -1074,6 +1090,9 @@ impl<'a> TabViewer for EditorTabViewer<'a> { menu_action = Some(EditorTabMenuAction::ViewportOption); } } + EditorTab::KotlinREPL => { + // No context menu actions for REPL tab yet + } } }) }); @@ -1,6 +1,7 @@ pub mod component; pub mod dock; pub mod input; +pub mod repl; pub mod scene; pub(crate) use crate::editor::dock::*; @@ -113,7 +114,7 @@ impl Editor { surface.split_right(NodeIndex::root(), 0.25, vec![EditorTab::ModelEntityList]); let [_old, _] = surface.split_left(NodeIndex::root(), 0.20, vec![EditorTab::ResourceInspector]); - let [_old, _] = surface.split_below(right, 0.5, vec![EditorTab::AssetViewer]); + let [_old, _] = surface.split_below(right, 0.5, vec![EditorTab::AssetViewer, EditorTab::KotlinREPL]); // this shit doesn't work :( // nvm it works @@ -583,6 +584,9 @@ 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); + } }); { let cfg = PROJECT.read(); @@ -0,0 +1,221 @@ +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" { + return 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)) { + if !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)) { + if 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)) + ); + }); + }); + } +} @@ -1,9 +1,116 @@ package com.dropbear.math +import com.dropbear.ffi.NativeEngine + /** * A class that keeps all the values of a Transform, which consists of a * position ([Vector3D]), rotation([QuaternionD]) and a scale([Vector3D]). + * + * This class provides direct access to transform data from the Rust engine. */ -class Transform(var position: Vector3D, var rotation: QuaternionD, var scale: Vector3D) { - -} +class Transform private constructor( + private var _position: Vector3D, + private var _rotation: QuaternionD, + private var _scale: Vector3D +) { + /** + * Get the position from the native engine + */ + var position: Vector3D + get() { + _position = Vector3D( + NativeEngine.getPositionX(), + NativeEngine.getPositionY(), + NativeEngine.getPositionZ() + ) + return _position + } + set(value) { + _position = value + NativeEngine.setPosition(value.x, value.y, value.z) + } + + /** + * Get the rotation from the native engine + */ + var rotation: QuaternionD + get() { + _rotation = Quaternion( + NativeEngine.getRotationX(), + NativeEngine.getRotationY(), + NativeEngine.getRotationZ(), + NativeEngine.getRotationW() + ) + return _rotation + } + set(value) { + _rotation = value + NativeEngine.setRotation(value.x, value.y, value.z, value.w) + } + + /** + * Get the scale from the native engine + */ + var scale: Vector3D + get() { + _scale = Vector3D( + NativeEngine.getScaleX(), + NativeEngine.getScaleY(), + NativeEngine.getScaleZ() + ) + return _scale + } + set(value) { + _scale = value + NativeEngine.setScale(value.x, value.y, value.z) + } + + companion object { + /** + * Creates a new Transform with everything to its default values. + * This creates a live Transform that reads from the native engine. + */ + fun default(): Transform { + return Transform( + Vector3D.zero(), + Quaternion.identity(), + Vector3D(1.0, 1.0, 1.0) + ) + } + + /** + * Creates a Transform from the current native engine state + */ + fun fromNative(): Transform { + val pos = Vector3D( + NativeEngine.getPositionX(), + NativeEngine.getPositionY(), + NativeEngine.getPositionZ() + ) + val rot = Quaternion( + NativeEngine.getRotationX(), + NativeEngine.getRotationY(), + NativeEngine.getRotationZ(), + NativeEngine.getRotationW() + ) + val scale = Vector3D( + NativeEngine.getScaleX(), + NativeEngine.getScaleY(), + NativeEngine.getScaleZ() + ) + return Transform(pos, rot, scale) + } + } + + /** + * Translate (move) the transform by a delta + */ + fun translate(delta: Vector3D) { + val current = position + position = Vector3D( + current.x + delta.x, + current.y + delta.y, + current.z + delta.z + ) + } +} @@ -0,0 +1,47 @@ +package com.dropbear.input + +import com.dropbear.ffi.NativeEngine + +/** + * High-level input management wrapper for accessing input state + * from Kotlin scripts. + */ +object Input { + /** + * Check if a specific key is currently pressed + * + * @param keyCode The key to check (use KeyCode constants) + * @return true if the key is pressed, false otherwise + * + * @example + * ```kotlin + * if (Input.isKeyPressed(KeyCode.W)) { + * // Move forward + * } + * ``` + */ + fun isKeyPressed(keyCode: Long): Boolean { + return NativeEngine.isKeyPressed(keyCode) + } + + /** + * Get the current mouse X position + */ + fun getMouseX(): Double { + return NativeEngine.getMouseX() + } + + /** + * Get the current mouse Y position + */ + fun getMouseY(): Double { + return NativeEngine.getMouseY() + } + + /** + * Get the mouse position as a pair + */ + fun getMousePosition(): Pair<Double, Double> { + return Pair(getMouseX(), getMouseY()) + } +} @@ -0,0 +1,47 @@ +package com.dropbear.input + +/** + * Key codes that match the Rust KeyCode enum. + * These are used with NativeEngine.isKeyPressed() + */ +object KeyCode { + // Letters + const val A = 65L + const val B = 66L + const val C = 67L + const val D = 68L + const val E = 69L + const val F = 70L + const val G = 71L + const val H = 72L + const val I = 73L + const val J = 74L + const val K = 75L + const val L = 76L + const val M = 77L + const val N = 78L + const val O = 79L + const val P = 80L + const val Q = 81L + const val R = 82L + const val S = 83L + const val T = 84L + const val U = 85L + const val V = 86L + const val W = 87L + const val X = 88L + const val Y = 89L + const val Z = 90L + + // Arrow keys + const val ARROW_LEFT = 37L + const val ARROW_UP = 38L + const val ARROW_RIGHT = 39L + const val ARROW_DOWN = 40L + + // Special keys + const val SPACE = 32L + const val SHIFT = 16L + const val CONTROL = 17L + const val ESCAPE = 27L +}