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};
#[derive(Reflect, Serialize, Deserialize, From)]
pub enum AssetId<A: Asset> {
Index {
index: AssetIndex,
#[reflect(ignore)]
marker: PhantomData<fn() -> A>,
},
Uuid { uuid: Uuid },
}
impl<A: Asset> AssetId<A> {
pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631);
pub const INVALID_UUID: Uuid = Uuid::from_u128(108428345662029828789348721013522787528);
#[inline]
pub const fn invalid() -> Self {
Self::Uuid {
uuid: Self::INVALID_UUID,
}
}
#[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,
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum UntypedAssetId {
Index { type_id: TypeId, index: AssetIndex },
Uuid { type_id: TypeId, uuid: Uuid },
}
impl UntypedAssetId {
#[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 },
}
}
#[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()
}
#[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
}
#[inline]
pub fn try_typed<A: Asset>(self) -> Result<AssetId<A>, UntypedAssetIdConversionError> {
AssetId::try_from(self)
}
#[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))
}
}
#[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 },
}
}
}
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 }),
}
}
}
#[derive(Error, Display, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub enum UntypedAssetIdConversionError {
#[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);
fn hash<T: Hash>(data: &T) -> u64 {
use core::hash::Hasher;
let mut hasher = bevy_utils::AHasher::default();
data.hash(&mut hasher);
hasher.finish()
}
#[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);
}
#[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);
}
#[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));
}
#[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);
}
}