bevy_asset/
id.rs

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