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