kitgit

tirbofish/dropbear · commit

f662502e76f08018038ed28d958df861aeaa94a1

implemented parent and child api, fixes #65

Unverified

Thribhu K <4tkbytes@pm.me> · 2025-11-29 08:32

view full diff

diff --git a/.run/Build eucalyptus-editor.run.xml b/.run/Build eucalyptus-editor.run.xml
deleted file mode 100644
index 72b14db..0000000
--- a/.run/Build eucalyptus-editor.run.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Build eucalyptus-editor" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
-    <option name="buildProfileId" value="dev" />
-    <option name="command" value="build" />
-    <option name="workingDirectory" value="file://$PROJECT_DIR$" />
-    <envs />
-    <option name="emulateTerminal" value="true" />
-    <option name="channel" value="DEFAULT" />
-    <option name="requiredFeatures" value="true" />
-    <option name="allFeatures" value="false" />
-    <option name="withSudo" value="false" />
-    <option name="buildTarget" value="REMOTE" />
-    <option name="backtrace" value="SHORT" />
-    <option name="isRedirectInput" value="false" />
-    <option name="redirectInputPath" value="" />
-    <method v="2">
-      <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
-    </method>
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Build eucalyptus_core cdylib.run.xml b/.run/Build eucalyptus_core cdylib.run.xml
deleted file mode 100644
index 03d23c4..0000000
--- a/.run/Build eucalyptus_core cdylib.run.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Build eucalyptus_core cdylib" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
-    <option name="buildProfileId" value="release" />
-    <option name="command" value="build -p eucalyptus-core --features &quot;editor&quot;" />
-    <option name="workingDirectory" value="file://$PROJECT_DIR$" />
-    <envs />
-    <option name="emulateTerminal" value="true" />
-    <option name="channel" value="DEFAULT" />
-    <option name="requiredFeatures" value="true" />
-    <option name="allFeatures" value="false" />
-    <option name="withSudo" value="false" />
-    <option name="buildTarget" value="REMOTE" />
-    <option name="backtrace" value="SHORT" />
-    <option name="isRedirectInput" value="false" />
-    <option name="redirectInputPath" value="" />
-    <method v="2">
-      <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
-    </method>
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Clean literally everything.run.xml b/.run/Clean literally everything.run.xml
deleted file mode 100644
index aa395a9..0000000
--- a/.run/Clean literally everything.run.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Clean literally everything" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
-    <option name="command" value="clean" />
-    <option name="workingDirectory" value="file://$PROJECT_DIR$" />
-    <envs />
-    <option name="emulateTerminal" value="true" />
-    <option name="channel" value="DEFAULT" />
-    <option name="requiredFeatures" value="true" />
-    <option name="allFeatures" value="false" />
-    <option name="withSudo" value="false" />
-    <option name="buildTarget" value="REMOTE" />
-    <option name="backtrace" value="SHORT" />
-    <option name="isRedirectInput" value="false" />
-    <option name="redirectInputPath" value="" />
-    <method v="2">
-      <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
-      <option name="Gradle.BeforeRunTask" enabled="true" tasks="clean" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" />
-    </method>
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Run eucalyptus-editor.run.xml b/.run/Run eucalyptus-editor.run.xml
index 9d06838..2be66ab 100644
--- a/.run/Run eucalyptus-editor.run.xml
+++ b/.run/Run eucalyptus-editor.run.xml
@@ -15,7 +15,7 @@
     <option name="redirectInputPath" value="" />
     <method v="2">
       <option name="RunConfigurationTask" enabled="true" run_configuration_name="fatJar" run_configuration_type="GradleRunConfiguration" />
-      <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+      <option name="RunConfigurationTask" enabled="true" run_configuration_name="Build" run_configuration_type="CargoCommandRunConfiguration" run_configuration_target="RsBuildProfile:dev" />
     </method>
   </configuration>
 </component>
