1use std::{
2 cmp::Ordering as CmpOrdering,
3 io::{self, Cursor},
4 sync::{Arc, Weak, atomic::Ordering},
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use crate::block_entity::{BLOCK_ENTITIES, SharedBlockEntity};
9use crate::chunk::full_chunk::FullChunkRef;
10use crate::chunk::heightmap::{ChunkHeightmaps, Heightmap, HeightmapType};
11use crate::chunk::light::{
12 ChunkLightData, ChunkLightLayerStorage, DATA_LAYER_SIZE, LightSection, LightSectionData,
13};
14use crate::chunk::paletted_container::PalettedContainer;
15use crate::chunk::section::{ChunkSection, SectionHolder, Sections};
16use crate::chunk::{Chunk, status::ChunkStatus};
17use crate::chunk_saver::bit_pack::{bits_for_palette_len, pack_indices_from_iter, unpack_indices};
18use crate::entity::{
19 ENTITIES, Entity, EntityBase, EntityBaseSaveData, EntityFireFreezeState, EntityLoadRequest,
20 MAX_ENTITY_TAGS, RemovalReason, SharedEntity,
21};
22use crate::world::World;
23use crate::world::tick_scheduler::{BlockTickList, FluidTickList, SavedTick, TickPriority};
24use glam::{DVec3, IVec3};
25use rustc_hash::FxHashSet;
26use simdnbt::borrow::read_compound as read_borrowed_compound;
27use simdnbt::owned::NbtCompound;
28use steel_registry::structure::{
29 LiquidSettingsData, OceanRuinBiomeTempData, RuinedPortalPlacementData, TerrainAdjustment,
30};
31use steel_registry::template_pool::{PoolElement, ProcessorList, Projection};
32use steel_registry::{
33 REGISTRY, Registry, RegistryExt,
34 blocks::{BlockRef, block_state_ext::BlockStateExt as _},
35 fluid::FluidRef,
36};
37use steel_utils::{
38 BlockPos, BlockStateId, ChunkPos, Direction, Identifier, PackedChunkPos, Rotation,
39};
40use text_components::TextComponent;
41
42use steel_worldgen::structure::desert_pyramid::DesertPyramidPieceData;
43use steel_worldgen::structure::fortress::FortressPieceData;
44use steel_worldgen::structure::jigsaw::{JigsawJunction, JigsawPieceData};
45use steel_worldgen::structure::jungle_temple::JungleTemplePieceData;
46use steel_worldgen::structure::mineshaft::{
47 MineshaftPieceKind, MineshaftPiecePayload, MineshaftType,
48};
49use steel_worldgen::structure::ocean_monument::{
50 OceanMonumentChildPiece, OceanMonumentChildPieceKind, OceanMonumentPieceData,
51 OceanMonumentRoomData,
52};
53use steel_worldgen::structure::stronghold::{StrongholdPieceData, StrongholdSmallDoorType};
54use steel_worldgen::structure::swamp_hut::SwampHutPieceData;
55use steel_worldgen::structure::{
56 ProceduralPieceData, RuinedPortalProperties, StructureBlockIgnore, StructureMirror,
57 StructurePiece, StructurePiecePayload, StructureReferenceMap, StructureStart,
58 StructureStartMap, TemplateMarkerHandling, TemplatePieceData, TemplatePlacementAdjustment,
59 TemplatePlacementClip, TemplatePostProcess, TemplateProcessorList,
60};
61
62mod chunk;
63mod entities;
64mod light;
65mod structures;
66mod ticks;
67
68#[cfg(test)]
69mod tests;
70
71const fn direction_to_2d(dir: Option<Direction>) -> i8 {
74 match dir {
75 Some(Direction::South) => 0,
76 Some(Direction::West) => 1,
77 Some(Direction::North) => 2,
78 Some(Direction::East) => 3,
79 None | Some(Direction::Down | Direction::Up) => -1,
80 }
81}
82
83const fn direction_from_2d(value: i8) -> Option<Direction> {
85 match value {
86 0 => Some(Direction::South),
87 1 => Some(Direction::West),
88 2 => Some(Direction::North),
89 3 => Some(Direction::East),
90 _ => None,
91 }
92}
93
94const fn required_direction_from_2d(value: i8) -> Direction {
95 match value {
96 1 => Direction::West,
97 2 => Direction::North,
98 3 => Direction::East,
99 _ => Direction::South,
100 }
101}
102
103const fn mineshaft_type_to_persistent(mineshaft_type: MineshaftType) -> i8 {
104 match mineshaft_type {
105 MineshaftType::Normal => 0,
106 MineshaftType::Mesa => 1,
107 }
108}
109
110const fn mineshaft_type_from_persistent(value: i8) -> MineshaftType {
111 match value {
112 1 => MineshaftType::Mesa,
113 _ => MineshaftType::Normal,
114 }
115}
116
117const fn projection_to_persistent(projection: Option<Projection>) -> i8 {
118 match projection {
119 None => -1,
120 Some(Projection::Rigid) => 0,
121 Some(Projection::TerrainMatching) => 1,
122 }
123}
124
125const fn projection_from_persistent(value: i8) -> Option<Projection> {
126 match value {
127 0 => Some(Projection::Rigid),
128 1 => Some(Projection::TerrainMatching),
129 _ => None,
130 }
131}
132
133const fn required_projection_from_persistent(value: i8) -> Projection {
134 match value {
135 1 => Projection::TerrainMatching,
136 _ => Projection::Rigid,
137 }
138}
139
140const fn rotation_to_persistent(rotation: Rotation) -> i8 {
141 match rotation {
142 Rotation::None => 0,
143 Rotation::Clockwise90 => 1,
144 Rotation::Clockwise180 => 2,
145 Rotation::CounterClockwise90 => 3,
146 }
147}
148
149const fn rotation_from_persistent(value: i8) -> Rotation {
150 match value {
151 1 => Rotation::Clockwise90,
152 2 => Rotation::Clockwise180,
153 3 => Rotation::CounterClockwise90,
154 _ => Rotation::None,
155 }
156}
157
158const fn liquid_settings_to_persistent(settings: LiquidSettingsData) -> i8 {
159 match settings {
160 LiquidSettingsData::ApplyWaterlogging => 0,
161 LiquidSettingsData::IgnoreWaterlogging => 1,
162 }
163}
164
165const fn liquid_settings_from_persistent(value: i8) -> LiquidSettingsData {
166 match value {
167 1 => LiquidSettingsData::IgnoreWaterlogging,
168 _ => LiquidSettingsData::ApplyWaterlogging,
169 }
170}
171
172const fn ruined_portal_placement_to_persistent(placement: RuinedPortalPlacementData) -> i8 {
173 match placement {
174 RuinedPortalPlacementData::OnLandSurface => 0,
175 RuinedPortalPlacementData::PartlyBuried => 1,
176 RuinedPortalPlacementData::Underground => 2,
177 RuinedPortalPlacementData::InMountain => 3,
178 RuinedPortalPlacementData::OnOceanFloor => 4,
179 RuinedPortalPlacementData::InNether => 5,
180 }
181}
182
183const fn ruined_portal_placement_from_persistent(value: i8) -> RuinedPortalPlacementData {
184 match value {
185 1 => RuinedPortalPlacementData::PartlyBuried,
186 2 => RuinedPortalPlacementData::Underground,
187 3 => RuinedPortalPlacementData::InMountain,
188 4 => RuinedPortalPlacementData::OnOceanFloor,
189 5 => RuinedPortalPlacementData::InNether,
190 _ => RuinedPortalPlacementData::OnLandSurface,
191 }
192}
193
194const fn mirror_to_persistent(mirror: StructureMirror) -> i8 {
195 match mirror {
196 StructureMirror::None => 0,
197 StructureMirror::FrontBack => 1,
198 StructureMirror::LeftRight => 2,
199 }
200}
201
202const fn mirror_from_persistent(value: i8) -> StructureMirror {
203 match value {
204 1 => StructureMirror::FrontBack,
205 2 => StructureMirror::LeftRight,
206 _ => StructureMirror::None,
207 }
208}
209
210const fn block_ignore_to_persistent(block_ignore: StructureBlockIgnore) -> i8 {
211 match block_ignore {
212 StructureBlockIgnore::None => 0,
213 StructureBlockIgnore::StructureBlock => 1,
214 StructureBlockIgnore::StructureAndAir => 2,
215 }
216}
217
218const fn block_ignore_from_persistent(value: i8) -> StructureBlockIgnore {
219 match value {
220 1 => StructureBlockIgnore::StructureBlock,
221 2 => StructureBlockIgnore::StructureAndAir,
222 _ => StructureBlockIgnore::None,
223 }
224}
225
226const fn marker_handling_to_persistent(marker_handling: TemplateMarkerHandling) -> i8 {
227 match marker_handling {
228 TemplateMarkerHandling::Ignore => 0,
229 TemplateMarkerHandling::DataMarkers => 1,
230 TemplateMarkerHandling::Shipwreck => 2,
231 TemplateMarkerHandling::Igloo => 3,
232 TemplateMarkerHandling::OceanRuin { is_large: false } => 4,
233 TemplateMarkerHandling::OceanRuin { is_large: true } => 5,
234 TemplateMarkerHandling::EndCity => 6,
235 TemplateMarkerHandling::WoodlandMansion => 7,
236 }
237}
238
239const fn marker_handling_from_persistent(value: i8) -> TemplateMarkerHandling {
240 match value {
241 1 => TemplateMarkerHandling::DataMarkers,
242 2 => TemplateMarkerHandling::Shipwreck,
243 3 => TemplateMarkerHandling::Igloo,
244 4 => TemplateMarkerHandling::OceanRuin { is_large: false },
245 5 => TemplateMarkerHandling::OceanRuin { is_large: true },
246 6 => TemplateMarkerHandling::EndCity,
247 7 => TemplateMarkerHandling::WoodlandMansion,
248 _ => TemplateMarkerHandling::Ignore,
249 }
250}
251
252const fn ocean_ruin_biome_temp_to_persistent(biome_temp: OceanRuinBiomeTempData) -> i8 {
253 match biome_temp {
254 OceanRuinBiomeTempData::Warm => 0,
255 OceanRuinBiomeTempData::Cold => 1,
256 }
257}
258
259const fn ocean_ruin_biome_temp_from_persistent(value: i8) -> OceanRuinBiomeTempData {
260 match value {
261 1 => OceanRuinBiomeTempData::Cold,
262 _ => OceanRuinBiomeTempData::Warm,
263 }
264}
265
266const fn placement_adjustment_to_persistent(
267 adjustment: TemplatePlacementAdjustment,
268) -> PersistentTemplatePlacementAdjustment {
269 match adjustment {
270 TemplatePlacementAdjustment::None => PersistentTemplatePlacementAdjustment::None,
271 TemplatePlacementAdjustment::Shipwreck {
272 is_beached,
273 height_adjusted,
274 } => PersistentTemplatePlacementAdjustment::Shipwreck {
275 is_beached,
276 height_adjusted,
277 },
278 TemplatePlacementAdjustment::Igloo { template_offset } => {
279 PersistentTemplatePlacementAdjustment::Igloo {
280 template_offset: [template_offset.0, template_offset.1, template_offset.2],
281 }
282 }
283 TemplatePlacementAdjustment::OceanRuin => PersistentTemplatePlacementAdjustment::OceanRuin,
284 }
285}
286
287const fn placement_adjustment_from_persistent(
288 adjustment: &PersistentTemplatePlacementAdjustment,
289) -> TemplatePlacementAdjustment {
290 match adjustment {
291 PersistentTemplatePlacementAdjustment::None => TemplatePlacementAdjustment::None,
292 PersistentTemplatePlacementAdjustment::Shipwreck {
293 is_beached,
294 height_adjusted,
295 } => TemplatePlacementAdjustment::Shipwreck {
296 is_beached: *is_beached,
297 height_adjusted: *height_adjusted,
298 },
299 PersistentTemplatePlacementAdjustment::Igloo { template_offset } => {
300 TemplatePlacementAdjustment::Igloo {
301 template_offset: (template_offset[0], template_offset[1], template_offset[2]),
302 }
303 }
304 PersistentTemplatePlacementAdjustment::OceanRuin => TemplatePlacementAdjustment::OceanRuin,
305 }
306}
307
308const fn placement_clip_to_persistent(placement_clip: TemplatePlacementClip) -> i8 {
309 match placement_clip {
310 TemplatePlacementClip::CenterChunk => 0,
311 TemplatePlacementClip::CenterChunkExpandedToTemplate => 1,
312 TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate => 2,
313 }
314}
315
316const fn placement_clip_from_persistent(value: i8) -> TemplatePlacementClip {
317 match value {
318 1 => TemplatePlacementClip::CenterChunkExpandedToTemplate,
319 2 => TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate,
320 _ => TemplatePlacementClip::CenterChunk,
321 }
322}
323
324const fn post_process_to_persistent(post_process: TemplatePostProcess) -> i8 {
325 match post_process {
326 TemplatePostProcess::None => 0,
327 TemplatePostProcess::NetherFossil => 1,
328 TemplatePostProcess::IglooTop => 2,
329 TemplatePostProcess::RuinedPortal => 3,
330 }
331}
332
333const fn post_process_from_persistent(value: i8) -> TemplatePostProcess {
334 match value {
335 1 => TemplatePostProcess::NetherFossil,
336 2 => TemplatePostProcess::IglooTop,
337 3 => TemplatePostProcess::RuinedPortal,
338 _ => TemplatePostProcess::None,
339 }
340}
341
342fn compare_identifiers(a: &Identifier, b: &Identifier) -> CmpOrdering {
343 a.namespace
344 .cmp(&b.namespace)
345 .then_with(|| a.path.cmp(&b.path))
346}
347
348fn homogeneous_packed_light_value(data: &[u8; DATA_LAYER_SIZE]) -> Option<u8> {
349 let first = data[0];
350 let value = first & 0x0F;
351 if first >> 4 != value {
352 return None;
353 }
354 data.iter().all(|byte| *byte == first).then_some(value)
355}
356
357#[derive(Clone, Copy)]
358enum EntityPersistenceMode {
359 ChunkSave,
360 DimensionTransition,
361}
362
363use super::ram_only::RamOnlyStorage;
364use super::region_manager::RegionManager;
365use super::{
366 PersistentBiomeData, PersistentBlockEntity, PersistentBlockState, PersistentBoundingBox,
367 PersistentChunk, PersistentDesertPyramidPieceData, PersistentEntity, PersistentHeightmap,
368 PersistentJigsawJunction, PersistentJigsawPieceData, PersistentJungleTemplePieceData,
369 PersistentLightData, PersistentLightSection, PersistentMineshaftPieceData,
370 PersistentMineshaftPieceKind, PersistentNetherFortressPieceData,
371 PersistentOceanMonumentChildPiece, PersistentOceanMonumentChildPieceKind,
372 PersistentOceanMonumentPieceData, PersistentOceanMonumentRoomData, PersistentPoi,
373 PersistentPoolElement, PersistentProceduralPieceData, PersistentProcessorList,
374 PersistentSection, PersistentStrongholdPieceData, PersistentStrongholdSmallDoorType,
375 PersistentStructurePiece, PersistentStructurePiecePayload, PersistentStructureReference,
376 PersistentStructureStart, PersistentSwampHutPieceData, PersistentTemplatePieceData,
377 PersistentTemplatePlacementAdjustment, PersistentTemplateProcessorList, PersistentTick,
378 PreparedChunkSave,
379};
380
381struct ChunkBuilder<'a> {
383 block_states: Vec<PersistentBlockState<'static>>,
384 biomes: Vec<Identifier>,
385 registry: &'a Registry,
386}
387
388impl<'a> ChunkBuilder<'a> {
389 const fn new(registry: &'a Registry) -> Self {
390 Self {
391 block_states: Vec::new(),
392 biomes: Vec::new(),
393 registry,
394 }
395 }
396
397 fn ensure_block_state(&mut self, block_id: BlockStateId) -> u16 {
399 let block = self
401 .registry
402 .blocks
403 .by_state_id(block_id)
404 .expect("Invalid block state ID");
405 let properties = self.registry.blocks.properties(block_id).collect();
406
407 let persistent = PersistentBlockState {
408 name: block.key.clone(),
409 properties,
410 };
411
412 if let Some(idx) = self.block_states.iter().position(|s| s == &persistent) {
414 return idx as u16;
415 }
416
417 let idx = self.block_states.len();
419 self.block_states.push(persistent);
420 idx as u16
421 }
422
423 fn ensure_biome(&mut self, biome_id: u16) -> u16 {
425 let biome = self
427 .registry
428 .biomes
429 .by_id(biome_id as usize)
430 .expect("Invalid biome ID");
431 let identifier = biome.key.clone();
432
433 if let Some(idx) = self.biomes.iter().position(|b| b == &identifier) {
434 return idx as u16;
435 }
436
437 let idx = self.biomes.len();
438 self.biomes.push(identifier);
439 idx as u16
440 }
441}
442
443pub enum ChunkStorage {
449 Disk(RegionManager),
451 RamOnly(RamOnlyStorage),
453}
454
455pub struct LoadedChunk {
457 pub chunk: Chunk,
459 pub status: ChunkStatus,
461 pub pending_entities: Vec<SharedEntity>,
463}
464
465impl ChunkStorage {
466 pub async fn load_chunk(
472 &self,
473 pos: ChunkPos,
474 min_y: i32,
475 height: i32,
476 level: Weak<World>,
477 thread_pool: &rayon::ThreadPool,
478 ) -> io::Result<Option<LoadedChunk>> {
479 match self {
480 Self::Disk(rm) => rm.load_chunk(pos, min_y, height, level, thread_pool).await,
481 Self::RamOnly(ram) => ram.load_chunk(pos, min_y, height, level).await,
482 }
483 }
484
485 pub async fn save_chunk_data(
489 &self,
490 prepared: PreparedChunkSave,
491 thread_pool: &rayon::ThreadPool,
492 ) -> io::Result<bool> {
493 match self {
494 Self::Disk(rm) => rm.save_chunk_data(prepared, thread_pool).await,
495 Self::RamOnly(ram) => ram.save_chunk_data(prepared).await,
496 }
497 }
498
499 pub async fn chunk_exists(&self, pos: ChunkPos) -> io::Result<bool> {
501 match self {
502 Self::Disk(rm) => rm.chunk_exists(pos).await,
503 Self::RamOnly(ram) => ram.chunk_exists(pos).await,
504 }
505 }
506
507 pub async fn acquire_chunk(&self, pos: ChunkPos) -> io::Result<bool> {
512 match self {
513 Self::Disk(rm) => rm.acquire_chunk(pos).await,
514 Self::RamOnly(ram) => ram.chunk_exists(pos).await,
515 }
516 }
517
518 pub async fn release_chunk(&self, pos: ChunkPos) -> io::Result<()> {
520 match self {
521 Self::Disk(rm) => rm.release_chunk(pos).await,
522 Self::RamOnly(_) => Ok(()), }
524 }
525
526 pub async fn flush_all(&self) -> io::Result<()> {
528 match self {
529 Self::Disk(rm) => rm.flush_all().await,
530 Self::RamOnly(_) => Ok(()), }
532 }
533
534 pub async fn close_all(&self) -> io::Result<()> {
536 match self {
537 Self::Disk(rm) => rm.close_all().await,
538 Self::RamOnly(_) => Ok(()), }
540 }
541
542 #[must_use]
551 #[expect(
552 clippy::similar_names,
553 reason = "`pois` vs `pos` are semantically distinct"
554 )]
555 #[expect(
556 clippy::too_many_lines,
557 reason = "chunk save preparation keeps related serialization setup in one pass"
558 )]
559 pub(crate) fn prepare_chunk_save(
560 chunk: &Chunk,
561 status: ChunkStatus,
562 runtime_entities: &[SharedEntity],
563 force: bool,
564 ) -> Option<PreparedChunkSave> {
565 let has_full_runtime = chunk.full_runtime().is_some();
566 assert!(
567 status != ChunkStatus::Full || has_full_runtime,
568 "a chunk persisted as Full must have its Full runtime state initialized"
569 );
570 if status != ChunkStatus::Full && has_full_runtime {
571 tracing::debug!(
572 chunk = ?chunk.pos(),
573 ?status,
574 "Abandoning chunk save preparation: the chunk was promoted while it was snapshotted"
575 );
576 return None;
577 }
578 if !force && !chunk.is_dirty() {
579 return None;
580 }
581
582 let pos = chunk.pos();
583
584 let full = (status == ChunkStatus::Full).then(|| FullChunkRef::from_full_context(chunk));
585 let (block_entities, pending_block_entities) = if full.is_some() {
586 chunk.block_entities.save_snapshot()
587 } else {
588 chunk
589 .block_entities
590 .save_snapshot_without_lifecycle_filter()
591 };
592
593 let mut seen_entity_ids = FxHashSet::default();
594 let mut seen_entity_uuids = FxHashSet::default();
595 let mut entities = Vec::new();
596 for entity in if full.is_some() {
597 Vec::new()
598 } else {
599 chunk.get_saveable_entities()
600 } {
601 if !Self::entity_position_is_finite(entity.as_ref()) {
602 Self::warn_skipping_non_finite_entity(entity.as_ref());
603 continue;
604 }
605 if seen_entity_ids.insert(entity.id()) {
606 Self::assert_unique_save_uuid(
607 &mut seen_entity_uuids,
608 entity.uuid(),
609 entity.id(),
610 pos,
611 );
612 entities.push(entity);
613 }
614 }
615 let mut handled_runtime_entity_ids = Vec::new();
616 for entity in runtime_entities {
617 handled_runtime_entity_ids.push(entity.id());
618 if !Self::entity_position_is_finite(entity.as_ref()) {
619 Self::warn_skipping_non_finite_entity(entity.as_ref());
620 continue;
621 }
622 if seen_entity_ids.insert(entity.id()) {
623 Self::assert_unique_save_uuid(
624 &mut seen_entity_uuids,
625 entity.uuid(),
626 entity.id(),
627 pos,
628 );
629 entities.push(Arc::clone(entity));
630 }
631 }
632
633 let (block_ticks, fluid_ticks) = if let Some(full) = full {
635 let snapshot = full.scheduled_tick_snapshot();
636 let bt = Self::block_ticks_to_persistent(snapshot.block, pos);
637 let ft = Self::fluid_ticks_to_persistent(snapshot.fluid, pos);
638 (bt, ft)
639 } else {
640 let Some(snapshot) = chunk.scheduled_ticks.snapshot(0) else {
643 tracing::debug!(
644 chunk = ?pos,
645 "Abandoning chunk save preparation: the proto scheduled-tick container was \
646 finalized while the chunk was snapshotted"
647 );
648 return None;
649 };
650 let bt = Self::block_ticks_to_persistent(snapshot.block, pos);
651 let ft = Self::fluid_ticks_to_persistent(snapshot.fluid, pos);
652 (bt, ft)
653 };
654
655 let heightmaps = chunk.heightmaps.read();
657 if full.is_some() {
658 for &heightmap_type in HeightmapType::final_types() {
659 let _ = heightmaps.get_final(heightmap_type);
660 }
661 }
662 let heightmaps = Self::heightmaps_to_persistent(&heightmaps, status);
663
664 let light = Self::light_to_persistent(&chunk.light.read());
665
666 let structure_starts = Self::structure_starts_to_persistent(&chunk.structure_starts());
668 let structure_references =
669 Self::structure_references_to_persistent(&chunk.structure_references());
670
671 let pois = full.map_or_default(|full| Self::pois_to_persistent(full, pos));
673
674 let postprocessing = if let Some(full) = full {
675 full.postprocessing_for_serialization()
676 } else {
677 chunk.postprocessing.lock().iter().map(Vec::clone).collect()
678 };
679
680 let persistent = Self::to_persistent(
681 chunk.sections(),
682 &block_entities,
683 &pending_block_entities,
684 &entities,
685 block_ticks,
686 fluid_ticks,
687 heightmaps,
688 light,
689 postprocessing,
690 structure_starts,
691 structure_references,
692 pois,
693 pos,
694 );
695
696 Some(PreparedChunkSave {
697 pos,
698 status,
699 persistent,
700 handled_runtime_entity_ids,
701 })
702 }
703
704 fn entity_position_is_finite(entity: &dyn Entity) -> bool {
705 let pos = entity.position();
706 pos.x.is_finite() && pos.y.is_finite() && pos.z.is_finite()
707 }
708
709 fn warn_skipping_non_finite_entity(entity: &dyn Entity) {
710 tracing::warn!(
711 uuid = ?entity.uuid(),
712 "Entity has non-finite position {:?}, skipping save",
713 entity.position()
714 );
715 }
716
717 fn assert_unique_save_uuid(
718 seen_uuids: &mut FxHashSet<uuid::Uuid>,
719 uuid: uuid::Uuid,
720 entity_id: i32,
721 chunk_pos: ChunkPos,
722 ) {
723 assert!(
724 seen_uuids.insert(uuid),
725 "duplicate saveable entity uuid {uuid} while preparing chunk {chunk_pos:?} for save; latest entity id {entity_id}"
726 );
727 }
728
729 #[expect(
731 clippy::too_many_arguments,
732 clippy::similar_names,
733 reason = "chunk serialization requires all fields; `block_ticks`/`fluid_ticks` are distinct"
734 )]
735 fn to_persistent(
736 sections: &Sections,
737 block_entities: &[SharedBlockEntity],
738 pending_block_entities: &[BlockPos],
739 entities: &[SharedEntity],
740 block_ticks: Vec<PersistentTick>,
741 fluid_ticks: Vec<PersistentTick>,
742 heightmaps: Vec<PersistentHeightmap>,
743 light: PersistentLightData,
744 postprocessing: Vec<Vec<u16>>,
745 structure_starts: Vec<PersistentStructureStart>,
746 structure_references: Vec<PersistentStructureReference>,
747 pois: Vec<PersistentPoi>,
748 chunk_pos: ChunkPos,
749 ) -> PersistentChunk<'static> {
750 let mut builder = ChunkBuilder::new(®ISTRY);
751
752 let persistent_sections = sections
753 .sections
754 .iter()
755 .map(|section| Self::section_to_persistent(section, &mut builder))
756 .collect();
757
758 let persistent_block_entities: Vec<PersistentBlockEntity> = block_entities
760 .iter()
761 .map(|entity| {
762 let pos = entity.get_block_pos();
763
764 let nbt = entity.save_without_metadata();
766 let mut nbt_bytes = Vec::new();
767 nbt.write(&mut nbt_bytes);
768
769 PersistentBlockEntity {
770 x: (pos.0.x - chunk_pos.0.x * 16) as u8,
771 y: pos.0.y as i16,
772 z: (pos.0.z - chunk_pos.0.y * 16) as u8,
773 entity_type: Some(entity.get_type().key.clone()),
774 nbt_data: nbt_bytes,
775 }
776 })
777 .chain(
778 pending_block_entities
779 .iter()
780 .map(|pos| PersistentBlockEntity {
781 x: (pos.0.x - chunk_pos.0.x * 16) as u8,
782 y: pos.0.y as i16,
783 z: (pos.0.z - chunk_pos.0.y * 16) as u8,
784 entity_type: None,
785 nbt_data: Vec::new(),
786 }),
787 )
788 .collect();
789
790 let persistent_entities = Self::entities_to_persistent(entities);
791
792 PersistentChunk {
793 last_modified: SystemTime::now()
794 .duration_since(UNIX_EPOCH)
795 .map_or(0, |d| d.as_secs() as u32),
796 block_states: builder.block_states,
797 biomes: builder.biomes,
798 sections: persistent_sections,
799 block_entities: persistent_block_entities,
800 entities: persistent_entities,
801 block_ticks,
802 fluid_ticks,
803 heightmaps,
804 light,
805 postprocessing,
806 structure_starts,
807 structure_references,
808 pois,
809 }
810 }
811}