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, 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 ENTITY_LOAD_MAX_HORIZONTAL_POSITION: f64 = 3.000_051_2E7;
74const ENTITY_LOAD_MAX_VERTICAL_POSITION: f64 = 2.0E7;
75
76const fn direction_to_2d(dir: Option<Direction>) -> i8 {
79 match dir {
80 Some(Direction::South) => 0,
81 Some(Direction::West) => 1,
82 Some(Direction::North) => 2,
83 Some(Direction::East) => 3,
84 None | Some(Direction::Down | Direction::Up) => -1,
85 }
86}
87
88const fn direction_from_2d(value: i8) -> Option<Direction> {
90 match value {
91 0 => Some(Direction::South),
92 1 => Some(Direction::West),
93 2 => Some(Direction::North),
94 3 => Some(Direction::East),
95 _ => None,
96 }
97}
98
99const fn required_direction_from_2d(value: i8) -> Direction {
100 match value {
101 1 => Direction::West,
102 2 => Direction::North,
103 3 => Direction::East,
104 _ => Direction::South,
105 }
106}
107
108const fn mineshaft_type_to_persistent(mineshaft_type: MineshaftType) -> i8 {
109 match mineshaft_type {
110 MineshaftType::Normal => 0,
111 MineshaftType::Mesa => 1,
112 }
113}
114
115const fn mineshaft_type_from_persistent(value: i8) -> MineshaftType {
116 match value {
117 1 => MineshaftType::Mesa,
118 _ => MineshaftType::Normal,
119 }
120}
121
122const fn projection_to_persistent(projection: Option<Projection>) -> i8 {
123 match projection {
124 None => -1,
125 Some(Projection::Rigid) => 0,
126 Some(Projection::TerrainMatching) => 1,
127 }
128}
129
130const fn projection_from_persistent(value: i8) -> Option<Projection> {
131 match value {
132 0 => Some(Projection::Rigid),
133 1 => Some(Projection::TerrainMatching),
134 _ => None,
135 }
136}
137
138const fn required_projection_from_persistent(value: i8) -> Projection {
139 match value {
140 1 => Projection::TerrainMatching,
141 _ => Projection::Rigid,
142 }
143}
144
145const fn rotation_to_persistent(rotation: Rotation) -> i8 {
146 match rotation {
147 Rotation::None => 0,
148 Rotation::Clockwise90 => 1,
149 Rotation::Clockwise180 => 2,
150 Rotation::CounterClockwise90 => 3,
151 }
152}
153
154const fn rotation_from_persistent(value: i8) -> Rotation {
155 match value {
156 1 => Rotation::Clockwise90,
157 2 => Rotation::Clockwise180,
158 3 => Rotation::CounterClockwise90,
159 _ => Rotation::None,
160 }
161}
162
163const fn liquid_settings_to_persistent(settings: LiquidSettingsData) -> i8 {
164 match settings {
165 LiquidSettingsData::ApplyWaterlogging => 0,
166 LiquidSettingsData::IgnoreWaterlogging => 1,
167 }
168}
169
170const fn liquid_settings_from_persistent(value: i8) -> LiquidSettingsData {
171 match value {
172 1 => LiquidSettingsData::IgnoreWaterlogging,
173 _ => LiquidSettingsData::ApplyWaterlogging,
174 }
175}
176
177const fn ruined_portal_placement_to_persistent(placement: RuinedPortalPlacementData) -> i8 {
178 match placement {
179 RuinedPortalPlacementData::OnLandSurface => 0,
180 RuinedPortalPlacementData::PartlyBuried => 1,
181 RuinedPortalPlacementData::Underground => 2,
182 RuinedPortalPlacementData::InMountain => 3,
183 RuinedPortalPlacementData::OnOceanFloor => 4,
184 RuinedPortalPlacementData::InNether => 5,
185 }
186}
187
188const fn ruined_portal_placement_from_persistent(value: i8) -> RuinedPortalPlacementData {
189 match value {
190 1 => RuinedPortalPlacementData::PartlyBuried,
191 2 => RuinedPortalPlacementData::Underground,
192 3 => RuinedPortalPlacementData::InMountain,
193 4 => RuinedPortalPlacementData::OnOceanFloor,
194 5 => RuinedPortalPlacementData::InNether,
195 _ => RuinedPortalPlacementData::OnLandSurface,
196 }
197}
198
199const fn mirror_to_persistent(mirror: StructureMirror) -> i8 {
200 match mirror {
201 StructureMirror::None => 0,
202 StructureMirror::FrontBack => 1,
203 StructureMirror::LeftRight => 2,
204 }
205}
206
207const fn mirror_from_persistent(value: i8) -> StructureMirror {
208 match value {
209 1 => StructureMirror::FrontBack,
210 2 => StructureMirror::LeftRight,
211 _ => StructureMirror::None,
212 }
213}
214
215const fn block_ignore_to_persistent(block_ignore: StructureBlockIgnore) -> i8 {
216 match block_ignore {
217 StructureBlockIgnore::None => 0,
218 StructureBlockIgnore::StructureBlock => 1,
219 StructureBlockIgnore::StructureAndAir => 2,
220 }
221}
222
223const fn block_ignore_from_persistent(value: i8) -> StructureBlockIgnore {
224 match value {
225 1 => StructureBlockIgnore::StructureBlock,
226 2 => StructureBlockIgnore::StructureAndAir,
227 _ => StructureBlockIgnore::None,
228 }
229}
230
231const fn marker_handling_to_persistent(marker_handling: TemplateMarkerHandling) -> i8 {
232 match marker_handling {
233 TemplateMarkerHandling::Ignore => 0,
234 TemplateMarkerHandling::DataMarkers => 1,
235 TemplateMarkerHandling::Shipwreck => 2,
236 TemplateMarkerHandling::Igloo => 3,
237 TemplateMarkerHandling::OceanRuin { is_large: false } => 4,
238 TemplateMarkerHandling::OceanRuin { is_large: true } => 5,
239 TemplateMarkerHandling::EndCity => 6,
240 TemplateMarkerHandling::WoodlandMansion => 7,
241 }
242}
243
244const fn marker_handling_from_persistent(value: i8) -> TemplateMarkerHandling {
245 match value {
246 1 => TemplateMarkerHandling::DataMarkers,
247 2 => TemplateMarkerHandling::Shipwreck,
248 3 => TemplateMarkerHandling::Igloo,
249 4 => TemplateMarkerHandling::OceanRuin { is_large: false },
250 5 => TemplateMarkerHandling::OceanRuin { is_large: true },
251 6 => TemplateMarkerHandling::EndCity,
252 7 => TemplateMarkerHandling::WoodlandMansion,
253 _ => TemplateMarkerHandling::Ignore,
254 }
255}
256
257const fn ocean_ruin_biome_temp_to_persistent(biome_temp: OceanRuinBiomeTempData) -> i8 {
258 match biome_temp {
259 OceanRuinBiomeTempData::Warm => 0,
260 OceanRuinBiomeTempData::Cold => 1,
261 }
262}
263
264const fn ocean_ruin_biome_temp_from_persistent(value: i8) -> OceanRuinBiomeTempData {
265 match value {
266 1 => OceanRuinBiomeTempData::Cold,
267 _ => OceanRuinBiomeTempData::Warm,
268 }
269}
270
271const fn placement_adjustment_to_persistent(
272 adjustment: TemplatePlacementAdjustment,
273) -> PersistentTemplatePlacementAdjustment {
274 match adjustment {
275 TemplatePlacementAdjustment::None => PersistentTemplatePlacementAdjustment::None,
276 TemplatePlacementAdjustment::Shipwreck {
277 is_beached,
278 height_adjusted,
279 } => PersistentTemplatePlacementAdjustment::Shipwreck {
280 is_beached,
281 height_adjusted,
282 },
283 TemplatePlacementAdjustment::Igloo { template_offset } => {
284 PersistentTemplatePlacementAdjustment::Igloo {
285 template_offset: [template_offset.0, template_offset.1, template_offset.2],
286 }
287 }
288 TemplatePlacementAdjustment::OceanRuin => PersistentTemplatePlacementAdjustment::OceanRuin,
289 }
290}
291
292const fn placement_adjustment_from_persistent(
293 adjustment: &PersistentTemplatePlacementAdjustment,
294) -> TemplatePlacementAdjustment {
295 match adjustment {
296 PersistentTemplatePlacementAdjustment::None => TemplatePlacementAdjustment::None,
297 PersistentTemplatePlacementAdjustment::Shipwreck {
298 is_beached,
299 height_adjusted,
300 } => TemplatePlacementAdjustment::Shipwreck {
301 is_beached: *is_beached,
302 height_adjusted: *height_adjusted,
303 },
304 PersistentTemplatePlacementAdjustment::Igloo { template_offset } => {
305 TemplatePlacementAdjustment::Igloo {
306 template_offset: (template_offset[0], template_offset[1], template_offset[2]),
307 }
308 }
309 PersistentTemplatePlacementAdjustment::OceanRuin => TemplatePlacementAdjustment::OceanRuin,
310 }
311}
312
313const fn placement_clip_to_persistent(placement_clip: TemplatePlacementClip) -> i8 {
314 match placement_clip {
315 TemplatePlacementClip::CenterChunk => 0,
316 TemplatePlacementClip::CenterChunkExpandedToTemplate => 1,
317 TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate => 2,
318 }
319}
320
321const fn placement_clip_from_persistent(value: i8) -> TemplatePlacementClip {
322 match value {
323 1 => TemplatePlacementClip::CenterChunkExpandedToTemplate,
324 2 => TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate,
325 _ => TemplatePlacementClip::CenterChunk,
326 }
327}
328
329const fn post_process_to_persistent(post_process: TemplatePostProcess) -> i8 {
330 match post_process {
331 TemplatePostProcess::None => 0,
332 TemplatePostProcess::NetherFossil => 1,
333 TemplatePostProcess::IglooTop => 2,
334 TemplatePostProcess::RuinedPortal => 3,
335 }
336}
337
338const fn post_process_from_persistent(value: i8) -> TemplatePostProcess {
339 match value {
340 1 => TemplatePostProcess::NetherFossil,
341 2 => TemplatePostProcess::IglooTop,
342 3 => TemplatePostProcess::RuinedPortal,
343 _ => TemplatePostProcess::None,
344 }
345}
346
347fn compare_identifiers(a: &Identifier, b: &Identifier) -> CmpOrdering {
348 a.namespace
349 .cmp(&b.namespace)
350 .then_with(|| a.path.cmp(&b.path))
351}
352
353fn homogeneous_packed_light_value(data: &[u8; DATA_LAYER_SIZE]) -> Option<u8> {
354 let first = data[0];
355 let value = first & 0x0F;
356 if first >> 4 != value {
357 return None;
358 }
359 data.iter().all(|byte| *byte == first).then_some(value)
360}
361
362#[derive(Clone, Copy)]
363enum EntityPersistenceMode {
364 ChunkSave,
365 DimensionTransition,
366}
367
368use super::ram_only::RamOnlyStorage;
369use super::region_manager::RegionManager;
370use super::{
371 PersistentBiomeData, PersistentBlockEntity, PersistentBlockState, PersistentBoundingBox,
372 PersistentChunk, PersistentDesertPyramidPieceData, PersistentEntity, PersistentHeightmap,
373 PersistentJigsawJunction, PersistentJigsawPieceData, PersistentJungleTemplePieceData,
374 PersistentLightData, PersistentLightSection, PersistentMineshaftPieceData,
375 PersistentMineshaftPieceKind, PersistentNetherFortressPieceData,
376 PersistentOceanMonumentChildPiece, PersistentOceanMonumentChildPieceKind,
377 PersistentOceanMonumentPieceData, PersistentOceanMonumentRoomData, PersistentPoi,
378 PersistentPoolElement, PersistentProceduralPieceData, PersistentProcessorList,
379 PersistentSection, PersistentStrongholdPieceData, PersistentStrongholdSmallDoorType,
380 PersistentStructurePiece, PersistentStructurePiecePayload, PersistentStructureReference,
381 PersistentStructureStart, PersistentSwampHutPieceData, PersistentTemplatePieceData,
382 PersistentTemplatePlacementAdjustment, PersistentTemplateProcessorList, PersistentTick,
383 PreparedChunkSave,
384};
385
386struct ChunkBuilder<'a> {
388 block_states: Vec<PersistentBlockState<'static>>,
389 biomes: Vec<Identifier>,
390 registry: &'a Registry,
391}
392
393impl<'a> ChunkBuilder<'a> {
394 const fn new(registry: &'a Registry) -> Self {
395 Self {
396 block_states: Vec::new(),
397 biomes: Vec::new(),
398 registry,
399 }
400 }
401
402 fn ensure_block_state(&mut self, block_id: BlockStateId) -> u16 {
404 let block = self
406 .registry
407 .blocks
408 .by_state_id(block_id)
409 .expect("Invalid block state ID");
410 let properties = self.registry.blocks.get_properties(block_id);
411
412 let persistent = PersistentBlockState {
413 name: block.key.clone(),
414 properties,
415 };
416
417 if let Some(idx) = self.block_states.iter().position(|s| s == &persistent) {
419 return idx as u16;
420 }
421
422 let idx = self.block_states.len();
424 self.block_states.push(persistent);
425 idx as u16
426 }
427
428 fn ensure_biome(&mut self, biome_id: u16) -> u16 {
430 let biome = self
432 .registry
433 .biomes
434 .by_id(biome_id as usize)
435 .expect("Invalid biome ID");
436 let identifier = biome.key.clone();
437
438 if let Some(idx) = self.biomes.iter().position(|b| b == &identifier) {
439 return idx as u16;
440 }
441
442 let idx = self.biomes.len();
443 self.biomes.push(identifier);
444 idx as u16
445 }
446}
447
448pub enum ChunkStorage {
454 Disk(RegionManager),
456 RamOnly(RamOnlyStorage),
458}
459
460pub struct LoadedChunk {
462 pub chunk: Chunk,
464 pub status: ChunkStatus,
466 pub pending_entities: Vec<SharedEntity>,
468}
469
470impl ChunkStorage {
471 pub async fn load_chunk(
477 &self,
478 pos: ChunkPos,
479 min_y: i32,
480 height: i32,
481 level: Weak<World>,
482 thread_pool: &rayon::ThreadPool,
483 ) -> io::Result<Option<LoadedChunk>> {
484 match self {
485 Self::Disk(rm) => rm.load_chunk(pos, min_y, height, level, thread_pool).await,
486 Self::RamOnly(ram) => ram.load_chunk(pos, min_y, height, level).await,
487 }
488 }
489
490 pub async fn save_chunk_data(
494 &self,
495 prepared: PreparedChunkSave,
496 thread_pool: &rayon::ThreadPool,
497 ) -> io::Result<bool> {
498 match self {
499 Self::Disk(rm) => rm.save_chunk_data(prepared, thread_pool).await,
500 Self::RamOnly(ram) => ram.save_chunk_data(prepared).await,
501 }
502 }
503
504 pub async fn chunk_exists(&self, pos: ChunkPos) -> io::Result<bool> {
506 match self {
507 Self::Disk(rm) => rm.chunk_exists(pos).await,
508 Self::RamOnly(ram) => ram.chunk_exists(pos).await,
509 }
510 }
511
512 pub async fn acquire_chunk(&self, pos: ChunkPos) -> io::Result<bool> {
517 match self {
518 Self::Disk(rm) => rm.acquire_chunk(pos).await,
519 Self::RamOnly(ram) => ram.chunk_exists(pos).await,
520 }
521 }
522
523 pub async fn release_chunk(&self, pos: ChunkPos) -> io::Result<()> {
525 match self {
526 Self::Disk(rm) => rm.release_chunk(pos).await,
527 Self::RamOnly(_) => Ok(()), }
529 }
530
531 pub async fn flush_all(&self) -> io::Result<()> {
533 match self {
534 Self::Disk(rm) => rm.flush_all().await,
535 Self::RamOnly(_) => Ok(()), }
537 }
538
539 pub async fn close_all(&self) -> io::Result<()> {
541 match self {
542 Self::Disk(rm) => rm.close_all().await,
543 Self::RamOnly(_) => Ok(()), }
545 }
546
547 #[must_use]
563 #[expect(
564 clippy::similar_names,
565 reason = "`pois` vs `pos` are semantically distinct"
566 )]
567 #[expect(
568 clippy::too_many_lines,
569 reason = "chunk save preparation keeps related serialization setup in one pass"
570 )]
571 pub(crate) fn prepare_chunk_save(
572 chunk: &Chunk,
573 status: ChunkStatus,
574 runtime_entities: &[SharedEntity],
575 force: bool,
576 ) -> Option<PreparedChunkSave> {
577 assert_eq!(
578 status == ChunkStatus::Full,
579 chunk.full_runtime().is_some(),
580 "persisted chunk status must match its Full runtime state"
581 );
582 if !force && !chunk.is_dirty() {
583 return None;
584 }
585
586 for section_holder in &chunk.sections().sections {
592 let mut guard = section_holder.write();
593 if matches!(&guard.states, PalettedContainer::Building(_)) {
594 guard.recalculate_counts();
595 }
596 }
597
598 let pos = chunk.pos();
599
600 let full = (status == ChunkStatus::Full).then(|| FullChunkRef::from_full_context(chunk));
601 let (block_entities, pending_block_entities) = if full.is_some() {
602 chunk.block_entities.save_snapshot()
603 } else {
604 chunk
605 .block_entities
606 .save_snapshot_without_lifecycle_filter()
607 };
608
609 let mut seen_entity_ids = FxHashSet::default();
610 let mut seen_entity_uuids = FxHashSet::default();
611 let mut entities = Vec::new();
612 for entity in if full.is_some() {
613 Vec::new()
614 } else {
615 chunk.get_saveable_entities()
616 } {
617 if !Self::entity_position_is_finite(entity.as_ref()) {
618 Self::warn_skipping_non_finite_entity(entity.as_ref());
619 continue;
620 }
621 if seen_entity_ids.insert(entity.id()) {
622 Self::assert_unique_save_uuid(
623 &mut seen_entity_uuids,
624 entity.uuid(),
625 entity.id(),
626 pos,
627 );
628 entities.push(entity);
629 }
630 }
631 let mut handled_runtime_entity_ids = Vec::new();
632 for entity in runtime_entities {
633 handled_runtime_entity_ids.push(entity.id());
634 if !Self::entity_position_is_finite(entity.as_ref()) {
635 Self::warn_skipping_non_finite_entity(entity.as_ref());
636 continue;
637 }
638 if seen_entity_ids.insert(entity.id()) {
639 Self::assert_unique_save_uuid(
640 &mut seen_entity_uuids,
641 entity.uuid(),
642 entity.id(),
643 pos,
644 );
645 entities.push(Arc::clone(entity));
646 }
647 }
648
649 let (block_ticks, fluid_ticks) = if let Some(full) = full {
651 let snapshot = full.scheduled_tick_snapshot();
652 let bt = Self::block_ticks_to_persistent(snapshot.block, pos);
653 let ft = Self::fluid_ticks_to_persistent(snapshot.fluid, pos);
654 (bt, ft)
655 } else {
656 let Some(snapshot) = chunk.scheduled_ticks.snapshot(0) else {
659 panic!("Proto chunk scheduled-tick container was finalized before saving");
660 };
661 let bt = Self::block_ticks_to_persistent(snapshot.block, pos);
662 let ft = Self::fluid_ticks_to_persistent(snapshot.fluid, pos);
663 (bt, ft)
664 };
665
666 let heightmaps = chunk.heightmaps.read();
668 if full.is_some() {
669 for &heightmap_type in HeightmapType::final_types() {
670 let _ = heightmaps.get_final(heightmap_type);
671 }
672 }
673 let heightmaps = Self::heightmaps_to_persistent(&heightmaps, status);
674
675 let light = Self::light_to_persistent(&chunk.light.read());
676
677 let structure_starts = Self::structure_starts_to_persistent(&chunk.structure_starts());
679 let structure_references =
680 Self::structure_references_to_persistent(&chunk.structure_references());
681
682 let pois = full
684 .map(|full| Self::pois_to_persistent(full, pos))
685 .unwrap_or_default();
686
687 let carving_mask = if full.is_some() {
688 None
689 } else {
690 chunk
691 .carving_mask
692 .read()
693 .as_ref()
694 .map(CarvingMask::to_packed_u64s)
695 };
696
697 let postprocessing = if let Some(full) = full {
698 full.postprocessing_for_serialization()
699 } else {
700 chunk.postprocessing.lock().iter().map(Vec::clone).collect()
701 };
702
703 let persistent = Self::to_persistent(
704 chunk.sections(),
705 &block_entities,
706 &pending_block_entities,
707 &entities,
708 block_ticks,
709 fluid_ticks,
710 heightmaps,
711 light,
712 carving_mask,
713 postprocessing,
714 structure_starts,
715 structure_references,
716 pois,
717 pos,
718 );
719
720 Some(PreparedChunkSave {
721 pos,
722 status,
723 persistent,
724 handled_runtime_entity_ids,
725 })
726 }
727
728 fn entity_position_is_finite(entity: &dyn Entity) -> bool {
729 let pos = entity.position();
730 pos.x.is_finite() && pos.y.is_finite() && pos.z.is_finite()
731 }
732
733 fn warn_skipping_non_finite_entity(entity: &dyn Entity) {
734 tracing::warn!(
735 uuid = ?entity.uuid(),
736 "Entity has non-finite position {:?}, skipping save",
737 entity.position()
738 );
739 }
740
741 fn assert_unique_save_uuid(
742 seen_uuids: &mut FxHashSet<uuid::Uuid>,
743 uuid: uuid::Uuid,
744 entity_id: i32,
745 chunk_pos: ChunkPos,
746 ) {
747 assert!(
748 seen_uuids.insert(uuid),
749 "duplicate saveable entity uuid {uuid} while preparing chunk {chunk_pos:?} for save; latest entity id {entity_id}"
750 );
751 }
752
753 #[expect(
755 clippy::too_many_arguments,
756 clippy::similar_names,
757 reason = "chunk serialization requires all fields; `block_ticks`/`fluid_ticks` are distinct"
758 )]
759 fn to_persistent(
760 sections: &Sections,
761 block_entities: &[SharedBlockEntity],
762 pending_block_entities: &[BlockPos],
763 entities: &[SharedEntity],
764 block_ticks: Vec<PersistentTick>,
765 fluid_ticks: Vec<PersistentTick>,
766 heightmaps: Vec<PersistentHeightmap>,
767 light: PersistentLightData,
768 carving_mask: Option<Vec<u64>>,
769 postprocessing: Vec<Vec<u16>>,
770 structure_starts: Vec<PersistentStructureStart>,
771 structure_references: Vec<PersistentStructureReference>,
772 pois: Vec<PersistentPoi>,
773 chunk_pos: ChunkPos,
774 ) -> PersistentChunk<'static> {
775 let mut builder = ChunkBuilder::new(®ISTRY);
776
777 let persistent_sections = sections
778 .sections
779 .iter()
780 .map(|section| Self::section_to_persistent(section, &mut builder))
781 .collect();
782
783 let persistent_block_entities: Vec<PersistentBlockEntity> = block_entities
785 .iter()
786 .map(|entity| {
787 let pos = entity.get_block_pos();
788
789 let mut nbt = NbtCompound::new();
791 entity.save_additional(&mut nbt);
792 let mut nbt_bytes = Vec::new();
793 nbt.write(&mut nbt_bytes);
794
795 PersistentBlockEntity {
796 x: (pos.0.x - chunk_pos.0.x * 16) as u8,
797 y: pos.0.y as i16,
798 z: (pos.0.z - chunk_pos.0.y * 16) as u8,
799 entity_type: Some(entity.get_type().key.clone()),
800 nbt_data: nbt_bytes,
801 }
802 })
803 .chain(
804 pending_block_entities
805 .iter()
806 .map(|pos| PersistentBlockEntity {
807 x: (pos.0.x - chunk_pos.0.x * 16) as u8,
808 y: pos.0.y as i16,
809 z: (pos.0.z - chunk_pos.0.y * 16) as u8,
810 entity_type: None,
811 nbt_data: Vec::new(),
812 }),
813 )
814 .collect();
815
816 let persistent_entities = Self::entities_to_persistent(entities);
817
818 PersistentChunk {
819 last_modified: SystemTime::now()
820 .duration_since(UNIX_EPOCH)
821 .map_or(0, |d| d.as_secs() as u32),
822 block_states: builder.block_states,
823 biomes: builder.biomes,
824 sections: persistent_sections,
825 block_entities: persistent_block_entities,
826 entities: persistent_entities,
827 block_ticks,
828 fluid_ticks,
829 heightmaps,
830 light,
831 carving_mask,
832 postprocessing,
833 structure_starts,
834 structure_references,
835 pois,
836 }
837 }
838}