bevy_asset/
id.rs

1use crate::{Asset, AssetIndex, Handle, UntypedHandle};
2use bevy_reflect::{std_traits::ReflectDefault, Reflect};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use core::{
7    any::TypeId,
8    fmt::{Debug, Display},
9    hash::Hash,
10    marker::PhantomData,
11};
12use derive_more::derive::From;
13use thiserror::Error;
14
15/// A unique runtime-only identifier for an [`Asset`]. This is cheap to [`Copy`]/[`Clone`] and is not directly tied to the
16/// lifetime of the Asset. This means it _can_ point to an [`Asset`] that no longer exists.
17///
18/// For an identifier tied to the lifetime of an asset, see [`Handle`](`crate::Handle`).
19///
20/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
21#[derive(Reflect, Serialize, Deserialize, From)]
22#[reflect(Clone, Default, Debug, PartialEq, Hash)]
23pub enum AssetId<A: Asset> {
24    /// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
25    /// the "default" identifier used for assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are
26    /// explicitly registered that way.
27    ///
28    /// [`Assets`]: crate::Assets
29    Index {
30        /// The unstable, opaque index of the asset.
31        index: AssetIndex,
32        /// A marker to store the type information of the asset.
33        #[reflect(ignore, clone)]
34        marker: PhantomData<fn() -> A>,
35    },
36    /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
37    /// with one.
38    ///
39    /// [`Assets`]: crate::Assets
40    Uuid {
41        /// The UUID provided during asset registration.
42        uuid: Uuid,
43    },
44}
45
46impl<A: Asset> AssetId<A> {
47    /// The uuid for the default [`AssetId`]. It is valid to assign a value to this in [`Assets`](crate::Assets)
48    /// and by convention (where appropriate) assets should support this pattern.
49    pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631);
50
51    /// This asset id _should_ never be valid. Assigning a value to this in [`Assets`](crate::Assets) will
52    /// produce undefined behavior, so don't do it!
53    pub const INVALID_UUID: Uuid = Uuid::from_u128(108428345662029828789348721013522787528);
54
55    /// Returns an [`AssetId`] with [`Self::INVALID_UUID`], which _should_ never be assigned to.
56    #[inline]
57    pub const fn invalid() -> Self {
58        Self::Uuid {
59            uuid: Self::INVALID_UUID,
60        }
61    }
62
63    /// Converts this to an "untyped" / "generic-less" [`Asset`] identifier that stores the type information
64    /// _inside_ the [`UntypedAssetId`].
65    #[inline]
66    pub fn untyped(self) -> UntypedAssetId {
67        self.into()
68    }
69
70    #[inline]
71    fn internal(self) -> InternalAssetId {
72        match self {
73            AssetId::Index { index, .. } => InternalAssetId::Index(index),
74            AssetId::Uuid { uuid } => InternalAssetId::Uuid(uuid),
75        }
76    }
77}
78
79impl<A: Asset> Default for AssetId<A> {
80    fn default() -> Self {
81        AssetId::Uuid {
82            uuid: Self::DEFAULT_UUID,
83        }
84    }
85}
86
87impl<A: Asset> Clone for AssetId<A> {
88    fn clone(&self) -> Self {
89        *self
90    }
91}
92
93impl<A: Asset> Copy for AssetId<A> {}
94
95impl<A: Asset> Display for AssetId<A> {
96    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
97        Debug::fmt(self, f)
98    }
99}
100
101impl<A: Asset> Debug for AssetId<A> {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        match self {
104            AssetId::Index { index, .. } => {
105                write!(
106                    f,
107                    "AssetId<{}>{{ index: {}, generation: {}}}",
108                    core::any::type_name::<A>(),
109                    index.index,
110                    index.generation
111                )
112            }
113            AssetId::Uuid { uuid } => {
114                write!(
115                    f,
116                    "AssetId<{}>{{uuid: {}}}",
117                    core::any::type_name::<A>(),
118                    uuid
119                )
120            }
121        }
122    }
123}
124
125impl<A: Asset> Hash for AssetId<A> {
126    #[inline]
127    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
128        self.internal().hash(state);
129        TypeId::of::<A>().hash(state);
130    }
131}
132
133impl<A: Asset> PartialEq for AssetId<A> {
134    #[inline]
135    fn eq(&self, other: &Self) -> bool {
136        self.internal().eq(&other.internal())
137    }
138}
139
140impl<A: Asset> Eq for AssetId<A> {}
141
142impl<A: Asset> PartialOrd for AssetId<A> {
143    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
144        Some(self.cmp(other))
145    }
146}
147
148impl<A: Asset> Ord for AssetId<A> {
149    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
150        self.internal().cmp(&other.internal())
151    }
152}
153
154impl<A: Asset> From<AssetIndex> for AssetId<A> {
155    #[inline]
156    fn from(value: AssetIndex) -> Self {
157        Self::Index {
158            index: value,
159            marker: PhantomData,
160        }
161    }
162}
163
164/// An "untyped" / "generic-less" [`Asset`] identifier that behaves much like [`AssetId`], but stores the [`Asset`] type
165/// information at runtime instead of compile-time. This increases the size of the type, but it enables storing asset ids
166/// across asset types together and enables comparisons between them.
167#[derive(Debug, Copy, Clone, Reflect)]
168pub enum UntypedAssetId {
169    /// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
170    /// the "default" identifier used for assets. The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are
171    /// explicitly registered that way.
172    ///
173    /// [`Assets`]: crate::Assets
174    Index {
175        /// An identifier that records the underlying asset type.
176        type_id: TypeId,
177        /// The unstable, opaque index of the asset.
178        index: AssetIndex,
179    },
180    /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
181    /// with one.
182    ///
183    /// [`Assets`]: crate::Assets
184    Uuid {
185        /// An identifier that records the underlying asset type.
186        type_id: TypeId,
187        /// The UUID provided during asset registration.
188        uuid: Uuid,
189    },
190}
191
192impl UntypedAssetId {
193    /// Converts this to a "typed" [`AssetId`] without checking the stored type to see if it matches the target `A` [`Asset`] type.
194    /// This should only be called if you are _absolutely certain_ the asset type matches the stored type. And even then, you should
195    /// consider using [`UntypedAssetId::typed_debug_checked`] instead.
196    #[inline]
197    pub fn typed_unchecked<A: Asset>(self) -> AssetId<A> {
198        match self {
199            UntypedAssetId::Index { index, .. } => AssetId::Index {
200                index,
201                marker: PhantomData,
202            },
203            UntypedAssetId::Uuid { uuid, .. } => AssetId::Uuid { uuid },
204        }
205    }
206
207    /// Converts this to a "typed" [`AssetId`]. When compiled in debug-mode it will check to see if the stored type
208    /// matches the target `A` [`Asset`] type. When compiled in release-mode, this check will be skipped.
209    ///
210    /// # Panics
211    ///
212    /// Panics if compiled in debug mode and the [`TypeId`] of `A` does not match the stored [`TypeId`].
213    #[inline]
214    pub fn typed_debug_checked<A: Asset>(self) -> AssetId<A> {
215        debug_assert_eq!(
216            self.type_id(),
217            TypeId::of::<A>(),
218            "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
219            core::any::type_name::<A>()
220        );
221        self.typed_unchecked()
222    }
223
224    /// Converts this to a "typed" [`AssetId`].
225    ///
226    /// # Panics
227    ///
228    /// Panics if the [`TypeId`] of `A` does not match the stored type id.
229    #[inline]
230    pub fn typed<A: Asset>(self) -> AssetId<A> {
231        let Ok(id) = self.try_typed() else {
232            panic!(
233                "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
234                core::any::type_name::<A>()
235            )
236        };
237
238        id
239    }
240
241    /// Try to convert this to a "typed" [`AssetId`].
242    #[inline]
243    pub fn try_typed<A: Asset>(self) -> Result<AssetId<A>, UntypedAssetIdConversionError> {
244        AssetId::try_from(self)
245    }
246
247    /// Returns the stored [`TypeId`] of the referenced [`Asset`].
248    #[inline]
249    pub fn type_id(&self) -> TypeId {
250        match self {
251            UntypedAssetId::Index { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => {
252                *type_id
253            }
254        }
255    }
256
257    #[inline]
258    fn internal(self) -> InternalAssetId {
259        match self {
260            UntypedAssetId::Index { index, .. } => InternalAssetId::Index(index),
261            UntypedAssetId::Uuid { uuid, .. } => InternalAssetId::Uuid(uuid),
262        }
263    }
264}
265
266impl Display for UntypedAssetId {
267    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
268        let mut writer = f.debug_struct("UntypedAssetId");
269        match self {
270            UntypedAssetId::Index { index, type_id } => {
271                writer
272                    .field("type_id", type_id)
273                    .field("index", &index.index)
274                    .field("generation", &index.generation);
275            }
276            UntypedAssetId::Uuid { uuid, type_id } => {
277                writer.field("type_id", type_id).field("uuid", uuid);
278            }
279        }
280        writer.finish()
281    }
282}
283
284impl PartialEq for UntypedAssetId {
285    #[inline]
286    fn eq(&self, other: &Self) -> bool {
287        self.type_id() == other.type_id() && self.internal().eq(&other.internal())
288    }
289}
290
291impl Eq for UntypedAssetId {}
292
293impl Hash for UntypedAssetId {
294    #[inline]
295    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
296        self.internal().hash(state);
297        self.type_id().hash(state);
298    }
299}
300
301impl Ord for UntypedAssetId {
302    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
303        self.type_id()
304            .cmp(&other.type_id())
305            .then_with(|| self.internal().cmp(&other.internal()))
306    }
307}
308
309impl PartialOrd for UntypedAssetId {
310    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
311        Some(self.cmp(other))
312    }
313}
314
315/// An asset id without static or dynamic types associated with it.
316///
317/// This is provided to make implementing traits easier for the many different asset ID types.
318#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, From)]
319enum InternalAssetId {
320    Index(AssetIndex),
321    Uuid(Uuid),
322}
323
324/// An asset index bundled with its (dynamic) type.
325#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
326pub(crate) struct ErasedAssetIndex {
327    pub(crate) index: AssetIndex,
328    pub(crate) type_id: TypeId,
329}
330
331impl ErasedAssetIndex {
332    pub(crate) fn new(index: AssetIndex, type_id: TypeId) -> Self {
333        Self { index, type_id }
334    }
335}
336
337impl Display for ErasedAssetIndex {
338    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
339        f.debug_struct("ErasedAssetIndex")
340            .field("type_id", &self.type_id)
341            .field("index", &self.index.index)
342            .field("generation", &self.index.generation)
343            .finish()
344    }
345}
346
347// Cross Operations
348
349impl<A: Asset> PartialEq<UntypedAssetId> for AssetId<A> {
350    #[inline]
351    fn eq(&self, other: &UntypedAssetId) -> bool {
352        TypeId::of::<A>() == other.type_id() && self.internal().eq(&other.internal())
353    }
354}
355
356impl<A: Asset> PartialEq<AssetId<A>> for UntypedAssetId {
357    #[inline]
358    fn eq(&self, other: &AssetId<A>) -> bool {
359        other.eq(self)
360    }
361}
362
363impl<A: Asset> PartialOrd<UntypedAssetId> for AssetId<A> {
364    #[inline]
365    fn partial_cmp(&self, other: &UntypedAssetId) -> Option<core::cmp::Ordering> {
366        if TypeId::of::<A>() != other.type_id() {
367            None
368        } else {
369            Some(self.internal().cmp(&other.internal()))
370        }
371    }
372}
373
374impl<A: Asset> PartialOrd<AssetId<A>> for UntypedAssetId {
375    #[inline]
376    fn partial_cmp(&self, other: &AssetId<A>) -> Option<core::cmp::Ordering> {
377        Some(other.partial_cmp(self)?.reverse())
378    }
379}
380
381impl<A: Asset> From<AssetId<A>> for UntypedAssetId {
382    #[inline]
383    fn from(value: AssetId<A>) -> Self {
384        let type_id = TypeId::of::<A>();
385
386        match value {
387            AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index },
388            AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid },
389        }
390    }
391}
392
393impl<A: Asset> TryFrom<UntypedAssetId> for AssetId<A> {
394    type Error = UntypedAssetIdConversionError;
395
396    #[inline]
397    fn try_from(value: UntypedAssetId) -> Result<Self, Self::Error> {
398        let found = value.type_id();
399        let expected = TypeId::of::<A>();
400
401        match value {
402            UntypedAssetId::Index { index, type_id } if type_id == expected => Ok(AssetId::Index {
403                index,
404                marker: PhantomData,
405            }),
406            UntypedAssetId::Uuid { uuid, type_id } if type_id == expected => {
407                Ok(AssetId::Uuid { uuid })
408            }
409            _ => Err(UntypedAssetIdConversionError::TypeIdMismatch { expected, found }),
410        }
411    }
412}
413
414impl TryFrom<UntypedAssetId> for ErasedAssetIndex {
415    type Error = UuidNotSupportedError;
416
417    fn try_from(asset_id: UntypedAssetId) -> Result<Self, Self::Error> {
418        match asset_id {
419            UntypedAssetId::Index { type_id, index } => Ok(ErasedAssetIndex { index, type_id }),
420            UntypedAssetId::Uuid { .. } => Err(UuidNotSupportedError),
421        }
422    }
423}
424
425impl<A: Asset> TryFrom<&Handle<A>> for ErasedAssetIndex {
426    type Error = UuidNotSupportedError;
427
428    fn try_from(handle: &Handle<A>) -> Result<Self, Self::Error> {
429        match handle {
430            Handle::Strong(handle) => Ok(Self::new(handle.index, handle.type_id)),
431            Handle::Uuid(..) => Err(UuidNotSupportedError),
432        }
433    }
434}
435
436impl TryFrom<&UntypedHandle> for ErasedAssetIndex {
437    type Error = UuidNotSupportedError;
438
439    fn try_from(handle: &UntypedHandle) -> Result<Self, Self::Error> {
440        match handle {
441            UntypedHandle::Strong(handle) => Ok(Self::new(handle.index, handle.type_id)),
442            UntypedHandle::Uuid { .. } => Err(UuidNotSupportedError),
443        }
444    }
445}
446
447impl From<ErasedAssetIndex> for UntypedAssetId {
448    fn from(value: ErasedAssetIndex) -> Self {
449        Self::Index {
450            type_id: value.type_id,
451            index: value.index,
452        }
453    }
454}
455
456#[derive(Error, Debug)]
457#[error("Attempted to create a TypedAssetIndex from a Uuid")]
458pub(crate) struct UuidNotSupportedError;
459
460/// Errors preventing the conversion of to/from an [`UntypedAssetId`] and an [`AssetId`].
461#[derive(Error, Debug, PartialEq, Clone)]
462#[non_exhaustive]
463pub enum UntypedAssetIdConversionError {
464    /// Caused when trying to convert an [`UntypedAssetId`] into an [`AssetId`] of the wrong type.
465    #[error("This UntypedAssetId is for {found:?} and cannot be converted into an AssetId<{expected:?}>")]
466    TypeIdMismatch {
467        /// The [`TypeId`] of the asset that we are trying to convert to.
468        expected: TypeId,
469        /// The [`TypeId`] of the asset that we are trying to convert from.
470        found: TypeId,
471    },
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    type TestAsset = ();
479
480    const UUID_1: Uuid = Uuid::from_u128(123);
481    const UUID_2: Uuid = Uuid::from_u128(456);
482
483    /// Simple utility to directly hash a value using a fixed hasher
484    fn hash<T: Hash>(data: &T) -> u64 {
485        use core::hash::BuildHasher;
486
487        bevy_platform::hash::FixedHasher.hash_one(data)
488    }
489
490    /// Typed and Untyped `AssetIds` should be equivalent to each other and themselves
491    #[test]
492    fn equality() {
493        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
494        let untyped = UntypedAssetId::Uuid {
495            type_id: TypeId::of::<TestAsset>(),
496            uuid: UUID_1,
497        };
498
499        assert_eq!(Ok(typed), AssetId::try_from(untyped));
500        assert_eq!(UntypedAssetId::from(typed), untyped);
501        assert_eq!(typed, untyped);
502    }
503
504    /// Typed and Untyped `AssetIds` should be orderable amongst each other and themselves
505    #[test]
506    fn ordering() {
507        assert!(UUID_1 < UUID_2);
508
509        let typed_1 = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
510        let typed_2 = AssetId::<TestAsset>::Uuid { uuid: UUID_2 };
511        let untyped_1 = UntypedAssetId::Uuid {
512            type_id: TypeId::of::<TestAsset>(),
513            uuid: UUID_1,
514        };
515        let untyped_2 = UntypedAssetId::Uuid {
516            type_id: TypeId::of::<TestAsset>(),
517            uuid: UUID_2,
518        };
519
520        assert!(typed_1 < typed_2);
521        assert!(untyped_1 < untyped_2);
522
523        assert!(UntypedAssetId::from(typed_1) < untyped_2);
524        assert!(untyped_1 < UntypedAssetId::from(typed_2));
525
526        assert!(AssetId::try_from(untyped_1).unwrap() < typed_2);
527        assert!(typed_1 < AssetId::try_from(untyped_2).unwrap());
528
529        assert!(typed_1 < untyped_2);
530        assert!(untyped_1 < typed_2);
531    }
532
533    /// Typed and Untyped `AssetIds` should be equivalently hashable to each other and themselves
534    #[test]
535    fn hashing() {
536        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
537        let untyped = UntypedAssetId::Uuid {
538            type_id: TypeId::of::<TestAsset>(),
539            uuid: UUID_1,
540        };
541
542        assert_eq!(
543            hash(&typed),
544            hash(&AssetId::<TestAsset>::try_from(untyped).unwrap())
545        );
546        assert_eq!(hash(&UntypedAssetId::from(typed)), hash(&untyped));
547        assert_eq!(hash(&typed), hash(&untyped));
548    }
549
550    /// Typed and Untyped `AssetIds` should be interchangeable
551    #[test]
552    fn conversion() {
553        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
554        let untyped = UntypedAssetId::Uuid {
555            type_id: TypeId::of::<TestAsset>(),
556            uuid: UUID_1,
557        };
558
559        assert_eq!(Ok(typed), AssetId::try_from(untyped));
560        assert_eq!(UntypedAssetId::from(typed), untyped);
561    }
562}