Skip to main content

fyrox_resource/
loader.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 loader. It manages resource loading.
22
23use crate::{
24    core::{uuid::Uuid, TypeUuidProvider},
25    io::ResourceIo,
26    options::BaseImportOptions,
27    state::LoadError,
28    ResourceData, TypedResourceData,
29};
30use fyrox_core::io::FileError;
31use fyrox_core::platform::TargetPlatform;
32use fyrox_core::visitor::{Format, Visitor};
33use std::{
34    any::Any,
35    future::Future,
36    path::{Path, PathBuf},
37    pin::Pin,
38    sync::Arc,
39};
40
41#[cfg(target_arch = "wasm32")]
42#[doc(hidden)]
43pub trait BaseResourceLoader: Any {}
44#[cfg(target_arch = "wasm32")]
45impl<T: Any> BaseResourceLoader for T {}
46
47#[cfg(not(target_arch = "wasm32"))]
48#[doc(hidden)]
49pub trait BaseResourceLoader: Any + Send {}
50#[cfg(not(target_arch = "wasm32"))]
51impl<T: Any + Send> BaseResourceLoader for T {}
52
53fn convert_ascii_to_binary<F>(
54    src_path: PathBuf,
55    dest_path: PathBuf,
56    is_native_extension: F,
57    _platform: TargetPlatform,
58    io: Arc<dyn ResourceIo>,
59) -> Pin<Box<dyn Future<Output = Result<(), FileError>>>>
60where
61    F: Fn(&str) -> bool,
62{
63    if src_path
64        .extension()
65        .and_then(|src_ext| src_ext.to_str())
66        .is_some_and(is_native_extension)
67    {
68        Box::pin(async move {
69            let data = io.load_file(&src_path).await?;
70            match Visitor::detect_format_from_slice(&data) {
71                Format::Unknown => Err(FileError::Custom("Unknown format!".to_string())),
72                Format::Binary => {
73                    // Copy the binary format as-is.
74                    Ok(io.copy_file(&src_path, &dest_path).await?)
75                }
76                Format::Ascii => {
77                    // Resave the ascii format as binary.
78                    let visitor = Visitor::load_from_memory(&data).map_err(|err| {
79                        FileError::Custom(format!(
80                            "Unable to load {}. Reason: {err}",
81                            src_path.display()
82                        ))
83                    })?;
84                    visitor.save_binary_to_file(dest_path).map_err(|err| {
85                        FileError::Custom(format!(
86                            "Unable to save {}. Reason: {err}",
87                            src_path.display()
88                        ))
89                    })?;
90                    Ok(())
91                }
92            }
93        })
94    } else {
95        Box::pin(async move { io.copy_file(&src_path, &dest_path).await })
96    }
97}
98
99/// Trait for resource loading.
100pub trait ResourceLoader: BaseResourceLoader {
101    /// Returns a list of file extensions supported by the loader. Resource manager will use this list
102    /// to pick the correct resource loader when the user requests a resource.
103    fn extensions(&self) -> &[&str];
104
105    /// Returns `true` if the given extension corresponds to a resource in the native file format.
106    /// The default implementation returns `false`, which assumes that the extension corresponds
107    /// to a foreign file format.
108    fn is_native_extension(&self, #[allow(unused_variables)] ext: &str) -> bool {
109        false
110    }
111
112    /// Checks if the given extension is supported by this loader. Comparison is case-insensitive.
113    fn supports_extension(&self, ext: &str) -> bool {
114        self.extensions()
115            .iter()
116            .any(|e| fyrox_core::cmp_strings_case_insensitive(e, ext))
117    }
118
119    /// Must return a type uuid of the resource data type.
120    fn data_type_uuid(&self) -> Uuid;
121
122    /// Loads or reloads a resource.
123    fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture;
124
125    /// Loads a resource from the given path and converts it to a format, that is the most efficient
126    /// fot the given platform. This method is usually used to convert resources for production builds;
127    /// to make the resources as efficient as possible for the given platform. This method saves the
128    /// converted resource to the given `dest_path`. If the resource is already in its final form,
129    /// then this method should just copy the file from `src_path` to `dest_path`.
130    fn convert(
131        &self,
132        src_path: PathBuf,
133        dest_path: PathBuf,
134        #[allow(unused_variables)] platform: TargetPlatform,
135        io: Arc<dyn ResourceIo>,
136    ) -> Pin<Box<dyn Future<Output = Result<(), FileError>>>> {
137        convert_ascii_to_binary(
138            src_path,
139            dest_path,
140            |ext| self.is_native_extension(ext),
141            platform,
142            io,
143        )
144    }
145
146    /// Tries to load import settings for a resource.
147    fn try_load_import_settings(
148        &self,
149        #[allow(unused_variables)] resource_path: PathBuf,
150        #[allow(unused_variables)] io: Arc<dyn ResourceIo>,
151    ) -> BoxedImportOptionsLoaderFuture {
152        Box::pin(async move { None })
153    }
154
155    /// Returns default import options for the resource.
156    fn default_import_options(&self) -> Option<Box<dyn BaseImportOptions>> {
157        None
158    }
159}
160
161/// A result of executing a resource loader.
162pub struct LoaderPayload(pub(crate) Box<dyn ResourceData>);
163
164impl LoaderPayload {
165    /// Creates a new resource loader payload.
166    pub fn new<T: ResourceData>(data: T) -> Self {
167        Self(Box::new(data))
168    }
169}
170
171/// Future type for resource loading. See 'ResourceLoader'.
172#[cfg(target_arch = "wasm32")]
173pub type BoxedLoaderFuture = Pin<Box<dyn Future<Output = Result<LoaderPayload, LoadError>>>>;
174
175/// Future type for resource loading. See 'ResourceLoader'.
176#[cfg(not(target_arch = "wasm32"))]
177pub type BoxedLoaderFuture = Pin<Box<dyn Future<Output = Result<LoaderPayload, LoadError>> + Send>>;
178
179/// Future type for resource import options loading.
180pub type BoxedImportOptionsLoaderFuture =
181    Pin<Box<dyn Future<Output = Option<Box<dyn BaseImportOptions>>>>>;
182
183/// Container for resource loaders.
184#[derive(Default)]
185pub struct ResourceLoadersContainer {
186    loaders: Vec<Box<dyn ResourceLoader>>,
187}
188
189impl ResourceLoadersContainer {
190    /// Creates new empty resource loaders container.
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Adds new resource loader or replaces existing. There could be only one loader of a given type
196    /// at the same time. You can use this method to replace resource loaders with your own loaders.
197    pub fn set<T>(&mut self, loader: T) -> Option<T>
198    where
199        T: ResourceLoader,
200    {
201        if let Some(existing_loader) = self
202            .loaders
203            .iter_mut()
204            .find_map(|l| (&mut **l as &mut dyn Any).downcast_mut::<T>())
205        {
206            Some(std::mem::replace(existing_loader, loader))
207        } else {
208            self.loaders.push(Box::new(loader));
209            None
210        }
211    }
212
213    /// Searches for an instance of a resource loader of type `Prev` and replaces it with an other instance
214    /// of a type `New`.
215    pub fn try_replace<Prev, New>(&mut self, new_loader: New) -> Option<Prev>
216    where
217        Prev: ResourceLoader,
218        New: ResourceLoader,
219    {
220        if let Some(pos) = self
221            .loaders
222            .iter()
223            .position(|l| (&**l as &dyn Any).is::<Prev>())
224        {
225            let prev_untyped = std::mem::replace(&mut self.loaders[pos], Box::new(new_loader));
226            (prev_untyped as Box<dyn Any>)
227                .downcast::<Prev>()
228                .ok()
229                .map(|boxed| *boxed)
230        } else {
231            None
232        }
233    }
234
235    /// Tries to find an instance of a resource loader of the given type `T.
236    pub fn find<T>(&self) -> Option<&T>
237    where
238        T: ResourceLoader,
239    {
240        self.loaders
241            .iter()
242            .find_map(|loader| (&**loader as &dyn Any).downcast_ref())
243    }
244
245    /// Tries to find an instance of a resource loader of the given type `T.
246    pub fn find_mut<T>(&mut self) -> Option<&mut T>
247    where
248        T: ResourceLoader,
249    {
250        self.loaders
251            .iter_mut()
252            .find_map(|loader| (&mut **loader as &mut dyn Any).downcast_mut())
253    }
254
255    /// Returns total amount of resource loaders in the container.
256    pub fn len(&self) -> usize {
257        self.loaders.len()
258    }
259
260    /// Return `true` if the container contains no resource loaders.
261    pub fn is_empty(&self) -> bool {
262        self.loaders.is_empty()
263    }
264
265    /// Returns an iterator yielding shared references to "untyped" resource loaders.
266    pub fn iter(&self) -> impl Iterator<Item = &dyn ResourceLoader> {
267        self.loaders.iter().map(|boxed| &**boxed)
268    }
269
270    /// Returns an iterator yielding mutable references to "untyped" resource loaders.
271    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut dyn ResourceLoader> {
272        self.loaders.iter_mut().map(|boxed| &mut **boxed)
273    }
274
275    /// Returns `true` if there's at least one resource loader, that supports the extension of the
276    /// file at the given path.
277    pub fn is_supported_resource(&self, path: &Path) -> bool {
278        if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
279            self.loaders
280                .iter()
281                .any(|loader| loader.supports_extension(extension))
282        } else {
283            false
284        }
285    }
286
287    /// Tries to fina a loader for the specified data type uuid.
288    pub fn loader_for_data_type(&self, data_type_uuid: Uuid) -> Option<&dyn ResourceLoader> {
289        self.loaders
290            .iter()
291            .find(|loader| loader.data_type_uuid() == data_type_uuid)
292            .map(|l| &**l)
293    }
294
295    /// Checks if there's a resource loader for the given path and the data type produced by the
296    /// loader matches the given type `T`.
297    pub fn is_extension_matches_type<T>(&self, path: &Path) -> bool
298    where
299        T: TypedResourceData,
300    {
301        path.extension().is_some_and(|extension| {
302            self.loaders
303                .iter()
304                .find(|loader| loader.supports_extension(&extension.to_string_lossy()))
305                .is_some_and(|loader| {
306                    loader.data_type_uuid() == <T as TypeUuidProvider>::type_uuid()
307                })
308        })
309    }
310
311    /// Checks if there's a loader for the given path.
312    pub fn loader_for(&self, path: &Path) -> Option<&dyn ResourceLoader> {
313        path.extension().and_then(|extension| {
314            self.loaders
315                .iter()
316                .find(|loader| loader.supports_extension(&extension.to_string_lossy()))
317                .map(|l| &**l)
318        })
319    }
320}
321
322#[cfg(test)]
323mod test {
324    use super::*;
325
326    #[derive(Eq, PartialEq, Debug)]
327    struct MyResourceLoader;
328
329    impl ResourceLoader for MyResourceLoader {
330        fn extensions(&self) -> &[&str] {
331            &[]
332        }
333
334        fn data_type_uuid(&self) -> Uuid {
335            Default::default()
336        }
337
338        fn load(&self, _path: PathBuf, _io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
339            todo!()
340        }
341    }
342
343    #[test]
344    fn resource_loader_container_new() {
345        let container = ResourceLoadersContainer::new();
346        assert!(container.loaders.is_empty());
347
348        let container = ResourceLoadersContainer::default();
349        assert!(container.loaders.is_empty());
350    }
351
352    #[test]
353    fn resource_loader_container_set() {
354        let mut container = ResourceLoadersContainer::new();
355        let res = container.set(MyResourceLoader);
356        let res2 = container.set(MyResourceLoader);
357        assert_eq!(res, None);
358        assert_eq!(res2, Some(MyResourceLoader));
359
360        assert_eq!(container.len(), 1);
361    }
362
363    #[test]
364    fn resource_loader_container_find() {
365        let mut container = ResourceLoadersContainer::new();
366
367        let res = container.find::<MyResourceLoader>();
368        assert_eq!(res, None);
369
370        container.set(MyResourceLoader);
371        let res = container.find::<MyResourceLoader>();
372
373        assert_eq!(res, Some(&MyResourceLoader));
374    }
375
376    #[test]
377    fn resource_loader_container_find_mut() {
378        let mut container = ResourceLoadersContainer::new();
379
380        let res = container.find_mut::<MyResourceLoader>();
381        assert_eq!(res, None);
382
383        container.set(MyResourceLoader);
384        let res = container.find_mut::<MyResourceLoader>();
385
386        assert_eq!(res, Some(&mut MyResourceLoader));
387    }
388
389    #[test]
390    fn resource_loader_container_getters() {
391        let mut container = ResourceLoadersContainer::new();
392        assert!(container.is_empty());
393        assert_eq!(container.len(), 0);
394
395        container.set(MyResourceLoader);
396        container.set(MyResourceLoader);
397        assert!(!container.is_empty());
398        assert_eq!(container.len(), 1);
399    }
400}