Skip to main content

fyrox_resource/
registry.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 registry is a database, that contains `UUID -> Path` mappings for every **external**
22//! resource used in your game. See [`ResourceRegistry`] docs for more info.
23
24use crate::{
25    core::{
26        append_extension, err, info, io::FileError, ok_or_return, parking_lot::Mutex, warn, Uuid,
27    },
28    io::ResourceIo,
29    loader::ResourceLoadersContainer,
30    metadata::ResourceMetadata,
31};
32use fxhash::FxHashSet;
33use fyrox_core::{futures::executor::block_on, SafeLock};
34use ron::ser::PrettyConfig;
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::{
37    collections::BTreeMap,
38    path::{Path, PathBuf},
39    sync::Arc,
40};
41
42/// A type alias for the actual registry data container.
43pub type RegistryContainer = BTreeMap<Uuid, PathBuf>;
44
45/// An extension trait with save/load methods.
46#[allow(async_fn_in_trait)]
47pub trait RegistryContainerExt: Sized {
48    /// Serializes the registry into a formatted string.
49    fn serialize_to_string(&self) -> Result<String, FileError>;
50
51    /// Tries to load a registry from a file using the specified resource IO. This method is intended
52    /// to be used only in async contexts.
53    async fn load_from_file(path: &Path, resource_io: &dyn ResourceIo) -> Result<Self, FileError>;
54
55    /// Tries to save the registry into a file using the specified resource IO. This method is
56    /// intended to be used only in async contexts.
57    async fn save(&self, path: &Path, resource_io: &dyn ResourceIo) -> Result<(), FileError>;
58
59    /// Same as [`Self::save`], but synchronous.
60    fn save_sync(&self, path: &Path, resource_io: &dyn ResourceIo) -> Result<(), FileError>;
61}
62
63impl RegistryContainerExt for RegistryContainer {
64    fn serialize_to_string(&self) -> Result<String, FileError> {
65        ron::ser::to_string_pretty(self, PrettyConfig::default()).map_err(|err| {
66            FileError::Custom(format!(
67                "Unable to serialize resource registry! Reason: {err}"
68            ))
69        })
70    }
71
72    async fn load_from_file(path: &Path, resource_io: &dyn ResourceIo) -> Result<Self, FileError> {
73        resource_io.load_file(path).await.and_then(|metadata| {
74            ron::de::from_bytes::<Self>(&metadata).map_err(|err| {
75                FileError::Custom(format!(
76                    "Unable to deserialize the resource registry. Reason: {err}"
77                ))
78            })
79        })
80    }
81
82    async fn save(&self, path: &Path, resource_io: &dyn ResourceIo) -> Result<(), FileError> {
83        let string = self.serialize_to_string()?;
84        resource_io.write_file(path, string.into_bytes()).await
85    }
86
87    fn save_sync(&self, path: &Path, resource_io: &dyn ResourceIo) -> Result<(), FileError> {
88        let string = self.serialize_to_string()?;
89        resource_io.write_file_sync(path, string.as_bytes())
90    }
91}
92
93/// A shared flag that can be used to fetch the current status of a resource registry.
94#[derive(Default, Clone)]
95pub struct ResourceRegistryStatusFlag(Arc<AtomicBool>);
96
97impl ResourceRegistryStatusFlag {
98    /// Returns `true` if the registry loaded, `false` - otherwise.
99    pub fn is_loaded(&self) -> bool {
100        self.0.load(Ordering::SeqCst)
101    }
102
103    /// Marks the registry as loaded.
104    pub fn mark_as_loaded(&self) {
105        self.0.store(true, Ordering::SeqCst);
106        info!("Resource registry finished loading.");
107    }
108
109    /// Marks the registry as loading. This method should be used before trying to load a registry
110    /// from an external source.
111    pub fn mark_as_unloaded(&self) {
112        self.0.store(false, Ordering::SeqCst);
113    }
114}
115
116/// A mutable reference to a resource registry. Automatically saves the registry back to a source
117/// from which it was loaded when an instance of this object is dropped. To prevent saving use
118/// [`std::mem::forget`] after you've finished working with the mutable reference.
119pub struct ResourceRegistryRefMut<'a> {
120    registry: &'a mut ResourceRegistry,
121    changed: bool,
122}
123
124/// A value returned from an update operation to a [`ResourceRegistryRefMut`],
125/// including a flag that indicates whether the operation changed the registry.
126pub struct RegistryUpdate<T> {
127    /// True if the registry was changed.
128    pub changed: bool,
129    /// The value produced by the operation.
130    pub value: T,
131}
132
133impl ResourceRegistryRefMut<'_> {
134    /// Read the metadata from the file at the given path.
135    pub fn read_metadata(
136        &mut self,
137        path: PathBuf,
138    ) -> Result<RegistryUpdate<ResourceMetadata>, FileError> {
139        let value = block_on(ResourceMetadata::load_from_file_async(
140            &append_extension(&path, ResourceMetadata::EXTENSION),
141            &*self.registry.io,
142        ))?;
143        let changed = self.register(value.resource_id, path).changed;
144        Ok(RegistryUpdate { changed, value })
145    }
146    /// Writes the new metadata file for a resource at the given path and registers the resource
147    /// in the registry.
148    pub fn write_metadata(
149        &mut self,
150        uuid: Uuid,
151        path: PathBuf,
152    ) -> Result<RegistryUpdate<Option<PathBuf>>, FileError> {
153        ResourceMetadata { resource_id: uuid }.save_sync(
154            &append_extension(&path, ResourceMetadata::EXTENSION),
155            &*self.registry.io,
156        )?;
157        Ok(self.register(uuid, path))
158    }
159
160    /// Unregisters the resource at the given path (if any) from the registry and deletes its
161    /// associated metadata file.
162    pub fn remove_metadata(&mut self, path: impl AsRef<Path>) -> Result<(), FileError> {
163        if self.unregister_path(&path).is_some() {
164            let metadata_path = append_extension(path.as_ref(), ResourceMetadata::EXTENSION);
165
166            self.registry.io.delete_file_sync(&metadata_path)?;
167
168            Ok(())
169        } else {
170            Err(FileError::Custom(format!(
171                "The {:?} resource is not registered in the registry!",
172                path.as_ref()
173            )))
174        }
175    }
176
177    /// Registers a new pair `UUID -> Path`, and returns the former path for this UUID.
178    pub fn register(&mut self, uuid: Uuid, path: PathBuf) -> RegistryUpdate<Option<PathBuf>> {
179        if path.as_os_str().is_empty() {
180            panic!("Registering empty path.");
181        }
182        use std::collections::btree_map::Entry;
183        match self.registry.paths.entry(uuid) {
184            Entry::Vacant(entry) => {
185                info!("Registered: {uuid} -> {path:?}");
186                self.changed = true;
187                entry.insert(path);
188                RegistryUpdate {
189                    changed: true,
190                    value: None,
191                }
192            }
193            Entry::Occupied(mut entry) => {
194                let changed = entry.get() != &path;
195                if changed {
196                    info!("Registry update: {uuid} -> {path:?}");
197                    self.changed = true;
198                }
199                let value = Some(entry.insert(path));
200                RegistryUpdate { changed, value }
201            }
202        }
203    }
204
205    /// Unregisters a resource path with the given UUID, and returns the former path for the Uuid, if it was registered.
206    pub fn unregister(&mut self, uuid: Uuid) -> Option<PathBuf> {
207        let former = self.registry.paths.remove(&uuid);
208        if former.is_some() {
209            info!("Registry remove UUID: {uuid} -> {former:?}");
210            self.changed = true;
211        }
212        former
213    }
214
215    /// Unregisters a resource path, and returns the UUID if the given path was previously registered.
216    pub fn unregister_path(&mut self, path: impl AsRef<Path>) -> Option<Uuid> {
217        let path = path.as_ref();
218        let uuid = self.registry.path_to_uuid(path)?;
219        info!("Registry remove path: {uuid} -> {path:?}");
220        self.changed = true;
221        self.registry.paths.remove(&uuid);
222        Some(uuid)
223    }
224
225    /// Completely replaces the internal storage.
226    pub fn set_container(&mut self, registry_container: RegistryContainer) {
227        if self.registry.paths == registry_container {
228            return;
229        }
230        // Log the new registrations.
231        let current = &self.registry.paths;
232        for (uuid, path) in &registry_container {
233            if current.get(uuid) != Some(path) {
234                info!("Resource {path:?} was registered with {uuid} UUID.");
235            }
236        }
237        self.changed = true;
238        self.registry.paths = registry_container;
239    }
240}
241
242impl Drop for ResourceRegistryRefMut<'_> {
243    fn drop(&mut self) {
244        if self.changed {
245            self.registry.save_sync();
246        }
247    }
248}
249
250/// Resource registry is responsible for UUID mapping of resource files. It maintains a map of
251/// `UUID -> Resource Path`.
252#[derive(Clone)]
253pub struct ResourceRegistry {
254    /// The path to which the resource registry is (or may) be saved.
255    path: PathBuf,
256    /// The UUIDs registered for each resource and the corresponding path of the resource.
257    /// These UUIDs should be read from the registry file or from the meta file associated with the resource.
258    /// They may also be randomly generated if a path is requested that does not have a meta file, in which
259    /// case the random UUID is dynamically added to the registry.
260    paths: RegistryContainer,
261    /// A shared flag that can be used to fetch the current status of a resource registry. This
262    /// supports the [`Future`] trait, which means that you can `.await` it in an async context to wait
263    /// until the registry is fully loaded (or failed to load). Any access to the registry in an async
264    /// context must be guarded with such `.await` call.
265    status: ResourceRegistryStatusFlag,
266    io: Arc<dyn ResourceIo>,
267    /// A list of folder that should be excluded when scanning the project folder for supported
268    /// resources. By default, it contains `./target` (a folder with build artifacts) and `./build`
269    /// (a folder with production builds) folders.
270    pub excluded_folders: FxHashSet<PathBuf>,
271}
272
273impl ResourceRegistry {
274    /// Default path of the registry. It can be overridden on a registry instance using
275    /// [`Self::set_path`] method.
276    pub const DEFAULT_PATH: &'static str = "data/resources.registry";
277
278    /// Creates a new resource registry with the given resource IO.
279    pub fn new(io: Arc<dyn ResourceIo>) -> Self {
280        let mut excluded_folders = FxHashSet::default();
281
282        // Exclude build artifacts folder by default.
283        excluded_folders.insert(PathBuf::from("target"));
284        // Exclude the standard production build folder as well.
285        excluded_folders.insert(PathBuf::from("build"));
286
287        Self {
288            path: PathBuf::from(Self::DEFAULT_PATH),
289            paths: Default::default(),
290            status: Default::default(),
291            io,
292            excluded_folders,
293        }
294    }
295
296    /// Returns a shared reference to the status flag. See [`ResourceRegistryStatusFlag`] docs for
297    /// more info.
298    pub fn status_flag(&self) -> ResourceRegistryStatusFlag {
299        self.status.clone()
300    }
301
302    /// Normalizes the path by resolving all `.` and `..` and removing any prefixes.
303    /// Absolute paths are converted to relative paths by removing the project root prefix, if possible.
304    /// Also replaces `\\` slashes to cross-platform `/` slashes.
305    ///
306    /// # Panics
307    ///
308    /// Panics if the given path is invalid, such as if it includes a directory that does not exist.
309    pub fn normalize_path(&self, path: impl AsRef<Path>) -> PathBuf {
310        self.io.canonicalize_path(path.as_ref()).unwrap()
311    }
312
313    /// Returns a reference to the actual container of the resource entries.
314    pub fn inner(&self) -> &RegistryContainer {
315        &self.paths
316    }
317
318    /// Sets a new path for the registry, but **does not** saves it.
319    pub fn set_path(&mut self, path: impl AsRef<Path>) {
320        self.path = path.as_ref().to_owned();
321    }
322
323    /// Returns a path to which the resource registry is (or may) be saved.
324    pub fn path(&self) -> &Path {
325        &self.path
326    }
327
328    /// Returns a directory to which the resource registry is (or may) be saved.
329    pub fn directory(&self) -> Option<&Path> {
330        self.path.parent()
331    }
332
333    /// Asynchronously saves the registry.
334    pub async fn save(&self) {
335        if self.io.can_write() {
336            match self.paths.save(&self.path, &*self.io).await {
337                Err(error) => {
338                    err!(
339                        "Unable to write the resource registry at the {:?} path! Reason: {:?}",
340                        self.path,
341                        error
342                    )
343                }
344                Ok(_) => {
345                    info!("The registry was successfully saved to {:?}!", self.path)
346                }
347            }
348        }
349    }
350
351    /// Returns `true` if the resource registry file exists, `false` - otherwise.
352    pub fn exists_sync(&self) -> bool {
353        self.io.exists_sync(&self.path)
354    }
355
356    /// Same as [`Self::save`], but synchronous.
357    pub fn save_sync(&self) {
358        #[cfg(not(target_arch = "wasm32"))]
359        if self.io.can_write() {
360            if let Some(folder) = self.path.parent() {
361                if !self.io.exists_sync(folder) {
362                    fyrox_core::log::Log::verify(self.io.create_dir_all_sync(folder));
363                }
364            }
365
366            match self.paths.save_sync(&self.path, &*self.io) {
367                Err(error) => {
368                    err!(
369                        "Unable to write the resource registry at the {} path! Reason: {:?}",
370                        self.path.display(),
371                        error
372                    )
373                }
374                Ok(_) => {
375                    info!(
376                        "The registry was successfully saved to {}!",
377                        self.path.display()
378                    )
379                }
380            }
381        }
382    }
383
384    /// Begins registry modification. See [`ResourceRegistryRefMut`] docs for more info.
385    pub fn modify(&mut self) -> ResourceRegistryRefMut<'_> {
386        ResourceRegistryRefMut {
387            changed: false,
388            registry: self,
389        }
390    }
391
392    /// Tries to get a path associated with the given resource UUID.
393    pub fn uuid_to_path(&self, uuid: Uuid) -> Option<&Path> {
394        self.paths.get(&uuid).map(|path| path.as_path())
395    }
396
397    /// Same as [`Self::uuid_to_path`], but returns [`PathBuf`] instead of `&Path`.
398    pub fn uuid_to_path_buf(&self, uuid: Uuid) -> Option<PathBuf> {
399        self.uuid_to_path(uuid).map(|path| path.to_path_buf())
400    }
401
402    /// Tries to find a UUID that corresponds for the given path.
403    pub fn path_to_uuid(&self, path: &Path) -> Option<Uuid> {
404        self.paths
405            .iter()
406            .find_map(|(k, v)| if v == path { Some(*k) } else { None })
407    }
408
409    /// Checks if the path is registered in the resource registry.
410    pub fn is_registered(&self, path: &Path) -> bool {
411        self.path_to_uuid(path).is_some()
412    }
413
414    /// Searches for supported resources starting from the given path and builds a mapping `UUID -> Path`.
415    /// If a supported resource does not have a metadata file besides it, this method will automatically
416    /// create the metadata file with a new UUID and add the resource to the registry.
417    ///
418    /// This method does **not** load any resource, instead it checks extension of every file in the
419    /// given directory, and if there's a loader for it, "remember" the resource.
420    pub async fn scan(
421        resource_io: Arc<dyn ResourceIo>,
422        loaders: Arc<Mutex<ResourceLoadersContainer>>,
423        root: impl AsRef<Path>,
424        excluded_folders: FxHashSet<PathBuf>,
425    ) -> RegistryContainer {
426        let registry_path = root.as_ref();
427        let registry_folder = registry_path
428            .parent()
429            .map(|path| path.to_path_buf())
430            .unwrap_or_else(|| PathBuf::from("."));
431
432        info!(
433            "Scanning {} folder for supported resources...",
434            registry_folder.display()
435        );
436
437        let mut container = RegistryContainer::default();
438
439        let mut paths_to_visit = ok_or_return!(
440            resource_io.read_directory(&registry_folder).await,
441            container
442        )
443        .collect::<Vec<_>>();
444
445        while let Some(fs_path) = paths_to_visit.pop() {
446            let path = match resource_io.canonicalize_path(&fs_path) {
447                Ok(path) => path,
448                Err(err) => {
449                    warn!(
450                        "Unable to make relative path from {fs_path:?} path! The resource won't be \
451                    included in the registry! Reason: {err:?}",
452                    );
453                    continue;
454                }
455            };
456
457            if excluded_folders.contains(&path) {
458                continue;
459            }
460
461            if resource_io.is_dir(&path).await {
462                // Continue iterating on subfolders.
463                if let Ok(iter) = resource_io.read_directory(&path).await {
464                    paths_to_visit.extend(iter);
465                }
466
467                continue;
468            }
469
470            if !loaders.safe_lock().is_supported_resource(&path) {
471                continue;
472            }
473
474            let metadata_path = append_extension(&path, ResourceMetadata::EXTENSION);
475            let metadata =
476                match ResourceMetadata::load_from_file_async(&metadata_path, &*resource_io).await {
477                    Ok(metadata) => metadata,
478                    Err(err) => {
479                        warn!(
480                            "Unable to load metadata for {path:?} resource. Reason: {err}.
481                            The metadata file will be added/recreated, do **NOT** delete it!
482                            Add it to the version control!",
483                        );
484                        let new_metadata = ResourceMetadata::new_with_random_id();
485                        if let Err(err) =
486                            new_metadata.save_async(&metadata_path, &*resource_io).await
487                        {
488                            warn!("Unable to save resource {path:?} metadata. Reason: {err}");
489                        }
490                        new_metadata
491                    }
492                };
493
494            if let Some(former) = container.insert(metadata.resource_id, path.clone()) {
495                warn!("Resource UUID collision between {path:?} and {former:?}");
496            }
497        }
498
499        container
500    }
501}