bevy_asset/io/
source.rs

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