1use crate::{
2 io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher},
3 processor::ProcessingState,
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::warn;
16
17use super::{ErasedAssetReader, ErasedAssetWriter};
18
19#[derive(Default, Clone, Debug, Eq)]
24pub enum AssetSourceId<'a> {
25 #[default]
27 Default,
28 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 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 pub fn as_str(&self) -> Option<&str> {
53 match self {
54 AssetSourceId::Default => None,
55 AssetSourceId::Name(v) => Some(v),
56 }
57 }
58
59 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 #[inline]
70 pub fn clone_owned(&self) -> AssetSourceId<'static> {
71 self.clone().into_owned()
72 }
73}
74
75impl 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
117pub struct AssetSourceBuilder {
120 pub reader: Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>,
122 pub writer: Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
124 pub watcher: Option<
126 Box<
127 dyn FnMut(async_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
128 + Send
129 + Sync,
130 >,
131 >,
132 pub processed_reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
134 pub processed_writer:
136 Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
137 pub processed_watcher: Option<
139 Box<
140 dyn FnMut(async_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
141 + Send
142 + Sync,
143 >,
144 >,
145 pub watch_warning: Option<&'static str>,
147 pub processed_watch_warning: Option<&'static str>,
149}
150
151impl AssetSourceBuilder {
152 pub fn new(
154 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
155 ) -> AssetSourceBuilder {
156 Self {
157 reader: Box::new(reader),
158 writer: None,
159 watcher: None,
160 processed_reader: None,
161 processed_writer: None,
162 processed_watcher: None,
163 watch_warning: None,
164 processed_watch_warning: None,
165 }
166 }
167
168 pub fn build(
171 &mut self,
172 id: AssetSourceId<'static>,
173 watch: bool,
174 watch_processed: bool,
175 ) -> AssetSource {
176 let reader = self.reader.as_mut()();
177 let writer = self.writer.as_mut().and_then(|w| w(false));
178 let processed_writer = self.processed_writer.as_mut().and_then(|w| w(true));
179 let mut source = AssetSource {
180 id: id.clone(),
181 reader,
182 writer,
183 processed_reader: self
184 .processed_reader
185 .as_mut()
186 .map(|r| r())
187 .map(Into::<Arc<_>>::into),
188 ungated_processed_reader: None,
189 processed_writer,
190 event_receiver: None,
191 watcher: None,
192 processed_event_receiver: None,
193 processed_watcher: None,
194 };
195
196 if watch {
197 let (sender, receiver) = async_channel::unbounded();
198 match self.watcher.as_mut().and_then(|w| w(sender)) {
199 Some(w) => {
200 source.watcher = Some(w);
201 source.event_receiver = Some(receiver);
202 }
203 None => {
204 if let Some(warning) = self.watch_warning {
205 warn!("{id} does not have an AssetWatcher configured. {warning}");
206 }
207 }
208 }
209 }
210
211 if watch_processed {
212 let (sender, receiver) = async_channel::unbounded();
213 match self.processed_watcher.as_mut().and_then(|w| w(sender)) {
214 Some(w) => {
215 source.processed_watcher = Some(w);
216 source.processed_event_receiver = Some(receiver);
217 }
218 None => {
219 if let Some(warning) = self.processed_watch_warning {
220 warn!("{id} does not have a processed AssetWatcher configured. {warning}");
221 }
222 }
223 }
224 }
225 source
226 }
227
228 pub fn with_reader(
230 mut self,
231 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
232 ) -> Self {
233 self.reader = Box::new(reader);
234 self
235 }
236
237 pub fn with_writer(
239 mut self,
240 writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
241 ) -> Self {
242 self.writer = Some(Box::new(writer));
243 self
244 }
245
246 pub fn with_watcher(
248 mut self,
249 watcher: impl FnMut(async_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
250 + Send
251 + Sync
252 + 'static,
253 ) -> Self {
254 self.watcher = Some(Box::new(watcher));
255 self
256 }
257
258 pub fn with_processed_reader(
260 mut self,
261 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
262 ) -> Self {
263 self.processed_reader = Some(Box::new(reader));
264 self
265 }
266
267 pub fn with_processed_writer(
269 mut self,
270 writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
271 ) -> Self {
272 self.processed_writer = Some(Box::new(writer));
273 self
274 }
275
276 pub fn with_processed_watcher(
278 mut self,
279 watcher: impl FnMut(async_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
280 + Send
281 + Sync
282 + 'static,
283 ) -> Self {
284 self.processed_watcher = Some(Box::new(watcher));
285 self
286 }
287
288 pub fn with_watch_warning(mut self, warning: &'static str) -> Self {
290 self.watch_warning = Some(warning);
291 self
292 }
293
294 pub fn with_processed_watch_warning(mut self, warning: &'static str) -> Self {
296 self.processed_watch_warning = Some(warning);
297 self
298 }
299
300 pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self {
304 let default = Self::new(AssetSource::get_default_reader(path.to_string()))
305 .with_writer(AssetSource::get_default_writer(path.to_string()))
306 .with_watcher(AssetSource::get_default_watcher(
307 path.to_string(),
308 Duration::from_millis(300),
309 ))
310 .with_watch_warning(AssetSource::get_default_watch_warning());
311 if let Some(processed_path) = processed_path {
312 default
313 .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string()))
314 .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string()))
315 .with_processed_watcher(AssetSource::get_default_watcher(
316 processed_path.to_string(),
317 Duration::from_millis(300),
318 ))
319 .with_processed_watch_warning(AssetSource::get_default_watch_warning())
320 } else {
321 default
322 }
323 }
324}
325
326#[derive(Resource, Default)]
329pub struct AssetSourceBuilders {
330 sources: HashMap<CowArc<'static, str>, AssetSourceBuilder>,
331 default: Option<AssetSourceBuilder>,
332}
333
334impl AssetSourceBuilders {
335 pub fn insert(&mut self, id: impl Into<AssetSourceId<'static>>, source: AssetSourceBuilder) {
337 match id.into() {
338 AssetSourceId::Default => {
339 self.default = Some(source);
340 }
341 AssetSourceId::Name(name) => {
342 self.sources.insert(name, source);
343 }
344 }
345 }
346
347 pub fn get_mut<'a, 'b>(
349 &'a mut self,
350 id: impl Into<AssetSourceId<'b>>,
351 ) -> Option<&'a mut AssetSourceBuilder> {
352 match id.into() {
353 AssetSourceId::Default => self.default.as_mut(),
354 AssetSourceId::Name(name) => self.sources.get_mut(&name.into_owned()),
355 }
356 }
357
358 pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources {
361 let mut sources = <HashMap<_, _>>::default();
362 for (id, source) in &mut self.sources {
363 let source = source.build(
364 AssetSourceId::Name(id.clone_owned()),
365 watch,
366 watch_processed,
367 );
368 sources.insert(id.clone_owned(), source);
369 }
370
371 AssetSources {
372 sources,
373 default: self
374 .default
375 .as_mut()
376 .map(|p| p.build(AssetSourceId::Default, watch, watch_processed))
377 .expect(MISSING_DEFAULT_SOURCE),
378 }
379 }
380
381 pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) {
383 self.default
384 .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path));
385 }
386}
387
388pub struct AssetSource {
391 id: AssetSourceId<'static>,
392 reader: Box<dyn ErasedAssetReader>,
393 writer: Option<Box<dyn ErasedAssetWriter>>,
394 processed_reader: Option<Arc<dyn ErasedAssetReader>>,
395 ungated_processed_reader: Option<Arc<dyn ErasedAssetReader>>,
400 processed_writer: Option<Box<dyn ErasedAssetWriter>>,
401 watcher: Option<Box<dyn AssetWatcher>>,
402 processed_watcher: Option<Box<dyn AssetWatcher>>,
403 event_receiver: Option<async_channel::Receiver<AssetSourceEvent>>,
404 processed_event_receiver: Option<async_channel::Receiver<AssetSourceEvent>>,
405}
406
407impl AssetSource {
408 #[inline]
410 pub fn id(&self) -> AssetSourceId<'static> {
411 self.id.clone()
412 }
413
414 #[inline]
416 pub fn reader(&self) -> &dyn ErasedAssetReader {
417 &*self.reader
418 }
419
420 #[inline]
422 pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> {
423 self.writer
424 .as_deref()
425 .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned()))
426 }
427
428 #[inline]
430 pub fn processed_reader(
431 &self,
432 ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> {
433 self.processed_reader
434 .as_deref()
435 .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned()))
436 }
437
438 #[inline]
441 pub(crate) fn ungated_processed_reader(&self) -> Option<&dyn ErasedAssetReader> {
442 self.ungated_processed_reader.as_deref()
443 }
444
445 #[inline]
447 pub fn processed_writer(
448 &self,
449 ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> {
450 self.processed_writer
451 .as_deref()
452 .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned()))
453 }
454
455 #[inline]
457 pub fn event_receiver(&self) -> Option<&async_channel::Receiver<AssetSourceEvent>> {
458 self.event_receiver.as_ref()
459 }
460
461 #[inline]
463 pub fn processed_event_receiver(&self) -> Option<&async_channel::Receiver<AssetSourceEvent>> {
464 self.processed_event_receiver.as_ref()
465 }
466
467 #[inline]
469 pub fn should_process(&self) -> bool {
470 self.processed_writer.is_some()
471 }
472
473 pub fn get_default_reader(
476 _path: String,
477 ) -> impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync {
478 move || {
479 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
480 return Box::new(super::file::FileAssetReader::new(&_path));
481 #[cfg(target_arch = "wasm32")]
482 return Box::new(super::wasm::HttpWasmAssetReader::new(&_path));
483 #[cfg(target_os = "android")]
484 return Box::new(super::android::AndroidAssetReader);
485 }
486 }
487
488 pub fn get_default_writer(
491 _path: String,
492 ) -> impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync {
493 move |_create_root: bool| {
494 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
495 return Some(Box::new(super::file::FileAssetWriter::new(
496 &_path,
497 _create_root,
498 )));
499 #[cfg(any(target_arch = "wasm32", target_os = "android"))]
500 return None;
501 }
502 }
503
504 pub fn get_default_watch_warning() -> &'static str {
506 #[cfg(target_arch = "wasm32")]
507 return "Web does not currently support watching assets.";
508 #[cfg(target_os = "android")]
509 return "Android does not currently support watching assets.";
510 #[cfg(all(
511 not(target_arch = "wasm32"),
512 not(target_os = "android"),
513 not(feature = "file_watcher")
514 ))]
515 return "Consider enabling the `file_watcher` feature.";
516 #[cfg(all(
517 not(target_arch = "wasm32"),
518 not(target_os = "android"),
519 feature = "file_watcher"
520 ))]
521 return "Consider adding an \"assets\" directory.";
522 }
523
524 #[cfg_attr(
530 any(
531 not(feature = "file_watcher"),
532 target_arch = "wasm32",
533 target_os = "android"
534 ),
535 expect(
536 unused_variables,
537 reason = "The `path` and `file_debounce_wait_time` arguments are unused when on WASM, Android, or if the `file_watcher` feature is disabled."
538 )
539 )]
540 pub fn get_default_watcher(
541 path: String,
542 file_debounce_wait_time: Duration,
543 ) -> impl FnMut(async_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>> + Send + Sync
544 {
545 move |sender: async_channel::Sender<AssetSourceEvent>| {
546 #[cfg(all(
547 feature = "file_watcher",
548 not(target_arch = "wasm32"),
549 not(target_os = "android")
550 ))]
551 {
552 let path = super::file::get_base_path().join(path.clone());
553 if path.exists() {
554 Some(Box::new(
555 super::file::FileWatcher::new(
556 path.clone(),
557 sender,
558 file_debounce_wait_time,
559 )
560 .unwrap_or_else(|e| {
561 panic!("Failed to create file watcher from path {path:?}, {e:?}")
562 }),
563 ))
564 } else {
565 warn!("Skip creating file watcher because path {path:?} does not exist.");
566 None
567 }
568 }
569 #[cfg(any(
570 not(feature = "file_watcher"),
571 target_arch = "wasm32",
572 target_os = "android"
573 ))]
574 return None;
575 }
576 }
577
578 pub(crate) fn gate_on_processor(&mut self, processing_state: Arc<ProcessingState>) {
581 if let Some(reader) = self.processed_reader.take() {
582 self.ungated_processed_reader = Some(reader.clone());
583 self.processed_reader = Some(Arc::new(ProcessorGatedReader::new(
584 self.id(),
585 reader,
586 processing_state,
587 )));
588 }
589 }
590}
591
592pub struct AssetSources {
594 sources: HashMap<CowArc<'static, str>, AssetSource>,
595 default: AssetSource,
596}
597
598impl AssetSources {
599 pub fn get<'a, 'b>(
601 &'a self,
602 id: impl Into<AssetSourceId<'b>>,
603 ) -> Result<&'a AssetSource, MissingAssetSourceError> {
604 match id.into().into_owned() {
605 AssetSourceId::Default => Ok(&self.default),
606 AssetSourceId::Name(name) => self
607 .sources
608 .get(&name)
609 .ok_or(MissingAssetSourceError(AssetSourceId::Name(name))),
610 }
611 }
612
613 pub fn iter(&self) -> impl Iterator<Item = &AssetSource> {
615 self.sources.values().chain(Some(&self.default))
616 }
617
618 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
620 self.sources.values_mut().chain(Some(&mut self.default))
621 }
622
623 pub fn iter_processed(&self) -> impl Iterator<Item = &AssetSource> {
625 self.iter().filter(|p| p.should_process())
626 }
627
628 pub fn iter_processed_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
630 self.iter_mut().filter(|p| p.should_process())
631 }
632
633 pub fn ids(&self) -> impl Iterator<Item = AssetSourceId<'static>> + '_ {
635 self.sources
636 .keys()
637 .map(|k| AssetSourceId::Name(k.clone_owned()))
638 .chain(Some(AssetSourceId::Default))
639 }
640
641 pub(crate) fn gate_on_processor(&mut self, processing_state: Arc<ProcessingState>) {
644 for source in self.iter_processed_mut() {
645 source.gate_on_processor(processing_state.clone());
646 }
647 }
648}
649
650#[derive(Error, Debug, Clone, PartialEq, Eq)]
652#[error("Asset Source '{0}' does not exist")]
653pub struct MissingAssetSourceError(AssetSourceId<'static>);
654
655#[derive(Error, Debug, Clone)]
657#[error("Asset Source '{0}' does not have an AssetWriter.")]
658pub struct MissingAssetWriterError(AssetSourceId<'static>);
659
660#[derive(Error, Debug, Clone, PartialEq, Eq)]
662#[error("Asset Source '{0}' does not have a processed AssetReader.")]
663pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>);
664
665#[derive(Error, Debug, Clone)]
667#[error("Asset Source '{0}' does not have a processed AssetWriter.")]
668pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>);
669
670const MISSING_DEFAULT_SOURCE: &str =
671 "A default AssetSource is required. Add one to `AssetSourceBuilders`";