\ No newline at end of file
diff --git a/.run/generateJNIHeaders.run.xml b/.run/generateJNIHeaders.run.xml
new file mode 100644
index 0000000..365668c
--- /dev/null
+++ b/.run/generateJNIHeaders.run.xml
@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="generateJNIHeaders" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="generateJniHeaders" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/.run/publishToMavenLocal.run.xml b/.run/publishToMavenLocal.run.xml
new file mode 100644
index 0000000..075fca5
--- /dev/null
+++ b/.run/publishToMavenLocal.run.xml
@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="publishToMavenLocal" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="publishToMavenLocal" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting/jni/exports.rs b/eucalyptus-core/src/scripting/jni/exports.rs
index 49c186d..ab5549c 100644
--- a/eucalyptus-core/src/scripting/jni/exports.rs
+++ b/eucalyptus-core/src/scripting/jni/exports.rs
@@ -1,5 +1,7 @@
+#![allow(non_snake_case)]
+
 use crate::camera::{CameraComponent, CameraType};
-use crate::hierarchy::EntityTransformExt;
+use crate::hierarchy::{Children, EntityTransformExt, Parent};
 use crate::ptr::{AssetRegistryPtr, GraphicsPtr, InputStatePtr, WorldPtr};
 use crate::scripting::jni::utils::{
     create_vector3, extract_vector3, java_button_to_rust, new_float_array,
@@ -18,9 +20,7 @@ use glam::{DQuat, DVec3};
 use hecs::World;
 use jni::JNIEnv;
 use jni::objects::{JClass, JObject, JPrimitiveArray, JString, JValue};
-use jni::sys::{
-    JNI_FALSE, jboolean, jclass, jdouble, jfloatArray, jint, jlong, jobject, jobjectArray, jstring,
-};
+use jni::sys::{JNI_FALSE, jboolean, jclass, jdouble, jfloatArray, jint, jlong, jobject, jobjectArray, jstring, jlongArray};
 use parking_lot::Mutex;
 use std::collections::{HashMap, HashSet};
 use std::sync::Arc;
@@ -389,7 +389,7 @@ pub fn Java_com_dropbear_ffi_JNINative_propagateTransform(
                 }
             };
 
