Skip to main content

bevy_asset/processor/
process.rs

1use crate::{
2    io::{
3        AssetReaderError, AssetWriterError, MissingAssetWriterError,
4        MissingProcessedAssetReaderError, MissingProcessedAssetWriterError, Reader, Writer,
5    },
6    meta::{AssetAction, AssetMeta, AssetMetaDyn, ProcessDependencyInfo, ProcessedInfo, Settings},
7    processor::AssetProcessor,
8    saver::{AssetSaver, SavedAsset},
9    transformer::{AssetTransformer, IdentityAssetTransformer, TransformedAsset},
10    AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset,
11    MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError,
12};
13use alloc::{
14    borrow::ToOwned,
15    boxed::Box,
16    string::{String, ToString},
17    vec::Vec,
18};
19use bevy_ecs::error::BevyError;
20use bevy_reflect::TypePath;
21use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
22use core::marker::PhantomData;
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25
26/// Asset "processor" logic that reads input asset bytes (stored on [`ProcessContext`]), processes the value in some way,
27/// and then writes the final processed bytes with [`Writer`]. The resulting bytes must be loadable with the given [`Process::OutputLoader`].
28///
29/// This is a "low level", maximally flexible interface. Most use cases are better served by the [`LoadTransformAndSave`] implementation
30/// of [`Process`].
31pub trait Process: TypePath + Send + Sync + Sized + 'static {
32    /// The configuration / settings used to process the asset. This will be stored in the [`AssetMeta`] and is user-configurable per-asset.
33    type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
34    /// The [`AssetLoader`] that will be used to load the final processed asset.
35    type OutputLoader: AssetLoader;
36    /// Processes the asset stored on `context` in some way using the settings stored on `meta`. The results are written to `writer`. The
37    /// final written processed asset is loadable using [`Process::OutputLoader`]. This load will use the returned [`AssetLoader::Settings`].
38    fn process(
39        &self,
40        context: &mut ProcessContext,
41        settings: &Self::Settings,
42        writer: &mut Writer,
43    ) -> impl ConditionalSendFuture<
44        Output = Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError>,
45    >;
46}
47
48/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then transforms
49/// the `L` asset into an `S` [`AssetSaver`] asset using the `T` [`AssetTransformer`], and lastly saves the asset using the `S` [`AssetSaver`].
50///
51/// When creating custom processors, it is generally recommended to use the [`LoadTransformAndSave`] [`Process`] implementation,
52/// as it encourages you to separate your code into an [`AssetLoader`] capable of loading assets without processing enabled,
53/// an [`AssetTransformer`] capable of converting from an `L` asset to an `S` asset, and
54/// an [`AssetSaver`] that allows you save any `S` asset. However you can
55/// also implement [`Process`] directly if [`LoadTransformAndSave`] feels limiting or unnecessary.
56///
57/// If your [`Process`] does not need to transform the [`Asset`], you can use [`IdentityAssetTransformer`] as `T`.
58/// This will directly return the input [`Asset`], allowing your [`Process`] to directly load and then save an [`Asset`].
59/// However, this pattern should only be used for cases such as file format conversion.
60/// Otherwise, consider refactoring your [`AssetLoader`] and [`AssetSaver`] to isolate the transformation step into an explicit [`AssetTransformer`].
61///
62/// This uses [`LoadTransformAndSaveSettings`] to configure the processor.
63///
64/// [`Asset`]: crate::Asset
65#[derive(TypePath)]
66pub struct LoadTransformAndSave<
67    L: AssetLoader,
68    T: AssetTransformer<AssetInput = L::Asset>,
69    S: AssetSaver<Asset = T::AssetOutput>,
70> {
71    transformer: T,
72    saver: S,
73    marker: PhantomData<fn() -> L>,
74}
75
76impl<L: AssetLoader, S: AssetSaver<Asset = L::Asset>> From<S>
77    for LoadTransformAndSave<L, IdentityAssetTransformer<L::Asset>, S>
78{
79    fn from(value: S) -> Self {
80        LoadTransformAndSave {
81            transformer: IdentityAssetTransformer::new(),
82            saver: value,
83            marker: PhantomData,
84        }
85    }
86}
87
88/// Settings for the [`LoadTransformAndSave`] [`Process::Settings`] implementation.
89///
90/// `LoaderSettings` corresponds to [`AssetLoader::Settings`], `TransformerSettings` corresponds to [`AssetTransformer::Settings`],
91/// and `SaverSettings` corresponds to [`AssetSaver::Settings`].
92#[derive(Serialize, Deserialize, Default)]
93pub struct LoadTransformAndSaveSettings<LoaderSettings, TransformerSettings, SaverSettings> {
94    /// The [`AssetLoader::Settings`] for [`LoadTransformAndSave`].
95    pub loader_settings: LoaderSettings,
96    /// The [`AssetTransformer::Settings`] for [`LoadTransformAndSave`].
97    pub transformer_settings: TransformerSettings,
98    /// The [`AssetSaver::Settings`] for [`LoadTransformAndSave`].
99    pub saver_settings: SaverSettings,
100}
101
102impl<
103        L: AssetLoader,
104        T: AssetTransformer<AssetInput = L::Asset>,
105        S: AssetSaver<Asset = T::AssetOutput>,
106    > LoadTransformAndSave<L, T, S>
107{
108    pub fn new(transformer: T, saver: S) -> Self {
109        LoadTransformAndSave {
110            transformer,
111            saver,
112            marker: PhantomData,
113        }
114    }
115}
116
117/// An error that is encountered during [`Process::process`].
118#[derive(Error, Debug)]
119pub enum ProcessError {
120    #[error(transparent)]
121    MissingAssetLoaderForExtension(#[from] MissingAssetLoaderForExtensionError),
122    #[error(transparent)]
123    MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError),
124    /// No processor with that name is registered..
125    #[error("The processor '{0}' does not exist")]
126    #[from(ignore)]
127    MissingProcessor(String),
128    /// The given short name is ambiguous between several processors.
129    #[error("The processor '{processor_short_name}' is ambiguous between several processors: {ambiguous_processor_names:?}")]
130    AmbiguousProcessor {
131        /// The given string for the processor name.
132        processor_short_name: String,
133        /// The list of processors that might match it.
134        ambiguous_processor_names: Vec<&'static str>,
135    },
136    /// Encountered an [`AssetReaderError`] for this path.
137    #[error("Encountered an AssetReader error for '{path}': {err}")]
138    #[from(ignore)]
139    AssetReaderError {
140        path: AssetPath<'static>,
141        err: AssetReaderError,
142    },
143    /// Encountered an [`AssetWriterError`] for this path.
144    #[error("Encountered an AssetWriter error for '{path}': {err}")]
145    #[from(ignore)]
146    AssetWriterError {
147        path: AssetPath<'static>,
148        err: AssetWriterError,
149    },
150    #[error(transparent)]
151    MissingAssetWriterError(#[from] MissingAssetWriterError),
152    #[error(transparent)]
153    MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError),
154    #[error(transparent)]
155    MissingProcessedAssetWriterError(#[from] MissingProcessedAssetWriterError),
156    /// Encountered an [`AssetReaderError`] when reading the asset metadata for this path.
157    #[error("Failed to read asset metadata for {path}: {err}")]
158    #[from(ignore)]
159    ReadAssetMetaError {
160        path: AssetPath<'static>,
161        err: AssetReaderError,
162    },
163    #[error(transparent)]
164    DeserializeMetaError(#[from] DeserializeMetaError),
165    #[error(transparent)]
166    AssetLoadError(#[from] AssetLoadError),
167    /// The wrong meta type was passed into a processor.
168    /// This is probably an internal implementation error.
169    #[error("The wrong meta type was passed into a processor. This is probably an internal implementation error.")]
170    WrongMetaType,
171    /// Encountered an error while saving the asset.
172    #[error("Encountered an error while saving the asset: {0}")]
173    #[from(ignore)]
174    AssetSaveError(BevyError),
175    /// Encountered an error while transforming the asset.
176    #[error("Encountered an error while transforming the asset: {0}")]
177    #[from(ignore)]
178    AssetTransformError(Box<dyn core::error::Error + Send + Sync + 'static>),
179    /// Assets without extensions are not supported.
180    #[error("Assets without extensions are not supported.")]
181    ExtensionRequired,
182}
183
184impl<Loader, Transformer, Saver> Process for LoadTransformAndSave<Loader, Transformer, Saver>
185where
186    Loader: AssetLoader,
187    Transformer: AssetTransformer<AssetInput = Loader::Asset>,
188    Saver: AssetSaver<Asset = Transformer::AssetOutput>,
189{
190    type Settings =
191        LoadTransformAndSaveSettings<Loader::Settings, Transformer::Settings, Saver::Settings>;
192    type OutputLoader = Saver::OutputLoader;
193
194    async fn process(
195        &self,
196        context: &mut ProcessContext<'_>,
197        settings: &Self::Settings,
198        writer: &mut Writer,
199    ) -> Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError> {
200        let pre_transformed_asset = TransformedAsset::<Loader::Asset>::from_loaded(
201            context
202                .load_source_asset::<Loader>(&settings.loader_settings)
203                .await?,
204        )
205        .unwrap();
206
207        let post_transformed_asset = self
208            .transformer
209            .transform(pre_transformed_asset, &settings.transformer_settings)
210            .await
211            .map_err(|err| ProcessError::AssetTransformError(err.into()))?;
212
213        let saved_asset =
214            SavedAsset::<Transformer::AssetOutput>::from_transformed(&post_transformed_asset);
215
216        let output_settings = self
217            .saver
218            .save(
219                writer,
220                saved_asset,
221                &settings.saver_settings,
222                context.path.clone(),
223            )
224            .await
225            .map_err(|error| ProcessError::AssetSaveError(error.into()))?;
226        Ok(output_settings)
227    }
228}
229
230/// A type-erased variant of [`Process`] that enables interacting with processor implementations without knowing
231/// their type.
232pub trait ErasedProcessor: Send + Sync {
233    /// Type-erased variant of [`Process::process`].
234    fn process<'a>(
235        &'a self,
236        context: &'a mut ProcessContext,
237        settings: &'a dyn Settings,
238        writer: &'a mut Writer,
239    ) -> BoxedFuture<'a, Result<Box<dyn AssetMetaDyn>, ProcessError>>;
240    /// Deserialized `meta` as type-erased [`AssetMeta`], operating under the assumption that it matches the meta
241    /// for the underlying [`Process`] impl.
242    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
243    /// Returns the type-path of the original [`Process`].
244    fn type_path(&self) -> &'static str;
245    /// Returns the short type path of this processor.
246    fn short_type_path(&self) -> &'static str;
247    /// Returns the default type-erased [`AssetMeta`] for the underlying [`Process`] impl.
248    fn default_meta(&self, processor_path_kind: MetaTypePathKind) -> Box<dyn AssetMetaDyn>;
249}
250
251/// Specifies which kind of path to use to specify a type.
252pub enum MetaTypePathKind {
253    /// Use the short type path.
254    Short,
255    /// Use the fully-qualified type path.
256    Long,
257}
258
259impl<P: Process> ErasedProcessor for P {
260    fn process<'a>(
261        &'a self,
262        context: &'a mut ProcessContext,
263        settings: &'a dyn Settings,
264        writer: &'a mut Writer,
265    ) -> BoxedFuture<'a, Result<Box<dyn AssetMetaDyn>, ProcessError>> {
266        Box::pin(async move {
267            let settings = settings.downcast_ref().ok_or(ProcessError::WrongMetaType)?;
268            let loader_settings = <P as Process>::process(self, context, settings, writer).await?;
269            let output_meta: Box<dyn AssetMetaDyn> =
270                Box::new(AssetMeta::<P::OutputLoader, ()>::new(AssetAction::Load {
271                    loader: P::OutputLoader::type_path().to_string(),
272                    settings: loader_settings,
273                }));
274            Ok(output_meta)
275        })
276    }
277
278    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
279        let meta: AssetMeta<(), P> = ron::de::from_bytes(meta)?;
280        Ok(Box::new(meta))
281    }
282
283    fn type_path(&self) -> &'static str {
284        P::type_path()
285    }
286
287    fn short_type_path(&self) -> &'static str {
288        P::short_type_path()
289    }
290
291    fn default_meta(&self, processor_path_kind: MetaTypePathKind) -> Box<dyn AssetMetaDyn> {
292        let type_path = match processor_path_kind {
293            MetaTypePathKind::Short => P::short_type_path(),
294            MetaTypePathKind::Long => P::type_path(),
295        };
296        Box::new(AssetMeta::<(), P>::new(AssetAction::Process {
297            processor: type_path.to_string(),
298            settings: P::Settings::default(),
299        }))
300    }
301}
302
303/// Provides scoped data access to the [`AssetProcessor`].
304/// This must only expose processor data that is represented in the asset's hash.
305pub struct ProcessContext<'a> {
306    /// The "new" processed info for the final processed asset. It is [`ProcessContext`]'s
307    /// job to populate `process_dependencies` with any asset dependencies used to process
308    /// this asset (ex: loading an asset value from the [`AssetServer`] of the [`AssetProcessor`])
309    ///
310    /// DO NOT CHANGE ANY VALUES HERE OTHER THAN APPENDING TO `process_dependencies`
311    ///
312    /// Do not expose this publicly as it would be too easily to invalidate state.
313    ///
314    /// [`AssetServer`]: crate::server::AssetServer
315    pub(crate) new_processed_info: &'a mut ProcessedInfo,
316    /// This exists to expose access to asset values (via the [`AssetServer`]).
317    ///
318    /// ANY ASSET VALUE THAT IS ACCESSED SHOULD BE ADDED TO `new_processed_info.process_dependencies`
319    ///
320    /// Do not expose this publicly as it would be too easily to invalidate state by forgetting to update
321    /// `process_dependencies`.
322    ///
323    /// [`AssetServer`]: crate::server::AssetServer
324    processor: &'a AssetProcessor,
325    path: &'a AssetPath<'static>,
326    reader: Box<dyn Reader + 'a>,
327}
328
329impl<'a> ProcessContext<'a> {
330    pub(crate) fn new(
331        processor: &'a AssetProcessor,
332        path: &'a AssetPath<'static>,
333        reader: Box<dyn Reader + 'a>,
334        new_processed_info: &'a mut ProcessedInfo,
335    ) -> Self {
336        Self {
337            processor,
338            path,
339            reader,
340            new_processed_info,
341        }
342    }
343
344    /// Load the source asset using the `L` [`AssetLoader`] and the passed in `meta` config.
345    /// This will take the "load dependencies" (asset values used when loading with `L`]) and
346    /// register them as "process dependencies" because they are asset values required to process the
347    /// current asset.
348    pub async fn load_source_asset<L: AssetLoader>(
349        &mut self,
350        settings: &L::Settings,
351    ) -> Result<ErasedLoadedAsset, AssetLoadError> {
352        let server = &self.processor.server;
353        let loader_name = L::type_path();
354        let loader = server.get_asset_loader_with_type_name(loader_name).await?;
355        let loaded_asset = server
356            .load_with_settings_loader_and_reader(
357                self.path,
358                settings,
359                &*loader,
360                &mut self.reader,
361                false,
362                true,
363            )
364            .await?;
365        for (path, full_hash) in &loaded_asset.loader_dependencies {
366            self.new_processed_info
367                .process_dependencies
368                .push(ProcessDependencyInfo {
369                    full_hash: *full_hash,
370                    path: path.to_owned(),
371                });
372        }
373        Ok(loaded_asset)
374    }
375
376    /// The path of the asset being processed.
377    #[inline]
378    pub fn path(&self) -> &AssetPath<'static> {
379        self.path
380    }
381
382    /// The reader for the asset being processed.
383    #[inline]
384    pub fn asset_reader(&mut self) -> &mut dyn Reader {
385        &mut self.reader
386    }
387}