diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 5720b7479fe90..ad8951a4aa31e 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -11,6 +11,7 @@ use std::{ hash::{Hash, Hasher}, sync::Arc, }; +use thiserror::Error; /// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_. /// This should _only_ be used for one specific asset type. @@ -191,10 +192,7 @@ impl Handle { /// [`Handle::Weak`]. #[inline] pub fn untyped(self) -> UntypedHandle { - match self { - Handle::Strong(handle) => UntypedHandle::Strong(handle), - Handle::Weak(id) => UntypedHandle::Weak(id.untyped()), - } + self.into() } } @@ -224,13 +222,14 @@ impl std::fmt::Debug for Handle { impl Hash for Handle { #[inline] fn hash(&self, state: &mut H) { - Hash::hash(&self.id(), state); + self.id().hash(state); + TypeId::of::().hash(state); } } impl PartialOrd for Handle { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.id().cmp(&other.id())) + Some(self.cmp(other)) } } @@ -249,6 +248,34 @@ impl PartialEq for Handle { impl Eq for Handle {} +impl From> for AssetId { + #[inline] + fn from(value: Handle) -> Self { + value.id() + } +} + +impl From<&Handle> for AssetId { + #[inline] + fn from(value: &Handle) -> Self { + value.id() + } +} + +impl From> for UntypedAssetId { + #[inline] + fn from(value: Handle) -> Self { + value.id().into() + } +} + +impl From<&Handle> for UntypedAssetId { + #[inline] + fn from(value: &Handle) -> Self { + value.id().into() + } +} + /// An untyped variant of [`Handle`], which internally stores the [`Asset`] type information at runtime /// as a [`TypeId`] instead of encoding it in the compile-time type. This allows handles across [`Asset`] types /// to be stored together and compared. @@ -326,12 +353,20 @@ impl UntypedHandle { /// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A` #[inline] pub fn typed(self) -> Handle { - assert_eq!( - self.type_id(), - TypeId::of::(), - "The target Handle's TypeId does not match the TypeId of this UntypedHandle" - ); - self.typed_unchecked() + let Ok(handle) = self.try_typed() else { + panic!( + "The target Handle<{}>'s TypeId does not match the TypeId of this UntypedHandle", + std::any::type_name::() + ) + }; + + handle + } + + /// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A` + #[inline] + pub fn try_typed(self) -> Result, UntypedAssetConversionError> { + Handle::try_from(self) } /// The "meta transform" for the strong handle. This will only be [`Some`] if the handle is strong and there is a meta transform @@ -383,3 +418,212 @@ impl std::fmt::Debug for UntypedHandle { } } } + +impl PartialOrd for UntypedHandle { + fn partial_cmp(&self, other: &Self) -> Option { + if self.type_id() == other.type_id() { + self.id().partial_cmp(&other.id()) + } else { + None + } + } +} + +impl From for UntypedAssetId { + #[inline] + fn from(value: UntypedHandle) -> Self { + value.id() + } +} + +impl From<&UntypedHandle> for UntypedAssetId { + #[inline] + fn from(value: &UntypedHandle) -> Self { + value.id() + } +} + +// Cross Operations + +impl PartialEq for Handle { + #[inline] + fn eq(&self, other: &UntypedHandle) -> bool { + TypeId::of::() == other.type_id() && self.id() == other.id() + } +} + +impl PartialEq> for UntypedHandle { + #[inline] + fn eq(&self, other: &Handle) -> bool { + other.eq(self) + } +} + +impl PartialOrd for Handle { + #[inline] + fn partial_cmp(&self, other: &UntypedHandle) -> Option { + if TypeId::of::() != other.type_id() { + None + } else { + self.id().partial_cmp(&other.id()) + } + } +} + +impl PartialOrd> for UntypedHandle { + #[inline] + fn partial_cmp(&self, other: &Handle) -> Option { + Some(other.partial_cmp(self)?.reverse()) + } +} + +impl From> for UntypedHandle { + fn from(value: Handle) -> Self { + match value { + Handle::Strong(handle) => UntypedHandle::Strong(handle), + Handle::Weak(id) => UntypedHandle::Weak(id.into()), + } + } +} + +impl TryFrom for Handle { + type Error = UntypedAssetConversionError; + + fn try_from(value: UntypedHandle) -> Result { + let found = value.type_id(); + let expected = TypeId::of::(); + + if found != expected { + return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); + } + + match value { + UntypedHandle::Strong(handle) => Ok(Handle::Strong(handle)), + UntypedHandle::Weak(id) => { + let Ok(id) = id.try_into() else { + return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); + }; + Ok(Handle::Weak(id)) + } + } + } +} + +/// Errors preventing the conversion of to/from an [`UntypedHandle`] and an [`Handle`]. +#[derive(Error, Debug, PartialEq, Clone)] +#[non_exhaustive] +pub enum UntypedAssetConversionError { + /// Caused when trying to convert an [`UntypedHandle`] into an [`Handle`] of the wrong type. + #[error( + "This UntypedHandle is for {found:?} and cannot be converted into an Handle<{expected:?}>" + )] + TypeIdMismatch { expected: TypeId, found: TypeId }, +} + +#[cfg(test)] +mod tests { + use super::*; + + type TestAsset = (); + + const UUID_1: Uuid = Uuid::from_u128(123); + const UUID_2: Uuid = Uuid::from_u128(456); + + /// Simple utility to directly hash a value using a fixed hasher + fn hash(data: &T) -> u64 { + let mut hasher = bevy_utils::AHasher::default(); + data.hash(&mut hasher); + hasher.finish() + } + + /// Typed and Untyped `Handles` should be equivalent to each other and themselves + #[test] + fn equality() { + let typed = AssetId::::Uuid { uuid: UUID_1 }; + let untyped = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + + let typed = Handle::Weak(typed); + let untyped = UntypedHandle::Weak(untyped); + + assert_eq!( + Ok(typed.clone()), + Handle::::try_from(untyped.clone()) + ); + assert_eq!(UntypedHandle::from(typed.clone()), untyped); + assert_eq!(typed, untyped); + } + + /// Typed and Untyped `Handles` should be orderable amongst each other and themselves + #[allow(clippy::cmp_owned)] + #[test] + fn ordering() { + assert!(UUID_1 < UUID_2); + + let typed_1 = AssetId::::Uuid { uuid: UUID_1 }; + let typed_2 = AssetId::::Uuid { uuid: UUID_2 }; + let untyped_1 = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + let untyped_2 = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_2, + }; + + let typed_1 = Handle::Weak(typed_1); + let typed_2 = Handle::Weak(typed_2); + let untyped_1 = UntypedHandle::Weak(untyped_1); + let untyped_2 = UntypedHandle::Weak(untyped_2); + + assert!(typed_1 < typed_2); + assert!(untyped_1 < untyped_2); + + assert!(UntypedHandle::from(typed_1.clone()) < untyped_2); + assert!(untyped_1 < UntypedHandle::from(typed_2.clone())); + + assert!(Handle::::try_from(untyped_1.clone()).unwrap() < typed_2); + assert!(typed_1 < Handle::::try_from(untyped_2.clone()).unwrap()); + + assert!(typed_1 < untyped_2); + assert!(untyped_1 < typed_2); + } + + /// Typed and Untyped `Handles` should be equivalently hashable to each other and themselves + #[test] + fn hashing() { + let typed = AssetId::::Uuid { uuid: UUID_1 }; + let untyped = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + + let typed = Handle::Weak(typed); + let untyped = UntypedHandle::Weak(untyped); + + assert_eq!( + hash(&typed), + hash(&Handle::::try_from(untyped.clone()).unwrap()) + ); + assert_eq!(hash(&UntypedHandle::from(typed.clone())), hash(&untyped)); + assert_eq!(hash(&typed), hash(&untyped)); + } + + /// Typed and Untyped `Handles` should be interchangeable + #[test] + fn conversion() { + let typed = AssetId::::Uuid { uuid: UUID_1 }; + let untyped = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + + let typed = Handle::Weak(typed); + let untyped = UntypedHandle::Weak(untyped); + + assert_eq!(typed, Handle::try_from(untyped.clone()).unwrap()); + assert_eq!(UntypedHandle::from(typed.clone()), untyped); + } +} diff --git a/crates/bevy_asset/src/id.rs b/crates/bevy_asset/src/id.rs index 428b992ca0742..a75ae3858e07c 100644 --- a/crates/bevy_asset/src/id.rs +++ b/crates/bevy_asset/src/id.rs @@ -1,4 +1,4 @@ -use crate::{Asset, AssetIndex, Handle, UntypedHandle}; +use crate::{Asset, AssetIndex}; use bevy_reflect::{Reflect, Uuid}; use std::{ any::TypeId, @@ -6,11 +6,12 @@ use std::{ hash::Hash, marker::PhantomData, }; +use thiserror::Error; /// A unique runtime-only identifier for an [`Asset`]. This is cheap to [`Copy`]/[`Clone`] and is not directly tied to the /// lifetime of the Asset. This means it _can_ point to an [`Asset`] that no longer exists. /// -/// For an identifier tied to the lifetime of an asset, see [`Handle`]. +/// For an identifier tied to the lifetime of an asset, see [`Handle`](`crate::Handle`). /// /// For an "untyped" / "generic-less" id, see [`UntypedAssetId`]. #[derive(Reflect)] @@ -53,16 +54,7 @@ impl AssetId { /// _inside_ the [`UntypedAssetId`]. #[inline] pub fn untyped(self) -> UntypedAssetId { - match self { - AssetId::Index { index, .. } => UntypedAssetId::Index { - index, - type_id: TypeId::of::(), - }, - AssetId::Uuid { uuid } => UntypedAssetId::Uuid { - uuid, - type_id: TypeId::of::(), - }, - } + self.into() } #[inline] @@ -95,6 +87,7 @@ impl Display for AssetId { Debug::fmt(self, f) } } + impl Debug for AssetId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -123,6 +116,7 @@ impl Hash for AssetId { #[inline] fn hash(&self, state: &mut H) { self.internal().hash(state); + TypeId::of::().hash(state); } } @@ -164,52 +158,10 @@ impl From for AssetId { } } -impl From> for AssetId { - #[inline] - fn from(value: Handle) -> Self { - value.id() - } -} - -impl From<&Handle> for AssetId { - #[inline] - fn from(value: &Handle) -> Self { - value.id() - } -} - -impl From for AssetId { - #[inline] - fn from(value: UntypedHandle) -> Self { - value.id().typed() - } -} - -impl From<&UntypedHandle> for AssetId { - #[inline] - fn from(value: &UntypedHandle) -> Self { - value.id().typed() - } -} - -impl From for AssetId { - #[inline] - fn from(value: UntypedAssetId) -> Self { - value.typed() - } -} - -impl From<&UntypedAssetId> for AssetId { - #[inline] - fn from(value: &UntypedAssetId) -> Self { - value.typed() - } -} - /// An "untyped" / "generic-less" [`Asset`] identifier that behaves much like [`AssetId`], but stores the [`Asset`] type /// information at runtime instead of compile-time. This increases the size of the type, but it enables storing asset ids /// across asset types together and enables comparisons between them. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone)] pub enum UntypedAssetId { /// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is /// the "default" identifier used for assets. The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are @@ -263,13 +215,20 @@ impl UntypedAssetId { /// Panics if the [`TypeId`] of `A` does not match the stored type id. #[inline] pub fn typed(self) -> AssetId { - assert_eq!( - self.type_id(), - TypeId::of::(), - "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId", - std::any::type_name::() - ); - self.typed_unchecked() + let Ok(id) = self.try_typed() else { + panic!( + "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId", + std::any::type_name::() + ) + }; + + id + } + + /// Try to convert this to a "typed" [`AssetId`]. + #[inline] + pub fn try_typed(self) -> Result, UntypedAssetIdConversionError> { + AssetId::try_from(self) } /// Returns the stored [`TypeId`] of the referenced [`Asset`]. @@ -309,24 +268,30 @@ impl Display for UntypedAssetId { } } -impl From> for UntypedAssetId { +impl PartialEq for UntypedAssetId { #[inline] - fn from(value: AssetId) -> Self { - value.untyped() + fn eq(&self, other: &Self) -> bool { + self.internal().eq(&other.internal()) } } -impl From> for UntypedAssetId { +impl Eq for UntypedAssetId {} + +impl Hash for UntypedAssetId { #[inline] - fn from(value: Handle) -> Self { - value.id().untyped() + fn hash(&self, state: &mut H) { + self.internal().hash(state); + self.type_id().hash(state); } } -impl From<&Handle> for UntypedAssetId { - #[inline] - fn from(value: &Handle) -> Self { - value.id().untyped() +impl PartialOrd for UntypedAssetId { + fn partial_cmp(&self, other: &Self) -> Option { + if self.type_id() != other.type_id() { + None + } else { + Some(self.internal().cmp(&other.internal())) + } } } @@ -375,3 +340,171 @@ impl From for InternalAssetId { Self::Uuid(value) } } + +// Cross Operations + +impl PartialEq for AssetId { + #[inline] + fn eq(&self, other: &UntypedAssetId) -> bool { + TypeId::of::() == other.type_id() && self.internal().eq(&other.internal()) + } +} + +impl PartialEq> for UntypedAssetId { + #[inline] + fn eq(&self, other: &AssetId) -> bool { + other.eq(self) + } +} + +impl PartialOrd for AssetId { + #[inline] + fn partial_cmp(&self, other: &UntypedAssetId) -> Option { + if TypeId::of::() != other.type_id() { + None + } else { + Some(self.internal().cmp(&other.internal())) + } + } +} + +impl PartialOrd> for UntypedAssetId { + #[inline] + fn partial_cmp(&self, other: &AssetId) -> Option { + Some(other.partial_cmp(self)?.reverse()) + } +} + +impl From> for UntypedAssetId { + #[inline] + fn from(value: AssetId) -> Self { + let type_id = TypeId::of::(); + + match value { + AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index }, + AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid }, + } + } +} + +impl TryFrom for AssetId { + type Error = UntypedAssetIdConversionError; + + #[inline] + fn try_from(value: UntypedAssetId) -> Result { + let found = value.type_id(); + let expected = TypeId::of::(); + + match value { + UntypedAssetId::Index { index, type_id } if type_id == expected => Ok(AssetId::Index { + index, + marker: PhantomData, + }), + UntypedAssetId::Uuid { uuid, type_id } if type_id == expected => { + Ok(AssetId::Uuid { uuid }) + } + _ => Err(UntypedAssetIdConversionError::TypeIdMismatch { expected, found }), + } + } +} + +/// Errors preventing the conversion of to/from an [`UntypedAssetId`] and an [`AssetId`]. +#[derive(Error, Debug, PartialEq, Clone)] +#[non_exhaustive] +pub enum UntypedAssetIdConversionError { + /// Caused when trying to convert an [`UntypedAssetId`] into an [`AssetId`] of the wrong type. + #[error("This UntypedAssetId is for {found:?} and cannot be converted into an AssetId<{expected:?}>")] + TypeIdMismatch { expected: TypeId, found: TypeId }, +} + +#[cfg(test)] +mod tests { + use super::*; + + type TestAsset = (); + + const UUID_1: Uuid = Uuid::from_u128(123); + const UUID_2: Uuid = Uuid::from_u128(456); + + /// Simple utility to directly hash a value using a fixed hasher + fn hash(data: &T) -> u64 { + use std::hash::Hasher; + + let mut hasher = bevy_utils::AHasher::default(); + data.hash(&mut hasher); + hasher.finish() + } + + /// Typed and Untyped `AssetIds` should be equivalent to each other and themselves + #[test] + fn equality() { + let typed = AssetId::::Uuid { uuid: UUID_1 }; + let untyped = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + + assert_eq!(Ok(typed), AssetId::try_from(untyped)); + assert_eq!(UntypedAssetId::from(typed), untyped); + assert_eq!(typed, untyped); + } + + /// Typed and Untyped `AssetIds` should be orderable amongst each other and themselves + #[test] + fn ordering() { + assert!(UUID_1 < UUID_2); + + let typed_1 = AssetId::::Uuid { uuid: UUID_1 }; + let typed_2 = AssetId::::Uuid { uuid: UUID_2 }; + let untyped_1 = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + let untyped_2 = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_2, + }; + + assert!(typed_1 < typed_2); + assert!(untyped_1 < untyped_2); + + assert!(UntypedAssetId::from(typed_1) < untyped_2); + assert!(untyped_1 < UntypedAssetId::from(typed_2)); + + assert!(AssetId::try_from(untyped_1).unwrap() < typed_2); + assert!(typed_1 < AssetId::try_from(untyped_2).unwrap()); + + assert!(typed_1 < untyped_2); + assert!(untyped_1 < typed_2); + } + + /// Typed and Untyped `AssetIds` should be equivalently hashable to each other and themselves + #[test] + fn hashing() { + let typed = AssetId::::Uuid { uuid: UUID_1 }; + let untyped = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + + assert_eq!( + hash(&typed), + hash(&AssetId::::try_from(untyped).unwrap()) + ); + assert_eq!(hash(&UntypedAssetId::from(typed)), hash(&untyped)); + assert_eq!(hash(&typed), hash(&untyped)); + } + + /// Typed and Untyped `AssetIds` should be interchangeable + #[test] + fn conversion() { + let typed = AssetId::::Uuid { uuid: UUID_1 }; + let untyped = UntypedAssetId::Uuid { + type_id: TypeId::of::(), + uuid: UUID_1, + }; + + assert_eq!(Ok(typed), AssetId::try_from(untyped)); + assert_eq!(UntypedAssetId::from(typed), untyped); + } +}