bevy_asset/io/
source.rs

1use crate::{
2    io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher},
3    processor::AssetProcessorData,
4};
5use bevy_ecs::system::Resource;
6use bevy_utils::tracing::{error, warn};
7use bevy_utils::{CowArc, Duration, HashMap};
8use std::{fmt::Display, hash::Hash, sync::Arc};
9use thiserror::Error;
10
11use super::{ErasedAssetReader, ErasedAssetWriter};
12
13// Needed for doc strings.
14#[allow(unused_imports)]
15use crate::io::{AssetReader, AssetWriter};
16
17/// A reference to an "asset source", which maps to an [`AssetReader`] and/or [`AssetWriter`].
18///
19/// * [`AssetSourceId::Default`] corresponds to "default asset paths" that don't specify a source: `/path/to/asset.png`
20/// * [`AssetSourceId::Name`] corresponds to asset paths that _do_ specify a source: `remote://path/to/asset.png`, where `remote` is the name.
21#[derive(Default, Clone, Debug, Eq)]
22pub enum AssetSourceId<'a> {
23    /// The default asset source.
24    #[default]
25    Default,
26    /// A non-default named asset source.
27    Name(CowArc<'a, str>),
28}
29
30impl<'a> Display for AssetSourceId<'a> {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self.as_str() {
33            None => write!(f, "AssetSourceId::Default"),
34            Some(v) => write!(f, "AssetSourceId::Name({v})"),
35        }
36    }
37}
38
39impl<'a> AssetSourceId<'a> {
40    /// Creates a new [`AssetSourceId`]
41    pub fn new(source: Option<impl Into<CowArc<'a, str>>>) -> AssetSourceId<'a> {
42        match source {
43            Some(source) => AssetSourceId::Name(source.into()),
44            None => AssetSourceId::Default,
45        }
46    }
47
48    /// Returns [`None`] if this is [`AssetSourceId::Default`] and [`Some`] containing the
49    /// name if this is [`AssetSourceId::Name`].
50    pub fn as_str(&self) -> Option<&str> {
51        match self {
52            AssetSourceId::Default => None,
53            AssetSourceId::Name(v) => Some(v),
54        }
55    }
56
57    /// If this is not already an owned / static id, create one. Otherwise, it will return itself (with a static lifetime).
58    pub fn into_owned(self) -> AssetSourceId<'static> {
59        match self {
60            AssetSourceId::Default => AssetSourceId::Default,
61            AssetSourceId::Name(v) => AssetSourceId::Name(v.into_owned()),
62        }
63    }
64
65    /// Clones into an owned [`AssetSourceId<'static>`].
66    /// This is equivalent to `.clone().into_owned()`.
67    #[inline]
68    pub fn clone_owned(&self) -> AssetSourceId<'static> {
69        self.clone().into_owned()
70    }
71}
72
73impl From<&'static str> for AssetSourceId<'static> {
74    fn from(value: &'static str) -> Self {
75        AssetSourceId::Name(value.into())
76    }
77}
78
79impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> {
80    fn from(value: &'a AssetSourceId<'b>) -> Self {
81        value.clone()
82    }
83}
84
85impl From<Option<&'static str>> for AssetSourceId<'static> {
86    fn from(value: Option<&'static str>) -> Self {
87        match value {
88            Some(value) => AssetSourceId::Name(value.into()),
89            None => AssetSourceId::Default,
90        }
91    }
92}
93
94impl From<String> for AssetSourceId<'static> {
95    fn from(value: String) -> Self {
96        AssetSourceId::Name(value.into())
97    }
98}
99
100impl<'a> Hash for AssetSourceId<'a> {
101    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
102        self.as_str().hash(state);
103    }
104}
105
106impl<'a> PartialEq for AssetSourceId<'a> {
107    fn eq(&self, other: &Self) -> bool {
108        self.as_str().eq(&other.as_str())
109    }
110}
111
112/// Metadata about an "asset source", such as how to construct the [`AssetReader`] and [`AssetWriter`] for the source,
113/// and whether or not the source is processed.
114#[derive(Default)]
115pub struct AssetSourceBuilder {
116    pub reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
117    pub writer: Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
118    pub watcher: Option<
119        Box<
120            dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
121                + Send
122                + Sync,
123        >,
124    >,
125    pub processed_reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
126    pub processed_writer:
127        Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
128    pub processed_watcher: Option<
129        Box<
130            dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
131                + Send
132                + Sync,
133        >,
134    >,
135    pub watch_warning: Option<&'static str>,
136    pub processed_watch_warning: Option<&'static str>,
137}
138
139impl AssetSourceBuilder {
140    /// Builds a new [`AssetSource`] with the given `id`. If `watch` is true, the unprocessed source will watch for changes.
141    /// If `watch_processed` is true, the processed source will watch for changes.
142    pub fn build(
143        &mut self,
144        id: AssetSourceId<'static>,
145        watch: bool,
146        watch_processed: bool,
147    ) -> Option<AssetSource> {
148        let reader = self.reader.as_mut()?();
149        let writer = self.writer.as_mut().and_then(|w| w(false));
150        let processed_writer = self.processed_writer.as_mut().and_then(|w| w(true));
151        let mut source = AssetSource {
152            id: id.clone(),
153            reader,
154            writer,
155            processed_reader: self.processed_reader.as_mut().map(|r| r()),
156            processed_writer,
157            event_receiver: None,
158            watcher: None,
159            processed_event_receiver: None,
160            processed_watcher: None,
161        };
162
163        if watch {
164            let (sender, receiver) = crossbeam_channel::unbounded();
165            match self.watcher.as_mut().and_then(|w| w(sender)) {
166                Some(w) => {
167                    source.watcher = Some(w);
168                    source.event_receiver = Some(receiver);
169                }
170                None => {
171                    if let Some(warning) = self.watch_warning {
172                        warn!("{id} does not have an AssetWatcher configured. {warning}");
173                    }
174                }
175            }
176        }
177
178        if watch_processed {
179            let (sender, receiver) = crossbeam_channel::unbounded();
180            match self.processed_watcher.as_mut().and_then(|w| w(sender)) {
181                Some(w) => {
182                    source.processed_watcher = Some(w);
183                    source.processed_event_receiver = Some(receiver);
184                }
185                None => {
186                    if let Some(warning) = self.processed_watch_warning {
187                        warn!("{id} does not have a processed AssetWatcher configured. {warning}");
188                    }
189                }
190            }
191        }
192        Some(source)
193    }
194
195    /// Will use the given `reader` function to construct unprocessed [`AssetReader`] instances.
196    pub fn with_reader(
197        mut self,
198        reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
199    ) -> Self {
200        self.reader = Some(Box::new(reader));
201        self
202    }
203
204    /// Will use the given `writer` function to construct unprocessed [`AssetWriter`] instances.
205    pub fn with_writer(
206        mut self,
207        writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
208    ) -> Self {
209        self.writer = Some(Box::new(writer));
210        self
211    }
212
213    /// Will use the given `watcher` function to construct unprocessed [`AssetWatcher`] instances.
214    pub fn with_watcher(
215        mut self,
216        watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
217            + Send
218            + Sync
219            + 'static,
220    ) -> Self {
221        self.watcher = Some(Box::new(watcher));
222        self
223    }
224
225    /// Will use the given `reader` function to construct processed [`AssetReader`] instances.
226    pub fn with_processed_reader(
227        mut self,
228        reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
229    ) -> Self {
230        self.processed_reader = Some(Box::new(reader));
231        self
232    }
233
234    /// Will use the given `writer` function to construct processed [`AssetWriter`] instances.
235    pub fn with_processed_writer(
236        mut self,
237        writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
238    ) -> Self {
239        self.processed_writer = Some(Box::new(writer));
240        self
241    }
242
243    /// Will use the given `watcher` function to construct processed [`AssetWatcher`] instances.
244    pub fn with_processed_watcher(
245        mut self,
246        watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
247            + Send
248            + Sync
249            + 'static,
250    ) -> Self {
251        self.processed_watcher = Some(Box::new(watcher));
252        self
253    }
254
255    /// Enables a warning for the unprocessed source watcher, which will print when watching is enabled and the unprocessed source doesn't have a watcher.
256    pub fn with_watch_warning(mut self, warning: &'static str) -> Self {
257        self.watch_warning = Some(warning);
258        self
259    }
260
261    /// Enables a warning for the processed source watcher, which will print when watching is enabled and the processed source doesn't have a watcher.
262    pub fn with_processed_watch_warning(mut self, warning: &'static str) -> Self {
263        self.processed_watch_warning = Some(warning);
264        self
265    }
266
267    /// Returns a builder containing the "platform default source" for the given `path` and `processed_path`.
268    /// For most platforms, this will use [`FileAssetReader`](crate::io::file::FileAssetReader) / [`FileAssetWriter`](crate::io::file::FileAssetWriter),
269    /// but some platforms (such as Android) have their own default readers / writers / watchers.
270    pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self {
271        let default = Self::default()
272            .with_reader(AssetSource::get_default_reader(path.to_string()))
273            .with_writer(AssetSource::get_default_writer(path.to_string()))
274            .with_watcher(AssetSource::get_default_watcher(
275                path.to_string(),
276                Duration::from_millis(300),
277            ))
278            .with_watch_warning(AssetSource::get_default_watch_warning());
279        if let Some(processed_path) = processed_path {
280            default
281                .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string()))
282                .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string()))
283                .with_processed_watcher(AssetSource::get_default_watcher(
284                    processed_path.to_string(),
285                    Duration::from_millis(300),
286                ))
287                .with_processed_watch_warning(AssetSource::get_default_watch_warning())
288        } else {
289            default
290        }
291    }
292}
293
294/// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`] and [`AssetWriter`] instances
295/// for a given asset source.
296#[derive(Resource, Default)]
297pub struct AssetSourceBuilders {
298    sources: HashMap<CowArc<'static, str>, AssetSourceBuilder>,
299    default: Option<AssetSourceBuilder>,
300}
301
302impl AssetSourceBuilders {
303    /// Inserts a new builder with the given `id`
304    pub fn insert(&mut self, id: impl Into<AssetSourceId<'static>>, source: AssetSourceBuilder) {
305        match id.into() {
306            AssetSourceId::Default => {
307                self.default = Some(source);
308            }
309            AssetSourceId::Name(name) => {
310                self.sources.insert(name, source);
311            }
312        }
313    }
314
315    /// Gets a mutable builder with the given `id`, if it exists.
316    pub fn get_mut<'a, 'b>(
317        &'a mut self,
318        id: impl Into<AssetSourceId<'b>>,
319    ) -> Option<&'a mut AssetSourceBuilder> {
320        match id.into() {
321            AssetSourceId::Default => self.default.as_mut(),
322            AssetSourceId::Name(name) => self.sources.get_mut(&name.into_owned()),
323        }
324    }
325
326    /// Builds a new [`AssetSources`] collection. If `watch` is true, the unprocessed sources will watch for changes.
327    /// If `watch_processed` is true, the processed sources will watch for changes.
328    pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources {
329        let mut sources = HashMap::new();
330        for (id, source) in &mut self.sources {
331            if let Some(data) = source.build(
332                AssetSourceId::Name(id.clone_owned()),
333                watch,
334                watch_processed,
335            ) {
336                sources.insert(id.clone_owned(), data);
337            }
338        }
339
340        AssetSources {
341            sources,
342            default: self
343                .default
344                .as_mut()
345                .and_then(|p| p.build(AssetSourceId::Default, watch, watch_processed))
346                .expect(MISSING_DEFAULT_SOURCE),
347        }
348    }
349
350    /// Initializes the default [`AssetSourceBuilder`] if it has not already been set.
351    pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) {
352        self.default
353            .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path));
354    }
355}
356
357/// A collection of unprocessed and processed [`AssetReader`], [`AssetWriter`], and [`AssetWatcher`] instances
358/// for a specific asset source, identified by an [`AssetSourceId`].
359pub struct AssetSource {
360    id: AssetSourceId<'static>,
361    reader: Box<dyn ErasedAssetReader>,
362    writer: Option<Box<dyn ErasedAssetWriter>>,
363    processed_reader: Option<Box<dyn ErasedAssetReader>>,
364    processed_writer: Option<Box<dyn ErasedAssetWriter>>,
365    watcher: Option<Box<dyn AssetWatcher>>,
366    processed_watcher: Option<Box<dyn AssetWatcher>>,
367    event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
368    processed_event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
369}
370
371impl AssetSource {
372    /// Starts building a new [`AssetSource`].
373    pub fn build() -> AssetSourceBuilder {
374        AssetSourceBuilder::default()
375    }
376
377    /// Returns this source's id.
378    #[inline]
379    pub fn id(&self) -> AssetSourceId<'static> {
380        self.id.clone()
381    }
382
383    /// Return's this source's unprocessed [`AssetReader`].
384    #[inline]
385    pub fn reader(&self) -> &dyn ErasedAssetReader {
386        &*self.reader
387    }
388
389    /// Return's this source's unprocessed [`AssetWriter`], if it exists.
390    #[inline]
391    pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> {
392        self.writer
393            .as_deref()
394            .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned()))
395    }
396
397    /// Return's this source's processed [`AssetReader`], if it exists.
398    #[inline]
399    pub fn processed_reader(
400        &self,
401    ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> {
402        self.processed_reader
403            .as_deref()
404            .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned()))
405    }
406
407    /// Return's this source's processed [`AssetWriter`], if it exists.
408    #[inline]
409    pub fn processed_writer(
410        &self,
411    ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> {
412        self.processed_writer
413            .as_deref()
414            .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned()))
415    }
416
417    /// Return's this source's unprocessed event receiver, if the source is currently watching for changes.
418    #[inline]
419    pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
420        self.event_receiver.as_ref()
421    }
422
423    /// Return's this source's processed event receiver, if the source is currently watching for changes.
424    #[inline]
425    pub fn processed_event_receiver(
426        &self,
427    ) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
428        self.processed_event_receiver.as_ref()
429    }
430
431    /// Returns true if the assets in this source should be processed.
432    #[inline]
433    pub fn should_process(&self) -> bool {
434        self.processed_writer.is_some()
435    }
436
437    /// Returns a builder function for this platform's default [`AssetReader`]. `path` is the relative path to
438    /// the asset root.
439    pub fn get_default_reader(
440        _path: String,
441    ) -> impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync {
442        move || {
443            #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
444            return Box::new(super::file::FileAssetReader::new(&_path));
445            #[cfg(target_arch = "wasm32")]
446            return Box::new(super::wasm::HttpWasmAssetReader::new(&_path));
447            #[cfg(target_os = "android")]
448            return Box::new(super::android::AndroidAssetReader);
449        }
450    }
451
452    /// Returns a builder function for this platform's default [`AssetWriter`]. `path` is the relative path to
453    /// the asset root. This will return [`None`] if this platform does not support writing assets by default.
454    pub fn get_default_writer(
455        _path: String,
456    ) -> impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync {
457        move |_create_root: bool| {
458            #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
459            return Some(Box::new(super::file::FileAssetWriter::new(
460                &_path,
461                _create_root,
462            )));
463            #[cfg(any(target_arch = "wasm32", target_os = "android"))]
464            return None;
465        }
466    }
467
468    /// Returns the default non-existent [`AssetWatcher`] warning for the current platform.
469    pub fn get_default_watch_warning() -> &'static str {
470        #[cfg(target_arch = "wasm32")]
471        return "Web does not currently support watching assets.";
472        #[cfg(target_os = "android")]
473        return "Android does not currently support watching assets.";
474        #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
475        return "Consider enabling the `file_watcher` feature.";
476    }
477
478    /// Returns a builder function for this platform's default [`AssetWatcher`]. `path` is the relative path to
479    /// the asset root. This will return [`None`] if this platform does not support watching assets by default.
480    /// `file_debounce_time` is the amount of time to wait (and debounce duplicate events) before returning an event.
481    /// Higher durations reduce duplicates but increase the amount of time before a change event is processed. If the
482    /// duration is set too low, some systems might surface events _before_ their filesystem has the changes.
483    #[allow(unused)]
484    pub fn get_default_watcher(
485        path: String,
486        file_debounce_wait_time: Duration,
487    ) -> impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
488           + Send
489           + Sync {
490        move |sender: crossbeam_channel::Sender<AssetSourceEvent>| {
491            #[cfg(all(
492                feature = "file_watcher",
493                not(target_arch = "wasm32"),
494                not(target_os = "android")
495            ))]
496            return Some(Box::new(
497                super::file::FileWatcher::new(
498                    std::path::PathBuf::from(path.clone()),
499                    sender,
500                    file_debounce_wait_time,
501                )
502                .unwrap_or_else(|e| {
503                    panic!("Failed to create file watcher from path {path:?}, {e:?}")
504                }),
505            ));
506            #[cfg(any(
507                not(feature = "file_watcher"),
508                target_arch = "wasm32",
509                target_os = "android"
510            ))]
511            return None;
512        }
513    }
514
515    /// This will cause processed [`AssetReader`] futures (such as [`AssetReader::read`]) to wait until
516    /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset.
517    pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
518        if let Some(reader) = self.processed_reader.take() {
519            self.processed_reader = Some(Box::new(ProcessorGatedReader::new(
520                self.id(),
521                reader,
522                processor_data,
523            )));
524        }
525    }
526}
527
528/// A collection of [`AssetSource`]s.
529pub struct AssetSources {
530    sources: HashMap<CowArc<'static, str>, AssetSource>,
531    default: AssetSource,
532}
533
534impl AssetSources {
535    /// Gets the [`AssetSource`] with the given `id`, if it exists.
536    pub fn get<'a, 'b>(
537        &'a self,
538        id: impl Into<AssetSourceId<'b>>,
539    ) -> Result<&'a AssetSource, MissingAssetSourceError> {
540        match id.into().into_owned() {
541            AssetSourceId::Default => Ok(&self.default),
542            AssetSourceId::Name(name) => self
543                .sources
544                .get(&name)
545                .ok_or_else(|| MissingAssetSourceError(AssetSourceId::Name(name))),
546        }
547    }
548
549    /// Iterates all asset sources in the collection (including the default source).
550    pub fn iter(&self) -> impl Iterator<Item = &AssetSource> {
551        self.sources.values().chain(Some(&self.default))
552    }
553
554    /// Mutably iterates all asset sources in the collection (including the default source).
555    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
556        self.sources.values_mut().chain(Some(&mut self.default))
557    }
558
559    /// Iterates all processed asset sources in the collection (including the default source).
560    pub fn iter_processed(&self) -> impl Iterator<Item = &AssetSource> {
561        self.iter().filter(|p| p.should_process())
562    }
563
564    /// Mutably iterates all processed asset sources in the collection (including the default source).
565    pub fn iter_processed_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
566        self.iter_mut().filter(|p| p.should_process())
567    }
568
569    /// Iterates over the [`AssetSourceId`] of every [`AssetSource`] in the collection (including the default source).
570    pub fn ids(&self) -> impl Iterator<Item = AssetSourceId<'static>> + '_ {
571        self.sources
572            .keys()
573            .map(|k| AssetSourceId::Name(k.clone_owned()))
574            .chain(Some(AssetSourceId::Default))
575    }
576
577    /// This will cause processed [`AssetReader`] futures (such as [`AssetReader::read`]) to wait until
578    /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset.
579    pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
580        for source in self.iter_processed_mut() {
581            source.gate_on_processor(processor_data.clone());
582        }
583    }
584}
585
586/// An error returned when an [`AssetSource`] does not exist for a given id.
587#[derive(Error, Debug, Clone, PartialEq, Eq)]
588#[error("Asset Source '{0}' does not exist")]
589pub struct MissingAssetSourceError(AssetSourceId<'static>);
590
591/// An error returned when an [`AssetWriter`] does not exist for a given id.
592#[derive(Error, Debug, Clone)]
593#[error("Asset Source '{0}' does not have an AssetWriter.")]
594pub struct MissingAssetWriterError(AssetSourceId<'static>);
595
596/// An error returned when a processed [`AssetReader`] does not exist for a given id.
597#[derive(Error, Debug, Clone, PartialEq, Eq)]
598#[error("Asset Source '{0}' does not have a processed AssetReader.")]
599pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>);
600
601/// An error returned when a processed [`AssetWriter`] does not exist for a given id.
602#[derive(Error, Debug, Clone)]
603#[error("Asset Source '{0}' does not have a processed AssetWriter.")]
604pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>);
605
606const MISSING_DEFAULT_SOURCE: &str =
607    "A default AssetSource is required. Add one to `AssetSourceBuilders`";