kitgit

tirbofish/dropbear · commit

6c47dd80d79e6b1a2a6195e569aed18c12008120

feature: physics shape cast. fix: implement the shadowJar for creating farJars to package jarvis, run github actions Unverified

Thribhu K <4tkbytes@pm.me> · 2026-01-19 16:21

view full diff

diff --git a/.github/workflows/build_fatty.yaml b/.github/workflows/build_fatty.yaml
index 730147a..c83d8f4 100644
--- a/.github/workflows/build_fatty.yaml
+++ b/.github/workflows/build_fatty.yaml
@@ -25,7 +25,7 @@ jobs:
         run: chmod +x gradlew
 
       - name: Build with Gradle
-        run: ./gradlew build fatJar
+        run: ./gradlew build shadowJar
 
       - name: Upload JAR artifact
         uses: actions/upload-artifact@v4
diff --git a/.github/workflows/create_executable.yaml b/.github/workflows/create_executable.yaml
index 0ecba95..2849e23 100644
--- a/.github/workflows/create_executable.yaml
+++ b/.github/workflows/create_executable.yaml
@@ -41,8 +41,8 @@ jobs:
       - name: give gradle permissions
         if: matrix.os != 'windows-latest'
         run: chmod +x gradlew
-      - name: build fatjar
-        run: ./gradlew fatJar
+      - name: build shadowJar
+        run: ./gradlew shadowJar
       - name: install libudev-dev (Linux)
         if: matrix.os == 'ubuntu-latest'
         run: sudo apt-get update && sudo apt-get install -y libudev-dev
diff --git a/.run/Run eucalyptus-editor.run.xml b/.run/Run eucalyptus-editor.run.xml
index 8a0a48b..d7393d3 100644
--- a/.run/Run eucalyptus-editor.run.xml
+++ b/.run/Run eucalyptus-editor.run.xml
@@ -13,8 +13,6 @@
     <option name="backtrace" value="FULL" />
     <option name="isRedirectInput" value="false" />
     <option name="redirectInputPath" value="" />
-    <method v="2">
-      <option name="RunConfigurationTask" enabled="true" run_configuration_name="fatJar" run_configuration_type="GradleRunConfiguration" />
-    </method>
+    <method v="2" />
   </configuration>
 </component>
\ No newline at end of file
diff --git a/.run/fatJar.run.xml b/.run/fatJar.run.xml
index cb795fa..49a5db4 100644
--- a/.run/fatJar.run.xml
+++ b/.run/fatJar.run.xml
@@ -1,5 +1,5 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="fatJar" type="GradleRunConfiguration" factoryName="Gradle">
+  <configuration default="false" name="shadowJar" type="GradleRunConfiguration" factoryName="Gradle">
     <ExternalSystemSettings>
       <option name="executionName" />
       <option name="externalProjectPath" value="$PROJECT_DIR$" />
@@ -10,7 +10,7 @@
       </option>
       <option name="taskNames">
         <list>
-          <option value="fatJar" />
+          <option value="shadowJar" />
         </list>
       </option>
       <option name="vmOptions" />
diff --git a/README.md b/README.md
index 03c84ea..581fb3d 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ git clone git@github.com:tirbofish/dropbear
 cd dropbear
 
 # eucalyptus-editor requires dropbear-1.0-SNAPSHOT-all.jar to be built first
