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