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