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    /// Checks if there's a resource loader for the given path and the data type produced by the
288    /// loader matches the given type `T`.
289    pub fn is_extension_matches_type<T>(&self, path: &Path) -> bool
290    where
291        T: TypedResourceData,
292    {
293        path.extension().is_some_and(|extension| {
294            self.loaders
295                .iter()
296                .find(|loader| loader.supports_extension(&extension.to_string_lossy()))
297                .is_some_and(|loader| {
298                    loader.data_type_uuid() == <T as TypeUuidProvider>::type_uuid()
299                })
300        })
301    }
302
303    /// Checks if there's a loader for the given path.
304    pub fn loader_for(&self, path: &Path) -> Option<&dyn ResourceLoader> {
305        path.extension().and_then(|extension| {
306            self.loaders
307                .iter()
308                .find(|loader| loader.supports_extension(&extension.to_string_lossy()))
309                .map(|l| &**l)
310        })
311    }
312}
313
314#[cfg(test)]
315mod test {
316    use super::*;
317
318    #[derive(Eq, PartialEq, Debug)]
319    struct MyResourceLoader;
320
321    impl ResourceLoader for MyResourceLoader {
322        fn extensions(&self) -> &[&str] {
323            &[]
324        }
325
326        fn data_type_uuid(&self) -> Uuid {
327            Default::default()
328        }
329
330        fn load(&self, _path: PathBuf, _io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
331            todo!()
332        }
333    }
334
335    #[test]
336    fn resource_loader_container_new() {
337        let container = ResourceLoadersContainer::new();
338        assert!(container.loaders.is_empty());
339
340        let container = ResourceLoadersContainer::default();
341        assert!(container.loaders.is_empty());
342    }
343
344    #[test]
345    fn resource_loader_container_set() {
346        let mut container = ResourceLoadersContainer::new();
347        let res = container.set(MyResourceLoader);
348        let res2 = container.set(MyResourceLoader);
349        assert_eq!(res, None);
350        assert_eq!(res2, Some(MyResourceLoader));
351
352        assert_eq!(container.len(), 1);
353    }
354
355    #[test]
356    fn resource_loader_container_find() {
357        let mut container = ResourceLoadersContainer::new();
358
359        let res = container.find::<MyResourceLoader>();
360        assert_eq!(res, None);
361
362        container.set(MyResourceLoader);
363        let res = container.find::<MyResourceLoader>();
364
365        assert_eq!(res, Some(&MyResourceLoader));
366    }
367
368    #[test]
369    fn resource_loader_container_find_mut() {
370        let mut container = ResourceLoadersContainer::new();
371
372        let res = container.find_mut::<MyResourceLoader>();
373        assert_eq!(res, None);
374
375        container.set(MyResourceLoader);
376        let res = container.find_mut::<MyResourceLoader>();
377
378        assert_eq!(res, Some(&mut MyResourceLoader));
379    }
380
381    #[test]
382    fn resource_loader_container_getters() {
383        let mut container = ResourceLoadersContainer::new();
384        assert!(container.is_empty());
385        assert_eq!(container.len(), 0);
386
387        container.set(MyResourceLoader);
388        container.set(MyResourceLoader);
389        assert!(!container.is_empty());
390        assert_eq!(container.len(), 1);
391    }
392}