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