1use super::{
2 BlockPos, BlockStateId, BlockTickList, CarvingMask, Chunk, ChunkBuilder, ChunkHeightmaps,
3 ChunkPos, ChunkSection, ChunkStatus, ChunkStorage, DATA_LAYER_SIZE, FluidTickList,
4 FullChunkRef, FxHashSet, Heightmap, HeightmapType, LoadedChunk, Ordering, PalettedContainer,
5 PersistentBiomeData, PersistentChunk, PersistentHeightmap, PersistentLightSection,
6 PersistentPoi, PersistentSection, REGISTRY, RegistryExt, SectionHolder, Sections, Weak, World,
7 bits_for_palette_len, io, pack_indices, unpack_indices,
8};
9
10impl ChunkStorage {
11 fn invalid_chunk_data(message: impl Into<String>) -> io::Error {
12 io::Error::new(io::ErrorKind::InvalidData, message.into())
13 }
14
15 fn validate_packed_palette(
16 palette: &[u16],
17 bits_per_entry: u8,
18 data: &[u64],
19 entry_count: usize,
20 global_palette_len: usize,
21 name: &str,
22 ) -> io::Result<()> {
23 let Some(expected_bits) = bits_for_palette_len(palette.len()) else {
24 return Err(Self::invalid_chunk_data(format!(
25 "heterogeneous {name} palette has fewer than two entries"
26 )));
27 };
28 if bits_per_entry != expected_bits {
29 return Err(Self::invalid_chunk_data(format!(
30 "heterogeneous {name} palette uses {bits_per_entry} bits, expected {expected_bits}"
31 )));
32 }
33 let values_per_word = 64 / usize::from(bits_per_entry);
34 let expected_words = entry_count.div_ceil(values_per_word);
35 if data.len() != expected_words {
36 return Err(Self::invalid_chunk_data(format!(
37 "heterogeneous {name} data has {} words, expected {expected_words}",
38 data.len()
39 )));
40 }
41 if let Some(invalid) = palette
42 .iter()
43 .find(|&&index| usize::from(index) >= global_palette_len)
44 {
45 return Err(Self::invalid_chunk_data(format!(
46 "{name} palette references missing global entry {invalid}"
47 )));
48 }
49 if let Some(invalid) = unpack_indices(data, bits_per_entry)
50 .take(entry_count)
51 .find(|&index| index as usize >= palette.len())
52 {
53 return Err(Self::invalid_chunk_data(format!(
54 "{name} data references missing local palette entry {invalid}"
55 )));
56 }
57 Ok(())
58 }
59
60 #[expect(
61 clippy::too_many_lines,
62 reason = "keeps complete chunk payload validation in one ordered pass"
63 )]
64 fn validate_persistent_chunk(
65 persistent: &PersistentChunk<'_>,
66 status: ChunkStatus,
67 min_y: i32,
68 height: i32,
69 ) -> io::Result<()> {
70 if height <= 0 || height % 16 != 0 || min_y % 16 != 0 {
71 return Err(io::Error::new(
72 io::ErrorKind::InvalidInput,
73 format!(
74 "chunk world range must be section-aligned, got min_y={min_y}, height={height}"
75 ),
76 ));
77 }
78 let expected_sections = (height / 16) as usize;
79 if persistent.sections.len() != expected_sections {
80 return Err(Self::invalid_chunk_data(format!(
81 "chunk has {} sections, expected {expected_sections}",
82 persistent.sections.len()
83 )));
84 }
85
86 for (section_index, section) in persistent.sections.iter().enumerate() {
87 let biomes = match section {
88 PersistentSection::Homogeneous {
89 block_state,
90 biomes,
91 } => {
92 if usize::from(*block_state) >= persistent.block_states.len() {
93 return Err(Self::invalid_chunk_data(format!(
94 "section {section_index} references missing block-state entry {block_state}"
95 )));
96 }
97 biomes
98 }
99 PersistentSection::Heterogeneous {
100 palette,
101 bits_per_entry,
102 block_data,
103 biomes,
104 } => {
105 Self::validate_packed_palette(
106 palette,
107 *bits_per_entry,
108 block_data,
109 4096,
110 persistent.block_states.len(),
111 "block-state",
112 )?;
113 biomes
114 }
115 };
116 match biomes {
117 PersistentBiomeData::Homogeneous { biome } => {
118 if usize::from(*biome) >= persistent.biomes.len() {
119 return Err(Self::invalid_chunk_data(format!(
120 "section {section_index} references missing biome entry {biome}"
121 )));
122 }
123 }
124 PersistentBiomeData::Heterogeneous {
125 palette,
126 bits_per_entry,
127 biome_data,
128 } => Self::validate_packed_palette(
129 palette,
130 *bits_per_entry,
131 biome_data,
132 64,
133 persistent.biomes.len(),
134 "biome",
135 )?,
136 }
137 }
138
139 let mut heightmap_types = FxHashSet::default();
140 for heightmap in &persistent.heightmaps {
141 let Some(heightmap_type) = HeightmapType::from_persistence_id(heightmap.heightmap_type)
142 else {
143 return Err(Self::invalid_chunk_data(format!(
144 "unknown heightmap type {}",
145 heightmap.heightmap_type
146 )));
147 };
148 if heightmap.data.len() != 256 {
149 return Err(Self::invalid_chunk_data(format!(
150 "{heightmap_type:?} heightmap has {} columns, expected 256",
151 heightmap.data.len()
152 )));
153 }
154 if !heightmap_types.insert(heightmap_type) {
155 return Err(Self::invalid_chunk_data(format!(
156 "duplicate {heightmap_type:?} heightmap"
157 )));
158 }
159 if let Some(value) = heightmap
160 .data
161 .iter()
162 .find(|&&value| i32::from(value) > height)
163 {
164 return Err(Self::invalid_chunk_data(format!(
165 "{heightmap_type:?} heightmap contains out-of-range value {value} for height {height}"
166 )));
167 }
168 }
169
170 let light_section_count = expected_sections + 2;
171 for (layer_name, sections) in [
172 ("block", persistent.light.block.as_slice()),
173 ("sky", persistent.light.sky.as_slice()),
174 ] {
175 let mut indices = FxHashSet::default();
176 for section in sections {
177 let index = usize::try_from(section.section_index()).map_err(|_| {
178 Self::invalid_chunk_data(format!(
179 "{layer_name} light section index does not fit this platform"
180 ))
181 })?;
182 if index >= light_section_count {
183 return Err(Self::invalid_chunk_data(format!(
184 "{layer_name} light section index {index} is outside 0..{light_section_count}"
185 )));
186 }
187 if !indices.insert(index) {
188 return Err(Self::invalid_chunk_data(format!(
189 "duplicate {layer_name} light section {index}"
190 )));
191 }
192 match section {
193 PersistentLightSection::Initialized { data, .. }
194 | PersistentLightSection::Internal { data, .. }
195 if data.len() != DATA_LAYER_SIZE =>
196 {
197 return Err(Self::invalid_chunk_data(format!(
198 "{layer_name} light section {index} has {} bytes, expected {DATA_LAYER_SIZE}",
199 data.len()
200 )));
201 }
202 _ => {}
203 }
204 }
205 }
206
207 let max_y = min_y.checked_add(height).ok_or_else(|| {
208 io::Error::new(io::ErrorKind::InvalidInput, "world height range overflowed")
209 })?;
210 for (name, x, y, z) in persistent
211 .block_entities
212 .iter()
213 .map(|entry| ("block entity", entry.x, entry.y, entry.z))
214 .chain(
215 persistent
216 .block_ticks
217 .iter()
218 .map(|entry| ("block tick", entry.x, entry.y, entry.z)),
219 )
220 .chain(
221 persistent
222 .fluid_ticks
223 .iter()
224 .map(|entry| ("fluid tick", entry.x, entry.y, entry.z)),
225 )
226 .chain(
227 persistent
228 .pois
229 .iter()
230 .map(|entry| ("POI", entry.x, entry.y, entry.z)),
231 )
232 {
233 let y = i32::from(y);
234 if x >= 16 || z >= 16 || y < min_y || y >= max_y {
235 return Err(Self::invalid_chunk_data(format!(
236 "{name} position ({x}, {y}, {z}) is outside the chunk"
237 )));
238 }
239 }
240
241 if status == ChunkStatus::Full && persistent.carving_mask.is_some() {
242 return Err(Self::invalid_chunk_data(
243 "Full chunk contains a proto carving mask",
244 ));
245 }
246 if let Some(mask) = &persistent.carving_mask {
247 let max_words = (256usize * height as usize).div_ceil(64);
248 if mask.len() > max_words {
249 return Err(Self::invalid_chunk_data(format!(
250 "carving mask has {} words, maximum is {max_words}",
251 mask.len()
252 )));
253 }
254 }
255 if persistent.postprocessing.len() > expected_sections {
256 return Err(Self::invalid_chunk_data(format!(
257 "chunk has {} postprocessing section lists, maximum is {expected_sections}",
258 persistent.postprocessing.len()
259 )));
260 }
261
262 Ok(())
263 }
264
265 pub(super) fn section_to_persistent(
267 section: &SectionHolder,
268 builder: &mut ChunkBuilder,
269 ) -> PersistentSection {
270 let section = section.read();
271 let biomes = Self::biomes_to_persistent(§ion.biomes, builder);
272
273 match §ion.states {
274 PalettedContainer::Homogeneous(block_id) => {
275 let block_idx = builder.ensure_block_state(*block_id);
276 PersistentSection::Homogeneous {
277 block_state: block_idx,
278 biomes,
279 }
280 }
281 PalettedContainer::Heterogeneous(data) => {
282 let palette: Vec<u16> = data
284 .palette
285 .iter()
286 .map(|(block_id, _)| builder.ensure_block_state(*block_id))
287 .collect();
288
289 let bits = bits_for_palette_len(palette.len())
291 .expect("Heterogeneous section should have palette length >= 2");
292 let indices: Vec<u32> = data
293 .cube
294 .iter()
295 .flatten()
296 .flatten()
297 .map(|block_id| {
298 data.palette
299 .iter()
300 .position(|(v, _)| v == block_id)
301 .unwrap_or(0) as u32
302 })
303 .collect();
304
305 let block_data = pack_indices(&indices, bits);
306
307 PersistentSection::Heterogeneous {
308 palette,
309 bits_per_entry: bits,
310 block_data,
311 biomes,
312 }
313 }
314 PalettedContainer::Building(_) => panic!(
315 "section_to_persistent called on a section still in worldgen Building mode; \
316 finalize_building must be called before serialization"
317 ),
318 }
319 }
320
321 pub(super) fn biomes_to_persistent(
323 biomes: &PalettedContainer<u16, 4>,
324 builder: &mut ChunkBuilder,
325 ) -> PersistentBiomeData {
326 match biomes {
327 PalettedContainer::Homogeneous(biome_id) => {
328 let biome_idx = builder.ensure_biome(*biome_id);
329 PersistentBiomeData::Homogeneous { biome: biome_idx }
330 }
331 PalettedContainer::Heterogeneous(data) => {
332 let palette: Vec<u16> = data
334 .palette
335 .iter()
336 .map(|(biome_id, _)| builder.ensure_biome(*biome_id))
337 .collect();
338
339 let bits = bits_for_palette_len(palette.len())
340 .expect("Heterogeneous biome data should have palette length >= 2");
341 let indices: Vec<u32> = data
342 .cube
343 .iter()
344 .flatten()
345 .flatten()
346 .map(|biome_id| {
347 data.palette
348 .iter()
349 .position(|(v, _)| v == biome_id)
350 .unwrap_or(0) as u32
351 })
352 .collect();
353
354 let biome_data = pack_indices(&indices, bits);
355
356 PersistentBiomeData::Heterogeneous {
357 palette,
358 bits_per_entry: bits,
359 biome_data,
360 }
361 }
362 PalettedContainer::Building(_) => panic!(
363 "biomes_to_persistent called on a section still in worldgen Building mode; \
364 finalize_building must be called before serialization"
365 ),
366 }
367 }
368
369 #[expect(
380 clippy::too_many_lines,
381 reason = "chunk persistence conversion is a linear field-by-field transform"
382 )]
383 pub(crate) fn try_persistent_to_chunk(
384 persistent: &PersistentChunk<'_>,
385 pos: ChunkPos,
386 status: ChunkStatus,
387 min_y: i32,
388 height: i32,
389 level: Weak<World>,
390 ) -> io::Result<LoadedChunk> {
391 Self::validate_persistent_chunk(persistent, status, min_y, height)?;
395 let sections: Vec<ChunkSection> = persistent
396 .sections
397 .iter()
398 .map(|section| Self::persistent_to_section(section, persistent))
399 .collect::<io::Result<_>>()?;
400 let sections = Sections::from_owned(sections.into_boxed_slice());
401
402 let structure_starts = Self::persistent_to_structure_starts(&persistent.structure_starts);
404 let structure_references =
405 Self::persistent_to_structure_references(&persistent.structure_references);
406 let light = Self::persistent_to_light(&persistent.light, min_y, height, status);
407 let mut heightmaps =
408 Self::persistent_to_heightmaps(&persistent.heightmaps, status, min_y, height);
409 heightmaps.prime_from_sections(
410 status.heightmaps_after(),
411 min_y,
412 height,
413 §ions.sections,
414 );
415
416 if status == ChunkStatus::Full {
417 let block_ticks = BlockTickList::from_saved_ticks(
419 Self::persistent_to_block_saved_ticks(&persistent.block_ticks, pos),
420 );
421 let fluid_ticks = FluidTickList::from_saved_ticks(
422 Self::persistent_to_fluid_saved_ticks(&persistent.fluid_ticks, pos),
423 );
424
425 let chunk = Chunk::from_full_disk(
426 sections,
427 pos,
428 min_y,
429 height,
430 level.clone(),
431 block_ticks,
432 fluid_ticks,
433 heightmaps,
434 persistent.postprocessing.iter().map(Vec::clone).collect(),
435 structure_starts,
436 structure_references,
437 light,
438 );
439 let full = FullChunkRef::from_full_context(&chunk);
440
441 for persistent_be in &persistent.block_entities {
443 if persistent_be.entity_type.is_none() {
444 let block_entity_pos = Self::persistent_block_entity_pos(persistent_be, pos);
445 full.set_pending_block_entity(block_entity_pos);
446 continue;
447 }
448 if let Some(block_entity) =
449 Self::persistent_to_block_entity(persistent_be, pos, full)
450 {
451 let _ = full.add_and_register_block_entity(block_entity);
452 }
453 }
454
455 let mut pending_entities = Vec::with_capacity(persistent.entities.len());
456 let level_weak = full.level_weak();
457 for persistent_entity in &persistent.entities {
458 let mut loaded_entities =
459 Self::persistent_to_entity_tree_at_level(persistent_entity, pos, &level_weak);
460 pending_entities.append(&mut loaded_entities);
461 }
462
463 if !persistent.pois.is_empty()
465 && let Some(world) = level.upgrade()
466 {
467 let tickets: Vec<_> = persistent
468 .pois
469 .iter()
470 .map(|p| {
471 let block_pos = BlockPos::new(
472 pos.0.x * 16 + i32::from(p.x),
473 i32::from(p.y),
474 pos.0.y * 16 + i32::from(p.z),
475 );
476 (block_pos, p.free_tickets)
477 })
478 .collect();
479 world.poi_storage.lock().restore_tickets(pos, &tickets);
480 }
481
482 full.common().dirty.store(false, Ordering::Release);
484
485 Ok(LoadedChunk {
486 chunk,
487 status,
488 pending_entities,
489 })
490 } else {
491 let block_ticks = BlockTickList::from_proto_saved_ticks(
492 Self::persistent_to_block_saved_ticks(&persistent.block_ticks, pos),
493 );
494 let fluid_ticks = FluidTickList::from_proto_saved_ticks(
495 Self::persistent_to_fluid_saved_ticks(&persistent.fluid_ticks, pos),
496 );
497 let carving_mask = persistent
498 .carving_mask
499 .as_deref()
500 .map(|packed| CarvingMask::from_packed_u64s(height, min_y, packed));
501
502 let chunk = Chunk::from_disk(
503 sections,
504 pos,
505 status,
506 min_y,
507 height,
508 heightmaps,
509 structure_starts,
510 structure_references,
511 carving_mask,
512 persistent.postprocessing.iter().map(Vec::clone).collect(),
513 block_ticks,
514 fluid_ticks,
515 level.clone(),
516 light,
517 );
518
519 for persistent_be in &persistent.block_entities {
520 let block_entity_pos = Self::persistent_block_entity_pos(persistent_be, pos);
521 if persistent_be.entity_type.is_none() {
522 chunk.set_pending_block_entity(block_entity_pos);
523 continue;
524 }
525 let state = chunk.get_block_state(block_entity_pos);
526 if let Some(block_entity) = Self::persistent_to_block_entity_at(
527 persistent_be,
528 block_entity_pos,
529 level.clone(),
530 state,
531 ) {
532 let _ = chunk.set_block_entity(block_entity);
533 }
534 }
535
536 for persistent_entity in &persistent.entities {
537 let loaded_entities =
538 Self::persistent_to_entity_tree_at_level(persistent_entity, pos, &level);
539 for entity in loaded_entities {
540 chunk.add_entity(entity);
541 }
542 }
543
544 chunk.dirty.store(false, Ordering::Release);
545
546 Ok(LoadedChunk {
547 chunk,
548 status,
549 pending_entities: Vec::new(),
550 })
551 }
552 }
553
554 #[cfg(test)]
555 pub(crate) fn persistent_to_chunk(
556 persistent: &PersistentChunk<'_>,
557 pos: ChunkPos,
558 status: ChunkStatus,
559 min_y: i32,
560 height: i32,
561 level: Weak<World>,
562 ) -> LoadedChunk {
563 Self::try_persistent_to_chunk(persistent, pos, status, min_y, height, level)
564 .expect("test persistent chunk should be valid")
565 }
566
567 pub(super) fn heightmaps_to_persistent(
569 heightmaps: &ChunkHeightmaps,
570 status: ChunkStatus,
571 ) -> Vec<PersistentHeightmap> {
572 status
573 .heightmaps_after()
574 .iter()
575 .filter_map(|&hm_type| {
576 let hm = heightmaps.get(hm_type)?;
577 Some(PersistentHeightmap {
578 heightmap_type: hm_type.persistence_id(),
579 data: hm.raw_data().to_vec(),
580 })
581 })
582 .collect()
583 }
584
585 pub(super) fn persistent_to_heightmaps(
587 persistent: &[PersistentHeightmap],
588 status: ChunkStatus,
589 min_y: i32,
590 height: i32,
591 ) -> ChunkHeightmaps {
592 let mut heightmaps = ChunkHeightmaps::empty();
593
594 for ph in persistent {
595 let Some(hm_type) = HeightmapType::from_persistence_id(ph.heightmap_type) else {
596 continue;
597 };
598 if !status.heightmaps_after().contains(&hm_type) {
599 continue;
600 }
601 if ph.data.len() != 256 {
602 tracing::warn!(
603 "Heightmap data length mismatch: expected 256, got {}. Skipping.",
604 ph.data.len()
605 );
606 continue;
607 }
608 let mut data = Box::new([0u16; 256]);
609 data.copy_from_slice(&ph.data);
610 heightmaps.replace(Heightmap::from_raw_data(hm_type, min_y, height, data));
611 }
612
613 heightmaps
614 }
615
616 pub(super) fn pois_to_persistent(
618 chunk: FullChunkRef<'_>,
619 chunk_pos: ChunkPos,
620 ) -> Vec<PersistentPoi> {
621 let Some(world) = chunk.get_level() else {
622 return Vec::new();
623 };
624 world
625 .poi_storage
626 .lock()
627 .collect_for_chunk(chunk_pos)
628 .into_iter()
629 .map(|(pos, free_tickets)| PersistentPoi {
630 x: (pos.0.x - chunk_pos.0.x * 16) as u8,
631 y: pos.0.y as i16,
632 z: (pos.0.z - chunk_pos.0.y * 16) as u8,
633 free_tickets,
634 })
635 .collect()
636 }
637
638 pub(super) fn persistent_to_section(
640 persistent: &PersistentSection,
641 chunk: &PersistentChunk<'_>,
642 ) -> io::Result<ChunkSection> {
643 match persistent {
644 PersistentSection::Homogeneous {
645 block_state,
646 biomes,
647 } => {
648 let block_id = Self::resolve_block_state(chunk, *block_state)?;
649 let biome_data = Self::persistent_to_biomes(biomes, chunk)?;
650 Ok(ChunkSection::new_with_biomes(
651 PalettedContainer::Homogeneous(block_id),
652 biome_data,
653 ))
654 }
655 PersistentSection::Heterogeneous {
656 palette,
657 bits_per_entry,
658 block_data,
659 biomes,
660 } => {
661 let mut indices = unpack_indices(block_data, *bits_per_entry);
662 let runtime_palette: Vec<BlockStateId> = palette
663 .iter()
664 .map(|&idx| Self::resolve_block_state(chunk, idx))
665 .collect::<io::Result<_>>()?;
666 let mut cube = Box::new([[[BlockStateId(0); 16]; 16]; 16]);
667 for plane in &mut cube {
668 for row in plane {
669 for cell in row {
670 *cell = runtime_palette[indices.next().expect(
671 "this should never fail, we know the iterator is long enough",
672 ) as usize];
673 }
674 }
675 }
676 let states = PalettedContainer::from_cube(cube);
677 let biome_data = Self::persistent_to_biomes(biomes, chunk)?;
678 Ok(ChunkSection::new_with_biomes(states, biome_data))
679 }
680 }
681 }
682
683 pub(super) fn persistent_to_biomes(
685 persistent: &PersistentBiomeData,
686 chunk: &PersistentChunk<'_>,
687 ) -> io::Result<PalettedContainer<u16, 4>> {
688 match persistent {
689 PersistentBiomeData::Homogeneous { biome } => {
690 let biome_id = Self::resolve_biome(chunk, *biome)?;
691 Ok(PalettedContainer::Homogeneous(biome_id))
692 }
693 PersistentBiomeData::Heterogeneous {
694 palette,
695 bits_per_entry,
696 biome_data,
697 } => {
698 let mut indices = unpack_indices(biome_data, *bits_per_entry);
699 let runtime_palette: Vec<u16> = palette
700 .iter()
701 .map(|&idx| Self::resolve_biome(chunk, idx))
702 .collect::<io::Result<_>>()?;
703 let mut cube = [[[0u16; 4]; 4]; 4];
704 for plane in &mut cube {
705 for row in plane {
706 for cell in row {
707 *cell = runtime_palette[indices.next().expect(
708 "this should never fail, we know the iterator is long enough",
709 ) as usize];
710 }
711 }
712 }
713 Ok(PalettedContainer::from_cube(Box::new(cube)))
714 }
715 }
716 }
717
718 pub(super) fn resolve_block_state(
720 chunk: &PersistentChunk<'_>,
721 index: u16,
722 ) -> io::Result<BlockStateId> {
723 let Some(state) = chunk.block_states.get(index as usize) else {
724 return Err(Self::invalid_chunk_data(format!(
725 "missing block-state palette entry {index}"
726 )));
727 };
728 let Some(state_id) = REGISTRY
729 .blocks
730 .state_id_from_properties(&state.name, &state.properties)
731 else {
732 return Err(Self::invalid_chunk_data(format!(
733 "unresolvable block state {} with properties {:?}",
734 state.name, state.properties
735 )));
736 };
737 let canonical = REGISTRY.blocks.get_properties(state_id);
738 let unique_names = state
739 .properties
740 .iter()
741 .map(|(name, _)| *name)
742 .collect::<FxHashSet<_>>();
743 if unique_names.len() != state.properties.len()
744 || canonical.len() != state.properties.len()
745 || canonical
746 .iter()
747 .any(|property| !state.properties.contains(property))
748 {
749 return Err(Self::invalid_chunk_data(format!(
750 "noncanonical block state {} with properties {:?}",
751 state.name, state.properties
752 )));
753 }
754 Ok(state_id)
755 }
756
757 pub(super) fn resolve_biome(chunk: &PersistentChunk<'_>, index: u16) -> io::Result<u16> {
759 let Some(biome_key) = chunk.biomes.get(index as usize) else {
760 return Err(Self::invalid_chunk_data(format!(
761 "missing biome palette entry {index}"
762 )));
763 };
764 let Some(id) = REGISTRY.biomes.id_from_key(biome_key) else {
765 return Err(Self::invalid_chunk_data(format!(
766 "unknown biome {biome_key}"
767 )));
768 };
769 u16::try_from(id).map_err(|_| {
770 Self::invalid_chunk_data(format!("biome {biome_key} id {id} does not fit u16"))
771 })
772 }
773}