tirbofish/dropbear · diff
feature: OnRails, which allows an entities EntityTransform be constricted by a set path.
refactor: made collider wireframe use DebugDraw instead of its own pipeline.
feature+refactor: allowed for components to be created within the users kotlin codebase instead of a rust only implementation.
wip: improving UI with a stolen WidgetType struct from egui LOL
Signature present but could not be verified.
Unverified
@@ -90,6 +90,7 @@ rkyv = "0.8" bevy_mikktspace = "1.0.0" naga = { version = "29", features = ["wgsl-in"] } uuid = { version = "1.22", features = ["v4", "serde"] } +splines = "5.0" [workspace.dependencies.image] version = "0.25" @@ -239,6 +239,24 @@ impl DebugDraw { } } + /// Draws a wireframe cylinder centered at `center`, aligned to `axis`. + pub fn draw_cylinder(&mut self, center: Vec3, half_height: f32, radius: f32, axis: Vec3, colour: [f32; 4]) { + let axis = axis.normalize(); + let top = center + axis * half_height; + let bottom = center - axis * half_height; + + self.draw_circle(top, radius, axis, colour); + self.draw_circle(bottom, radius, axis, colour); + + let up = if axis.dot(Vec3::Y).abs() < 0.99 { Vec3::Y } else { Vec3::Z }; + let tangent = axis.cross(up).normalize(); + let bitangent = axis.cross(tangent).normalize(); + + for dir in [tangent, -tangent, bitangent, -bitangent] { + self.draw_line(top + dir * radius, bottom + dir * radius, colour); + } + } + /// Draws a wireframe cone from `apex` extending in `dir`. /// /// `angle` is the half-angle of the cone in radians. @@ -51,6 +51,7 @@ wgpu = {workspace = true, features = ["serde"]} rkyv.workspace = true bitflags.workspace = true uuid.workspace = true +splines.workspace = true [features] default = [] @@ -1,5 +1,6 @@ use crate::hierarchy::EntityTransformExt; use crate::physics::PhysicsState; +use crate::scripting::types::KotlinComponents; use crate::ser::model::EucalyptusModel; use crate::states::{SerializedMaterialCustomisation, SerializedMeshRenderer}; use crate::utils::{ResolveReference}; @@ -13,6 +14,7 @@ use dropbear_engine::texture::{Texture, TextureBuilder, TextureReference}; use dropbear_engine::utils::ResourceReference; use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, UiBuilder}; use hecs::{Entity, World}; +use parking_lot::Mutex; pub use serde::{Deserialize, Serialize}; use std::any::TypeId; use std::collections::HashMap; @@ -20,13 +22,34 @@ use std::collections::hash_map::DefaultHasher; use std::future::Future; use std::hash::{Hash, Hasher}; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use std::time::{SystemTime, UNIX_EPOCH}; pub use typetag::*; pub const DRAGGED_ASSET_ID: &str = "dragged_asset_reference"; +/// Metadata for a single Kotlin-defined component type, queued via JNI during JVM startup. +#[derive(Clone, Debug)] +pub struct KotlinComponentDecl { + /// Fully qualified class name, e.g. `"com.game.PlayerHealth"`. + pub fqcn: String, + /// Human-readable short name, e.g. `"PlayerHealth"`. + pub type_name: String, + /// Optional editor category, e.g. `"Gameplay"`. + pub category: Option<String>, + /// Optional one-line description shown as a tooltip in the editor. + pub description: Option<String>, +} + +/// Pending Kotlin component declarations pushed by the JNI bridge before the +/// [`ComponentRegistry`] is available. +/// +/// Call [`ComponentRegistry::drain_kotlin_queue`] once after JVM initialisation +/// to absorb these into the registry. +pub static KOTLIN_COMPONENT_QUEUE: LazyLock<Mutex<Vec<KotlinComponentDecl>>> = + LazyLock::new(|| Mutex::new(Vec::new())); + pub struct ComponentRegistry { /// Maps TypeId to ComponentDescriptor for quick lookups descriptors: HashMap<LanguageTypeId, ComponentDescriptor>, @@ -66,11 +89,8 @@ pub type ComponentInitFuture<'a, T> = std::pin::Pin< type LoaderFuture<'a> = Pin< Box< - dyn Future< - Output = anyhow::Result< - Box<dyn for<'b> FnOnce(&'b mut hecs::EntityBuilder) + Send + Sync>, - >, - > + Send + dyn Future<Output = anyhow::Result<Box<dyn ComponentApply + Send + Sync>>> + + Send + Sync + 'a, >, @@ -93,6 +113,11 @@ type InspectFn = Box< // fn inspect(&mut self, world: &hecs::World, entity: hecs::Entity, ui: &mut egui::Ui, graphics: Arc<SharedGraphicsContext>); +/// A type of TypeID that is respective of its language. +/// +/// # Currently supported languages: +/// - Rust - this uses [`TypeId`] to display types. +/// - Kotlin - this uses [`String`] to display a FQCN (fully qualified class name, e.g. "com.dropbear.EntityRef") #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum LanguageTypeId { Rust(TypeId), @@ -121,7 +146,7 @@ impl ComponentRegistry { where T: Component + InspectableComponent + Send + Sync + 'static, T::SerializedForm: 'static + Default, - T::RequiredComponentTypes: Send + Sync, + T::RequiredComponentTypes: Send + Sync + 'static, { let type_id = LanguageTypeId::Rust(TypeId::of::<T>()); let serialized_type_id = LanguageTypeId::Rust(TypeId::of::<T::SerializedForm>()); @@ -158,10 +183,8 @@ impl ComponentRegistry { Box::pin(async move { let bundle = T::init(serialized, graphics).await?; - let applier: Box<dyn FnOnce(&mut hecs::EntityBuilder) + Send + Sync> = - Box::new(move |builder: &mut hecs::EntityBuilder| { - builder.add_bundle(bundle); - }); + let applier: Box<dyn ComponentApply + Send + Sync> = + Box::new(TwoWayApplier::<T> { bundle, _phantom: std::marker::PhantomData }); Ok(applier) }) }), @@ -439,6 +462,74 @@ impl ComponentRegistry { .find(|&type_id| Self::numeric_id(type_id) == id) .cloned() } + + /// Drains all pending Kotlin component declarations from [`KOTLIN_COMPONENT_QUEUE`] and + /// registers them with the registry via [`Self::register_kotlin_descriptor`]. + /// + /// Call this once after JVM initialisation (i.e. after `ScriptManager::load_script` returns) + /// and before the first scene load. Declarations pushed by the JNI bridge during JVM startup + /// will then be visible in the editor Add-Component picker, finders, and removers. + pub fn drain_kotlin_queue(&mut self) { + let decls: Vec<KotlinComponentDecl> = + std::mem::take(&mut *KOTLIN_COMPONENT_QUEUE.lock()); + for decl in decls { + self.register_kotlin_descriptor(decl); + } + } + + /// Registers a single Kotlin-defined component type with the registry. + /// + /// This records a descriptor (visible in the editor), a per-FQCN remover, and a finder. + /// Serialisation round-trips are handled by the aggregated [`KotlinComponents`] hecs + /// component, which is registered as a normal Rust component via + /// `component_registry.register::<KotlinComponents>()`. + pub fn register_kotlin_descriptor(&mut self, decl: KotlinComponentDecl) { + let id = LanguageTypeId::Kotlin(decl.fqcn.clone()); + if self.descriptors.contains_key(&id) { + return; + } + + self.fqtn_to_type.insert(decl.fqcn.clone(), id.clone()); + if let Some(ref cat) = decl.category { + self.categories + .entry(cat.clone()) + .or_default() + .push(id.clone()); + } + self.descriptors.insert( + id.clone(), + ComponentDescriptor { + fqtn: decl.fqcn.clone(), + type_name: decl.type_name, + category: decl.category, + description: decl.description, + disabled_flags: DisabilityFlags::Never, + internal: false, + }, + ); + + let fqcn = decl.fqcn.clone(); + self.removers.insert( + id.clone(), + Box::new(move |world, entity| { + if let Ok(mut kc) = world.get::<&mut KotlinComponents>(entity) { + kc.detach(&fqcn); + } + }), + ); + + let fqcn = decl.fqcn; + self.finders.insert( + id, + Box::new(move |world| { + world + .query::<(hecs::Entity, &KotlinComponents)>() + .iter() + .filter_map(|(entity, kc)| if kc.has(&fqcn) { Some(entity) } else { None }) + .collect() + }), + ); + } } impl Default for ComponentRegistry { @@ -454,12 +545,59 @@ pub trait SerializedComponent: Downcast + dyn_clone::DynClone + Send + Sync {} impl_downcast!(SerializedComponent); dyn_clone::clone_trait_object!(SerializedComponent); +/// An applier produced by the registry loader that can either populate a fresh +/// [`hecs::EntityBuilder`] (during scene spawning) or be inserted into an already-existing +/// entity (when a component is added at runtime). +pub trait ComponentApply: Send + Sync { + /// Adds this component bundle to a [`hecs::EntityBuilder`] for a fresh entity spawn. + fn apply_to_builder(self: Box<Self>, builder: &mut hecs::EntityBuilder); + + /// Inserts this component bundle into an already-existing entity. + /// + /// The default behaviour overwrites any pre-existing component of the same types. + /// Override [`Component::apply_to_existing_entity`] on the concrete component type to + /// preserve existing auxiliary components. + fn apply_to_existing_entity( + self: Box<Self>, + world: &mut hecs::World, + entity: hecs::Entity, + ) -> anyhow::Result<()>; +} + +/// Concrete [`ComponentApply`] produced by the registry loader. +/// +/// Delegates `apply_to_existing_entity` to [`Component::apply_to_existing_entity`] so that +/// individual component types can override the insertion behaviour. +pub(crate) struct TwoWayApplier<T: Component> { + pub bundle: T::RequiredComponentTypes, + pub _phantom: std::marker::PhantomData<T>, +} + +impl<T> ComponentApply for TwoWayApplier<T> +where + T: Component + Send + Sync + 'static, + T::RequiredComponentTypes: Send + Sync + 'static, +{ + fn apply_to_builder(self: Box<Self>, builder: &mut hecs::EntityBuilder) { + builder.add_bundle(self.bundle); + } + + fn apply_to_existing_entity( + self: Box<Self>, + world: &mut hecs::World, + entity: hecs::Entity, + ) -> anyhow::Result<()> { + T::apply_to_existing_entity(self.bundle, world, entity) + } +} + #[derive(Debug, Clone, Default)] pub enum DisabilityFlags { /// Skip this component's `update_component` when the entity is Disabled. #[default] Disabled, /// Skip this component's `update_component` when the entity is Hidden or Disabled. + // they both do the same thing. that needs to be fixed. oh well... Hidden, /// Never skip this component. Used for meta-components such as `EntityStatus`. Never, @@ -501,6 +639,11 @@ pub trait Component: Sync + Send { /// Converts [`Self::SerializedForm`] into a [`Component`] instance that can be added to /// `hecs::EntityBuilder` during scene initialisation. + /// + /// Typically the default is this: + /// ```rust no-run + /// Box::pin(async move { Ok((ser.clone(),)) }) + /// ``` fn init( ser: &'_ Self::SerializedForm, _graphics: Arc<SharedGraphicsContext>, @@ -519,6 +662,26 @@ pub trait Component: Sync + Send { /// Called when saving the scene to disk. Returns the [`Self::SerializedForm`] of the component that can be /// saved to disk. fn save(&self, _world: &hecs::World, _entity: hecs::Entity) -> Box<dyn SerializedComponent>; + + /// Inserts a bundle returned by [`Self::init`] into an already-existing entity. + /// + /// The default implementation inserts the full bundle, **overwriting** any auxiliary + /// component types that may already be present on the entity. Override this when some + /// auxiliary types (e.g. [`EntityTransform`]) should be preserved. + fn apply_to_existing_entity( + bundle: Self::RequiredComponentTypes, + world: &mut hecs::World, + entity: hecs::Entity, + ) -> anyhow::Result<()> + where + Self::RequiredComponentTypes: 'static, + { + let mut builder = hecs::EntityBuilder::new(); + builder.add_bundle(bundle); + world + .insert(entity, builder.build()) + .map_err(|e| anyhow::anyhow!("{e}")) + } } pub trait InspectableComponent: Send + Sync { @@ -16,7 +16,7 @@ macro_rules! with_debug { kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawLine"), c )] -fn debug_draw_line( +fn draw_line( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, start: &NVector3, end: &NVector3, @@ -32,7 +32,7 @@ fn debug_draw_line( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawRay"), c )] -fn debug_draw_ray( +fn draw_ray( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, origin: &NVector3, dir: &NVector3, @@ -48,7 +48,7 @@ fn debug_draw_ray( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawArrow"), c )] -fn debug_draw_arrow( +fn draw_arrow( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, start: &NVector3, end: &NVector3, @@ -64,7 +64,7 @@ fn debug_draw_arrow( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawPoint"), c )] -fn debug_draw_point( +fn draw_point( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, pos: &NVector3, size: f32, @@ -79,7 +79,7 @@ fn debug_draw_point( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCircle"), c )] -fn debug_draw_circle( +fn draw_circle( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, center: &NVector3, radius: f32, @@ -96,7 +96,7 @@ fn debug_draw_circle( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawSphere"), c )] -fn debug_draw_sphere( +fn draw_sphere( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, center: &NVector3, radius: f32, @@ -111,7 +111,7 @@ fn debug_draw_sphere( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawGlobe"), c )] -fn debug_draw_globe( +fn draw_globe( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, center: &NVector3, radius: f32, @@ -128,7 +128,7 @@ fn debug_draw_globe( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawAabb"), c )] -fn debug_draw_aabb( +fn draw_aabb( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, min: &NVector3, max: &NVector3, @@ -144,7 +144,7 @@ fn debug_draw_aabb( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawObb"), c )] -fn debug_draw_obb( +fn draw_obb( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, center: &NVector3, half_extents: &NVector3, @@ -162,7 +162,7 @@ fn debug_draw_obb( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCapsule"), c )] -fn debug_draw_capsule( +fn draw_capsule( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, a: &NVector3, b: &NVector3, @@ -176,10 +176,28 @@ fn debug_draw_capsule( } #[dropbear_macro::export( + kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCylinder"), + c +)] +fn draw_cylinder( + #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, + center: &NVector3, + half_height: f32, + radius: f32, + axis: &NVector3, + colour: &NColour, +) -> DropbearNativeResult<()> { + let c = Vec3::new(center.x as f32, center.y as f32, center.z as f32); + let ax = Vec3::new(axis.x as f32, axis.y as f32, axis.z as f32); + with_debug!(graphics, |dd| dd.draw_cylinder(c, half_height, radius, ax, colour.to_f32_array())); + Ok(()) +} + +#[dropbear_macro::export( kotlin(class = "com.dropbear.rendering.DebugDrawNative", func = "drawCone"), c )] -fn debug_draw_cone( +fn draw_cone( #[dropbear_macro::define(GraphicsContextPtr)] graphics: &SharedGraphicsContext, apex: &NVector3, dir: &NVector3, @@ -39,6 +39,7 @@ use crate::physics::kcc::KCC; use crate::physics::rigidbody::RigidBody; use crate::billboard::BillboardComponent; use crate::entity_status::EntityStatus; +use crate::scripting::types::KotlinComponents; use crate::states::Script; use crate::ui::HUDComponent; use dropbear_engine::animation::AnimationComponent; @@ -48,6 +49,7 @@ use dropbear_engine::lighting::Light; pub use egui; use properties::CustomProperties; pub use rapier3d; +use crate::transform::OnRails; /// The appdata directory for storing any information. /// @@ -72,4 +74,6 @@ pub fn register_components(component_registry: &mut ComponentRegistry) { component_registry.register::<AnimationComponent>(); component_registry.register::<BillboardComponent>(); component_registry.register::<HUDComponent>(); + component_registry.register::<OnRails>(); + component_registry.register::<KotlinComponents>(); } @@ -257,7 +257,7 @@ impl SceneConfig { for component in components.iter() { if component.as_any().downcast_ref::<Parent>().is_some() { log::debug!( - "Skipping Parent component for '{}' - will be rebuilt from hierarchy_map", + "Skipping Parent component for '{}', will be rebuilt from hierarchy_map", label_for_logs ); continue; @@ -274,7 +274,7 @@ impl SceneConfig { }; let applier = loader_future.await?; - applier(&mut builder); + applier.apply_to_builder(&mut builder); } let entity = world.spawn(builder.build()); @@ -7,6 +7,7 @@ pub mod jni; pub mod native; pub mod result; pub mod utils; +pub mod types; pub static JVM_ARGS: OnceLock<String> = OnceLock::new(); pub static AWAIT_JDB: OnceLock<bool> = OnceLock::new(); @@ -0,0 +1,141 @@ +use crate::component::{ + Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, + SerializedComponent, +}; +use crate::physics::PhysicsState; +use crate::scripting::result::DropbearNativeResult; +use dropbear_engine::graphics::SharedGraphicsContext; +use egui::{CollapsingHeader, Ui}; +use hecs::{Entity, World}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// Single [`hecs`] component that represents all Kotlin components on this entity. + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct KotlinComponents { + /// A list of `fully qualified class names`. + pub fqcns: Vec<String>, +} + +#[typetag::serde] +impl SerializedComponent for KotlinComponents {} + +impl KotlinComponents { + pub fn attach(&mut self, fqcn: &str) { + if !self.fqcns.iter().any(|f| f == fqcn) { + self.fqcns.push(fqcn.to_owned()); + } + } + + pub fn detach(&mut self, fqcn: &str) { + self.fqcns.retain(|f| f != fqcn); + } + + pub fn has(&self, fqcn: &str) -> bool { + self.fqcns.iter().any(|f| f == fqcn) + } +} + +impl Component for KotlinComponents { + type SerializedForm = Self; + type RequiredComponentTypes = (Self,); + + fn descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "eucalyptus_core::scripting::types::KotlinComponents".to_string(), + type_name: "KotlinComponents".to_string(), + category: None, + description: Some( + "Tracks all Kotlin-defined components attached to this entity.".to_string(), + ), + disabled_flags: DisabilityFlags::Never, + internal: true, + } + } + + fn init( + ser: &'_ Self::SerializedForm, + _graphics: Arc<SharedGraphicsContext>, + ) -> ComponentInitFuture<'_, Self> { + let cloned = ser.clone(); + Box::pin(async move { Ok((cloned,)) }) + } + + fn update_component( + &mut self, + _world: &World, + _physics: &mut PhysicsState, + _entity: Entity, + _dt: f32, + _graphics: Arc<SharedGraphicsContext>, + ) { + } + + fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> { + Box::new(self.clone()) + } +} + +impl InspectableComponent for KotlinComponents { + fn inspect( + &mut self, + _world: &World, + _entity: Entity, + ui: &mut Ui, + _graphics: Arc<SharedGraphicsContext>, + ) { + CollapsingHeader::new("Kotlin Components") + .default_open(true) + .show(ui, |ui| { + for fqcn in &self.fqcns { + ui.label(fqcn); + } + }); + } +} + +/// Queries whether an entity has a specific Kotlin component attached. +/// +/// Called from JVM via `ComponentNative.hasKotlinComponent(worldPtr, entityId, fqcn)`. +#[dropbear_macro::export(kotlin( + class = "com.dropbear.components.ComponentNative", + func = "hasKotlinComponent", +))] +fn has_kotlin_component( + #[dropbear_macro::define(WorldPtr)] world: &World, + #[dropbear_macro::entity] entity: Entity, + fqcn: String, +) -> DropbearNativeResult<bool> { + if let Ok(kc) = world.get::<&KotlinComponents>(entity) { + Ok(kc.has(&fqcn)) + } else { + Ok(false) + } +} + +/// Registers a Kotlin component type with the engine's [`crate::component::ComponentRegistry`]. +/// +/// Called from JVM during startup via `ComponentNative.registerKotlinComponent(...)`. +/// The declaration is queued in [`crate::component::KOTLIN_COMPONENT_QUEUE`] and consumed +/// by [`crate::component::ComponentRegistry::drain_kotlin_queue`] after JVM init completes. +#[dropbear_macro::export(kotlin( + class = "com.dropbear.components.ComponentNative", + func = "registerKotlinComponent", +))] +fn register_kotlin_component_jni( + #[dropbear_macro::define(WorldPtr)] _world: &World, + fqcn: String, + type_name: String, + category: String, + description: String, +) -> DropbearNativeResult<()> { + use crate::component::{KotlinComponentDecl, KOTLIN_COMPONENT_QUEUE}; + KOTLIN_COMPONENT_QUEUE.lock().push(KotlinComponentDecl { + fqcn, + type_name, + category: if category.is_empty() { None } else { Some(category) }, + description: if description.is_empty() { None } else { Some(description) }, + }); + Ok(()) +} @@ -1,4 +1,5 @@ -use crate::component::{Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent}; +use std::fmt::{Display, Formatter}; +use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent}; use crate::hierarchy::EntityTransformExt; use crate::ptr::WorldPtr; use crate::scripting::jni::utils::{FromJObject, ToJObject}; @@ -9,10 +10,388 @@ use ::jni::JNIEnv; use ::jni::objects::{JObject, JValue}; use dropbear_engine::entity::{EntityTransform, Transform}; use dropbear_engine::graphics::SharedGraphicsContext; -use egui::{CollapsingHeader, Ui}; -use glam::{DQuat, DVec3}; +use egui::{CollapsingHeader, ComboBox, Ui}; +use glam::{DQuat, DVec3, Vec3}; use hecs::{Entity, World}; +use splines::{Interpolation, Key, Spline}; use std::sync::Arc; +use crate::physics::PhysicsState; + +/// Allows for the entity to have constricted movement, such as a Camera that follows a player +/// on a set path. +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct OnRails { + /// Whether the component actively moves the entity along the path each frame. + pub enabled: bool, + pub path: Vec<DVec3>, + /// Progress of the entity on the spline between 0.0 to 1.0. + pub progress: f32, // between 0.0 to 1.0 + pub drive: RailDrive, + /// Allows for showing the debug path of the rail. + #[serde(skip)] + pub debug: bool, +} + +/// Returns the progress `t` (0.0–1.0) of the path point closest to `pos`. +fn project_to_path(path: &[DVec3], pos: DVec3) -> f32 { + let n = path.len(); + if n < 2 { + return 0.0; + } + let mut best_t = 0.0f32; + let mut best_dist_sq = f64::MAX; + for i in 0..(n - 1) { + let a = path[i]; + let b = path[i + 1]; + let t_a = i as f32 / (n - 1) as f32; + let t_b = (i + 1) as f32 / (n - 1) as f32; + let ab = b - a; + let ab_len_sq = ab.length_squared(); + let (seg_t, proj_pos) = if ab_len_sq < 1e-10 { + (t_a, a) + } else { + let frac = ((pos - a).dot(ab) / ab_len_sq).clamp(0.0, 1.0); + (t_a + frac as f32 * (t_b - t_a), a + ab * frac) + }; + let dist_sq = pos.distance_squared(proj_pos); + if dist_sq < best_dist_sq { + best_dist_sq = dist_sq; + best_t = seg_t; + } + } + best_t +} + +/// Linearly samples the path at `t` in [0, 1]. Matches the `Interpolation::Linear` splines used +/// in `update_component`, so the editor gizmo stays consistent with runtime behaviour. +fn sample_path_linear(path: &[DVec3], t: f32) -> DVec3 { + let n = path.len(); + if n == 0 { + return DVec3::ZERO; + } + if n == 1 { + return path[0]; + } + let t = t.clamp(0.0, 1.0) as f64; + let seg_f = t * (n - 1) as f64; + let i = (seg_f as usize).min(n - 2); + let frac = seg_f - i as f64; + path[i].lerp(path[i + 1], frac) +} + +/// How the progression of the [`OnRails`] component is handled. +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum RailDrive { + /// Progress advances automatically at a fixed speed. + /// + /// Use for cutscenes, intros, or automated camera pans. + Automatic { + speed: f32, + looping: bool, + }, + + /// Progress is tied to the closest point on the rail to a target entity. + /// + /// Use for third-person or side-scroller cameras following a player. + FollowEntity { + target: Entity, + /// If true, progress can never decrease and the camera won't scroll backwards. + monotonic: bool + }, + + /// Progress is driven by a specific axis of the target entity's world position. + /// + /// e.g. AxisDriven { target: player, axis: Vec3::X } scrolls as the player moves right. + /// + /// Use for side-scrollers or top-down cameras with a dominant movement axis. + AxisDriven { + target: Entity, + axis: Vec3, + /// Maps world units along the axis to 0.0..1.0 progress. + range: (f32, f32), + }, + + /// Progress is set entirely by external code, such as scripting + /// + /// The system won't touch progress at all. + #[default] + Manual, +} + +impl Display for RailDrive { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + RailDrive::Automatic { .. } => "Automatic", + RailDrive::FollowEntity { .. } => "FollowEntity", + RailDrive::AxisDriven { .. } => "AxisDriven", + RailDrive::Manual => "Manual", + }) + } +} + +#[typetag::serde] +impl SerializedComponent for OnRails {} + +impl Component for OnRails { + type SerializedForm = Self; + type RequiredComponentTypes = (Self, EntityTransform); + + fn descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "eucalyptus_core::transform::OnRails".to_string(), + type_name: "OnRails".to_string(), + category: Some("Transform".to_string()), + description: Some("Moves the entity along a fixed path".to_string()), + disabled_flags: DisabilityFlags::Disabled, + internal: false, + } + } + + fn init(ser: &'_ Self::SerializedForm, _graphics: Arc<SharedGraphicsContext>) -> ComponentInitFuture<'_, Self> { + Box::pin(async move { Ok((ser.clone(), EntityTransform::default())) }) + } + + fn update_component(&mut self, world: &World, _physics: &mut PhysicsState, entity: Entity, dt: f32, _graphics: Arc<SharedGraphicsContext>) { + if !self.enabled { + return; + } + let n = self.path.len(); + if n < 2 { + return; + } + + let make_spline = |get: fn(&DVec3) -> f64| -> Spline<f64, f64> { + Spline::from_vec( + self.path + .iter() + .enumerate() + .map(|(i, p)| Key::new(i as f64 / (n - 1) as f64, get(p), Interpolation::Linear)) + .collect(), + ) + }; + let sx = make_spline(|p| p.x); + let sy = make_spline(|p| p.y); + let sz = make_spline(|p| p.z); + + match &self.drive { + RailDrive::Automatic { speed, looping } => { + let (speed, looping) = (*speed, *looping); + self.progress += speed * dt; + if looping { + self.progress = self.progress.rem_euclid(1.0); + } else { + self.progress = self.progress.clamp(0.0, 1.0); + } + } + RailDrive::FollowEntity { target, monotonic } => { + let (target, monotonic) = (*target, *monotonic); + let Ok(target_et) = world.get::<&EntityTransform>(target) else { + return; + }; + let target_pos = target_et.world().position; + drop(target_et); + + let mut best_t = self.progress; + let mut best_dist_sq = f64::MAX; + for i in 0..(n - 1) { + let a = self.path[i]; + let b = self.path[i + 1]; + let t_a = i as f32 / (n - 1) as f32; + let t_b = (i + 1) as f32 / (n - 1) as f32; + let ab = b - a; + let ab_len_sq = ab.length_squared(); + let (seg_t, proj_pos) = if ab_len_sq < 1e-10 { + (t_a, a) + } else { + let frac = ((target_pos - a).dot(ab) / ab_len_sq).clamp(0.0, 1.0); + (t_a + frac as f32 * (t_b - t_a), a + ab * frac) + }; + let dist_sq = target_pos.distance_squared(proj_pos); + if dist_sq < best_dist_sq { + best_dist_sq = dist_sq; + best_t = seg_t; + } + } + self.progress = if monotonic { + self.progress.max(best_t) + } else { + best_t + }; + } + RailDrive::AxisDriven { target, axis, range } => { + let (target, axis, range) = (*target, *axis, *range); + let Ok(target_et) = world.get::<&EntityTransform>(target) else { + return; + }; + let target_pos = target_et.world().position.as_vec3(); + drop(target_et); + + let proj = target_pos.dot(axis); + self.progress = ((proj - range.0) / (range.1 - range.0)).clamp(0.0, 1.0); + } + RailDrive::Manual => {} + } + + let t = self.progress as f64; + if let (Some(x), Some(y), Some(z)) = ( + sx.clamped_sample(t), + sy.clamped_sample(t), + sz.clamped_sample(t), + ) { + if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { + et.world_mut().position = DVec3::new(x, y, z); + } + } + } + + fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> { + Box::new(self.clone()) + } +} + +impl InspectableComponent for OnRails { + fn inspect( + &mut self, + world: &World, + entity: Entity, + ui: &mut Ui, + _graphics: Arc<SharedGraphicsContext>, + ) { + let n = self.path.len(); + + // if the entity is dragged off the path, it will + if self.enabled && n >= 2 { + let expected = sample_path_linear(&self.path, self.progress); + let current_pos = world + .get::<&EntityTransform>(entity) + .ok() + .map(|et| et.world().position); + if let Some(pos) = current_pos { + if pos.distance_squared(expected) > 1e-8 { + self.progress = project_to_path(&self.path, pos); + let snapped = sample_path_linear(&self.path, self.progress); + if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { + et.world_mut().position = snapped; + } + } + } + } + + CollapsingHeader::new("OnRails") + .default_open(true) + .id_salt(format!("OnRails {}", entity.to_bits())) + .show(ui, |ui| { + ui.checkbox(&mut self.enabled, "Enabled"); + ui.add_space(4.0); + + let slider = ui.add( + egui::Slider::new(&mut self.progress, 0.0..=1.0) + .text("Progress") + .min_decimals(2) + .max_decimals(4), + ); + if slider.changed() && n >= 2 { + let pos = sample_path_linear(&self.path, self.progress); + if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { + et.world_mut().position = pos; + } + } + + ui.add_space(4.0); + + let mut drive_tag: i32 = match &self.drive { + RailDrive::Automatic { .. } => 0, + RailDrive::FollowEntity { .. } => 1, + RailDrive::AxisDriven { .. } => 2, + RailDrive::Manual => 3, + }; + let prev_tag = drive_tag; + ComboBox::new(format!("OnRails ComboBox {}", entity.to_bits()), "Drive") + .selected_text(format!("{}", self.drive)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut drive_tag, 0, "Automatic"); + ui.selectable_value(&mut drive_tag, 1, "FollowEntity"); + ui.selectable_value(&mut drive_tag, 2, "AxisDriven"); + ui.selectable_value(&mut drive_tag, 3, "Manual"); + }); + if drive_tag != prev_tag { + self.drive = match drive_tag { + 0 => RailDrive::Automatic { speed: 0.1, looping: false }, + 1 => RailDrive::FollowEntity { target: entity, monotonic: false }, + 2 => RailDrive::AxisDriven { + target: entity, + axis: Vec3::X, + range: (0.0, 100.0), + }, + _ => RailDrive::Manual, + }; + } + + ui.add_space(4.0); + + match &mut self.drive { + RailDrive::Automatic { speed, looping } => { + ui.horizontal(|ui| { + ui.label("Speed:"); + ui.add(egui::DragValue::new(speed).speed(0.001).range(0.0_f32..=f32::MAX)); + }); + ui.checkbox(looping, "Looping"); + } + RailDrive::FollowEntity { target, monotonic } => { + ui.label(format!("Target entity: {}", target.to_bits())); + ui.checkbox(monotonic, "Monotonic"); + } + RailDrive::AxisDriven { target, axis, range } => { + ui.label(format!("Target entity: {}", target.to_bits())); + ui.horizontal(|ui| { + ui.label("Axis:"); + ui.add(egui::DragValue::new(&mut axis.x).speed(0.01).prefix("X:")); + ui.add(egui::DragValue::new(&mut axis.y).speed(0.01).prefix("Y:")); + ui.add(egui::DragValue::new(&mut axis.z).speed(0.01).prefix("Z:")); + }); + ui.horizontal(|ui| { + ui.label("Range:"); + ui.add(egui::DragValue::new(&mut range.0).speed(0.1).prefix("Min:")); + ui.add(egui::DragValue::new(&mut range.1).speed(0.1).prefix("Max:")); + }); + } + RailDrive::Manual => { + ui.label("Progress is set externally."); + } + } + + ui.add_space(4.0); + + CollapsingHeader::new(format!("Path ({} points)", n)) + .default_open(false) + .id_salt(format!("OnRails Path {}", entity.to_bits())) + .show(ui, |ui| { + let mut remove_idx: Option<usize> = None; + for (i, point) in self.path.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.label(format!("[{}]", i)); + ui.add(egui::DragValue::new(&mut point.x).speed(0.1).prefix("X:")); + ui.add(egui::DragValue::new(&mut point.y).speed(0.1).prefix("Y:")); + ui.add(egui::DragValue::new(&mut point.z).speed(0.1).prefix("Z:")); + if ui.small_button("-").clicked() { + remove_idx = Some(i); + } + }); + } + if let Some(idx) = remove_idx { + self.path.remove(idx); + } + if ui.button("+ Add Point").clicked() { + let last = self.path.last().copied().unwrap_or(DVec3::ZERO); + self.path.push(last + DVec3::new(1.0, 0.0, 0.0)); + } + }); + + ui.add_space(4.0); + ui.checkbox(&mut self.debug, "Debug path"); + }); + } +} #[typetag::serde] impl SerializedComponent for EntityTransform {} @@ -27,15 +406,15 @@ impl Component for EntityTransform { internal: false, fqtn: "dropbear_engine::entity::EntityTransform".to_string(), type_name: "EntityTransform".to_string(), - category: None, + category: Some("Transform".to_string()), description: Some("Allows the entity to have a space within the world".to_string()), } } - fn init<'a>( - ser: &'a Self::SerializedForm, + fn init( + ser: &Self::SerializedForm, _: Arc<SharedGraphicsContext>, - ) -> crate::component::ComponentInitFuture<'a, Self> { + ) -> crate::component::ComponentInitFuture<Self> { Box::pin(async move { Ok((ser.clone(),)) }) } @@ -314,7 +693,7 @@ fn set_world_transform( ), c )] -fn propogate_transform( +fn propagate_transform( #[dropbear_macro::define(WorldPtr)] world: &hecs::World, #[dropbear_macro::entity] entity: hecs::Entity, ) -> DropbearNativeResult<NTransform> { @@ -325,3 +704,300 @@ fn propogate_transform( Err(DropbearNativeError::MissingComponent) } } + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getEnabled"), + c +)] +fn on_rails_get_enabled( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<bool> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + Ok(rails.enabled) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setEnabled"), + c +)] +fn on_rails_set_enabled( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + enabled: bool, +) -> DropbearNativeResult<()> { + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.enabled = enabled; + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "existsForEntity"), + c +)] +fn on_rails_exists_for_entity( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<bool> { + Ok(world.get::<&OnRails>(entity).is_ok()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getProgress"), + c +)] +fn on_rails_get_progress( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<f32> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + Ok(rails.progress) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setProgress"), + c +)] +fn on_rails_set_progress( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + progress: f32, +) -> DropbearNativeResult<()> { + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.progress = progress.clamp(0.0, 1.0); + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getPathLen"), + c +)] +fn on_rails_get_path_len( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<i32> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + Ok(rails.path.len() as i32) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getPathPoint"), + c +)] +fn on_rails_get_path_point( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + index: i32, +) -> DropbearNativeResult<NVector3> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + let point = rails.path.get(index as usize).ok_or(DropbearNativeError::InvalidArgument)?; + Ok(NVector3::from(point)) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "clearPath"), + c +)] +fn on_rails_clear_path( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<()> { + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.path.clear(); + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "pushPathPoint"), + c +)] +fn on_rails_push_path_point( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + point: &NVector3, +) -> DropbearNativeResult<()> { + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.path.push(DVec3::from(point)); + Ok(()) +} + +// `0` = Automatic, `1` = FollowEntity, `2` = AxisDriven, `3` = Manual. +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveType"), + c +)] +fn on_rails_get_drive_type( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<i32> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + let tag = match &rails.drive { + RailDrive::Automatic { .. } => 0, + RailDrive::FollowEntity { .. } => 1, + RailDrive::AxisDriven { .. } => 2, + RailDrive::Manual => 3, + }; + Ok(tag) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setDriveAutomatic"), + c +)] +fn on_rails_set_drive_automatic( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + speed: f32, + looping: bool, +) -> DropbearNativeResult<()> { + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.drive = RailDrive::Automatic { speed, looping }; + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setDriveFollowEntity"), + c +)] +fn on_rails_set_drive_follow_entity( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + target: u64, + monotonic: bool, +) -> DropbearNativeResult<()> { + let target_entity = hecs::Entity::from_bits(target).ok_or(DropbearNativeError::InvalidEntity)?; + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.drive = RailDrive::FollowEntity { target: target_entity, monotonic }; + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setDriveAxisDriven"), + c +)] +fn on_rails_set_drive_axis_driven( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + target: u64, + axis: &NVector3, + range_min: f32, + range_max: f32, +) -> DropbearNativeResult<()> { + let target_entity = hecs::Entity::from_bits(target).ok_or(DropbearNativeError::InvalidEntity)?; + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.drive = RailDrive::AxisDriven { + target: target_entity, + axis: Vec3::new(axis.x as f32, axis.y as f32, axis.z as f32), + range: (range_min, range_max), + }; + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "setDriveManual"), + c +)] +fn on_rails_set_drive_manual( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<()> { + let mut rails = world.get::<&mut OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + rails.drive = RailDrive::Manual; + Ok(()) +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveAutomaticSpeed"), + c +)] +fn on_rails_get_drive_automatic_speed( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<f32> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::Automatic { speed, .. } = &rails.drive { Ok(*speed) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveAutomaticLooping"), + c +)] +fn on_rails_get_drive_automatic_looping( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<bool> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::Automatic { looping, .. } = &rails.drive { Ok(*looping) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveFollowEntityTarget"), + c +)] +fn on_rails_get_drive_follow_entity_target( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<u64> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::FollowEntity { target, .. } = &rails.drive { Ok(target.to_bits().get()) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveFollowEntityMonotonic"), + c +)] +fn on_rails_get_drive_follow_entity_monotonic( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<bool> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::FollowEntity { monotonic, .. } = &rails.drive { Ok(*monotonic) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveAxisDrivenTarget"), + c +)] +fn on_rails_get_drive_axis_driven_target( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<u64> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::AxisDriven { target, .. } = &rails.drive { Ok(target.to_bits().get()) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveAxisDrivenAxis"), + c +)] +fn on_rails_get_drive_axis_driven_axis( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<NVector3> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::AxisDriven { axis, .. } = &rails.drive { Ok(NVector3::from(*axis)) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveAxisDrivenRangeMin"), + c +)] +fn on_rails_get_drive_axis_driven_range_min( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<f32> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::AxisDriven { range, .. } = &rails.drive { Ok(range.0) } else { Err(DropbearNativeError::InvalidArgument) } +} + +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.camera.OnRailsNative", func = "getDriveAxisDrivenRangeMax"), + c +)] +fn on_rails_get_drive_axis_driven_range_max( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, +) -> DropbearNativeResult<f32> { + let rails = world.get::<&OnRails>(entity).map_err(|_| DropbearNativeError::MissingComponent)?; + if let RailDrive::AxisDriven { range, .. } = &rails.drive { Ok(range.1) } else { Err(DropbearNativeError::InvalidArgument) } +} @@ -766,6 +766,12 @@ macro_rules! ffi_error_return { } } + impl ErrorValue for f32 { + fn error_value() -> Self { + f32::NAN + } + } + impl<T> ErrorValue for $crate::scripting::result::DropbearNativeResult<T> { fn error_value() -> Self { Err($crate::scripting::native::DropbearNativeError::NullPointer) @@ -4,6 +4,7 @@ use dropbear_engine::entity::{EntityTransform, MeshRenderer}; use dropbear_engine::future::{FutureHandle, FutureQueue}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::model::Model; +use eucalyptus_core::component::ComponentApply; use eucalyptus_core::hierarchy::{Hierarchy, Parent}; use eucalyptus_core::scene::SceneEntity; use eucalyptus_core::states::Label; @@ -58,9 +59,7 @@ impl PendingSpawnController for Editor { let graphics = graphics.clone(); let future = async move { - let mut appliers: Vec< - Box<dyn for<'a> FnOnce(&'a mut EntityBuilder) + Send + Sync>, - > = Vec::new(); + let mut appliers: Vec<Box<dyn ComponentApply + Send + Sync>> = Vec::new(); for component in components { if component.as_any().downcast_ref::<Parent>().is_some() { continue; @@ -80,7 +79,7 @@ impl PendingSpawnController for Editor { Ok::< ( Label, - Vec<Box<dyn for<'a> FnOnce(&'a mut EntityBuilder) + Send + Sync>>, + Vec<Box<dyn ComponentApply + Send + Sync>>, ), anyhow::Error, >((label, appliers)) @@ -94,14 +93,14 @@ impl PendingSpawnController for Editor { if let Some(result) = queue.exchange_owned(handle) { if let Ok(r) = result.downcast::<anyhow::Result<( Label, - Vec<Box<dyn for<'a> FnOnce(&'a mut EntityBuilder) + Send + Sync>>, + Vec<Box<dyn ComponentApply + Send + Sync>>, )>>() { match Arc::try_unwrap(r) { Ok(Ok((label, appliers))) => { let mut builder = EntityBuilder::new(); builder.add(label.clone()); for applier in appliers { - applier(&mut builder); + applier.apply_to_builder(&mut builder); } let entity = self.world.spawn(builder.build()); @@ -110,7 +109,7 @@ impl PendingSpawnController for Editor { self.world.insert_one(entity, EntityTransform::default()); } - // Attach to parent if requested. + // attach to parent if let Some(ref parent_label) = spawn.parent_label { let parent_entity = self .world @@ -162,12 +161,10 @@ impl PendingSpawnController for Editor { let mut completed_components = Vec::new(); for (index, (entity, handle)) in self.pending_components.iter().enumerate() { if let Some(result) = queue.exchange_owned(handle) { - if let Ok(r) = result.downcast::<anyhow::Result<Box<dyn for<'a> FnOnce(&'a mut EntityBuilder) + Send + Sync>>>() { + if let Ok(r) = result.downcast::<anyhow::Result<Box<dyn ComponentApply + Send + Sync>>>() { match Arc::try_unwrap(r) { Ok(Ok(applier)) => { - let mut builder = EntityBuilder::new(); - applier(&mut builder); - if let Err(e) = self.world.insert(*entity, builder.build()) { + if let Err(e) = applier.apply_to_existing_entity(&mut self.world, *entity) { fatal!("Failed to add component bundle: {}", e); } else { success!("Added component to entity {:?}", entity); @@ -79,3 +79,43 @@ impl Border { Self { colour, width } } } + +/// States the different types of widgets. +/// +/// Also serves as a little task checklist for this library. +/// +/// Also it was ripped from the egui library. +pub enum WidgetType { + Label, + + TextEdit, + + Button, + + Checkbox, + + RadioButton, + + SelectableLabel, + + ComboBox, + + Slider, + + DragValue, + + ColourButton, + + Image, + + CollapsingHeader, + + Panel, + + ProgressIndicator, + + /// If you cannot fit any of the above slots. + /// + /// If this is something you think should be added, file an issue. + Other, +} @@ -110,6 +110,21 @@ impl Generator for KotlinJVMGenerator { writeln!(output, "}}")?; + writeln!(output)?; + writeln!(output, "object ComponentManager {{")?; + writeln!(output, " @Synchronized")?; + writeln!(output, " fun registerAll() {{")?; + for component in manifest.components() { + writeln!( + output, + " com.dropbear.ecs.registerKotlinComponentType({fqcn}, {simple}, null, null)", + fqcn = format!("\"{}\"", component.fqcn()), + simple = format!("\"{}\"", component.simple_name()), + )?; + } + writeln!(output, " }}")?; + writeln!(output, "}}")?; + Ok(output) } } @@ -554,6 +554,20 @@ object ScriptManager {{ writeln!(output, "}}")?; + writeln!(output)?; + writeln!(output, "object ComponentManager {{")?; + writeln!(output, " fun registerAll() {{")?; + for component in manifest.components() { + writeln!( + output, + " com.dropbear.ecs.registerKotlinComponentType({fqcn}, {simple}, null, null)", + fqcn = format!("\"{}\"", component.fqcn()), + simple = format!("\"{}\"", component.simple_name()), + )?; + } + writeln!(output, " }}")?; + writeln!(output, "}}")?; + // ADD CNAME FUNCTIONS HERE writeln!( output, @@ -567,6 +581,7 @@ fun CPointer<ULongVar>.toLongArray(length: Int): LongArray {{ @CName("dropbear_init") fun dropbear_native_init(dropbearContextPtr: CPointer<DropbearContext>?): Int {{ + ComponentManager.registerAll() return ScriptManager.init(dropbearContextPtr) }} @@ -18,6 +18,7 @@ struct AnnotationInfo<'a> { #[derive(Debug, Clone)] pub struct ScriptManifest { items: Vec<ManifestItem>, + components: Vec<ComponentManifestItem>, } impl Default for ScriptManifest { @@ -28,7 +29,7 @@ impl Default for ScriptManifest { impl ScriptManifest { pub fn new() -> Self { - Self { items: Vec::new() } + Self { items: Vec::new(), components: Vec::new() } } pub fn add_item(&mut self, item: ManifestItem) { @@ -38,6 +39,14 @@ impl ScriptManifest { pub fn items(&self) -> &[ManifestItem] { &self.items } + + pub fn add_component(&mut self, item: ComponentManifestItem) { + self.components.push(item); + } + + pub fn components(&self) -> &[ComponentManifestItem] { + &self.components + } } /// Represents a single script class. This struct contains all the necessary information to generate @@ -126,6 +135,76 @@ impl KotlinProcessor { Ok(Self { parser }) } + /// Processes the file for `@EcsComponent` annotations, returning a + /// [`ComponentManifestItem`] for every annotated class found. + pub fn process_file_for_components( + &mut self, + source_code: &str, + file_path: PathBuf, + ) -> anyhow::Result<Vec<ComponentManifestItem>> { + let tree = self + .parser + .parse(source_code, None) + .ok_or_else(|| anyhow::anyhow!("Failed to parse source code"))?; + + let root_node = tree.root_node(); + let package = self.extract_package(root_node, source_code)?; + + let mut items = Vec::new(); + let mut stack = vec![root_node]; + + while let Some(node) = stack.pop() { + if node.kind() == "class_declaration" { + if let Some(class_name) = self.class_name_from_node(node, source_code)? { + if self.node_has_annotation(node, source_code, "EcsComponent")? { + let fqcn = if package.is_empty() { + class_name.clone() + } else { + format!("{}.{}", package, class_name) + }; + items.push(ComponentManifestItem::new( + fqcn, + class_name, + file_path.clone(), + )); + } + } + } + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + stack.push(child); + } + } + + Ok(items) + } + + /// Returns `true` if the given class declaration node has an annotation with the given name. + fn node_has_annotation( + &self, + class_node: tree_sitter::Node, + source: &str, + annotation_name: &str, + ) -> anyhow::Result<bool> { + let Some(modifiers) = self.first_child_by_kind(class_node, "modifiers") else { + return Ok(false); + }; + + let mut mod_cursor = modifiers.walk(); + for modifier in modifiers.children(&mut mod_cursor) { + if modifier.kind() != "annotation" { + continue; + } + let (name, _) = self.annotation_name_and_args(modifier, source)?; + if name.as_deref() == Some(annotation_name) { + return Ok(true); + } + } + + Ok(false) + } + /// Processes the file for `@Runnable` annotations, and check if that /// class inherits the `System()` abstract class. pub fn process_file( @@ -741,6 +820,10 @@ pub fn visit_kotlin_files( if let Some(item) = processor.process_file(&source_code, path.clone())? { manifest.add_item(item); } + + for component in processor.process_file_for_components(&source_code, path.clone())? { + manifest.add_component(component); + } } } } @@ -16,9 +16,6 @@ use eucalyptus_core::command::COMMAND_BUFFER; use eucalyptus_core::component::ComponentRegistry; use eucalyptus_core::input::InputState; use eucalyptus_core::physics::PhysicsState; -use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; -use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use eucalyptus_core::ptr::{ CommandBufferPtr, GraphicsContextPtr, InputStatePtr, PhysicsStatePtr, UiBufferPtr, WorldPtr, }; @@ -139,7 +136,6 @@ pub struct PlayMode { shader_globals: Option<GlobalsUniform>, instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>, - collider_wireframe_pipeline: Option<ColliderWireframePipeline>, sky_pipeline: Option<SkyPipeline>, camera_bind_group: Option<wgpu::BindGroup>, default_skinning_buffer: Option<wgpu::Buffer>, @@ -173,8 +169,6 @@ pub struct PlayMode { collision_force_event_receiver: Option<std::sync::mpsc::Receiver<ContactForceEvent>>, event_collector: ChannelEventCollector, - collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, - collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, viewport_offset: (f32, f32), // ui @@ -221,9 +215,6 @@ impl PlayMode { physics_state: Box::new(PhysicsState::new()), pending_physics_state: Default::default(), physics_receiver: Default::default(), - collider_wireframe_pipeline: None, - collider_wireframe_geometry_cache: HashMap::new(), - collider_instance_buffer: None, viewport_offset: (0.0, 0.0), collision_event_receiver: Some(ce_r), collision_force_event_receiver: Some(cfe_r), @@ -264,7 +255,6 @@ impl PlayMode { self.light_cube_pipeline = None; self.main_pipeline = None; self.shader_globals = None; - self.collider_wireframe_pipeline = None; self.kino = None; self.sky_pipeline = None; self.camera_bind_group = None; @@ -288,7 +278,6 @@ impl PlayMode { graphics.clone(), Some("runtime shader globals"), )); - self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); let mut camera_bind_group = None; let mut pending_sky_pipeline = None; @@ -17,10 +17,7 @@ use eucalyptus_core::entity_status::EntityStatus; use eucalyptus_core::command::CommandBufferPoller; use eucalyptus_core::egui::CentralPanel; use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; -use eucalyptus_core::physics::collider::ColliderGroup; -use eucalyptus_core::physics::collider::ColliderShapeKey; -use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; +use eucalyptus_core::physics::collider::{ColliderGroup, ColliderShape}; use eucalyptus_core::physics::kcc::KCC; use eucalyptus_core::rapier3d::geometry::SharedShape; use eucalyptus_core::rapier3d::prelude::QueryFilter; @@ -28,7 +25,7 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResu use eucalyptus_core::states::SCENES; use eucalyptus_core::states::{Label, PROJECT}; use eucalyptus_core::ui::HUDComponent; -use glam::{DVec3, Mat4, Quat, Vec2}; +use glam::{DVec3, Mat4, Quat, Vec2, Vec3}; use hecs::Entity; use kino_ui::WidgetTree; use kino_ui::rendering::KinoRenderTargetId; @@ -1204,7 +1201,7 @@ impl Scene for PlayMode { render_pass.draw(0..3, 0..1); } - // collider pipeline + // collider debug draw { let show_hitboxes = self .current_scene @@ -1219,117 +1216,71 @@ impl Scene for PlayMode { .unwrap_or(false); if show_hitboxes { - puffin::profile_scope!("collider wireframe pipeline"); - if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { - log_once::debug_once!("Found collider wireframe pipeline"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("collider wireframe render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - }); - - render_pass.set_pipeline(&collider_pipeline.pipeline); - render_pass.set_bind_group(0, camera_bind_group, &[]); - - let mut instances_by_shape: HashMap< - ColliderShapeKey, - Vec<ColliderInstanceRaw>, - > = HashMap::new(); - + puffin::profile_scope!("collider debug draw"); + if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { + let colour = [0.0, 1.0, 0.0, 1.0]; let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); for (entity_transform, group) in q.iter() { for collider in &group.colliders { let world_tf = entity_transform.sync(); - let entity_matrix = world_tf.matrix().as_mat4(); - let offset_transform = Transform::new() .with_offset(collider.translation, collider.rotation); let offset_matrix = offset_transform.matrix().as_mat4(); - let final_matrix = entity_matrix * offset_matrix; - - let color = [0.0, 1.0, 0.0, 1.0]; - let instance = ColliderInstanceRaw::from_matrix(final_matrix, color); - - let key = ColliderShapeKey::from(&collider.shape); - instances_by_shape.entry(key).or_default().push(instance); - - self.collider_wireframe_geometry_cache - .entry(key) - .or_insert_with(|| { - create_wireframe_geometry(graphics.clone(), &collider.shape) - }); - } - } - - if !instances_by_shape.is_empty() { - let total_instances: usize = - instances_by_shape.values().map(|v| v.len()).sum(); - let mut all_instances = Vec::with_capacity(total_instances); - let mut draws: Vec<(ColliderShapeKey, usize, usize)> = Vec::new(); - - for (key, instances) in instances_by_shape { - let start = all_instances.len(); - all_instances.extend_from_slice(&instances); - let count = instances.len(); - draws.push((key, start, count)); - } - - let instance_buffer = - self.collider_instance_buffer.get_or_insert_with(|| { - ResizableBuffer::new( - &graphics.device, - all_instances.len().max(10), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Collider Instance Buffer", - ) - }); - instance_buffer.write(&graphics.device, &graphics.queue, &all_instances); - - for (key, start, count) in draws { - let Some(geometry) = self.collider_wireframe_geometry_cache.get(&key) - else { - continue; - }; - - let start_bytes = (start * std::mem::size_of::<ColliderInstanceRaw>()) - as wgpu::BufferAddress; - let end_bytes = ((start + count) - * std::mem::size_of::<ColliderInstanceRaw>()) - as wgpu::BufferAddress; - - render_pass.set_vertex_buffer( - 1, - instance_buffer.buffer().slice(start_bytes..end_bytes), - ); - render_pass.set_vertex_buffer(0, geometry.vertex_buffer.slice(..)); - render_pass.set_index_buffer( - geometry.index_buffer.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.draw_indexed(0..geometry.index_count, 0, 0..count as u32); + let (scale, rotation, translation) = + final_matrix.to_scale_rotation_translation(); + + match &collider.shape { + ColliderShape::Box { half_extents } => { + let he = Vec3::new( + half_extents.x as f32 * scale.x, + half_extents.y as f32 * scale.y, + half_extents.z as f32 * scale.z, + ); + debug_draw.draw_obb(translation, he, rotation, colour); + } + ColliderShape::Sphere { radius } => { + let r = radius * scale.x.max(scale.y).max(scale.z); + debug_draw.draw_sphere(translation, r, colour); + } + ColliderShape::Capsule { half_height, radius } => { + let axis = rotation * Vec3::Y; + let top = translation + axis * (half_height * scale.y); + let bottom = translation - axis * (half_height * scale.y); + debug_draw.draw_capsule( + bottom, + top, + radius * scale.x.max(scale.z), + colour, + ); + } + ColliderShape::Cylinder { half_height, radius } => { + let axis = rotation * Vec3::Y; + debug_draw.draw_cylinder( + translation, + half_height * scale.y, + radius * scale.x.max(scale.z), + axis, + colour, + ); + } + ColliderShape::Cone { half_height, radius } => { + let axis = rotation * Vec3::Y; + let apex = translation + axis * (half_height * scale.y); + let height = 2.0 * half_height * scale.y; + let r = radius * scale.x.max(scale.z); + debug_draw.draw_cone( + apex, + -(rotation * Vec3::Y), + (r / height).atan(), + height, + colour, + ); + } + } } } - } else { - log_once::warn_once!("No collider pipeline found"); } } } @@ -520,17 +520,18 @@ int32_t dropbear_collider_set_collider_restitution(PhysicsStatePtr physics, cons int32_t dropbear_collider_set_collider_rotation(PhysicsStatePtr physics, const NCollider* collider, const NVector3* rotation); int32_t dropbear_collider_set_collider_shape(PhysicsStatePtr physics, const NCollider* collider, const ColliderShape* shape); int32_t dropbear_collider_set_collider_translation(PhysicsStatePtr physics, const NCollider* collider, const NVector3* translation); -int32_t dropbear_debug_debug_draw_aabb(GraphicsContextPtr graphics, const NVector3* min, const NVector3* max, const NColour* colour); -int32_t dropbear_debug_debug_draw_arrow(GraphicsContextPtr graphics, const NVector3* start, const NVector3* end, const NColour* colour); -int32_t dropbear_debug_debug_draw_capsule(GraphicsContextPtr graphics, const NVector3* a, const NVector3* b, float radius, const NColour* colour); -int32_t dropbear_debug_debug_draw_circle(GraphicsContextPtr graphics, const NVector3* center, float radius, const NVector3* normal, const NColour* colour); -int32_t dropbear_debug_debug_draw_cone(GraphicsContextPtr graphics, const NVector3* apex, const NVector3* dir, float angle, float length, const NColour* colour); -int32_t dropbear_debug_debug_draw_globe(GraphicsContextPtr graphics, const NVector3* center, float radius, uint32_t lat_lines, uint32_t lon_lines, const NColour* colour); -int32_t dropbear_debug_debug_draw_line(GraphicsContextPtr graphics, const NVector3* start, const NVector3* end, const NColour* colour); -int32_t dropbear_debug_debug_draw_obb(GraphicsContextPtr graphics, const NVector3* center, const NVector3* half_extents, const NQuaternion* rotation, const NColour* colour); -int32_t dropbear_debug_debug_draw_point(GraphicsContextPtr graphics, const NVector3* pos, float size, const NColour* colour); -int32_t dropbear_debug_debug_draw_ray(GraphicsContextPtr graphics, const NVector3* origin, const NVector3* dir, const NColour* colour); -int32_t dropbear_debug_debug_draw_sphere(GraphicsContextPtr graphics, const NVector3* center, float radius, const NColour* colour); +int32_t dropbear_debug_draw_aabb(GraphicsContextPtr graphics, const NVector3* min, const NVector3* max, const NColour* colour); +int32_t dropbear_debug_draw_arrow(GraphicsContextPtr graphics, const NVector3* start, const NVector3* end, const NColour* colour); +int32_t dropbear_debug_draw_capsule(GraphicsContextPtr graphics, const NVector3* a, const NVector3* b, float radius, const NColour* colour); +int32_t dropbear_debug_draw_circle(GraphicsContextPtr graphics, const NVector3* center, float radius, const NVector3* normal, const NColour* colour); +int32_t dropbear_debug_draw_cone(GraphicsContextPtr graphics, const NVector3* apex, const NVector3* dir, float angle, float length, const NColour* colour); +int32_t dropbear_debug_draw_cylinder(GraphicsContextPtr graphics, const NVector3* center, float half_height, float radius, const NVector3* axis, const NColour* colour); +int32_t dropbear_debug_draw_globe(GraphicsContextPtr graphics, const NVector3* center, float radius, uint32_t lat_lines, uint32_t lon_lines, const NColour* colour); +int32_t dropbear_debug_draw_line(GraphicsContextPtr graphics, const NVector3* start, const NVector3* end, const NColour* colour); +int32_t dropbear_debug_draw_obb(GraphicsContextPtr graphics, const NVector3* center, const NVector3* half_extents, const NQuaternion* rotation, const NColour* colour); +int32_t dropbear_debug_draw_point(GraphicsContextPtr graphics, const NVector3* pos, float size, const NColour* colour); +int32_t dropbear_debug_draw_ray(GraphicsContextPtr graphics, const NVector3* origin, const NVector3* dir, const NColour* colour); +int32_t dropbear_debug_draw_sphere(GraphicsContextPtr graphics, const NVector3* center, float radius, const NColour* colour); int32_t dropbear_engine_get_asset(AssetRegistryPtr asset, const char* label, const AssetKind* kind, uint64_t* out0, bool* out0_present); int32_t dropbear_engine_get_entity(WorldPtr world, const char* label, uint64_t* out0); int32_t dropbear_engine_quit(CommandBufferPtr command_buffer); @@ -640,7 +641,29 @@ int32_t dropbear_scripting_switch_to_scene_immediate(CommandBufferPtr command_bu int32_t dropbear_transform_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0); int32_t dropbear_transform_get_local_transform(WorldPtr world, uint64_t entity, NTransform* out0); int32_t dropbear_transform_get_world_transform(WorldPtr world, uint64_t entity, NTransform* out0); -int32_t dropbear_transform_propogate_transform(WorldPtr world, uint64_t entity, NTransform* out0); +int32_t dropbear_transform_on_rails_clear_path(WorldPtr world, uint64_t entity); +int32_t dropbear_transform_on_rails_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0); +int32_t dropbear_transform_on_rails_get_drive_automatic_looping(WorldPtr world, uint64_t entity, bool* out0); +int32_t dropbear_transform_on_rails_get_drive_automatic_speed(WorldPtr world, uint64_t entity, float* out0); +int32_t dropbear_transform_on_rails_get_drive_axis_driven_axis(WorldPtr world, uint64_t entity, NVector3* out0); +int32_t dropbear_transform_on_rails_get_drive_axis_driven_range_max(WorldPtr world, uint64_t entity, float* out0); +int32_t dropbear_transform_on_rails_get_drive_axis_driven_range_min(WorldPtr world, uint64_t entity, float* out0); +int32_t dropbear_transform_on_rails_get_drive_axis_driven_target(WorldPtr world, uint64_t entity, uint64_t* out0); +int32_t dropbear_transform_on_rails_get_drive_follow_entity_monotonic(WorldPtr world, uint64_t entity, bool* out0); +int32_t dropbear_transform_on_rails_get_drive_follow_entity_target(WorldPtr world, uint64_t entity, uint64_t* out0); +int32_t dropbear_transform_on_rails_get_drive_type(WorldPtr world, uint64_t entity, int32_t* out0); +int32_t dropbear_transform_on_rails_get_enabled(WorldPtr world, uint64_t entity, bool* out0); +int32_t dropbear_transform_on_rails_get_path_len(WorldPtr world, uint64_t entity, int32_t* out0); +int32_t dropbear_transform_on_rails_get_path_point(WorldPtr world, uint64_t entity, int32_t index, NVector3* out0); +int32_t dropbear_transform_on_rails_get_progress(WorldPtr world, uint64_t entity, float* out0); +int32_t dropbear_transform_on_rails_push_path_point(WorldPtr world, uint64_t entity, const NVector3* point); +int32_t dropbear_transform_on_rails_set_drive_automatic(WorldPtr world, uint64_t entity, float speed, bool looping); +int32_t dropbear_transform_on_rails_set_drive_axis_driven(WorldPtr world, uint64_t entity, uint64_t target, const NVector3* axis, float range_min, float range_max); +int32_t dropbear_transform_on_rails_set_drive_follow_entity(WorldPtr world, uint64_t entity, uint64_t target, bool monotonic); +int32_t dropbear_transform_on_rails_set_drive_manual(WorldPtr world, uint64_t entity); +int32_t dropbear_transform_on_rails_set_enabled(WorldPtr world, uint64_t entity, bool enabled); +int32_t dropbear_transform_on_rails_set_progress(WorldPtr world, uint64_t entity, float progress); +int32_t dropbear_transform_propagate_transform(WorldPtr world, uint64_t entity, NTransform* out0); int32_t dropbear_transform_set_local_transform(WorldPtr world, uint64_t entity, const NTransform* transform); int32_t dropbear_transform_set_world_transform(WorldPtr world, uint64_t entity, const NTransform* transform); @@ -8,6 +8,8 @@ import com.dropbear.input.InputState import com.dropbear.scene.SceneManager import com.dropbear.ui.UIInstruction import com.dropbear.ui.UIInstructionSet +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid internal var exceptionOnError: Boolean = false var lastErrorMessage: String? = null @@ -45,7 +47,12 @@ class DropbearEngine(val native: NativeEngine) { } fun getAssetEntry(displayName: String): AssetEntry { + TODO("getAssetEntry not yet implemented") + } + @OptIn(ExperimentalUuidApi::class) + fun getAssetEntry(uuid: Uuid): AssetEntry { + TODO("getAssetEntry not yet implemented") } /** @@ -1,12 +1,10 @@ package com.dropbear.animation import com.dropbear.EntityId -import com.dropbear.components.Camera -import com.dropbear.components.cameraExistsForEntity -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent -class AnimationComponent(val parentEntity: EntityId) : Component(parentEntity, "AnimationComponent") { +class AnimationComponent(val parentEntity: EntityId) : ExternalComponent("dropbear_engine::animation::AnimationComponent") { val availableAnimations: List<String> get() = getAvailableAnimations() @@ -1,8 +1,8 @@ package com.dropbear.components import com.dropbear.EntityId -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.math.Vector3d /** @@ -10,9 +10,8 @@ import com.dropbear.math.Vector3d */ class Camera( internal val entity: EntityId, -): Component( +): ExternalComponent( fullyQualifiedTypeName = "dropbear_engine::camera::Camera", - typeName = "Camera3D", ) { /** * The eye/position of the camera. @@ -136,6 +135,8 @@ class Camera( } } +internal expect fun cameraExistsForEntity(entity: EntityId): Boolean + internal expect fun Camera.getCameraEye(entity: EntityId): Vector3d internal expect fun Camera.setCameraEye(entity: EntityId, value: Vector3d) internal expect fun Camera.getCameraTarget(entity: EntityId): Vector3d @@ -156,6 +157,4 @@ internal expect fun Camera.setCameraPitch(entity: EntityId, value: Double) internal expect fun Camera.getCameraSpeed(entity: EntityId): Double internal expect fun Camera.setCameraSpeed(entity: EntityId, value: Double) internal expect fun Camera.getCameraSensitivity(entity: EntityId): Double -internal expect fun Camera.setCameraSensitivity(entity: EntityId, value: Double) - -internal expect fun cameraExistsForEntity(entity: EntityId): Boolean +internal expect fun Camera.setCameraSensitivity(entity: EntityId, value: Double) @@ -1,8 +1,8 @@ package com.dropbear.components import com.dropbear.EntityId -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.math.Vector3d import com.dropbear.math.Vector3f import com.dropbear.math.Vector3i @@ -12,9 +12,8 @@ import com.dropbear.math.Vector3i * * The entity must include the `CustomProperties` component in the editor. */ -class CustomProperties(val id: EntityId): Component( +class CustomProperties(val id: EntityId): ExternalComponent( fullyQualifiedTypeName = "eucalyptus_core::properties::CustomProperties", - typeName = "CustomProperties", ) { /** * Fetches the property of the ModelProperty component on the entity. @@ -1,8 +1,8 @@ package com.dropbear.components import com.dropbear.EntityId -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.math.Transform /** @@ -10,9 +10,8 @@ import com.dropbear.math.Transform * * This entity must contain the `EntityTransform` component to be queryable. */ -class EntityTransform(val id: EntityId): Component( +class EntityTransform(val id: EntityId): ExternalComponent( fullyQualifiedTypeName = "dropbear_engine::entity::EntityTransform", - typeName = "EntityTransform", ) { /** * The local transform. @@ -2,8 +2,8 @@ package com.dropbear.components import com.dropbear.EntityId import com.dropbear.EntityRef -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.getEntityLabel /** @@ -15,9 +15,8 @@ import com.dropbear.getEntityLabel */ class Label( internal val entity: EntityId -): Component( +): ExternalComponent( fullyQualifiedTypeName = "eucalyptus_core::states::Label", - typeName = "Label", ) { val name: String get() = EntityRef.getEntityLabel(entity) @@ -3,19 +3,17 @@ package com.dropbear.components import com.dropbear.EntityId import com.dropbear.asset.ModelHandle import com.dropbear.asset.TextureHandle -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent /** * A component that allows for a 3D model to be rendered. * * It must require the `MeshRenderer` component in the editor to be queryable. */ -class MeshRenderer(val id: EntityId) : Component( +class MeshRenderer(val id: EntityId) : ExternalComponent( fullyQualifiedTypeName = "dropbear_engine::entity::MeshRenderer", - typeName = "MeshRenderer", ) { - /** * The active model currently assigned to this entity. * Setting this to null removes the model. @@ -0,0 +1,137 @@ +package com.dropbear.components.camera + +import com.dropbear.EntityId +import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent +import com.dropbear.math.Vector3d +import com.dropbear.math.Vector3f + +/** + * Controls how the [OnRails] component advances its progress along the path. + */ +sealed class RailDrive { + /** + * Progress advances automatically at a fixed speed. + * + * Use for cutscenes, intros, or automated camera pans. + * + * @param speed Units of progress (0.0–1.0) advanced per second. + * @param looping When `true`, progress wraps back to 0.0 at the end. + */ + data class Automatic( + val speed: Float, + val looping: Boolean, + ) : RailDrive() + + /** + * Progress is tied to the closest point on the rail to a target entity. + * + * Use for third-person or side-scroller cameras following a player. + * + * @param target The entity whose position drives rail progress. + * @param monotonic When `true`, progress can never decrease. The camera won't scroll backward. + */ + data class FollowEntity( + val target: EntityId, + val monotonic: Boolean = false, + ) : RailDrive() + + /** + * Progress is driven by a specific axis of the target entity's world position. + * + * e.g. `AxisDriven(player, Vector3f(1f, 0f, 0f), 0f, 100f)` scrolls as the player moves + * along the X axis from 0 to 100 world units. + * + * Use for side-scrollers or top-down cameras with a dominant movement axis. + * + * @param target The entity whose position is projected onto [axis]. + * @param axis World-space direction vector to project onto. + * @param rangeMin World-space axis value that maps to progress 0.0. + * @param rangeMax World-space axis value that maps to progress 1.0. + */ + data class AxisDriven( + val target: EntityId, + val axis: Vector3f, + val rangeMin: Float, + val rangeMax: Float, + ) : RailDrive() + + /** + * Progress is set entirely by external code (scripting, cutscene sequencer, etc.). + * + * The system will not touch [OnRails.progress] at all. + */ + data object Manual : RailDrive() +} + +/** + * Constrains an entity's movement to a fixed spline path. + * + * The entity must also have an `EntityTransform` component. [progress] is a value in `[0.0, 1.0]` + * that indicates how far along the path the entity sits; the native system updates `world.position` + * every frame based on the active [drive] mode. + * + * @param entity The entity this component is attached to. + */ +class OnRails( + val entity: EntityId, +) : ExternalComponent("eucalyptus_core::transform::OnRails") { + + /** + * Whether the component actively moves the entity along the path each frame. + * + * When `false`, the system will not update the entity's position from the path; `progress` + * can still be read or written externally (e.g., for cutscene sequencers). + */ + var enabled: Boolean + get() = onRailsGetEnabled() + set(value) = onRailsSetEnabled(value) + + /** + * The ordered list of world-space waypoints that define the rail. + * + * Requires at least 2 points. Internally stored as `Vec<DVec3>` on the Rust side. + */ + var path: List<Vector3d> + get() = onRailsGetPath() + set(value) = onRailsSetPath(value) + + /** + * Current interpolation position along the rail, in the range `[0.0, 1.0]`. + * + * `0.0` is the first waypoint and `1.0` is the last. Can be set directly when using + * [RailDrive.Manual]. + */ + var progress: Float + get() = onRailsGetProgress() + set(value) = onRailsSetProgress(value) + + /** + * How the rail progress is updated each frame. + * + * @see RailDrive + */ + var drive: RailDrive + get() = onRailsGetDrive() + set(value) = onRailsSetDrive(value) + + companion object : ComponentType<OnRails> { + override fun get(entityId: EntityId): OnRails? { + return if (onRailsExistsForEntity(entityId)) OnRails(entityId) else null + } + } +} + +internal expect fun onRailsExistsForEntity(entityId: EntityId): Boolean + +internal expect fun OnRails.onRailsGetEnabled(): Boolean +internal expect fun OnRails.onRailsSetEnabled(enabled: Boolean) + +internal expect fun OnRails.onRailsGetPath(): List<Vector3d> +internal expect fun OnRails.onRailsSetPath(path: List<Vector3d>) + +internal expect fun OnRails.onRailsGetProgress(): Float +internal expect fun OnRails.onRailsSetProgress(progress: Float) + +internal expect fun OnRails.onRailsGetDrive(): RailDrive +internal expect fun OnRails.onRailsSetDrive(drive: RailDrive) @@ -1,8 +1,11 @@ package com.dropbear.components.camera +import com.dropbear.ecs.NativeComponent import com.dropbear.math.Vector3d import com.dropbear.physics.ColliderShape import com.dropbear.physics.Physics +import com.dropbear.ui.Inspect +import com.dropbear.ui.WidgetType /** * A springy camera that is used for any camera. It uses ray casting to check if the distance from the player to the @@ -10,7 +13,11 @@ import com.dropbear.physics.Physics * * Inspired by Godot's SpringyCameraController (other engines probably have their own, never used them before). */ -class SpringyCameraController { +class SpringyCameraController: NativeComponent( + fullyQualifiedTypeName = "com.dropbear.components.camera.SpringyCameraController", + typeName = "SpringyCameraController", +) { + @Inspect(WidgetType.DragValue) private var currentDistance: Double = 5.0 private val margin = 0.3 private val sphereRadius = 0.2 @@ -47,4 +54,7 @@ class SpringyCameraController { return playerPos + (dir * currentDistance) } + + override fun inspect() {} + override fun updateComponent() {} } @@ -5,9 +5,9 @@ import com.dropbear.EntityId /** * Marks a class as a user-defined ECS component for the dropbear engine. * - * Annotated classes must extend [Component]. The magna-carta tool scans for this annotation at - * build time to generate a [ComponentManager] that registers the component type with the native - * engine and dispatches lifecycle callbacks from Rust. + * Annotated classes must extend [NativeComponent]. The `magna-carta` tool scans for this annotation + * at build time to generate a [ComponentManager] that registers each component type with the + * native engine's ComponentRegistry via [registerKotlinComponentType]. * * Retention is [AnnotationRetention.RUNTIME] so JNI reflection can discover types at startup * without relying on build-time scanning alone. @@ -15,35 +15,76 @@ import com.dropbear.EntityId @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class EcsComponent + +internal expect fun registerKotlinComponentType( + fqcn: String, + typeName: String?, + category: String?, + description: String?, +) + /** - * A custom property that *can* be attached to an entity in the dropbear ECS system. + * The base class of any component. * - * # Note - * A [ComponentType] `companion object` is required to be implemented to be queryable. Take a look at - * the documentation for the object. + * Extend [NativeComponent] or [ExternalComponent], not this class directly. + */ +sealed class Component + +/** + * States a Kotlin-defined ECS component. * + * Annotate subclasses with [@EcsComponent][EcsComponent] so that `magna-carta` can discover them + * at build time and emit a `ComponentManager` that calls [register] for every discovered type. * + * @param fullyQualifiedTypeName The fully qualified class name used as the stable key inside the + * Rust ComponentRegistry (e.g. `"com.example.PlayerHealth"`). Prefer using the literal string + * directly so the value survives code shrinking / obfuscation. + * @param typeName Human-readable short name shown in the editor (e.g. `"PlayerHealth"`). + * @param category Optional grouping shown in the "Add Component" picker (e.g. `"Gameplay"`). + * @param description Optional one-line description shown as a tooltip in the editor. */ -abstract class Component( - val fullyQualifiedTypeName: String?, - val typeName: String?, - val category: String?, - val description: String?, -) { +abstract class NativeComponent( + val fullyQualifiedTypeName: String, + val typeName: String, + val category: String? = null, + val description: String? = null, +): Component() { /** - * Ran on a component being attached to an entity. + * Registers this component type with the engine's ComponentRegistry. + * + * This is called once per type at startup */ - abstract fun onAttach() + fun register() { + registerKotlinComponentType(fullyQualifiedTypeName, typeName, category, description) + } /** - * Ran on a component being updated. + * Renders the inspector UI for this component in the editor. + * + * Called by the editor for every entity that has this component type attached. */ - abstract fun update() + abstract fun inspect() /** - * Ran on a component being detached from an entity. + * Updates the component for all entities that contain this component. */ - abstract fun onDetach() + abstract fun updateComponent() +} + +/** + * Proxy for an ECS component that is defined outside Kotlin, typically in a Rust crate or + * another native shared library, and registered with the engine's ComponentRegistry under a + * known [fullyQualifiedTypeName]. + * + * Use this when you need to query whether an entity carries a non-Kotlin component, without + * owning or duplicating its data on the Kotlin side. + * + * @param fullyQualifiedTypeName The fully qualified type name that the external component was + * registered under in the ComponentRegistry (e.g. `"eucalyptus_core::components::RigidBody"`). + */ +abstract class ExternalComponent( + val fullyQualifiedTypeName: String, +): Component() { } /** @@ -62,7 +103,7 @@ abstract class Component( */ interface ComponentType<T : Component> { /** - * Uses the FFI to check if the entity is a components. + * Uses the FFI to check if the entity is a component. * * Since most components are technically just classes with a ctor containing the parentEntity's id, * and getters and setters, it just needs to query the world. @@ -1,8 +1,8 @@ package com.dropbear.lighting import com.dropbear.EntityId -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.math.Vector3d import com.dropbear.utils.Colour import com.dropbear.utils.Range @@ -17,7 +17,7 @@ import com.dropbear.utils.Range */ class Light( val entity: EntityId -): Component(entity, "Light") { +): ExternalComponent("dropbear_engine::lighting::Light") { /** * The position of the light in 3D space. * @@ -1,8 +1,8 @@ package com.dropbear.physics -import com.dropbear.ecs.Component import com.dropbear.EntityId import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent /** * A component that can added to an entity that defines all the colliders. @@ -10,8 +10,8 @@ import com.dropbear.ecs.ComponentType * This entity requires you to have the `ColliderGroup` component attached to the entity. */ class ColliderGroup( - internal val entity: EntityId, -) : Component(entity, "ColliderGroup") { + val entity: EntityId, +) : ExternalComponent("eucalyptus_core::physics::ColliderGroup") { /** * Fetches all colliders in the group. @@ -1,8 +1,8 @@ package com.dropbear.physics import com.dropbear.EntityId -import com.dropbear.ecs.Component import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.math.Quaterniond import com.dropbear.math.Vector3d import com.dropbear.math.degreesToRadians @@ -10,7 +10,7 @@ import kotlin.math.cos class KinematicCharacterController( val entity: EntityId, -) : Component(entity, "KCC") { +) : ExternalComponent("eucalyptus_core::physics::KCC") { val movementResult: CharacterMovementResult? get() = getMovementResult() @@ -1,8 +1,8 @@ package com.dropbear.physics -import com.dropbear.ecs.Component import com.dropbear.EntityId import com.dropbear.ecs.ComponentType +import com.dropbear.ecs.ExternalComponent import com.dropbear.math.Vector3d /** @@ -13,8 +13,8 @@ import com.dropbear.math.Vector3d */ class RigidBody( internal val index: Index, - internal val entity: EntityId, -) : Component(entity, "RigidBody") { + val entity: EntityId, +) : ExternalComponent("eucalyptus_core::physics::RigidBody") { /** * The mode of the rigidbody, as determined through [RigidBodyMode]. */ @@ -66,6 +66,13 @@ object DebugDraw { drawCapsuleNative(a, b, radius.toFloat(), colour) /** + * Draws a wireframe cylinder centered at [center], aligned to [axis]. + * [halfHeight] is the half-height along the axis. + */ + fun drawCylinder(center: Vector3d, halfHeight: Double, radius: Double, axis: Vector3d = Vector3d.up(), colour: Colour = Colour.WHITE) = + drawCylinderNative(center, halfHeight.toFloat(), radius.toFloat(), axis, colour) + + /** * Draws a wireframe cone from [apex] extending in [dir]. * [angle] is the half-angle in radians. [length] controls how far the cone extends. */ @@ -83,4 +90,5 @@ internal expect fun DebugDraw.drawGlobeNative(center: Vector3d, radius: Float, l internal expect fun DebugDraw.drawAabbNative(min: Vector3d, max: Vector3d, colour: Colour) internal expect fun DebugDraw.drawObbNative(center: Vector3d, halfExtents: Vector3d, rotation: Quaterniond, colour: Colour) internal expect fun DebugDraw.drawCapsuleNative(a: Vector3d, b: Vector3d, radius: Float, colour: Colour) +internal expect fun DebugDraw.drawCylinderNative(center: Vector3d, halfHeight: Float, radius: Float, axis: Vector3d, colour: Colour) internal expect fun DebugDraw.drawConeNative(apex: Vector3d, dir: Vector3d, angle: Float, length: Float, colour: Colour) @@ -0,0 +1,11 @@ +package com.dropbear.ui + +/** + * Marks a property of a [NativeComponent][com.dropbear.ecs.NativeComponent] subclass for + * display in the editor's inspector panel. + * + * @param widgetType The widget to render for this property in the inspector. + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Inspect(val widgetType: WidgetType) @@ -0,0 +1,19 @@ +package com.dropbear.ui + +enum class WidgetType { + Label, + TextEdit, + Button, + Checkbox, + RadioButton, + SelectableLabel, + ComboBox, + Slider, + DragValue, + ColourButton, + Image, + CollapsingHeader, // im not sure about this + Panel, // idk what this does + ProgressIndicator, + Other, +} @@ -0,0 +1,42 @@ +package com.dropbear.components.camera; + +import com.dropbear.EucalyptusCoreLoader; +import com.dropbear.math.Vector3d; + +public class OnRailsNative { + static { + new EucalyptusCoreLoader().ensureLoaded(); + } + + public static native boolean existsForEntity(long worldPtr, long entityId); + + public static native boolean getEnabled(long worldPtr, long entityId); + public static native void setEnabled(long worldPtr, long entityId, boolean enabled); + + public static native float getProgress(long worldPtr, long entityId); + public static native void setProgress(long worldPtr, long entityId, float progress); + + public static native int getPathLen(long worldPtr, long entityId); + public static native Vector3d getPathPoint(long worldPtr, long entityId, int index); + public static native void clearPath(long worldPtr, long entityId); + public static native void pushPathPoint(long worldPtr, long entityId, Vector3d point); + + // 0=Automatic, 1=FollowEntity, 2=AxisDriven, 3=Manual + public static native int getDriveType(long worldPtr, long entityId); + + public static native float getDriveAutomaticSpeed(long worldPtr, long entityId); + public static native boolean getDriveAutomaticLooping(long worldPtr, long entityId); + + public static native long getDriveFollowEntityTarget(long worldPtr, long entityId); + public static native boolean getDriveFollowEntityMonotonic(long worldPtr, long entityId); + + public static native long getDriveAxisDrivenTarget(long worldPtr, long entityId); + public static native Vector3d getDriveAxisDrivenAxis(long worldPtr, long entityId); + public static native float getDriveAxisDrivenRangeMin(long worldPtr, long entityId); + public static native float getDriveAxisDrivenRangeMax(long worldPtr, long entityId); + + public static native void setDriveAutomatic(long worldPtr, long entityId, float speed, boolean looping); + public static native void setDriveFollowEntity(long worldPtr, long entityId, long target, boolean monotonic); + public static native void setDriveAxisDriven(long worldPtr, long entityId, long target, Vector3d axis, float rangeMin, float rangeMax); + public static native void setDriveManual(long worldPtr, long entityId); +} @@ -20,5 +20,6 @@ public class DebugDrawNative { public static native void drawAabb(long graphicsContextPtr, Vector3d min, Vector3d max, Colour colour); public static native void drawObb(long graphicsContextPtr, Vector3d center, Vector3d halfExtents, Quaterniond rotation, Colour colour); public static native void drawCapsule(long graphicsContextPtr, Vector3d a, Vector3d b, float radius, Colour colour); + public static native void drawCylinder(long graphicsContextPtr, Vector3d center, float halfHeight, float radius, Vector3d axis, Colour colour); public static native void drawCone(long graphicsContextPtr, Vector3d apex, Vector3d dir, float angle, float length, Colour colour); } @@ -4,9 +4,6 @@ import com.dropbear.DropbearEngine import com.dropbear.EntityId import com.dropbear.components.ComponentNative -internal actual fun hasKotlinComponent(entityId: EntityId, fqcn: String): Boolean = - ComponentNative.hasKotlinComponent(DropbearEngine.native.worldHandle, entityId.raw, fqcn) - internal actual fun registerKotlinComponentType( fqcn: String, typeName: String?, @@ -37,5 +37,8 @@ internal actual fun DebugDraw.drawObbNative(center: Vector3d, halfExtents: Vecto internal actual fun DebugDraw.drawCapsuleNative(a: Vector3d, b: Vector3d, radius: Float, colour: Colour) = DebugDrawNative.drawCapsule(g, a, b, radius, colour) +internal actual fun DebugDraw.drawCylinderNative(center: Vector3d, halfHeight: Float, radius: Float, axis: Vector3d, colour: Colour) = + DebugDrawNative.drawCylinder(g, center, halfHeight, radius, axis, colour) + internal actual fun DebugDraw.drawConeNative(apex: Vector3d, dir: Vector3d, angle: Float, length: Float, colour: Colour) = DebugDrawNative.drawCone(g, apex, dir, angle, length, colour) @@ -29,4 +29,8 @@ internal actual fun SceneManager.switchToSceneImmediateNative(sceneName: String) DropbearEngine.native.commandBufferHandle, sceneName ) +} + +internal actual fun SceneManager.getSceneMetadataNative(sceneName: String): SceneMetadata? { + TODO("Not yet implemented") } @@ -0,0 +1,5 @@ +package com.dropbear.scene + +import com.dropbear.EntityRef + +actual fun SceneMetadata.getEntities(): List<EntityRef> = emptyList() @@ -20,12 +20,12 @@ internal actual fun getAsset(eucaURI: String): Long? = memScoped { val outPresent = alloc<BooleanVar>() val kindVar = alloc<UIntVar>() - // Try Texture + // try tex kindVar.value = AssetKind_Texture var rc = dropbear_engine_get_asset(assets, eucaURI, kindVar.ptr, outId.ptr, outPresent.ptr) if (rc == 0 && outPresent.value) return@memScoped outId.value.toLong() - // Try Model + // try model kindVar.value = AssetKind_Model rc = dropbear_engine_get_asset(assets, eucaURI, kindVar.ptr, outId.ptr, outPresent.ptr) if (rc == 0 && outPresent.value) outId.value.toLong() else null @@ -45,6 +45,6 @@ internal actual fun EntityTransform.setWorldTransform(entityId: EntityId, transf internal actual fun EntityTransform.propagateTransform(entityId: EntityId): Transform? = memScoped { val world = DropbearEngine.native.worldHandle ?: return@memScoped null val out = alloc<NTransform>() - val rc = dropbear_transform_propogate_transform(world, entityId.raw.toULong(), out.ptr) + val rc = dropbear_transform_propagate_transform(world, entityId.raw.toULong(), out.ptr) if (rc != 0) null else readTransform(out) } @@ -1,22 +1,11 @@ -@file:OptIn(ExperimentalForeignApi::class) - package com.dropbear.ecs -import com.dropbear.DropbearEngine import com.dropbear.EntityId -import com.dropbear.ffi.generated.* -import kotlinx.cinterop.* -internal actual fun hasKotlinComponent(entityId: EntityId, fqcn: String): Boolean = memScoped { - val world = DropbearEngine.native.worldHandle ?: return@memScoped false - dropbear_kotlin_component_exists(world, entityId.raw.toULong(), fqcn) -} internal actual fun registerKotlinComponentType( fqcn: String, typeName: String?, category: String?, description: String?, -) { - dropbear_register_kotlin_component(fqcn, typeName, category, description) -} +) = Unit @@ -59,86 +59,12 @@ internal actual fun DebugDraw.drawCapsuleNative(a: Vector3d, b: Vector3d, radius dropbear_debug_draw_capsule(g, allocVec3(a).ptr, allocVec3(b).ptr, radius, allocColour(colour).ptr) } -internal actual fun DebugDraw.drawConeNative(apex: Vector3d, dir: Vector3d, angle: Float, length: Float, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - dropbear_debug_draw_cone(g, allocVec3(apex).ptr, allocVec3(dir).ptr, angle, length, allocColour(colour).ptr) -} - - -internal actual fun DebugDraw.drawLineNative(start: Vector3d, end: Vector3d, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val s = allocVec3(start); val e = allocVec3(end) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_line(g, s.ptr, e.ptr, c) -} - -internal actual fun DebugDraw.drawRayNative(origin: Vector3d, dir: Vector3d, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val o = allocVec3(origin); val d = allocVec3(dir) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_ray(g, o.ptr, d.ptr, c) -} - -internal actual fun DebugDraw.drawArrowNative(start: Vector3d, end: Vector3d, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val s = allocVec3(start); val e = allocVec3(end) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_arrow(g, s.ptr, e.ptr, c) -} - -internal actual fun DebugDraw.drawPointNative(pos: Vector3d, size: Float, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val p = allocVec3(pos) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_point(g, p.ptr, size, c) -} - -internal actual fun DebugDraw.drawCircleNative(center: Vector3d, radius: Float, normal: Vector3d, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val ct = allocVec3(center); val n = allocVec3(normal) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_circle(g, ct.ptr, radius, n.ptr, c) -} - -internal actual fun DebugDraw.drawSphereNative(center: Vector3d, radius: Float, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val ct = allocVec3(center) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_sphere(g, ct.ptr, radius, c) -} - -internal actual fun DebugDraw.drawGlobeNative(center: Vector3d, radius: Float, latLines: Int, lonLines: Int, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val ct = allocVec3(center) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_globe(g, ct.ptr, radius, latLines.toUInt(), lonLines.toUInt(), c) -} - -internal actual fun DebugDraw.drawAabbNative(min: Vector3d, max: Vector3d, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val mn = allocVec3(min); val mx = allocVec3(max) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_aabb(g, mn.ptr, mx.ptr, c) -} - -internal actual fun DebugDraw.drawObbNative(center: Vector3d, halfExtents: Vector3d, rotation: Quaterniond, colour: Colour) = memScoped { +internal actual fun DebugDraw.drawCylinderNative(center: Vector3d, halfHeight: Float, radius: Float, axis: Vector3d, colour: Colour) = memScoped { val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val ct = allocVec3(center); val he = allocVec3(halfExtents) - val rot = allocQuat(rotation) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_obb(g, ct.ptr, he.ptr, rot.ptr, c) -} - -internal actual fun DebugDraw.drawCapsuleNative(a: Vector3d, b: Vector3d, radius: Float, colour: Colour) = memScoped { - val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val va = allocVec3(a); val vb = allocVec3(b) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_capsule(g, va.ptr, vb.ptr, radius, c) + dropbear_debug_draw_cylinder(g, allocVec3(center).ptr, halfHeight, radius, allocVec3(axis).ptr, allocColour(colour).ptr) } internal actual fun DebugDraw.drawConeNative(apex: Vector3d, dir: Vector3d, angle: Float, length: Float, colour: Colour) = memScoped { val g = DropbearEngine.native.graphicsContextHandle ?: return@memScoped - val a = allocVec3(apex); val d = allocVec3(dir) - val c = nColour(colour).toCValue(this) - dropbear_debug_draw_cone(g, a.ptr, d.ptr, angle, length, c) + dropbear_debug_draw_cone(g, allocVec3(apex).ptr, allocVec3(dir).ptr, angle, length, allocColour(colour).ptr) } @@ -26,4 +26,10 @@ internal actual fun SceneManager.loadSceneAsyncNative(sceneName: String, loading internal actual fun SceneManager.switchToSceneImmediateNative(sceneName: String) { val cmd = DropbearEngine.native.commandBufferHandle ?: return memScoped { dropbear_scripting_switch_to_scene_immediate(cmd, sceneName) } -} +} + +internal actual fun SceneManager.getSceneMetadataNative(sceneName: String): SceneMetadata? { + TODO("Not yet implemented") +} + +actual fun SceneMetadata.getEntities(): List<com.dropbear.EntityRef> = emptyList() @@ -0,0 +1,77 @@ +package com.dropbear.components.camera + +import com.dropbear.DropbearEngine +import com.dropbear.EntityId +import com.dropbear.math.Vector3d +import com.dropbear.math.Vector3f + +internal actual fun onRailsExistsForEntity(entityId: EntityId): Boolean = + OnRailsNative.existsForEntity(DropbearEngine.native.worldHandle, entityId.raw) + +internal actual fun OnRails.onRailsGetEnabled(): Boolean = + OnRailsNative.getEnabled(DropbearEngine.native.worldHandle, entity.raw) + +internal actual fun OnRails.onRailsSetEnabled(enabled: Boolean) = + OnRailsNative.setEnabled(DropbearEngine.native.worldHandle, entity.raw, enabled) + +internal actual fun OnRails.onRailsGetProgress(): Float = + OnRailsNative.getProgress(DropbearEngine.native.worldHandle, entity.raw) + +internal actual fun OnRails.onRailsSetProgress(progress: Float) = + OnRailsNative.setProgress(DropbearEngine.native.worldHandle, entity.raw, progress) + +internal actual fun OnRails.onRailsGetPath(): List<Vector3d> { + val world = DropbearEngine.native.worldHandle + val len = OnRailsNative.getPathLen(world, entity.raw) + return (0 until len).map { i -> OnRailsNative.getPathPoint(world, entity.raw, i) } +} + +internal actual fun OnRails.onRailsSetPath(path: List<Vector3d>) { + val world = DropbearEngine.native.worldHandle + OnRailsNative.clearPath(world, entity.raw) + path.forEach { p -> OnRailsNative.pushPathPoint(world, entity.raw, p) } +} + +internal actual fun OnRails.onRailsGetDrive(): RailDrive { + val world = DropbearEngine.native.worldHandle + val entityRaw = entity.raw + return when (OnRailsNative.getDriveType(world, entityRaw)) { + 0 -> RailDrive.Automatic( + speed = OnRailsNative.getDriveAutomaticSpeed(world, entityRaw), + looping = OnRailsNative.getDriveAutomaticLooping(world, entityRaw), + ) + 1 -> RailDrive.FollowEntity( + target = EntityId(OnRailsNative.getDriveFollowEntityTarget(world, entityRaw)), + monotonic = OnRailsNative.getDriveFollowEntityMonotonic(world, entityRaw), + ) + 2 -> { + val axisD = OnRailsNative.getDriveAxisDrivenAxis(world, entityRaw) + RailDrive.AxisDriven( + target = EntityId(OnRailsNative.getDriveAxisDrivenTarget(world, entityRaw)), + axis = Vector3f(axisD.x.toFloat(), axisD.y.toFloat(), axisD.z.toFloat()), + rangeMin = OnRailsNative.getDriveAxisDrivenRangeMin(world, entityRaw), + rangeMax = OnRailsNative.getDriveAxisDrivenRangeMax(world, entityRaw), + ) + } + else -> RailDrive.Manual + } +} + +internal actual fun OnRails.onRailsSetDrive(drive: RailDrive) { + val world = DropbearEngine.native.worldHandle + val entityRaw = entity.raw + when (drive) { + is RailDrive.Automatic -> + OnRailsNative.setDriveAutomatic(world, entityRaw, drive.speed, drive.looping) + is RailDrive.FollowEntity -> + OnRailsNative.setDriveFollowEntity(world, entityRaw, drive.target.raw, drive.monotonic) + is RailDrive.AxisDriven -> + OnRailsNative.setDriveAxisDriven( + world, entityRaw, drive.target.raw, + Vector3d(drive.axis.x.toDouble(), drive.axis.y.toDouble(), drive.axis.z.toDouble()), + drive.rangeMin, drive.rangeMax, + ) + is RailDrive.Manual -> + OnRailsNative.setDriveManual(world, entityRaw) + } +} @@ -0,0 +1,2 @@ +package com.dropbear.scene + @@ -0,0 +1,33 @@ +package com.dropbear.scene + +actual fun com.dropbear.scene.SceneSettings.getPreload(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.getHitboxState(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setHitboxState(value: Boolean) { +} + +actual fun SceneSettings.getOverlayHUDState(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setOverlayHUDState(value: Boolean) { +} + +actual fun SceneSettings.getOverlayBillboardState(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setOverlayBillboardState(value: Boolean) { +} + +actual fun SceneSettings.getAmbientStrength(): Double { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setAmbientStrength(value: Double) { +} @@ -0,0 +1,123 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package com.dropbear.components.camera + +import com.dropbear.DropbearEngine +import com.dropbear.EntityId +import com.dropbear.ffi.generated.* +import com.dropbear.math.Vector3d +import com.dropbear.math.Vector3f +import kotlinx.cinterop.* + +internal actual fun onRailsExistsForEntity(entityId: EntityId): Boolean = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped false + val out = alloc<BooleanVar>() + dropbear_transform_on_rails_exists_for_entity(world, entityId.raw.toULong(), out.ptr) + out.value +} + +internal actual fun OnRails.onRailsGetEnabled(): Boolean = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped false + val out = alloc<BooleanVar>() + dropbear_transform_on_rails_get_enabled(world, entity.raw.toULong(), out.ptr) + out.value +} + +internal actual fun OnRails.onRailsSetEnabled(enabled: Boolean) = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped + dropbear_transform_on_rails_set_enabled(world, entity.raw.toULong(), enabled) +} + +internal actual fun OnRails.onRailsGetProgress(): Float = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped 0f + val out = alloc<FloatVar>() + dropbear_transform_on_rails_get_progress(world, entity.raw.toULong(), out.ptr) + out.value +} + +internal actual fun OnRails.onRailsSetProgress(progress: Float) = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped + dropbear_transform_on_rails_set_progress(world, entity.raw.toULong(), progress) +} + +internal actual fun OnRails.onRailsGetPath(): List<Vector3d> = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped emptyList() + val lenOut = alloc<IntVar>() + val rc = dropbear_transform_on_rails_get_path_len(world, entity.raw.toULong(), lenOut.ptr) + if (rc != 0) return@memScoped emptyList() + val len = lenOut.value + (0 until len).mapNotNull { i -> + val out = alloc<NVector3>() + val rc2 = dropbear_transform_on_rails_get_path_point(world, entity.raw.toULong(), i, out.ptr) + if (rc2 != 0) null else Vector3d(out.x, out.y, out.z) + } +} + +internal actual fun OnRails.onRailsSetPath(path: List<Vector3d>) = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped + dropbear_transform_on_rails_clear_path(world, entity.raw.toULong()) + for (point in path) { + val v = alloc<NVector3>() + v.x = point.x; v.y = point.y; v.z = point.z + dropbear_transform_on_rails_push_path_point(world, entity.raw.toULong(), v.ptr) + } +} + +internal actual fun OnRails.onRailsGetDrive(): RailDrive = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped RailDrive.Manual + val typeOut = alloc<IntVar>() + val rc = dropbear_transform_on_rails_get_drive_type(world, entity.raw.toULong(), typeOut.ptr) + if (rc != 0) return@memScoped RailDrive.Manual + + when (typeOut.value) { + 0 -> { + val speed = alloc<FloatVar>() + val looping = alloc<BooleanVar>() + dropbear_transform_on_rails_get_drive_automatic_speed(world, entity.raw.toULong(), speed.ptr) + dropbear_transform_on_rails_get_drive_automatic_looping(world, entity.raw.toULong(), looping.ptr) + RailDrive.Automatic(speed.value, looping.value) + } + 1 -> { + val target = alloc<ULongVar>() + val monotonic = alloc<BooleanVar>() + dropbear_transform_on_rails_get_drive_follow_entity_target(world, entity.raw.toULong(), target.ptr) + dropbear_transform_on_rails_get_drive_follow_entity_monotonic(world, entity.raw.toULong(), monotonic.ptr) + RailDrive.FollowEntity(EntityId(target.value.toLong()), monotonic.value) + } + 2 -> { + val target = alloc<ULongVar>() + val axis = alloc<NVector3>() + val rangeMin = alloc<FloatVar>() + val rangeMax = alloc<FloatVar>() + dropbear_transform_on_rails_get_drive_axis_driven_target(world, entity.raw.toULong(), target.ptr) + dropbear_transform_on_rails_get_drive_axis_driven_axis(world, entity.raw.toULong(), axis.ptr) + dropbear_transform_on_rails_get_drive_axis_driven_range_min(world, entity.raw.toULong(), rangeMin.ptr) + dropbear_transform_on_rails_get_drive_axis_driven_range_max(world, entity.raw.toULong(), rangeMax.ptr) + RailDrive.AxisDriven( + EntityId(target.value.toLong()), + Vector3f(axis.x.toFloat(), axis.y.toFloat(), axis.z.toFloat()), + rangeMin.value, + rangeMax.value, + ) + } + else -> RailDrive.Manual + } +} + +internal actual fun OnRails.onRailsSetDrive(drive: RailDrive) = memScoped { + val world = DropbearEngine.native.worldHandle ?: return@memScoped + val entityBits = entity.raw.toULong() + when (drive) { + is RailDrive.Automatic -> + dropbear_transform_on_rails_set_drive_automatic(world, entityBits, drive.speed, drive.looping) + is RailDrive.FollowEntity -> + dropbear_transform_on_rails_set_drive_follow_entity(world, entityBits, drive.target.raw.toULong(), drive.monotonic) + is RailDrive.AxisDriven -> { + val v = alloc<NVector3>() + v.x = drive.axis.x.toDouble(); v.y = drive.axis.y.toDouble(); v.z = drive.axis.z.toDouble() + dropbear_transform_on_rails_set_drive_axis_driven(world, entityBits, drive.target.raw.toULong(), v.ptr, drive.rangeMin, drive.rangeMax) + } + is RailDrive.Manual -> + dropbear_transform_on_rails_set_drive_manual(world, entityBits) + } +} @@ -0,0 +1 @@ +package com.dropbear.scene @@ -0,0 +1,33 @@ +package com.dropbear.scene + +actual fun com.dropbear.scene.SceneSettings.getPreload(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.getHitboxState(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setHitboxState(value: Boolean) { +} + +actual fun SceneSettings.getOverlayHUDState(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setOverlayHUDState(value: Boolean) { +} + +actual fun SceneSettings.getOverlayBillboardState(): Boolean { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setOverlayBillboardState(value: Boolean) { +} + +actual fun SceneSettings.getAmbientStrength(): Double { + TODO("Not yet implemented") +} + +actual fun SceneSettings.setAmbientStrength(value: Double) { +}