-./gradlew fatJar
+./gradlew shadowJar
 # this will build all the projects in the workspace
 cargo build
 ```
diff --git a/build.gradle.kts b/build.gradle.kts
index 5ba1f2c..ced3d17 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,6 +3,7 @@ plugins {
     alias(libs.plugins.kotlinxSerialization)
     `maven-publish`
     id("org.jetbrains.dokka") version "2.1.0"
+    id("com.gradleup.shadow") version "9.2.2"
 }
 
 group = "com.dropbear"
@@ -126,6 +127,12 @@ kotlin {
         }
     }
 
+    java {
+        sourceSets.getByName("jvmMain") {
+            java.srcDirs("scripting/jvmMain/java")
+        }
+    }
+
     targets.all {
         compilations.all {
             compileTaskProvider.configure {
@@ -207,21 +214,4 @@ publishing {
             }
         }
     }
-}
-
-tasks.register<Jar>("fatJar") {
-    archiveClassifier.set("all")
-    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-
-    from(kotlin.jvm().compilations["main"].output)
-
-    configurations.named("jvmRuntimeClasspath").get().forEach { file ->
-        if (file.name.endsWith(".jar")) {
-            from(zipTree(file))
-        } else {
-            from(file)
-        }
-    }
-
-    manifest {}
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/physics.rs b/crates/eucalyptus-core/src/physics.rs
index 3fe01f6..e5d3df1 100644
--- a/crates/eucalyptus-core/src/physics.rs
+++ b/crates/eucalyptus-core/src/physics.rs
@@ -334,7 +334,7 @@ pub mod jni {
     use crate::physics::nalgebra;
     use crate::physics::PhysicsState;
     use crate::scripting::jni::utils::{FromJObject, ToJObject};
-    use crate::types::{ColliderFFI, IndexNative, RayHit, Vector3};
+    use crate::types::{ColliderFFI, IndexNative, RayHit, ShapeCastHitFFI, Vector3};
     use hecs::Entity;
     use jni::objects::{JClass, JObject};
     use jni::sys::{jboolean, jdouble, jlong, jobject};
@@ -342,6 +342,9 @@ pub mod jni {
     use rapier3d::parry::query::DefaultQueryDispatcher;
     use rapier3d::pipeline::QueryFilter;
     use rapier3d::prelude::{point, vector, Ray};
+    use rapier3d::parry::query::ShapeCastOptions;
+    use rapier3d::math::{Pose3, Vec3};
+    use crate::physics::collider::ColliderShape;
 
     #[unsafe(no_mangle)]
     pub extern "system" fn Java_com_dropbear_physics_PhysicsNative_getGravity(
@@ -475,6 +478,161 @@ pub mod jni {
         }
     }
 
+    fn collider_ffi_from_handle(physics: &PhysicsState, handle: rapier3d::prelude::ColliderHandle) -> ColliderFFI {
+        let (idx, generation) = handle.into_raw_parts();
+
+        let mut found_label = None;
+        for (label, colliders) in physics.colliders_entity_map.iter() {
+            for (_, c) in colliders {
+                if c.0 == handle.0 {
+                    found_label = Some(label);
+                    break;
+                }
+            }
+            if found_label.is_some() {
+                break;
+            }
+        }
+
+        let entity_id = if let Some(label) = found_label {
+            physics
+                .entity_label_map
+                .iter()
+                .find(|(_, l)| *l == label)
+                .map(|(e, _)| e.to_bits().get())
+                .unwrap_or(Entity::DANGLING.to_bits().get())
+        } else {
+            Entity::DANGLING.to_bits().get()
+        };
+
+        ColliderFFI {
+            index: IndexNative { index: idx, generation },
+            entity_id,
+            id: idx,
+        }
+    }
+
+    fn shared_shape_from_collider_shape(shape: &ColliderShape) -> rapier3d::geometry::SharedShape {
+        match shape {
+            ColliderShape::Box { half_extents } => {
+                rapier3d::geometry::SharedShape::cuboid(half_extents[0], half_extents[1], half_extents[2])
+            }
+            ColliderShape::Sphere { radius } => rapier3d::geometry::SharedShape::ball(*radius),
+            ColliderShape::Capsule { half_height, radius } => {
+                rapier3d::geometry::SharedShape::capsule_y(*half_height, *radius)
+            }
+            ColliderShape::Cylinder { half_height, radius } => {
+                rapier3d::geometry::SharedShape::cylinder(*half_height, *radius)
+            }
+            ColliderShape::Cone { half_height, radius } => {
+                rapier3d::geometry::SharedShape::cone(*half_height, *radius)
+            }
+        }
+    }
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_physics_PhysicsNative_shapeCast(
+        mut env: JNIEnv,
+        _: JClass,
+        physics_handle: jlong,
+        origin: JObject,
+        direction: JObject,
+        shape: JObject,
+        time_of_impact: jdouble,
+        solid: jboolean,
+    ) -> jobject {
+        let physics = crate::convert_ptr!(mut physics_handle => PhysicsState);
+
+        let qp = physics.broad_phase.as_query_pipeline(
+            &DefaultQueryDispatcher,
+            &physics.bodies,
+            &physics.colliders,
+            QueryFilter::new(),
+        );
+
+        let origin = match Vector3::from_jobject(&mut env, &origin) {
+            Ok(v) => v,
+            Err(e) => {
+                let _ = env.throw_new(
+                    "java/lang/RuntimeException",
+                    format!("Unable to convert origin to Vector3: {e}"),
+                );
+                return std::ptr::null_mut();
+            }
+        };
+
+        let direction = match Vector3::from_jobject(&mut env, &direction) {
+            Ok(v) => v,
+            Err(e) => {
+                let _ = env.throw_new(
+                    "java/lang/RuntimeException",
+                    format!("Unable to convert direction to Vector3: {e}"),
+                );
+                return std::ptr::null_mut();
+            }
+        };
+
+        let shape = match ColliderShape::from_jobject(&mut env, &shape) {
+            Ok(v) => v,
+            Err(e) => {
+                let _ = env.throw_new(
+                    "java/lang/RuntimeException",
+                    format!("Unable to convert shape to ColliderShape: {e}"),
+                );
+                return std::ptr::null_mut();
+            }
+        };
+
+        let dir_len = ((direction.x * direction.x) + (direction.y * direction.y) + (direction.z * direction.z)).sqrt();
+        if dir_len <= f64::EPSILON {
+            return std::ptr::null_mut();
+        }
+
+        let dir_unit = Vector3 {
+            x: direction.x / dir_len,
+            y: direction.y / dir_len,
+            z: direction.z / dir_len,
+        };
+
+        let cast_shape = shared_shape_from_collider_shape(&shape);
+        let iso: Pose3 = nalgebra::Isometry3::translation(origin.x as f32, origin.y as f32, origin.z as f32).into();
+        let vel: Vec3 = vector![dir_unit.x as f32, dir_unit.y as f32, dir_unit.z as f32].into();
+
+        let options = ShapeCastOptions {
+            max_time_of_impact: time_of_impact as f32,
+            target_distance: 0.0,
+            stop_at_penetration: solid != 0,
+            compute_impact_geometry_on_penetration: true,
+        };
+
+        let Some((hit_handle, toi)) = qp.cast_shape(&iso, vel, cast_shape.as_ref(), options) else {
+            return std::ptr::null_mut();
+        };
+
+        let collider = collider_ffi_from_handle(&physics, hit_handle);
+
+        let hit = ShapeCastHitFFI {
+            collider,
+            distance: toi.time_of_impact as f64,
+            witness1: Vector3::from([toi.witness1.x, toi.witness1.y, toi.witness1.z]),
+            witness2: Vector3::from([toi.witness2.x, toi.witness2.y, toi.witness2.z]),
+            normal1: Vector3::from([toi.normal1.x, toi.normal1.y, toi.normal1.z]),
+            normal2: Vector3::from([toi.normal2.x, toi.normal2.y, toi.normal2.z]),
+            status: toi.status,
+        };
+
+        match hit.to_jobject(&mut env) {
+            Ok(v) => v.into_raw(),
+            Err(e) => {
+                let _ = env.throw_new(
+                    "java/lang/RuntimeException",
+                    format!("Unable to create ShapeCastHit object: {e}"),
+                );
+                std::ptr::null_mut()
+            }
+        }
+    }
+
     #[unsafe(no_mangle)]
     pub extern "system" fn Java_com_dropbear_physics_PhysicsNative_isOverlapping(
         mut env: JNIEnv,
diff --git a/crates/eucalyptus-core/src/scripting.rs b/crates/eucalyptus-core/src/scripting.rs
index 0b5b333..f2a6965 100644
--- a/crates/eucalyptus-core/src/scripting.rs
+++ b/crates/eucalyptus-core/src/scripting.rs
@@ -723,15 +723,15 @@ pub async fn build_jvm(
 
     let gradle_cmd = get_gradle_command(project_root);
 
-    let _ = status_sender.send(BuildStatus::Building(format!("Running: {} fatJar", gradle_cmd)));
+    let _ = status_sender.send(BuildStatus::Building(format!("Running: {} shadowJar", gradle_cmd)));
 
     let mut child = Command::new(&gradle_cmd)
         .current_dir(project_root)
-        .args(["--console=plain", "fatJar"])
+        .args(["--console=plain", "shadowJar"])
         .stdout(std::process::Stdio::piped())
         .stderr(std::process::Stdio::piped())
         .spawn()
-        .context(format!("Failed to spawn `{} fatJar`", gradle_cmd))?;
+        .context(format!("Failed to spawn `{} shadowJar`", gradle_cmd))?;
 
     let stdout = child.stdout.take().expect("Stdout was piped");
     let stderr = child.stderr.take().expect("Stderr was piped");
diff --git a/crates/eucalyptus-core/src/types.rs b/crates/eucalyptus-core/src/types.rs
index 75f5788..0860283 100644
--- a/crates/eucalyptus-core/src/types.rs
+++ b/crates/eucalyptus-core/src/types.rs
@@ -510,6 +510,59 @@ impl ToJObject for RayHit {
 
 #[repr(C)]
 #[derive(Clone, Copy)]
+pub struct ShapeCastHitFFI {
+    pub collider: ColliderFFI,
+    pub distance: f64,
+    pub witness1: Vector3,
+    pub witness2: Vector3,
+    pub normal1: Vector3,
+    pub normal2: Vector3,
+    pub status: rapier3d::parry::query::ShapeCastStatus,
+}
+
+impl ToJObject for ShapeCastHitFFI {
+    fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> {
+        use jni::sys::jdouble;
+
+        let collider = self.collider.to_jobject(env)?;
+        let witness1 = self.witness1.to_jobject(env)?;
+        let witness2 = self.witness2.to_jobject(env)?;
+        let normal1 = self.normal1.to_jobject(env)?;
+        let normal2 = self.normal2.to_jobject(env)?;
+        let status = self.status.to_jobject(env)?;
+
+        let distance = self.distance as jdouble;
+
+        let class = env.find_class("com/dropbear/physics/ShapeCastHit").map_err(|e| {
+            eprintln!("[JNI Error] Failed to find ShapeCastHit class: {:?}", e);
+            DropbearNativeError::JNIClassNotFound
+        })?;
+
+        let object = env
+            .new_object(
+                class,
+                "(Lcom/dropbear/physics/Collider;DLcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/physics/ShapeCastStatus;)V",
+                &[
+                    JValue::Object(&collider),
+                    JValue::Double(distance),
+                    JValue::Object(&witness1),
+                    JValue::Object(&witness2),
+                    JValue::Object(&normal1),
+                    JValue::Object(&normal2),
+                    JValue::Object(&status),
+                ],
+            )
+            .map_err(|e| {
+                eprintln!("[JNI Error] Failed to create ShapeCastHit object: {:?}", e);
+                DropbearNativeError::JNIFailedToCreateObject
+            })?;
+
+        Ok(object)
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
 /// Class: `com.dropbear.physics.CollisionEventType`
 pub enum CollisionEventType {
     Started,
diff --git a/scripting/commonMain/kotlin/com/dropbear/math/Math.kt b/scripting/commonMain/kotlin/com/dropbear/math/Math.kt
index 5979e49..ee34a7f 100644
--- a/scripting/commonMain/kotlin/com/dropbear/math/Math.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/math/Math.kt
@@ -35,4 +35,12 @@ fun normalizeRadians(radians: Double): Double {
         normalized += 2 * PI
     }
     return normalized
+}
+
+
+/**
+ * Compute the linear interpolation between two points x and y with a factor of t.
+ */
+fun lerp(x: Double, y: Double, t: Double): Double {
+    return x*(1-t)+y*t
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/physics/Physics.kt b/scripting/commonMain/kotlin/com/dropbear/physics/Physics.kt
index a78dadb..1e329c4 100644
--- a/scripting/commonMain/kotlin/com/dropbear/physics/Physics.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/physics/Physics.kt
@@ -57,6 +57,30 @@ class Physics {
         fun touching(entity1: EntityRef, entity2: EntityRef): Boolean {
             return isTouching(entity1, entity2)
         }
+
+        /**
+         * Casts a [shape] from the [origin] in a specific [direction]. Returns a nullable [ShapeCastHit] object.
+         *
+         * @param origin The origin of the cast.
+         * @param direction The direction of the cast. Prefer unit vectors.
+         * @param shape The shape to cast.
+         * @param maxDistance The maximum distance before quitting operation. Set to `null` if no maxDistance.
+         * @param solid If true, detects hits even if the cast starts inside a shape.
+         *              If false, the cast "passes through" from the inside until it exits.
+         */
+        fun shapeCast(
+            origin: Vector3d,
+            direction: Vector3d,
+            shape: ColliderShape,
+            maxDistance: Double?,
+            solid: Boolean,
+        ): ShapeCastHit? {
+            return if (maxDistance != null) {
+                shapeCast(origin, direction, shape, toi = maxDistance, solid)
+            } else {
+                shapeCast(origin, direction, shape, toi = Double.MAX_VALUE, solid)
+            }
+        }
     }
 }
 
@@ -66,4 +90,6 @@ internal expect fun setGravity(gravity: Vector3d)
 internal expect fun raycast(origin: Vector3d, direction: Vector3d, toi: Double, solid: Boolean): RayHit?
 internal expect fun isOverlapping(collider1: Collider, collider2: Collider): Boolean
 internal expect fun isTriggering(collider1: Collider, collider2: Collider): Boolean
-internal expect fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean
\ No newline at end of file
+internal expect fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean
+
+internal expect fun shapeCast(origin: Vector3d, direction: Vector3d, shape: ColliderShape, toi: Double, solid: Boolean): ShapeCastHit?
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/physics/ShapeCastHit.kt b/scripting/commonMain/kotlin/com/dropbear/physics/ShapeCastHit.kt
new file mode 100644
index 0000000..c3d70d8
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/physics/ShapeCastHit.kt
@@ -0,0 +1,28 @@
+package com.dropbear.physics
+
+import com.dropbear.math.Vector3d
+
+/**
+ * Defines a hit from a shape-cast (sweeping a volume through space).
+ *
+ * @param collider The first collider that is hit.
+ * @param distance The distance travelled along the cast direction at the time of impact.
+ * @param witness1 Contact point on the world collider.
+ * @param witness2 Contact point on the casted shape.
+ * @param normal1 Normal pointing from the world collider toward the casted shape.
+ * @param normal2 Normal pointing from the casted shape toward the world collider.
+ * @param status Status of the cast result.
+ */
+class ShapeCastHit(
+    val collider: Collider,
+    val distance: Double,
+    val witness1: Vector3d,
+    val witness2: Vector3d,
+    val normal1: Vector3d,
+    val normal2: Vector3d,
+    val status: ShapeCastStatus,
+) {
+    override fun toString(): String {
+        return "ShapeCastHit(collider=$collider, distance=$distance, status=$status)"
+    }
+}
diff --git a/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java b/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
index c324a60..fbf3e1a 100644
--- a/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
+++ b/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
@@ -2,7 +2,6 @@ package com.dropbear.physics;
 
 import com.dropbear.EucalyptusCoreLoader;
 import com.dropbear.math.Vector3d;
-import org.jetbrains.annotations.Nullable;
 
 // fuck, this got a long ass name
 public class KinematicCharacterControllerNative {
diff --git a/scripting/jvmMain/java/com/dropbear/physics/PhysicsNative.java b/scripting/jvmMain/java/com/dropbear/physics/PhysicsNative.java
index 078e3ba..389a510 100644
--- a/scripting/jvmMain/java/com/dropbear/physics/PhysicsNative.java
+++ b/scripting/jvmMain/java/com/dropbear/physics/PhysicsNative.java
@@ -12,6 +12,7 @@ public class PhysicsNative {
     public static native void setGravity(long physicsHandle, Vector3d gravity);
 
     public static native RayHit raycast(long physicsHandle, Vector3d origin, Vector3d direction, double toi, boolean solid);
+    public static native ShapeCastHit shapeCast(long physicsHandle, Vector3d origin, Vector3d direction, ColliderShape shape, double toi, boolean solid);
     public static native boolean isOverlapping(long physicsHandle, Collider collider1, Collider collider2);
     public static native boolean isTriggering(long physicsHandle, Collider collider1, Collider collider2);
     public static native boolean isTouching(long physicsHandle, long entity1, long entity2);
diff --git a/scripting/jvmMain/kotlin/com/dropbear/physics/Physics.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/physics/Physics.jvm.kt
index 844c9a1..61dc248 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/physics/Physics.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/physics/Physics.jvm.kt
@@ -31,4 +31,14 @@ internal actual fun isTriggering(collider1: Collider, collider2: Collider): Bool
 
 internal actual fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean {
     return PhysicsNative.isTouching(DropbearEngine.native.physicsEngineHandle, entity1.id.raw, entity2.id.raw)
+}
+
+internal actual fun shapeCast(
+    origin: Vector3d,
+    direction: Vector3d,
+    shape: ColliderShape,
+    toi: Double,
+    solid: Boolean
+): ShapeCastHit? {
+    return PhysicsNative.shapeCast(DropbearEngine.native.physicsEngineHandle, origin, direction, shape, toi, solid)
 }
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/physics/Physics.native.kt b/scripting/nativeMain/kotlin/com/dropbear/physics/Physics.native.kt
index 5298951..22500c9 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/physics/Physics.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/physics/Physics.native.kt
@@ -29,4 +29,14 @@ internal actual fun isTriggering(collider1: Collider, collider2: Collider): Bool
 
 internal actual fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean {
     TODO("Not implemented yet")
+}
+
+internal actual fun shapeCast(
+    origin: Vector3d,
+    direction: Vector3d,
+    shape: ColliderShape,
+    toi: Double,
+    solid: Boolean
+): ShapeCastHit? {
+    TODO("Not yet implemented")
 }
\ No newline at end of file