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#[derive(Reflect, Serialize, Deserialize, From)]
22#[reflect(Clone, Default, Debug, PartialEq, Hash)]
23pub enum AssetId<A: Asset> {
24 Index {
30 index: AssetIndex,
32 #[reflect(ignore, clone)]
34 marker: PhantomData<fn() -> A>,
35 },
36 Uuid {
41 uuid: Uuid,
43 },
44}
45
46impl<A: Asset> AssetId<A> {
47 pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631);
50
51 #[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 #[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 #[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#[derive(Debug, Copy, Clone, Reflect)]
177pub enum UntypedAssetId {
178 Index {
184 type_id: TypeId,
186 index: AssetIndex,
188 },
189 Uuid {
194 type_id: TypeId,
196 uuid: Uuid,
198 },
199}
200
201impl UntypedAssetId {
202 #[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 #[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 #[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 #[inline]
252 pub fn try_typed<A: Asset>(self) -> Result<AssetId<A>, UntypedAssetIdConversionError> {
253 AssetId::try_from(self)
254 }
255
256 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, From)]
328enum InternalAssetId {
329 Index(AssetIndex),
330 Uuid(Uuid),
331}
332
333#[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
356impl<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#[derive(Error, Debug, PartialEq, Clone)]
471#[non_exhaustive]
472pub enum UntypedAssetIdConversionError {
473 #[error("This UntypedAssetId is for {found:?} and cannot be converted into an AssetId<{expected:?}>")]
475 TypeIdMismatch {
476 expected: TypeId,
478 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 fn hash<T: Hash>(data: &T) -> u64 {
494 use core::hash::BuildHasher;
495
496 bevy_platform::hash::FixedHasher.hash_one(data)
497 }
498
499 #[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 #[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 #[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 #[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}