Skip to main content

shuyi_assets/
hot.rs

1//! Values that reload (ADR-0071). A [`Hot`] value is an asset decoded
2//! through the registry by id, which remembers every file its decode asked
3//! for: the file itself, the files its format read through [`DecodeCx`]
4//! (an atlas's picture, a level's tileset) and its import settings, found
5//! or not. When the platform says files changed, [`Hot::refresh`] decodes
6//! it again if one of them did. If the new decode fails, the old value
7//! stays and the errors come back ([`Reload::Failed`]), so a broken file
8//! never takes a running game down.
9//!
10//! A `Hot` reads through to its value:
11//!
12//! ```
13//! use std::collections::BTreeMap;
14//! use std::sync::Arc;
15//! use shuyi_assets::bundle::{Bundle, Layer};
16//! use shuyi_assets::format::Formats;
17//! use shuyi_assets::hot::{Hot, Reload};
18//! use shuyi_assets::palette::PaletteDef;
19//!
20//! let palette = |ink: &str| format!("{{ \"ink\": \"{ink}\" }}").into_bytes();
21//! let files = BTreeMap::from([("palette.ron".to_owned(), palette("#000000"))]);
22//! let layer = Layer::owned("disk".to_owned(), files).unwrap();
23//! let bundle = Bundle::new(vec![layer]);
24//! let formats = Arc::new(Formats::standard());
25//! let mut colors = Hot::<PaletteDef>::load(&formats, &bundle, "palette.ron").unwrap();
26//!
27//! assert_eq!(colors.refresh(&["other.ron".to_owned()]), Reload::Unchanged);
28//! assert!(bundle.replace("palette.ron", palette("#ffffff")));
29//! assert_eq!(colors.refresh(&["palette.ron".to_owned()]), Reload::Reloaded);
30//! assert_eq!(colors.get("ink"), Some([255, 255, 255]));
31//! ```
32//!
33//! [`DecodeCx`]: crate::format::DecodeCx
34
35use crate::error::ContentError;
36use crate::prelude::*;
37
38/// What a [`Hot::refresh`] did.
39#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum Reload {
42    /// None of its files changed.
43    Unchanged,
44    /// A file changed, and the value is the new decode's.
45    Reloaded,
46    /// A file changed and the new decode failed: every problem, with its
47    /// file and line. The value is still the old one.
48    Failed(Vec<ContentError>),
49}
50
51#[cfg(feature = "std")]
52pub use with_std::Hot;
53
54#[cfg(feature = "std")]
55mod with_std {
56    use alloc::collections::BTreeSet;
57    use alloc::sync::Arc;
58    use core::fmt;
59    use core::ops::Deref;
60
61    use super::Reload;
62    use crate::bundle::{Bundle, reads_of};
63    use crate::error::ContentError;
64    use crate::format::{AssetKind, Formats};
65    use crate::prelude::*;
66
67    /// An asset of kind `T` that decodes again when a file it read changes
68    /// (the [module docs](super)).
69    pub struct Hot<T: AssetKind> {
70        value: T,
71        /// What it was asked for by.
72        id: String,
73        formats: Arc<Formats>,
74        bundle: Bundle,
75        /// The files its decode asked for, found or not: one that isn't
76        /// there yet (a `.meta` file) reloads it when it appears. After a
77        /// failed decode, also those of the decode before.
78        reads: BTreeSet<String>,
79    }
80
81    impl<T: AssetKind> Hot<T> {
82        /// The asset `id` of `bundle`, read with `formats` as
83        /// [`Formats::load`] reads it.
84        ///
85        /// # Errors
86        ///
87        /// Every problem [`Formats::load`] found.
88        pub fn load(
89            formats: &Arc<Formats>,
90            bundle: &Bundle,
91            id: &str,
92        ) -> Result<Self, Vec<ContentError>> {
93            let (value, reads) = reads_of(|| formats.load::<T>(bundle, id));
94            Ok(Self {
95                value: value?,
96                id: id.to_owned(),
97                formats: Arc::clone(formats),
98                bundle: bundle.clone(),
99                reads: reads.asked,
100            })
101        }
102
103        /// What it was asked for by.
104        pub fn id(&self) -> &str {
105            &self.id
106        }
107
108        /// The files its decode asked for, sorted: a change to any of them
109        /// reloads it.
110        pub fn reads(&self) -> impl Iterator<Item = &str> {
111            self.reads.iter().map(String::as_str)
112        }
113
114        /// Decodes it again if a file it read is among `changed` (paths in
115        /// the bundle, as [`Bundle::bytes`] takes them). A value that fails
116        /// keeps the old one, and tries again the next time one of its
117        /// files changes.
118        pub fn refresh(&mut self, changed: &[String]) -> Reload {
119            if !changed.iter().any(|path| self.reads.contains(path)) {
120                return Reload::Unchanged;
121            }
122            let (decoded, reads) = reads_of(|| self.formats.load::<T>(&self.bundle, &self.id));
123            match decoded {
124                Ok(value) => {
125                    self.value = value;
126                    self.reads = reads.asked;
127                    Reload::Reloaded
128                }
129                Err(errors) => {
130                    // A file the broken one names may be what is missing.
131                    self.reads.extend(reads.asked);
132                    Reload::Failed(errors)
133                }
134            }
135        }
136    }
137
138    impl<T: AssetKind> Deref for Hot<T> {
139        type Target = T;
140
141        fn deref(&self) -> &T {
142            &self.value
143        }
144    }
145
146    impl<T: AssetKind + fmt::Debug> fmt::Debug for Hot<T> {
147        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148            f.debug_struct("Hot")
149                .field("id", &self.id)
150                .field("value", &self.value)
151                .field("reads", &self.reads)
152                .finish_non_exhaustive()
153        }
154    }
155}