-            return transform_obj.into_raw();
+            transform_obj.into_raw()
         } else {
             println!(
                 "[Java_com_dropbear_ffi_JNINative_propagateTransform] [ERROR] Failed to get entity transform"
@@ -2477,3 +2477,98 @@ pub fn Java_com_dropbear_ffi_JNINative_getAsset(
     };
     0 as jlong
 }
+
+/// `JNIEXPORT jlongArray JNICALL Java_com_dropbear_ffi_JNINative_getChildren
+///   (JNIEnv *, jclass, jlong, jlong);`
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_getChildren(
+    env: JNIEnv,
+    _class: JClass,
+    world_handle: jlong,
+    entity_id: jlong,
+) -> jlongArray {
+    let world = convert_ptr!(world_handle, WorldPtr => World);
+    let entity = convert_jlong_to_entity!(entity_id);
+
+    let entities = if let Ok(mut q) = world.query_one::<&Children>(entity) && let Some(children) = q.get() {
+        let children = children.children();
+        let mut array = vec![];
+        for child in children {
+            array.push(child.to_bits().get() as i64);
+        }
+        array
+    } else {
+        vec![]
+    };
+
+    let array = match env.new_long_array(entities.len() as i32) {
+        Ok(array) => array,
+        Err(e) => {
+            return crate::ffi_error_return!("Unable to create a new long array: {}", e);
+        }
+    };
+
+    if let Err(e) = env.set_long_array_region(&array, 0, &entities) {
+        return crate::ffi_error_return!("Unable to populate long array: {}", e);
+    }
+
+    array.into_raw()
+}
+
+/// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getChildByLabel
+///   (JNIEnv *, jclass, jlong, jlong, jstring);`
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_getChildByLabel(
+    mut env: JNIEnv,
+    _class: JClass,
+    world_handle: jlong,
+    entity_id: jlong,
+    label: JString,
+) -> jlong {
+    let world = convert_ptr!(world_handle, WorldPtr => World);
+    let entity = convert_jlong_to_entity!(entity_id);
+    let target = convert_jstring!(env, label);
+
+    if let Ok(mut q) = world.query_one::<&Children>(entity) && let Some(children) = q.get() {
+        for child in children.children() {
+            if let Ok(label) = world.get::<&Label>(entity) {
+                if label.as_str() == target {
+                    return child.to_bits().get() as jlong;
+                }
+            } else {
+                // skip if error or no entity
+                continue;
+            }
+        }
+
+    } else {
+        // no children exist for the entity
+        return -2 as jlong;
+    };
+
+    // no children exist with that label
+    -2 as jlong
+}
+
+/// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getParent
+///   (JNIEnv *, jclass, jlong, jlong);`
+#[unsafe(no_mangle)]
+pub fn Java_com_dropbear_ffi_JNINative_getParent(
+    _env: JNIEnv,
+    _class: JClass,
+    world_handle: jlong,
+    entity_id: jlong,
+) -> jlong {
+    let world = convert_ptr!(world_handle, WorldPtr => World);
+    let entity = convert_jlong_to_entity!(entity_id);
+
+    if let Ok(mut q) = world.query_one::<&Parent>(entity) {
+        if let Some(parent) = q.get() {
+            parent.parent().to_bits().get() as jlong
+        } else {
+            -2 as jlong
+        }
+    } else {
+        crate::ffi_error_return!("No entity exists")
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/utils.rs b/eucalyptus-core/src/utils.rs
index 5b47d69..77b6eda 100644
--- a/eucalyptus-core/src/utils.rs
+++ b/eucalyptus-core/src/utils.rs
@@ -367,6 +367,21 @@ macro_rules! convert_jstring {
     }};
 }
 
+/// A convenient macro for returning from a function when you cbb to add a specific return value.
+///
+/// # Usage
+/// ```
+/// use anyhow::anyhow;
+///
+/// fn some_native_function() -> i32 {
+///     let error_value = anyhow!("This is an error. Uh oh!");
+///     let Ok(val) = error_value else {
+///         return eucalyptus_core::ffi_error_return!();
+///         // eucalyptus_core::ffi_error_return!("Optional message")
+///         // this expands out to `return -1`
+///     }
+/// }
+/// ```
 #[macro_export]
 macro_rules! ffi_error_return {
     () => {{
@@ -411,7 +426,7 @@ macro_rules! ffi_error_return {
         impl ErrorValue for u8 {
             fn error_value() -> Self {
                 0
-            } // most likely a char or a jboolean
+            }
         }
 
         impl ErrorValue for u16 {
@@ -462,10 +477,25 @@ macro_rules! ffi_error_return {
             }
         }
 
-        // todo: implement other types
-
         ErrorValue::error_value()
     }};
+
+    ($($arg:tt)*) => {{
+        println!(
+            "[{}] [ERROR] {}",
+            {
+                fn type_name_of<T>(_: T) -> &'static str {
+                    std::any::type_name::<T>()
+                }
+                type_name_of(|| {})
+                    .rsplit("::")
+                    .find(|s| !s.starts_with("{{"))
+                    .unwrap_or("unknown")
+            },
+            format!($($arg)*)
+        );
+        $crate::ffi_error_return!()
+    }};
 }
 
 /// Converts a jlong to a hecs::Entity with automatic error handling.
diff --git a/src/commonMain/kotlin/com/dropbear/EntityRef.kt b/src/commonMain/kotlin/com/dropbear/EntityRef.kt
index 61752f8..29a909d 100644
--- a/src/commonMain/kotlin/com/dropbear/EntityRef.kt
+++ b/src/commonMain/kotlin/com/dropbear/EntityRef.kt
@@ -13,7 +13,8 @@ import com.dropbear.math.Transform
  * The ECS system the dropbear engine uses is `hecs` ECS, which is a Rust crate that has blazing fast
  * querying systems. The id passed is just a primitive integer value that points to the entity in the world.
  *
