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}