tirbofish/dropbear · diff
worked on the NativeEngine, i think im getting somewhere (hopefully).
all i remember is that eucalyptus-core is now a cdylib and a rlib, and a world is passed as a pointer. thats all i remember lol.
Signature present but could not be verified.
Unverified
@@ -51,4 +51,8 @@ Cargo.lock **/*.rs.bk *.pdb -.idea/ +.idea/ + +src/*.dll +src/*.dylib +src/*.so @@ -0,0 +1,24 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="generateJniHeaders" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value="generateJniHeaders" /> + </list> + </option> + <option name="vmOptions" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <RunAsTest>false</RunAsTest> + <method v="2" /> + </configuration> +</component> @@ -26,18 +26,17 @@ With Unix systems (macOS not tested), you will have to download a couple depende <!-- 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. --> -### Dependencies ```bash # ubuntu sudo apt install libudev-dev pkg-config libssl-dev clang cmake meson assimp-utils # 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 ``` -### Engine Build +Then run this to build the project ```bash git clone git@github.com:4tkbytes/dropbear @@ -45,6 +44,7 @@ cd dropbear # this will build all the projects in the workspace cargo build +./gradlew build ``` [//]: # (# ensure submodules are checked-out) @@ -64,7 +64,14 @@ If you do not want to build it locally, you are able to download the latest acti Despite the dropbear-engine (and other components) being made in Rust, the editor has chosen the scripting language of choice to be `Kotlin` because of previous experience and that Kotlin is more multiplatform than Swift. -Java is possible (as the JVM runs the script), however it is not officially supported and Kotlin is recommended. . +The dropbear engine uses Kotlin Multiplatform, which allows the cooked up product to be compatible with all platforms +KMP can support, which includes mobile, WASM and desktop. Because the editor is only available on desktop, the JVM is +used to evaluate the scripts as it allows for hot-reloading (not made yet). + +It is recommended to use IntelliJ IDEA with the Rust plugin to help contribute to the engine. If you are a normal joe, +then just use the standard IntelliJ IDEA. + +## Documentation API documentation and articles are available at (todo) @@ -77,11 +84,11 @@ API documentation and articles are available at (todo) <sup>1</sup> Will never be implemented; not intended for that platform. -<sup>2</sup> Made some progress on implementing, but currently a WIP. +<sup>2</sup> Made some progress on implementing, but currently a WIP. ## Contributions -Yeah yeah, go ahead and contribute. Make sure it works, and its not spam, and any tests pass. +Yeah, yeah, go ahead and contribute. Make sure it works, and its not spam, and any tests pass. # Licensing @@ -29,12 +29,11 @@ kotlin { compilations.getByName("main") { cinterops { val dropbear by creating { - defFile(project.file("src/nativeInterop/cinterop/dropbear.def")) - includeDirs.headerFilterOnly(project.file("src/nativeInterop/cinterop")) + defFile(project.file("src/dropbear.def")) + includeDirs.headerFilterOnly(project.file("headers")) } } } - binaries { sharedLib { baseName = "dropbear" @@ -43,6 +42,11 @@ kotlin { } sourceSets { + commonMain { + dependencies { + implementation("co.touchlab:kermit:2.0.4") + } + } nativeMain { dependencies { implementation(libs.kotlinxSerializationJson) @@ -6,6 +6,9 @@ license-file.workspace = true repository.workspace = true readme = "README.md" +[lib] +crate-type = ["rlib", "cdylib"] + [dependencies] anyhow.workspace = true bincode.workspace = true @@ -2,4 +2,6 @@ The core libraries of the eucalyptus editor. Great for embedding into `redback-runtime` and `eucalyptus-editor` as one big change instead of a bunch of features. -This is a library, so if tools are wished to be made, this is the perfect library for you. +This is a library, so if tools are wished to be made, this is the perfect library for you. + +it also produces a shared library for Kotlin/Native and the JVM :) @@ -12,83 +12,12 @@ use crate::ptr::SafePointer; pub const TEMPLATE_SCRIPT: &str = include_str!("../../resources/scripting/kotlin/Template.kt"); -static CONTEXT: LazyLock<DropbearScriptingAPIContext> = LazyLock::new(|| DropbearScriptingAPIContext::new()); - -pub struct DropbearScriptingAPIContext { - pub current_entity: Option<Entity>, - // fyi: im pretty sure this is safe because I can just null with [`Option::None`] - current_world: Option<SafePointer<World>>, - pub current_input: Option<InputState>, - pub persistent_data: HashMap<String, Value>, - pub frame_data: HashMap<String, Value>, -} - -impl Default for DropbearScriptingAPIContext { - fn default() -> Self { - Self::new() - } -} - -impl DropbearScriptingAPIContext { - pub fn new() -> Self { - Self { - current_entity: None, - current_world: None, - current_input: None, - persistent_data: HashMap::new(), - frame_data: HashMap::new(), - } - } - - pub fn set_context(&mut self, entity: Entity, world: &mut World, input: &InputState) { - self.current_entity = Some(entity); - self.current_world = Some(SafePointer::new(world)); - self.current_input = Some(input.clone()); - } - - pub fn clear_context(&mut self) { - self.current_entity = None; - self.current_world = None; - self.current_input = None; - self.frame_data.clear(); - } - - pub fn get_current_entity(&self) -> Option<Entity> { - self.current_entity - } - - pub fn get_input(&self) -> Option<&InputState> { - self.current_input.as_ref() - } - - pub fn set_persistent_data(&mut self, key: String, value: Value) { - self.persistent_data.insert(key, value); - } - - pub fn get_persistent_data(&self, key: &str) -> Option<&Value> { - self.persistent_data.get(key) - } - - pub fn set_frame_data(&mut self, key: String, value: Value) { - self.frame_data.insert(key, value); - } - - pub fn get_frame_data(&self, key: &str) -> Option<&Value> { - self.frame_data.get(key) - } - - pub fn cleanup_entity_data(&mut self, entity: Entity) { - let entity_prefix = format!("entity_{:?}_", entity); - self.persistent_data - .retain(|k, _| !k.starts_with(&entity_prefix)); - } -} - pub struct ScriptManager; impl ScriptManager { pub fn new() -> anyhow::Result<Self> { - Err(anyhow::anyhow!("it aint ready yet bozo")) + Ok(Self) + // Err(anyhow::anyhow!("it aint ready yet bozo")) } pub fn load_script( @@ -101,7 +30,7 @@ impl ScriptManager { pub fn init_entity_script( &mut self, entity_id: hecs::Entity, - tags: &Vec<String>, + tags: Vec<String>, world: &mut World, input_state: &InputState, ) -> anyhow::Result<()> { @@ -112,7 +41,7 @@ impl ScriptManager { pub fn update_entity_script( &mut self, entity_id: hecs::Entity, - tags: &Vec<String>, + tags: Vec<String>, world: &mut World, input_state: &InputState, dt: f32, @@ -1 +1,2 @@ -//! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate +//! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate + @@ -1 +1,28 @@ -//! Deals with Kotlin/Native library loading for different platforms. +//! Deals with Kotlin/Native library loading for different platforms. + +use std::ffi::c_char; +use hecs::World; +use dropbear_engine::entity::AdoptedEntity; + +#[unsafe(no_mangle)] +pub extern "C" fn dropbear_get_entity(label: *const c_char, world_ptr: *const World) -> usize { + if world_ptr.is_null() { + log::debug!("World pointer is null"); + return 0; + } + + unsafe { + let world = &*world_ptr; + + for (id, entity) in world.query::<&AdoptedEntity>().iter() { + if let Ok(label) = std::ffi::CStr::from_ptr(label).to_str() + && entity.model.label == label + { + log::debug!("Found entity with label: {}", label); + return id.id() as usize; + } + } + log::warn!("Entity with label: {} not found", std::ffi::CStr::from_ptr(label).to_str().unwrap()); + 0 + } +} @@ -48,7 +48,7 @@ use dropbear_engine::model::{ModelId}; pub struct Editor { scene_command: SceneCommand, - pub world: World, + pub world: Box<World>, dock_state: DockState<EditorTab>, texture_id: Option<egui::TextureId>, size: Extent3d, @@ -149,7 +149,7 @@ impl Editor { is_viewport_focused: false, // is_cursor_locked: false, window: None, - world: World::new(), + world: Box::new(World::new()), show_new_project: false, project_name: String::new(), project_path: Arc::new(Mutex::new(None)), @@ -52,7 +52,7 @@ impl Scene for Editor { if let Some(mut receiver) = self.world_receiver.take() { self.show_project_loading_window(&graphics.shared.get_egui_context()); if let Ok(loaded_world) = receiver.try_recv() { - self.world = loaded_world; + self.world = Box::new(loaded_world); self.is_world_loaded.mark_project_loaded(); if let Some(dock_state_shared) = &self.dock_state_shared && @@ -121,7 +121,7 @@ impl Scene for Editor { for (entity_id, script_name) in script_entities { if let Err(e) = self.script_manager.update_entity_script( entity_id, - &script_name, + script_name.clone(), &mut self.world, &self.input_state, dt, @@ -175,46 +175,6 @@ impl Scene for Editor { } } - // let camera_follow_data: Vec<(Entity, String, glam::Vec3)> = { - // self.world - // .query::<(&Camera, &CameraComponent)>() - // .iter() - // .filter_map(|(entity_id, (_, _))| { - // follow_target.map(|target| { - // ( - // entity_id, - // target.follow_target.clone(), - // target.offset.as_vec3() - // ) - // }) - // }) - // .collect() - // }; - - - // for (camera_entity, target_label, offset) in camera_follow_data { - // let target_position = { - // self.world - // .query::<(&AdoptedEntity, &Transform)>() - // .iter() - // .find_map(|(_, (adopted, transform))| { - // if adopted.model.label == target_label { - // Some(transform.position) - // } else { - // None - // } - // }) - // }; - // - // - // if let Some(pos) = target_position - // && let Ok(mut query) = self.world.query_one::<&mut Camera>(camera_entity) - // && let Some(camera) = query.get() { - // camera.eye = pos + offset.as_dvec3(); - // camera.target = pos; - // } - // } - { for (_entity_id, (camera, component)) in self.world .query::<(&mut Camera, &mut CameraComponent)>() @@ -24,6 +24,7 @@ impl SignalController for Editor { match &self.signal { Signal::None => { + // returns absolutely nothing because no signal is set. Ok::<(), anyhow::Error>(()) } Signal::Copy(_) => {Ok(())} @@ -92,210 +93,6 @@ impl SignalController for Editor { self.signal = Signal::None; Ok(()) } - // Signal::ScriptAction(action) => match action { - // ScriptAction::AttachScript { - // script_path, - // script_name, - // } => { - // if let Some(selected_entity) = self.selected_entity { - // match scripting::move_script_to_src(script_path) { - // Ok(moved_path) => { - // let new_script = ScriptComponent { - // name: script_name.clone(), - // path: moved_path.clone(), - // }; - // - // let replaced = { - // if let Ok(mut sc) = self.world.get::<&mut ScriptComponent>(selected_entity) { - // sc.name = new_script.name.clone(); - // sc.path = new_script.path.clone(); - // true - // } else { - // false - // } - // }; - // - // if !replaced { - // match scripting::attach_script_to_entity( - // &mut self.world, - // selected_entity, - // new_script.clone(), - // ) { - // Ok(_) => { - // } - // Err(e) => { - // self.signal = Signal::None; - // fatal!("Failed to attach script to entity {:?}: {}", - // selected_entity, - // e); - // return Err(anyhow::anyhow!(e)); - // } - // } - // } - // - // { - // if let Err(e) = scripting::convert_entity_to_group( - // &mut self.world, - // selected_entity, - // ) { - // log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - // } - // } - // - // success!( - // "{} script '{}' at {} to entity {:?}", - // if replaced { "Reattached" } else { "Attached" }, - // script_name, - // moved_path.display(), - // selected_entity - // ); - // } - // Err(e) => { - // fatal!("Move failed: {}", e); - // } - // } - // } else { - // fatal!("AttachScript requested but no entity is selected"); - // } - // - // self.signal = Signal::None; - // Ok(()) - // } - // ScriptAction::CreateAndAttachScript { - // script_path, - // script_name, - // } => { - // if let Some(selected_entity) = self.selected_entity { - // let new_script = ScriptComponent { - // name: script_name.clone(), - // path: script_path.clone(), - // }; - // - // let replaced = { - // if let Ok(mut sc) = self.world.get::<&mut ScriptComponent>(selected_entity) { - // sc.name = new_script.name.clone(); - // sc.path = new_script.path.clone(); - // true - // } else { - // false - // } - // }; - // - // if !replaced { - // match scripting::attach_script_to_entity( - // &mut self.world, - // selected_entity, - // new_script.clone(), - // ) { - // Ok(_) => { - // } - // Err(e) => { - // self.signal = Signal::None; - // fatal!("Failed to attach new script: {}", e); - // return Err(anyhow::anyhow!(e)); - // } - // } - // } - // - // { - // if let Err(e) = scripting::convert_entity_to_group( - // &mut self.world, - // selected_entity, - // ) { - // log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - // } - // } - // - // success!( - // "{} new script '{}' at {} to entity {:?}", - // if replaced { "Replaced" } else { "Attached" }, - // script_name, - // script_path.display(), - // selected_entity - // ); - // } else { - // warn_without_console!("No selected entity to attach new script"); - // log::warn!("CreateAndAttachScript requested but no entity is selected"); - // } - // self.signal = Signal::None; - // Ok(()) - // } - // ScriptAction::RemoveScript => { - // if let Some(selected_entity) = self.selected_entity { - // let mut success = false; - // let mut comp = ScriptComponent::default(); - // { - // if let Ok(script) = self.world - // .remove_one::<ScriptComponent>(selected_entity) - // { - // success!("Removed script from entity {:?}", selected_entity); - // success = true; - // comp = script.clone(); - // } else { - // warn!("No script component found on entity {:?}", selected_entity); - // } - // match self.world.insert_one(selected_entity, ScriptComponent::default()) { - // Ok(_) => { - // log::debug!("Inserted default script component"); - // } - // Err(e) => { - // log::warn!("No such entity is available. Additional info: {}", e); - // } - // } - // } - // - // if success { - // if let Err(e) = scripting::convert_entity_to_group( - // &mut self.world, - // selected_entity, - // ) { - // log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - // } - // log::debug!("Pushing remove component to undo stack"); - // UndoableAction::push_to_undo( - // &mut self.undo_stack, - // UndoableAction::RemoveComponent( - // selected_entity, - // Box::new(ComponentType::Script(comp)), - // ), - // ); - // } - // } else { - // warn!("No entity selected to remove script from"); - // } - // - // self.signal = Signal::None; - // Ok(()) - // } - // ScriptAction::EditScript => { - // if let Some(selected_entity) = self.selected_entity { - // let script_opt = { - // if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(selected_entity) { - // q.get().cloned() - // } else { - // None - // } - // }; - // - // if let Some(script) = script_opt { - // match open::that(script.path.clone()) { - // Ok(()) => { - // success!("Opened {}", script.name) - // } - // Err(e) => { - // warn!("Error while opening {}: {}", script.name, e); - // } - // } - // } else { - // warn!("No script component found on entity {:?}", selected_entity); - // } - // } else { - // warn!("No entity selected to edit script"); - // } - // self.signal = Signal::None; - // Ok(()) - // } - // }, Signal::Play => { if matches!(self.editor_state, EditorState::Playing) { fatal!("Unable to play: already in playing mode"); @@ -339,7 +136,7 @@ impl SignalController for Editor { Ok(_) => { if let Err(e) = self.script_manager.init_entity_script( entity_id, - &script.tags, + script.tags.clone(), &mut self.world, &self.input_state, ) { @@ -392,28 +189,6 @@ impl SignalController for Editor { self.signal = Signal::None; Ok(()) } - // Signal::CameraAction(action) => match action { - // CameraAction::SetPlayerTarget { .. } => { - // log::warn!("Deprecated: CameraAction::SetPlayerTarget"); - // self.signal = Signal::None; - // Ok(()) - // } - // CameraAction::ClearPlayerTarget => { - // log::warn!("Deprecated: CameraAction::ClearPlayerTarget"); - // self.signal = Signal::None; - // Ok(()) - // } - // CameraAction::SetCurrentPositionAsOffset(_) => { - // // if let Ok((camera, target)) = self.world.query_one_mut::<(&Camera, &mut CameraFollowTarget)>(*entity) { - // // target.offset = camera.target - // // } else { - // // warn!("Unable to query camera to set current camera position to offset"); - // // } - // log::warn!("Deprecated: CameraAction::SetCurrentPositionAsOffset"); - // self.signal = Signal::None; - // Ok(()) - // } - // }, Signal::AddComponent(entity, e_type) => { match e_type { EntityType::Entity => { @@ -0,0 +1,23 @@ +#ifndef DROPBEAR_H +#define DROPBEAR_H + +#include <stddef.h> +#include <stdint.h> + +typedef struct World World; // opaque pointer + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// =========================================== + +// returns 0 on success, non-zero on failure +int dropbear_get_entity(const char* label, const World* world_ptr, uint64_t* out_entity); + +// =========================================== + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus +#endif // DROPBEAR_H @@ -1,24 +1,21 @@ package com.dropbear import com.dropbear.ffi.NativeEngine +import getProjectScriptMetadata class DropbearEngine(val native: NativeEngine) { - /** - * Fetches an entity based on its label in the editor. - * - * If there is no entity under that label, it will return a - * `null` in the form of a [Result] - */ + private val globalScripts = mutableListOf<System>() + + fun init() { + val scriptRegistration = getProjectScriptMetadata() + } + fun getEntity(label: String): Result<EntityRef> { val entityId = native.getEntity(label) return (when (entityId) { null -> { Result.failure(Exception("JNI returned null")) } - -1L -> { - // -1L means that query couldn't find entity with such a label - Result.failure(Exception("Entity with id $label not found")) - } else -> { Result.success(EntityRef(EntityId(entityId))) } @@ -1,3 +1,3 @@ package com.dropbear -data class EntityId(val id: Long) +data class EntityId(val id: ULong) @@ -17,7 +17,7 @@ import com.dropbear.math.Vector3D * 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(val id: EntityId = EntityId(-1)) { +class EntityRef(val id: EntityId = EntityId(0u)) { /** * Sets the position of the entity by a Vector */ @@ -9,7 +9,15 @@ fun playerMovement(engine: DropbearEngine, entityId: EntityId, deltaTime: Double } class Player: System { - override fun update(engine: DropbearEngine, entityId: EntityId, deltaTime: Float) { + override fun load(engine: DropbearEngine) { + TODO("Not yet implemented") + } + + override fun update(engine: DropbearEngine, deltaTime: Float) { + TODO("Not yet implemented") + } + + override fun destroy(engine: DropbearEngine) { TODO("Not yet implemented") } } @@ -23,4 +31,6 @@ class Metadata : ProjectScriptingMetadata { ), ) } -} +} + +fun getProjectScriptMetadata(): ProjectScriptingMetadata = Metadata() @@ -1,5 +1,7 @@ package com.dropbear -fun interface System { - fun update(engine: DropbearEngine, current_entity: EntityId, deltaTime: Float) +interface System { + fun load(engine: DropbearEngine) + fun update(engine: DropbearEngine, deltaTime: Float) + fun destroy(engine: DropbearEngine) } @@ -3,5 +3,6 @@ package com.dropbear.ffi import com.dropbear.EntityRef expect class NativeEngine { - fun getEntity(label: String): Long? + fun init(handle: ULong) + fun getEntity(label: String): ULong? } @@ -0,0 +1,4 @@ +package: com.dropbear.ffi.generated +headers: dropbear.h +headerFilter: dropbear.h +libraryPaths: src @@ -1,5 +1,5 @@ package com.dropbear.ffi; public class JNINative { - public native long getEntity(String label); + public native long getEntity(long handle, String label); } @@ -1,9 +1,23 @@ package com.dropbear.ffi actual class NativeEngine { + private var worldHandle: ULong = 0u private val jni = JNINative() - actual fun getEntity(label: String): Long? { - return jni.getEntity(label) + actual fun getEntity(label: String): ULong? { + val result = jni.getEntity(worldHandle.toLong(), label) + return if (result < 0) { + null + } else { + result.toULong() + } + } + + actual fun init(handle: ULong) { + this.worldHandle = handle + if (this.worldHandle == 0uL) { + println("NativeEngine: Error - Invalid world handle received!") + return + } } } @@ -1,7 +0,0 @@ -headers = dropbear.h -headerFilter = dropbear.h -package = com.dropbear.ffi.native - - -long long dropbear_get_entity(const char* param); @@ -1,9 +0,0 @@ -#ifndef DROPBEAR_H -#define DROPBEAR_H - -#include <stdint.h> -#include <stdbool.h> - -long long dropbear_get_entity(const char* param); - -#endif // DROPBEAR_H @@ -0,0 +1,25 @@ +@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) + +package com.dropbear.ffi + +import co.touchlab.kermit.Logger +import com.dropbear.DropbearEngine +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + +@CName("dropbear_entry") +fun entry(worldHandle: ULong) { + Logger.i { "Starting kotlin scripting guest" } + val nativeEngine = NativeEngine() + nativeEngine.init(worldHandle) +} + +@CName("dropbear_load") +fun loadScriptByTag(tag: String?) { + +} + +@CName("dropbear_update") +fun updateScriptByTag(tag: String?, deltaTime: Double) { + +} @@ -1,13 +1,26 @@ -@file:OptIn(ExperimentalForeignApi::class) +@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package com.dropbear.ffi +import co.touchlab.kermit.Logger import kotlinx.cinterop.* +import kotlin.experimental.ExperimentalNativeApi actual class NativeEngine { - actual fun getEntity(label: String): Long? { - return com.dropbear.ffi.native.dropbear_get_entity(label) -// return 0L + private var worldHandle: ULong = 0u + + actual fun init(handle: ULong) { + this.worldHandle = handle + if (this.worldHandle == 0uL) { + Logger.i("NativeEngine: Error - Invalid world handle received!") + return + } else { + Logger.i("NativeEngine: Initialized with world handle: ${this.worldHandle}") + } + } + + actual fun getEntity(label: String): ULong? { + TODO("Not yet implemented") } }