tirbofish/dropbear · commit
a591388d497bbc26838ef4c1f34796e5977aef24
material and model API (such as comparison and swapping)
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -108,7 +108,7 @@ kotlin { sourceSets { commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") + api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") } } nativeMain { @@ -1,4 +1,7 @@ -use std::sync::{Arc, atomic::{AtomicU64, Ordering}, LazyLock}; +use std::sync::{ + Arc, LazyLock, + atomic::{AtomicU64, Ordering}, +}; use dashmap::DashMap; @@ -15,7 +18,9 @@ impl AssetHandle { /// /// This function does not guarantee if the raw value exists in the registry. /// You will have to check yourself. - pub fn new(raw: u64) -> Self { Self(raw) } + pub fn new(raw: impl Into<u64>) -> Self { + Self(raw.into()) + } /// Returns the raw/primitive [`u64`] value. pub fn raw(&self) -> u64 { self.0 @@ -88,10 +93,12 @@ impl AssetRegistry { } } + /// Adds a pointer to the asset registry. pub fn add_pointer(&self, pointer_kind: PointerKind, pointer: usize) { self.pointers.insert(pointer_kind, pointer); } + /// Attempts to fetch a pointer from the [`AssetRegistry`] by its given [`PointerKind`] pub fn get_pointer(&self, pointer_kind: PointerKind) -> Option<usize> { self.pointers.get(&pointer_kind).map(|entry| *entry.value()) } @@ -128,20 +135,36 @@ impl AssetRegistry { /// Iterates through all models, allowing you to iterate through all items in the /// model registry. pub fn iter_model(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> { + self.iter_model_raw() + } + + pub fn iter_model_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> { self.models.iter() } pub fn iter_material(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> { + self.iter_material_raw() + } + + pub fn iter_material_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> { self.materials.iter() } /// Returns the cached model handle if it exists. pub fn model_handle(&self, reference: &ResourceReference) -> Option<AssetHandle> { + self.model_handle_raw(reference) + } + + pub fn model_handle_raw(&self, reference: &ResourceReference) -> Option<AssetHandle> { self.model_handles.get(reference).map(|entry| *entry) } /// Fetches a model by handle. pub fn get_model(&self, handle: AssetHandle) -> Option<Arc<Model>> { + self.get_model_raw(handle) + } + + pub fn get_model_raw(&self, handle: AssetHandle) -> Option<Arc<Model>> { self.models .get(&handle) .map(|entry| Arc::clone(entry.value())) @@ -149,6 +172,10 @@ impl AssetRegistry { /// Fetches a material by handle. pub fn get_material(&self, handle: AssetHandle) -> Option<Arc<Material>> { + self.get_material_raw(handle) + } + + pub fn get_material_raw(&self, handle: AssetHandle) -> Option<Arc<Material>> { self.materials .get(&handle) .map(|entry| Arc::clone(entry.value())) @@ -156,6 +183,10 @@ impl AssetRegistry { /// Fetches a mesh by handle. pub fn get_mesh(&self, handle: AssetHandle) -> Option<Arc<Mesh>> { + self.get_mesh_raw(handle) + } + + pub fn get_mesh_raw(&self, handle: AssetHandle) -> Option<Arc<Mesh>> { self.meshes .get(&handle) .map(|entry| Arc::clone(entry.value())) @@ -245,6 +276,10 @@ impl AssetRegistry { /// Returns the owning model ID for the given material handle. pub fn material_owner(&self, handle: AssetHandle) -> Option<ModelId> { + self.material_owner_raw(handle) + } + + pub fn material_owner_raw(&self, handle: AssetHandle) -> Option<ModelId> { self.material_owners.get(&handle).map(|entry| *entry) } @@ -255,6 +290,13 @@ impl AssetRegistry { /// Returns the synthetic `ResourceReference` associated with a material handle. pub fn material_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> { + self.material_reference_for_handle_raw(handle) + } + + pub fn material_reference_for_handle_raw( + &self, + handle: AssetHandle, + ) -> Option<ResourceReference> { self.material_references .get(&handle) .map(|entry| entry.value().clone()) @@ -262,6 +304,10 @@ impl AssetRegistry { /// Returns the synthetic `ResourceReference` associated with a mesh handle. pub fn mesh_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> { + self.mesh_reference_for_handle_raw(handle) + } + + pub fn mesh_reference_for_handle_raw(&self, handle: AssetHandle) -> Option<ResourceReference> { self.mesh_references .get(&handle) .map(|entry| entry.value().clone()) @@ -272,6 +318,13 @@ impl AssetRegistry { &self, reference: &ResourceReference, ) -> Option<AssetHandle> { + self.material_handle_from_reference_raw(reference) + } + + pub fn material_handle_from_reference_raw( + &self, + reference: &ResourceReference, + ) -> Option<AssetHandle> { self.material_reference_lookup .get(reference) .map(|entry| *entry) @@ -279,6 +332,13 @@ impl AssetRegistry { /// Attempts to resolve a mesh handle from its synthetic `ResourceReference`. pub fn mesh_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> { + self.mesh_handle_from_reference_raw(reference) + } + + pub fn mesh_handle_from_reference_raw( + &self, + reference: &ResourceReference, + ) -> Option<AssetHandle> { self.mesh_reference_lookup .get(reference) .map(|entry| *entry) @@ -300,7 +360,10 @@ impl AssetRegistry { self.material_owners.insert(handle, model_id); - if let Some(reference) = material_reference(model_id, &name) { + let reference = material_reference_from_model(model.as_ref(), &name) + .or_else(|| material_reference_fallback(model_id, &name)); + + if let Some(reference) = reference { self.material_references.insert(handle, reference.clone()); self.material_reference_lookup.insert(reference, handle); } @@ -339,7 +402,21 @@ impl Default for AssetRegistry { pub static ASSET_REGISTRY: LazyLock<AssetRegistry> = LazyLock::new(AssetRegistry::new); -fn material_reference(model_id: ModelId, name: &str) -> Option<ResourceReference> { +fn material_reference_from_model(model: &Model, name: &str) -> Option<ResourceReference> { + let base_uri = model.path.as_uri()?; + let material_component = sanitize_material_component(name); + + if material_component.is_empty() { + return None; + } + + let base = base_uri.trim_end_matches('/'); + let combined = format!("{}/{}", base, material_component); + + ResourceReference::from_euca_uri(combined).ok() +} + +fn material_reference_fallback(model_id: ModelId, name: &str) -> Option<ResourceReference> { resource_reference_for("materials", model_id, name) } @@ -360,6 +437,21 @@ fn resource_reference_for( ResourceReference::from_euca_uri(uri).ok() } +fn sanitize_material_component(input: &str) -> String { + let trimmed = input.trim(); + if trimmed.is_empty() { + return String::new(); + } + + trimmed + .chars() + .map(|ch| match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch, + _ => '_', + }) + .collect() +} + fn sanitize_component(input: &str) -> String { let trimmed = input.trim(); if trimmed.is_empty() { @@ -1,12 +1,16 @@ use glam::{DMat4, DQuat, DVec3, Mat4}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::Path, sync::Arc}; +use std::{ + collections::HashMap, + path::Path, + sync::{Arc, LazyLock}, +}; use crate::{ - asset::{AssetRegistry, AssetHandle, AssetKind, ASSET_REGISTRY}, + asset::{ASSET_REGISTRY, AssetHandle, AssetKind, AssetRegistry}, graphics::{Instance, SharedGraphicsContext, Texture}, - model::{LoadedModel, Model, ModelId, MODEL_CACHE}, + model::{LoadedModel, MODEL_CACHE, Model, ModelId}, utils::ResourceReference, }; use anyhow::anyhow; @@ -160,6 +164,10 @@ impl MeshRenderer { /// Swaps the currently loaded model for that renderer by the provided [`LoadedModel`] pub fn set_handle(&mut self, handle: LoadedModel) { + self.set_handle_raw(handle); + } + + pub fn set_handle_raw(&mut self, handle: LoadedModel) { self.handle = handle; self.material_overrides.clear(); self.original_material_snapshots.clear(); @@ -187,18 +195,20 @@ impl MeshRenderer { .get_model(handle) .ok_or_else(|| anyhow!("Model handle {} not found", handle.raw()))?; - self.set_handle(LoadedModel::from_registered(handle, model)); - self.material_overrides.clear(); - self.original_material_snapshots.clear(); + self.set_handle_raw(LoadedModel::from_registered(handle, model)); Ok(()) } - - /// Swaps the loaded model to a different one by using an AssetHandle. - /// - /// The main difference between [`MeshRenderer::set_asset_handle`] and + + /// Swaps the loaded model to a different one by using an AssetHandle. + /// + /// The main difference between [`MeshRenderer::set_asset_handle`] and /// [`MeshRenderer::set_asset_handle_raw`] is that it does not use any static variables - /// (like [`ASSET_REGISTRY`]), instead allowing for the registry to be manually provided. - pub fn set_asset_handle_raw(&mut self, registry: &AssetRegistry, handle: AssetHandle) -> anyhow::Result<()> { + /// (like [`ASSET_REGISTRY`]), instead allowing for the registry to be manually provided. + pub fn set_asset_handle_raw( + &mut self, + registry: &AssetRegistry, + handle: AssetHandle, + ) -> anyhow::Result<()> { if !registry.contains_handle(handle) { return Err(anyhow!( "Asset handle {} is not registered with the asset registry", @@ -217,9 +227,7 @@ impl MeshRenderer { .get_model(handle) .ok_or_else(|| anyhow!("Model handle {} not found", handle.raw()))?; - self.set_handle(LoadedModel::from_registered(handle, model)); - self.material_overrides.clear(); - self.original_material_snapshots.clear(); + self.set_handle_raw(LoadedModel::from_registered(handle, model)); Ok(()) } @@ -240,11 +248,27 @@ impl MeshRenderer { } pub fn material_handle(&self, material_name: &str) -> Option<AssetHandle> { - ASSET_REGISTRY.material_handle(self.model_id(), material_name) + self.material_handle_raw(&ASSET_REGISTRY, material_name) + } + + pub fn material_handle_raw( + &self, + registry: &AssetRegistry, + material_name: &str, + ) -> Option<AssetHandle> { + registry.material_handle(self.model_id(), material_name) } pub fn mesh_handle(&self, mesh_name: &str) -> Option<AssetHandle> { - ASSET_REGISTRY.mesh_handle(self.model_id(), mesh_name) + self.mesh_handle_raw(&ASSET_REGISTRY, mesh_name) + } + + pub fn mesh_handle_raw( + &self, + registry: &AssetRegistry, + mesh_name: &str, + ) -> Option<AssetHandle> { + registry.mesh_handle(self.model_id(), mesh_name) } pub fn apply_material_override( @@ -255,7 +279,7 @@ impl MeshRenderer { ) -> anyhow::Result<()> { self.apply_material_override_raw( &ASSET_REGISTRY, - &*MODEL_CACHE, + LazyLock::force(&MODEL_CACHE), target_material, source_model, source_material, @@ -357,10 +381,7 @@ impl MeshRenderer { // ensure downstream caches observe the newly applied material state self.handle.refresh_registry_raw(registry); - { - let mut cache = model_cache.lock(); - self.refresh_model_cache_raw(&mut cache); - } + self.refresh_model_cache_with(model_cache); Ok(()) } @@ -375,6 +396,19 @@ impl MeshRenderer { } pub fn restore_original_material(&mut self, target_material: &str) -> anyhow::Result<()> { + self.restore_original_material_raw( + target_material, + &ASSET_REGISTRY, + LazyLock::force(&MODEL_CACHE), + ) + } + + pub fn restore_original_material_raw( + &mut self, + target_material: &str, + registry: &AssetRegistry, + model_cache: &Mutex<HashMap<String, Arc<Model>>>, + ) -> anyhow::Result<()> { let snapshot = self .original_material_snapshots .get(target_material) @@ -400,21 +434,20 @@ impl MeshRenderer { let _ = model.clear_material_texture_tag(target_material); } - self.original_material_snapshots - .remove(target_material); + self.original_material_snapshots.remove(target_material); } - self.handle.refresh_registry(); - self.refresh_model_cache(); + self.handle.refresh_registry_raw(registry); + self.refresh_model_cache_with(model_cache); Ok(()) } - fn refresh_model_cache(&self) { - let mut cache = MODEL_CACHE.lock(); - self.refresh_model_cache_raw(&mut cache); + fn refresh_model_cache_with(&self, cache: &Mutex<HashMap<String, Arc<Model>>>) { + let mut guard = cache.lock(); + self.refresh_model_cache_raw(&mut guard); } - + fn refresh_model_cache_raw(&self, cache: &mut HashMap<String, Arc<Model>>) { let current = self.handle.get(); let keys: Vec<String> = cache @@ -1,3 +1,4 @@ +use crate::asset::AssetRegistry; use crate::{ asset::{ASSET_REGISTRY, AssetHandle}, graphics::{SharedGraphicsContext, Texture}, @@ -13,12 +14,11 @@ use std::sync::{Arc, LazyLock}; use std::time::Instant; use std::{mem, ops::Range, path::PathBuf}; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt}; -use crate::asset::AssetRegistry; pub const GREY_TEXTURE_BYTES: &[u8] = include_bytes!("../../resources/textures/grey.png"); - -pub static MODEL_CACHE: LazyLock<Mutex<HashMap<String, Arc<Model>>>> = LazyLock::new(|| Mutex::new(HashMap::new())); +pub static MODEL_CACHE: LazyLock<Mutex<HashMap<String, Arc<Model>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ModelId(pub u64); @@ -46,8 +46,12 @@ pub struct LoadedModel { impl LoadedModel { pub fn new(inner: Arc<Model>) -> Self { + Self::new_raw(&ASSET_REGISTRY, inner) + } + + pub fn new_raw(registry: &AssetRegistry, inner: Arc<Model>) -> Self { let reference = inner.path.clone(); - let handle = ASSET_REGISTRY.register_model(reference, Arc::clone(&inner)); + let handle = registry.register_model(reference, Arc::clone(&inner)); Self { inner, handle } } @@ -62,9 +66,7 @@ impl LoadedModel { } pub fn from_asset_handle(handle: AssetHandle) -> Option<Arc<LoadedModel>> { - ASSET_REGISTRY - .get_model(handle) - .map(|model| Arc::new(Self::from_registered(handle, model))) + Self::from_asset_handle_raw(&ASSET_REGISTRY, handle).map(|model| Arc::new(model)) } /// Returns the unique identifier of the underlying model asset. @@ -94,9 +96,7 @@ impl LoadedModel { /// Re-registers the model with the global asset registry, ensuring cached /// sub-assets stay in sync after mutations. pub fn refresh_registry(&mut self) { - let reference = self.inner.path.clone(); - let updated_handle = ASSET_REGISTRY.register_model(reference, self.get()); - self.handle = updated_handle; + self.refresh_registry_raw(&ASSET_REGISTRY); } pub fn refresh_registry_raw(&mut self, registry: &AssetRegistry) { @@ -106,11 +106,28 @@ impl LoadedModel { } pub fn contains_material_handle(&self, handle: AssetHandle) -> bool { - self.inner.contains_material_handle(handle) + self.contains_material_handle_raw(&ASSET_REGISTRY, handle) + } + + pub fn contains_material_handle_raw( + &self, + registry: &AssetRegistry, + handle: AssetHandle, + ) -> bool { + self.inner.contains_material_handle_raw(registry, handle) } pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool { - self.inner.contains_material_reference(reference) + self.contains_material_reference_raw(&ASSET_REGISTRY, reference) + } + + pub fn contains_material_reference_raw( + &self, + registry: &AssetRegistry, + reference: &ResourceReference, + ) -> bool { + self.inner + .contains_material_reference_raw(registry, reference) } } @@ -187,7 +204,11 @@ impl Model { /// Returns the registered asset handle for this model, if available. pub fn asset_handle(&self) -> Option<AssetHandle> { - ASSET_REGISTRY.model_handle_from_reference(&self.path) + self.asset_handle_raw(&ASSET_REGISTRY) + } + + pub fn asset_handle_raw(&self, registry: &AssetRegistry) -> Option<AssetHandle> { + registry.model_handle_from_reference(&self.path) } /// Returns `true` if this model was loaded from the specified resource reference. @@ -197,15 +218,32 @@ impl Model { /// Returns `true` if this model owns the supplied material handle. pub fn contains_material_handle(&self, material_handle: AssetHandle) -> bool { - ASSET_REGISTRY - .material_owner(material_handle) == Some(self.id) + self.contains_material_handle_raw(&ASSET_REGISTRY, material_handle) + } + + pub fn contains_material_handle_raw( + &self, + registry: &AssetRegistry, + material_handle: AssetHandle, + ) -> bool { + registry.material_owner(material_handle) == Some(self.id) } /// Returns `true` if this model owns a material registered under the provided resource reference. pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool { - ASSET_REGISTRY + self.contains_material_reference_raw(&ASSET_REGISTRY, reference) + } + + pub fn contains_material_reference_raw( + &self, + registry: &AssetRegistry, + reference: &ResourceReference, + ) -> bool { + registry .material_handle_from_reference(reference) - .map_or(false, |handle| self.contains_material_handle(handle)) + .map_or(false, |handle| { + self.contains_material_handle_raw(registry, handle) + }) } /// Returns `true` if any material on this model is tagged with `texture_tag`. @@ -220,22 +258,49 @@ impl Model { self.materials .iter() .find(|mat| mat.name == material_name) - .and_then(|mat| mat.texture_tag.as_deref()) == Some(texture_tag) + .and_then(|mat| mat.texture_tag.as_deref()) + == Some(texture_tag) } - pub async fn load_from_memory( + pub async fn load_from_memory<B>( graphics: Arc<SharedGraphicsContext>, - buffer: impl AsRef<[u8]>, + buffer: B, label: Option<&str>, - ) -> anyhow::Result<LoadedModel> { + ) -> anyhow::Result<LoadedModel> + where + B: AsRef<[u8]>, + { + Self::load_from_memory_raw( + graphics, + buffer, + label, + &ASSET_REGISTRY, + LazyLock::force(&MODEL_CACHE), + ) + .await + } + + pub async fn load_from_memory_raw<B>( + graphics: Arc<SharedGraphicsContext>, + buffer: B, + label: Option<&str>, + registry: &AssetRegistry, + cache: &Mutex<HashMap<String, Arc<Model>>>, + ) -> anyhow::Result<LoadedModel> + where + B: AsRef<[u8]>, + { let start = Instant::now(); let mut hasher = DefaultHasher::new(); let cache_key = label.unwrap_or("default").to_string(); - if let Some(cached_model) = MODEL_CACHE.lock().get(&cache_key) { + if let Some(cached_model) = { + let cache_guard = cache.lock(); + cache_guard.get(&cache_key).cloned() + } { log::debug!("Model loaded from memory cache: {:?}", cache_key); - return Ok(LoadedModel::new(cached_model.clone())); + return Ok(LoadedModel::new_raw(registry, cached_model)); } log::trace!( @@ -416,9 +481,12 @@ impl Model { id: ModelId(hasher.finish()), }); - let loaded = LoadedModel::new(Arc::clone(&model)); + let loaded = LoadedModel::new_raw(registry, Arc::clone(&model)); - MODEL_CACHE.lock().insert(cache_key.clone(), model); + { + let mut cache_guard = cache.lock(); + cache_guard.insert(cache_key.clone(), model); + } log::trace!("==================== DONE ===================="); log::debug!("Model cached from memory: {:?}", label); log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label); @@ -431,22 +499,42 @@ impl Model { path: &PathBuf, label: Option<&str>, ) -> anyhow::Result<LoadedModel> { + Self::load_raw( + graphics, + path, + label, + &ASSET_REGISTRY, + LazyLock::force(&MODEL_CACHE), + ) + .await + } + + pub async fn load_raw( + graphics: Arc<SharedGraphicsContext>, + path: &PathBuf, + label: Option<&str>, + registry: &AssetRegistry, + cache: &Mutex<HashMap<String, Arc<Model>>>, + ) -> anyhow::Result<LoadedModel> { let file_name = path.file_name(); log::debug!("Loading model [{:?}]", file_name); let path_str = path.to_string_lossy().to_string(); log::debug!("Checking if model exists in cache"); - if let Some(cached_model) = MODEL_CACHE.lock().get(&path_str) { + if let Some(cached_model) = { + let cache_guard = cache.lock(); + cache_guard.get(&path_str).cloned() + } { log::debug!("Model loaded from cache: {:?}", path_str); - return Ok(LoadedModel::new(cached_model.clone())); + return Ok(LoadedModel::new_raw(registry, cached_model)); } log::debug!("Model does not exist in cache, loading memory..."); log::debug!("Path of model: {}", path.display()); let buffer = std::fs::read(path)?; - let loaded = Self::load_from_memory(graphics, buffer, label).await?; + let loaded = Self::load_from_memory_raw(graphics, buffer, label, registry, cache).await?; let mut model_clone: Model = (*loaded).clone(); if let Ok(reference) = ResourceReference::from_path(path) { @@ -458,15 +546,15 @@ impl Model { let updated = Arc::new(model_clone); { - let mut cache = MODEL_CACHE.lock(); - cache.insert(path_str, Arc::clone(&updated)); + let mut cache_guard = cache.lock(); + cache_guard.insert(path_str.clone(), Arc::clone(&updated)); if let Some(custom_label) = label { - cache.insert(custom_label.to_string(), Arc::clone(&updated)); + cache_guard.insert(custom_label.to_string(), Arc::clone(&updated)); } } log::debug!("Model cached and loaded: {:?}", file_name); - Ok(LoadedModel::new(updated)) + Ok(LoadedModel::new_raw(registry, updated)) } } @@ -3,12 +3,15 @@ //! Inspiration taken from `https://github.com/4tkbytes/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, //! my old game engine made in C sharp, where this is the plane "algorithm". +use crate::asset::{ASSET_REGISTRY, AssetRegistry}; use crate::entity::MeshRenderer; use crate::graphics::{SharedGraphicsContext, Texture}; use crate::model::{LoadedModel, MODEL_CACHE, Material, Mesh, Model, ModelId, ModelVertex}; use crate::utils::{ResourceReference, ResourceReferenceType}; +use parking_lot::Mutex; +use std::collections::HashMap; use std::hash::{DefaultHasher, Hash, Hasher}; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use wgpu::{AddressMode, util::DeviceExt}; /// Creates a plane wrapped in a [`MeshRenderer`]. @@ -48,10 +51,28 @@ impl PlaneBuilder { } pub async fn build( + self, + graphics: Arc<SharedGraphicsContext>, + texture_bytes: &[u8], + label: Option<&str>, + ) -> anyhow::Result<MeshRenderer> { + self.build_raw( + graphics, + texture_bytes, + label, + &ASSET_REGISTRY, + LazyLock::force(&MODEL_CACHE), + ) + .await + } + + pub async fn build_raw( mut self, graphics: Arc<SharedGraphicsContext>, texture_bytes: &[u8], label: Option<&str>, + registry: &AssetRegistry, + cache: &Mutex<HashMap<String, Arc<Model>>>, ) -> anyhow::Result<MeshRenderer> { let label = if let Some(label) = label { label.to_string() @@ -98,9 +119,12 @@ impl PlaneBuilder { let hash = hasher.finish(); - if let Some(cached_model) = MODEL_CACHE.lock().get(&label) { + if let Some(cached_model) = { + let cache_guard = cache.lock(); + cache_guard.get(&label).cloned() + } { log::debug!("Model loaded from cache: {:?}", label); - let handle = LoadedModel::new(Arc::clone(cached_model)); + let handle = LoadedModel::new_raw(registry, cached_model); return Ok(MeshRenderer::from_handle(handle)); } @@ -145,9 +169,12 @@ impl PlaneBuilder { id: ModelId(hash), }); - MODEL_CACHE.lock().insert(label, Arc::clone(&model)); + { + let mut cache_guard = cache.lock(); + cache_guard.insert(label.clone(), Arc::clone(&model)); + } - let handle = LoadedModel::new(model); + let handle = LoadedModel::new_raw(registry, model); Ok(MeshRenderer::from_handle(handle)) } } @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::path::Path; -pub const euca_SCHEME: &str = "euca://"; +pub const EUCA_SCHEME: &str = "euca://"; /// Converts any supported resource reference into the canonical `euca://` form. /// @@ -24,7 +24,7 @@ pub fn canonicalize_euca_uri(uri: &str) -> anyhow::Result<String> { } let normalized = trimmed.replace('\\', "/"); - let (had_scheme, without_scheme) = if let Some(rest) = normalized.strip_prefix(euca_SCHEME) { + let (had_scheme, without_scheme) = if let Some(rest) = normalized.strip_prefix(EUCA_SCHEME) { (true, rest) } else { (false, normalized.as_str()) @@ -49,16 +49,16 @@ pub fn canonicalize_euca_uri(uri: &str) -> anyhow::Result<String> { log::debug!( "Canonicalized legacy resource reference '{}' to '{}{}'", uri, - euca_SCHEME, + EUCA_SCHEME, clean ); } - Ok(format!("{euca_SCHEME}{clean}")) + Ok(format!("{EUCA_SCHEME}{clean}")) } pub fn relative_path_from_euca<'a>(uri: &'a str) -> anyhow::Result<&'a str> { - let without_scheme = uri.strip_prefix(euca_SCHEME).unwrap_or(uri); + let without_scheme = uri.strip_prefix(EUCA_SCHEME).unwrap_or(uri); let stripped = without_scheme.trim_start_matches('/'); if stripped.is_empty() { @@ -224,7 +224,7 @@ impl ResourceReference { .collect::<Vec<_>>() .join("/"); - let canonical = canonicalize_euca_uri(&format!("{euca_SCHEME}{resource_path}"))?; + let canonical = canonicalize_euca_uri(&format!("{EUCA_SCHEME}{resource_path}"))?; return Ok(Self { ref_type: ResourceReferenceType::File(canonical), @@ -1,8 +1,8 @@ use crate::input::InputState; use crate::window::GraphicsCommand; use crossbeam_channel::Sender; -use hecs::World; use dropbear_engine::asset::AssetRegistry; +use hecs::World; pub type WorldPtr = *mut World; pub type InputStatePtr = *mut InputState; @@ -8,12 +8,12 @@ use crate::scripting::native::NativeLibrary; use crate::states::ScriptComponent; use anyhow::Context; use crossbeam_channel::Sender; +use dropbear_engine::asset::ASSET_REGISTRY; use hecs::{Entity, World}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; -use dropbear_engine::asset::{ASSET_REGISTRY}; /// The target of the script. This can be either a JVM or a native library. #[derive(Default, Clone)] @@ -1,6 +1,7 @@ #![allow(non_snake_case)] //! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate +pub mod error; pub mod exception; pub mod exports; pub mod utils; @@ -0,0 +1,51 @@ +use parking_lot::Mutex; +use std::sync::LazyLock; + +/// Shared buffer holding the most recent JNI error message. +static LAST_ERROR_MESSAGE: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new())); + +fn buffer() -> &'static Mutex<String> { + LazyLock::force(&LAST_ERROR_MESSAGE) +} + +/// Returns a raw pointer to the shared error buffer so it can be cached on the +/// Kotlin side. The pointer is stable for the lifetime of the process. +pub fn get_last_error_message_ptr() -> *const Mutex<String> { + buffer() as *const Mutex<String> +} + +/// Provides direct access to the shared error buffer for callers that prefer a +/// reference over the raw pointer. +pub fn last_error_message_mutex() -> &'static Mutex<String> { + buffer() +} + +/// Replaces the stored error message with `message`. +pub fn set_last_error_message(message: impl AsRef<str>) { + let mut guard = buffer().lock(); + guard.clear(); + guard.push_str(message.as_ref()); +} + +/// Clears any stored error message. +pub fn clear_last_error_message() { + buffer().lock().clear(); +} + +/// Clones and returns the currently stored error message. +/// +/// The buffer contents remain untouched so callers can fetch the message multiple +/// times if necessary. +pub fn get_last_error_message() -> String { + buffer().lock().clone() +} + +/// Convenience helper for appending additional context onto the current +/// error message. +pub fn append_last_error_message(fragment: impl AsRef<str>) { + let mut guard = buffer().lock(); + if !guard.is_empty() { + guard.push('\n'); + } + guard.push_str(fragment.as_ref()); +} @@ -1,20 +1,32 @@ use crate::camera::{CameraComponent, CameraType}; use crate::ptr::{AssetRegistryPtr, GraphicsPtr, InputStatePtr, WorldPtr}; +use crate::scripting::jni::error::{ + clear_last_error_message, get_last_error_message, get_last_error_message_ptr, + set_last_error_message, +}; use crate::scripting::jni::utils::{ create_vector3, extract_vector3, java_button_to_rust, new_float_array, }; use crate::states::{Label, ModelProperties, Value}; use crate::utils::keycode_from_ordinal; use crate::window::{GraphicsCommand, WindowCommand}; +use crate::{convert_jstring, convert_ptr}; +use dropbear_engine::asset::PointerKind::Const; +use dropbear_engine::asset::{ASSET_REGISTRY, AssetHandle, AssetRegistry}; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{MeshRenderer, Transform}; +use dropbear_engine::model::Model; +use dropbear_engine::utils::ResourceReference; 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, jstring}; -use dropbear_engine::asset::{AssetHandle}; -use dropbear_engine::utils::ResourceReference; +use jni::sys::{ + JNI_FALSE, jboolean, jclass, jdouble, jfloatArray, jint, jlong, jobject, jobjectArray, jstring, +}; +use parking_lot::Mutex; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; /// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getEntity /// (JNIEnv *, jclass, jlong, jstring);` @@ -1726,11 +1738,15 @@ pub fn Java_com_dropbear_ffi_JNINative_getModel( let world = unsafe { &*world }; let entity = unsafe { world.find_entity_from_id(entity as u32) }; - if let Ok(mut q) = world.query_one::<&MeshRenderer>(entity) && let Some(model) = q.get() { + if let Ok(mut q) = world.query_one::<&MeshRenderer>(entity) + && let Some(model) = q.get() + { let handle = model.asset_handle(); handle.raw() as jlong } else { - println!("[Java_com_dropbear_ffi_JNINative_getModel] [ERROR] Unable to find entity in world"); + println!( + "[Java_com_dropbear_ffi_JNINative_getModel] [ERROR] Unable to find entity in world" + ); -1 } } @@ -1754,7 +1770,9 @@ pub fn Java_com_dropbear_ffi_JNINative_setModel( let asset = asset_handle as AssetRegistryPtr; if asset.is_null() { - println!("[Java_com_dropbear_ffi_JNINative_setModel] [ERROR] Asset registry pointer is null"); + println!( + "[Java_com_dropbear_ffi_JNINative_setModel] [ERROR] Asset registry pointer is null" + ); return; } @@ -1762,8 +1780,8 @@ pub fn Java_com_dropbear_ffi_JNINative_setModel( let asset = unsafe { &*asset }; let entity = unsafe { world.find_entity_from_id(entity as u32) }; - if let Ok(mut q) = world.query_one::<&mut MeshRenderer>(entity) && - let Some(model) = q.get() + if let Ok(mut q) = world.query_one::<&mut MeshRenderer>(entity) + && let Some(model) = q.get() { let asset_handle = AssetHandle::new(model_handle as u64); if !asset.contains_handle(asset_handle) { @@ -1771,18 +1789,68 @@ pub fn Java_com_dropbear_ffi_JNINative_setModel( return; } if let Err(e) = model.set_asset_handle_raw(asset, asset_handle) { - println!("[Java_com_dropbear_ffi_JNINative_setModel] [ERROR] Unable to set model: {}", e); + println!( + "[Java_com_dropbear_ffi_JNINative_setModel] [ERROR] Unable to set model: {}", + e + ); + } + } +} + +/// `JNIEXPORT jboolean JNICALL Java_com_dropbear_ffi_JNINative_isModelHandle +/// (JNIEnv *, jclass, jlong, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_isModelHandle( + _env: JNIEnv, + _class: JClass, + asset_handle: jlong, + model_id: jlong, +) -> jboolean { + let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); + + asset + .contains_handle(AssetHandle::new(model_id as u64)) + .into() +} + +/// `JNIEXPORT jboolean JNICALL Java_com_dropbear_ffi_JNINative_isUsingModel +/// (JNIEnv *, jclass, jlong, jlong, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_isUsingModel( + _env: JNIEnv, + _class: JClass, + world_handle: jlong, + entity: jlong, + model_handle: jlong, +) -> jboolean { + let world = convert_ptr!(world_handle, WorldPtr => World); + let entity = unsafe { world.find_entity_from_id(entity as u32) }; + + let handle = AssetHandle::new(model_handle as u64); + if let Ok(mut q) = world.query_one::<&MeshRenderer>(entity) + && let Some(model) = q.get() + { + if model.asset_handle() == handle { + true.into() + } else { + false.into() } + } else { + println!( + "[Java_com_dropbear_ffi_JNINative_isUsingModel] [ERROR] Unable to find entity in world" + ); + false.into() } } /// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getTexture -/// (JNIEnv *, jclass, jlong, jlong, jstring);` +/// (JNIEnv *, jclass, jlong, jlong, jlong, jstring);` #[unsafe(no_mangle)] pub fn Java_com_dropbear_ffi_JNINative_getTexture( mut env: JNIEnv, _class: JClass, world_handle: jlong, + asset_handle: jlong, entity: jlong, name: JString, ) -> jlong { @@ -1793,188 +1861,356 @@ pub fn Java_com_dropbear_ffi_JNINative_getTexture( } let world = unsafe { &*world }; + + let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); let entity = unsafe { world.find_entity_from_id(entity as u32) }; - if let Ok(mut q) = world.query_one::<&MeshRenderer>(entity) && let Some(mesh) = q.get() { - let jni_result = env.get_string(&name); - let str = match jni_result { - Ok(java_string) => match java_string.to_str() { - Ok(rust_str) => rust_str.to_string(), + if let Ok(mut q) = world.query_one::<&MeshRenderer>(entity) + && let Some(mesh) = q.get() + { + let str = convert_jstring!(env, name); + if let Some(handle) = mesh.material_handle_raw(asset, str.as_str()) { + handle.raw() as jlong + } else { + println!("[Java_com_dropbear_ffi_JNINative_getTexture] [ERROR] Invalid texture handle"); + 0 + } + } else { + println!( + "[Java_com_dropbear_ffi_JNINative_getTexture] [ERROR] Unable to find entity in world" + ); + 0 + } +} + +/// `JNIEXPORT jobjectArray JNICALL Java_com_dropbear_ffi_JNINative_getAllTextures +/// (JNIEnv *, jclass, jlong, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_getAllTextures( + mut env: JNIEnv, + _class: JClass, + world_handle: jlong, + entity: jlong, +) -> jobjectArray { + let world = convert_ptr!(world_handle, WorldPtr => World); + let entity = unsafe { world.find_entity_from_id(entity as u32) }; + + let mut query = match world.query_one::<&MeshRenderer>(entity) { + Ok(query) => query, + Err(e) => { + let message = format!( + "[Java_com_dropbear_ffi_JNINative_getAllTextures] [ERROR] Failed to query entity: {}", + e + ); + set_last_error_message(&message); + println!("{}", message); + return std::ptr::null_mut(); + } + }; + + let renderer = match query.get() { + Some(renderer) => renderer, + None => { + let message = "[Java_com_dropbear_ffi_JNINative_getAllTextures] [ERROR] Entity does not have a MeshRenderer component"; + set_last_error_message(message); + println!("{}", message); + return std::ptr::null_mut(); + } + }; + + let model = renderer.model(); + let model_id = renderer.model_id(); + + let mut seen = HashSet::new(); + let mut textures = Vec::new(); + + for material in &model.materials { + let reference = ASSET_REGISTRY + .material_handle(model_id, &material.name) + .and_then(|handle| ASSET_REGISTRY.material_reference_for_handle(handle)) + .and_then(|reference| reference.as_uri().map(|uri| uri.to_string())) + .or_else(|| material.texture_tag.clone()) + .unwrap_or_else(|| material.name.clone()); + + if seen.insert(reference.clone()) { + textures.push(reference); + } + } + + let string_class = match env.find_class("java/lang/String") { + Ok(class) => class, + Err(e) => { + let message = format!( + "[Java_com_dropbear_ffi_JNINative_getAllTextures] [ERROR] Failed to locate java/lang/String: {}", + e + ); + set_last_error_message(&message); + println!("{}", message); + return std::ptr::null_mut(); + } + }; + + let array = match env.new_object_array(textures.len() as i32, string_class, JObject::null()) { + Ok(array) => array, + Err(e) => { + let message = format!( + "[Java_com_dropbear_ffi_JNINative_getAllTextures] [ERROR] Failed to allocate string array: {}", + e + ); + set_last_error_message(&message); + println!("{}", message); + return std::ptr::null_mut(); + } + }; + + for (index, value) in textures.iter().enumerate() { + let java_string = match env.new_string(value) { + Ok(string) => string, + Err(e) => { + let message = format!( + "[Java_com_dropbear_ffi_JNINative_getAllTextures] [ERROR] Failed to create Java string: {}", + e + ); + set_last_error_message(&message); + println!("{}", message); + return std::ptr::null_mut(); + } + }; + + if let Err(e) = + env.set_object_array_element(&array, index as i32, JObject::from(java_string)) + { + let message = format!( + "[Java_com_dropbear_ffi_JNINative_getAllTextures] [ERROR] Failed to set array element: {}", + e + ); + set_last_error_message(&message); + println!("{}", message); + return std::ptr::null_mut(); + } + } + + clear_last_error_message(); + array.into_raw() +} + +/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_setTexture +/// (JNIEnv *, jclass, jlong, jlong, jlong, jstring, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_setTexture( + mut env: JNIEnv, + _class: JClass, + world_handle: jlong, + asset_handle: jlong, + entity: jlong, + old_material_name: JString, + new_texture_handle: jlong, +) { + let world = world_handle as WorldPtr; + if world.is_null() { + println!("[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] World pointer is null"); + return; + } + + let asset = asset_handle as AssetRegistryPtr; + if asset.is_null() { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Asset registry pointer is null" + ); + return; + } + + let asset = unsafe { &*asset }; + + let world = unsafe { &*world }; + let entity = unsafe { world.find_entity_from_id(entity as u32) }; + + match world.query_one::<&mut MeshRenderer>(entity) { + Ok(mut query) => { + let Some(renderer) = query.get() else { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Entity does not have a MeshRenderer component" + ); + return; + }; + + let Some(cache) = asset.get_pointer(Const("model_cache")) else { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Asset registry does not contain model cache" + ); + return; + }; + + let cache = cache as *const Mutex<HashMap<String, Arc<Model>>>; + let cache = unsafe { &*cache }; + + let jni_result = env.get_string(&old_material_name); + let target_material = match jni_result { + Ok(java_string) => match java_string.to_str() { + Ok(rust_str) => rust_str.to_string(), + Err(e) => { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Failed to convert Java string to Rust string: {}", + e + ); + return; + } + }, Err(e) => { println!( - "[Java_com_dropbear_ffi_JNINative_getTexture] [ERROR] Failed to convert Java string to Rust string: {}", + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Failed to get string from JNI: {}", e ); - return -1; + return; } - }, - Err(e) => { + }; + + let handle = AssetHandle::new(new_texture_handle as u64); + + if !asset.contains_handle(handle) { println!( - "[Java_com_dropbear_ffi_JNINative_getTexture] [ERROR] Failed to get string from JNI: {}", + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Invalid texture handle: {}", + new_texture_handle + ); + return; + } + + if !asset.is_material(handle) { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Handle {} does not refer to a material", + new_texture_handle + ); + return; + } + + let Some(material) = asset.get_material(handle) else { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Invalid texture handle" + ); + return; + }; + + let Some(owner_model_id) = asset.material_owner(handle) else { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to determine owning model for material handle {}", + new_texture_handle + ); + return; + }; + + let Some(owner_model_handle) = asset.model_handle_from_id(owner_model_id) else { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to resolve model handle for owner id {:?}", + owner_model_id + ); + return; + }; + + let Some(source_reference) = asset.model_reference_for_handle(owner_model_handle) + else { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to resolve model reference for handle {}", + owner_model_handle.raw() + ); + return; + }; + + let material_name = material.name.clone(); + + if let Err(e) = renderer.apply_material_override_raw( + asset, + cache, + target_material.as_str(), + source_reference, + material_name.as_str(), + ) { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Failed to apply material override: {}", e ); - return -1; } - }; - if let Some(handle) = mesh.material_handle(str.as_str()) { - handle.raw() as jlong - } else { - -1 } + Err(err) => { + println!( + "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to query MeshRenderer: {}", + err + ); + } + } +} + +/// `JNIEXPORT jstring JNICALL Java_com_dropbear_ffi_JNINative_getTextureName +/// (JNIEnv *, jclass, jlong, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_getTextureName( + env: JNIEnv, + _class: JClass, + asset_handle: jlong, + texture_id: jlong, +) -> jstring { + let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); + + let texture_id = AssetHandle::new(texture_id as u64); + asset.get_material(texture_id).map_or_else( + || { + println!( + "[Java_com_dropbear_ffi_JNINative_getTextureName] [ERROR] Invalid texture handle" + ); + return std::ptr::null_mut(); + }, + |material| { + let Ok(str) = env.new_string(material.name.as_str()) else { + return std::ptr::null_mut(); + }; + + str.into_raw() + }, + ) +} + +/// `JNIEXPORT jboolean JNICALL Java_com_dropbear_ffi_JNINative_isTextureHandle +/// (JNIEnv *, jclass, jlong, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_isTextureHandle( + _env: JNIEnv, + _class: JClass, + asset_handle: jlong, + texture_id: jlong, +) -> jboolean { + let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); + let texture_id = AssetHandle::new(texture_id as u64); + if asset.is_material(texture_id) { + true.into() } else { - -1 + false.into() } } -// /// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_setTexture -// /// (JNIEnv *, jclass, jlong, jlong, jlong, jlong);` -// #[unsafe(no_mangle)] -// pub fn Java_com_dropbear_ffi_JNINative_setTexture( -// _env: JNIEnv, -// _class: JClass, -// world_handle: jlong, -// asset_handle: jlong, -// entity: jlong, -// texture_handle: jlong, -// ) { -// let world = world_handle as WorldPtr; -// if world.is_null() { -// println!("[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] World pointer is null"); -// return; -// } -// -// let asset = asset_handle as AssetRegistryPtr; -// if asset.is_null() { -// println!("[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Asset registry pointer is null"); -// return; -// } -// -// let asset = unsafe { &*asset }; -// -// let world = unsafe { &*world }; -// let entity = unsafe { world.find_entity_from_id(entity as u32) }; -// -// match world.query_one::<&mut MeshRenderer>(entity) { -// Ok(mut query) => { -// let Some(renderer) = query.get() else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Entity does not have a MeshRenderer component" -// ); -// return; -// }; -// -// let material_handle = AssetHandle::new(texture_handle as u64); -// if !asset.contains_handle(material_handle) { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Material handle {} is not registered", -// material_handle.raw() -// ); -// return; -// } -// -// if !asset.is_material(material_handle) { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Handle {} does not reference a material", -// material_handle.raw() -// ); -// return; -// } -// -// let Some(material) = asset.get_material(material_handle) else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to fetch material for handle {}", -// material_handle.raw() -// ); -// return; -// }; -// -// let Some(owner_model_id) = asset.material_owner(material_handle) else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to resolve owning model for material {}", -// material_handle.raw() -// ); -// return; -// }; -// -// let Some(owner_model_handle) = asset.model_handle_from_id(owner_model_id) else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to resolve model handle for owner {:?}", -// owner_model_id -// ); -// return; -// }; -// -// let Some(source_model_reference) = asset.model_reference_for_handle(owner_model_handle) else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to resolve resource reference for model handle {}", -// owner_model_handle.raw() -// ); -// return; -// }; -// -// let source_material_name = material.name.clone(); -// -// let target_material_name = { -// let current_model = renderer.model(); -// if current_model -// .materials -// .iter() -// .any(|existing| existing.name == source_material_name) -// { -// Some(source_material_name.clone()) -// } else { -// current_model -// .materials -// .first() -// .map(|existing| existing.name.clone()) -// } -// }; -// -// let Some(target_material_name) = target_material_name else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Entity model has no materials to override" -// ); -// return; -// }; -// -// let Some(cache_raw) = asset.get_pointer(PointerKind::Const("model_cache")) else { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Model cache pointer is not registered" -// ); -// return; -// }; -// -// let cache_mutex_ptr = cache_raw as *const Mutex<HashMap<String, Arc<Model>>>; -// if cache_mutex_ptr.is_null() { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Model cache pointer is null" -// ); -// return; -// } -// -// let cache_mutex = unsafe { &*cache_mutex_ptr }; -// -// if let Err(err) = renderer.apply_material_override_raw( -// asset, -// cache_mutex, -// &target_material_name, -// source_model_reference, -// &source_material_name, -// ) { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Failed to apply material override: {}", -// err -// ); -// } -// } -// Err(err) => { -// println!( -// "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to query MeshRenderer: {}", -// err -// ); -// } -// } -// } +/// `JNIEXPORT jboolean JNICALL Java_com_dropbear_ffi_JNINative_isUsingTexture +/// (JNIEnv *, jclass, jlong, jlong, jstring);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_isUsingTexture( + _env: JNIEnv, + _class: JClass, + world_handle: jlong, + entity: jlong, + texture_handle: jlong, +) -> jboolean { + let world = convert_ptr!(world_handle, WorldPtr => World); + let entity = unsafe { world.find_entity_from_id(entity as u32) }; + + if let Ok(mut q) = world.query_one::<&MeshRenderer>(entity) + && let Some(mesh) = q.get() + { + mesh.contains_material_handle(AssetHandle::new(texture_handle as u64)) + .into() + } else { + println!( + "[Java_com_dropbear_ffi_JNINative_isUsingTexture] [ERROR] Unable to find entity in world" + ); + false.into() + } +} /// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getAsset /// (JNIEnv *, jclass, jlong, jstring);` @@ -1987,7 +2223,9 @@ pub fn Java_com_dropbear_ffi_JNINative_getAsset( ) -> jlong { let asset = asset_handle as AssetRegistryPtr; if asset.is_null() { - println!("[Java_com_dropbear_ffi_JNINative_getAsset] [ERROR] Asset registry pointer is null"); + println!( + "[Java_com_dropbear_ffi_JNINative_getAsset] [ERROR] Asset registry pointer is null" + ); return -1; } @@ -2013,8 +2251,60 @@ pub fn Java_com_dropbear_ffi_JNINative_getAsset( return -1; } }; - if let Ok(res) = ResourceReference::from_euca_uri(str) && let Some(asset_handle) = asset.get_handle_from_reference(&res) { + if let Ok(res) = ResourceReference::from_euca_uri(str) + && let Some(asset_handle) = asset.get_handle_from_reference(&res) + { return asset_handle.raw() as jlong; }; - -1 as jlong -} + 0 as jlong +} + +/// `JNIEXPORT jstring JNICALL Java_com_dropbear_ffi_JNINative_getLastErrorMsg +/// (JNIEnv *, jclass, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_getLastErrorMsg( + env: JNIEnv, + _class: JClass, + asset_handle: jlong, +) -> jstring { + let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); + if asset.get_pointer(Const("last_error_msg")).is_none() { + let ptr = get_last_error_message_ptr() as usize; + asset.add_pointer(Const("last_error_msg"), ptr); + } + let message = get_last_error_message(); + match env.new_string(&message) { + Ok(java_string) => { + clear_last_error_message(); + java_string.into_raw() + } + Err(e) => { + let fallback = format!( + "[Java_com_dropbear_ffi_JNINative_getLastErrorMsg] [ERROR] Failed to allocate Java string: {}", + e + ); + println!("{}", fallback); + set_last_error_message(&fallback); + std::ptr::null_mut() + } + } +} + +/// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getLastErrMsgPtr +/// (JNIEnv *, jclass, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_getLastErrMsgPtr( + _env: JNIEnv, + _class: JClass, + asset_handle: jlong, +) -> jlong { + let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); + let pointer = asset + .get_pointer(Const("last_error_msg")) + .unwrap_or_else(|| { + let ptr = get_last_error_message_ptr() as usize; + asset.add_pointer(Const("last_error_msg"), ptr); + ptr + }); + pointer as jlong +} @@ -118,6 +118,11 @@ impl NativeLibrary { /// Displays the types of errors that can be returned by the native library. pub enum DropbearNativeError { + /// An error in the case the function returns an unsigned value. + /// + /// Subtract [`DropbearNativeError::UnsignedGenericError`] with another value + /// to get the alternative unsigned error. + UnsignedGenericError = 65535, Success = 0, NullPointer = -1, QueryFailed = -2, @@ -3,8 +3,12 @@ use crate::ptr::{AssetRegistryPtr, GraphicsPtr, InputStatePtr, WorldPtr}; use std::ffi::c_char; /// CName: `dropbear_init` -pub type Init = - unsafe extern "C" fn(world: WorldPtr, input: InputStatePtr, graphics: GraphicsPtr, asset: AssetRegistryPtr) -> i32; +pub type Init = unsafe extern "C" fn( + world: WorldPtr, + input: InputStatePtr, + graphics: GraphicsPtr, + asset: AssetRegistryPtr, +) -> i32; /// CName: `dropbear_load_tagged` pub type LoadTagged = unsafe extern "C" fn(tag: *const c_char) -> i32; /// CName: `dropbear_update_all` @@ -1,7 +1,6 @@ -use std::path::PathBuf; - use crate::states::Node; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca}; +use std::path::PathBuf; use winit::keyboard::KeyCode; pub const PROTO_TEXTURE: &[u8] = include_bytes!("../../resources/textures/proto.png"); @@ -287,3 +286,175 @@ impl ResolveReference for ResourceReference { } } } + +/// Validates and converts a raw pointer to a reference. +/// Returns early if the pointer is null. +/// +/// # Example +/// ```rust +/// use eucalyptus_core::{convert_ptr, ptr::AssetRegistryPtr}; +/// use dropbear_engine::asset::AssetRegistry; +/// +/// let asset_handle = 0x12345678; // pointer +/// +/// let asset = convert_ptr!(asset_handle, AssetRegistryPtr => AssetRegistry); +/// ``` +#[macro_export] +macro_rules! convert_ptr { + ($ptr:expr, $ptr_ty:ty => $target_ty:ty) => {{ + let ptr = $ptr as $ptr_ty; + if ptr.is_null() { + let message = format!( + "[{}] [ERROR] {} pointer is null", + std::any::type_name::<$target_ty>(), + stringify!($ptr) + ); + crate::scripting::jni::error::set_last_error_message(&message); + println!("{}", message); + return $crate::ffi_error_return!(); + } + unsafe { &*(ptr as *const $target_ty) } + }}; + + ($ptr:expr => $target_ty:ty) => {{ + let ptr = $ptr as *const $target_ty; + if ptr.is_null() { + let message = format!( + "[{}] [ERROR] {} pointer is null", + std::any::type_name::<$target_ty>(), + stringify!($ptr) + ); + crate::scripting::jni::error::set_last_error_message(&message); + println!("{}", message); + return $crate::ffi_error_return!(); + } + unsafe { &*ptr } + }}; +} + +/// Converts a JString to a Rust String with automatic error handling. +/// Automatically infers the appropriate error return value based on the function's return type. +/// +/// # Usage +/// ```rust +/// convert_jstring!(env, jstring); +/// ``` +#[macro_export] +macro_rules! convert_jstring { + ($env:expr, $jstring:expr) => {{ + match $env.get_string(&$jstring) { + Ok(java_string) => match java_string.to_str() { + Ok(rust_str) => rust_str.to_string(), + Err(e) => { + let message = format!( + "[{}] [ERROR] Failed to convert Java string to Rust string: {}", + stringify!($jstring), + e + ); + crate::scripting::jni::error::set_last_error_message(&message); + println!("{}", message); + return $crate::ffi_error_return!(); + } + }, + Err(e) => { + let message = format!( + "[{}] [ERROR] Failed to get string from JNI: {}", + stringify!($jstring), + e + ); + crate::scripting::jni::error::set_last_error_message(&message); + println!("{}", message); + return $crate::ffi_error_return!(); + } + } + }}; +} + +#[macro_export] +macro_rules! ffi_error_return { + () => {{ + trait ErrorValue { + fn error_value() -> Self; + } + + impl ErrorValue for () { + fn error_value() -> Self {} + } + + impl ErrorValue for i8 { + fn error_value() -> Self { + -1 + } + } + + impl ErrorValue for i16 { + fn error_value() -> Self { + -1 + } + } + + impl ErrorValue for i32 { + fn error_value() -> Self { + -1 + } + } + + impl ErrorValue for i64 { + fn error_value() -> Self { + -1 + } + } + + impl ErrorValue for isize { + fn error_value() -> Self { + -1 + } + } + + impl ErrorValue for u8 { + fn error_value() -> Self { + 0 + } // most likely a char or a jboolean + } + + impl ErrorValue for u16 { + fn error_value() -> Self { + u16::MAX + } + } + + impl ErrorValue for u32 { + fn error_value() -> Self { + u32::MAX + } + } + + impl ErrorValue for u64 { + fn error_value() -> Self { + u64::MAX + } + } + + impl ErrorValue for usize { + fn error_value() -> Self { + usize::MAX + } + } + + impl<T> ErrorValue for *mut T { + fn error_value() -> Self { + std::ptr::null_mut() + } + } + + impl<T> ErrorValue for *const T { + fn error_value() -> Self { + std::ptr::null() + } + } + + // todo: implement other types + + ErrorValue::error_value() + }}; +} @@ -758,6 +758,19 @@ impl InspectableComponent for MeshRenderer { CollapsingHeader::new("Materials") .default_open(false) .show(ui, |ui| { + let material_uri_for = |model_reference: &ResourceReference, + material_name: &str| + -> Option<String> { + let model_handle = + ASSET_REGISTRY.model_handle_from_reference(model_reference)?; + let model_arc = ASSET_REGISTRY.get_model(model_handle)?; + let material_handle = + ASSET_REGISTRY.material_handle(model_arc.id, material_name)?; + let reference = + ASSET_REGISTRY.material_reference_for_handle(material_handle)?; + reference.as_uri().map(|uri| uri.to_string()) + }; + for material in self.model().materials.iter() { let override_snapshot = self .material_overrides() @@ -766,11 +779,16 @@ impl InspectableComponent for MeshRenderer { .cloned(); let selected_label = if let Some(override_entry) = &override_snapshot { - let reference = override_entry - .source_model - .as_uri() - .map(|uri| uri.to_string()) - .unwrap_or_else(|| "inline".to_string()); + let reference = material_uri_for( + &override_entry.source_model, + &override_entry.source_material, + ) + .or_else(|| { + override_entry.source_model.as_uri().map(|uri| { + format!("{}/{}", uri, override_entry.source_material) + }) + }) + .unwrap_or_else(|| "inline".to_string()); format!("{} [{}]", override_entry.source_material, reference) } else { "Original".to_string() @@ -840,16 +858,17 @@ impl InspectableComponent for MeshRenderer { response }; - let original_identifier = format!( - "bytes://material-original-{}", - material.name - ); - let original_path = self - .model() - .path - .as_uri() - .map(|uri| uri.to_string()) - .unwrap_or_else(|| "embedded".to_string()); + let original_identifier = + format!("bytes://material-original-{}", material.name); + let original_path = + material_uri_for(&self.model().path, &material.name) + .or_else(|| { + self.model() + .path + .as_uri() + .map(|uri| format!("{}/{}", uri, material.name)) + }) + .unwrap_or_else(|| "embedded".to_string()); let original_response = render_row( ui, &original_identifier, @@ -900,9 +919,11 @@ impl InspectableComponent for MeshRenderer { }) .unwrap_or(false); - let resource_path = source_model - .as_uri() - .map(|uri| uri.to_string()) + let resource_path = ASSET_REGISTRY + .material_reference_for_handle(handle) + .and_then(|reference| { + reference.as_uri().map(|uri| uri.to_string()) + }) .unwrap_or_else(|| "embedded".to_string()); let identifier = format!( @@ -930,8 +951,7 @@ impl InspectableComponent for MeshRenderer { if let Err(err) = self.restore_original_material(&material.name) { fatal!("Failed to restore material: {}", err); } - } else if let Some((source_model, source_material)) = pending_override - { + } else if let Some((source_model, source_material)) = pending_override { if let Err(err) = self.apply_material_override( &material.name, source_model, @@ -13,6 +13,7 @@ use crate::graphics::OutlineShader; use crate::plugin::PluginRegistry; use crate::stats::NerdStats; use crossbeam_channel::Receiver; +use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::shader::Shader; use dropbear_engine::{ camera::Camera, @@ -1408,11 +1409,8 @@ impl Editor { log::debug!("Pointers before sendoff:"); log::debug!("World: {:p}", world_ptr); log::debug!("Input: {:p}", input_ptr); - log::debug!( - "script graphics_command: 0x{:p}, 0x{:p}", - &GRAPHICS_COMMAND.0, - &GRAPHICS_COMMAND.1 - ); + log::debug!("Graphics Command Queue: {:p}", &GRAPHICS_COMMAND.0,); + log::debug!("Asset Registry: {:p}", &raw const ASSET_REGISTRY); if let Err(e) = self .script_manager @@ -1,6 +1,7 @@ use super::*; use crate::signal::SignalController; use crate::spawn::PendingSpawnController; +use dropbear_engine::asset::{ASSET_REGISTRY, PointerKind}; use dropbear_engine::graphics::{InstanceRaw, RenderContext}; use dropbear_engine::model::MODEL_CACHE; use dropbear_engine::{ @@ -18,7 +19,6 @@ use tokio::sync::mpsc::unbounded_channel; use wgpu::Color; use wgpu::util::DeviceExt; use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; -use dropbear_engine::asset::{PointerKind, ASSET_REGISTRY}; impl Scene for Editor { fn load(&mut self, graphics: &mut RenderContext) { @@ -170,10 +170,15 @@ impl Scene for Editor { log::info!("Plugins loaded"); } } + let cache_mutex_ptr = std::sync::LazyLock::force(&MODEL_CACHE) as *const _; + ASSET_REGISTRY.add_pointer(PointerKind::Const("model_cache"), cache_mutex_ptr as usize); + + let last_error_msg_ptr = + eucalyptus_core::scripting::jni::error::get_last_error_message_ptr(); ASSET_REGISTRY.add_pointer( - PointerKind::Const("model_cache"), - cache_mutex_ptr as usize, + PointerKind::Const("last_error_msg"), + last_error_msg_ptr as usize, ); if let Some((_, tab)) = self.dock_state.find_active_focused() { @@ -40,12 +40,18 @@ enum Target { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - if !cli.raw || (cli.stdout && cli.output.is_some()) { + if !(cli.stdout || cli.output.is_some() || cli.raw) { return Err(anyhow::anyhow!( "No output given. --stdout, --output <target> or --raw must be used." )); } + if cli.stdout && cli.output.is_some() { + return Err(anyhow::anyhow!( + "--stdout and --output cannot be used together. Choose one output destination." + )); + } + let mut processor = KotlinProcessor::new()?; let mut manifest = ScriptManifest::new(); @@ -75,10 +81,7 @@ fn main() -> anyhow::Result<()> { if cli.stdout { print!("{}", generated_content); - } else if cli.raw && !(cli.stdout || cli.output.is_some()) { - return Ok(()); - } else { - let output_dir = cli.output.unwrap(); + } else if let Some(output_dir) = cli.output { fs::create_dir_all(&output_dir)?; let filename = match cli.target { @@ -4,7 +4,6 @@ import com.dropbear.asset.AssetHandle import com.dropbear.ffi.NativeEngine import com.dropbear.input.InputState import com.dropbear.logging.Logger -import com.dropbear.math.Transform internal var exceptionOnError: Boolean = false @@ -22,6 +21,9 @@ class DropbearEngine(val native: NativeEngine) { } } + /** + * Fetches an [EntityRef] with the given label. + */ fun getEntity(label: String): EntityRef? { val entityId = native.getEntity(label) val entityRef = if (entityId != null) EntityRef(EntityId(entityId)) else null @@ -29,6 +31,9 @@ class DropbearEngine(val native: NativeEngine) { return entityRef } + /** + * Fetches the information of the camera with the given label. + */ fun getCamera(label: String): Camera? { val result = native.getCamera(label) if (result != null) { @@ -37,6 +42,9 @@ class DropbearEngine(val native: NativeEngine) { return result } + /** + * Gets the current [InputState] for that frame. + */ fun getInputState(): InputState { if (this.inputState == null) { Logger.trace("InputState not initialised, creating new one") @@ -45,17 +53,29 @@ class DropbearEngine(val native: NativeEngine) { return this.inputState!! } - internal fun getTransform(entityId: EntityId): Transform? { - val result = native.getTransform(entityId) - return result - } - - internal fun setTransform(entityId: EntityId, transform: Transform) { - native.setTransform(entityId, transform) - } - + /** + * Fetches the asset information from the internal AssetRegistry (located in + * `dropbear_engine::asset::AssetRegistry`). + * + * ## Warning + * The eucalyptus asset URI (or `euca://`) is case-sensitive. + */ fun getAsset(eucaURI: String): AssetHandle? { val id = native.getAsset(eucaURI) return if (id != null) AssetHandle(id) else null } + + /** + * Globally sets whether exceptions should be thrown when an error occurs. + * + * This can be run in your update loop without consequences. + */ + fun callExceptionOnError(toggle: Boolean) = DropbearEngine.callExceptionOnError(toggle) + + /** + * Fetches the last error message during the native call. + */ + fun getLastErrorMsg(): String? = native.getLastErrorMsg() + + fun getLastErrorMsgPtr(): Long = native.getLastErrorMsgPtr() } @@ -2,9 +2,19 @@ package com.dropbear import com.dropbear.asset.ModelHandle import com.dropbear.asset.TextureHandle -import com.dropbear.exception.DropbearNativeException import com.dropbear.math.Transform +/** + * A reference to an ECS Entity stored inside the dropbear engine. + * + * The dropbear engine prefers careful mutability, which is why a reference is passed (as a handle) instead + * of its full information. Also conserves memory. + * + * 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` + */ class EntityRef(val id: EntityId = EntityId(0L)) { lateinit var engine: DropbearEngine @@ -12,15 +22,24 @@ class EntityRef(val id: EntityId = EntityId(0L)) { return "EntityRef(id=$id)" } + /** + * Fetches the transform component for the entity. + */ fun getTransform(): Transform? { - return engine.getTransform(id) + return engine.native.getTransform(id) } + /** + * Sets and replaces the transform component for the entity. + */ fun setTransform(transform: Transform?) { if (transform == null) return - return engine.setTransform(id, transform) + return engine.native.setTransform(id, transform) } + /** + * Fetches the property of the ModelProperty component on the entity. + */ inline fun <reified T> getProperty(key: String): T? { return when (T::class) { String::class -> engine.native.getStringProperty(id.id, key) as T? @@ -35,6 +54,18 @@ class EntityRef(val id: EntityId = EntityId(0L)) { } } + /** + * Sets a property of the ModelProperty component on the entity. + * + * # Supported types + * - [kotlin.String] + * - [kotlin.Long] + * - [kotlin.Int] + * - [kotlin.Double] + * - [kotlin.Float] + * - [kotlin.Boolean] + * - [com.dropbear.math.Vector3] + */ fun setProperty(key: String, value: Any) { when (value) { is String -> engine.native.setStringProperty(id.id, key, value) @@ -51,6 +82,11 @@ class EntityRef(val id: EntityId = EntityId(0L)) { } } + /** + * Fetches the attached camera for the entity. + * + * Returns null if no camera is attached as a component according to the editor. + */ fun getAttachedCamera(): Camera? { val result = engine.native.getAttachedCamera(id) if (result != null) { @@ -59,30 +95,63 @@ class EntityRef(val id: EntityId = EntityId(0L)) { return result } + /** + * Fetches the texture for the given material name in the model. + */ fun getTexture(materialName: String): TextureHandle? { val result = engine.native.getTexture(id.id, materialName) - if (result == -1L) { - if (exceptionOnError) { - throw DropbearNativeException("Unable to get texture for material $materialName") - } - return null + return if (result == null) { + null } else { - return TextureHandle(result ?: throw Exception("Native returned null texture handle")) + TextureHandle(result) } } - fun hasTexture(eucaURI: String): Boolean { - return engine.native.isUsingTexture(id.id, eucaURI) + /** + * Returns an array containing the texture identifiers applied to this entity's model. + */ + fun getAllTextures(): Array<String> { + return engine.native.getAllTextures(id.id) + } + + /** + * Checks if the current model being rendered by this entity contains the texture with the given [TextureHandle] + */ + fun hasTexture(textureHandle: TextureHandle): Boolean { + return engine.native.isUsingTexture(id.id, textureHandle.raw()) + } + + /** + * Fetches the active model that is currently being used + */ + fun getModel(): ModelHandle? { + val result = engine.native.getModel(id.id) + return if (result == null) { + null + } else { + ModelHandle(result) + } } + /** + * Sets the active model for the entity from a ModelHandle + */ fun setModel(modelHandle: ModelHandle) { engine.native.setModel(id.id, modelHandle.raw()) } + /** + * Checks if the entity is currently using the given model handle. + * + * Returns false if not using, true if is. + */ fun usingModel(modelHandle: ModelHandle): Boolean { return engine.native.isUsingModel(id.id, modelHandle.raw()) } + /** + * Sets a texture override for the given material on the active model. + */ fun setTextureOverride(materialName: String, textureHandle: TextureHandle) { engine.native.setTextureOverride(id.id, materialName, textureHandle) } @@ -22,4 +22,19 @@ abstract class Handle(private val id: Long) { override fun toString(): String { return "Handle(id=$id)" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + val otherAsset = when (other) { + is Handle -> other.asAssetHandle() + is AssetHandle -> other + else -> return false + } + + val thisAsset = asAssetHandle() + return thisAsset.raw() == otherAsset.raw() + } + + override fun hashCode(): Int = asAssetHandle().raw().hashCode() } @@ -21,9 +21,10 @@ expect class NativeEngine { fun getTexture(entityHandle: Long, name: String): Long? fun setTextureOverride(entityHandle: Long, oldMaterialName: String, newTextureHandle: TextureHandle) - fun isUsingTexture(entityHandle: Long, name: String): Boolean + fun isUsingTexture(entityHandle: Long, textureHandle: Long): Boolean fun isTextureHandle(id: Long): Boolean fun getTextureName(textureHandle: Long): String? + fun getAllTextures(entityHandle: Long): Array<String> fun getCamera(label: String): Camera? fun getAttachedCamera(entityId: EntityId): Camera? @@ -65,6 +66,8 @@ expect class NativeEngine { fun isCursorHidden(): Boolean fun setCursorHidden(hidden: Boolean) fun getLastMousePos(): Vector2D? + fun getLastErrorMsg(): String? + fun getLastErrorMsgPtr(): Long // fun getConnectedGamepads(): List<Gamepad> // ------------------------------------------------------------------- @@ -8,7 +8,7 @@ public class JNINative { System.loadLibrary("eucalyptus_core"); } - // entity + // getters public static native long getEntity(long worldHandle, String label); public static native long getAsset(long assetRegistryHandle, String eucaURI); @@ -19,12 +19,12 @@ public class JNINative { public static native boolean isUsingModel(long worldHandle, long entityHandle, long modelHandle); // texture - public static native long getTexture(long worldHandle, long entityHandle, String name); + public static native long getTexture(long worldHandle, long assetHandle, long entityHandle, String name); public static native String getTextureName(long assetHandle, long textureHandle); public static native void setTexture(long worldHandle, long assetRegistryHandle, long entityHandle, String oldMaterialName, long textureHandle); public static native boolean isTextureHandle(long assetRegistryHandle, long handle); - public static native boolean isUsingTexture(long worldHandle, long entityHandle, String name); + public static native boolean isUsingTexture(long worldHandle, long entityHandle, long textureHandle); // camera public static native Camera getCamera(long worldHandle, String label); @@ -61,4 +61,7 @@ public class JNINative { public static native float[] getLastMousePos(long inputHandle); public static native boolean isCursorHidden(long inputHandle); public static native void setCursorHidden(long inputHandle, long graphicsHandle, boolean hidden); + public static native String getLastErrorMsg(long assetHandle); + public static native long getLastErrMsgPtr(long assetHandle); + public static native String[] getAllTextures(long worldHandle, long entityHandle); } @@ -242,6 +242,8 @@ actual class NativeEngine { } else { null } + } else if (result == 0L) { + null } else { result } @@ -252,15 +254,17 @@ actual class NativeEngine { } actual fun getTexture(entityHandle: Long, name: String): Long? { - val result = JNINative.getTexture(worldHandle, entityHandle, name) + val result = JNINative.getTexture(worldHandle, assetHandle, entityHandle, name) return if (result == -1L) { if (exceptionOnError) { throw DropbearNativeException("Unable to get texture for entity $entityHandle") } else { null } + } else if (result == 0L) { + null } else { - JNINative.getTexture(worldHandle, entityHandle, name) + result } } @@ -282,8 +286,8 @@ actual class NativeEngine { return JNINative.isUsingModel(worldHandle, entityHandle, modelHandle) } - actual fun isUsingTexture(entityHandle: Long, name: String): Boolean { - return JNINative.isUsingTexture(worldHandle, entityHandle, name) + actual fun isUsingTexture(entityHandle: Long, textureHandle: Long): Boolean { + return JNINative.isUsingTexture(worldHandle, entityHandle, textureHandle) } actual fun getAsset(eucaURI: String): Long? { @@ -294,6 +298,9 @@ actual class NativeEngine { } else { null } + } else if (result == 0L) { + // no asset found + null } else { result } @@ -306,4 +313,17 @@ actual class NativeEngine { actual fun isTextureHandle(id: Long): Boolean { return JNINative.isTextureHandle(assetHandle, id) } + + actual fun getLastErrorMsg(): String? { + val message = JNINative.getLastErrorMsg(assetHandle) + return if (message.isEmpty()) null else message + } + + actual fun getLastErrorMsgPtr(): Long { + return JNINative.getLastErrMsgPtr(assetHandle) + } + + actual fun getAllTextures(entityHandle: Long): Array<String> { + return JNINative.getAllTextures(worldHandle, entityHandle) ?: emptyArray() + } } @@ -810,7 +810,7 @@ actual class NativeEngine { TODO("Not yet implemented") } - actual fun isUsingTexture(entityHandle: Long, name: String): Boolean { + actual fun isUsingTexture(entityHandle: Long, textureHandle: Long): Boolean { TODO("Not yet implemented") } @@ -832,4 +832,16 @@ actual class NativeEngine { actual fun getTextureName(textureHandle: Long): String? { TODO("Not yet implemented") } + + actual fun getLastErrorMsg(): String? { + TODO("Not yet implemented") + } + + actual fun getLastErrorMsgPtr(): Long { + TODO("Not yet implemented") + } + + actual fun getAllTextures(entityHandle: Long): Array<String> { + TODO("Not yet implemented") + } }