steel_core/chunk/chunk_map/
persistence.rs1use super::{
2 Arc, Chunk, ChunkHolder, ChunkMap, ChunkSaveDependency, ChunkStatus, ChunkStorage,
3 ClearedBlockEntities, FinalizedBlockEntityUnload, FxHashSet, instrument, io, mem,
4};
5use crate::chunk_saver::PreparedChunkSave;
6use tokio::sync::oneshot;
7
8impl ChunkMap {
9 async fn prepare_chunk_save_on_pool(
10 self: &Arc<Self>,
11 chunk_holder: &Arc<ChunkHolder>,
12 ) -> Option<PreparedChunkSave> {
13 let (sender, receiver) = oneshot::channel();
14 let map = Arc::clone(self);
15 let holder = Arc::clone(chunk_holder);
16 self.chunk_encoding_pool.spawn(move || {
17 let chunk_pos = holder.get_pos();
18 let prepared = {
19 let Some(save_preparation) = holder.try_begin_save_preparation() else {
20 let _ = sender.send(None);
21 return;
22 };
23 let Some(chunk_guard) = holder.try_chunk(ChunkStatus::StructureStarts) else {
24 let _ = sender.send(None);
28 return;
29 };
30
31 let Some(status) = holder.published_status() else {
32 let _ = sender.send(None);
33 return;
34 };
35
36 let world = map.world_gen_context.world();
37 let runtime_entities = world
38 .entity_manager()
39 .get_saveable_entities_for_chunk(chunk_pos);
40 let force = world.entity_manager().has_save_pending_for_chunk(chunk_pos);
41 let dirty = chunk_guard.take_dirty();
42 let prepared = if dirty || force {
43 ChunkStorage::prepare_chunk_save(chunk_guard, status, &runtime_entities, true)
44 } else {
45 None
46 };
47
48 let prepared = save_preparation.finish(prepared).flatten();
49
50 if prepared.is_none() && dirty {
51 chunk_guard.mark_dirty();
52 }
53
54 prepared
55 };
56
57 if let Err(unsent) = sender.send(prepared) {
58 if unsent.is_some() {
59 Self::mark_chunk_dirty_for_save_retry(&holder);
60 }
61 tracing::trace!(
62 chunk = ?chunk_pos,
63 "Discarding prepared chunk after its save task was canceled"
64 );
65 }
66 });
67
68 if let Ok(prepared) = receiver.await {
69 prepared
70 } else {
71 Self::mark_chunk_dirty_for_save_retry(chunk_holder);
72 tracing::error!(
73 chunk = ?chunk_holder.get_pos(),
74 "Chunk save preparation task ended without returning a result"
75 );
76 None
77 }
78 }
79
80 #[instrument(level = "trace", skip(self, chunk_holder, _save_dependency), fields(chunk = ?chunk_holder.get_pos()))]
82 pub(super) async fn save_chunk(
83 self: &Arc<Self>,
84 chunk_holder: &Arc<ChunkHolder>,
85 _save_dependency: ChunkSaveDependency,
86 ) {
87 let chunk_pos = chunk_holder.get_pos();
88 self.flush_queued_light_changes_touching_chunk_for_save(chunk_pos)
89 .await;
90
91 if let Some(mut prepared) = self.prepare_chunk_save_on_pool(chunk_holder).await {
94 let handled_runtime_entity_ids = mem::take(&mut prepared.handled_runtime_entity_ids);
95 let world = self.world_gen_context.world();
96 match self
97 .storage
98 .save_chunk_data(prepared, self.chunk_encoding_pool.as_ref())
99 .await
100 {
101 Ok(true) => world
102 .entity_manager()
103 .on_chunk_saved(chunk_pos, &handled_runtime_entity_ids),
104 Ok(false) => Self::mark_chunk_dirty_for_save_retry(chunk_holder),
105 Err(e) => {
106 tracing::error!("Error saving chunk: {e}");
107 Self::mark_chunk_dirty_for_save_retry(chunk_holder);
108 }
109 }
110 }
111 }
112
113 pub(super) fn mark_chunk_dirty_for_save_retry(chunk_holder: &ChunkHolder) {
114 let Some(chunk) = chunk_holder.try_chunk(ChunkStatus::StructureStarts) else {
115 return;
116 };
117 chunk.mark_dirty();
118 }
119
120 #[instrument(level = "trace", skip(self))]
126 pub(super) fn process_unloads(self: &Arc<Self>) {
127 self.propagate_queued_light_changes();
128
129 let mut finalized = Vec::new();
130 {
131 let light_updates = self.light_updates.lock();
132 self.unloading_chunks.retain_sync(|pos, holder| {
133 if light_updates.touches_chunk(*pos) {
134 return true;
135 }
136
137 if Arc::strong_count(holder) != 1 {
138 return true;
139 }
140
141 let is_dirty = holder
142 .try_chunk(ChunkStatus::StructureStarts)
143 .is_some_and(Chunk::is_dirty);
144 let has_save_pending_entities = self
145 .world_gen_context
146 .world()
147 .entity_manager()
148 .has_save_pending_for_chunk(*pos);
149
150 if is_dirty || has_save_pending_entities {
151 let save_dependency = holder.add_save_dependency();
152 let holder_clone = Arc::clone(holder);
153 let map_clone = Arc::clone(self);
154 self.task_tracker.spawn_on(
155 async move {
156 map_clone.save_chunk(&holder_clone, save_dependency).await;
157 },
158 self.chunk_runtime.handle(),
159 );
160 return true;
161 }
162
163 let has_chunk = holder.try_chunk(ChunkStatus::Empty).is_some();
164 finalized.push((*pos, Arc::clone(holder), has_chunk));
165 false
166 });
167 }
168
169 let world = self.world_gen_context.world();
170 for (pos, holder, has_chunk) in finalized {
171 let cleared = if has_chunk {
172 holder.try_chunk(ChunkStatus::Empty).map_or_else(
173 ClearedBlockEntities::default,
174 |chunk| {
175 if let Some(full) = holder.try_full_chunk() {
176 full.clear_all_block_entities_staged()
177 } else {
178 chunk.clear_all_block_entities();
179 ClearedBlockEntities::default()
180 }
181 },
182 )
183 } else {
184 ClearedBlockEntities::default()
185 };
186 self.finalized_block_entity_unloads
187 .lock()
188 .push(FinalizedBlockEntityUnload {
189 holder: Arc::clone(&holder),
190 lifecycle_dispatchers: cleared.lifecycle_dispatchers,
191 positions: cleared.positions,
192 });
193
194 world.unregister_full_chunk_ticks(pos);
195 world.on_entity_chunk_unload_finalized(pos);
196 if has_chunk {
197 let map_clone = Arc::clone(self);
198 self.task_tracker.spawn_on(
199 async move {
200 if let Err(e) = map_clone.storage.release_chunk(pos).await {
201 tracing::error!(?pos, "Error releasing chunk: {e}");
202 }
203 },
204 self.chunk_runtime.handle(),
205 );
206 }
207 }
208 }
209
210 #[instrument(level = "info", skip(self), name = "save_all_chunks")]
220 #[expect(
221 clippy::too_many_lines,
222 reason = "shutdown persistence keeps chunk coverage and entity auditing in one pass"
223 )]
224 pub async fn save_all_chunks(self: &Arc<Self>) -> io::Result<usize> {
225 let mut saved_count = 0;
226
227 self.flush_queued_light_changes_for_save().await;
228
229 let all_chunks: Vec<Arc<ChunkHolder>> = {
231 let mut chunks = Vec::new();
232 self.chunks.iter_sync(|_, holder| {
233 chunks.push(Arc::clone(holder));
234 true
235 });
236 self.unloading_chunks.iter_sync(|_, holder| {
237 chunks.push(Arc::clone(holder));
238 true
239 });
240 chunks
241 };
242 let mut covered_chunk_positions = FxHashSet::default();
243
244 tracing::info!(chunk_count = all_chunks.len(), "Saving chunks");
245
246 for holder in &all_chunks {
248 let chunk_pos = holder.get_pos();
249 let prepared = {
250 let Some(chunk) = holder.try_chunk(ChunkStatus::StructureStarts) else {
251 continue;
255 };
256 let Some(status) = holder.published_status() else {
257 continue;
258 };
259 let world = self.world_gen_context.world();
260 let runtime_entities = world
261 .entity_manager()
262 .get_saveable_entities_for_chunk(chunk_pos);
263 let force = world.entity_manager().has_save_pending_for_chunk(chunk_pos);
264 let dirty = chunk.take_dirty();
265 let prepared = if dirty || force {
266 ChunkStorage::prepare_chunk_save(chunk, status, &runtime_entities, true)
267 } else {
268 None
269 };
270 let Some(prepared) = prepared else {
271 if dirty {
272 chunk.mark_dirty();
273 } else if !force {
274 covered_chunk_positions.insert(chunk_pos);
275 }
276 continue;
277 };
278 prepared
279 };
280
281 let mut prepared = prepared;
282 let handled_runtime_entity_ids = mem::take(&mut prepared.handled_runtime_entity_ids);
283 let world = self.world_gen_context.world();
284 let _save_dependency = holder.add_save_dependency();
285 match self
286 .storage
287 .save_chunk_data(prepared, self.chunk_encoding_pool.as_ref())
288 .await
289 {
290 Ok(true) => {
291 world
292 .entity_manager()
293 .on_chunk_saved(chunk_pos, &handled_runtime_entity_ids);
294 covered_chunk_positions.insert(chunk_pos);
295 saved_count += 1;
296 }
297 Ok(false) => Self::mark_chunk_dirty_for_save_retry(holder),
298 Err(e) => {
299 tracing::error!(chunk = ?holder.get_pos(), "Failed to save chunk: {e}");
300 Self::mark_chunk_dirty_for_save_retry(holder);
301 }
302 }
303 }
304
305 let world = self.world_gen_context.world();
306 let covered_chunk_positions = covered_chunk_positions.into_iter().collect::<Vec<_>>();
307 let unsaved_entities = world
308 .entity_manager()
309 .saveable_entities_outside_chunks(&covered_chunk_positions);
310 if !unsaved_entities.is_empty() {
311 let chunk_count = unsaved_entities
312 .iter()
313 .map(|entity| entity.chunk)
314 .collect::<FxHashSet<_>>()
315 .len();
316 let sample = unsaved_entities
317 .iter()
318 .take(16)
319 .map(|entity| format!("{}:{}@{:?}", entity.entity_id, entity.uuid, entity.chunk))
320 .collect::<Vec<_>>()
321 .join(", ");
322 tracing::warn!(
323 entity_count = unsaved_entities.len(),
324 chunk_count,
325 sample = %sample,
326 "Saveable runtime entities remain in chunks without save holders after chunk save"
327 );
328 }
329
330 if let Err(e) = self.storage.close_all().await {
332 tracing::error!("Failed to close region files: {e}");
333 }
334
335 tracing::info!(
336 saved_count,
337 total_checked = all_chunks.len(),
338 "Chunk save complete"
339 );
340
341 Ok(saved_count)
342 }
343}