- * @property id The unique identifier of the entity as set by `hecs::World`
+ * @property id The unique identifier of the entity as set by `hecs::World`. This value changes dynamically during different
+ *              playthroughs, so it is recommended not to store this value.
  */
 class EntityRef(val id: EntityId = EntityId(0L)) {
     lateinit var engine: DropbearEngine
@@ -165,4 +166,50 @@ class EntityRef(val id: EntityId = EntityId(0L)) {
     fun setTextureOverride(materialName: String, textureHandle: TextureHandle) {
         engine.native.setTextureOverride(id.id, materialName, textureHandle)
     }
+
+    /**
+     * Fetches all direct children available to that entity. It does not go any deeper than that level.
+     *
+     * It will return `null` if there was an error, or an empty array if no children have been found.
+     *
+     * # Example
+     * ```
+     * |- cat
+     * |    |- wizard_hat
+     * |    |    |- pom_pom
+     * ```
+     *
+     * By running [getChildren] on `cat`, it will return `[ wizard_hat ]`, not `pom_pom`.
+     */
+    fun getChildren(): Array<EntityRef>? {
+        return engine.native.getChildren(id)
+    }
+
+    /**
+     * Fetches a direct child by a specific label.
+     *
+     * Returns `null` if an error occurred or no child exists, otherwise the entity.
+     */
+    fun getChildByLabel(label: String): EntityRef? {
+        return engine.native.getChildByLabel(id, label)
+    }
+
+    /**
+     * Fetches the parent of this entity.
+     *
+     * Returns `null` if no parent exists. If it exists, it will return the [EntityRef] of that parent.
+     *
+     * # Note
+     * You will see in the editor something like this:
+     * ```
+     * Scene_name
+     * |- cat
+     * |- bat
+     * ```
+     *
+     * Calling [getParent] on `cat` will return `null`, as the Scene is not an entity.
+     */
+    fun getParent(): EntityRef? {
+        return engine.native.getParent(id)
+    }
 }
\ No newline at end of file
diff --git a/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt b/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
index 02ac201..b242e85 100644
--- a/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
+++ b/src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
@@ -2,6 +2,7 @@ package com.dropbear.ffi
 
 import com.dropbear.Camera
 import com.dropbear.EntityId
+import com.dropbear.EntityRef
 import com.dropbear.EntityTransform
 import com.dropbear.asset.AssetHandle
 import com.dropbear.asset.ModelHandle
@@ -41,6 +42,10 @@ expect class NativeEngine {
     fun propagateTransform(entityId: EntityId): Transform?
     fun setTransform(entityId: EntityId, transform: EntityTransform)
 
+    fun getChildren(entityId: EntityId): Array<EntityRef>?
+    fun getChildByLabel(entityId: EntityId, label: String): EntityRef?
+    fun getParent(entityId: EntityId): EntityRef? 
+
     // ------------------------ MODEL PROPERTIES -------------------------
 
     fun getStringProperty(entityHandle: Long, label: String): String?
diff --git a/src/jvmMain/java/com/dropbear/ffi/JNINative.java b/src/jvmMain/java/com/dropbear/ffi/JNINative.java
index 0a0abcf..1b921a6 100644
--- a/src/jvmMain/java/com/dropbear/ffi/JNINative.java
+++ b/src/jvmMain/java/com/dropbear/ffi/JNINative.java
@@ -41,6 +41,11 @@ public class JNINative {
     public static native Transform propagateTransform(long worldHandle, long id);
     public static native void setTransform(long worldHandle, long id, EntityTransform transform);
 
+    // hierarchy
+    public static native long[] getChildren(long worldHandle, long entityId);
+    public static native long getChildByLabel(long worldHandle, long entityId, String label);
+    public static native long getParent(long worldHandle, long entityId);
+
     // properties
     public static native String getStringProperty(long worldHandle, long entityHandle, String label);
     public static native int getIntProperty(long worldHandle, long entityHandle, String label);
diff --git a/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt b/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
index 7d38a80..ce5d6dc 100644
--- a/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
+++ b/src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
@@ -3,6 +3,7 @@ package com.dropbear.ffi
 import com.dropbear.Camera
 import com.dropbear.DropbearEngine
 import com.dropbear.EntityId
+import com.dropbear.EntityRef
 import com.dropbear.EntityTransform
 import com.dropbear.asset.TextureHandle
 import com.dropbear.exception.DropbearNativeException
@@ -324,4 +325,53 @@ actual class NativeEngine {
     actual fun getAllTextures(entityHandle: Long): Array<String> {
         return JNINative.getAllTextures(worldHandle, entityHandle) ?: emptyArray()
     }
+
+    actual fun getChildren(entityId: EntityId): Array<EntityRef>? {
+        val result = JNINative.getChildren(worldHandle, entityId.id)
+        // i shouldn't expect it to return null unless an error, otherwise it must
+        // return an empty array
+        if (result == null) {
+            if (exceptionOnError) {
+                throw DropbearNativeException("Unable to query for all children for entity ${entityId.id}")
+            } else {
+                return null
+            }
+        } else {
+            val entityRefs = mutableListOf<EntityRef>()
+            result.forEach { e ->
+                entityRefs.add(EntityRef(EntityId(e)))
+            }
+            return entityRefs.toTypedArray() // must be an array so it cannot be mutated
+        }
+    }
+
+    actual fun getChildByLabel(entityId: EntityId, label: String): EntityRef? {
+        val result = JNINative.getChildByLabel(worldHandle, entityId.id, label)
+        return if (result == -1L) {
+            if (exceptionOnError) {
+                throw DropbearNativeException("Unable to get child by label $entityId $label")
+            } else {
+                null
+            }
+        } else if (result == -2L) {
+            null
+        } else {
+            EntityRef(EntityId(result))
+        }
+    }
+
+    actual fun getParent(entityId: EntityId): EntityRef? {
+        val result = JNINative.getParent(worldHandle, entityId.id)
+        return if (result == -1L) {
+            if (exceptionOnError) {
+                throw DropbearNativeException("Unable to get parent of entity $entityId")
+            } else {
+                null
+            }
+        } else if (result == -2L) {
+            null
+        } else {
+            EntityRef(EntityId(result))
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
index bdc7cc1..7eee108 100644
--- a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
+++ b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
@@ -9,6 +9,7 @@ package com.dropbear.ffi
 
 import com.dropbear.Camera
 import com.dropbear.EntityId
+import com.dropbear.EntityRef
 import com.dropbear.EntityTransform
 import com.dropbear.asset.TextureHandle
 import com.dropbear.exception.DropbearNativeException
@@ -807,6 +808,7 @@ actual class NativeEngine {
     }
 
     actual fun setModel(entityHandle: Long, modelHandle: Long) {
+        TODO("Not yet implemented")
     }
 
     actual fun getTexture(entityHandle: Long, name: String): Long? {
@@ -834,6 +836,7 @@ actual class NativeEngine {
     }
 
     actual fun setTextureOverride(entityHandle: Long, oldMaterialName: String, newTextureHandle: TextureHandle) {
+        TODO("Not yet implemented")
     }
 
     actual fun getTextureName(textureHandle: Long): String? {
@@ -843,4 +846,16 @@ actual class NativeEngine {
     actual fun getAllTextures(entityHandle: Long): Array<String> {
         TODO("Not yet implemented")
     }
+
+    actual fun getChildren(entityId: EntityId): Array<EntityRef>? {
+        TODO("Not yet implemented")
+    }
+
+    actual fun getChildByLabel(entityId: EntityId, label: String): EntityRef? {
+        TODO("Not yet implemented")
+    }
+
+    actual fun getParent(entityId: EntityId): EntityRef? {
+        TODO("Not yet implemented")
+    }
 }