tirbofish/dropbear · diff
what a fucking mess. i dont even think this works, but it can build a nice bridge (hopefully)...
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 @@ -22,7 +22,7 @@ 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. --> @@ -30,10 +30,10 @@ With Unix systems (macOS not tested), you will have to download a couple depende ```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 +sudo pacman -Syu base-devel systemd pkgconf openssl clang cmake meson assimp jdk21-openjdk ``` @@ -53,6 +53,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). @@ -1,5 +1,6 @@ plugins { kotlin("jvm") version "2.1.21" +// kotlin("multiplatform") version "2.1.21" } group = "com.dropbear" @@ -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 @@ -6,6 +6,9 @@ license-file.workspace = true repository.workspace = true readme = "README.md" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] anyhow.workspace = true bincode.workspace = true @@ -1,59 +1,60 @@ use std::fs::{self, File}; use std::io::Cursor; +use std::process::Command; fn main() -> anyhow::Result<()> { // todo: move this into the "setup" process - let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; - let response = reqwest::blocking::get(repo_zip_url) - .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? - .bytes() - .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; - - let reader = Cursor::new(response); - let mut zip = zip::ZipArchive::new(reader) - .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; - - let app_info = app_dirs2::AppInfo { - name: "Eucalyptus", - author: "4tkbytes", - }; - let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) - .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; - - fs::create_dir_all(&app_data_dir) - .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; - - let resource_prefix = "dropbear-main/resources/"; - let mut found_resource = false; - for i in 0..zip.len() { - let mut file = zip.by_index(i).unwrap(); - let name = file.name(); - - if name.starts_with(resource_prefix) && !name.ends_with('/') { - found_resource = true; - let rel_path = &name[resource_prefix.len()..]; - let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); - let dest_path = app_data_dir.join(rel_path); - - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; - } - - println!("Copying {} to {:?}", name, dest_path); - - let mut outfile = File::create(&dest_path) - .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; - std::io::copy(&mut file, &mut outfile) - .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; - } - } - - if !found_resource { - return Err(anyhow::anyhow!( - "No resources folder found in the github repository [4tkbytes/dropbear] :(" - )); - } + // let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; + // let response = reqwest::blocking::get(repo_zip_url) + // .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? + // .bytes() + // .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; + // + // let reader = Cursor::new(response); + // let mut zip = zip::ZipArchive::new(reader) + // .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; + // + // let app_info = app_dirs2::AppInfo { + // name: "Eucalyptus", + // author: "4tkbytes", + // }; + // let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) + // .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; + // + // fs::create_dir_all(&app_data_dir) + // .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; + // + // let resource_prefix = "dropbear-main/resources/"; + // let mut found_resource = false; + // for i in 0..zip.len() { + // let mut file = zip.by_index(i).unwrap(); + // let name = file.name(); + // + // if name.starts_with(resource_prefix) && !name.ends_with('/') { + // found_resource = true; + // let rel_path = &name[resource_prefix.len()..]; + // let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); + // let dest_path = app_data_dir.join(rel_path); + // + // if let Some(parent) = dest_path.parent() { + // fs::create_dir_all(parent) + // .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; + // } + // + // println!("Copying {} to {:?}", name, dest_path); + // + // let mut outfile = File::create(&dest_path) + // .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; + // std::io::copy(&mut file, &mut outfile) + // .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; + // } + // } + // + // if !found_resource { + // return Err(anyhow::anyhow!( + // "No resources folder found in the github repository [4tkbytes/dropbear] :(" + // )); + // } // fuck you windows :( #[cfg(target_os = "windows")] @@ -83,19 +83,18 @@ impl DropbearScriptingAPIContext { /// A message from Kotlin that gets sent to Rust pub enum KotlinMessage { - + } /// A message from Rust that gets sent to Kotlin pub enum RustMessage { - + Ping } pub struct ScriptManager { + #[allow(dead_code)] script_context: DropbearScriptingAPIContext, java: JavaContext, - from_kotlin: crossbeam_channel::Receiver<KotlinMessage>, - to_kotlin: crossbeam_channel::Sender<RustMessage>, } impl ScriptManager { @@ -115,7 +114,7 @@ impl ScriptManager { pub fn load_script( &mut self, script_name: &String, - script_content: String, + _script_content: String, ) -> anyhow::Result<String> { log::debug!("Loaded library [{}]", script_name); @@ -131,6 +130,20 @@ impl ScriptManager { ) -> anyhow::Result<()> { log_once::debug_once!("init_entity_script: {} for {:?}", script_name, entity_id); + // Update the script context + java::update_script_context( + entity_id, + world, + input_state.mouse_pos, + &input_state.pressed_keys.iter().copied().collect::<Vec<_>>() + ); + + // Try to load and initialize the script + if let Ok(script_instance) = self.java.load_script_class(script_name) { + self.java.call_script_load(script_instance)?; + log::info!("Initialized script {} for entity {:?}", script_name, entity_id); + } + Ok(()) } @@ -140,10 +153,26 @@ impl ScriptManager { script_name: &str, world: &mut World, input_state: &InputState, - dt: f32, + _dt: f32, ) -> anyhow::Result<()> { log_once::debug_once!("Update entity script name: {}", script_name); + // Update the script context before calling script + java::update_script_context( + entity_id, + world, + input_state.mouse_pos, + &input_state.pressed_keys.iter().copied().collect::<Vec<_>>() + ); + + // Call the update method on the script + if let Ok(script_instance) = self.java.load_script_class(script_name) { + self.java.call_script_update(script_instance)?; + } + + // Sync changes back to world + java::sync_transform_to_world(world); + Ok(()) } } @@ -1,5 +1,22 @@ +#![allow(non_snake_case)] use jni::{InitArgsBuilder, JNIEnv, JNIVersion, JavaVM}; -use jni::objects::{JObject, JString}; +use jni::objects::{JClass, JObject}; +use jni::sys::{jdouble, jlong, jboolean}; +use parking_lot::RwLock; +use once_cell::sync::Lazy; +use dropbear_engine::entity::Transform; +use glam::{DVec3, DQuat}; +use hecs::{Entity, World}; +use winit::keyboard::KeyCode; + +// Thread-safe global context using RwLock instead of Mutex for better performance +static CURRENT_ENTITY: Lazy<RwLock<Option<Entity>>> = Lazy::new(|| RwLock::new(None)); +static CURRENT_WORLD: Lazy<RwLock<Option<usize>>> = Lazy::new(|| RwLock::new(None)); // Store as usize +static CURRENT_POSITION: Lazy<RwLock<DVec3>> = Lazy::new(|| RwLock::new(DVec3::ZERO)); +static CURRENT_ROTATION: Lazy<RwLock<DQuat>> = Lazy::new(|| RwLock::new(DQuat::IDENTITY)); +static CURRENT_SCALE: Lazy<RwLock<DVec3>> = Lazy::new(|| RwLock::new(DVec3::ONE)); +static CURRENT_MOUSE: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0))); +static CURRENT_KEYS: Lazy<RwLock<Vec<i64>>> = Lazy::new(|| RwLock::new(Vec::new())); /// A dropbear wrapper for Java Virtual Machine (JVM) based functions pub(crate) struct JavaContext { @@ -11,6 +28,7 @@ impl JavaContext { pub fn new() -> anyhow::Result<Self> { let jvm_args = InitArgsBuilder::new() .version(JNIVersion::V8) + .option("-Djava.class.path=./build/libs/dropbear-1.0-SNAPSHOT.jar") .build()?; let jvm = JavaVM::new(jvm_args)?; @@ -19,25 +37,266 @@ impl JavaContext { jvm }) } + + /// Get a JNI environment for the current thread + pub fn get_env(&self) -> anyhow::Result<jni::AttachGuard<'_>> { + Ok(self.jvm.attach_current_thread()?) + } + + /// Call a script's load method + pub fn call_script_load(&self, script_instance: JObject) -> anyhow::Result<()> { + let mut env = self.get_env()?; + env.call_method(script_instance, "load", "()V", &[])?; + Ok(()) + } + + /// Call a script's update method + pub fn call_script_update(&self, script_instance: JObject) -> anyhow::Result<()> { + let mut env = self.get_env()?; + env.call_method(script_instance, "update", "()V", &[])?; + Ok(()) + } + + /// Load a script class and create an instance + pub fn load_script_class(&self, class_name: &str) -> anyhow::Result<JObject<'_>> { + let mut env = self.get_env()?; + + // Find the class + let class = env.find_class(class_name)?; + + // Create an instance + let instance = env.new_object(class, "()V", &[])?; + + Ok(instance) + } +} + +/// Update context from Rust side before calling scripts +pub fn update_script_context(entity: Entity, world: &World, mouse_pos: (f64, f64), pressed_keys: &[KeyCode]) { + // Store entity + *CURRENT_ENTITY.write() = Some(entity); + + // Store world pointer as usize + *CURRENT_WORLD.write() = Some(world as *const World as usize); + + // Read transform from world + if let Ok(transform) = world.get::<&Transform>(entity) { + *CURRENT_POSITION.write() = transform.position; + *CURRENT_ROTATION.write() = transform.rotation; + *CURRENT_SCALE.write() = transform.scale; + } + + // Store input state + *CURRENT_MOUSE.write() = mouse_pos; + *CURRENT_KEYS.write() = pressed_keys.iter().map(|k| keycode_to_i64(k)).collect(); +} + +/// Sync changes back to world after script execution +pub fn sync_transform_to_world(world: &mut World) { + if let Some(entity) = *CURRENT_ENTITY.read() { + if let Ok(mut transform) = world.get::<&mut Transform>(entity) { + transform.position = *CURRENT_POSITION.read(); + transform.rotation = *CURRENT_ROTATION.read(); + transform.scale = *CURRENT_SCALE.read(); + } + } +} + +// ============================================================================= +// JNI Native Functions - Called from Kotlin +// ============================================================================= + +/// Get the transform position of the current entity +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getPositionX( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_POSITION.read().x +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getPositionY( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_POSITION.read().y +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getPositionZ( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_POSITION.read().z +} + +/// Set the transform position of the current entity +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_setPosition( + _env: JNIEnv, + _class: JClass, + x: jdouble, + y: jdouble, + z: jdouble, +) { + *CURRENT_POSITION.write() = DVec3::new(x, y, z); +} + +/// Get rotation quaternion components +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationX( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_ROTATION.read().x +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationY( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_ROTATION.read().y +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationZ( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_ROTATION.read().z +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationW( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_ROTATION.read().w +} + +/// Set rotation quaternion +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_setRotation( + _env: JNIEnv, + _class: JClass, + x: jdouble, + y: jdouble, + z: jdouble, + w: jdouble, +) { + *CURRENT_ROTATION.write() = DQuat::from_xyzw(x, y, z, w); +} + +/// Get scale components +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getScaleX( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_SCALE.read().x +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getScaleY( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_SCALE.read().y +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getScaleZ( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_SCALE.read().z +} + +/// Set scale +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_setScale( + _env: JNIEnv, + _class: JClass, + x: jdouble, + y: jdouble, + z: jdouble, +) { + *CURRENT_SCALE.write() = DVec3::new(x, y, z); +} + +// ============================================================================= +// Input System JNI Functions +// ============================================================================= + +/// Check if a key is pressed +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_isKeyPressed( + _env: JNIEnv, + _class: JClass, + keycode: jlong, +) -> jboolean { + let keys = CURRENT_KEYS.read(); + keys.contains(&keycode) as jboolean } -// JNIEXPORT jstring JNICALL Java_com_dropbear_ffi_NativeEngine_Ping -// (JNIEnv *, jobject, jstring); +/// Get mouse X position #[unsafe(no_mangle)] -#[allow(non_snake_case)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_Ping<'local>( - mut env: JNIEnv<'local>, - _this: JObject<'local>, - input: JString<'local>, -) -> JString<'local> { - let message: String = env.get_string(&input) - .expect("Failed to get input string") - .into(); +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getMouseX( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_MOUSE.read().0 +} - let response = format!("Pong! You sent: {}", message); +/// Get mouse Y position +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getMouseY( + _env: JNIEnv, + _class: JClass, +) -> jdouble { + CURRENT_MOUSE.read().1 +} - let output = env.new_string(response) - .expect("Failed to create output string"); +/// Helper function to convert KeyCode to i64 +fn keycode_to_i64(key: &KeyCode) -> i64 { + match key { + KeyCode::KeyA => 65, + KeyCode::KeyB => 66, + KeyCode::KeyC => 67, + KeyCode::KeyD => 68, + KeyCode::KeyE => 69, + KeyCode::KeyF => 70, + KeyCode::KeyG => 71, + KeyCode::KeyH => 72, + KeyCode::KeyI => 73, + KeyCode::KeyJ => 74, + KeyCode::KeyK => 75, + KeyCode::KeyL => 76, + KeyCode::KeyM => 77, + KeyCode::KeyN => 78, + KeyCode::KeyO => 79, + KeyCode::KeyP => 80, + KeyCode::KeyQ => 81, + KeyCode::KeyR => 82, + KeyCode::KeyS => 83, + KeyCode::KeyT => 84, + KeyCode::KeyU => 85, + KeyCode::KeyV => 86, + KeyCode::KeyW => 87, + KeyCode::KeyX => 88, + KeyCode::KeyY => 89, + KeyCode::KeyZ => 90, + KeyCode::ArrowLeft => 37, + KeyCode::ArrowUp => 38, + KeyCode::ArrowRight => 39, + KeyCode::ArrowDown => 40, + KeyCode::Space => 32, + KeyCode::ShiftLeft | KeyCode::ShiftRight => 16, + KeyCode::ControlLeft | KeyCode::ControlRight => 17, + KeyCode::Escape => 27, + _ => 0, + } +} - output.into() -} @@ -1353,6 +1353,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. @@ -17,57 +17,57 @@ fn main() -> anyhow::Result<()> { println!("cargo:rerun-if-changed=.git/refs/heads"); // todo: move this into the "setup" process - let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; - let response = reqwest::blocking::get(repo_zip_url) - .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? - .bytes() - .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; - - let reader = Cursor::new(response); - let mut zip = zip::ZipArchive::new(reader) - .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; - - let app_info = app_dirs2::AppInfo { - name: "Eucalyptus", - author: "4tkbytes", - }; - let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) - .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; - - fs::create_dir_all(&app_data_dir) - .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; - - let resource_prefix = "dropbear-main/resources/"; - let mut found_resource = false; - for i in 0..zip.len() { - let mut file = zip.by_index(i).unwrap(); - let name = file.name(); - - if name.starts_with(resource_prefix) && !name.ends_with('/') { - found_resource = true; - let rel_path = &name[resource_prefix.len()..]; - let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); - let dest_path = app_data_dir.join(rel_path); - - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; - } - - println!("Copying {} to {:?}", name, dest_path); - - let mut outfile = File::create(&dest_path) - .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; - std::io::copy(&mut file, &mut outfile) - .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; - } - } - - if !found_resource { - return Err(anyhow::anyhow!( - "No resources folder found in the github repository [4tkbytes/dropbear] :(" - )); - } + // let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; + // let response = reqwest::blocking::get(repo_zip_url) + // .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? + // .bytes() + // .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; + // + // let reader = Cursor::new(response); + // let mut zip = zip::ZipArchive::new(reader) + // .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; + // + // let app_info = app_dirs2::AppInfo { + // name: "Eucalyptus", + // author: "4tkbytes", + // }; + // let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) + // .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; + // + // fs::create_dir_all(&app_data_dir) + // .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; + // + // let resource_prefix = "dropbear-main/resources/"; + // let mut found_resource = false; + // for i in 0..zip.len() { + // let mut file = zip.by_index(i).unwrap(); + // let name = file.name(); + // + // if name.starts_with(resource_prefix) && !name.ends_with('/') { + // found_resource = true; + // let rel_path = &name[resource_prefix.len()..]; + // let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); + // let dest_path = app_data_dir.join(rel_path); + // + // if let Some(parent) = dest_path.parent() { + // fs::create_dir_all(parent) + // .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; + // } + // + // println!("Copying {} to {:?}", name, dest_path); + // + // let mut outfile = File::create(&dest_path) + // .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; + // std::io::copy(&mut file, &mut outfile) + // .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; + // } + // } + // + // if !found_resource { + // return Err(anyhow::anyhow!( + // "No resources folder found in the github repository [4tkbytes/dropbear] :(" + // )); + // } // fuck you windows :( #[cfg(target_os = "windows")] @@ -594,7 +594,7 @@ impl InspectableComponent for ScriptComponent { ui.horizontal(|ui| { if ui.button("Browse").clicked() && let Some(script_file) = rfd::FileDialog::new() - .add_filter("Typescript", &["ts"]) + .add_filter("Kotlin", &["kt"]) .pick_file() { let script_name = script_file @@ -610,8 +610,8 @@ impl InspectableComponent for ScriptComponent { if ui.button("New").clicked() && let Some(script_path) = rfd::FileDialog::new() - .add_filter("TypeScript", &["ts"]) - .set_file_name(format!("{}_script.ts", label)) + .add_filter("Kotlin", &["kt"]) + .set_file_name(format!("{}_script.kt", label)) .save_file() { match std::fs::write(&script_path, TEMPLATE_SCRIPT) { @@ -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(), } } @@ -1021,6 +1026,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; @@ -1071,6 +1087,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,11 +1,30 @@ package com.dropbear import com.dropbear.ffi.NativeEngine +import com.dropbear.math.Transform +/** + * Main interface to the Dropbear game engine. + * + * This class provides high-level access to engine features for Kotlin scripts. + * It wraps the low-level NativeEngine JNI bindings with a more ergonomic API. + */ class DropbearEngine { - var nativeEngine: NativeEngine? = null + /** + * Get the transform of the current entity. + * + * @return A Transform object that provides live access to position, rotation, and scale + * + * @example + * ```kotlin + * val transform = engine.getTransform() + * transform.position.y += 0.1 // Move up + * ``` + */ + fun getTransform(): Transform { + return Transform.fromNative() + } - // figure out how to create a new class /** * Fetches the currently active entity the script is attached to. * @@ -13,7 +32,8 @@ class DropbearEngine { * a `null` in the form of a [Result] */ fun getActiveEntity(): Result<EntityRef> { - return Result.failure(Exception("Function not implemented")) + // This would need additional native support to get entity ID and label + return Result.failure(Exception("Function not fully implemented - requires entity label support")) } /** @@ -23,6 +43,7 @@ class DropbearEngine { * `null` in the form of a [Result] */ fun getEntity(label: String): EntityRef? { + // This would need additional native support for entity lookup by label return null } -} +} @@ -1,27 +1,18 @@ package com.dropbear +import com.dropbear.math.Transform import com.dropbear.math.Vector3D /** * A class to hold a reference to an entity. - * - * The dropbear engine interface is made in Rust, which uses a - * borrow checker paradigm, which heavily utilises immutability - * unless explicitly provided with the `mut` keyword. - * - * Because of this, the primary source of the World (place to store - * entities) is stored in Rust, and passing an EntityRef (which contains - * an ID) follows Rust's ideologies of passing references instead of the entire entity, - * which provides immutability. - * - * To edit any values part of the entity, take a look at the functions provided - * by [EntityRef], which will require a reference to [DropbearEngine] to push the commands. */ -class EntityRef { - var label: String = "" - - /** - * Sets the position of the entity by a Vector - */ - fun setPosition(position: Vector3D, engine: DropbearEngine) {} -} +class EntityRef(val label: String) { + fun getTransform(engine: DropbearEngine): Transform { + return engine.getTransform() + } + + fun setPosition(position: Vector3D, engine: DropbearEngine) { + val transform = engine.getTransform() + transform.position = position + } +} @@ -1,12 +1,40 @@ package com.dropbear -class Example(override var engine: DropbearEngine) : RunnableScript { - override fun load() { - val entity = engine.getActiveEntity() +import com.dropbear.input.Input +import com.dropbear.input.KeyCode +import com.dropbear.math.Vector3D +class Example : RunnableScript { + override var engine: DropbearEngine = DropbearEngine() + + private var speed = 0.1 + + override fun load() { + println("Example script loaded!") } - + override fun update() { - TODO("Not yet implemented") + val transform = engine.getTransform() + engine.getEntity("player")?.getTransform(engine) + + if (Input.isKeyPressed(KeyCode.W)) { + transform.translate(Vector3D(0.0, 0.0, -speed)) + } + if (Input.isKeyPressed(KeyCode.S)) { + transform.translate(Vector3D(0.0, 0.0, speed)) + } + if (Input.isKeyPressed(KeyCode.A)) { + transform.translate(Vector3D(-speed, 0.0, 0.0)) + } + if (Input.isKeyPressed(KeyCode.D)) { + transform.translate(Vector3D(speed, 0.0, 0.0)) + } + + if (Input.isKeyPressed(KeyCode.SPACE)) { + transform.translate(Vector3D(0.0, speed, 0.0)) + } + if (Input.isKeyPressed(KeyCode.SHIFT)) { + transform.translate(Vector3D(0.0, -speed, 0.0)) + } } -} +} @@ -1,22 +1,77 @@ package com.dropbear.ffi; -import com.dropbear.math.Transform; - /** - * A class for dealing with native functions that is required by the Kotlin scripting - * and the Rust game engine. + * Native interface to the Rust game engine. + * This class provides low-level FFI bindings via JNI. + * + * All methods are static and call into native Rust code. */ public class NativeEngine { + static { + // Load the native library + // In production, this should load from the JAR or a known location + try { + System.loadLibrary("eucalyptus_core"); + } catch (UnsatisfiedLinkError e) { + System.err.println("Failed to load native library: " + e.getMessage()); + } + } + + // ============================================================================= + // Context Management + // ============================================================================= + /** - * Fetches the transform of the entity by its label in the editor - * @param label The label of the entity - * @return The {@link Transform} component if the entity exists, null if not. + * Set the current scripting context (internal use only) */ - public native Transform getTransformOfEntity(String label); + public static native void setContext(long entityId, long worldPtr); + + // ============================================================================= + // Transform - Position + // ============================================================================= + + public static native double getPositionX(); + public static native double getPositionY(); + public static native double getPositionZ(); + public static native void setPosition(double x, double y, double z); + + // ============================================================================= + // Transform - Rotation (Quaternion) + // ============================================================================= + + public static native double getRotationX(); + public static native double getRotationY(); + public static native double getRotationZ(); + public static native double getRotationW(); + public static native void setRotation(double x, double y, double z, double w); + + // ============================================================================= + // Transform - Scale + // ============================================================================= + + public static native double getScaleX(); + public static native double getScaleY(); + public static native double getScaleZ(); + public static native void setScale(double x, double y, double z); + // ============================================================================= + // Input System + // ============================================================================= + + /** + * Check if a key is currently pressed + * @param keycode The keycode to check (use KeyCode constants) + * @return true if the key is pressed, false otherwise + */ + public static native boolean isKeyPressed(long keycode); + + /** + * Get the current mouse X position + */ + public static native double getMouseX(); + /** - * Fetches the label of the entity this script is currently attached to. - * @return The label of the attached entity or null if its not attached + * Get the current mouse Y position */ - public native String getLabelOfAttachedEntity(); + public static native double getMouseY(); } @@ -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 +} @@ -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 + ) + } +}