Skip to main content

goud_engine/assets/
handle.rs

1//! Asset handles for type-safe, reference-counted asset access.
2//!
3//! This module provides specialized handle types for the asset system:
4//!
5//! - [`AssetHandle<A>`]: A typed handle to a specific asset type
6//! - [`UntypedAssetHandle`]: A type-erased handle for dynamic asset access
7//! - [`AssetPath`]: A path-based identifier for assets
8//!
9//! # Design Philosophy
10//!
11//! Asset handles differ from regular [`Handle<T>`](crate::core::handle::Handle) in several ways:
12//!
13//! 1. **Load State Tracking**: Asset handles know if their asset is loading, loaded, or failed
14//! 2. **Path Association**: Assets can be loaded by path, and handles preserve this association
15//! 3. **Type Erasure**: Untyped handles allow heterogeneous asset collections
16//! 4. **Reference Semantics**: Multiple handles can reference the same asset
17//!
18//! # FFI Safety
19//!
20//! Both `AssetHandle<A>` and `UntypedAssetHandle` are FFI-compatible:
21//! - Fixed size (16 bytes for typed, 24 bytes for untyped)
22//! - `#[repr(C)]` layout
23//! - Can be converted to/from integer representations
24//!
25//! # Example
26//!
27//! ```
28//! use goud_engine::assets::{Asset, AssetHandle, AssetPath, HandleLoadState};
29//!
30//! // Define a texture asset type
31//! struct Texture { width: u32, height: u32 }
32//! impl Asset for Texture {}
33//!
34//! // Create a handle (normally done by AssetServer)
35//! let handle: AssetHandle<Texture> = AssetHandle::new(0, 1);
36//!
37//! // Check handle state
38//! assert!(handle.is_valid());
39//!
40//! // Asset handles can be associated with paths
41//! let path = AssetPath::new("textures/player.png");
42//! assert_eq!(path.extension(), Some("png"));
43//! ```
44
45use crate::assets::{Asset, AssetId, AssetState, AssetType};
46use std::borrow::Cow;
47use std::fmt;
48use std::hash::{Hash, Hasher};
49use std::marker::PhantomData;
50use std::path::Path;
51
52// =============================================================================
53// AssetHandle<A>
54// =============================================================================
55
56/// A typed handle to an asset of type `A`.
57///
58/// `AssetHandle` is the primary way to reference loaded assets. It provides:
59///
60/// - **Type Safety**: `AssetHandle<Texture>` and `AssetHandle<Audio>` are distinct types
61/// - **Generation Counting**: Prevents use-after-free when assets are unloaded
62/// - **FFI Compatibility**: Can be passed across language boundaries
63///
64/// # Handle States
65///
66/// An asset handle can be in several states:
67/// - **Invalid**: The sentinel value `INVALID`, representing "no asset"
68/// - **Valid but Loading**: Points to an asset slot where loading is in progress
69/// - **Valid and Loaded**: Points to a fully loaded, usable asset
70/// - **Stale**: The asset was unloaded, handle generation no longer matches
71///
72/// Use `AssetServer::get_state()` to check the current state of an asset.
73///
74/// # FFI Layout
75///
76/// ```text
77/// Offset 0:  index      (u32, 4 bytes)
78/// Offset 4:  generation (u32, 4 bytes)
79/// Offset 8:  _marker    (PhantomData, 0 bytes)
80/// Total:     8 bytes, alignment 4
81/// ```
82///
83/// # Example
84///
85/// ```
86/// use goud_engine::assets::{Asset, AssetHandle};
87///
88/// struct Shader { /* ... */ }
89/// impl Asset for Shader {}
90///
91/// // Create an invalid handle (default)
92/// let handle: AssetHandle<Shader> = AssetHandle::INVALID;
93/// assert!(!handle.is_valid());
94///
95/// // Create a valid handle (normally done by AssetServer)
96/// let handle: AssetHandle<Shader> = AssetHandle::new(42, 1);
97/// assert!(handle.is_valid());
98/// assert_eq!(handle.index(), 42);
99/// assert_eq!(handle.generation(), 1);
100/// ```
101#[repr(C)]
102pub struct AssetHandle<A: Asset> {
103    /// Slot index in the asset storage.
104    index: u32,
105
106    /// Generation counter for stale handle detection.
107    generation: u32,
108
109    /// Zero-sized marker for type safety.
110    _marker: PhantomData<A>,
111}
112
113impl<A: Asset> AssetHandle<A> {
114    /// The invalid handle constant, representing "no asset".
115    ///
116    /// This is the default value for `AssetHandle` and is guaranteed to never
117    /// match any valid asset handle.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use goud_engine::assets::{Asset, AssetHandle};
123    ///
124    /// struct Mesh;
125    /// impl Asset for Mesh {}
126    ///
127    /// let handle: AssetHandle<Mesh> = AssetHandle::INVALID;
128    /// assert!(!handle.is_valid());
129    /// assert_eq!(handle.index(), u32::MAX);
130    /// assert_eq!(handle.generation(), 0);
131    /// ```
132    pub const INVALID: Self = Self {
133        index: u32::MAX,
134        generation: 0,
135        _marker: PhantomData,
136    };
137
138    /// Creates a new asset handle with the given index and generation.
139    ///
140    /// This is typically called by the asset system, not by user code.
141    ///
142    /// # Arguments
143    ///
144    /// * `index` - Slot index in the asset storage
145    /// * `generation` - Generation counter for this slot
146    ///
147    /// # Example
148    ///
149    /// ```
150    /// use goud_engine::assets::{Asset, AssetHandle};
151    ///
152    /// struct Audio;
153    /// impl Asset for Audio {}
154    ///
155    /// let handle: AssetHandle<Audio> = AssetHandle::new(10, 3);
156    /// assert_eq!(handle.index(), 10);
157    /// assert_eq!(handle.generation(), 3);
158    /// ```
159    #[inline]
160    pub const fn new(index: u32, generation: u32) -> Self {
161        Self {
162            index,
163            generation,
164            _marker: PhantomData,
165        }
166    }
167
168    /// Returns the index component of this handle.
169    ///
170    /// The index is the slot number in the asset storage array.
171    #[inline]
172    pub const fn index(&self) -> u32 {
173        self.index
174    }
175
176    /// Returns the generation component of this handle.
177    ///
178    /// The generation is incremented each time a slot is reused,
179    /// preventing stale handles from accessing wrong assets.
180    #[inline]
181    pub const fn generation(&self) -> u32 {
182        self.generation
183    }
184
185    /// Returns `true` if this handle is not the `INVALID` sentinel.
186    ///
187    /// Note: A "valid" handle may still be stale if the asset was unloaded.
188    /// Use `AssetServer::is_alive()` for definitive liveness checks.
189    ///
190    /// # Example
191    ///
192    /// ```
193    /// use goud_engine::assets::{Asset, AssetHandle};
194    ///
195    /// struct Font;
196    /// impl Asset for Font {}
197    ///
198    /// let valid: AssetHandle<Font> = AssetHandle::new(0, 1);
199    /// assert!(valid.is_valid());
200    ///
201    /// let invalid: AssetHandle<Font> = AssetHandle::INVALID;
202    /// assert!(!invalid.is_valid());
203    /// ```
204    #[inline]
205    pub const fn is_valid(&self) -> bool {
206        !(self.index == u32::MAX && self.generation == 0)
207    }
208
209    /// Converts this handle to a type-erased `UntypedAssetHandle`.
210    ///
211    /// The untyped handle preserves the asset type ID for runtime type checking.
212    ///
213    /// # Example
214    ///
215    /// ```
216    /// use goud_engine::assets::{Asset, AssetHandle, AssetId};
217    ///
218    /// struct Texture;
219    /// impl Asset for Texture {}
220    ///
221    /// let typed: AssetHandle<Texture> = AssetHandle::new(5, 2);
222    /// let untyped = typed.untyped();
223    ///
224    /// assert_eq!(untyped.index(), 5);
225    /// assert_eq!(untyped.generation(), 2);
226    /// assert_eq!(untyped.asset_id(), AssetId::of::<Texture>());
227    /// ```
228    #[inline]
229    pub fn untyped(&self) -> UntypedAssetHandle {
230        UntypedAssetHandle::new(self.index, self.generation, AssetId::of::<A>())
231    }
232
233    /// Packs this handle into a single u64 value for FFI.
234    ///
235    /// Format: upper 32 bits = generation, lower 32 bits = index.
236    ///
237    /// Note: This does NOT preserve the asset type; use `untyped()` if you
238    /// need type information across FFI boundaries.
239    #[inline]
240    pub const fn to_u64(&self) -> u64 {
241        ((self.generation as u64) << 32) | (self.index as u64)
242    }
243
244    /// Creates a handle from a packed u64 value.
245    ///
246    /// Format: upper 32 bits = generation, lower 32 bits = index.
247    #[inline]
248    pub const fn from_u64(packed: u64) -> Self {
249        let index = packed as u32;
250        let generation = (packed >> 32) as u32;
251        Self::new(index, generation)
252    }
253
254    /// Returns the asset type ID for this handle's asset type.
255    ///
256    /// This is a convenience method equivalent to `AssetId::of::<A>()`.
257    #[inline]
258    pub fn asset_id() -> AssetId {
259        AssetId::of::<A>()
260    }
261
262    /// Returns the asset type category for this handle's asset type.
263    ///
264    /// This is a convenience method equivalent to `A::asset_type()`.
265    #[inline]
266    pub fn asset_type() -> AssetType {
267        A::asset_type()
268    }
269}
270
271// Trait implementations for AssetHandle<A>
272
273impl<A: Asset> Clone for AssetHandle<A> {
274    #[inline]
275    fn clone(&self) -> Self {
276        *self
277    }
278}
279
280impl<A: Asset> Copy for AssetHandle<A> {}
281
282impl<A: Asset> Default for AssetHandle<A> {
283    /// Returns `AssetHandle::INVALID`.
284    #[inline]
285    fn default() -> Self {
286        Self::INVALID
287    }
288}
289
290impl<A: Asset> fmt::Debug for AssetHandle<A> {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        let type_name = A::asset_type_name();
293        if self.is_valid() {
294            write!(
295                f,
296                "AssetHandle<{}>({}:{})",
297                type_name, self.index, self.generation
298            )
299        } else {
300            write!(f, "AssetHandle<{}>(INVALID)", type_name)
301        }
302    }
303}
304
305impl<A: Asset> fmt::Display for AssetHandle<A> {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        if self.is_valid() {
308            write!(f, "{}:{}", self.index, self.generation)
309        } else {
310            write!(f, "INVALID")
311        }
312    }
313}
314
315impl<A: Asset> PartialEq for AssetHandle<A> {
316    #[inline]
317    fn eq(&self, other: &Self) -> bool {
318        self.index == other.index && self.generation == other.generation
319    }
320}
321
322impl<A: Asset> Eq for AssetHandle<A> {}
323
324impl<A: Asset> Hash for AssetHandle<A> {
325    #[inline]
326    fn hash<H: Hasher>(&self, state: &mut H) {
327        self.to_u64().hash(state);
328    }
329}
330
331impl<A: Asset> PartialOrd for AssetHandle<A> {
332    #[inline]
333    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
334        Some(self.cmp(other))
335    }
336}
337
338impl<A: Asset> Ord for AssetHandle<A> {
339    #[inline]
340    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
341        self.to_u64().cmp(&other.to_u64())
342    }
343}
344
345// Conversions
346impl<A: Asset> From<AssetHandle<A>> for u64 {
347    #[inline]
348    fn from(handle: AssetHandle<A>) -> u64 {
349        handle.to_u64()
350    }
351}
352
353impl<A: Asset> From<u64> for AssetHandle<A> {
354    #[inline]
355    fn from(packed: u64) -> Self {
356        Self::from_u64(packed)
357    }
358}
359
360// =============================================================================
361// UntypedAssetHandle
362// =============================================================================
363
364/// A type-erased handle to any asset type.
365///
366/// `UntypedAssetHandle` allows storing handles to different asset types in the
367/// same collection, at the cost of runtime type checking instead of compile-time.
368///
369/// # Use Cases
370///
371/// - Asset registries that store mixed asset types
372/// - Dynamic asset loading where type is determined at runtime
373/// - FFI where type information must be preserved explicitly
374///
375/// # FFI Layout
376///
377/// ```text
378/// Offset 0:  index      (u32, 4 bytes)
379/// Offset 4:  generation (u32, 4 bytes)
380/// Offset 8:  asset_id   (AssetId, varies by platform)
381/// ```
382///
383/// For FFI, use `to_packed()` which provides a consistent representation.
384///
385/// # Example
386///
387/// ```
388/// use goud_engine::assets::{Asset, AssetHandle, AssetId, UntypedAssetHandle};
389///
390/// struct Texture;
391/// impl Asset for Texture {}
392///
393/// struct Audio;
394/// impl Asset for Audio {}
395///
396/// // Create typed handles
397/// let tex_handle: AssetHandle<Texture> = AssetHandle::new(1, 1);
398/// let audio_handle: AssetHandle<Audio> = AssetHandle::new(2, 1);
399///
400/// // Convert to untyped for storage in a single collection
401/// let handles: Vec<UntypedAssetHandle> = vec![
402///     tex_handle.untyped(),
403///     audio_handle.untyped(),
404/// ];
405///
406/// // Runtime type checking
407/// assert_eq!(handles[0].asset_id(), AssetId::of::<Texture>());
408/// assert_eq!(handles[1].asset_id(), AssetId::of::<Audio>());
409///
410/// // Convert back to typed (with type check)
411/// let recovered: Option<AssetHandle<Texture>> = handles[0].typed::<Texture>();
412/// assert!(recovered.is_some());
413///
414/// // Wrong type returns None
415/// let wrong: Option<AssetHandle<Audio>> = handles[0].typed::<Audio>();
416/// assert!(wrong.is_none());
417/// ```
418#[derive(Clone, Copy)]
419pub struct UntypedAssetHandle {
420    /// Slot index in the asset storage.
421    index: u32,
422
423    /// Generation counter for stale handle detection.
424    generation: u32,
425
426    /// The asset type this handle refers to.
427    asset_id: AssetId,
428}
429
430impl UntypedAssetHandle {
431    /// The invalid untyped handle constant.
432    ///
433    /// Uses `AssetId::of_raw::<()>()` as a placeholder type ID.
434    pub fn invalid() -> Self {
435        Self {
436            index: u32::MAX,
437            generation: 0,
438            asset_id: AssetId::of_raw::<()>(),
439        }
440    }
441
442    /// Creates a new untyped handle.
443    ///
444    /// # Arguments
445    ///
446    /// * `index` - Slot index in asset storage
447    /// * `generation` - Generation counter
448    /// * `asset_id` - The asset type identifier
449    #[inline]
450    pub const fn new(index: u32, generation: u32, asset_id: AssetId) -> Self {
451        Self {
452            index,
453            generation,
454            asset_id,
455        }
456    }
457
458    /// Creates an untyped handle from a typed handle.
459    ///
460    /// Equivalent to calling `typed_handle.untyped()`.
461    #[inline]
462    pub fn from_typed<A: Asset>(handle: AssetHandle<A>) -> Self {
463        handle.untyped()
464    }
465
466    /// Returns the index component.
467    #[inline]
468    pub const fn index(&self) -> u32 {
469        self.index
470    }
471
472    /// Returns the generation component.
473    #[inline]
474    pub const fn generation(&self) -> u32 {
475        self.generation
476    }
477
478    /// Returns the asset type identifier.
479    #[inline]
480    pub const fn asset_id(&self) -> AssetId {
481        self.asset_id
482    }
483
484    /// Returns `true` if this is not the invalid sentinel.
485    #[inline]
486    pub fn is_valid(&self) -> bool {
487        !(self.index == u32::MAX && self.generation == 0)
488    }
489
490    /// Attempts to convert this untyped handle to a typed handle.
491    ///
492    /// Returns `Some(handle)` if the asset type matches, `None` otherwise.
493    ///
494    /// # Type Safety
495    ///
496    /// This performs a runtime type check using `AssetId`. If you're certain
497    /// of the type, you can use `typed_unchecked()` instead.
498    ///
499    /// # Example
500    ///
501    /// ```
502    /// use goud_engine::assets::{Asset, AssetHandle, UntypedAssetHandle};
503    ///
504    /// struct Texture;
505    /// impl Asset for Texture {}
506    ///
507    /// struct Audio;
508    /// impl Asset for Audio {}
509    ///
510    /// let typed: AssetHandle<Texture> = AssetHandle::new(1, 1);
511    /// let untyped = typed.untyped();
512    ///
513    /// // Correct type succeeds
514    /// assert!(untyped.typed::<Texture>().is_some());
515    ///
516    /// // Wrong type fails
517    /// assert!(untyped.typed::<Audio>().is_none());
518    /// ```
519    #[inline]
520    pub fn typed<A: Asset>(&self) -> Option<AssetHandle<A>> {
521        if self.asset_id == AssetId::of::<A>() {
522            Some(AssetHandle::new(self.index, self.generation))
523        } else {
524            None
525        }
526    }
527
528    /// Converts this untyped handle to a typed handle without checking the type.
529    ///
530    /// # Safety
531    ///
532    /// The caller must ensure that this handle was created from an asset of type `A`.
533    /// Using a handle with the wrong type leads to undefined behavior when
534    /// accessing the asset data.
535    ///
536    /// # Example
537    ///
538    /// ```
539    /// use goud_engine::assets::{Asset, AssetHandle, UntypedAssetHandle};
540    ///
541    /// struct Texture;
542    /// impl Asset for Texture {}
543    ///
544    /// let typed: AssetHandle<Texture> = AssetHandle::new(1, 1);
545    /// let untyped = typed.untyped();
546    ///
547    /// // SAFETY: We know this was created from a Texture handle
548    /// let recovered: AssetHandle<Texture> = unsafe { untyped.typed_unchecked() };
549    /// assert_eq!(typed, recovered);
550    /// ```
551    #[inline]
552    pub unsafe fn typed_unchecked<A: Asset>(&self) -> AssetHandle<A> {
553        AssetHandle::new(self.index, self.generation)
554    }
555
556    /// Checks if this handle's type matches the given asset type.
557    ///
558    /// # Example
559    ///
560    /// ```
561    /// use goud_engine::assets::{Asset, AssetHandle, UntypedAssetHandle};
562    ///
563    /// struct Texture;
564    /// impl Asset for Texture {}
565    ///
566    /// struct Audio;
567    /// impl Asset for Audio {}
568    ///
569    /// let typed: AssetHandle<Texture> = AssetHandle::new(1, 1);
570    /// let untyped = typed.untyped();
571    ///
572    /// assert!(untyped.is_type::<Texture>());
573    /// assert!(!untyped.is_type::<Audio>());
574    /// ```
575    #[inline]
576    pub fn is_type<A: Asset>(&self) -> bool {
577        self.asset_id == AssetId::of::<A>()
578    }
579
580    /// Packs index and generation into a u64 (does not include type info).
581    #[inline]
582    pub const fn to_u64(&self) -> u64 {
583        ((self.generation as u64) << 32) | (self.index as u64)
584    }
585}
586
587impl Default for UntypedAssetHandle {
588    #[inline]
589    fn default() -> Self {
590        Self::invalid()
591    }
592}
593
594impl fmt::Debug for UntypedAssetHandle {
595    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596        if self.is_valid() {
597            write!(
598                f,
599                "UntypedAssetHandle({}:{}, {:?})",
600                self.index, self.generation, self.asset_id
601            )
602        } else {
603            write!(f, "UntypedAssetHandle(INVALID)")
604        }
605    }
606}
607
608impl fmt::Display for UntypedAssetHandle {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        if self.is_valid() {
611            write!(f, "{}:{}", self.index, self.generation)
612        } else {
613            write!(f, "INVALID")
614        }
615    }
616}
617
618impl PartialEq for UntypedAssetHandle {
619    #[inline]
620    fn eq(&self, other: &Self) -> bool {
621        self.index == other.index
622            && self.generation == other.generation
623            && self.asset_id == other.asset_id
624    }
625}
626
627impl Eq for UntypedAssetHandle {}
628
629impl Hash for UntypedAssetHandle {
630    #[inline]
631    fn hash<H: Hasher>(&self, state: &mut H) {
632        self.index.hash(state);
633        self.generation.hash(state);
634        self.asset_id.hash(state);
635    }
636}
637
638// =============================================================================
639// HandleLoadState
640// =============================================================================
641
642/// Combined handle and load state for convenient asset status checking.
643///
644/// This type wraps an asset handle together with its current loading state,
645/// allowing users to check both validity and load progress in one place.
646///
647/// # Example
648///
649/// ```
650/// use goud_engine::assets::{Asset, AssetHandle, AssetState, HandleLoadState};
651///
652/// struct Texture;
653/// impl Asset for Texture {}
654///
655/// // Simulate an asset that's loading
656/// let handle: AssetHandle<Texture> = AssetHandle::new(0, 1);
657/// let state = HandleLoadState::new(handle, AssetState::Loading { progress: 0.5 });
658///
659/// assert!(state.is_loading());
660/// assert_eq!(state.progress(), Some(0.5));
661///
662/// // When loaded
663/// let state = HandleLoadState::new(handle, AssetState::Loaded);
664/// assert!(state.is_ready());
665/// ```
666#[derive(Debug, Clone, PartialEq)]
667pub struct HandleLoadState<A: Asset> {
668    /// The asset handle.
669    handle: AssetHandle<A>,
670
671    /// Current loading state.
672    state: AssetState,
673}
674
675impl<A: Asset> HandleLoadState<A> {
676    /// Creates a new handle load state.
677    #[inline]
678    pub fn new(handle: AssetHandle<A>, state: AssetState) -> Self {
679        Self { handle, state }
680    }
681
682    /// Creates a handle load state for an invalid handle.
683    #[inline]
684    pub fn invalid() -> Self {
685        Self {
686            handle: AssetHandle::INVALID,
687            state: AssetState::NotLoaded,
688        }
689    }
690
691    /// Returns a reference to the handle.
692    #[inline]
693    pub fn handle(&self) -> &AssetHandle<A> {
694        &self.handle
695    }
696
697    /// Returns a reference to the asset state.
698    #[inline]
699    pub fn state(&self) -> &AssetState {
700        &self.state
701    }
702
703    /// Returns `true` if the handle is valid (not INVALID sentinel).
704    #[inline]
705    pub fn is_valid(&self) -> bool {
706        self.handle.is_valid()
707    }
708
709    /// Returns `true` if the asset is fully loaded and ready for use.
710    #[inline]
711    pub fn is_ready(&self) -> bool {
712        self.handle.is_valid() && self.state.is_ready()
713    }
714
715    /// Returns `true` if the asset is currently loading.
716    #[inline]
717    pub fn is_loading(&self) -> bool {
718        self.state.is_loading()
719    }
720
721    /// Returns `true` if the asset failed to load.
722    #[inline]
723    pub fn is_failed(&self) -> bool {
724        self.state.is_failed()
725    }
726
727    /// Returns the loading progress if currently loading.
728    #[inline]
729    pub fn progress(&self) -> Option<f32> {
730        self.state.progress()
731    }
732
733    /// Returns the error message if loading failed.
734    #[inline]
735    pub fn error(&self) -> Option<&str> {
736        self.state.error()
737    }
738
739    /// Consumes self and returns the inner handle.
740    #[inline]
741    pub fn into_handle(self) -> AssetHandle<A> {
742        self.handle
743    }
744
745    /// Updates the state.
746    #[inline]
747    pub fn set_state(&mut self, state: AssetState) {
748        self.state = state;
749    }
750}
751
752impl<A: Asset> Default for HandleLoadState<A> {
753    #[inline]
754    fn default() -> Self {
755        Self::invalid()
756    }
757}
758
759// =============================================================================
760// AssetPath
761// =============================================================================
762
763/// A path identifier for assets.
764///
765/// `AssetPath` represents the path used to load an asset, supporting both
766/// owned and borrowed string data. It provides utility methods for working
767/// with asset paths.
768///
769/// # Path Format
770///
771/// Asset paths use forward slashes as separators, regardless of platform:
772/// - `textures/player.png`
773/// - `audio/music/theme.ogg`
774/// - `shaders/basic.vert`
775///
776/// # FFI Considerations
777///
778/// For FFI, convert to a C string using `as_str()` and standard FFI string
779/// handling. The path does not include a null terminator by default.
780///
781/// # Example
782///
783/// ```
784/// use goud_engine::assets::AssetPath;
785///
786/// let path = AssetPath::new("textures/player.png");
787///
788/// assert_eq!(path.as_str(), "textures/player.png");
789/// assert_eq!(path.file_name(), Some("player.png"));
790/// assert_eq!(path.extension(), Some("png"));
791/// assert_eq!(path.directory(), Some("textures"));
792///
793/// // From owned string
794/// let owned = AssetPath::from_string("audio/sfx/jump.wav".to_string());
795/// assert_eq!(owned.extension(), Some("wav"));
796/// ```
797#[derive(Clone, PartialEq, Eq, Hash)]
798pub struct AssetPath<'a> {
799    /// The path string, either borrowed or owned.
800    path: Cow<'a, str>,
801}
802
803impl<'a> AssetPath<'a> {
804    /// Creates a new asset path from a string slice.
805    ///
806    /// # Example
807    ///
808    /// ```
809    /// use goud_engine::assets::AssetPath;
810    ///
811    /// let path = AssetPath::new("textures/player.png");
812    /// assert_eq!(path.as_str(), "textures/player.png");
813    /// ```
814    #[inline]
815    pub fn new(path: &'a str) -> Self {
816        Self {
817            path: Cow::Borrowed(path),
818        }
819    }
820
821    /// Creates a new asset path from an owned string.
822    ///
823    /// # Example
824    ///
825    /// ```
826    /// use goud_engine::assets::AssetPath;
827    ///
828    /// let path = AssetPath::from_string("textures/player.png".to_string());
829    /// assert_eq!(path.as_str(), "textures/player.png");
830    /// ```
831    #[inline]
832    pub fn from_string(path: String) -> AssetPath<'static> {
833        AssetPath {
834            path: Cow::Owned(path),
835        }
836    }
837
838    /// Returns the path as a string slice.
839    #[inline]
840    pub fn as_str(&self) -> &str {
841        &self.path
842    }
843
844    /// Returns `true` if the path is empty.
845    #[inline]
846    pub fn is_empty(&self) -> bool {
847        self.path.is_empty()
848    }
849
850    /// Returns the length of the path in bytes.
851    #[inline]
852    pub fn len(&self) -> usize {
853        self.path.len()
854    }
855
856    /// Returns the file name component of the path.
857    ///
858    /// # Example
859    ///
860    /// ```
861    /// use goud_engine::assets::AssetPath;
862    ///
863    /// assert_eq!(AssetPath::new("textures/player.png").file_name(), Some("player.png"));
864    /// assert_eq!(AssetPath::new("player.png").file_name(), Some("player.png"));
865    /// assert_eq!(AssetPath::new("textures/").file_name(), None);
866    /// ```
867    pub fn file_name(&self) -> Option<&str> {
868        let path = self.path.as_ref();
869        if path.ends_with('/') {
870            return None;
871        }
872        path.rsplit('/').next().filter(|s| !s.is_empty())
873    }
874
875    /// Returns the file extension, if any.
876    ///
877    /// # Example
878    ///
879    /// ```
880    /// use goud_engine::assets::AssetPath;
881    ///
882    /// assert_eq!(AssetPath::new("player.png").extension(), Some("png"));
883    /// assert_eq!(AssetPath::new("textures/player.png").extension(), Some("png"));
884    /// assert_eq!(AssetPath::new("Makefile").extension(), None);
885    /// assert_eq!(AssetPath::new(".gitignore").extension(), None);
886    /// ```
887    pub fn extension(&self) -> Option<&str> {
888        let file_name = self.file_name()?;
889        let dot_pos = file_name.rfind('.')?;
890
891        // Handle hidden files like ".gitignore" (no extension)
892        if dot_pos == 0 {
893            return None;
894        }
895
896        Some(&file_name[dot_pos + 1..])
897    }
898
899    /// Returns the directory component of the path.
900    ///
901    /// # Example
902    ///
903    /// ```
904    /// use goud_engine::assets::AssetPath;
905    ///
906    /// assert_eq!(AssetPath::new("textures/player.png").directory(), Some("textures"));
907    /// assert_eq!(AssetPath::new("a/b/c/file.txt").directory(), Some("a/b/c"));
908    /// assert_eq!(AssetPath::new("file.txt").directory(), None);
909    /// ```
910    pub fn directory(&self) -> Option<&str> {
911        let path = self.path.as_ref();
912        let pos = path.rfind('/')?;
913        if pos == 0 {
914            return None;
915        }
916        Some(&path[..pos])
917    }
918
919    /// Returns the file stem (file name without extension).
920    ///
921    /// # Example
922    ///
923    /// ```
924    /// use goud_engine::assets::AssetPath;
925    ///
926    /// assert_eq!(AssetPath::new("player.png").stem(), Some("player"));
927    /// assert_eq!(AssetPath::new("textures/player.png").stem(), Some("player"));
928    /// assert_eq!(AssetPath::new("archive.tar.gz").stem(), Some("archive.tar"));
929    /// assert_eq!(AssetPath::new(".gitignore").stem(), Some(".gitignore"));
930    /// ```
931    pub fn stem(&self) -> Option<&str> {
932        let file_name = self.file_name()?;
933        if let Some(dot_pos) = file_name.rfind('.') {
934            if dot_pos == 0 {
935                // Hidden file with no extension
936                Some(file_name)
937            } else {
938                Some(&file_name[..dot_pos])
939            }
940        } else {
941            // No extension
942            Some(file_name)
943        }
944    }
945
946    /// Converts this path to an owned `AssetPath<'static>`.
947    ///
948    /// If the path is already owned, this is a no-op. If borrowed,
949    /// the string is cloned.
950    pub fn into_owned(self) -> AssetPath<'static> {
951        AssetPath {
952            path: Cow::Owned(self.path.into_owned()),
953        }
954    }
955
956    /// Creates an `AssetPath` from a `std::path::Path`.
957    ///
958    /// Converts backslashes to forward slashes for platform consistency.
959    ///
960    /// # Example
961    ///
962    /// ```
963    /// use goud_engine::assets::AssetPath;
964    /// use std::path::Path;
965    ///
966    /// let path = AssetPath::from_path(Path::new("textures/player.png"));
967    /// assert_eq!(path.as_str(), "textures/player.png");
968    /// ```
969    pub fn from_path(path: &Path) -> AssetPath<'static> {
970        let path_str = path.to_string_lossy();
971        // Normalize to forward slashes
972        let normalized = path_str.replace('\\', "/");
973        AssetPath::from_string(normalized)
974    }
975
976    /// Joins this path with another path component.
977    ///
978    /// # Example
979    ///
980    /// ```
981    /// use goud_engine::assets::AssetPath;
982    ///
983    /// let base = AssetPath::new("textures");
984    /// let full = base.join("player.png");
985    /// assert_eq!(full.as_str(), "textures/player.png");
986    ///
987    /// // Handles trailing slashes
988    /// let base = AssetPath::new("textures/");
989    /// let full = base.join("player.png");
990    /// assert_eq!(full.as_str(), "textures/player.png");
991    /// ```
992    pub fn join(&self, other: &str) -> AssetPath<'static> {
993        let base = self.path.trim_end_matches('/');
994        let other = other.trim_start_matches('/');
995
996        if base.is_empty() {
997            AssetPath::from_string(other.to_string())
998        } else if other.is_empty() {
999            AssetPath::from_string(base.to_string())
1000        } else {
1001            AssetPath::from_string(format!("{}/{}", base, other))
1002        }
1003    }
1004
1005    /// Returns the path with a different extension.
1006    ///
1007    /// # Example
1008    ///
1009    /// ```
1010    /// use goud_engine::assets::AssetPath;
1011    ///
1012    /// let path = AssetPath::new("textures/player.png");
1013    /// let new_path = path.with_extension("jpg");
1014    /// assert_eq!(new_path.as_str(), "textures/player.jpg");
1015    ///
1016    /// // Add extension to file without one
1017    /// let path = AssetPath::new("Makefile");
1018    /// let new_path = path.with_extension("bak");
1019    /// assert_eq!(new_path.as_str(), "Makefile.bak");
1020    /// ```
1021    pub fn with_extension(&self, ext: &str) -> AssetPath<'static> {
1022        if let Some(stem) = self.stem() {
1023            if let Some(dir) = self.directory() {
1024                AssetPath::from_string(format!("{}/{}.{}", dir, stem, ext))
1025            } else {
1026                AssetPath::from_string(format!("{}.{}", stem, ext))
1027            }
1028        } else {
1029            // No file name, just append
1030            AssetPath::from_string(format!("{}.{}", self.path, ext))
1031        }
1032    }
1033}
1034
1035impl<'a> fmt::Debug for AssetPath<'a> {
1036    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1037        write!(f, "AssetPath({:?})", self.path)
1038    }
1039}
1040
1041impl<'a> fmt::Display for AssetPath<'a> {
1042    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1043        write!(f, "{}", self.path)
1044    }
1045}
1046
1047impl<'a> AsRef<str> for AssetPath<'a> {
1048    #[inline]
1049    fn as_ref(&self) -> &str {
1050        &self.path
1051    }
1052}
1053
1054impl<'a> From<&'a str> for AssetPath<'a> {
1055    #[inline]
1056    fn from(s: &'a str) -> Self {
1057        Self::new(s)
1058    }
1059}
1060
1061impl From<String> for AssetPath<'static> {
1062    #[inline]
1063    fn from(s: String) -> Self {
1064        Self::from_string(s)
1065    }
1066}
1067
1068impl<'a> PartialEq<str> for AssetPath<'a> {
1069    #[inline]
1070    fn eq(&self, other: &str) -> bool {
1071        self.path.as_ref() == other
1072    }
1073}
1074
1075impl<'a> PartialEq<&str> for AssetPath<'a> {
1076    #[inline]
1077    fn eq(&self, other: &&str) -> bool {
1078        self.path.as_ref() == *other
1079    }
1080}
1081
1082// =============================================================================
1083// WeakAssetHandle
1084// =============================================================================
1085
1086/// A weak reference to an asset that doesn't prevent unloading.
1087///
1088/// Unlike `AssetHandle`, a `WeakAssetHandle` does not contribute to the
1089/// reference count of an asset. This is useful for caches and lookup tables
1090/// where you want to reference assets without keeping them loaded.
1091///
1092/// # Example
1093///
1094/// ```
1095/// use goud_engine::assets::{Asset, AssetHandle, WeakAssetHandle};
1096///
1097/// struct Texture;
1098/// impl Asset for Texture {}
1099///
1100/// let strong: AssetHandle<Texture> = AssetHandle::new(1, 1);
1101/// let weak: WeakAssetHandle<Texture> = WeakAssetHandle::from_handle(&strong);
1102///
1103/// assert!(weak.is_valid());
1104/// assert_eq!(weak.index(), 1);
1105/// assert_eq!(weak.generation(), 1);
1106///
1107/// // Upgrade to strong handle for access (liveness must be checked separately)
1108/// let upgraded = weak.upgrade();
1109/// assert_eq!(upgraded.index(), 1);
1110/// ```
1111#[repr(C)]
1112pub struct WeakAssetHandle<A: Asset> {
1113    /// Slot index.
1114    index: u32,
1115
1116    /// Generation counter.
1117    generation: u32,
1118
1119    /// Zero-sized marker.
1120    _marker: PhantomData<A>,
1121}
1122
1123impl<A: Asset> WeakAssetHandle<A> {
1124    /// The invalid weak handle constant.
1125    pub const INVALID: Self = Self {
1126        index: u32::MAX,
1127        generation: 0,
1128        _marker: PhantomData,
1129    };
1130
1131    /// Creates a weak handle from a strong handle.
1132    #[inline]
1133    pub fn from_handle(handle: &AssetHandle<A>) -> Self {
1134        Self {
1135            index: handle.index,
1136            generation: handle.generation,
1137            _marker: PhantomData,
1138        }
1139    }
1140
1141    /// Creates a new weak handle with the given index and generation.
1142    #[inline]
1143    pub const fn new(index: u32, generation: u32) -> Self {
1144        Self {
1145            index,
1146            generation,
1147            _marker: PhantomData,
1148        }
1149    }
1150
1151    /// Returns the index component.
1152    #[inline]
1153    pub const fn index(&self) -> u32 {
1154        self.index
1155    }
1156
1157    /// Returns the generation component.
1158    #[inline]
1159    pub const fn generation(&self) -> u32 {
1160        self.generation
1161    }
1162
1163    /// Returns `true` if not the INVALID sentinel.
1164    #[inline]
1165    pub const fn is_valid(&self) -> bool {
1166        !(self.index == u32::MAX && self.generation == 0)
1167    }
1168
1169    /// Upgrades to a strong handle.
1170    ///
1171    /// Note: This does NOT check if the asset is still alive. Use
1172    /// `AssetServer::is_alive()` to check liveness before using the handle.
1173    #[inline]
1174    pub fn upgrade(&self) -> AssetHandle<A> {
1175        AssetHandle::new(self.index, self.generation)
1176    }
1177}
1178
1179impl<A: Asset> Clone for WeakAssetHandle<A> {
1180    #[inline]
1181    fn clone(&self) -> Self {
1182        *self
1183    }
1184}
1185
1186impl<A: Asset> Copy for WeakAssetHandle<A> {}
1187
1188impl<A: Asset> Default for WeakAssetHandle<A> {
1189    #[inline]
1190    fn default() -> Self {
1191        Self::INVALID
1192    }
1193}
1194
1195impl<A: Asset> fmt::Debug for WeakAssetHandle<A> {
1196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1197        let type_name = A::asset_type_name();
1198        if self.is_valid() {
1199            write!(
1200                f,
1201                "WeakAssetHandle<{}>({}:{})",
1202                type_name, self.index, self.generation
1203            )
1204        } else {
1205            write!(f, "WeakAssetHandle<{}>(INVALID)", type_name)
1206        }
1207    }
1208}
1209
1210impl<A: Asset> PartialEq for WeakAssetHandle<A> {
1211    #[inline]
1212    fn eq(&self, other: &Self) -> bool {
1213        self.index == other.index && self.generation == other.generation
1214    }
1215}
1216
1217impl<A: Asset> Eq for WeakAssetHandle<A> {}
1218
1219impl<A: Asset> Hash for WeakAssetHandle<A> {
1220    #[inline]
1221    fn hash<H: Hasher>(&self, state: &mut H) {
1222        self.index.hash(state);
1223        self.generation.hash(state);
1224    }
1225}
1226
1227impl<A: Asset> From<&AssetHandle<A>> for WeakAssetHandle<A> {
1228    #[inline]
1229    fn from(handle: &AssetHandle<A>) -> Self {
1230        Self::from_handle(handle)
1231    }
1232}
1233
1234// =============================================================================
1235// AssetHandleAllocator
1236// =============================================================================
1237
1238/// Allocator for asset handles with generation counting and slot reuse.
1239///
1240/// `AssetHandleAllocator` manages the allocation and deallocation of asset handles,
1241/// similar to [`HandleAllocator`](crate::core::handle::HandleAllocator) but specialized
1242/// for asset-specific use cases.
1243///
1244/// # Features
1245///
1246/// - **Generation Counting**: Prevents use-after-free by invalidating stale handles
1247/// - **Slot Reuse**: Deallocated slots are recycled via a free list
1248/// - **Type Safety**: Each allocator is generic over the asset type
1249///
1250/// # Example
1251///
1252/// ```
1253/// use goud_engine::assets::{Asset, AssetHandleAllocator};
1254///
1255/// struct Texture;
1256/// impl Asset for Texture {}
1257///
1258/// let mut allocator: AssetHandleAllocator<Texture> = AssetHandleAllocator::new();
1259///
1260/// // Allocate handles
1261/// let h1 = allocator.allocate();
1262/// let h2 = allocator.allocate();
1263///
1264/// assert!(allocator.is_alive(h1));
1265/// assert!(allocator.is_alive(h2));
1266///
1267/// // Deallocate
1268/// allocator.deallocate(h1);
1269/// assert!(!allocator.is_alive(h1));
1270///
1271/// // Slot is reused with new generation
1272/// let h3 = allocator.allocate();
1273/// assert_ne!(h1, h3); // Different generations
1274/// ```
1275pub struct AssetHandleAllocator<A: Asset> {
1276    /// Generation counter for each slot.
1277    /// Generation starts at 1 (0 reserved for INVALID).
1278    generations: Vec<u32>,
1279
1280    /// Free list of available slot indices.
1281    free_list: Vec<u32>,
1282
1283    /// Phantom marker for type parameter.
1284    _marker: PhantomData<A>,
1285}
1286
1287impl<A: Asset> AssetHandleAllocator<A> {
1288    /// Creates a new, empty allocator.
1289    ///
1290    /// # Example
1291    ///
1292    /// ```
1293    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1294    ///
1295    /// struct Audio;
1296    /// impl Asset for Audio {}
1297    ///
1298    /// let allocator: AssetHandleAllocator<Audio> = AssetHandleAllocator::new();
1299    /// assert!(allocator.is_empty());
1300    /// ```
1301    #[inline]
1302    pub fn new() -> Self {
1303        Self {
1304            generations: Vec::new(),
1305            free_list: Vec::new(),
1306            _marker: PhantomData,
1307        }
1308    }
1309
1310    /// Creates a new allocator with pre-allocated capacity.
1311    ///
1312    /// # Arguments
1313    ///
1314    /// * `capacity` - Number of slots to pre-allocate
1315    ///
1316    /// # Example
1317    ///
1318    /// ```
1319    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1320    ///
1321    /// struct Mesh;
1322    /// impl Asset for Mesh {}
1323    ///
1324    /// let allocator: AssetHandleAllocator<Mesh> = AssetHandleAllocator::with_capacity(1000);
1325    /// assert!(allocator.is_empty());
1326    /// ```
1327    #[inline]
1328    pub fn with_capacity(capacity: usize) -> Self {
1329        Self {
1330            generations: Vec::with_capacity(capacity),
1331            free_list: Vec::new(),
1332            _marker: PhantomData,
1333        }
1334    }
1335
1336    /// Allocates a new handle.
1337    ///
1338    /// Reuses slots from the free list when available, otherwise allocates new slots.
1339    ///
1340    /// # Returns
1341    ///
1342    /// A new, valid `AssetHandle<A>`.
1343    ///
1344    /// # Panics
1345    ///
1346    /// Panics if the number of slots exceeds `u32::MAX - 1`.
1347    ///
1348    /// # Example
1349    ///
1350    /// ```
1351    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1352    ///
1353    /// struct Shader;
1354    /// impl Asset for Shader {}
1355    ///
1356    /// let mut allocator: AssetHandleAllocator<Shader> = AssetHandleAllocator::new();
1357    /// let handle = allocator.allocate();
1358    ///
1359    /// assert!(handle.is_valid());
1360    /// assert!(allocator.is_alive(handle));
1361    /// ```
1362    pub fn allocate(&mut self) -> AssetHandle<A> {
1363        if let Some(index) = self.free_list.pop() {
1364            // Reuse slot
1365            let generation = self.generations[index as usize];
1366            AssetHandle::new(index, generation)
1367        } else {
1368            // Allocate new slot
1369            let index = self.generations.len();
1370            assert!(
1371                index < u32::MAX as usize,
1372                "AssetHandleAllocator exceeded maximum capacity"
1373            );
1374
1375            // New slots start at generation 1
1376            self.generations.push(1);
1377            AssetHandle::new(index as u32, 1)
1378        }
1379    }
1380
1381    /// Deallocates a handle, making it stale.
1382    ///
1383    /// The slot's generation is incremented, invalidating any handles that
1384    /// reference the old generation. The slot is added to the free list.
1385    ///
1386    /// # Arguments
1387    ///
1388    /// * `handle` - The handle to deallocate
1389    ///
1390    /// # Returns
1391    ///
1392    /// `true` if the handle was valid and successfully deallocated,
1393    /// `false` if the handle was invalid or stale.
1394    ///
1395    /// # Example
1396    ///
1397    /// ```
1398    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1399    ///
1400    /// struct Font;
1401    /// impl Asset for Font {}
1402    ///
1403    /// let mut allocator: AssetHandleAllocator<Font> = AssetHandleAllocator::new();
1404    /// let handle = allocator.allocate();
1405    ///
1406    /// assert!(allocator.deallocate(handle));
1407    /// assert!(!allocator.is_alive(handle));
1408    /// assert!(!allocator.deallocate(handle)); // Already deallocated
1409    /// ```
1410    pub fn deallocate(&mut self, handle: AssetHandle<A>) -> bool {
1411        if !handle.is_valid() {
1412            return false;
1413        }
1414
1415        let index = handle.index() as usize;
1416
1417        // Check bounds
1418        if index >= self.generations.len() {
1419            return false;
1420        }
1421
1422        // Check generation matches
1423        if self.generations[index] != handle.generation() {
1424            return false;
1425        }
1426
1427        // Increment generation (wrap to 1 if overflows to 0)
1428        let new_gen = self.generations[index].wrapping_add(1);
1429        self.generations[index] = if new_gen == 0 { 1 } else { new_gen };
1430
1431        // Add to free list
1432        self.free_list.push(handle.index());
1433
1434        true
1435    }
1436
1437    /// Checks if a handle is still alive (not deallocated).
1438    ///
1439    /// # Arguments
1440    ///
1441    /// * `handle` - The handle to check
1442    ///
1443    /// # Returns
1444    ///
1445    /// `true` if the handle is valid and its generation matches the current slot generation.
1446    ///
1447    /// # Example
1448    ///
1449    /// ```
1450    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1451    ///
1452    /// struct Material;
1453    /// impl Asset for Material {}
1454    ///
1455    /// let mut allocator: AssetHandleAllocator<Material> = AssetHandleAllocator::new();
1456    /// let handle = allocator.allocate();
1457    ///
1458    /// assert!(allocator.is_alive(handle));
1459    ///
1460    /// allocator.deallocate(handle);
1461    /// assert!(!allocator.is_alive(handle));
1462    /// ```
1463    #[inline]
1464    pub fn is_alive(&self, handle: AssetHandle<A>) -> bool {
1465        if !handle.is_valid() {
1466            return false;
1467        }
1468
1469        let index = handle.index() as usize;
1470        index < self.generations.len() && self.generations[index] == handle.generation()
1471    }
1472
1473    /// Returns the number of currently allocated (alive) handles.
1474    ///
1475    /// # Example
1476    ///
1477    /// ```
1478    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1479    ///
1480    /// struct Sprite;
1481    /// impl Asset for Sprite {}
1482    ///
1483    /// let mut allocator: AssetHandleAllocator<Sprite> = AssetHandleAllocator::new();
1484    /// assert_eq!(allocator.len(), 0);
1485    ///
1486    /// let h1 = allocator.allocate();
1487    /// let h2 = allocator.allocate();
1488    /// assert_eq!(allocator.len(), 2);
1489    ///
1490    /// allocator.deallocate(h1);
1491    /// assert_eq!(allocator.len(), 1);
1492    /// ```
1493    #[inline]
1494    pub fn len(&self) -> usize {
1495        self.generations.len() - self.free_list.len()
1496    }
1497
1498    /// Returns the total capacity (number of slots).
1499    ///
1500    /// This includes both active and free slots.
1501    #[inline]
1502    pub fn capacity(&self) -> usize {
1503        self.generations.len()
1504    }
1505
1506    /// Returns `true` if no handles are currently allocated.
1507    #[inline]
1508    pub fn is_empty(&self) -> bool {
1509        self.len() == 0
1510    }
1511
1512    /// Clears all allocations, invalidating all existing handles.
1513    ///
1514    /// This increments all generations and rebuilds the free list.
1515    ///
1516    /// # Example
1517    ///
1518    /// ```
1519    /// use goud_engine::assets::{Asset, AssetHandleAllocator};
1520    ///
1521    /// struct Animation;
1522    /// impl Asset for Animation {}
1523    ///
1524    /// let mut allocator: AssetHandleAllocator<Animation> = AssetHandleAllocator::new();
1525    ///
1526    /// let h1 = allocator.allocate();
1527    /// let h2 = allocator.allocate();
1528    /// assert_eq!(allocator.len(), 2);
1529    ///
1530    /// allocator.clear();
1531    /// assert_eq!(allocator.len(), 0);
1532    /// assert!(!allocator.is_alive(h1));
1533    /// assert!(!allocator.is_alive(h2));
1534    /// ```
1535    pub fn clear(&mut self) {
1536        // Increment all generations
1537        for gen in &mut self.generations {
1538            let new_gen = gen.wrapping_add(1);
1539            *gen = if new_gen == 0 { 1 } else { new_gen };
1540        }
1541
1542        // Rebuild free list
1543        self.free_list.clear();
1544        self.free_list.reserve(self.generations.len());
1545        for i in (0..self.generations.len()).rev() {
1546            self.free_list.push(i as u32);
1547        }
1548    }
1549
1550    /// Shrinks the free list to fit its contents.
1551    #[inline]
1552    pub fn shrink_to_fit(&mut self) {
1553        self.free_list.shrink_to_fit();
1554    }
1555
1556    /// Returns the current generation for a slot index.
1557    ///
1558    /// Returns `None` if the index is out of bounds.
1559    #[inline]
1560    pub fn generation_at(&self, index: u32) -> Option<u32> {
1561        self.generations.get(index as usize).copied()
1562    }
1563}
1564
1565impl<A: Asset> Default for AssetHandleAllocator<A> {
1566    #[inline]
1567    fn default() -> Self {
1568        Self::new()
1569    }
1570}
1571
1572impl<A: Asset> fmt::Debug for AssetHandleAllocator<A> {
1573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1574        let type_name = A::asset_type_name();
1575        f.debug_struct(&format!("AssetHandleAllocator<{}>", type_name))
1576            .field("len", &self.len())
1577            .field("capacity", &self.capacity())
1578            .field("free_slots", &self.free_list.len())
1579            .finish()
1580    }
1581}
1582
1583// =============================================================================
1584// Tests
1585// =============================================================================
1586
1587#[cfg(test)]
1588mod tests {
1589    use super::*;
1590
1591    // Test asset types
1592    #[derive(Clone, Debug, PartialEq)]
1593    struct TestTexture {
1594        #[allow(dead_code)]
1595        width: u32,
1596    }
1597
1598    impl Asset for TestTexture {
1599        fn asset_type_name() -> &'static str {
1600            "TestTexture"
1601        }
1602
1603        fn asset_type() -> AssetType {
1604            AssetType::Texture
1605        }
1606    }
1607
1608    #[derive(Clone, Debug, PartialEq)]
1609    struct TestAudio {
1610        #[allow(dead_code)]
1611        duration: f32,
1612    }
1613
1614    impl Asset for TestAudio {
1615        fn asset_type_name() -> &'static str {
1616            "TestAudio"
1617        }
1618
1619        fn asset_type() -> AssetType {
1620            AssetType::Audio
1621        }
1622    }
1623
1624    // Simple asset with defaults
1625    struct SimpleAsset;
1626    impl Asset for SimpleAsset {}
1627
1628    // =========================================================================
1629    // AssetHandle Tests
1630    // =========================================================================
1631
1632    mod asset_handle {
1633        use super::*;
1634
1635        #[test]
1636        fn test_new() {
1637            let handle: AssetHandle<TestTexture> = AssetHandle::new(42, 7);
1638            assert_eq!(handle.index(), 42);
1639            assert_eq!(handle.generation(), 7);
1640        }
1641
1642        #[test]
1643        fn test_invalid() {
1644            let handle: AssetHandle<TestTexture> = AssetHandle::INVALID;
1645            assert_eq!(handle.index(), u32::MAX);
1646            assert_eq!(handle.generation(), 0);
1647            assert!(!handle.is_valid());
1648        }
1649
1650        #[test]
1651        fn test_is_valid() {
1652            let valid: AssetHandle<TestTexture> = AssetHandle::new(0, 1);
1653            assert!(valid.is_valid());
1654
1655            let invalid: AssetHandle<TestTexture> = AssetHandle::INVALID;
1656            assert!(!invalid.is_valid());
1657
1658            // Edge case: index=MAX but gen!=0 is still valid
1659            let edge: AssetHandle<TestTexture> = AssetHandle::new(u32::MAX, 1);
1660            assert!(edge.is_valid());
1661        }
1662
1663        #[test]
1664        fn test_default() {
1665            let handle: AssetHandle<TestTexture> = Default::default();
1666            assert!(!handle.is_valid());
1667            assert_eq!(handle, AssetHandle::INVALID);
1668        }
1669
1670        #[test]
1671        fn test_clone_copy() {
1672            let h1: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
1673            let h2 = h1; // Copy
1674            let h3 = h1.clone();
1675
1676            assert_eq!(h1, h2);
1677            assert_eq!(h1, h3);
1678        }
1679
1680        #[test]
1681        fn test_equality() {
1682            let h1: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
1683            let h2: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
1684            let h3: AssetHandle<TestTexture> = AssetHandle::new(1, 2);
1685            let h4: AssetHandle<TestTexture> = AssetHandle::new(2, 1);
1686
1687            assert_eq!(h1, h2);
1688            assert_ne!(h1, h3); // Different generation
1689            assert_ne!(h1, h4); // Different index
1690        }
1691
1692        #[test]
1693        fn test_hash() {
1694            use std::collections::HashSet;
1695
1696            let mut set = HashSet::new();
1697            set.insert(AssetHandle::<TestTexture>::new(1, 1));
1698            set.insert(AssetHandle::<TestTexture>::new(2, 1));
1699
1700            assert_eq!(set.len(), 2);
1701
1702            // Same handle shouldn't add again
1703            set.insert(AssetHandle::<TestTexture>::new(1, 1));
1704            assert_eq!(set.len(), 2);
1705        }
1706
1707        #[test]
1708        fn test_ord() {
1709            use std::collections::BTreeSet;
1710
1711            let mut set = BTreeSet::new();
1712            set.insert(AssetHandle::<TestTexture>::new(3, 1));
1713            set.insert(AssetHandle::<TestTexture>::new(1, 1));
1714            set.insert(AssetHandle::<TestTexture>::new(2, 1));
1715
1716            let vec: Vec<_> = set.iter().collect();
1717            assert!(vec[0].index() < vec[1].index());
1718            assert!(vec[1].index() < vec[2].index());
1719        }
1720
1721        #[test]
1722        fn test_debug() {
1723            let handle: AssetHandle<TestTexture> = AssetHandle::new(42, 7);
1724            let debug_str = format!("{:?}", handle);
1725            assert!(debug_str.contains("AssetHandle"));
1726            assert!(debug_str.contains("TestTexture"));
1727            assert!(debug_str.contains("42"));
1728            assert!(debug_str.contains("7"));
1729
1730            let invalid: AssetHandle<TestTexture> = AssetHandle::INVALID;
1731            let debug_str = format!("{:?}", invalid);
1732            assert!(debug_str.contains("INVALID"));
1733        }
1734
1735        #[test]
1736        fn test_display() {
1737            let handle: AssetHandle<TestTexture> = AssetHandle::new(42, 7);
1738            assert_eq!(format!("{}", handle), "42:7");
1739
1740            let invalid: AssetHandle<TestTexture> = AssetHandle::INVALID;
1741            assert_eq!(format!("{}", invalid), "INVALID");
1742        }
1743
1744        #[test]
1745        fn test_to_u64() {
1746            let handle: AssetHandle<TestTexture> = AssetHandle::new(42, 7);
1747            let packed = handle.to_u64();
1748
1749            // Upper 32 = generation, lower 32 = index
1750            assert_eq!(packed & 0xFFFFFFFF, 42);
1751            assert_eq!(packed >> 32, 7);
1752        }
1753
1754        #[test]
1755        fn test_from_u64() {
1756            let packed: u64 = (7u64 << 32) | 42u64;
1757            let handle: AssetHandle<TestTexture> = AssetHandle::from_u64(packed);
1758
1759            assert_eq!(handle.index(), 42);
1760            assert_eq!(handle.generation(), 7);
1761        }
1762
1763        #[test]
1764        fn test_u64_roundtrip() {
1765            let original: AssetHandle<TestTexture> = AssetHandle::new(12345, 99);
1766            let packed = original.to_u64();
1767            let recovered: AssetHandle<TestTexture> = AssetHandle::from_u64(packed);
1768
1769            assert_eq!(original, recovered);
1770        }
1771
1772        #[test]
1773        fn test_from_into_u64() {
1774            let handle: AssetHandle<TestTexture> = AssetHandle::new(10, 20);
1775            let packed: u64 = handle.into();
1776            let recovered: AssetHandle<TestTexture> = packed.into();
1777
1778            assert_eq!(handle, recovered);
1779        }
1780
1781        #[test]
1782        fn test_untyped() {
1783            let typed: AssetHandle<TestTexture> = AssetHandle::new(5, 3);
1784            let untyped = typed.untyped();
1785
1786            assert_eq!(untyped.index(), 5);
1787            assert_eq!(untyped.generation(), 3);
1788            assert_eq!(untyped.asset_id(), AssetId::of::<TestTexture>());
1789        }
1790
1791        #[test]
1792        fn test_asset_id() {
1793            assert_eq!(
1794                AssetHandle::<TestTexture>::asset_id(),
1795                AssetId::of::<TestTexture>()
1796            );
1797            assert_ne!(
1798                AssetHandle::<TestTexture>::asset_id(),
1799                AssetHandle::<TestAudio>::asset_id()
1800            );
1801        }
1802
1803        #[test]
1804        fn test_asset_type() {
1805            assert_eq!(AssetHandle::<TestTexture>::asset_type(), AssetType::Texture);
1806            assert_eq!(AssetHandle::<TestAudio>::asset_type(), AssetType::Audio);
1807        }
1808
1809        #[test]
1810        fn test_size_and_align() {
1811            // Should be 8 bytes (2 x u32), PhantomData is zero-sized
1812            assert_eq!(std::mem::size_of::<AssetHandle<TestTexture>>(), 8);
1813            assert_eq!(std::mem::align_of::<AssetHandle<TestTexture>>(), 4);
1814        }
1815
1816        #[test]
1817        fn test_is_send() {
1818            fn requires_send<T: Send>() {}
1819            requires_send::<AssetHandle<TestTexture>>();
1820        }
1821
1822        #[test]
1823        fn test_is_sync() {
1824            fn requires_sync<T: Sync>() {}
1825            requires_sync::<AssetHandle<TestTexture>>();
1826        }
1827    }
1828
1829    // =========================================================================
1830    // UntypedAssetHandle Tests
1831    // =========================================================================
1832
1833    mod untyped_asset_handle {
1834        use super::*;
1835
1836        #[test]
1837        fn test_new() {
1838            let handle = UntypedAssetHandle::new(42, 7, AssetId::of::<TestTexture>());
1839            assert_eq!(handle.index(), 42);
1840            assert_eq!(handle.generation(), 7);
1841            assert_eq!(handle.asset_id(), AssetId::of::<TestTexture>());
1842        }
1843
1844        #[test]
1845        fn test_invalid() {
1846            let handle = UntypedAssetHandle::invalid();
1847            assert!(!handle.is_valid());
1848        }
1849
1850        #[test]
1851        fn test_from_typed() {
1852            let typed: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
1853            let untyped = UntypedAssetHandle::from_typed(typed);
1854
1855            assert_eq!(untyped.index(), 10);
1856            assert_eq!(untyped.generation(), 5);
1857            assert_eq!(untyped.asset_id(), AssetId::of::<TestTexture>());
1858        }
1859
1860        #[test]
1861        fn test_typed() {
1862            let typed: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
1863            let untyped = typed.untyped();
1864
1865            // Correct type succeeds
1866            let recovered: Option<AssetHandle<TestTexture>> = untyped.typed();
1867            assert!(recovered.is_some());
1868            assert_eq!(recovered.unwrap(), typed);
1869
1870            // Wrong type fails
1871            let wrong: Option<AssetHandle<TestAudio>> = untyped.typed();
1872            assert!(wrong.is_none());
1873        }
1874
1875        #[test]
1876        fn test_typed_unchecked() {
1877            let typed: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
1878            let untyped = typed.untyped();
1879
1880            // SAFETY: We know this was created from TestTexture
1881            let recovered: AssetHandle<TestTexture> = unsafe { untyped.typed_unchecked() };
1882            assert_eq!(typed, recovered);
1883        }
1884
1885        #[test]
1886        fn test_is_type() {
1887            let typed: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
1888            let untyped = typed.untyped();
1889
1890            assert!(untyped.is_type::<TestTexture>());
1891            assert!(!untyped.is_type::<TestAudio>());
1892        }
1893
1894        #[test]
1895        fn test_equality() {
1896            let h1 = UntypedAssetHandle::new(1, 1, AssetId::of::<TestTexture>());
1897            let h2 = UntypedAssetHandle::new(1, 1, AssetId::of::<TestTexture>());
1898            let h3 = UntypedAssetHandle::new(1, 1, AssetId::of::<TestAudio>()); // Different type
1899            let h4 = UntypedAssetHandle::new(1, 2, AssetId::of::<TestTexture>()); // Different gen
1900
1901            assert_eq!(h1, h2);
1902            assert_ne!(h1, h3); // Different asset type
1903            assert_ne!(h1, h4); // Different generation
1904        }
1905
1906        #[test]
1907        fn test_hash() {
1908            use std::collections::HashSet;
1909
1910            let mut set = HashSet::new();
1911            set.insert(UntypedAssetHandle::new(1, 1, AssetId::of::<TestTexture>()));
1912            set.insert(UntypedAssetHandle::new(1, 1, AssetId::of::<TestAudio>()));
1913
1914            // Different types are different entries
1915            assert_eq!(set.len(), 2);
1916        }
1917
1918        #[test]
1919        fn test_debug() {
1920            let handle = UntypedAssetHandle::new(42, 7, AssetId::of::<TestTexture>());
1921            let debug_str = format!("{:?}", handle);
1922            assert!(debug_str.contains("UntypedAssetHandle"));
1923            assert!(debug_str.contains("42"));
1924            assert!(debug_str.contains("7"));
1925
1926            let invalid = UntypedAssetHandle::invalid();
1927            let debug_str = format!("{:?}", invalid);
1928            assert!(debug_str.contains("INVALID"));
1929        }
1930
1931        #[test]
1932        fn test_display() {
1933            let handle = UntypedAssetHandle::new(42, 7, AssetId::of::<TestTexture>());
1934            assert_eq!(format!("{}", handle), "42:7");
1935
1936            let invalid = UntypedAssetHandle::invalid();
1937            assert_eq!(format!("{}", invalid), "INVALID");
1938        }
1939
1940        #[test]
1941        fn test_clone_copy() {
1942            let h1 = UntypedAssetHandle::new(1, 1, AssetId::of::<TestTexture>());
1943            let h2 = h1; // Copy
1944            let h3 = h1.clone();
1945
1946            assert_eq!(h1, h2);
1947            assert_eq!(h1, h3);
1948        }
1949
1950        #[test]
1951        fn test_default() {
1952            let handle: UntypedAssetHandle = Default::default();
1953            assert!(!handle.is_valid());
1954        }
1955
1956        #[test]
1957        fn test_is_send() {
1958            fn requires_send<T: Send>() {}
1959            requires_send::<UntypedAssetHandle>();
1960        }
1961
1962        #[test]
1963        fn test_is_sync() {
1964            fn requires_sync<T: Sync>() {}
1965            requires_sync::<UntypedAssetHandle>();
1966        }
1967    }
1968
1969    // =========================================================================
1970    // HandleLoadState Tests
1971    // =========================================================================
1972
1973    mod handle_load_state {
1974        use super::*;
1975
1976        #[test]
1977        fn test_new() {
1978            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
1979            let state = HandleLoadState::new(handle, AssetState::Loaded);
1980
1981            assert_eq!(*state.handle(), handle);
1982            assert!(state.is_ready());
1983        }
1984
1985        #[test]
1986        fn test_invalid() {
1987            let state: HandleLoadState<TestTexture> = HandleLoadState::invalid();
1988            assert!(!state.is_valid());
1989            assert!(!state.is_ready());
1990            assert_eq!(*state.state(), AssetState::NotLoaded);
1991        }
1992
1993        #[test]
1994        fn test_is_ready() {
1995            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
1996
1997            let loaded = HandleLoadState::new(handle, AssetState::Loaded);
1998            assert!(loaded.is_ready());
1999
2000            let loading = HandleLoadState::new(handle, AssetState::Loading { progress: 0.5 });
2001            assert!(!loading.is_ready());
2002
2003            let failed = HandleLoadState::new(
2004                handle,
2005                AssetState::Failed {
2006                    error: "test".to_string(),
2007                },
2008            );
2009            assert!(!failed.is_ready());
2010        }
2011
2012        #[test]
2013        fn test_is_loading() {
2014            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2015            let state = HandleLoadState::new(handle, AssetState::Loading { progress: 0.5 });
2016
2017            assert!(state.is_loading());
2018            assert_eq!(state.progress(), Some(0.5));
2019        }
2020
2021        #[test]
2022        fn test_is_failed() {
2023            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2024            let state = HandleLoadState::new(
2025                handle,
2026                AssetState::Failed {
2027                    error: "File not found".to_string(),
2028                },
2029            );
2030
2031            assert!(state.is_failed());
2032            assert_eq!(state.error(), Some("File not found"));
2033        }
2034
2035        #[test]
2036        fn test_into_handle() {
2037            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2038            let state = HandleLoadState::new(handle, AssetState::Loaded);
2039
2040            let recovered = state.into_handle();
2041            assert_eq!(recovered, handle);
2042        }
2043
2044        #[test]
2045        fn test_set_state() {
2046            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2047            let mut state = HandleLoadState::new(handle, AssetState::Loading { progress: 0.0 });
2048
2049            assert!(state.is_loading());
2050
2051            state.set_state(AssetState::Loaded);
2052            assert!(state.is_ready());
2053        }
2054
2055        #[test]
2056        fn test_default() {
2057            let state: HandleLoadState<TestTexture> = Default::default();
2058            assert!(!state.is_valid());
2059        }
2060
2061        #[test]
2062        fn test_clone() {
2063            let handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2064            let state1 = HandleLoadState::new(handle, AssetState::Loaded);
2065            let state2 = state1.clone();
2066
2067            assert_eq!(state1, state2);
2068        }
2069    }
2070
2071    // =========================================================================
2072    // AssetPath Tests
2073    // =========================================================================
2074
2075    mod asset_path {
2076        use super::*;
2077
2078        #[test]
2079        fn test_new() {
2080            let path = AssetPath::new("textures/player.png");
2081            assert_eq!(path.as_str(), "textures/player.png");
2082        }
2083
2084        #[test]
2085        fn test_from_string() {
2086            let path = AssetPath::from_string("textures/player.png".to_string());
2087            assert_eq!(path.as_str(), "textures/player.png");
2088        }
2089
2090        #[test]
2091        fn test_is_empty() {
2092            let empty = AssetPath::new("");
2093            assert!(empty.is_empty());
2094            assert_eq!(empty.len(), 0);
2095
2096            let non_empty = AssetPath::new("file.txt");
2097            assert!(!non_empty.is_empty());
2098        }
2099
2100        #[test]
2101        fn test_file_name() {
2102            assert_eq!(
2103                AssetPath::new("textures/player.png").file_name(),
2104                Some("player.png")
2105            );
2106            assert_eq!(AssetPath::new("player.png").file_name(), Some("player.png"));
2107            assert_eq!(
2108                AssetPath::new("a/b/c/file.txt").file_name(),
2109                Some("file.txt")
2110            );
2111            assert_eq!(AssetPath::new("textures/").file_name(), None);
2112            assert_eq!(AssetPath::new("").file_name(), None);
2113        }
2114
2115        #[test]
2116        fn test_extension() {
2117            assert_eq!(AssetPath::new("player.png").extension(), Some("png"));
2118            assert_eq!(
2119                AssetPath::new("textures/player.png").extension(),
2120                Some("png")
2121            );
2122            assert_eq!(AssetPath::new("archive.tar.gz").extension(), Some("gz"));
2123            assert_eq!(AssetPath::new("Makefile").extension(), None);
2124            assert_eq!(AssetPath::new(".gitignore").extension(), None);
2125            assert_eq!(AssetPath::new("").extension(), None);
2126        }
2127
2128        #[test]
2129        fn test_directory() {
2130            assert_eq!(
2131                AssetPath::new("textures/player.png").directory(),
2132                Some("textures")
2133            );
2134            assert_eq!(AssetPath::new("a/b/c/file.txt").directory(), Some("a/b/c"));
2135            assert_eq!(AssetPath::new("file.txt").directory(), None);
2136            assert_eq!(AssetPath::new("").directory(), None);
2137        }
2138
2139        #[test]
2140        fn test_stem() {
2141            assert_eq!(AssetPath::new("player.png").stem(), Some("player"));
2142            assert_eq!(AssetPath::new("textures/player.png").stem(), Some("player"));
2143            assert_eq!(AssetPath::new("archive.tar.gz").stem(), Some("archive.tar"));
2144            assert_eq!(AssetPath::new(".gitignore").stem(), Some(".gitignore"));
2145            assert_eq!(AssetPath::new("Makefile").stem(), Some("Makefile"));
2146        }
2147
2148        #[test]
2149        fn test_into_owned() {
2150            let borrowed = AssetPath::new("textures/player.png");
2151            let owned = borrowed.into_owned();
2152            assert_eq!(owned.as_str(), "textures/player.png");
2153        }
2154
2155        #[test]
2156        fn test_from_path() {
2157            let path = AssetPath::from_path(Path::new("textures/player.png"));
2158            assert_eq!(path.as_str(), "textures/player.png");
2159        }
2160
2161        #[test]
2162        fn test_join() {
2163            let base = AssetPath::new("textures");
2164            let full = base.join("player.png");
2165            assert_eq!(full.as_str(), "textures/player.png");
2166
2167            // With trailing slash
2168            let base = AssetPath::new("textures/");
2169            let full = base.join("player.png");
2170            assert_eq!(full.as_str(), "textures/player.png");
2171
2172            // With leading slash in other
2173            let base = AssetPath::new("textures");
2174            let full = base.join("/player.png");
2175            assert_eq!(full.as_str(), "textures/player.png");
2176
2177            // Empty base
2178            let base = AssetPath::new("");
2179            let full = base.join("player.png");
2180            assert_eq!(full.as_str(), "player.png");
2181
2182            // Empty other
2183            let base = AssetPath::new("textures");
2184            let full = base.join("");
2185            assert_eq!(full.as_str(), "textures");
2186        }
2187
2188        #[test]
2189        fn test_with_extension() {
2190            let path = AssetPath::new("textures/player.png");
2191            let new_path = path.with_extension("jpg");
2192            assert_eq!(new_path.as_str(), "textures/player.jpg");
2193
2194            // No extension originally
2195            let path = AssetPath::new("Makefile");
2196            let new_path = path.with_extension("bak");
2197            assert_eq!(new_path.as_str(), "Makefile.bak");
2198
2199            // No directory
2200            let path = AssetPath::new("player.png");
2201            let new_path = path.with_extension("jpg");
2202            assert_eq!(new_path.as_str(), "player.jpg");
2203        }
2204
2205        #[test]
2206        fn test_equality() {
2207            let p1 = AssetPath::new("textures/player.png");
2208            let p2 = AssetPath::new("textures/player.png");
2209            let p3 = AssetPath::new("textures/enemy.png");
2210
2211            assert_eq!(p1, p2);
2212            assert_ne!(p1, p3);
2213        }
2214
2215        #[test]
2216        fn test_equality_with_str() {
2217            let path = AssetPath::new("textures/player.png");
2218            assert!(path == "textures/player.png");
2219            // Note: Can't compare with &&str, only &str, which is fine
2220            let str_ref: &str = "textures/player.png";
2221            assert!(path == str_ref);
2222        }
2223
2224        #[test]
2225        fn test_hash() {
2226            use std::collections::HashSet;
2227
2228            let mut set = HashSet::new();
2229            set.insert(AssetPath::new("a.txt").into_owned());
2230            set.insert(AssetPath::new("b.txt").into_owned());
2231
2232            assert_eq!(set.len(), 2);
2233
2234            set.insert(AssetPath::new("a.txt").into_owned());
2235            assert_eq!(set.len(), 2);
2236        }
2237
2238        #[test]
2239        fn test_debug() {
2240            let path = AssetPath::new("textures/player.png");
2241            let debug_str = format!("{:?}", path);
2242            assert!(debug_str.contains("AssetPath"));
2243            assert!(debug_str.contains("textures/player.png"));
2244        }
2245
2246        #[test]
2247        fn test_display() {
2248            let path = AssetPath::new("textures/player.png");
2249            assert_eq!(format!("{}", path), "textures/player.png");
2250        }
2251
2252        #[test]
2253        fn test_as_ref() {
2254            let path = AssetPath::new("textures/player.png");
2255            let s: &str = path.as_ref();
2256            assert_eq!(s, "textures/player.png");
2257        }
2258
2259        #[test]
2260        fn test_from_str() {
2261            let path: AssetPath = "textures/player.png".into();
2262            assert_eq!(path.as_str(), "textures/player.png");
2263        }
2264
2265        #[test]
2266        fn test_from_string_into() {
2267            let path: AssetPath<'static> = "textures/player.png".to_string().into();
2268            assert_eq!(path.as_str(), "textures/player.png");
2269        }
2270    }
2271
2272    // =========================================================================
2273    // WeakAssetHandle Tests
2274    // =========================================================================
2275
2276    mod weak_asset_handle {
2277        use super::*;
2278
2279        #[test]
2280        fn test_from_handle() {
2281            let strong: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
2282            let weak = WeakAssetHandle::from_handle(&strong);
2283
2284            assert_eq!(weak.index(), 10);
2285            assert_eq!(weak.generation(), 5);
2286        }
2287
2288        #[test]
2289        fn test_new() {
2290            let weak: WeakAssetHandle<TestTexture> = WeakAssetHandle::new(42, 7);
2291            assert_eq!(weak.index(), 42);
2292            assert_eq!(weak.generation(), 7);
2293        }
2294
2295        #[test]
2296        fn test_invalid() {
2297            let weak: WeakAssetHandle<TestTexture> = WeakAssetHandle::INVALID;
2298            assert!(!weak.is_valid());
2299        }
2300
2301        #[test]
2302        fn test_upgrade() {
2303            let strong: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
2304            let weak = WeakAssetHandle::from_handle(&strong);
2305            let upgraded = weak.upgrade();
2306
2307            assert_eq!(upgraded, strong);
2308        }
2309
2310        #[test]
2311        fn test_clone_copy() {
2312            let w1: WeakAssetHandle<TestTexture> = WeakAssetHandle::new(1, 1);
2313            let w2 = w1; // Copy
2314            let w3 = w1.clone();
2315
2316            assert_eq!(w1, w2);
2317            assert_eq!(w1, w3);
2318        }
2319
2320        #[test]
2321        fn test_default() {
2322            let weak: WeakAssetHandle<TestTexture> = Default::default();
2323            assert!(!weak.is_valid());
2324        }
2325
2326        #[test]
2327        fn test_equality() {
2328            let w1: WeakAssetHandle<TestTexture> = WeakAssetHandle::new(1, 1);
2329            let w2: WeakAssetHandle<TestTexture> = WeakAssetHandle::new(1, 1);
2330            let w3: WeakAssetHandle<TestTexture> = WeakAssetHandle::new(1, 2);
2331
2332            assert_eq!(w1, w2);
2333            assert_ne!(w1, w3);
2334        }
2335
2336        #[test]
2337        fn test_hash() {
2338            use std::collections::HashSet;
2339
2340            let mut set = HashSet::new();
2341            set.insert(WeakAssetHandle::<TestTexture>::new(1, 1));
2342            set.insert(WeakAssetHandle::<TestTexture>::new(2, 1));
2343
2344            assert_eq!(set.len(), 2);
2345        }
2346
2347        #[test]
2348        fn test_debug() {
2349            let weak: WeakAssetHandle<TestTexture> = WeakAssetHandle::new(42, 7);
2350            let debug_str = format!("{:?}", weak);
2351            assert!(debug_str.contains("WeakAssetHandle"));
2352            assert!(debug_str.contains("TestTexture"));
2353            assert!(debug_str.contains("42"));
2354        }
2355
2356        #[test]
2357        fn test_from_ref() {
2358            let strong: AssetHandle<TestTexture> = AssetHandle::new(10, 5);
2359            let weak: WeakAssetHandle<TestTexture> = (&strong).into();
2360
2361            assert_eq!(weak.index(), 10);
2362            assert_eq!(weak.generation(), 5);
2363        }
2364
2365        #[test]
2366        fn test_size_and_align() {
2367            // Should be 8 bytes (2 x u32), PhantomData is zero-sized
2368            assert_eq!(std::mem::size_of::<WeakAssetHandle<TestTexture>>(), 8);
2369            assert_eq!(std::mem::align_of::<WeakAssetHandle<TestTexture>>(), 4);
2370        }
2371
2372        #[test]
2373        fn test_is_send() {
2374            fn requires_send<T: Send>() {}
2375            requires_send::<WeakAssetHandle<TestTexture>>();
2376        }
2377
2378        #[test]
2379        fn test_is_sync() {
2380            fn requires_sync<T: Sync>() {}
2381            requires_sync::<WeakAssetHandle<TestTexture>>();
2382        }
2383    }
2384
2385    // =========================================================================
2386    // Integration Tests
2387    // =========================================================================
2388
2389    mod integration {
2390        use super::*;
2391
2392        #[test]
2393        fn test_handle_lifecycle() {
2394            // Simulate asset lifecycle with handles
2395            let handle: AssetHandle<TestTexture> = AssetHandle::new(0, 1);
2396
2397            // Initially loading
2398            let mut state = HandleLoadState::new(handle, AssetState::Loading { progress: 0.0 });
2399            assert!(state.is_loading());
2400
2401            // Progress update
2402            state.set_state(AssetState::Loading { progress: 0.5 });
2403            assert_eq!(state.progress(), Some(0.5));
2404
2405            // Loaded
2406            state.set_state(AssetState::Loaded);
2407            assert!(state.is_ready());
2408        }
2409
2410        #[test]
2411        fn test_mixed_handle_collection() {
2412            // Store different asset types in single collection
2413            let tex_handle: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2414            let audio_handle: AssetHandle<TestAudio> = AssetHandle::new(2, 1);
2415
2416            let handles: Vec<UntypedAssetHandle> =
2417                vec![tex_handle.untyped(), audio_handle.untyped()];
2418
2419            // Can filter by type
2420            let textures: Vec<_> = handles
2421                .iter()
2422                .filter_map(|h| h.typed::<TestTexture>())
2423                .collect();
2424            assert_eq!(textures.len(), 1);
2425            assert_eq!(textures[0], tex_handle);
2426        }
2427
2428        #[test]
2429        fn test_path_to_handle_workflow() {
2430            // Simulate path -> handle workflow
2431            let path = AssetPath::new("textures/player.png");
2432            assert_eq!(path.extension(), Some("png"));
2433
2434            // Asset system would create handle
2435            let handle: AssetHandle<TestTexture> = AssetHandle::new(0, 1);
2436
2437            // Check type matches extension
2438            assert_eq!(AssetHandle::<TestTexture>::asset_type(), AssetType::Texture);
2439        }
2440
2441        #[test]
2442        fn test_weak_handle_usage() {
2443            // Strong handle
2444            let strong: AssetHandle<TestTexture> = AssetHandle::new(1, 1);
2445
2446            // Create weak reference (for cache)
2447            let weak = WeakAssetHandle::from_handle(&strong);
2448
2449            // Upgrade when needed
2450            let upgraded = weak.upgrade();
2451            assert_eq!(upgraded, strong);
2452        }
2453    }
2454
2455    // =========================================================================
2456    // AssetHandleAllocator Tests
2457    // =========================================================================
2458
2459    mod asset_handle_allocator {
2460        use super::*;
2461
2462        #[test]
2463        fn test_new() {
2464            let allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2465            assert_eq!(allocator.len(), 0);
2466            assert!(allocator.is_empty());
2467            assert_eq!(allocator.capacity(), 0);
2468        }
2469
2470        #[test]
2471        fn test_with_capacity() {
2472            let allocator: AssetHandleAllocator<TestTexture> =
2473                AssetHandleAllocator::with_capacity(100);
2474            assert!(allocator.is_empty());
2475        }
2476
2477        #[test]
2478        fn test_allocate() {
2479            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2480
2481            let h1 = allocator.allocate();
2482            assert!(h1.is_valid());
2483            assert_eq!(h1.index(), 0);
2484            assert_eq!(h1.generation(), 1);
2485
2486            let h2 = allocator.allocate();
2487            assert!(h2.is_valid());
2488            assert_eq!(h2.index(), 1);
2489            assert_eq!(h2.generation(), 1);
2490
2491            assert_eq!(allocator.len(), 2);
2492        }
2493
2494        #[test]
2495        fn test_allocate_unique() {
2496            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2497
2498            let handles: Vec<_> = (0..100).map(|_| allocator.allocate()).collect();
2499
2500            // All handles should be unique
2501            for i in 0..handles.len() {
2502                for j in (i + 1)..handles.len() {
2503                    assert_ne!(handles[i], handles[j]);
2504                }
2505            }
2506        }
2507
2508        #[test]
2509        fn test_deallocate() {
2510            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2511
2512            let handle = allocator.allocate();
2513            assert!(allocator.is_alive(handle));
2514            assert_eq!(allocator.len(), 1);
2515
2516            assert!(allocator.deallocate(handle));
2517            assert!(!allocator.is_alive(handle));
2518            assert_eq!(allocator.len(), 0);
2519        }
2520
2521        #[test]
2522        fn test_deallocate_invalid() {
2523            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2524
2525            // Invalid handle
2526            assert!(!allocator.deallocate(AssetHandle::INVALID));
2527
2528            // Handle that was never allocated
2529            let fake_handle = AssetHandle::new(100, 1);
2530            assert!(!allocator.deallocate(fake_handle));
2531        }
2532
2533        #[test]
2534        fn test_deallocate_twice() {
2535            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2536
2537            let handle = allocator.allocate();
2538            assert!(allocator.deallocate(handle));
2539            assert!(!allocator.deallocate(handle)); // Already deallocated
2540        }
2541
2542        #[test]
2543        fn test_is_alive() {
2544            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2545
2546            let handle = allocator.allocate();
2547            assert!(allocator.is_alive(handle));
2548
2549            allocator.deallocate(handle);
2550            assert!(!allocator.is_alive(handle));
2551
2552            // INVALID is never alive
2553            assert!(!allocator.is_alive(AssetHandle::INVALID));
2554        }
2555
2556        #[test]
2557        fn test_slot_reuse() {
2558            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2559
2560            let h1 = allocator.allocate();
2561            let original_index = h1.index();
2562            let original_gen = h1.generation();
2563
2564            allocator.deallocate(h1);
2565
2566            let h2 = allocator.allocate();
2567
2568            // Same index, different generation
2569            assert_eq!(h2.index(), original_index);
2570            assert_eq!(h2.generation(), original_gen + 1);
2571            assert_ne!(h1, h2);
2572        }
2573
2574        #[test]
2575        fn test_slot_reuse_multiple() {
2576            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2577
2578            // Allocate 10 handles
2579            let handles: Vec<_> = (0..10).map(|_| allocator.allocate()).collect();
2580
2581            // Deallocate first 5
2582            for h in &handles[..5] {
2583                allocator.deallocate(*h);
2584            }
2585
2586            assert_eq!(allocator.len(), 5);
2587            assert_eq!(allocator.capacity(), 10);
2588
2589            // Allocate 5 more - should reuse slots
2590            for _ in 0..5 {
2591                let h = allocator.allocate();
2592                assert!(h.index() < 5); // Reused slot
2593                assert_eq!(h.generation(), 2); // Second generation
2594            }
2595
2596            assert_eq!(allocator.len(), 10);
2597            assert_eq!(allocator.capacity(), 10); // No new slots needed
2598        }
2599
2600        #[test]
2601        fn test_len_and_capacity() {
2602            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2603
2604            assert_eq!(allocator.len(), 0);
2605            assert_eq!(allocator.capacity(), 0);
2606
2607            let h1 = allocator.allocate();
2608            let h2 = allocator.allocate();
2609            let h3 = allocator.allocate();
2610
2611            assert_eq!(allocator.len(), 3);
2612            assert_eq!(allocator.capacity(), 3);
2613
2614            allocator.deallocate(h2);
2615
2616            assert_eq!(allocator.len(), 2);
2617            assert_eq!(allocator.capacity(), 3); // Capacity unchanged
2618        }
2619
2620        #[test]
2621        fn test_clear() {
2622            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2623
2624            let h1 = allocator.allocate();
2625            let h2 = allocator.allocate();
2626            let h3 = allocator.allocate();
2627
2628            assert_eq!(allocator.len(), 3);
2629
2630            allocator.clear();
2631
2632            assert_eq!(allocator.len(), 0);
2633            assert_eq!(allocator.capacity(), 3); // Capacity retained
2634
2635            // Old handles are stale
2636            assert!(!allocator.is_alive(h1));
2637            assert!(!allocator.is_alive(h2));
2638            assert!(!allocator.is_alive(h3));
2639
2640            // New handles have incremented generations
2641            let h4 = allocator.allocate();
2642            assert_eq!(h4.generation(), 2);
2643        }
2644
2645        #[test]
2646        fn test_generation_at() {
2647            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2648
2649            assert_eq!(allocator.generation_at(0), None);
2650
2651            let handle = allocator.allocate();
2652            assert_eq!(allocator.generation_at(0), Some(1));
2653
2654            allocator.deallocate(handle);
2655            assert_eq!(allocator.generation_at(0), Some(2));
2656        }
2657
2658        #[test]
2659        fn test_default() {
2660            let allocator: AssetHandleAllocator<TestTexture> = Default::default();
2661            assert!(allocator.is_empty());
2662        }
2663
2664        #[test]
2665        fn test_debug() {
2666            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2667            allocator.allocate();
2668            allocator.allocate();
2669
2670            let debug_str = format!("{:?}", allocator);
2671            assert!(debug_str.contains("AssetHandleAllocator"));
2672            assert!(debug_str.contains("TestTexture"));
2673            assert!(debug_str.contains("len"));
2674            assert!(debug_str.contains("2"));
2675        }
2676
2677        #[test]
2678        fn test_stress_allocate_deallocate() {
2679            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2680
2681            // Allocate many
2682            let handles: Vec<_> = (0..10000).map(|_| allocator.allocate()).collect();
2683            assert_eq!(allocator.len(), 10000);
2684
2685            // Deallocate all
2686            for h in handles {
2687                assert!(allocator.deallocate(h));
2688            }
2689            assert_eq!(allocator.len(), 0);
2690            assert_eq!(allocator.capacity(), 10000);
2691        }
2692
2693        #[test]
2694        fn test_stress_churn() {
2695            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2696
2697            // Simulate churn: allocate/deallocate repeatedly
2698            for _ in 0..100 {
2699                let handles: Vec<_> = (0..100).map(|_| allocator.allocate()).collect();
2700                for h in &handles[..50] {
2701                    allocator.deallocate(*h);
2702                }
2703                // Leave 50 alive
2704            }
2705
2706            // Should have 5000 alive (100 iterations * 50 kept)
2707            assert_eq!(allocator.len(), 5000);
2708        }
2709
2710        #[test]
2711        fn test_generation_wrap() {
2712            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2713
2714            // Manually test generation wrapping
2715            // This is mostly to verify the wrap logic; in practice u32 won't wrap
2716            let handle = allocator.allocate();
2717
2718            // Deallocate original handle first - now it's stale
2719            allocator.deallocate(handle);
2720            assert!(!allocator.is_alive(handle));
2721
2722            // Simulate many allocations/deallocations on the same slot
2723            for _ in 0..10 {
2724                let new_handle = allocator.allocate();
2725                allocator.deallocate(new_handle);
2726            }
2727
2728            // Original handle should still be stale (gen is now 12, original was 1)
2729            assert!(!allocator.is_alive(handle));
2730
2731            // Verify generation increased
2732            let gen = allocator.generation_at(0).unwrap();
2733            assert!(gen > handle.generation());
2734        }
2735
2736        #[test]
2737        fn test_shrink_to_fit() {
2738            let mut allocator: AssetHandleAllocator<TestTexture> = AssetHandleAllocator::new();
2739
2740            // Allocate then deallocate many
2741            let handles: Vec<_> = (0..100).map(|_| allocator.allocate()).collect();
2742            for h in handles {
2743                allocator.deallocate(h);
2744            }
2745
2746            // Free list should be large
2747            allocator.shrink_to_fit();
2748            // Can't directly test internal capacity, but shouldn't panic
2749        }
2750
2751        #[test]
2752        fn test_is_send() {
2753            fn requires_send<T: Send>() {}
2754            requires_send::<AssetHandleAllocator<TestTexture>>();
2755        }
2756
2757        // Note: AssetHandleAllocator is NOT Sync (contains Vec which isn't Sync for &mut)
2758        // This is intentional - allocators should be accessed through synchronization primitives
2759    }
2760}