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 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#[derive(Default)]
120pub struct AssetSourceBuilder {
121 pub reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
123 pub writer: Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
125 pub watcher: Option<
127 Box<
128 dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
129 + Send
130 + Sync,
131 >,
132 >,
133 pub processed_reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
135 pub processed_writer:
137 Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
138 pub processed_watcher: Option<
140 Box<
141 dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
142 + Send
143 + Sync,
144 >,
145 >,
146 pub watch_warning: Option<&'static str>,
148 pub processed_watch_warning: Option<&'static str>,
150}
151
152impl AssetSourceBuilder {
153 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 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 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 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 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 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 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 pub fn with_watch_warning(mut self, warning: &'static str) -> Self {
270 self.watch_warning = Some(warning);
271 self
272 }
273
274 pub fn with_processed_watch_warning(mut self, warning: &'static str) -> Self {
276 self.processed_watch_warning = Some(warning);
277 self
278 }
279
280 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#[derive(Resource, Default)]
310pub struct AssetSourceBuilders {
311 sources: HashMap<CowArc<'static, str>, AssetSourceBuilder>,
312 default: Option<AssetSourceBuilder>,
313}
314
315impl AssetSourceBuilders {
316 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 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 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 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
370pub 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 pub fn build() -> AssetSourceBuilder {
387 AssetSourceBuilder::default()
388 }
389
390 #[inline]
392 pub fn id(&self) -> AssetSourceId<'static> {
393 self.id.clone()
394 }
395
396 #[inline]
398 pub fn reader(&self) -> &dyn ErasedAssetReader {
399 &*self.reader
400 }
401
402 #[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 #[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 #[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 #[inline]
432 pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
433 self.event_receiver.as_ref()
434 }
435
436 #[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 #[inline]
446 pub fn should_process(&self) -> bool {
447 self.processed_writer.is_some()
448 }
449
450 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 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 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 #[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 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
569pub struct AssetSources {
571 sources: HashMap<CowArc<'static, str>, AssetSource>,
572 default: AssetSource,
573}
574
575impl AssetSources {
576 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 pub fn iter(&self) -> impl Iterator<Item = &AssetSource> {
592 self.sources.values().chain(Some(&self.default))
593 }
594
595 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
597 self.sources.values_mut().chain(Some(&mut self.default))
598 }
599
600 pub fn iter_processed(&self) -> impl Iterator<Item = &AssetSource> {
602 self.iter().filter(|p| p.should_process())
603 }
604
605 pub fn iter_processed_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
607 self.iter_mut().filter(|p| p.should_process())
608 }
609
610 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 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#[derive(Error, Debug, Clone, PartialEq, Eq)]
629#[error("Asset Source '{0}' does not exist")]
630pub struct MissingAssetSourceError(AssetSourceId<'static>);
631
632#[derive(Error, Debug, Clone)]
634#[error("Asset Source '{0}' does not have an AssetWriter.")]
635pub struct MissingAssetWriterError(AssetSourceId<'static>);
636
637#[derive(Error, Debug, Clone, PartialEq, Eq)]
639#[error("Asset Source '{0}' does not have a processed AssetReader.")]
640pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>);
641
642#[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`";