Skip to main content

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