bevy_cache/save_queue.rs
1use bevy::prelude::*;
2use bevy::reflect::serde::ReflectSerializer;
3
4use std::time::Duration;
5
6use crate::config::CacheConfig;
7use crate::error::CacheError;
8use crate::manifest::CacheManifest;
9
10/// A pending save entry — either reflected data awaiting serialization,
11/// or a reader ready to write.
12pub(crate) enum PendingPayload {
13 /// A reflected value to be serialized via [`ReflectSerializer`] → RON.
14 Reflect {
15 value: Box<dyn Reflect>,
16 extension: String,
17 },
18 /// Pre-serialized data provided as a [`Read`]er.
19 Bytes {
20 reader: Box<dyn std::io::Read + Send + Sync + 'static>,
21 extension: String,
22 },
23}
24
25pub(crate) struct PendingSaveEntry {
26 pub key: String,
27 pub max_age: Option<Duration>,
28 pub payload: PendingPayload,
29}
30
31/// Resource holding queued asset-save requests.
32///
33/// Assets can be enqueued in two ways:
34///
35/// 1. [`enqueue_reflect`](Self::enqueue_reflect) — pass any value that
36/// implements [`Reflect`]. The value will be serialized to RON via
37/// Bevy's [`ReflectSerializer`] using the [`AppTypeRegistry`].
38/// The type **must** be registered in the type registry (via
39/// `app.register_type::<T>()`) and should have `ReflectSerialize`
40/// type data (derived automatically for types that implement both
41/// `Reflect` and `serde::Serialize`).
42///
43/// 2. [`enqueue`](Self::enqueue) — pass any [`Read`]er containing
44/// pre-serialized data and a file extension. No reflection or registry
45/// required. Use [`std::io::Cursor`] in tests or for in-memory data.
46///
47/// Enqueued entries are processed by [`process_pending_saves`] which runs
48/// each frame in `PostUpdate`.
49#[derive(Resource, Default)]
50pub struct CacheQueue {
51 pub(crate) queue: Vec<PendingSaveEntry>,
52}
53
54impl CacheQueue {
55 /// Returns `true` when there are no pending save requests.
56 pub fn is_empty(&self) -> bool {
57 self.queue.is_empty()
58 }
59
60 /// Returns the number of pending save requests.
61 pub fn len(&self) -> usize {
62 self.queue.len()
63 }
64
65 /// Enqueue a reflected value for caching.
66 ///
67 /// The value will be serialized to RON using Bevy's
68 /// [`ReflectSerializer`] during [`process_pending_saves`].
69 /// The type must be registered in the [`AppTypeRegistry`].
70 ///
71 /// # Example
72 /// ```rust,ignore
73 /// #[derive(Asset, TypePath, Reflect, serde::Serialize)]
74 /// #[reflect(Serialize)]
75 /// struct LevelData { tiles: Vec<u32> }
76 ///
77 /// fn cache_level(
78 /// mut pending: ResMut<CacheQueue>,
79 /// assets: Res<Assets<LevelData>>,
80 /// handle: Res<MyLevelHandle>,
81 /// ) {
82 /// if let Some(level) = assets.get(&handle.0) {
83 /// pending.enqueue_reflect(
84 /// Box::new(level.clone()),
85 /// "level_01",
86 /// "ron",
87 /// None,
88 /// );
89 /// }
90 /// }
91 /// ```
92 pub fn enqueue_reflect(
93 &mut self,
94 value: Box<dyn Reflect>,
95 key: impl Into<String>,
96 extension: impl Into<String>,
97 max_age: Option<Duration>,
98 ) {
99 self.queue.push(PendingSaveEntry {
100 key: key.into(),
101 max_age,
102 payload: PendingPayload::Reflect {
103 value,
104 extension: extension.into(),
105 },
106 });
107 }
108
109 /// Enqueue a [`Read`]er for caching.
110 ///
111 /// The caller is responsible for serialization; the data is streamed
112 /// as-is to `{key}.{extension}` in the cache directory without buffering
113 /// the entire payload in memory.
114 ///
115 /// Use [`std::io::Cursor`] to wrap in-memory buffers:
116 ///
117 /// # Example
118 /// ```rust,ignore
119 /// fn cache_screenshot(mut pending: ResMut<CacheQueue>) {
120 /// let png_bytes: Vec<u8> = capture_screenshot();
121 /// pending.enqueue("scene_01", "png", std::io::Cursor::new(png_bytes), None);
122 /// }
123 /// ```
124 pub fn enqueue<R: std::io::Read + Send + Sync + 'static>(
125 &mut self,
126 key: impl Into<String>,
127 extension: impl Into<String>,
128 reader: R,
129 max_age: Option<Duration>,
130 ) {
131 self.queue.push(PendingSaveEntry {
132 key: key.into(),
133 max_age,
134 payload: PendingPayload::Bytes {
135 reader: Box::new(reader),
136 extension: extension.into(),
137 },
138 });
139 }
140}
141
142/// Serialize a reflected value to RON bytes using the type registry.
143fn serialize_reflect(
144 value: &dyn Reflect,
145 registry: &AppTypeRegistry,
146) -> Result<Vec<u8>, CacheError> {
147 let registry = registry.read();
148 let serializer = ReflectSerializer::new(value.as_partial_reflect(), ®istry);
149 let pretty = ron::ser::PrettyConfig::default();
150 let serialized =
151 ron::ser::to_string_pretty(&serializer, pretty).map_err(CacheError::RonSerialize)?;
152 Ok(serialized.into_bytes())
153}
154
155/// System that processes the pending save queue each frame.
156/// Reflected values are serialized via [`ReflectSerializer`], then all
157/// entries are written to the cache directory through [`CacheManifest::store`].
158pub fn process_pending_saves(world: &mut World) {
159 let queue = {
160 let mut pending = world
161 .get_resource_mut::<CacheQueue>()
162 .expect("CacheQueue resource should exist");
163 std::mem::take(&mut pending.queue)
164 };
165
166 if queue.is_empty() {
167 return;
168 }
169
170 let config = world
171 .get_resource::<CacheConfig>()
172 .expect("CacheConfig resource should exist")
173 .clone();
174
175 let registry = world
176 .get_resource::<AppTypeRegistry>()
177 .expect("AppTypeRegistry resource should exist")
178 .clone();
179
180 for entry in queue {
181 let PendingSaveEntry { key, max_age, payload } = entry;
182 let result = match payload {
183 PendingPayload::Reflect { value, extension } => {
184 serialize_reflect(value.as_ref(), ®istry)
185 .and_then(|bytes| {
186 let mut manifest = world
187 .get_resource_mut::<CacheManifest>()
188 .expect("CacheManifest resource should exist");
189 manifest.store(
190 &config,
191 &key,
192 &extension,
193 std::io::Cursor::new(bytes),
194 max_age,
195 )
196 })
197 }
198 PendingPayload::Bytes { mut reader, extension } => {
199 let mut manifest = world
200 .get_resource_mut::<CacheManifest>()
201 .expect("CacheManifest resource should exist");
202 manifest.store(
203 &config,
204 &key,
205 &extension,
206 &mut *reader,
207 max_age,
208 )
209 }
210 };
211
212 match result {
213 Ok(()) => {
214 tracing::debug!("Cached asset '{key}'");
215 }
216 Err(e) => {
217 tracing::error!("Failed to cache asset '{key}': {e}");
218 }
219 }
220 }
221}