500 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			500 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use crate::{Asset, AssetIndex};
 | |
| use bevy_reflect::Reflect;
 | |
| use serde::{Deserialize, Serialize};
 | |
| use uuid::Uuid;
 | |
| 
 | |
| use core::{
 | |
|     any::TypeId,
 | |
|     fmt::{Debug, Display},
 | |
|     hash::Hash,
 | |
|     marker::PhantomData,
 | |
| };
 | |
| use derive_more::derive::{Display, Error, From};
 | |
| 
 | |
| /// 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`](`crate::Handle`).
 | |
| ///
 | |
| /// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
 | |
| #[derive(Reflect, Serialize, Deserialize, From)]
 | |
| pub enum AssetId<A: Asset> {
 | |
|     /// 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: [`AssetId::Uuid`]) will only be used if assets are
 | |
|     /// explicitly registered that way.
 | |
|     ///
 | |
|     /// [`Assets`]: crate::Assets
 | |
|     Index {
 | |
|         index: AssetIndex,
 | |
|         #[reflect(ignore)]
 | |
|         marker: PhantomData<fn() -> A>,
 | |
|     },
 | |
|     /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
 | |
|     /// with one.
 | |
|     ///
 | |
|     /// [`Assets`]: crate::Assets
 | |
|     Uuid { uuid: Uuid },
 | |
| }
 | |
| 
 | |
| impl<A: Asset> AssetId<A> {
 | |
|     /// The uuid for the default [`AssetId`]. It is valid to assign a value to this in [`Assets`](crate::Assets)
 | |
|     /// and by convention (where appropriate) assets should support this pattern.
 | |
|     pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631);
 | |
| 
 | |
|     /// This asset id _should_ never be valid. Assigning a value to this in [`Assets`](crate::Assets) will
 | |
|     /// produce undefined behavior, so don't do it!
 | |
|     pub const INVALID_UUID: Uuid = Uuid::from_u128(108428345662029828789348721013522787528);
 | |
| 
 | |
|     /// Returns an [`AssetId`] with [`Self::INVALID_UUID`], which _should_ never be assigned to.
 | |
|     #[inline]
 | |
