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#[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 AssetSourceId<'static> {
76 #[inline]
78 pub fn as_static(self) -> Self {
79 match self {
80 Self::Default => Self::Default,
81 Self::Name(value) => Self::Name(value.as_static()),
82 }
83 }
84
85 #[inline]
87 pub fn from_static(value: impl Into<Self>) -> Self {
88 value.into().as_static()
89 }
90}
91
92impl<'a> From<&'a str> for AssetSourceId<'a> {
93 fn from(value: &'a str) -> Self {
94 AssetSourceId::Name(CowArc::Borrowed(value))
95 }
96}
97
98impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> {
99 fn from(value: &'a AssetSourceId<'b>) -> Self {
100 value.clone()
101 }
102}
103
104impl<'a> From<Option<&'a str>> for AssetSourceId<'a> {
105 fn from(value: Option<&'a str>) -> Self {
106 match value {
107 Some(value) => AssetSourceId::Name(CowArc::Borrowed(value)),
108 None => AssetSourceId::Default,
109 }
110 }
111}
112
113impl From<String> for AssetSourceId<'static> {
114 fn from(value: String) -> Self {
115 AssetSourceId::Name(value.into())
116 }
117}
118
119impl<'a> Hash for AssetSourceId<'a> {
120 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
121 self.as_str().hash(state);
122 }
123}
124
125impl<'a> PartialEq for AssetSourceId<'a> {
126 fn eq(&self, other: &Self) -> bool {
127 self.as_str().eq(&other.as_str())
128 }
129}
130
131#[derive(Default)]
134pub struct AssetSourceBuilder {
135 pub reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
137 pub writer: Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
139 pub watcher: Option<
141 Box<
142 dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
143 + Send
144 + Sync,
145 >,
146 >,
147 pub processed_reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
149 pub processed_writer:
151 Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
152 pub processed_watcher: Option<
154 Box<
155 dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
156 + Send
157 + Sync,
158 >,
159 >,
160 pub watch_warning: Option<&'static str>,
162 pub processed_watch_warning: Option<&'static str>,
164}
165
166impl AssetSourceBuilder {
167 pub fn build(
170 &mut self,
171 id: AssetSourceId<'static>,
172 watch: bool,
173 watch_processed: bool,
174 ) -> Option<AssetSource> {
175 let reader = self.reader.as_mut()?();
176 let writer = self.writer.as_mut().and_then(|w| w(false));
177 let processed_writer = self.processed_writer.as_mut().and_then(|w| w(true));
178 let mut source = AssetSource {
179 id: id.clone(),
180 reader,
181 writer,
182 processed_reader: self.processed_reader.as_mut().map(|r| r()),
183 processed_writer,
184 event_receiver: None,
185 watcher: None,
186 processed_event_receiver: None,
187 processed_watcher: None,
188 };
189
190 if watch {
191 let (sender, receiver) = crossbeam_channel::unbounded();
192 match self.watcher.as_mut().and_then(|w| w(sender)) {
193 Some(w) => {
194 source.watcher = Some(w);
195 source.event_receiver = Some(receiver);
196 }
197 None => {
198 if let Some(warning) = self.watch_warning {
199 warn!("{id} does not have an AssetWatcher configured. {warning}");
200 }
201 }
202 }
203 }
204
205 if watch_processed {
206 let (sender, receiver) = crossbeam_channel::unbounded();
207 match self.processed_watcher.as_mut().and_then(|w| w(sender)) {
208 Some(w) => {
209 source.processed_watcher = Some(w);
210 source.processed_event_receiver = Some(receiver);
211 }
212 None => {
213 if let Some(warning) = self.processed_watch_warning {
214 warn!("{id} does not have a processed AssetWatcher configured. {warning}");
215 }
216 }
217 }
218 }
219 Some(source)
220 }
221
222 pub fn with_reader(
224 mut self,
225 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
226 ) -> Self {
227 self.reader = Some(Box::new(reader));
228 self
229 }
230
231 pub fn with_writer(
233 mut self,
234 writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
235 ) -> Self {
236 self.writer = Some(Box::new(writer));
237 self
238 }
239
240 pub fn with_watcher(
242 mut self,
243 watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
244 + Send
245 + Sync
246 + 'static,
247 ) -> Self {
248 self.watcher = Some(Box::new(watcher));
249 self
250 }
251
252 pub fn with_processed_reader(
254 mut self,
255 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
256 ) -> Self {
257 self.processed_reader = Some(Box::new(reader));
258 self
259 }
260
261 pub fn with_processed_writer(
263 mut self,
264 writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
265 ) -> Self {
266 self.processed_writer = Some(Box::new(writer));
267 self
268 }
269
270 pub fn with_processed_watcher(
272 mut self,
273 watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
274 + Send
275 + Sync
276 + 'static,
277 ) -> Self {
278 self.processed_watcher = Some(Box::new(watcher));
279 self
280 }
281
282 pub fn with_watch_warning(mut self, warning: &'static str) -> Self {
284 self.watch_warning = Some(warning);
285 self
286 }
287
288 pub fn with_processed_watch_warning(mut self, warning: &'static str) -> Self {
290 self.processed_watch_warning = Some(warning);
291 self
292 }
293
294 pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self {
298 let default = Self::default()
299 .with_reader(AssetSource::get_default_reader(path.to_string()))
300 .with_writer(AssetSource::get_default_writer(path.to_string()))
301 .with_watcher(AssetSource::get_default_watcher(
302 path.to_string(),
303 Duration::from_millis(300),
304 ))
305 .with_watch_warning(AssetSource::get_default_watch_warning());
306 if let Some(processed_path) = processed_path {
307 default
308 .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string()))
309 .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string()))
310 .with_processed_watcher(AssetSource::get_default_watcher(
311 processed_path.to_string(),
312 Duration::from_millis(300),
313 ))
314 .with_processed_watch_warning(AssetSource::get_default_watch_warning())
315 } else {
316 default
317 }
318 }
319}
320
321#[derive(Resource, Default)]
324pub struct AssetSourceBuilders {
325 sources: HashMap<CowArc<'static, str>, AssetSourceBuilder>,
326 default: Option<AssetSourceBuilder>,
327}
328
329impl AssetSourceBuilders {
330 pub fn insert(&mut self, id: impl Into<AssetSourceId<'static>>, source: AssetSourceBuilder) {
332 match AssetSourceId::from_static(id) {
333 AssetSourceId::Default => {
334 self.default = Some(source);
335 }
336 AssetSourceId::Name(name) => {
337 self.sources.insert(name, source);
338 }
339 }
340 }
341
342 pub fn get_mut<'a, 'b>(
344 &'a mut self,
345 id: impl Into<AssetSourceId<'b>>,
346 ) -> Option<&'a mut AssetSourceBuilder> {
347 match id.into() {
348 AssetSourceId::Default => self.default.as_mut(),
349 AssetSourceId::Name(name) => self.sources.get_mut(&name.into_owned()),
350 }
351 }
352
353 pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources {
356 let mut sources = <HashMap<_, _>>::default();
357 for (id, source) in &mut self.sources {
358 if let Some(data) = source.build(
359 AssetSourceId::Name(id.clone_owned()),
360 watch,
361 watch_processed,
362 ) {
363 sources.insert(id.clone_owned(), data);
364 }
365 }
366
367 AssetSources {
368 sources,
369 default: self
370 .default
371 .as_mut()
372 .and_then(|p| p.build(AssetSourceId::Default, watch, watch_processed))
373 .expect(MISSING_DEFAULT_SOURCE),
374 }
375 }
376
377 pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) {
379 self.default
380 .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path));
381 }
382}
383
384pub struct AssetSource {
387 id: AssetSourceId<'static>,
388 reader: Box<dyn ErasedAssetReader>,
389 writer: Option<Box<dyn ErasedAssetWriter>>,
390 processed_reader: Option<Box<dyn ErasedAssetReader>>,
391 processed_writer: Option<Box<dyn ErasedAssetWriter>>,
392 watcher: Option<Box<dyn AssetWatcher>>,
393 processed_watcher: Option<Box<dyn AssetWatcher>>,
394 event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
395 processed_event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
396}
397
398impl AssetSource {
399 pub fn build() -> AssetSourceBuilder {
401 AssetSourceBuilder::default()
402 }
403
404 #[inline]
406 pub fn id(&self) -> AssetSourceId<'static> {
407 self.id.clone()
408 }
409
410 #[inline]
412 pub fn reader(&self) -> &dyn ErasedAssetReader {
413 &*self.reader
414 }
415
416 #[inline]
418 pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> {
419 self.writer
420 .as_deref()
421 .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned()))
422 }
423
424 #[inline]
426 pub fn processed_reader(
427 &self,
428 ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> {
429 self.processed_reader
430 .as_deref()
431 .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned()))
432 }
433
434 #[inline]
436 pub fn processed_writer(
437 &self,
438 ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> {
439 self.processed_writer
440 .as_deref()
441 .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned()))
442 }
443
444 #[inline]
446 pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
447 self.event_receiver.as_ref()
448 }
449
450 #[inline]
452 pub fn processed_event_receiver(
453 &self,
454 ) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
455 self.processed_event_receiver.as_ref()
456 }
457
458 #[inline]
460 pub fn should_process(&self) -> bool {
461 self.processed_writer.is_some()
462 }
463
464 pub fn get_default_reader(
467 _path: String,
468 ) -> impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync {
469 move || {
470 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
471 return Box::new(super::file::FileAssetReader::new(&_path));
472 #[cfg(target_arch = "wasm32")]
473 return Box::new(super::wasm::HttpWasmAssetReader::new(&_path));
474 #[cfg(target_os = "android")]
475 return Box::new(super::android::AndroidAssetReader);
476 }
477 }
478
479 pub fn get_default_writer(
482 _path: String,
483 ) -> impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync {
484 move |_create_root: bool| {
485 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
486 return Some(Box::new(super::file::FileAssetWriter::new(
487 &_path,
488 _create_root,
489 )));
490 #[cfg(any(target_arch = "wasm32", target_os = "android"))]
491 return None;
492 }
493 }
494
495 pub fn get_default_watch_warning() -> &'static str {
497 #[cfg(target_arch = "wasm32")]
498 return "Web does not currently support watching assets.";
499 #[cfg(target_os = "android")]
500 return "Android does not currently support watching assets.";
501 #[cfg(all(
502 not(target_arch = "wasm32"),
503 not(target_os = "android"),
504 not(feature = "file_watcher")
505 ))]
506 return "Consider enabling the `file_watcher` feature.";
507 #[cfg(all(
508 not(target_arch = "wasm32"),
509 not(target_os = "android"),
510 feature = "file_watcher"
511 ))]
512 return "Consider adding an \"assets\" directory.";
513 }
514
515 #[cfg_attr(
521 any(
522 not(feature = "file_watcher"),
523 target_arch = "wasm32",
524 target_os = "android"
525 ),
526 expect(
527 unused_variables,
528 reason = "The `path` and `file_debounce_wait_time` arguments are unused when on WASM, Android, or if the `file_watcher` feature is disabled."
529 )
530 )]
531 pub fn get_default_watcher(
532 path: String,
533 file_debounce_wait_time: Duration,
534 ) -> impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
535 + Send
536 + Sync {
537 move |sender: crossbeam_channel::Sender<AssetSourceEvent>| {
538 #[cfg(all(
539 feature = "file_watcher",
540 not(target_arch = "wasm32"),
541 not(target_os = "android")
542 ))]
543 {
544 let path = super::file::get_base_path().join(path.clone());
545 if path.exists() {
546 Some(Box::new(
547 super::file::FileWatcher::new(
548 path.clone(),
549 sender,
550 file_debounce_wait_time,
551 )
552 .unwrap_or_else(|e| {
553 panic!("Failed to create file watcher from path {path:?}, {e:?}")
554 }),
555 ))
556 } else {
557 warn!("Skip creating file watcher because path {path:?} does not exist.");
558 None
559 }
560 }
561 #[cfg(any(
562 not(feature = "file_watcher"),
563 target_arch = "wasm32",
564 target_os = "android"
565 ))]
566 return None;
567 }
568 }
569
570 pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
573 if let Some(reader) = self.processed_reader.take() {
574 self.processed_reader = Some(Box::new(ProcessorGatedReader::new(
575 self.id(),
576 reader,
577 processor_data,
578 )));
579 }
580 }
581}
582
583pub struct AssetSources {
585 sources: HashMap<CowArc<'static, str>, AssetSource>,
586 default: AssetSource,
587}
588
589impl AssetSources {
590 pub fn get<'a, 'b>(
592 &'a self,
593 id: impl Into<AssetSourceId<'b>>,
594 ) -> Result<&'a AssetSource, MissingAssetSourceError> {
595 match id.into().into_owned() {
596 AssetSourceId::Default => Ok(&self.default),
597 AssetSourceId::Name(name) => self
598 .sources
599 .get(&name)
600 .ok_or(MissingAssetSourceError(AssetSourceId::Name(name))),
601 }
602 }
603
604 pub fn iter(&self) -> impl Iterator<Item = &AssetSource> {
606 self.sources.values().chain(Some(&self.default))
607 }
608
609 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
611 self.sources.values_mut().chain(Some(&mut self.default))
612 }
613
614 pub fn iter_processed(&self) -> impl Iterator<Item = &AssetSource> {
616 self.iter().filter(|p| p.should_process())
617 }
618
619 pub fn iter_processed_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
621 self.iter_mut().filter(|p| p.should_process())
622 }
623
624 pub fn ids(&self) -> impl Iterator<Item = AssetSourceId<'static>> + '_ {
626 self.sources
627 .keys()
628 .map(|k| AssetSourceId::Name(k.clone_owned()))
629 .chain(Some(AssetSourceId::Default))
630 }
631
632 pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
635 for source in self.iter_processed_mut() {
636 source.gate_on_processor(processor_data.clone());
637 }
638 }
639}
640
641#[derive(Error, Debug, Clone, PartialEq, Eq)]
643#[error("Asset Source '{0}' does not exist")]
644pub struct MissingAssetSourceError(AssetSourceId<'static>);
645
646#[derive(Error, Debug, Clone)]
648#[error("Asset Source '{0}' does not have an AssetWriter.")]
649pub struct MissingAssetWriterError(AssetSourceId<'static>);
650
651#[derive(Error, Debug, Clone, PartialEq, Eq)]
653#[error("Asset Source '{0}' does not have a processed AssetReader.")]
654pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>);
655
656#[derive(Error, Debug, Clone)]
658#[error("Asset Source '{0}' does not have a processed AssetWriter.")]
659pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>);
660
661const MISSING_DEFAULT_SOURCE: &str =
662 "A default AssetSource is required. Add one to `AssetSourceBuilders`";