bevy_asset/
id.rs

1use crate::{Asset, AssetIndex};
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    pub(crate) 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)]
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    pub(crate) 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 exist to support efficient type erased id drop tracking. We
318/// could use [`UntypedAssetId`] for this, but the [`TypeId`] is unnecessary.
319///
320/// Do not _ever_ use this across asset types for comparison.
321/// [`InternalAssetId`] contains no type information and will happily collide
322/// with indices across types.
323#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, From)]
324pub(crate) enum InternalAssetId {
325    Index(AssetIndex),
326    Uuid(Uuid),
327}
328
329impl InternalAssetId {
330    #[inline]
331    pub(crate) fn typed<A: Asset>(self) -> AssetId<A> {
332        match self {
333            InternalAssetId::Index(index) => AssetId::Index {
334                index,
335                marker: PhantomData,
336            },
337            InternalAssetId::Uuid(uuid) => AssetId::Uuid { uuid },
338        }
339    }
340
341    #[inline]
342    pub(crate) fn untyped(self, type_id: TypeId) -> UntypedAssetId {
343        match self {
344            InternalAssetId::Index(index) => UntypedAssetId::Index { index, type_id },
345            InternalAssetId::Uuid(uuid) => UntypedAssetId::Uuid { uuid, type_id },
346        }
347    }
348}
349
350// Cross Operations
351
352impl<A: Asset> PartialEq<UntypedAssetId> for AssetId<A> {
353    #[inline]
354    fn eq(&self, other: &UntypedAssetId) -> bool {
355        TypeId::of::<A>() == other.type_id() && self.internal().eq(&other.internal())
356    }
357}
358
359impl<A: Asset> PartialEq<AssetId<A>> for UntypedAssetId {
360    #[inline]
361    fn eq(&self, other: &AssetId<A>) -> bool {
362        other.eq(self)
363    }
364}
365
366impl<A: Asset> PartialOrd<UntypedAssetId> for AssetId<A> {
367    #[inline]
368    fn partial_cmp(&self, other: &UntypedAssetId) -> Option<core::cmp::Ordering> {
369        if TypeId::of::<A>() != other.type_id() {
370            None
371        } else {
372            Some(self.internal().cmp(&other.internal()))
373        }
374    }
375}
376
377impl<A: Asset> PartialOrd<AssetId<A>> for UntypedAssetId {
378    #[inline]
379    fn partial_cmp(&self, other: &AssetId<A>) -> Option<core::cmp::Ordering> {
380        Some(other.partial_cmp(self)?.reverse())
381    }
382}
383
384impl<A: Asset> From<AssetId<A>> for UntypedAssetId {
385    #[inline]
386    fn from(value: AssetId<A>) -> Self {
387        let type_id = TypeId::of::<A>();
388
389        match value {
390            AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index },
391            AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid },
392        }
393    }
394}
395
396impl<A: Asset> TryFrom<UntypedAssetId> for AssetId<A> {
397    type Error = UntypedAssetIdConversionError;
398
399    #[inline]
400    fn try_from(value: UntypedAssetId) -> Result<Self, Self::Error> {
401        let found = value.type_id();
402        let expected = TypeId::of::<A>();
403
404        match value {
405            UntypedAssetId::Index { index, type_id } if type_id == expected => Ok(AssetId::Index {
406                index,
407                marker: PhantomData,
408            }),
409            UntypedAssetId::Uuid { uuid, type_id } if type_id == expected => {
410                Ok(AssetId::Uuid { uuid })
411            }
412            _ => Err(UntypedAssetIdConversionError::TypeIdMismatch { expected, found }),
413        }
414    }
415}
416
417/// Errors preventing the conversion of to/from an [`UntypedAssetId`] and an [`AssetId`].
418#[derive(Error, Debug, PartialEq, Clone)]
419#[non_exhaustive]
420pub enum UntypedAssetIdConversionError {
421    /// Caused when trying to convert an [`UntypedAssetId`] into an [`AssetId`] of the wrong type.
422    #[error("This UntypedAssetId is for {found:?} and cannot be converted into an AssetId<{expected:?}>")]
423    TypeIdMismatch {
424        /// The [`TypeId`] of the asset that we are trying to convert to.
425        expected: TypeId,
426        /// The [`TypeId`] of the asset that we are trying to convert from.
427        found: TypeId,
428    },
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    type TestAsset = ();
436
437    const UUID_1: Uuid = Uuid::from_u128(123);
438    const UUID_2: Uuid = Uuid::from_u128(456);
439
440    /// Simple utility to directly hash a value using a fixed hasher
441    fn hash<T: Hash>(data: &T) -> u64 {
442        use core::hash::BuildHasher;
443
444        bevy_platform::hash::FixedHasher.hash_one(data)
445    }
446
447    /// Typed and Untyped `AssetIds` should be equivalent to each other and themselves
448    #[test]
449    fn equality() {
450        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
451        let untyped = UntypedAssetId::Uuid {
452            type_id: TypeId::of::<TestAsset>(),
453            uuid: UUID_1,
454        };
455
456        assert_eq!(Ok(typed), AssetId::try_from(untyped));
457        assert_eq!(UntypedAssetId::from(typed), untyped);
458        assert_eq!(typed, untyped);
459    }
460
461    /// Typed and Untyped `AssetIds` should be orderable amongst each other and themselves
462    #[test]
463    fn ordering() {
464        assert!(UUID_1 < UUID_2);
465
466        let typed_1 = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
467        let typed_2 = AssetId::<TestAsset>::Uuid { uuid: UUID_2 };
468        let untyped_1 = UntypedAssetId::Uuid {
469            type_id: TypeId::of::<TestAsset>(),
470            uuid: UUID_1,
471        };
472        let untyped_2 = UntypedAssetId::Uuid {
473            type_id: TypeId::of::<TestAsset>(),
474            uuid: UUID_2,
475        };
476
477        assert!(typed_1 < typed_2);
478        assert!(untyped_1 < untyped_2);
479
480        assert!(UntypedAssetId::from(typed_1) < untyped_2);
481        assert!(untyped_1 < UntypedAssetId::from(typed_2));
482
483        assert!(AssetId::try_from(untyped_1).unwrap() < typed_2);
484        assert!(typed_1 < AssetId::try_from(untyped_2).unwrap());
485
486        assert!(typed_1 < untyped_2);
487        assert!(untyped_1 < typed_2);
488    }
489
490    /// Typed and Untyped `AssetIds` should be equivalently hashable to each other and themselves
491    #[test]
492    fn hashing() {
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!(
500            hash(&typed),
501            hash(&AssetId::<TestAsset>::try_from(untyped).unwrap())
502        );
503        assert_eq!(hash(&UntypedAssetId::from(typed)), hash(&untyped));
504        assert_eq!(hash(&typed), hash(&untyped));
505    }
506
507    /// Typed and Untyped `AssetIds` should be interchangeable
508    #[test]
509    fn conversion() {
510        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
511        let untyped = UntypedAssetId::Uuid {
512            type_id: TypeId::of::<TestAsset>(),
513            uuid: UUID_1,
514        };
515
516        assert_eq!(Ok(typed), AssetId::try_from(untyped));
517        assert_eq!(UntypedAssetId::from(typed), untyped);
518    }
519}