Skip to main content

fyrox_resource/
lib.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Resource management
22
23#![forbid(unsafe_code)]
24#![allow(clippy::doc_lazy_continuation)]
25#![allow(clippy::mutable_key_type)]
26#![warn(missing_docs)]
27
28use crate::{
29    core::{
30        combine_uuids,
31        parking_lot::MutexGuard,
32        reflect::prelude::*,
33        uuid::{uuid, Uuid},
34        visitor::prelude::*,
35        TypeUuidProvider,
36    },
37    state::{LoadError, ResourceState},
38    untyped::{ResourceHeader, ResourceKind, UntypedResource},
39};
40use fxhash::FxHashSet;
41pub use fyrox_core as core;
42use serde::{Deserialize, Serialize};
43use std::{
44    any::Any,
45    error::Error,
46    fmt::Display,
47    fmt::{Debug, Formatter},
48    future::Future,
49    hash::{Hash, Hasher},
50    marker::PhantomData,
51    ops::{Deref, DerefMut},
52    path::Path,
53    path::PathBuf,
54    pin::Pin,
55    task::{Context, Poll},
56};
57
58pub mod builtin;
59pub mod constructor;
60pub mod entry;
61pub mod event;
62pub mod graph;
63pub mod io;
64pub mod loader;
65pub mod manager;
66pub mod metadata;
67pub mod options;
68pub mod registry;
69pub mod state;
70pub mod untyped;
71
72/// A trait for resource data.
73pub trait ResourceData: Debug + Visit + Send + Reflect {
74    /// Returns unique data type id.
75    fn type_uuid(&self) -> Uuid;
76
77    /// Saves the resource data a file at the specified path. This method is free to
78    /// decide how the resource data is saved. This is needed, because there are multiple formats
79    /// that defines various kinds of resources. For example, a rectangular texture could be saved
80    /// into a bunch of formats, such as png, bmp, tga, jpg etc., but in the engine it is single
81    /// Texture resource. In any case, produced file should be compatible with a respective resource
82    /// loader.
83    fn save(&mut self, #[allow(unused_variables)] path: &Path) -> Result<(), Box<dyn Error>>;
84
85    /// Returns `true` if the resource data can be saved to a file, `false` - otherwise. Not every
86    /// resource type supports saving, for example there might be temporary resource type that is
87    /// used only at runtime which does not need saving at all.
88    fn can_be_saved(&self) -> bool;
89
90    /// Tries to clone the resource data. This method can return `None` if the underlying type is
91    /// non-cloneable.
92    fn try_clone_box(&self) -> Option<Box<dyn ResourceData>>;
93}
94
95/// Extension trait for a resource data of a particular type, which adds additional functionality,
96/// such as: a way to get default state of the data (`Default` impl), a way to get data's type uuid.
97/// The trait has automatic implementation for any type that implements
98/// ` ResourceData + Default + TypeUuidProvider` traits.
99pub trait TypedResourceData: ResourceData + Default + TypeUuidProvider {}
100
101impl<T> TypedResourceData for T where T: ResourceData + Default + TypeUuidProvider {}
102
103/// A trait for resource load error.
104pub trait ResourceLoadError: 'static + Debug + Display + Send + Sync {}
105
106impl<T> ResourceLoadError for T where T: 'static + Debug + Display + Send + Sync {}
107
108/// Provides typed access to a resource state.
109pub struct ResourceHeaderGuard<'a, T>
110where
111    T: TypedResourceData,
112{
113    guard: MutexGuard<'a, ResourceHeader>,
114    phantom: PhantomData<T>,
115}
116
117impl<'a, T> From<MutexGuard<'a, ResourceHeader>> for ResourceHeaderGuard<'a, T>
118where
119    T: TypedResourceData,
120{
121    fn from(guard: MutexGuard<'a, ResourceHeader>) -> Self {
122        Self {
123            guard,
124            phantom: PhantomData,
125        }
126    }
127}
128
129impl<T> ResourceHeaderGuard<'_, T>
130where
131    T: TypedResourceData,
132{
133    /// The UUID that universally identifies the resource.
134    pub fn resource_uuid(&self) -> Uuid {
135        self.guard.uuid
136    }
137
138    /// Returns resource kind of the locked resource.
139    pub fn kind(&self) -> ResourceKind {
140        self.guard.kind
141    }
142
143    /// Tries to fetch the underlying data of the resource type. This operation will fail if the
144    /// locked resource is not in [`ResourceState::Ok`] or if its actual data does not match the
145    /// type of the resource.
146    pub fn data(&mut self) -> Option<&mut T> {
147        if let ResourceState::Ok { ref mut data, .. } = self.guard.state {
148            (&mut **data as &mut dyn Any).downcast_mut::<T>()
149        } else {
150            None
151        }
152    }
153
154    /// Tries to fetch the underlying data of the resource type. This operation will fail if the
155    /// locked resource is not in [`ResourceState::Ok`] or if its actual data does not match the
156    /// type of the resource.
157    pub fn data_ref(&self) -> Option<&T> {
158        if let ResourceState::Ok { ref data, .. } = self.guard.state {
159            (&**data as &dyn Any).downcast_ref::<T>()
160        } else {
161            None
162        }
163    }
164
165    /// Tries to fetch the underlying data of the resource type. This operation will fail if the
166    /// locked resource is not in [`ResourceState::Ok`] or if its actual data does not match the
167    /// type of the resource.
168    pub fn data_ref_with_id(&self) -> Option<(&T, &Uuid)> {
169        let uuid = &self.guard.uuid;
170        if let ResourceState::Ok { ref data } = self.guard.state {
171            (&**data as &dyn Any)
172                .downcast_ref::<T>()
173                .map(|typed| (typed, uuid))
174        } else {
175            None
176        }
177    }
178}
179
180/// A resource of particular data type. It is a typed wrapper around [`UntypedResource`] which
181/// does type checks at runtime. See [`UntypedResource`] for more info.
182///
183/// ## Default State
184///
185/// Default state of the resource will be [`ResourceState::Ok`] with `T::default`.
186#[derive(Reflect, Serialize, Deserialize)]
187#[serde(
188    bound = "T: TypedResourceData",
189    from = "UntypedResource",
190    into = "UntypedResource"
191)]
192pub struct Resource<T: Debug> {
193    untyped: UntypedResource,
194    #[reflect(hidden)]
195    phantom: PhantomData<T>,
196}
197
198impl<T: Debug> Debug for Resource<T> {
199    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
200        write!(
201            f,
202            "Resource<{}>({})",
203            std::any::type_name::<T>(),
204            self.untyped
205        )
206    }
207}
208
209impl<T: TypedResourceData> AsRef<UntypedResource> for Resource<T> {
210    fn as_ref(&self) -> &UntypedResource {
211        &self.untyped
212    }
213}
214
215impl AsRef<UntypedResource> for UntypedResource {
216    fn as_ref(&self) -> &UntypedResource {
217        self
218    }
219}
220
221impl<T: TypedResourceData> TypeUuidProvider for Resource<T> {
222    fn type_uuid() -> Uuid {
223        combine_uuids(
224            uuid!("790b1a1c-a997-46c4-ac3b-8565501f0052"),
225            <T as TypeUuidProvider>::type_uuid(),
226        )
227    }
228}
229
230impl<T> Visit for Resource<T>
231where
232    T: TypedResourceData,
233{
234    fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
235        self.untyped
236            .visit_with_type_uuid(name, Some(<T as TypeUuidProvider>::type_uuid()), visitor)
237    }
238}
239
240impl<T> PartialEq for Resource<T>
241where
242    T: TypedResourceData,
243{
244    fn eq(&self, other: &Self) -> bool {
245        self.untyped == other.untyped
246    }
247}
248
249impl<T> Eq for Resource<T> where T: TypedResourceData {}
250
251impl<T> Hash for Resource<T>
252where
253    T: TypedResourceData,
254{
255    fn hash<H: Hasher>(&self, state: &mut H) {
256        self.untyped.hash(state)
257    }
258}
259
260impl<T> Resource<T>
261where
262    T: TypedResourceData,
263{
264    /// Create a summary of this resource handle, wht UUID, embedded/external, and Ok, Pending, Error, etc.
265    pub fn summary(&self) -> String {
266        format!("{}", self.untyped)
267    }
268    /// Creates new resource in pending state.
269    #[inline]
270    pub fn new_pending(uuid: Uuid, kind: ResourceKind) -> Self {
271        Self {
272            untyped: UntypedResource::new_pending(uuid, kind),
273            phantom: PhantomData,
274        }
275    }
276
277    /// Creates new resource in ok state (fully loaded).
278    #[inline]
279    pub fn new_ok(resource_uuid: Uuid, kind: ResourceKind, data: T) -> Self {
280        Self {
281            untyped: UntypedResource::new_ok(resource_uuid, kind, data),
282            phantom: PhantomData,
283        }
284    }
285
286    /// Creates a new embedded resource in ok state (fully loaded).
287    #[inline]
288    pub fn new_embedded(data: T) -> Self {
289        Self {
290            untyped: UntypedResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, data),
291            phantom: PhantomData,
292        }
293    }
294
295    /// Creates new resource in error state.
296    #[inline]
297    pub fn new_load_error(kind: ResourceKind, path: PathBuf, error: LoadError) -> Self {
298        Self {
299            untyped: UntypedResource::new_load_error(kind, path, error),
300            phantom: PhantomData,
301        }
302    }
303
304    /// Converts self to internal value.
305    #[inline]
306    pub fn into_untyped(self) -> UntypedResource {
307        self.untyped
308    }
309
310    /// Locks internal mutex provides access to the state.
311    #[inline]
312    pub fn state(&self) -> ResourceHeaderGuard<'_, T> {
313        self.untyped.typed_lock()
314    }
315
316    /// Tries to lock internal mutex provides access to the state.
317    #[inline]
318    pub fn try_acquire_state(&self) -> Option<ResourceHeaderGuard<'_, T>> {
319        self.untyped.try_typed_lock()
320    }
321
322    /// Locks the resource and provides access to its header. See [`ResourceHeader`] docs for more info.
323    #[inline]
324    pub fn header(&self) -> MutexGuard<'_, ResourceHeader> {
325        self.untyped.lock()
326    }
327
328    /// Returns true if the resource is still loading.
329    #[inline]
330    pub fn is_loading(&self) -> bool {
331        self.untyped.is_loading()
332    }
333
334    /// Returns true if the resource is fully loaded and ready for use.
335    #[inline]
336    pub fn is_ok(&self) -> bool {
337        self.untyped.is_ok()
338    }
339
340    /// Returns true if the resource is failed to load.
341    #[inline]
342    pub fn is_failed_to_load(&self) -> bool {
343        self.untyped.is_failed_to_load()
344    }
345
346    /// Returns exact amount of users of the resource.
347    #[inline]
348    pub fn use_count(&self) -> usize {
349        self.untyped.use_count()
350    }
351
352    /// Returns a pointer as numeric value which can be used as a hash.
353    #[inline]
354    pub fn key(&self) -> u64 {
355        self.untyped.key()
356    }
357
358    /// Returns kind of the resource, if the resource is registered.
359    #[inline]
360    pub fn kind(&self) -> ResourceKind {
361        self.untyped.kind()
362    }
363
364    /// The UUID of the resource. All resources must have a UUID, even if they are not loaded
365    /// because the UUID is how the resource manager knows the path to load from.
366    #[inline]
367    pub fn resource_uuid(&self) -> Uuid {
368        self.untyped.resource_uuid()
369    }
370
371    /// Sets a new kind of the resource.
372    #[inline]
373    pub fn set_path(&mut self, new_kind: ResourceKind) {
374        self.untyped.set_kind(new_kind);
375    }
376
377    /// Allows you to get a reference to the resource data. The returned object implements [`Deref`]
378    /// and [`DerefMut`] traits, and basically acts like a reference to the resource value.
379    ///
380    /// # Panic
381    ///
382    /// An attempt to dereference the returned object will result in panic if the resource is not
383    /// loaded yet, or there was a loading error. Usually this is ok because you should chain this
384    /// call like this `resource.await?.data_ref()`. Every resource implements [`Future`] trait,
385    /// and it returns Result, so if you'll await the future then you'll get Result, so call to
386    /// `data_ref` will be fine.
387    ///
388    /// You can also use [`ResourceDataRef::as_loaded_ref`] and [`ResourceDataRef::as_loaded_mut`]
389    /// methods that perform checked access to the resource internals.
390    #[inline]
391    pub fn data_ref(&self) -> ResourceDataRef<'_, T> {
392        ResourceDataRef {
393            guard: self.untyped.lock(),
394            phantom: Default::default(),
395        }
396    }
397
398    /// Tries to save the resource to the specified path.
399    pub fn save(&self, path: &Path) -> Result<(), Box<dyn Error>> {
400        self.untyped.save(path)
401    }
402}
403
404impl<T> Default for Resource<T>
405where
406    T: TypedResourceData,
407{
408    #[inline]
409    fn default() -> Self {
410        Self {
411            untyped: UntypedResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, T::default()),
412            phantom: Default::default(),
413        }
414    }
415}
416
417impl<T: Debug> Clone for Resource<T> {
418    #[inline]
419    fn clone(&self) -> Self {
420        Self {
421            untyped: self.untyped.clone(),
422            phantom: Default::default(),
423        }
424    }
425}
426
427impl<T> From<Uuid> for Resource<T>
428where
429    T: TypedResourceData,
430{
431    fn from(uuid: Uuid) -> Self {
432        UntypedResource::from(uuid).into()
433    }
434}
435
436impl<T> From<UntypedResource> for Resource<T>
437where
438    T: TypedResourceData,
439{
440    #[inline]
441    fn from(untyped: UntypedResource) -> Self {
442        if let Some(type_uuid) = untyped.type_uuid() {
443            let expected = <T as TypeUuidProvider>::type_uuid();
444            if type_uuid != expected {
445                panic!("Resource type mismatch. Expected: {expected}. Found: {type_uuid}");
446            }
447        }
448        Self {
449            untyped,
450            phantom: Default::default(),
451        }
452    }
453}
454
455#[allow(clippy::from_over_into)]
456impl<T> Into<UntypedResource> for Resource<T>
457where
458    T: TypedResourceData,
459{
460    #[inline]
461    fn into(self) -> UntypedResource {
462        self.untyped
463    }
464}
465
466impl<T> Future for Resource<T>
467where
468    T: TypedResourceData,
469{
470    type Output = Result<Self, LoadError>;
471
472    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
473        let mut inner = self.untyped.clone();
474        Pin::new(&mut inner)
475            .poll(cx)
476            .map(|r| r.map(|_| self.clone()))
477    }
478}
479
480#[doc(hidden)]
481pub struct ResourceDataRef<'a, T>
482where
483    T: TypedResourceData,
484{
485    guard: MutexGuard<'a, ResourceHeader>,
486    phantom: PhantomData<T>,
487}
488
489impl<T> ResourceDataRef<'_, T>
490where
491    T: TypedResourceData,
492{
493    #[inline]
494    pub fn as_loaded_ref(&self) -> Option<&T> {
495        match self.guard.state {
496            ResourceState::Ok { ref data, .. } => (&**data as &dyn Any).downcast_ref(),
497            _ => None,
498        }
499    }
500
501    #[inline]
502    pub fn as_loaded_mut(&mut self) -> Option<&mut T> {
503        match self.guard.state {
504            ResourceState::Ok { ref mut data, .. } => (&mut **data as &mut dyn Any).downcast_mut(),
505            _ => None,
506        }
507    }
508}
509
510impl<T> Debug for ResourceDataRef<'_, T>
511where
512    T: TypedResourceData,
513{
514    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
515        match self.guard.state {
516            ResourceState::Unloaded => {
517                write!(
518                    f,
519                    "Attempt to get reference to resource data while it is unloaded!"
520                )
521            }
522            ResourceState::Pending { .. } => {
523                write!(
524                    f,
525                    "Attempt to get reference to resource data while it is loading!"
526                )
527            }
528            ResourceState::LoadError { .. } => {
529                write!(
530                    f,
531                    "Attempt to get reference to resource data which failed to load!"
532                )
533            }
534            ResourceState::Ok { ref data, .. } => data.fmt(f),
535        }
536    }
537}
538
539impl<T> Deref for ResourceDataRef<'_, T>
540where
541    T: TypedResourceData,
542{
543    type Target = T;
544
545    fn deref(&self) -> &Self::Target {
546        match self.guard.state {
547            ResourceState::Unloaded => {
548                panic!(
549                    "Attempt to get reference to resource data while it is unloaded! Type {}",
550                    std::any::type_name::<T>()
551                )
552            }
553            ResourceState::Pending { .. } => {
554                panic!(
555                    "Attempt to get reference to resource data while it is loading! Type {}",
556                    std::any::type_name::<T>()
557                )
558            }
559            ResourceState::LoadError {
560                ref path,
561                ref error,
562            } => {
563                let path = if path.as_os_str().is_empty() {
564                    "Unknown".to_string()
565                } else {
566                    format!("{path:?}")
567                };
568                panic!("Attempt to get reference to resource data which failed to load! Type {}. Path: {path}. Error: {error:?}", std::any::type_name::<T>())
569            }
570            ResourceState::Ok { ref data } => (&**data as &dyn Any)
571                .downcast_ref()
572                .expect("Type mismatch!"),
573        }
574    }
575}
576
577impl<T> DerefMut for ResourceDataRef<'_, T>
578where
579    T: TypedResourceData,
580{
581    fn deref_mut(&mut self) -> &mut Self::Target {
582        let header = &mut *self.guard;
583        match header.state {
584            ResourceState::Unloaded => {
585                panic!("Attempt to get reference to resource data while it is unloaded!")
586            }
587            ResourceState::Pending { .. } => {
588                panic!("Attempt to get reference to resource data while it is loading!")
589            }
590            ResourceState::LoadError { .. } => {
591                panic!("Attempt to get reference to resource data which failed to load!")
592            }
593            ResourceState::Ok { ref mut data, .. } => (&mut **data as &mut dyn Any)
594                .downcast_mut()
595                .expect("Type mismatch!"),
596        }
597    }
598}
599
600/// Collects all resources used by a given entity. Internally, it uses reflection to iterate over
601/// each field of every descendant sub-object of the entity. This function could be used to collect
602/// all resources used by an object, which could be useful if you're building a resource dependency
603/// analyzer.
604pub fn collect_used_resources(
605    entity: &dyn Reflect,
606    resources_collection: &mut FxHashSet<UntypedResource>,
607) {
608    #[inline(always)]
609    fn type_is<T: Reflect>(entity: &dyn Reflect) -> bool {
610        let mut types_match = false;
611        entity.downcast_ref::<T>(&mut |v| {
612            types_match = v.is_some();
613        });
614        types_match
615    }
616
617    // Skip potentially large chunks of numeric data, that definitely cannot contain any resources.
618    // TODO: This is a brute-force solution which does not include all potential types with plain
619    // data.
620    let mut finished = type_is::<Vec<u8>>(entity)
621        || type_is::<Vec<u16>>(entity)
622        || type_is::<Vec<u32>>(entity)
623        || type_is::<Vec<u64>>(entity)
624        || type_is::<Vec<i8>>(entity)
625        || type_is::<Vec<i16>>(entity)
626        || type_is::<Vec<i32>>(entity)
627        || type_is::<Vec<i64>>(entity)
628        || type_is::<Vec<f32>>(entity)
629        || type_is::<Vec<f64>>(entity);
630
631    if finished {
632        return;
633    }
634
635    entity.downcast_ref::<UntypedResource>(&mut |v| {
636        if let Some(resource) = v {
637            resources_collection.insert(resource.clone());
638            finished = true;
639        }
640    });
641
642    if finished {
643        return;
644    }
645
646    entity.as_array(&mut |array| {
647        if let Some(array) = array {
648            for i in 0..array.reflect_len() {
649                if let Some(item) = array.reflect_index(i) {
650                    collect_used_resources(item, resources_collection)
651                }
652            }
653
654            finished = true;
655        }
656    });
657
658    if finished {
659        return;
660    }
661
662    entity.as_inheritable_variable(&mut |inheritable| {
663        if let Some(inheritable) = inheritable {
664            collect_used_resources(inheritable.inner_value_ref(), resources_collection);
665
666            finished = true;
667        }
668    });
669
670    if finished {
671        return;
672    }
673
674    entity.as_hash_map(&mut |hash_map| {
675        if let Some(hash_map) = hash_map {
676            for i in 0..hash_map.reflect_len() {
677                if let Some((key, value)) = hash_map.reflect_get_at(i) {
678                    collect_used_resources(key, resources_collection);
679                    collect_used_resources(value, resources_collection);
680                }
681            }
682
683            finished = true;
684        }
685    });
686
687    if finished {
688        return;
689    }
690
691    entity.fields_ref(&mut |fields| {
692        for field in fields {
693            collect_used_resources(field.value.field_value_as_reflect(), resources_collection);
694        }
695    })
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::{
702        io::{FsResourceIo, ResourceIo},
703        loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader, ResourceLoadersContainer},
704        manager::ResourceManager,
705        metadata::ResourceMetadata,
706        registry::ResourceRegistry,
707        state::LoadError,
708        ResourceData,
709    };
710    use fyrox_core::{
711        append_extension, futures::executor::block_on, io::FileError, parking_lot::Mutex,
712        reflect::prelude::*, task::TaskPool, uuid, visitor::prelude::*, SafeLock, TypeUuidProvider,
713        Uuid,
714    };
715    use ron::ser::PrettyConfig;
716    use serde::{Deserialize, Serialize};
717    use std::{
718        error::Error,
719        fs::File,
720        io::Write,
721        ops::Range,
722        path::{Path, PathBuf},
723        sync::Arc,
724    };
725
726    #[derive(Serialize, Deserialize, Default, Debug, Clone, Visit, Reflect, TypeUuidProvider)]
727    #[type_uuid(id = "241d14c7-079e-4395-a63c-364f0fc3e6ea")]
728    struct MyData {
729        data: u32,
730    }
731
732    impl MyData {
733        pub async fn load_from_file(
734            path: &Path,
735            resource_io: &dyn ResourceIo,
736        ) -> Result<Self, FileError> {
737            resource_io.load_file(path).await.and_then(|metadata| {
738                ron::de::from_bytes::<Self>(&metadata).map_err(|err| {
739                    FileError::Custom(format!(
740                        "Unable to deserialize the resource metadata. Reason: {err:?}"
741                    ))
742                })
743            })
744        }
745    }
746
747    impl ResourceData for MyData {
748        fn type_uuid(&self) -> Uuid {
749            <Self as TypeUuidProvider>::type_uuid()
750        }
751
752        fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
753            let string = ron::ser::to_string_pretty(self, PrettyConfig::default())
754                .map_err(|err| {
755                    FileError::Custom(format!(
756                        "Unable to serialize resource metadata for {} resource! Reason: {}",
757                        path.display(),
758                        err
759                    ))
760                })
761                .map_err(|_| "error".to_string())?;
762            let mut file = File::create(path)?;
763            file.write_all(string.as_bytes())?;
764            Ok(())
765        }
766
767        fn can_be_saved(&self) -> bool {
768            true
769        }
770
771        fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
772            Some(Box::new(self.clone()))
773        }
774    }
775
776    struct MyDataLoader {}
777
778    impl MyDataLoader {
779        const EXT: &'static str = "my_data";
780    }
781
782    impl ResourceLoader for MyDataLoader {
783        fn extensions(&self) -> &[&str] {
784            &[Self::EXT]
785        }
786
787        fn data_type_uuid(&self) -> Uuid {
788            <MyData as TypeUuidProvider>::type_uuid()
789        }
790
791        fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
792            Box::pin(async move {
793                let my_data = MyData::load_from_file(&path, io.as_ref())
794                    .await
795                    .map_err(LoadError::new)?;
796                Ok(LoaderPayload::new(my_data))
797            })
798        }
799    }
800
801    const TEST_FOLDER1: &str = "./test_output1";
802    const TEST_FOLDER2: &str = "./test_output2";
803    const TEST_FOLDER3: &str = "./test_output3";
804
805    fn make_file_path(root: &str, n: usize) -> PathBuf {
806        Path::new(root).join(format!("test{n}.{}", MyDataLoader::EXT))
807    }
808
809    fn make_metadata_file_path(root: &str, n: usize) -> PathBuf {
810        Path::new(root).join(format!(
811            "test{n}.{}.{}",
812            MyDataLoader::EXT,
813            ResourceMetadata::EXTENSION
814        ))
815    }
816
817    fn write_test_resources(root: &str, indices: Range<usize>) {
818        let path = Path::new(root);
819        if !std::fs::exists(path).unwrap() {
820            std::fs::create_dir_all(path).unwrap();
821        }
822
823        for i in indices {
824            MyData { data: i as u32 }
825                .save(&make_file_path(root, i))
826                .unwrap();
827        }
828    }
829
830    #[test]
831    fn test_serialize() {
832        let uuid = uuid!("6d1aadb5-42e1-485b-910b-fa4d81b61855");
833        let typed = Resource::<MyData>::from(uuid);
834        let untyped = UntypedResource::from(uuid);
835        let s = ron::ser::to_string(&typed).unwrap();
836        assert_eq!(&ron::ser::to_string(&uuid).unwrap(), &s);
837        assert_eq!(&ron::ser::to_string(&untyped).unwrap(), &s);
838        let output_uuid = ron::de::from_str::<Uuid>(&s).unwrap();
839        assert_eq!(output_uuid, uuid);
840        let untyped = ron::de::from_str::<UntypedResource>(&s).unwrap();
841        assert_eq!(untyped.resource_uuid(), uuid);
842        assert_eq!(untyped.kind(), ResourceKind::External);
843        let output = ron::de::from_str::<Resource<MyData>>(&s).unwrap();
844        assert_eq!(output.resource_uuid(), uuid);
845        assert_eq!(output.kind(), ResourceKind::External);
846    }
847
848    #[test]
849    fn test_registry_scan() {
850        write_test_resources(TEST_FOLDER1, 0..2);
851
852        assert!(std::fs::exists(make_file_path(TEST_FOLDER1, 0)).unwrap());
853        assert!(std::fs::exists(make_file_path(TEST_FOLDER1, 1)).unwrap());
854
855        let io = Arc::new(FsResourceIo);
856
857        let mut loaders = ResourceLoadersContainer::new();
858        loaders.set(MyDataLoader {});
859        let loaders = Arc::new(Mutex::new(loaders));
860
861        let registry = block_on(ResourceRegistry::scan(
862            io,
863            loaders,
864            Path::new(TEST_FOLDER1).join("resources.registry"),
865            Default::default(),
866        ));
867
868        assert!(std::fs::exists(make_metadata_file_path(TEST_FOLDER1, 0)).unwrap());
869        assert!(std::fs::exists(make_metadata_file_path(TEST_FOLDER1, 1)).unwrap());
870
871        assert_eq!(registry.len(), 2);
872    }
873
874    #[test]
875    fn test_resource_manager_request_simple() {
876        write_test_resources(TEST_FOLDER2, 2..4);
877        let resource_manager =
878            ResourceManager::new(Arc::new(FsResourceIo), Arc::new(TaskPool::new()));
879        resource_manager
880            .state()
881            .resource_registry
882            .safe_lock()
883            .set_path(Path::new(TEST_FOLDER2).join("resources.registry"));
884        resource_manager.add_loader(MyDataLoader {});
885        resource_manager.update_or_load_registry();
886        let path1 = make_file_path(TEST_FOLDER2, 2);
887        let path2 = make_file_path(TEST_FOLDER2, 3);
888        let res1 = resource_manager.request::<MyData>(&path1);
889        let res1_2 = resource_manager.request::<MyData>(&path1);
890        assert_eq!(res1.key(), res1_2.key());
891        let res2 = resource_manager.request::<MyData>(path2);
892        assert_ne!(res1.key(), res2.key());
893        assert_eq!(block_on(res1).unwrap().data_ref().data, 2);
894        assert_eq!(block_on(res2).unwrap().data_ref().data, 3);
895    }
896
897    #[test]
898    fn test_move_resource() {
899        write_test_resources(TEST_FOLDER3, 0..2);
900        let resource_manager =
901            ResourceManager::new(Arc::new(FsResourceIo), Arc::new(TaskPool::new()));
902        resource_manager
903            .state()
904            .resource_registry
905            .safe_lock()
906            .set_path(Path::new(TEST_FOLDER3).join("resources.registry"));
907        resource_manager.add_loader(MyDataLoader {});
908        resource_manager.update_or_load_registry();
909        let path1 = make_file_path(TEST_FOLDER3, 0);
910        let path2 = make_file_path(TEST_FOLDER3, 1);
911        let res1 = resource_manager.request::<MyData>(path1);
912        let res2 = resource_manager.request::<MyData>(path2);
913        assert_eq!(block_on(res1.clone()).unwrap().data_ref().data, 0);
914        assert_eq!(block_on(res2.clone()).unwrap().data_ref().data, 1);
915        let new_res1_path = resource_manager
916            .resource_io()
917            .canonicalize_path(&make_file_path(TEST_FOLDER3, 3))
918            .unwrap();
919        let new_res2_path = resource_manager
920            .resource_io()
921            .canonicalize_path(&make_file_path(TEST_FOLDER3, 4))
922            .unwrap();
923        block_on(resource_manager.move_resource(res1.as_ref(), &new_res1_path, true)).unwrap();
924        block_on(resource_manager.move_resource(res2.as_ref(), &new_res2_path, true)).unwrap();
925        assert_eq!(
926            resource_manager.resource_path(res1.as_ref()).unwrap(),
927            new_res1_path
928        );
929        assert!(
930            std::fs::exists(append_extension(new_res1_path, ResourceMetadata::EXTENSION)).unwrap()
931        );
932        assert_eq!(
933            resource_manager.resource_path(res2.as_ref()).unwrap(),
934            new_res2_path
935        );
936        assert!(
937            std::fs::exists(append_extension(new_res2_path, ResourceMetadata::EXTENSION)).unwrap()
938        );
939    }
940}