|     pub const fn invalid() -> Self {
 | |
|         Self::Uuid {
 | |
|             uuid: Self::INVALID_UUID,
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Converts this to an "untyped" / "generic-less" [`Asset`] identifier that stores the type information
 | |
|     /// _inside_ the [`UntypedAssetId`].
 | |
|     #[inline]
 | |
|     pub fn untyped(self) -> UntypedAssetId {
 | |
|         self.into()
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     pub(crate) fn internal(self) -> InternalAssetId {
 | |
|         match self {
 | |
|             AssetId::Index { index, .. } => InternalAssetId::Index(index),
 | |
|             AssetId::Uuid { uuid } => InternalAssetId::Uuid(uuid),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Default for AssetId<A> {
 | |
|     fn default() -> Self {
 | |
|         AssetId::Uuid {
 | |
|             uuid: Self::DEFAULT_UUID,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Clone for AssetId<A> {
 | |
|     fn clone(&self) -> Self {
 | |
|         *self
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Copy for AssetId<A> {}
 | |
| 
 | |
| impl<A: Asset> Display for AssetId<A> {
 | |
|     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
 | |
|         Debug::fmt(self, f)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Debug for AssetId<A> {
 | |
|     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
 | |
|         match self {
 | |
|             AssetId::Index { index, .. } => {
 | |
|                 write!(
 | |
|                     f,
 | |
|                     "AssetId<{}>{{ index: {}, generation: {}}}",
 | |
|                     core::any::type_name::<A>(),
 | |
|                     index.index,
 | |
|                     index.generation
 | |
|                 )
 | |
|             }
 | |
|             AssetId::Uuid { uuid } => {
 | |
|                 write!(
 | |
|                     f,
 | |
|                     "AssetId<{}>{{uuid: {}}}",
 | |
|                     core::any::type_name::<A>(),
 | |
|                     uuid
 | |
|                 )
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Hash for AssetId<A> {
 | |
|     #[inline]
 | |
|     fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
 | |
|         self.internal().hash(state);
 | |
|         TypeId::of::<A>().hash(state);
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> PartialEq for AssetId<A> {
 | |
|     #[inline]
 | |
|     fn eq(&self, other: &Self) -> bool {
 | |
|         self.internal().eq(&other.internal())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Eq for AssetId<A> {}
 | |
| 
 | |
| impl<A: Asset> PartialOrd for AssetId<A> {
 | |
|     fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
 | |
|         Some(self.cmp(other))
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> Ord for AssetId<A> {
 | |
|     fn cmp(&self, other: &Self) -> core::cmp::Ordering {
 | |
|         self.internal().cmp(&other.internal())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> From<AssetIndex> for AssetId<A> {
 | |
|     #[inline]
 | |
|     fn from(value: AssetIndex) -> Self {
 | |
|         Self::Index {
 | |
|             index: value,
 | |
|             marker: PhantomData,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// 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)]
 | |
| 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
 | |
|     /// explicitly registered that way.
 | |
|     ///
 | |
|     /// [`Assets`]: crate::Assets
 | |
|     Index { type_id: TypeId, index: AssetIndex },
 | |
|     /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
 | |
|     /// with one.
 | |
|     ///
 | |
|     /// [`Assets`]: crate::Assets
 | |
|     Uuid { type_id: TypeId, uuid: Uuid },
 | |
| }
 | |
| 
 | |
| impl UntypedAssetId {
 | |
|     /// Converts this to a "typed" [`AssetId`] without checking the stored type to see if it matches the target `A` [`Asset`] type.
 | |
|     /// This should only be called if you are _absolutely certain_ the asset type matches the stored type. And even then, you should
 | |
|     /// consider using [`UntypedAssetId::typed_debug_checked`] instead.
 | |
|     #[inline]
 | |
|     pub fn typed_unchecked<A: Asset>(self) -> AssetId<A> {
 | |
|         match self {
 | |
|             UntypedAssetId::Index { index, .. } => AssetId::Index {
 | |
|                 index,
 | |
|                 marker: PhantomData,
 | |
|             },
 | |
|             UntypedAssetId::Uuid { uuid, .. } => AssetId::Uuid { uuid },
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Converts this to a "typed" [`AssetId`]. When compiled in debug-mode it will check to see if the stored type
 | |
|     /// matches the target `A` [`Asset`] type. When compiled in release-mode, this check will be skipped.
 | |
|     ///
 | |
|     /// # Panics
 | |
|     ///
 | |
|     /// Panics if compiled in debug mode and the [`TypeId`] of `A` does not match the stored [`TypeId`].
 | |
|     #[inline]
 | |
|     pub fn typed_debug_checked<A: Asset>(self) -> AssetId<A> {
 | |
|         debug_assert_eq!(
 | |
|             self.type_id(),
 | |
|             TypeId::of::<A>(),
 | |
|             "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
 | |
|             core::any::type_name::<A>()
 | |
|         );
 | |
|         self.typed_unchecked()
 | |
|     }
 | |
| 
 | |
|     /// Converts this to a "typed" [`AssetId`].
 | |
|     ///
 | |
|     /// # Panics
 | |
|     ///
 | |
|     /// Panics if the [`TypeId`] of `A` does not match the stored type id.
 | |
|     #[inline]
 | |
|     pub fn typed<A: Asset>(self) -> AssetId<A> {
 | |
|         let Ok(id) = self.try_typed() else {
 | |
|             panic!(
 | |
|                 "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
 | |
|                 core::any::type_name::<A>()
 | |
|             )
 | |
|         };
 | |
| 
 | |
|         id
 | |
|     }
 | |
| 
 | |
|     /// Try to convert this to a "typed" [`AssetId`].
 | |
|     #[inline]
 | |
|     pub fn try_typed<A: Asset>(self) -> Result<AssetId<A>, UntypedAssetIdConversionError> {
 | |
|         AssetId::try_from(self)
 | |
|     }
 | |
| 
 | |
|     /// Returns the stored [`TypeId`] of the referenced [`Asset`].
 | |
|     #[inline]
 | |
|     pub fn type_id(&self) -> TypeId {
 | |
|         match self {
 | |
|             UntypedAssetId::Index { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => {
 | |
|                 *type_id
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     pub(crate) fn internal(self) -> InternalAssetId {
 | |
|         match self {
 | |
|             UntypedAssetId::Index { index, .. } => InternalAssetId::Index(index),
 | |
|             UntypedAssetId::Uuid { uuid, .. } => InternalAssetId::Uuid(uuid),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Display for UntypedAssetId {
 | |
|     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
 | |
|         let mut writer = f.debug_struct("UntypedAssetId");
 | |
|         match self {
 | |
|             UntypedAssetId::Index { index, type_id } => {
 | |
|                 writer
 | |
|                     .field("type_id", type_id)
 | |
|                     .field("index", &index.index)
 | |
|                     .field("generation", &index.generation);
 | |
|             }
 | |
|             UntypedAssetId::Uuid { uuid, type_id } => {
 | |
|                 writer.field("type_id", type_id).field("uuid", uuid);
 | |
|             }
 | |
|         }
 | |
|         writer.finish()
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl PartialEq for UntypedAssetId {
 | |
|     #[inline]
 | |
|     fn eq(&self, other: &Self) -> bool {
 | |
|         self.type_id() == other.type_id() && self.internal().eq(&other.internal())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Eq for UntypedAssetId {}
 | |
| 
 | |
| impl Hash for UntypedAssetId {
 | |
|     #[inline]
 | |
|     fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
 | |
|         self.internal().hash(state);
 | |
|         self.type_id().hash(state);
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Ord for UntypedAssetId {
 | |
|     fn cmp(&self, other: &Self) -> core::cmp::Ordering {
 | |
|         self.type_id()
 | |
|             .cmp(&other.type_id())
 | |
|             .then_with(|| self.internal().cmp(&other.internal()))
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl PartialOrd for UntypedAssetId {
 | |
|     fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
 | |
|         Some(self.cmp(other))
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// An asset id without static or dynamic types associated with it.
 | |
| ///
 | |
| /// This exist to support efficient type erased id drop tracking. We
 | |
| /// could use [`UntypedAssetId`] for this, but the [`TypeId`] is unnecessary.
 | |
| ///
 | |
| /// Do not _ever_ use this across asset types for comparison.
 | |
| /// [`InternalAssetId`] contains no type information and will happily collide
 | |
| /// with indices across types.
 | |
| #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, From)]
 | |
| pub(crate) enum InternalAssetId {
 | |
|     Index(AssetIndex),
 | |
|     Uuid(Uuid),
 | |
| }
 | |
| 
 | |
| impl InternalAssetId {
 | |
|     #[inline]
 | |
|     pub(crate) fn typed<A: Asset>(self) -> AssetId<A> {
 | |
|         match self {
 | |
|             InternalAssetId::Index(index) => AssetId::Index {
 | |
|                 index,
 | |
|                 marker: PhantomData,
 | |
|             },
 | |
|             InternalAssetId::Uuid(uuid) => AssetId::Uuid { uuid },
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     pub(crate) fn untyped(self, type_id: TypeId) -> UntypedAssetId {
 | |
|         match self {
 | |
|             InternalAssetId::Index(index) => UntypedAssetId::Index { index, type_id },
 | |
|             InternalAssetId::Uuid(uuid) => UntypedAssetId::Uuid { uuid, type_id },
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Cross Operations
 | |
| 
 | |
| impl<A: Asset> PartialEq<UntypedAssetId> for AssetId<A> {
 | |
|     #[inline]
 | |
|     fn eq(&self, other: &UntypedAssetId) -> bool {
 | |
|         TypeId::of::<A>() == other.type_id() && self.internal().eq(&other.internal())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> PartialEq<AssetId<A>> for UntypedAssetId {
 | |
|     #[inline]
 | |
|     fn eq(&self, other: &AssetId<A>) -> bool {
 | |
|         other.eq(self)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> PartialOrd<UntypedAssetId> for AssetId<A> {
 | |
|     #[inline]
 | |
|     fn partial_cmp(&self, other: &UntypedAssetId) -> Option<core::cmp::Ordering> {
 | |
|         if TypeId::of::<A>() != other.type_id() {
 | |
|             None
 | |
|         } else {
 | |
|             Some(self.internal().cmp(&other.internal()))
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> PartialOrd<AssetId<A>> for UntypedAssetId {
 | |
|     #[inline]
 | |
|     fn partial_cmp(&self, other: &AssetId<A>) -> Option<core::cmp::Ordering> {
 | |
|         Some(other.partial_cmp(self)?.reverse())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> From<AssetId<A>> for UntypedAssetId {
 | |
|     #[inline]
 | |
|     fn from(value: AssetId<A>) -> Self {
 | |
|         let type_id = TypeId::of::<A>();
 | |
| 
 | |
|         match value {
 | |
|             AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index },
 | |
|             AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid },
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<A: Asset> TryFrom<UntypedAssetId> for AssetId<A> {
 | |
|     type Error = UntypedAssetIdConversionError;
 | |
| 
 | |
|     #[inline]
 | |
|     fn try_from(value: UntypedAssetId) -> Result<Self, Self::Error> {
 | |
|         let found = value.type_id();
 | |
|         let expected = TypeId::of::<A>();
 | |
| 
 | |
|         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, Display, Debug, PartialEq, Clone)]
 | |
| #[non_exhaustive]
 | |
| pub enum UntypedAssetIdConversionError {
 | |
|     /// Caused when trying to convert an [`UntypedAssetId`] into an [`AssetId`] of the wrong type.
 | |
|     #[display("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<T: Hash>(data: &T) -> u64 {
 | |
|         use core::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::<TestAsset>::Uuid { uuid: UUID_1 };
 | |
|         let untyped = UntypedAssetId::Uuid {
 | |
|             type_id: TypeId::of::<TestAsset>(),
 | |
|             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::<TestAsset>::Uuid { uuid: UUID_1 };
 | |
|         let typed_2 = AssetId::<TestAsset>::Uuid { uuid: UUID_2 };
 | |
|         let untyped_1 = UntypedAssetId::Uuid {
 | |
|             type_id: TypeId::of::<TestAsset>(),
 | |
|             uuid: UUID_1,
 | |
|         };
 | |
|         let untyped_2 = UntypedAssetId::Uuid {
 | |
|             type_id: TypeId::of::<TestAsset>(),
 | |
|             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::<TestAsset>::Uuid { uuid: UUID_1 };
 | |
|         let untyped = UntypedAssetId::Uuid {
 | |
|             type_id: TypeId::of::<TestAsset>(),
 | |
|             uuid: UUID_1,
 | |
|         };
 | |
| 
 | |
|         assert_eq!(
 | |
|             hash(&typed),
 | |
|             hash(&AssetId::<TestAsset>::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::<TestAsset>::Uuid { uuid: UUID_1 };
 | |
|         let untyped = UntypedAssetId::Uuid {
 | |
|             type_id: TypeId::of::<TestAsset>(),
 | |
|             uuid: UUID_1,
 | |
|         };
 | |
| 
 | |
|         assert_eq!(Ok(typed), AssetId::try_from(untyped));
 | |
|         assert_eq!(UntypedAssetId::from(typed), untyped);
 | |
|     }
 | |
| }
 | 
