bevy_asset/
id.rs

1use crate::{Asset, AssetIndex};
2use bevy_reflect::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::{Display, Error, From};
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, From)]
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 core::fmt::Formatter<'_>) -> core::fmt::Result {
90        Debug::fmt(self, f)
91    }
92}
93
94impl<A: Asset> Debug for AssetId<A> {
95    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96        match self {
97            AssetId::Index { index, .. } => {
98                write!(
99                    f,
100                    "AssetId<{}>{{ index: {}, generation: {}}}",
101                    core::any::type_name::<A>(),
102                    index.index,
103                    index.generation
104                )
105            }
106            AssetId::Uuid { uuid } => {
107                write!(
108                    f,
109                    "AssetId<{}>{{uuid: {}}}",
110                    core::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: core::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<core::cmp::Ordering> {
137        Some(self.cmp(other))
138    }
139}
140
141impl<A: Asset> Ord for AssetId<A> {
142    fn cmp(&self, other: &Self) -> core::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
157/// An "untyped" / "generic-less" [`Asset`] identifier that behaves much like [`AssetId`], but stores the [`Asset`] type
158/// information at runtime instead of compile-time. This increases the size of the type, but it enables storing asset ids
159/// across asset types together and enables comparisons between them.
160#[derive(Debug, Copy, Clone)]
161pub enum UntypedAssetId {
162    /// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
163    /// the "default" identifier used for assets. The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are
164    /// explicitly registered that way.
165    ///
166    /// [`Assets`]: crate::Assets
167    Index { type_id: TypeId, index: AssetIndex },
168    /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
169    /// with one.
170    ///
171    /// [`Assets`]: crate::Assets
172    Uuid { type_id: TypeId, uuid: Uuid },
173}
174
175impl UntypedAssetId {
176    /// Converts this to a "typed" [`AssetId`] without checking the stored type to see if it matches the target `A` [`Asset`] type.
177    /// This should only be called if you are _absolutely certain_ the asset type matches the stored type. And even then, you should
178    /// consider using [`UntypedAssetId::typed_debug_checked`] instead.
179    #[inline]
180    pub fn typed_unchecked<A: Asset>(self) -> AssetId<A> {
181        match self {
182            UntypedAssetId::Index { index, .. } => AssetId::Index {
183                index,
184                marker: PhantomData,
185            },
186            UntypedAssetId::Uuid { uuid, .. } => AssetId::Uuid { uuid },
187        }
188    }
189
190    /// Converts this to a "typed" [`AssetId`]. When compiled in debug-mode it will check to see if the stored type
191    /// matches the target `A` [`Asset`] type. When compiled in release-mode, this check will be skipped.
192    ///
193    /// # Panics
194    ///
195    /// Panics if compiled in debug mode and the [`TypeId`] of `A` does not match the stored [`TypeId`].
196    #[inline]
197    pub fn typed_debug_checked<A: Asset>(self) -> AssetId<A> {
198        debug_assert_eq!(
199            self.type_id(),
200            TypeId::of::<A>(),
201            "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
202            core::any::type_name::<A>()
203        );
204        self.typed_unchecked()
205    }
206
207    /// Converts this to a "typed" [`AssetId`].
208    ///
209    /// # Panics
210    ///
211    /// Panics if the [`TypeId`] of `A` does not match the stored type id.
212    #[inline]
213    pub fn typed<A: Asset>(self) -> AssetId<A> {
214        let Ok(id) = self.try_typed() else {
215            panic!(
216                "The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
217                core::any::type_name::<A>()
218            )
219        };
220
221        id
222    }
223
224    /// Try to convert this to a "typed" [`AssetId`].
225    #[inline]
226    pub fn try_typed<A: Asset>(self) -> Result<AssetId<A>, UntypedAssetIdConversionError> {
227        AssetId::try_from(self)
228    }
229
230    /// Returns the stored [`TypeId`] of the referenced [`Asset`].
231    #[inline]
232    pub fn type_id(&self) -> TypeId {
233        match self {
234            UntypedAssetId::Index { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => {
235                *type_id
236            }
237        }
238    }
239
240    #[inline]
241    pub(crate) fn internal(self) -> InternalAssetId {
242        match self {
243            UntypedAssetId::Index { index, .. } => InternalAssetId::Index(index),
244            UntypedAssetId::Uuid { uuid, .. } => InternalAssetId::Uuid(uuid),
245        }
246    }
247}
248
249impl Display for UntypedAssetId {
250    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251        let mut writer = f.debug_struct("UntypedAssetId");
252        match self {
253            UntypedAssetId::Index { index, type_id } => {
254                writer
255                    .field("type_id", type_id)
256                    .field("index", &index.index)
257                    .field("generation", &index.generation);
258            }
259            UntypedAssetId::Uuid { uuid, type_id } => {
260                writer.field("type_id", type_id).field("uuid", uuid);
261            }
262        }
263        writer.finish()
264    }
265}
266
267impl PartialEq for UntypedAssetId {
268    #[inline]
269    fn eq(&self, other: &Self) -> bool {
270        self.type_id() == other.type_id() && self.internal().eq(&other.internal())
271    }
272}
273
274impl Eq for UntypedAssetId {}
275
276impl Hash for UntypedAssetId {
277    #[inline]
278    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
279        self.internal().hash(state);
280        self.type_id().hash(state);
281    }
282}
283
284impl Ord for UntypedAssetId {
285    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
286        self.type_id()
287            .cmp(&other.type_id())
288            .then_with(|| self.internal().cmp(&other.internal()))
289    }
290}
291
292impl PartialOrd for UntypedAssetId {
293    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
294        Some(self.cmp(other))
295    }
296}
297
298/// An asset id without static or dynamic types associated with it.
299///
300/// This exist to support efficient type erased id drop tracking. We
301/// could use [`UntypedAssetId`] for this, but the [`TypeId`] is unnecessary.
302///
303/// Do not _ever_ use this across asset types for comparison.
304/// [`InternalAssetId`] contains no type information and will happily collide
305/// with indices across types.
306#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, From)]
307pub(crate) enum InternalAssetId {
308    Index(AssetIndex),
309    Uuid(Uuid),
310}
311
312impl InternalAssetId {
313    #[inline]
314    pub(crate) fn typed<A: Asset>(self) -> AssetId<A> {
315        match self {
316            InternalAssetId::Index(index) => AssetId::Index {
317                index,
318                marker: PhantomData,
319            },
320            InternalAssetId::Uuid(uuid) => AssetId::Uuid { uuid },
321        }
322    }
323
324    #[inline]
325    pub(crate) fn untyped(self, type_id: TypeId) -> UntypedAssetId {
326        match self {
327            InternalAssetId::Index(index) => UntypedAssetId::Index { index, type_id },
328            InternalAssetId::Uuid(uuid) => UntypedAssetId::Uuid { uuid, type_id },
329        }
330    }
331}
332
333// Cross Operations
334
335impl<A: Asset> PartialEq<UntypedAssetId> for AssetId<A> {
336    #[inline]
337    fn eq(&self, other: &UntypedAssetId) -> bool {
338        TypeId::of::<A>() == other.type_id() && self.internal().eq(&other.internal())
339    }
340}
341
342impl<A: Asset> PartialEq<AssetId<A>> for UntypedAssetId {
343    #[inline]
344    fn eq(&self, other: &AssetId<A>) -> bool {
345        other.eq(self)
346    }
347}
348
349impl<A: Asset> PartialOrd<UntypedAssetId> for AssetId<A> {
350    #[inline]
351    fn partial_cmp(&self, other: &UntypedAssetId) -> Option<core::cmp::Ordering> {
352        if TypeId::of::<A>() != other.type_id() {
353            None
354        } else {
355            Some(self.internal().cmp(&other.internal()))
356        }
357    }
358}
359
360impl<A: Asset> PartialOrd<AssetId<A>> for UntypedAssetId {
361    #[inline]
362    fn partial_cmp(&self, other: &AssetId<A>) -> Option<core::cmp::Ordering> {
363        Some(other.partial_cmp(self)?.reverse())
364    }
365}
366
367impl<A: Asset> From<AssetId<A>> for UntypedAssetId {
368    #[inline]
369    fn from(value: AssetId<A>) -> Self {
370        let type_id = TypeId::of::<A>();
371
372        match value {
373            AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index },
374            AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid },
375        }
376    }
377}
378
379impl<A: Asset> TryFrom<UntypedAssetId> for AssetId<A> {
380    type Error = UntypedAssetIdConversionError;
381
382    #[inline]
383    fn try_from(value: UntypedAssetId) -> Result<Self, Self::Error> {
384        let found = value.type_id();
385        let expected = TypeId::of::<A>();
386
387        match value {
388            UntypedAssetId::Index { index, type_id } if type_id == expected => Ok(AssetId::Index {
389                index,
390                marker: PhantomData,
391            }),
392            UntypedAssetId::Uuid { uuid, type_id } if type_id == expected => {
393                Ok(AssetId::Uuid { uuid })
394            }
395            _ => Err(UntypedAssetIdConversionError::TypeIdMismatch { expected, found }),
396        }
397    }
398}
399
400/// Errors preventing the conversion of to/from an [`UntypedAssetId`] and an [`AssetId`].
401#[derive(Error, Display, Debug, PartialEq, Clone)]
402#[non_exhaustive]
403pub enum UntypedAssetIdConversionError {
404    /// Caused when trying to convert an [`UntypedAssetId`] into an [`AssetId`] of the wrong type.
405    #[display("This UntypedAssetId is for {found:?} and cannot be converted into an AssetId<{expected:?}>")]
406    TypeIdMismatch { expected: TypeId, found: TypeId },
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    type TestAsset = ();
414
415    const UUID_1: Uuid = Uuid::from_u128(123);
416    const UUID_2: Uuid = Uuid::from_u128(456);
417
418    /// Simple utility to directly hash a value using a fixed hasher
419    fn hash<T: Hash>(data: &T) -> u64 {
420        use core::hash::Hasher;
421
422        let mut hasher = bevy_utils::AHasher::default();
423        data.hash(&mut hasher);
424        hasher.finish()
425    }
426
427    /// Typed and Untyped `AssetIds` should be equivalent to each other and themselves
428    #[test]
429    fn equality() {
430        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
431        let untyped = UntypedAssetId::Uuid {
432            type_id: TypeId::of::<TestAsset>(),
433            uuid: UUID_1,
434        };
435
436        assert_eq!(Ok(typed), AssetId::try_from(untyped));
437        assert_eq!(UntypedAssetId::from(typed), untyped);
438        assert_eq!(typed, untyped);
439    }
440
441    /// Typed and Untyped `AssetIds` should be orderable amongst each other and themselves
442    #[test]
443    fn ordering() {
444        assert!(UUID_1 < UUID_2);
445
446        let typed_1 = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
447        let typed_2 = AssetId::<TestAsset>::Uuid { uuid: UUID_2 };
448        let untyped_1 = UntypedAssetId::Uuid {
449            type_id: TypeId::of::<TestAsset>(),
450            uuid: UUID_1,
451        };
452        let untyped_2 = UntypedAssetId::Uuid {
453            type_id: TypeId::of::<TestAsset>(),
454            uuid: UUID_2,
455        };
456
457        assert!(typed_1 < typed_2);
458        assert!(untyped_1 < untyped_2);
459
460        assert!(UntypedAssetId::from(typed_1) < untyped_2);
461        assert!(untyped_1 < UntypedAssetId::from(typed_2));
462
463        assert!(AssetId::try_from(untyped_1).unwrap() < typed_2);
464        assert!(typed_1 < AssetId::try_from(untyped_2).unwrap());
465
466        assert!(typed_1 < untyped_2);
467        assert!(untyped_1 < typed_2);
468    }
469
470    /// Typed and Untyped `AssetIds` should be equivalently hashable to each other and themselves
471    #[test]
472    fn hashing() {
473        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
474        let untyped = UntypedAssetId::Uuid {
475            type_id: TypeId::of::<TestAsset>(),
476            uuid: UUID_1,
477        };
478
479        assert_eq!(
480            hash(&typed),
481            hash(&AssetId::<TestAsset>::try_from(untyped).unwrap())
482        );
483        assert_eq!(hash(&UntypedAssetId::from(typed)), hash(&untyped));
484        assert_eq!(hash(&typed), hash(&untyped));
485    }
486
487    /// Typed and Untyped `AssetIds` should be interchangeable
488    #[test]
489    fn conversion() {
490        let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
491        let untyped = UntypedAssetId::Uuid {
492            type_id: TypeId::of::<TestAsset>(),
493            uuid: UUID_1,
494        };
495
496        assert_eq!(Ok(typed), AssetId::try_from(untyped));
497        assert_eq!(UntypedAssetId::from(typed), untyped);
498    }
499}