1use crate::{
2 io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher},
3 processor::AssetProcessorData,
4};
5use alloc::sync::Arc;
6use atomicow::CowArc;
7use bevy_ecs::system::Resource;
8use bevy_utils::{
9 tracing::{error, warn},
10 Duration, HashMap,
11};
12use core::{fmt::Display, hash::Hash};
13use derive_more::derive::{Display, Error};
14
15use super::{ErasedAssetReader, ErasedAssetWriter};
16
17#[derive(Default, Clone, Debug, Eq)]
22pub enum AssetSourceId<'a> {
23 #[default]
25 Default,
26 Name(CowArc<'a, str>),
28}
29
30impl<'a> Display for AssetSourceId<'a> {
31 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::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 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 pub fn as_str(&self) -> Option<&str> {
51 match self {
52 AssetSourceId::Default => None,
53 AssetSourceId::Name(v) => Some(v),
54 }
55 }
56
57 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 #[inline]
68 pub fn clone_owned(&self) -> AssetSourceId<'static> {
69 self.clone().into_owned()
70 }
71}
72
73impl AssetSourceId<'static> {
74 #[inline]
76 pub fn as_static(self) -> Self {
77 match self {
78 Self::Default => Self::Default,
79 Self::Name(value) => Self::Name(value.as_static()),
80 }
81 }
82
83 #[inline]
85 pub fn from_static(value: impl Into<Self>) -> Self {
86 value.into().as_static()
87 }
88}
89
90impl<'a> From<&'a str> for AssetSourceId<'a> {
91 fn from(value: &'a str) -> Self {
92 AssetSourceId::Name(CowArc::Borrowed(value))
93 }
94}
95
96impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> {
97 fn from(value: &'a AssetSourceId<'b>) -> Self {
98 value.clone()
99 }
100}
101
102impl<'a> From<Option<&'a str>> for AssetSourceId<'a> {
103 fn from(value: Option<&'a str>) -> Self {
104 match value {
105 Some(value) => AssetSourceId::Name(CowArc::Borrowed(value)),
106 None => AssetSourceId::Default,
107 }
108 }
109}
110
111impl From<String> for AssetSourceId<'static> {
112 fn from(value: String) -> Self {
113 AssetSourceId::Name(value.into())
114 }
115}
116
117impl<'a> Hash for AssetSourceId<'a> {
118 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
119 self.as_str().hash(state);
120 }
121}
122
123impl<'a> PartialEq for AssetSourceId<'a> {
124 fn eq(&self, other: &Self) -> bool {
125 self.as_str().eq(&other.as_str())
126 }
127}
128
129#[derive(Default)]
132pub struct AssetSourceBuilder {
133 pub reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
134 pub writer: Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
135 pub watcher: Option<
136 Box<
137 dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
138 + Send
139 + Sync,
140 >,
141 >,
142 pub processed_reader: Option<Box<dyn FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync>>,
143 pub processed_writer:
144 Option<Box<dyn FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync>>,
145 pub processed_watcher: Option<
146 Box<
147 dyn FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
148 + Send
149 + Sync,
150 >,
151 >,
152 pub watch_warning: Option<&'static str>,
153 pub processed_watch_warning: Option<&'static str>,
154}
155
156impl AssetSourceBuilder {
157 pub fn build(
160 &mut self,
161 id: AssetSourceId<'static>,
162 watch: bool,
163 watch_processed: bool,
164 ) -> Option<AssetSource> {
165 let reader = self.reader.as_mut()?();
166 let writer = self.writer.as_mut().and_then(|w| w(false));
167 let processed_writer = self.processed_writer.as_mut().and_then(|w| w(true));
168 let mut source = AssetSource {
169 id: id.clone(),
170 reader,
171 writer,
172 processed_reader: self.processed_reader.as_mut().map(|r| r()),
173 processed_writer,
174 event_receiver: None,
175 watcher: None,
176 processed_event_receiver: None,
177 processed_watcher: None,
178 };
179
180 if watch {
181 let (sender, receiver) = crossbeam_channel::unbounded();
182 match self.watcher.as_mut().and_then(|w| w(sender)) {
183 Some(w) => {
184 source.watcher = Some(w);
185 source.event_receiver = Some(receiver);
186 }
187 None => {
188 if let Some(warning) = self.watch_warning {
189 warn!("{id} does not have an AssetWatcher configured. {warning}");
190 }
191 }
192 }
193 }
194
195 if watch_processed {
196 let (sender, receiver) = crossbeam_channel::unbounded();
197 match self.processed_watcher.as_mut().and_then(|w| w(sender)) {
198 Some(w) => {
199 source.processed_watcher = Some(w);
200 source.processed_event_receiver = Some(receiver);
201 }
202 None => {
203 if let Some(warning) = self.processed_watch_warning {
204 warn!("{id} does not have a processed AssetWatcher configured. {warning}");
205 }
206 }
207 }
208 }
209 Some(source)
210 }
211
212 pub fn with_reader(
214 mut self,
215 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
216 ) -> Self {
217 self.reader = Some(Box::new(reader));
218 self
219 }
220
221 pub fn with_writer(
223 mut self,
224 writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
225 ) -> Self {
226 self.writer = Some(Box::new(writer));
227 self
228 }
229
230 pub fn with_watcher(
232 mut self,
233 watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
234 + Send
235 + Sync
236 + 'static,
237 ) -> Self {
238 self.watcher = Some(Box::new(watcher));
239 self
240 }
241
242 pub fn with_processed_reader(
244 mut self,
245 reader: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
246 ) -> Self {
247 self.processed_reader = Some(Box::new(reader));
248 self
249 }
250
251 pub fn with_processed_writer(
253 mut self,
254 writer: impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync + 'static,
255 ) -> Self {
256 self.processed_writer = Some(Box::new(writer));
257 self
258 }
259
260 pub fn with_processed_watcher(
262 mut self,
263 watcher: impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
264 + Send
265 + Sync
266 + 'static,
267 ) -> Self {
268 self.processed_watcher = Some(Box::new(watcher));
269 self
270 }
271
272 pub fn with_watch_warning(mut self, warning: &'static str) -> Self {
274 self.watch_warning = Some(warning);
275 self
276 }
277
278 pub fn with_processed_watch_warning(mut self, warning: &'static str) -> Self {
280 self.processed_watch_warning = Some(warning);
281 self
282 }
283
284 pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self {
288 let default = Self::default()
289 .with_reader(AssetSource::get_default_reader(path.to_string()))
290 .with_writer(AssetSource::get_default_writer(path.to_string()))
291 .with_watcher(AssetSource::get_default_watcher(
292 path.to_string(),
293 Duration::from_millis(300),
294 ))
295 .with_watch_warning(AssetSource::get_default_watch_warning());
296 if let Some(processed_path) = processed_path {
297 default
298 .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string()))
299 .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string()))
300 .with_processed_watcher(AssetSource::get_default_watcher(
301 processed_path.to_string(),
302 Duration::from_millis(300),
303 ))
304 .with_processed_watch_warning(AssetSource::get_default_watch_warning())
305 } else {
306 default
307 }
308 }
309}
310
311#[derive(Resource, Default)]
314pub struct AssetSourceBuilders {
315 sources: HashMap<CowArc<'static, str>, AssetSourceBuilder>,
316 default: Option<AssetSourceBuilder>,
317}
318
319impl AssetSourceBuilders {
320 pub fn insert(&mut self, id: impl Into<AssetSourceId<'static>>, source: AssetSourceBuilder) {
322 match AssetSourceId::from_static(id) {
323 AssetSourceId::Default => {
324 self.default = Some(source);
325 }
326 AssetSourceId::Name(name) => {
327 self.sources.insert(name, source);
328 }
329 }
330 }
331
332 pub fn get_mut<'a, 'b>(
334 &'a mut self,
335 id: impl Into<AssetSourceId<'b>>,
336 ) -> Option<&'a mut AssetSourceBuilder> {
337 match id.into() {
338 AssetSourceId::Default => self.default.as_mut(),
339 AssetSourceId::Name(name) => self.sources.get_mut(&name.into_owned()),
340 }
341 }
342
343 pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources {
346 let mut sources = HashMap::new();
347 for (id, source) in &mut self.sources {
348 if let Some(data) = source.build(
349 AssetSourceId::Name(id.clone_owned()),
350 watch,
351 watch_processed,
352 ) {
353 sources.insert(id.clone_owned(), data);
354 }
355 }
356
357 AssetSources {
358 sources,
359 default: self
360 .default
361 .as_mut()
362 .and_then(|p| p.build(AssetSourceId::Default, watch, watch_processed))
363 .expect(MISSING_DEFAULT_SOURCE),
364 }
365 }
366
367 pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) {
369 self.default
370 .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path));
371 }
372}
373
374pub struct AssetSource {
377 id: AssetSourceId<'static>,
378 reader: Box<dyn ErasedAssetReader>,
379 writer: Option<Box<dyn ErasedAssetWriter>>,
380 processed_reader: Option<Box<dyn ErasedAssetReader>>,
381 processed_writer: Option<Box<dyn ErasedAssetWriter>>,
382 watcher: Option<Box<dyn AssetWatcher>>,
383 processed_watcher: Option<Box<dyn AssetWatcher>>,
384 event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
385 processed_event_receiver: Option<crossbeam_channel::Receiver<AssetSourceEvent>>,
386}
387
388impl AssetSource {
389 pub fn build() -> AssetSourceBuilder {
391 AssetSourceBuilder::default()
392 }
393
394 #[inline]
396 pub fn id(&self) -> AssetSourceId<'static> {
397 self.id.clone()
398 }
399
400 #[inline]
402 pub fn reader(&self) -> &dyn ErasedAssetReader {
403 &*self.reader
404 }
405
406 #[inline]
408 pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> {
409 self.writer
410 .as_deref()
411 .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned()))
412 }
413
414 #[inline]
416 pub fn processed_reader(
417 &self,
418 ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> {
419 self.processed_reader
420 .as_deref()
421 .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned()))
422 }
423
424 #[inline]
426 pub fn processed_writer(
427 &self,
428 ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> {
429 self.processed_writer
430 .as_deref()
431 .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned()))
432 }
433
434 #[inline]
436 pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
437 self.event_receiver.as_ref()
438 }
439
440 #[inline]
442 pub fn processed_event_receiver(
443 &self,
444 ) -> Option<&crossbeam_channel::Receiver<AssetSourceEvent>> {
445 self.processed_event_receiver.as_ref()
446 }
447
448 #[inline]
450 pub fn should_process(&self) -> bool {
451 self.processed_writer.is_some()
452 }
453
454 pub fn get_default_reader(
457 _path: String,
458 ) -> impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync {
459 move || {
460 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
461 return Box::new(super::file::FileAssetReader::new(&_path));
462 #[cfg(target_arch = "wasm32")]
463 return Box::new(super::wasm::HttpWasmAssetReader::new(&_path));
464 #[cfg(target_os = "android")]
465 return Box::new(super::android::AndroidAssetReader);
466 }
467 }
468
469 pub fn get_default_writer(
472 _path: String,
473 ) -> impl FnMut(bool) -> Option<Box<dyn ErasedAssetWriter>> + Send + Sync {
474 move |_create_root: bool| {
475 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
476 return Some(Box::new(super::file::FileAssetWriter::new(
477 &_path,
478 _create_root,
479 )));
480 #[cfg(any(target_arch = "wasm32", target_os = "android"))]
481 return None;
482 }
483 }
484
485 pub fn get_default_watch_warning() -> &'static str {
487 #[cfg(target_arch = "wasm32")]
488 return "Web does not currently support watching assets.";
489 #[cfg(target_os = "android")]
490 return "Android does not currently support watching assets.";
491 #[cfg(all(
492 not(target_arch = "wasm32"),
493 not(target_os = "android"),
494 not(feature = "file_watcher")
495 ))]
496 return "Consider enabling the `file_watcher` feature.";
497 #[cfg(all(
498 not(target_arch = "wasm32"),
499 not(target_os = "android"),
500 feature = "file_watcher"
501 ))]
502 return "Consider adding an \"assets\" directory.";
503 }
504
505 #[cfg_attr(
511 any(
512 not(feature = "file_watcher"),
513 target_arch = "wasm32",
514 target_os = "android"
515 ),
516 expect(
517 unused_variables,
518 reason = "The `path` and `file_debounce_wait_time` arguments are unused when on WASM, Android, or if the `file_watcher` feature is disabled."
519 )
520 )]
521 pub fn get_default_watcher(
522 path: String,
523 file_debounce_wait_time: Duration,
524 ) -> impl FnMut(crossbeam_channel::Sender<AssetSourceEvent>) -> Option<Box<dyn AssetWatcher>>
525 + Send
526 + Sync {
527 move |sender: crossbeam_channel::Sender<AssetSourceEvent>| {
528 #[cfg(all(
529 feature = "file_watcher",
530 not(target_arch = "wasm32"),
531 not(target_os = "android")
532 ))]
533 {
534 let path = super::file::get_base_path().join(path.clone());
535 if path.exists() {
536 Some(Box::new(
537 super::file::FileWatcher::new(
538 path.clone(),
539 sender,
540 file_debounce_wait_time,
541 )
542 .unwrap_or_else(|e| {
543 panic!("Failed to create file watcher from path {path:?}, {e:?}")
544 }),
545 ))
546 } else {
547 warn!("Skip creating file watcher because path {path:?} does not exist.");
548 None
549 }
550 }
551 #[cfg(any(
552 not(feature = "file_watcher"),
553 target_arch = "wasm32",
554 target_os = "android"
555 ))]
556 return None;
557 }
558 }
559
560 pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
563 if let Some(reader) = self.processed_reader.take() {
564 self.processed_reader = Some(Box::new(ProcessorGatedReader::new(
565 self.id(),
566 reader,
567 processor_data,
568 )));
569 }
570 }
571}
572
573pub struct AssetSources {
575 sources: HashMap<CowArc<'static, str>, AssetSource>,
576 default: AssetSource,
577}
578
579impl AssetSources {
580 pub fn get<'a, 'b>(
582 &'a self,
583 id: impl Into<AssetSourceId<'b>>,
584 ) -> Result<&'a AssetSource, MissingAssetSourceError> {
585 match id.into().into_owned() {
586 AssetSourceId::Default => Ok(&self.default),
587 AssetSourceId::Name(name) => self
588 .sources
589 .get(&name)
590 .ok_or(MissingAssetSourceError(AssetSourceId::Name(name))),
591 }
592 }
593
594 pub fn iter(&self) -> impl Iterator<Item = &AssetSource> {
596 self.sources.values().chain(Some(&self.default))
597 }
598
599 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
601 self.sources.values_mut().chain(Some(&mut self.default))
602 }
603
604 pub fn iter_processed(&self) -> impl Iterator<Item = &AssetSource> {
606 self.iter().filter(|p| p.should_process())
607 }
608
609 pub fn iter_processed_mut(&mut self) -> impl Iterator<Item = &mut AssetSource> {
611 self.iter_mut().filter(|p| p.should_process())
612 }
613
614 pub fn ids(&self) -> impl Iterator<Item = AssetSourceId<'static>> + '_ {
616 self.sources
617 .keys()
618 .map(|k| AssetSourceId::Name(k.clone_owned()))
619 .chain(Some(AssetSourceId::Default))
620 }
621
622 pub fn gate_on_processor(&mut self, processor_data: Arc<AssetProcessorData>) {
625 for source in self.iter_processed_mut() {
626 source.gate_on_processor(processor_data.clone());
627 }
628 }
629}
630
631#[derive(Error, Display, Debug, Clone, PartialEq, Eq)]
633#[display("Asset Source '{_0}' does not exist")]
634#[error(ignore)]
635pub struct MissingAssetSourceError(AssetSourceId<'static>);
636
637#[derive(Error, Display, Debug, Clone)]
639#[display("Asset Source '{_0}' does not have an AssetWriter.")]
640#[error(ignore)]
641pub struct MissingAssetWriterError(AssetSourceId<'static>);
642
643#[derive(Error, Display, Debug, Clone, PartialEq, Eq)]
645#[display("Asset Source '{_0}' does not have a processed AssetReader.")]
646#[error(ignore)]
647pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>);
648
649#[derive(Error, Display, Debug, Clone)]
651#[display("Asset Source '{_0}' does not have a processed AssetWriter.")]
652#[error(ignore)]
653pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>);
654
655const MISSING_DEFAULT_SOURCE: &str =
656 "A default AssetSource is required. Add one to `AssetSourceBuilders`";