1use crate::error::{BedrockWorldError, Result};
9use crate::nbt::{NbtTag, parse_consecutive_root_nbt, parse_root_nbt_with_consumed};
10use crate::surface::is_air_block_name;
11use bytes::Bytes;
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15
16const MAX_SUBCHUNK_PALETTE_LEN: usize = 4096;
17pub const LEGACY_TERRAIN_BLOCK_COUNT: usize = 16 * 128 * 16;
19pub const LEGACY_TERRAIN_VALUE_LEN: usize = 83_200;
21pub const LEGACY_SUBCHUNK_BLOCK_COUNT: usize = 16 * 16 * 16;
23pub const LEGACY_SUBCHUNK_MIN_VALUE_LEN: usize =
25 1 + LEGACY_SUBCHUNK_BLOCK_COUNT + LEGACY_SUBCHUNK_BLOCK_COUNT / 2;
26pub const LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN: usize =
28 LEGACY_SUBCHUNK_MIN_VALUE_LEN + LEGACY_SUBCHUNK_BLOCK_COUNT;
29
30const LEGACY_TERRAIN_BLOCK_DATA_OFFSET: usize = LEGACY_TERRAIN_BLOCK_COUNT;
31const LEGACY_TERRAIN_SKY_LIGHT_OFFSET: usize =
32 LEGACY_TERRAIN_BLOCK_DATA_OFFSET + LEGACY_TERRAIN_BLOCK_COUNT / 2;
33const LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET: usize =
34 LEGACY_TERRAIN_SKY_LIGHT_OFFSET + LEGACY_TERRAIN_BLOCK_COUNT / 2;
35const LEGACY_TERRAIN_HEIGHTMAP_OFFSET: usize =
36 LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET + LEGACY_TERRAIN_BLOCK_COUNT / 2;
37const LEGACY_TERRAIN_BIOME_OFFSET: usize = LEGACY_TERRAIN_HEIGHTMAP_OFFSET + 16 * 16;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40pub enum Dimension {
42 Overworld,
44 Nether,
46 End,
48 Unknown(i32),
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum ChunkVersion {
55 Old,
57 New,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62pub struct BlockPos {
64 pub x: i32,
66 pub y: i32,
68 pub z: i32,
70}
71
72impl Dimension {
73 #[must_use]
74 pub const fn id(self) -> i32 {
76 match self {
77 Self::Overworld => 0,
78 Self::Nether => 1,
79 Self::End => 2,
80 Self::Unknown(value) => value,
81 }
82 }
83
84 #[must_use]
85 pub const fn from_id(id: i32) -> Self {
87 match id {
88 0 => Self::Overworld,
89 1 => Self::Nether,
90 2 => Self::End,
91 value => Self::Unknown(value),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
97pub struct ChunkPos {
99 pub x: i32,
101 pub z: i32,
103 pub dimension: Dimension,
105}
106
107impl ChunkPos {
108 #[must_use]
109 pub const fn y_range(self, version: ChunkVersion) -> (i32, i32) {
111 match self.dimension {
112 Dimension::Nether => (0, 127),
113 Dimension::End => (0, 255),
114 Dimension::Overworld => match version {
115 ChunkVersion::Old => (0, 255),
116 ChunkVersion::New => (-64, 319),
117 },
118 Dimension::Unknown(_) => (0, -1),
119 }
120 }
121
122 #[must_use]
123 pub const fn subchunk_index_range(self, version: ChunkVersion) -> (i8, i8) {
125 match self.dimension {
126 Dimension::Nether => (0, 7),
127 Dimension::End => (0, 15),
128 Dimension::Overworld => match version {
129 ChunkVersion::Old => (0, 15),
130 ChunkVersion::New => (-4, 19),
131 },
132 Dimension::Unknown(_) => (0, -1),
133 }
134 }
135
136 #[must_use]
137 pub const fn min_block_pos(self, version: ChunkVersion) -> BlockPos {
139 let (min_y, _) = self.y_range(version);
140 BlockPos {
141 x: self.x * 16,
142 y: min_y,
143 z: self.z * 16,
144 }
145 }
146
147 #[must_use]
148 pub const fn max_block_pos(self, version: ChunkVersion) -> BlockPos {
150 let (_, max_y) = self.y_range(version);
151 BlockPos {
152 x: self.x * 16 + 15,
153 y: max_y,
154 z: self.z * 16 + 15,
155 }
156 }
157}
158
159impl BlockPos {
160 #[must_use]
161 pub const fn to_chunk_pos(self, dimension: Dimension) -> ChunkPos {
163 let x = if self.x < 0 { self.x - 15 } else { self.x } / 16;
164 let z = if self.z < 0 { self.z - 15 } else { self.z } / 16;
165 ChunkPos { x, z, dimension }
166 }
167
168 #[must_use]
169 pub const fn in_chunk_offset(self) -> (u8, i32, u8) {
171 let mut x = self.x % 16;
172 let mut z = self.z % 16;
173 if x < 0 {
174 x += 16;
175 }
176 if z < 0 {
177 z += 16;
178 }
179 (x as u8, self.y, z as u8)
180 }
181}
182
183#[must_use]
184pub fn block_storage_index(local_x: u8, local_y: u8, local_z: u8) -> usize {
186 usize::from(local_x) * 256 + usize::from(local_z) * 16 + usize::from(local_y)
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
190pub enum ChunkRecordTag {
192 Data3D,
194 Data2D,
196 Data2DLegacy,
198 SubChunkPrefix,
200 LegacyTerrain,
202 BlockEntity,
204 Entity,
206 PendingTicks,
208 BlockExtraData,
210 BiomeState,
212 FinalizedState,
214 ConversionData,
216 BorderBlocks,
218 HardcodedSpawners,
220 RandomTicks,
222 Checksums,
224 GenerationSeed,
226 MetaDataHash,
228 GeneratedPreCavesAndCliffsBlending,
230 BlendingBiomeHeight,
232 BlendingData,
234 ActorDigestVersion,
236 Version,
238 VersionOld,
240 LegacyVersion,
242 Unknown(u8),
244}
245
246impl ChunkRecordTag {
247 #[must_use]
248 pub const fn byte(self) -> u8 {
250 match self {
251 Self::Data3D => 0x2b,
252 Self::Version => 0x2c,
253 Self::Data2D => 0x2d,
254 Self::Data2DLegacy => 0x2e,
255 Self::SubChunkPrefix => 0x2f,
256 Self::LegacyTerrain => 0x30,
257 Self::BlockEntity => 0x31,
258 Self::Entity => 0x32,
259 Self::PendingTicks => 0x33,
260 Self::BlockExtraData => 0x34,
261 Self::BiomeState => 0x35,
262 Self::FinalizedState => 0x36,
263 Self::ConversionData => 0x37,
264 Self::BorderBlocks => 0x38,
265 Self::HardcodedSpawners => 0x39,
266 Self::RandomTicks => 0x3a,
267 Self::Checksums => 0x3b,
268 Self::GenerationSeed => 0x3c,
269 Self::GeneratedPreCavesAndCliffsBlending => 0x3d,
270 Self::BlendingBiomeHeight => 0x3e,
271 Self::MetaDataHash => 0x3f,
272 Self::BlendingData => 0x40,
273 Self::ActorDigestVersion => 0x41,
274 Self::VersionOld => 0x76,
275 Self::LegacyVersion => 0x77,
276 Self::Unknown(value) => value,
277 }
278 }
279
280 #[must_use]
281 pub const fn from_byte(value: u8) -> Self {
283 match value {
284 0x2b => Self::Data3D,
285 0x2c => Self::Version,
286 0x2d => Self::Data2D,
287 0x2e => Self::Data2DLegacy,
288 0x2f => Self::SubChunkPrefix,
289 0x30 => Self::LegacyTerrain,
290 0x31 => Self::BlockEntity,
291 0x32 => Self::Entity,
292 0x33 => Self::PendingTicks,
293 0x34 => Self::BlockExtraData,
294 0x35 => Self::BiomeState,
295 0x36 => Self::FinalizedState,
296 0x37 => Self::ConversionData,
297 0x38 => Self::BorderBlocks,
298 0x39 => Self::HardcodedSpawners,
299 0x3a => Self::RandomTicks,
300 0x3b => Self::Checksums,
301 0x3c => Self::GenerationSeed,
302 0x3d => Self::GeneratedPreCavesAndCliffsBlending,
303 0x3e => Self::BlendingBiomeHeight,
304 0x3f => Self::MetaDataHash,
305 0x40 => Self::BlendingData,
306 0x41 => Self::ActorDigestVersion,
307 0x76 => Self::VersionOld,
308 0x77 => Self::LegacyVersion,
309 other => Self::Unknown(other),
310 }
311 }
312
313 #[must_use]
314 pub const fn is_render_chunk_record(self) -> bool {
316 matches!(
317 self,
318 Self::Data3D
319 | Self::Data2D
320 | Self::Data2DLegacy
321 | Self::LegacyTerrain
322 | Self::SubChunkPrefix
323 )
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, Hash)]
328pub enum BedrockDbKey {
334 Chunk(ChunkKey),
336 LocalPlayer,
338 RemotePlayer(String),
340 ActorPrefix {
342 actor_id: i64,
344 },
345 ActorDigest {
347 pos: ChunkPos,
349 },
350 Map(String),
352 Village(ParsedVillageKey),
354 Global(GlobalRecordKind),
356 Portals,
358 SchedulerWt,
360 StructureTemplate(String),
362 TickingArea(String),
364 GameFlatWorldLayers,
366 PlainString(String),
368 Unknown(Bytes),
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
373pub enum VillageRecordKind {
375 Info,
377 Dwellers,
379 Players,
381 Poi,
383 Unknown,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Hash)]
388pub struct ParsedVillageKey {
390 pub raw: String,
392 pub dimension: Option<Dimension>,
394 pub uuid: String,
396 pub kind: VillageRecordKind,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
401pub struct MapRecordId(String);
403
404impl MapRecordId {
405 pub fn new(id: impl Into<String>) -> Result<Self> {
412 let id = id.into();
413 if id.is_empty() || !id.as_bytes().iter().all(u8::is_ascii_graphic) {
414 return Err(BedrockWorldError::Validation(
415 "map id must be non-empty printable ASCII".to_string(),
416 ));
417 }
418 Ok(Self(id))
419 }
420
421 #[must_use]
422 pub fn unchecked(id: impl Into<String>) -> Self {
426 Self(id.into())
427 }
428
429 #[must_use]
430 pub fn as_str(&self) -> &str {
432 &self.0
433 }
434
435 #[must_use]
436 pub fn storage_key(&self) -> Bytes {
438 Bytes::from(format!("map_{}", self.0))
439 }
440
441 #[must_use]
442 pub fn from_storage_key(key: &[u8]) -> Option<Self> {
444 ascii_suffix(key, b"map_").map(Self)
445 }
446}
447
448impl std::fmt::Display for MapRecordId {
449 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 formatter.write_str(&self.0)
451 }
452}
453
454impl AsRef<str> for MapRecordId {
455 fn as_ref(&self) -> &str {
456 self.as_str()
457 }
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
461pub struct ActorUid(pub i64);
463
464impl ActorUid {
465 #[must_use]
466 pub fn storage_key(self) -> Bytes {
468 let mut bytes = Vec::with_capacity(19);
469 bytes.extend_from_slice(b"actorprefix");
470 bytes.extend_from_slice(&self.0.to_le_bytes());
471 Bytes::from(bytes)
472 }
473
474 #[must_use]
475 pub fn from_actorprefix_key(key: &[u8]) -> Option<Self> {
477 parse_i64_suffix(key, b"actorprefix").map(Self)
478 }
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
482pub struct ActorDigestKey {
484 pub pos: ChunkPos,
486}
487
488impl ActorDigestKey {
489 #[must_use]
490 pub const fn new(pos: ChunkPos) -> Self {
492 Self { pos }
493 }
494
495 #[must_use]
496 pub fn storage_key(self) -> Bytes {
498 let mut bytes = Vec::with_capacity(if self.pos.dimension == Dimension::Overworld {
499 12
500 } else {
501 16
502 });
503 bytes.extend_from_slice(b"digp");
504 bytes.extend_from_slice(&self.pos.x.to_le_bytes());
505 bytes.extend_from_slice(&self.pos.z.to_le_bytes());
506 if self.pos.dimension != Dimension::Overworld {
507 bytes.extend_from_slice(&self.pos.dimension.id().to_le_bytes());
508 }
509 Bytes::from(bytes)
510 }
511
512 #[must_use]
513 pub fn from_storage_key(key: &[u8]) -> Option<Self> {
515 parse_chunk_pos_suffix(key, b"digp").map(Self::new)
516 }
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
520pub enum GlobalRecordKind {
522 MobEvents,
524 Dimension(Dimension),
526 Scoreboard,
528 LocalPlayer,
530 AutonomousEntities,
532 BiomeData,
534 LevelChunkMetaDataDictionary,
536 WorldClocks,
538 Other(String),
540}
541
542impl GlobalRecordKind {
543 #[must_use]
544 pub fn from_key(key: &[u8]) -> Option<Self> {
546 let text = std::str::from_utf8(key).ok()?;
547 match text {
548 "mobevents" => Some(Self::MobEvents),
549 "Overworld" => Some(Self::Dimension(Dimension::Overworld)),
550 "Nether" => Some(Self::Dimension(Dimension::Nether)),
551 "TheEnd" => Some(Self::Dimension(Dimension::End)),
552 "scoreboard" => Some(Self::Scoreboard),
553 "LocalPlayer" => Some(Self::LocalPlayer),
554 "AutonomousEntities" | "autonomousentities" => Some(Self::AutonomousEntities),
555 "BiomeData" => Some(Self::BiomeData),
556 "LevelChunkMetaDataDictionary" => Some(Self::LevelChunkMetaDataDictionary),
557 "WorldClocks" => Some(Self::WorldClocks),
558 _ => None,
559 }
560 }
561
562 #[must_use]
563 pub fn name(&self) -> String {
565 match self {
566 Self::MobEvents => "mobevents".to_string(),
567 Self::Dimension(Dimension::Overworld) => "Overworld".to_string(),
568 Self::Dimension(Dimension::Nether) => "Nether".to_string(),
569 Self::Dimension(Dimension::End) => "TheEnd".to_string(),
570 Self::Dimension(Dimension::Unknown(id)) => format!("Dimension({id})"),
571 Self::Scoreboard => "scoreboard".to_string(),
572 Self::LocalPlayer => "LocalPlayer".to_string(),
573 Self::AutonomousEntities => "AutonomousEntities".to_string(),
574 Self::BiomeData => "BiomeData".to_string(),
575 Self::LevelChunkMetaDataDictionary => "LevelChunkMetaDataDictionary".to_string(),
576 Self::WorldClocks => "WorldClocks".to_string(),
577 Self::Other(name) => name.clone(),
578 }
579 }
580
581 #[must_use]
582 pub fn storage_key(&self) -> Bytes {
584 Bytes::from(self.name())
585 }
586}
587
588impl BedrockDbKey {
589 #[must_use]
590 pub fn decode(key: &[u8]) -> Self {
592 if key == b"~local_player" {
593 return Self::LocalPlayer;
594 }
595 if let Some(remote_player) = key.strip_prefix(b"player_") {
596 return Self::RemotePlayer(String::from_utf8_lossy(remote_player).into_owned());
597 }
598 if let Some(actor_id) = parse_i64_suffix(key, b"actorprefix") {
599 return Self::ActorPrefix { actor_id };
600 }
601 if let Some(pos) = parse_chunk_pos_suffix(key, b"digp") {
602 return Self::ActorDigest { pos };
603 }
604 if key == b"portals" {
605 return Self::Portals;
606 }
607 if key == b"schedulerWT" {
608 return Self::SchedulerWt;
609 }
610 if let Some(map_id) = ascii_suffix(key, b"map_") {
611 return Self::Map(map_id);
612 }
613 if let Some(village) = parse_village_key(key) {
614 return Self::Village(village);
615 }
616 if let Some(name) = ascii_suffix(key, b"structuretemplate") {
617 return Self::StructureTemplate(name);
618 }
619 if let Some(name) = ascii_suffix(key, b"tickingarea") {
620 return Self::TickingArea(name);
621 }
622 if key == b"game_flatworldlayers" {
623 return Self::GameFlatWorldLayers;
624 }
625 if let Some(kind) = GlobalRecordKind::from_key(key) {
626 return Self::Global(kind);
627 }
628 if key.iter().all(u8::is_ascii_graphic) {
629 return Self::PlainString(String::from_utf8_lossy(key).into_owned());
630 }
631 if let Ok(chunk_key) = ChunkKey::decode(key) {
632 if matches!(chunk_key.tag, ChunkRecordTag::Unknown(_)) {
633 return Self::Unknown(Bytes::copy_from_slice(key));
634 }
635 return Self::Chunk(chunk_key);
636 }
637 Self::Unknown(Bytes::copy_from_slice(key))
638 }
639
640 #[must_use]
641 pub fn summary_kind(&self) -> String {
643 match self {
644 Self::Chunk(key) => format!("Chunk::{:?}", key.tag),
645 Self::LocalPlayer => "LocalPlayer".to_string(),
646 Self::RemotePlayer(_) => "RemotePlayer".to_string(),
647 Self::ActorPrefix { .. } => "ActorPrefix".to_string(),
648 Self::ActorDigest { .. } => "ActorDigest".to_string(),
649 Self::Map(_) => "Map".to_string(),
650 Self::Village(village) => format!("Village::{:?}", village.kind),
651 Self::Global(kind) => format!("Global::{}", kind.name()),
652 Self::Portals => "Portals".to_string(),
653 Self::SchedulerWt => "SchedulerWt".to_string(),
654 Self::StructureTemplate(_) => "StructureTemplate".to_string(),
655 Self::TickingArea(_) => "TickingArea".to_string(),
656 Self::GameFlatWorldLayers => "GameFlatWorldLayers".to_string(),
657 Self::PlainString(value) => format!("PlainString::{value}"),
658 Self::Unknown(_) => "Unknown".to_string(),
659 }
660 }
661
662 #[must_use]
663 pub fn encode(&self) -> Option<Bytes> {
665 match self {
666 Self::Chunk(key) => Some(key.encode()),
667 Self::LocalPlayer => Some(Bytes::from_static(b"~local_player")),
668 Self::RemotePlayer(xuid) => Some(Bytes::from(format!("player_{xuid}"))),
669 Self::ActorPrefix { actor_id } => Some(ActorUid(*actor_id).storage_key()),
670 Self::ActorDigest { pos } => Some(ActorDigestKey::new(*pos).storage_key()),
671 Self::Map(id) => Some(MapRecordId::unchecked(id.clone()).storage_key()),
672 Self::Village(key) => Some(Bytes::copy_from_slice(key.raw.as_bytes())),
673 Self::Global(kind) => Some(kind.storage_key()),
674 Self::Portals => Some(Bytes::from_static(b"portals")),
675 Self::SchedulerWt => Some(Bytes::from_static(b"schedulerWT")),
676 Self::StructureTemplate(name) => Some(Bytes::from(format!("structuretemplate{name}"))),
677 Self::TickingArea(name) => Some(Bytes::from(format!("tickingarea{name}"))),
678 Self::GameFlatWorldLayers => Some(Bytes::from_static(b"game_flatworldlayers")),
679 Self::PlainString(name) => Some(Bytes::copy_from_slice(name.as_bytes())),
680 Self::Unknown(bytes) => Some(bytes.clone()),
681 }
682 }
683}
684
685#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
686pub struct ChunkKey {
688 pub pos: ChunkPos,
690 pub tag: ChunkRecordTag,
692 pub subchunk_y: Option<i8>,
694}
695
696impl ChunkKey {
697 #[must_use]
698 pub const fn new(pos: ChunkPos, tag: ChunkRecordTag) -> Self {
700 Self {
701 pos,
702 tag,
703 subchunk_y: None,
704 }
705 }
706
707 #[must_use]
708 pub const fn subchunk(pos: ChunkPos, y: i8) -> Self {
710 Self {
711 pos,
712 tag: ChunkRecordTag::SubChunkPrefix,
713 subchunk_y: Some(y),
714 }
715 }
716
717 #[must_use]
718 pub fn encode(&self) -> Bytes {
720 let mut bytes = Vec::with_capacity(if self.pos.dimension == Dimension::Overworld {
721 10
722 } else {
723 14
724 });
725 bytes.extend_from_slice(&self.pos.x.to_le_bytes());
726 bytes.extend_from_slice(&self.pos.z.to_le_bytes());
727 if self.pos.dimension != Dimension::Overworld {
728 bytes.extend_from_slice(&self.pos.dimension.id().to_le_bytes());
729 }
730 bytes.push(self.tag.byte());
731 if let Some(y) = self.subchunk_y {
732 bytes.push(y.to_ne_bytes()[0]);
733 }
734 Bytes::from(bytes)
735 }
736
737 pub fn decode(key: &[u8]) -> Result<Self> {
739 match key.len() {
740 9 | 10 | 13 | 14 => {}
741 len => {
742 return Err(BedrockWorldError::InvalidKey(format!(
743 "unsupported chunk key length: {len}"
744 )));
745 }
746 }
747
748 let x = read_i32(key, 0)?;
749 let z = read_i32(key, 4)?;
750 let (dimension, tag_index) = if key.len() >= 13 {
751 (Dimension::from_id(read_i32(key, 8)?), 12)
752 } else {
753 (Dimension::Overworld, 8)
754 };
755 let tag = ChunkRecordTag::from_byte(
756 *key.get(tag_index)
757 .ok_or_else(|| BedrockWorldError::InvalidKey("missing record tag".to_string()))?,
758 );
759 let subchunk_y = if matches!(key.len(), 10 | 14) {
760 Some(i8::from_ne_bytes([key[tag_index + 1]]))
761 } else {
762 None
763 };
764 Ok(Self {
765 pos: ChunkPos { x, z, dimension },
766 tag,
767 subchunk_y,
768 })
769 }
770}
771
772#[derive(Debug, Clone, PartialEq, Eq)]
773pub struct ChunkRecord {
775 pub key: ChunkKey,
777 pub value: Bytes,
779}
780
781#[derive(Debug, Clone, PartialEq)]
782pub struct BlockState {
784 pub name: String,
786 pub states: BTreeMap<String, NbtTag>,
788 pub version: Option<i32>,
790}
791
792#[derive(Debug, Clone, PartialEq)]
793pub struct BlockPalette {
795 pub states: Vec<BlockState>,
797 pub indices: Option<Vec<u16>>,
799 packed_indices: Option<PackedPaletteIndices>,
801 pub counts: Option<Vec<u16>>,
803}
804
805#[derive(Debug, Clone, PartialEq)]
806struct PackedPaletteIndices {
807 bytes: Bytes,
808 bits_per_block: u8,
809 palette_len: usize,
810}
811
812impl PackedPaletteIndices {
813 fn get(&self, index: usize) -> Option<u16> {
814 if index >= 4096 {
815 return None;
816 }
817 if self.bits_per_block == 0 {
818 return Some(0);
819 }
820 let values_per_word = usize::from(32 / self.bits_per_block);
821 let word_index = index / values_per_word;
822 let item_index = index % values_per_word;
823 let byte_offset = word_index.checked_mul(4)?;
824 let word_bytes: [u8; 4] = self
825 .bytes
826 .get(byte_offset..byte_offset + 4)?
827 .try_into()
828 .ok()?;
829 let word = u32::from_le_bytes(word_bytes);
830 let mask = (1_u32 << self.bits_per_block) - 1;
831 let value = ((word >> (item_index * usize::from(self.bits_per_block))) & mask) as u16;
832 (usize::from(value) < self.palette_len).then_some(value)
833 }
834}
835
836impl BlockPalette {
837 #[must_use]
838 pub fn with_unpacked_indices(
844 states: Vec<BlockState>,
845 indices: Vec<u16>,
846 counts: Option<Vec<u16>>,
847 ) -> Self {
848 Self {
849 states,
850 indices: Some(indices),
851 packed_indices: None,
852 counts,
853 }
854 }
855
856 #[must_use]
857 pub fn palette_index_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u16> {
859 if local_x >= 16 || local_y >= 16 || local_z >= 16 {
860 return None;
861 }
862 let index = block_storage_index(local_x, local_y, local_z);
863 self.indices
864 .as_ref()
865 .and_then(|indices| indices.get(index).copied())
866 .or_else(|| self.packed_indices.as_ref()?.get(index))
867 }
868
869 #[must_use]
870 pub fn block_state_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<&BlockState> {
872 let palette_index = usize::from(self.palette_index_at(local_x, local_y, local_z)?);
873 self.states.get(palette_index)
874 }
875
876 fn block_state_with_palette_index_at(
877 &self,
878 local_x: u8,
879 local_y: u8,
880 local_z: u8,
881 ) -> Option<BlockStatePaletteEntry<'_>> {
882 let palette_index = usize::from(self.palette_index_at(local_x, local_y, local_z)?);
883 let state = self.states.get(palette_index)?;
884 Some(BlockStatePaletteEntry {
885 state,
886 storage_index: 0,
887 palette_index,
888 })
889 }
890}
891
892#[derive(Debug, Clone, Copy)]
893pub(crate) struct BlockStatePaletteEntry<'chunk> {
894 pub(crate) state: &'chunk BlockState,
895 pub(crate) storage_index: usize,
896 pub(crate) palette_index: usize,
897}
898
899#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
900pub enum SubChunkDecodeMode {
902 CountsOnly,
904 SurfaceColumns,
906 #[default]
907 FullIndices,
909}
910
911#[derive(Debug, Clone, PartialEq)]
912pub enum SubChunkFormat {
914 LegacySubChunk(LegacySubChunk),
916 LegacyTerrain,
918 FixedArrayV1,
920 Paletted {
922 version: u8,
924 storages: Vec<BlockPalette>,
926 },
927 Raw {
929 version: Option<u8>,
931 bytes: Bytes,
933 },
934}
935
936#[derive(Debug, Clone, PartialEq)]
937pub struct SubChunk {
939 pub y: i8,
941 pub format: SubChunkFormat,
943}
944
945impl SubChunk {
946 #[must_use]
947 pub fn block_state_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<&BlockState> {
949 match &self.format {
950 SubChunkFormat::Paletted { storages, .. } => storages
951 .first()
952 .and_then(|storage| storage.block_state_at(local_x, local_y, local_z)),
953 _ => None,
954 }
955 }
956
957 #[must_use]
958 pub fn visible_block_state_at(
960 &self,
961 local_x: u8,
962 local_y: u8,
963 local_z: u8,
964 ) -> Option<&BlockState> {
965 self.visible_block_states_at(local_x, local_y, local_z)
966 .next()
967 }
968
969 #[must_use]
970 pub fn visible_block_states_at(
972 &self,
973 local_x: u8,
974 local_y: u8,
975 local_z: u8,
976 ) -> VisibleBlockStatesAt<'_> {
977 let storages = match &self.format {
978 SubChunkFormat::Paletted { storages, .. } => Some(storages.iter().rev()),
979 _ => None,
980 };
981 VisibleBlockStatesAt {
982 storages,
983 local_x,
984 local_y,
985 local_z,
986 }
987 }
988
989 pub(crate) fn visible_block_surface_states_at(
990 &self,
991 local_x: u8,
992 local_y: u8,
993 local_z: u8,
994 ) -> VisibleBlockSurfaceStatesAt<'_> {
995 let storages = match &self.format {
996 SubChunkFormat::Paletted { storages, .. } => Some(storages.iter().enumerate().rev()),
997 _ => None,
998 };
999 VisibleBlockSurfaceStatesAt {
1000 storages,
1001 local_x,
1002 local_y,
1003 local_z,
1004 }
1005 }
1006
1007 #[must_use]
1008 pub fn legacy_block_id_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1010 match &self.format {
1011 SubChunkFormat::LegacySubChunk(subchunk) => {
1012 subchunk.block_id_at(local_x, local_y, local_z)
1013 }
1014 _ => None,
1015 }
1016 }
1017
1018 #[must_use]
1019 pub fn legacy_block_data_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1021 match &self.format {
1022 SubChunkFormat::LegacySubChunk(subchunk) => {
1023 subchunk.block_data_at(local_x, local_y, local_z)
1024 }
1025 _ => None,
1026 }
1027 }
1028}
1029
1030pub struct VisibleBlockStatesAt<'chunk> {
1032 storages: Option<std::iter::Rev<std::slice::Iter<'chunk, BlockPalette>>>,
1033 local_x: u8,
1034 local_y: u8,
1035 local_z: u8,
1036}
1037
1038impl<'chunk> Iterator for VisibleBlockStatesAt<'chunk> {
1039 type Item = &'chunk BlockState;
1040
1041 fn next(&mut self) -> Option<Self::Item> {
1042 let storages = self.storages.as_mut()?;
1043 for storage in storages {
1044 let Some(entry) =
1045 storage.block_state_with_palette_index_at(self.local_x, self.local_y, self.local_z)
1046 else {
1047 continue;
1048 };
1049 if !is_air_block_name(&entry.state.name) {
1050 return Some(entry.state);
1051 }
1052 }
1053 None
1054 }
1055}
1056
1057pub(crate) struct VisibleBlockSurfaceStatesAt<'chunk> {
1058 storages: Option<std::iter::Rev<std::iter::Enumerate<std::slice::Iter<'chunk, BlockPalette>>>>,
1059 local_x: u8,
1060 local_y: u8,
1061 local_z: u8,
1062}
1063
1064impl<'chunk> Iterator for VisibleBlockSurfaceStatesAt<'chunk> {
1065 type Item = BlockStatePaletteEntry<'chunk>;
1066
1067 fn next(&mut self) -> Option<Self::Item> {
1068 let storages = self.storages.as_mut()?;
1069 for (storage_index, storage) in storages {
1070 let Some(mut entry) =
1071 storage.block_state_with_palette_index_at(self.local_x, self.local_y, self.local_z)
1072 else {
1073 continue;
1074 };
1075 if !is_air_block_name(&entry.state.name) {
1076 entry.storage_index = storage_index;
1077 return Some(entry);
1078 }
1079 }
1080 None
1081 }
1082}
1083
1084#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1085pub struct LegacyBiomeSample {
1087 pub biome_id: u8,
1089 pub red: u8,
1091 pub green: u8,
1093 pub blue: u8,
1095}
1096
1097impl LegacyBiomeSample {
1098 #[must_use]
1099 pub const fn rgb_u32(self) -> u32 {
1101 ((self.red as u32) << 16) | ((self.green as u32) << 8) | self.blue as u32
1102 }
1103}
1104
1105#[derive(Debug, Clone, PartialEq, Eq)]
1106pub struct LegacyTerrain {
1108 bytes: Bytes,
1109}
1110
1111impl LegacyTerrain {
1112 pub fn parse(bytes: Bytes) -> Result<Self> {
1114 if bytes.len() != LEGACY_TERRAIN_VALUE_LEN {
1115 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1116 "LegacyTerrain value must be {LEGACY_TERRAIN_VALUE_LEN} bytes, got {}",
1117 bytes.len()
1118 )));
1119 }
1120 Ok(Self { bytes })
1121 }
1122
1123 #[must_use]
1124 pub fn raw(&self) -> &Bytes {
1126 &self.bytes
1127 }
1128
1129 #[must_use]
1130 pub fn block_ids(&self) -> &[u8] {
1132 &self.bytes[..LEGACY_TERRAIN_BLOCK_COUNT]
1133 }
1134
1135 #[must_use]
1136 pub fn block_data(&self) -> &[u8] {
1138 &self.bytes[LEGACY_TERRAIN_BLOCK_DATA_OFFSET..LEGACY_TERRAIN_SKY_LIGHT_OFFSET]
1139 }
1140
1141 #[must_use]
1142 pub fn sky_light(&self) -> &[u8] {
1144 &self.bytes[LEGACY_TERRAIN_SKY_LIGHT_OFFSET..LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET]
1145 }
1146
1147 #[must_use]
1148 pub fn block_light(&self) -> &[u8] {
1150 &self.bytes[LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET..LEGACY_TERRAIN_HEIGHTMAP_OFFSET]
1151 }
1152
1153 #[must_use]
1154 pub fn heightmap(&self) -> &[u8] {
1156 &self.bytes[LEGACY_TERRAIN_HEIGHTMAP_OFFSET..LEGACY_TERRAIN_BIOME_OFFSET]
1157 }
1158
1159 #[must_use]
1160 pub fn biomes(&self) -> &[u8] {
1162 &self.bytes[LEGACY_TERRAIN_BIOME_OFFSET..LEGACY_TERRAIN_VALUE_LEN]
1163 }
1164
1165 #[must_use]
1166 pub fn block_index(local_x: u8, local_y: u8, local_z: u8) -> Option<usize> {
1168 if local_x < 16 && local_y < 128 && local_z < 16 {
1169 Some((usize::from(local_x) << 11) | (usize::from(local_z) << 7) | usize::from(local_y))
1170 } else {
1171 None
1172 }
1173 }
1174
1175 #[must_use]
1176 pub fn column_index(local_x: u8, local_z: u8) -> Option<usize> {
1178 if local_x < 16 && local_z < 16 {
1179 Some(usize::from(local_z) * 16 + usize::from(local_x))
1180 } else {
1181 None
1182 }
1183 }
1184
1185 #[must_use]
1186 pub fn block_id_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1188 Self::block_index(local_x, local_y, local_z)
1189 .and_then(|index| self.block_ids().get(index).copied())
1190 }
1191
1192 #[must_use]
1193 pub fn block_data_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1195 Self::block_index(local_x, local_y, local_z)
1196 .and_then(|index| nibble_at(self.block_data(), index))
1197 }
1198
1199 #[must_use]
1200 pub fn sky_light_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1202 Self::block_index(local_x, local_y, local_z)
1203 .and_then(|index| nibble_at(self.sky_light(), index))
1204 }
1205
1206 #[must_use]
1207 pub fn block_light_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1209 Self::block_index(local_x, local_y, local_z)
1210 .and_then(|index| nibble_at(self.block_light(), index))
1211 }
1212
1213 #[must_use]
1214 pub fn height_at(&self, local_x: u8, local_z: u8) -> Option<u8> {
1216 Self::column_index(local_x, local_z).and_then(|index| self.heightmap().get(index).copied())
1217 }
1218
1219 #[must_use]
1220 pub fn biome_sample_at(&self, local_x: u8, local_z: u8) -> Option<LegacyBiomeSample> {
1222 let offset = Self::column_index(local_x, local_z)?.checked_mul(4)?;
1223 let bytes = self.biomes().get(offset..offset + 4)?;
1224 Some(LegacyBiomeSample {
1225 biome_id: bytes[0],
1226 red: bytes[1],
1227 green: bytes[2],
1228 blue: bytes[3],
1229 })
1230 }
1231
1232 #[must_use]
1233 pub fn biome_color_at(&self, local_x: u8, local_z: u8) -> Option<u32> {
1235 self.biome_sample_at(local_x, local_z)
1236 .map(LegacyBiomeSample::rgb_u32)
1237 }
1238}
1239
1240#[derive(Debug, Clone, PartialEq, Eq)]
1241pub struct LegacySubChunk {
1243 version: u8,
1244 bytes: Bytes,
1245}
1246
1247impl LegacySubChunk {
1248 pub fn parse(bytes: Bytes) -> Result<Self> {
1250 let Some(version) = bytes.first().copied() else {
1251 return Err(BedrockWorldError::UnsupportedChunkFormat(
1252 "legacy subchunk value is empty".to_string(),
1253 ));
1254 };
1255 if !matches!(version, 0 | 2..=7) {
1256 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1257 "version {version} is not a legacy subchunk payload"
1258 )));
1259 }
1260 if !matches!(
1261 bytes.len(),
1262 LEGACY_SUBCHUNK_MIN_VALUE_LEN | LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN
1263 ) {
1264 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1265 "legacy subchunk value has invalid length {}",
1266 bytes.len()
1267 )));
1268 }
1269 Ok(Self { version, bytes })
1270 }
1271
1272 #[must_use]
1273 pub const fn version(&self) -> u8 {
1275 self.version
1276 }
1277
1278 #[must_use]
1279 pub fn raw(&self) -> &Bytes {
1281 &self.bytes
1282 }
1283
1284 #[must_use]
1285 pub fn block_ids(&self) -> &[u8] {
1287 let start = 1;
1288 let end = start + LEGACY_SUBCHUNK_BLOCK_COUNT;
1289 &self.bytes[start..end]
1290 }
1291
1292 #[must_use]
1293 pub fn block_data(&self) -> &[u8] {
1295 let start = 1 + LEGACY_SUBCHUNK_BLOCK_COUNT;
1296 let end = start + LEGACY_SUBCHUNK_BLOCK_COUNT / 2;
1297 &self.bytes[start..end]
1298 }
1299
1300 #[must_use]
1301 pub fn sky_light(&self) -> Option<&[u8]> {
1303 if self.bytes.len() != LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN {
1304 return None;
1305 }
1306 let start = 1 + LEGACY_SUBCHUNK_BLOCK_COUNT + LEGACY_SUBCHUNK_BLOCK_COUNT / 2;
1307 let end = start + LEGACY_SUBCHUNK_BLOCK_COUNT / 2;
1308 Some(&self.bytes[start..end])
1309 }
1310
1311 #[must_use]
1312 pub fn block_light(&self) -> Option<&[u8]> {
1314 if self.bytes.len() != LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN {
1315 return None;
1316 }
1317 let start = 1 + LEGACY_SUBCHUNK_BLOCK_COUNT + LEGACY_SUBCHUNK_BLOCK_COUNT;
1318 Some(&self.bytes[start..])
1319 }
1320
1321 #[must_use]
1322 pub fn block_index(local_x: u8, local_y: u8, local_z: u8) -> Option<usize> {
1324 if local_x < 16 && local_y < 16 && local_z < 16 {
1325 Some(usize::from(local_x) * 256 + usize::from(local_z) * 16 + usize::from(local_y))
1326 } else {
1327 None
1328 }
1329 }
1330
1331 #[must_use]
1332 pub fn block_id_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1334 Self::block_index(local_x, local_y, local_z)
1335 .and_then(|index| self.block_ids().get(index).copied())
1336 }
1337
1338 #[must_use]
1339 pub fn block_data_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1341 Self::block_index(local_x, local_y, local_z)
1342 .and_then(|index| nibble_at(self.block_data(), index))
1343 }
1344
1345 #[must_use]
1346 pub fn sky_light_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1348 Self::block_index(local_x, local_y, local_z)
1349 .and_then(|index| nibble_at(self.sky_light()?, index))
1350 }
1351
1352 #[must_use]
1353 pub fn block_light_at(&self, local_x: u8, local_y: u8, local_z: u8) -> Option<u8> {
1355 Self::block_index(local_x, local_y, local_z)
1356 .and_then(|index| nibble_at(self.block_light()?, index))
1357 }
1358}
1359
1360#[derive(Debug, Clone, PartialEq)]
1361pub struct EntityData {
1363 pub tag: NbtTag,
1365}
1366
1367#[derive(Debug, Clone, PartialEq)]
1368pub struct Chunk {
1370 pub pos: ChunkPos,
1372 pub version: Option<u8>,
1374 pub records: Vec<ChunkRecord>,
1376}
1377
1378impl Chunk {
1379 pub fn get_subchunk(&self, y: i8) -> Result<Option<SubChunk>> {
1381 let Some(record) = self.records.iter().find(|record| {
1382 record.key.tag == ChunkRecordTag::SubChunkPrefix && record.key.subchunk_y == Some(y)
1383 }) else {
1384 return Ok(None);
1385 };
1386 parse_subchunk(y, record.value.clone()).map(Some)
1387 }
1388
1389 pub fn legacy_terrain(&self) -> Result<Option<LegacyTerrain>> {
1391 let Some(record) = self
1392 .records
1393 .iter()
1394 .find(|record| record.key.tag == ChunkRecordTag::LegacyTerrain)
1395 else {
1396 return Ok(None);
1397 };
1398 LegacyTerrain::parse(record.value.clone()).map(Some)
1399 }
1400
1401 pub fn get_block(&self, x: u8, y: i16, z: u8) -> Result<BlockState> {
1403 if x >= 16 || z >= 16 {
1404 return Err(BedrockWorldError::Validation(format!(
1405 "local block coordinates must use x/z in 0..15, got x={x}, z={z}"
1406 )));
1407 }
1408
1409 let subchunk_y = i8::try_from(i32::from(y).div_euclid(16)).map_err(|_| {
1410 BedrockWorldError::Validation(format!(
1411 "block y={y} cannot be represented as a Bedrock subchunk index"
1412 ))
1413 })?;
1414 let local_y = u8::try_from(i32::from(y).rem_euclid(16)).map_err(|_| {
1415 BedrockWorldError::Validation(format!("block y={y} has invalid local subchunk offset"))
1416 })?;
1417 if let Some(subchunk) = self.get_subchunk(subchunk_y)? {
1418 if let Some(state) = subchunk.block_state_at(x, local_y, z) {
1419 return Ok(state.clone());
1420 }
1421 if let Some(id) = subchunk.legacy_block_id_at(x, local_y, z) {
1422 let mut states = BTreeMap::new();
1423 if let Some(data) = subchunk.legacy_block_data_at(x, local_y, z) {
1424 states.insert("data".to_string(), NbtTag::Byte(data as i8));
1425 }
1426 return Ok(BlockState {
1427 name: format!("legacy:{id}"),
1428 states,
1429 version: None,
1430 });
1431 }
1432 }
1433 if (0..=127).contains(&y) {
1434 let Some(terrain) = self.legacy_terrain()? else {
1435 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1436 "chunk {:?} has no legacy terrain record",
1437 self.pos
1438 )));
1439 };
1440 let local_y = u8::try_from(y).map_err(|_| {
1441 BedrockWorldError::Validation(format!("legacy block y={y} is outside 0..127"))
1442 })?;
1443 let id = terrain.block_id_at(x, local_y, z).ok_or_else(|| {
1444 BedrockWorldError::UnsupportedChunkFormat(format!(
1445 "chunk {:?} has no legacy block id at local ({x}, {y}, {z})",
1446 self.pos
1447 ))
1448 })?;
1449 let data = terrain.block_data_at(x, local_y, z).unwrap_or(0);
1450 let mut states = BTreeMap::new();
1451 states.insert("data".to_string(), NbtTag::Byte(data as i8));
1452 return Ok(BlockState {
1453 name: format!("legacy:{id}"),
1454 states,
1455 version: None,
1456 });
1457 }
1458 Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1459 "chunk {:?} does not expose a block state at local ({x}, {y}, {z})",
1460 self.pos
1461 )))
1462 }
1463
1464 pub fn set_block(&mut self, _x: u8, _y: i16, _z: u8, _block: BlockState) -> Result<()> {
1466 Err(BedrockWorldError::UnsupportedChunkFormat(
1467 "structured block editing is not enabled for this chunk format".to_string(),
1468 ))
1469 }
1470
1471 pub fn get_entities(&self) -> Result<Vec<EntityData>> {
1473 let mut entities = Vec::new();
1474 for record in self
1475 .records
1476 .iter()
1477 .filter(|record| record.key.tag == ChunkRecordTag::Entity)
1478 {
1479 entities.extend(parse_consecutive_nbt(record.value.as_ref())?);
1480 }
1481 Ok(entities)
1482 }
1483
1484 pub fn get_block_entities(&self) -> Result<Vec<EntityData>> {
1486 let mut entities = Vec::new();
1487 for record in self
1488 .records
1489 .iter()
1490 .filter(|record| record.key.tag == ChunkRecordTag::BlockEntity)
1491 {
1492 entities.extend(parse_consecutive_nbt(record.value.as_ref())?);
1493 }
1494 Ok(entities)
1495 }
1496}
1497
1498pub fn parse_subchunk(y: i8, bytes: Bytes) -> Result<SubChunk> {
1500 parse_subchunk_with_mode(y, bytes, SubChunkDecodeMode::FullIndices)
1501}
1502
1503pub fn parse_subchunk_with_mode(y: i8, bytes: Bytes, mode: SubChunkDecodeMode) -> Result<SubChunk> {
1505 let version = bytes.first().copied();
1506 let format = match version {
1507 Some(0 | 2..=7) => LegacySubChunk::parse(bytes.clone()).map_or_else(
1508 |_| SubChunkFormat::Raw { version, bytes },
1509 SubChunkFormat::LegacySubChunk,
1510 ),
1511 Some(version @ 1) => parse_exact_palette_storages(&bytes, 1, 1, mode).map_or_else(
1512 |_| SubChunkFormat::Raw {
1513 version: Some(version),
1514 bytes,
1515 },
1516 |storages| SubChunkFormat::Paletted { version, storages },
1517 ),
1518 Some(version @ 8..=u8::MAX) => parse_paletted_subchunk(version, &bytes, mode)
1519 .unwrap_or_else(|_| SubChunkFormat::Raw {
1520 version: Some(version),
1521 bytes,
1522 }),
1523 _ => SubChunkFormat::Raw { version, bytes },
1524 };
1525 Ok(SubChunk { y, format })
1526}
1527
1528fn parse_consecutive_nbt(bytes: &[u8]) -> Result<Vec<EntityData>> {
1529 parse_consecutive_root_nbt(bytes)
1530 .map(|tags| tags.into_iter().map(|tag| EntityData { tag }).collect())
1531}
1532
1533fn parse_paletted_subchunk(
1534 version: u8,
1535 bytes: &[u8],
1536 mode: SubChunkDecodeMode,
1537) -> Result<SubChunkFormat> {
1538 let Some(storage_count) = bytes.get(1).copied() else {
1539 return Err(BedrockWorldError::UnsupportedChunkFormat(
1540 "paletted subchunk is missing storage count".to_string(),
1541 ));
1542 };
1543 let offsets: &[usize] = if version == 9 { &[3, 2] } else { &[2] };
1544 for offset in offsets {
1545 if let Ok(storages) = parse_exact_palette_storages(bytes, *offset, storage_count, mode) {
1546 return Ok(SubChunkFormat::Paletted { version, storages });
1547 }
1548 }
1549 Err(BedrockWorldError::UnsupportedChunkFormat(
1550 "unsupported paletted subchunk layout".to_string(),
1551 ))
1552}
1553
1554fn parse_exact_palette_storages(
1555 bytes: &[u8],
1556 offset: usize,
1557 storage_count: u8,
1558 mode: SubChunkDecodeMode,
1559) -> Result<Vec<BlockPalette>> {
1560 let (storages, consumed) = parse_palette_storages(bytes, offset, storage_count, mode)?;
1561 if consumed != bytes.len() {
1562 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1563 "palette storage ended at byte {consumed} but payload has {} bytes",
1564 bytes.len()
1565 )));
1566 }
1567 Ok(storages)
1568}
1569
1570fn parse_palette_storages(
1571 bytes: &[u8],
1572 mut offset: usize,
1573 storage_count: u8,
1574 mode: SubChunkDecodeMode,
1575) -> Result<(Vec<BlockPalette>, usize)> {
1576 let mut storages = Vec::with_capacity(usize::from(storage_count));
1577 for _ in 0..storage_count {
1578 let header = *bytes.get(offset).ok_or_else(|| {
1579 BedrockWorldError::UnsupportedChunkFormat(
1580 "palette storage header is missing".to_string(),
1581 )
1582 })?;
1583 offset += 1;
1584
1585 let bits_per_block = header >> 1;
1586 if !matches!(bits_per_block, 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 16) {
1587 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1588 "unsupported bits-per-block value: {bits_per_block}"
1589 )));
1590 }
1591
1592 let word_count = packed_word_count(bits_per_block);
1593 let words_byte_len = word_count.checked_mul(4).ok_or_else(|| {
1594 BedrockWorldError::UnsupportedChunkFormat("palette word count overflowed".to_string())
1595 })?;
1596 let words_bytes = bytes.get(offset..offset + words_byte_len).ok_or_else(|| {
1597 BedrockWorldError::UnsupportedChunkFormat(
1598 "palette block indices are truncated".to_string(),
1599 )
1600 })?;
1601 offset += words_byte_len;
1602
1603 let palette_len = if bits_per_block == 0 {
1604 1
1605 } else {
1606 let palette_len = read_i32_at(bytes, offset)?;
1607 offset += 4;
1608 if palette_len < 0 {
1609 return Err(BedrockWorldError::UnsupportedChunkFormat(
1610 "palette length cannot be negative".to_string(),
1611 ));
1612 }
1613 let palette_len = usize::try_from(palette_len).map_err(|_| {
1614 BedrockWorldError::UnsupportedChunkFormat("palette length overflowed".to_string())
1615 })?;
1616 if palette_len > MAX_SUBCHUNK_PALETTE_LEN {
1617 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1618 "palette length {palette_len} exceeds maximum {MAX_SUBCHUNK_PALETTE_LEN}"
1619 )));
1620 }
1621 palette_len
1622 };
1623 let mut states = Vec::with_capacity(palette_len);
1624 for _ in 0..palette_len {
1625 let (tag, consumed) = parse_root_nbt_with_consumed(&bytes[offset..])?;
1626 offset += consumed;
1627 states.push(block_state_from_nbt(tag));
1628 }
1629 let mut counts =
1630 (mode != SubChunkDecodeMode::SurfaceColumns).then(|| vec![0_u16; palette_len]);
1631 let (indices, packed_indices) = match mode {
1632 SubChunkDecodeMode::FullIndices => {
1633 let indices = unpack_palette_indices(words_bytes, bits_per_block, palette_len)?;
1634 for index in &indices {
1635 if let Some(count) = counts
1636 .as_mut()
1637 .and_then(|counts| counts.get_mut(usize::from(*index)))
1638 {
1639 *count = count.saturating_add(1);
1640 }
1641 }
1642 (Some(indices), None)
1643 }
1644 SubChunkDecodeMode::CountsOnly => {
1645 count_packed_palette_indices(
1646 words_bytes,
1647 bits_per_block,
1648 palette_len,
1649 counts.as_deref_mut().ok_or_else(|| {
1650 BedrockWorldError::Validation(
1651 "counts-only decode did not allocate palette counts".to_string(),
1652 )
1653 })?,
1654 )?;
1655 (None, None)
1656 }
1657 SubChunkDecodeMode::SurfaceColumns => (
1658 None,
1659 Some(PackedPaletteIndices {
1660 bytes: Bytes::copy_from_slice(words_bytes),
1661 bits_per_block,
1662 palette_len,
1663 }),
1664 ),
1665 };
1666 storages.push(BlockPalette {
1667 states,
1668 indices,
1669 packed_indices,
1670 counts,
1671 });
1672 }
1673 Ok((storages, offset))
1674}
1675
1676fn count_packed_palette_indices(
1677 words_bytes: &[u8],
1678 bits_per_block: u8,
1679 palette_len: usize,
1680 counts: &mut [u16],
1681) -> Result<()> {
1682 if bits_per_block == 0 {
1683 if let Some(count) = counts.first_mut() {
1684 *count = 4096;
1685 }
1686 return Ok(());
1687 }
1688 let values_per_word = usize::from(32 / bits_per_block);
1689 let mask = (1_u32 << bits_per_block) - 1;
1690 let mut decoded = 0usize;
1691 for word_bytes in words_bytes.chunks_exact(4) {
1692 let word = u32::from_le_bytes(
1693 word_bytes
1694 .try_into()
1695 .map_err(|_| BedrockWorldError::CorruptWorld("bad palette word".to_string()))?,
1696 );
1697 for item_index in 0..values_per_word {
1698 if decoded == 4096 {
1699 return Ok(());
1700 }
1701 let value = ((word >> (item_index * usize::from(bits_per_block))) & mask) as u16;
1702 if usize::from(value) >= palette_len {
1703 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1704 "palette index {value} exceeds palette length {palette_len}"
1705 )));
1706 }
1707 if let Some(count) = counts.get_mut(usize::from(value)) {
1708 *count = count.saturating_add(1);
1709 }
1710 decoded = decoded.saturating_add(1);
1711 }
1712 }
1713 if decoded == 4096 {
1714 Ok(())
1715 } else {
1716 Err(BedrockWorldError::UnsupportedChunkFormat(
1717 "palette block indices are truncated".to_string(),
1718 ))
1719 }
1720}
1721
1722fn packed_word_count(bits_per_block: u8) -> usize {
1723 if bits_per_block == 0 {
1724 return 0;
1725 }
1726 let values_per_word = usize::from(32 / bits_per_block);
1727 4096_usize.div_ceil(values_per_word)
1728}
1729
1730fn unpack_palette_indices(
1731 words_bytes: &[u8],
1732 bits_per_block: u8,
1733 palette_len: usize,
1734) -> Result<Vec<u16>> {
1735 if bits_per_block == 0 {
1736 return Ok(vec![0; 4096]);
1737 }
1738 let values_per_word = usize::from(32 / bits_per_block);
1739 let mask = (1_u32 << bits_per_block) - 1;
1740 let mut indices = Vec::with_capacity(4096);
1741 for word_bytes in words_bytes.chunks_exact(4) {
1742 let word = u32::from_le_bytes(
1743 word_bytes
1744 .try_into()
1745 .map_err(|_| BedrockWorldError::CorruptWorld("bad palette word".to_string()))?,
1746 );
1747 for item_index in 0..values_per_word {
1748 if indices.len() == 4096 {
1749 break;
1750 }
1751 let value = ((word >> (item_index * usize::from(bits_per_block))) & mask) as u16;
1752 if palette_len > 0 && usize::from(value) >= palette_len {
1753 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1754 "palette index {value} exceeds palette length {palette_len}"
1755 )));
1756 }
1757 indices.push(value);
1758 }
1759 }
1760 if indices.len() != 4096 {
1761 return Err(BedrockWorldError::UnsupportedChunkFormat(format!(
1762 "palette produced {} block indices instead of 4096",
1763 indices.len()
1764 )));
1765 }
1766 Ok(indices)
1767}
1768
1769fn block_state_from_nbt(tag: NbtTag) -> BlockState {
1770 let NbtTag::Compound(root) = tag else {
1771 return BlockState {
1772 name: "<invalid>".to_string(),
1773 states: BTreeMap::new(),
1774 version: None,
1775 };
1776 };
1777 block_state_from_nbt_root(root)
1778}
1779
1780fn block_state_from_nbt_root(root: IndexMap<String, NbtTag>) -> BlockState {
1781 let mut name = None;
1782 let mut fallback_name = None;
1783 let mut states_tag = None;
1784 let mut fallback_states_tag = None;
1785 let mut saw_states_tag = false;
1786 let mut version = None;
1787 let mut fallback_version = None;
1788 for (key, value) in root {
1789 match (key.as_str(), value) {
1790 ("name", NbtTag::String(value)) => name = Some(value),
1791 ("Name", NbtTag::String(value)) => fallback_name = Some(value),
1792 ("states", value) => {
1793 saw_states_tag = true;
1794 states_tag = Some(value);
1795 }
1796 ("States", value) => fallback_states_tag = Some(value),
1797 ("version", value) => version = int_from_tag(value),
1798 ("Version", value) => fallback_version = int_from_tag(value),
1799 _ => {}
1800 }
1801 }
1802 let name = name
1803 .or(fallback_name)
1804 .unwrap_or_else(|| "<unknown>".to_string());
1805 let states = match if saw_states_tag {
1806 states_tag
1807 } else {
1808 fallback_states_tag
1809 } {
1810 Some(NbtTag::Compound(values)) => values.into_iter().collect(),
1811 _ => BTreeMap::new(),
1812 };
1813 let version = version.or(fallback_version);
1814 BlockState {
1815 name,
1816 states,
1817 version,
1818 }
1819}
1820
1821fn int_from_tag(tag: NbtTag) -> Option<i32> {
1822 match tag {
1823 NbtTag::Byte(value) => Some(i32::from(value)),
1824 NbtTag::Short(value) => Some(i32::from(value)),
1825 NbtTag::Int(value) => Some(value),
1826 _ => None,
1827 }
1828}
1829
1830fn read_i32_at(bytes: &[u8], offset: usize) -> Result<i32> {
1831 let slice: [u8; 4] = bytes
1832 .get(offset..offset + 4)
1833 .ok_or_else(|| {
1834 BedrockWorldError::UnsupportedChunkFormat("i32 field is truncated".to_string())
1835 })?
1836 .try_into()
1837 .map_err(|_| BedrockWorldError::UnsupportedChunkFormat("bad i32 field".to_string()))?;
1838 Ok(i32::from_le_bytes(slice))
1839}
1840
1841fn read_i32(bytes: &[u8], offset: usize) -> Result<i32> {
1842 let slice = bytes
1843 .get(offset..offset + 4)
1844 .ok_or_else(|| BedrockWorldError::InvalidKey("chunk key is truncated".to_string()))?;
1845 let slice: [u8; 4] = slice
1846 .try_into()
1847 .map_err(|_| BedrockWorldError::InvalidKey("invalid i32 field".to_string()))?;
1848 Ok(i32::from_le_bytes(slice))
1849}
1850
1851fn parse_i64_suffix(key: &[u8], prefix: &[u8]) -> Option<i64> {
1852 let suffix = key.strip_prefix(prefix)?;
1853 let bytes: [u8; 8] = suffix.try_into().ok()?;
1854 Some(i64::from_le_bytes(bytes))
1855}
1856
1857fn parse_chunk_pos_suffix(key: &[u8], prefix: &[u8]) -> Option<ChunkPos> {
1858 let suffix = key.strip_prefix(prefix)?;
1859 match suffix.len() {
1860 8 => Some(ChunkPos {
1861 x: read_i32_optional(suffix, 0)?,
1862 z: read_i32_optional(suffix, 4)?,
1863 dimension: Dimension::Overworld,
1864 }),
1865 12 => Some(ChunkPos {
1866 x: read_i32_optional(suffix, 0)?,
1867 z: read_i32_optional(suffix, 4)?,
1868 dimension: Dimension::from_id(read_i32_optional(suffix, 8)?),
1869 }),
1870 _ => None,
1871 }
1872}
1873
1874fn read_i32_optional(bytes: &[u8], offset: usize) -> Option<i32> {
1875 let slice: [u8; 4] = bytes.get(offset..offset + 4)?.try_into().ok()?;
1876 Some(i32::from_le_bytes(slice))
1877}
1878
1879fn nibble_at(bytes: &[u8], index: usize) -> Option<u8> {
1880 let byte = *bytes.get(index / 2)?;
1881 Some(if index.is_multiple_of(2) {
1882 byte & 0x0f
1883 } else {
1884 byte >> 4
1885 })
1886}
1887
1888fn ascii_suffix(key: &[u8], prefix: &[u8]) -> Option<String> {
1889 let suffix = key.strip_prefix(prefix)?;
1890 if suffix.iter().all(u8::is_ascii_graphic) {
1891 return Some(String::from_utf8_lossy(suffix).into_owned());
1892 }
1893 None
1894}
1895
1896fn parse_village_key(key: &[u8]) -> Option<ParsedVillageKey> {
1897 let raw = std::str::from_utf8(key).ok()?;
1898 let parts = raw.split('_').collect::<Vec<_>>();
1899 if !matches!(parts.as_slice(), ["VILLAGE", ..]) || !matches!(parts.len(), 3 | 4) {
1900 return None;
1901 }
1902 let (dimension, tail) = match parts.as_slice() {
1903 ["VILLAGE", dimension, _, _] => {
1904 let dimension = match *dimension {
1905 "Overworld" => Dimension::Overworld,
1906 "Nether" => Dimension::Nether,
1907 "TheEnd" => Dimension::End,
1908 _ => return None,
1909 };
1910 (Some(dimension), &parts[2..])
1911 }
1912 ["VILLAGE", _, _] => (None, &parts[1..]),
1913 _ => return None,
1914 };
1915 let uuid = tail[0];
1916 if uuid.len() != 36 {
1917 return None;
1918 }
1919 let kind = match tail[1] {
1920 "INFO" => VillageRecordKind::Info,
1921 "DWELLERS" => VillageRecordKind::Dwellers,
1922 "PLAYERS" => VillageRecordKind::Players,
1923 "POI" => VillageRecordKind::Poi,
1924 _ => VillageRecordKind::Unknown,
1925 };
1926 Some(ParsedVillageKey {
1927 raw: raw.to_string(),
1928 dimension,
1929 uuid: uuid.to_string(),
1930 kind,
1931 })
1932}
1933
1934#[cfg(test)]
1935mod tests {
1936 use super::*;
1937 use crate::nbt::serialize_root_nbt;
1938
1939 #[test]
1940 fn chunk_key_roundtrips_overworld_and_subchunk() {
1941 let pos = ChunkPos {
1942 x: -3,
1943 z: 7,
1944 dimension: Dimension::Overworld,
1945 };
1946 let key = ChunkKey::subchunk(pos, -4);
1947 let encoded = key.encode();
1948
1949 assert_eq!(encoded.len(), 10);
1950 assert_eq!(ChunkKey::decode(&encoded).expect("decode"), key);
1951 }
1952
1953 #[test]
1954 fn chunk_key_roundtrips_dimension_key() {
1955 let pos = ChunkPos {
1956 x: 1,
1957 z: 2,
1958 dimension: Dimension::Nether,
1959 };
1960 let key = ChunkKey::new(pos, ChunkRecordTag::Version);
1961 let encoded = key.encode();
1962
1963 assert_eq!(encoded.len(), 13);
1964 assert_eq!(ChunkKey::decode(&encoded).expect("decode"), key);
1965 }
1966
1967 #[test]
1968 fn bedrock_db_key_decodes_actor_and_digp_keys() {
1969 let mut actor_key = b"actorprefix".to_vec();
1970 actor_key.extend_from_slice(&42_i64.to_le_bytes());
1971 assert_eq!(
1972 BedrockDbKey::decode(&actor_key),
1973 BedrockDbKey::ActorPrefix { actor_id: 42 }
1974 );
1975
1976 let mut digp_key = b"digp".to_vec();
1977 digp_key.extend_from_slice(&1_i32.to_le_bytes());
1978 digp_key.extend_from_slice(&(-2_i32).to_le_bytes());
1979 assert_eq!(
1980 BedrockDbKey::decode(&digp_key),
1981 BedrockDbKey::ActorDigest {
1982 pos: ChunkPos {
1983 x: 1,
1984 z: -2,
1985 dimension: Dimension::Overworld
1986 }
1987 }
1988 );
1989 }
1990
1991 #[test]
1992 fn bedrock_db_key_encodes_documented_global_shapes() {
1993 let map_id = MapRecordId::new("42").expect("map id");
1994 assert_eq!(map_id.storage_key().as_ref(), b"map_42");
1995 assert_eq!(
1996 MapRecordId::from_storage_key(b"map_42"),
1997 Some(map_id.clone())
1998 );
1999 assert_eq!(
2000 BedrockDbKey::Map("42".to_string()).encode().as_deref(),
2001 Some(&b"map_42"[..])
2002 );
2003
2004 let pos = ChunkPos {
2005 x: 7,
2006 z: -8,
2007 dimension: Dimension::End,
2008 };
2009 let digest = ActorDigestKey::new(pos).storage_key();
2010 assert_eq!(
2011 ActorDigestKey::from_storage_key(&digest),
2012 Some(ActorDigestKey::new(pos))
2013 );
2014 assert_eq!(
2015 BedrockDbKey::Global(GlobalRecordKind::Scoreboard)
2016 .encode()
2017 .as_deref(),
2018 Some(&b"scoreboard"[..])
2019 );
2020 assert_eq!(
2021 BedrockDbKey::decode(b"TheEnd"),
2022 BedrockDbKey::Global(GlobalRecordKind::Dimension(Dimension::End))
2023 );
2024 }
2025
2026 #[test]
2027 fn chunk_record_tags_align_with_bedrock_level_reference() {
2028 let expected = [
2029 (0x2b, ChunkRecordTag::Data3D),
2030 (0x2c, ChunkRecordTag::Version),
2031 (0x2d, ChunkRecordTag::Data2D),
2032 (0x2e, ChunkRecordTag::Data2DLegacy),
2033 (0x2f, ChunkRecordTag::SubChunkPrefix),
2034 (0x30, ChunkRecordTag::LegacyTerrain),
2035 (0x31, ChunkRecordTag::BlockEntity),
2036 (0x32, ChunkRecordTag::Entity),
2037 (0x33, ChunkRecordTag::PendingTicks),
2038 (0x34, ChunkRecordTag::BlockExtraData),
2039 (0x35, ChunkRecordTag::BiomeState),
2040 (0x36, ChunkRecordTag::FinalizedState),
2041 (0x37, ChunkRecordTag::ConversionData),
2042 (0x38, ChunkRecordTag::BorderBlocks),
2043 (0x39, ChunkRecordTag::HardcodedSpawners),
2044 (0x3a, ChunkRecordTag::RandomTicks),
2045 (0x3b, ChunkRecordTag::Checksums),
2046 (0x3c, ChunkRecordTag::GenerationSeed),
2047 (0x3d, ChunkRecordTag::GeneratedPreCavesAndCliffsBlending),
2048 (0x3e, ChunkRecordTag::BlendingBiomeHeight),
2049 (0x3f, ChunkRecordTag::MetaDataHash),
2050 (0x40, ChunkRecordTag::BlendingData),
2051 (0x41, ChunkRecordTag::ActorDigestVersion),
2052 (0x76, ChunkRecordTag::VersionOld),
2053 ];
2054 for (byte, tag) in expected {
2055 assert_eq!(ChunkRecordTag::from_byte(byte), tag);
2056 assert_eq!(tag.byte(), byte);
2057 }
2058 }
2059
2060 #[test]
2061 fn bedrock_db_key_decodes_specific_ascii_keys_before_plain_keys() {
2062 assert_eq!(
2063 BedrockDbKey::decode(b"map_42"),
2064 BedrockDbKey::Map("42".to_string())
2065 );
2066 assert!(matches!(
2067 BedrockDbKey::decode(b"VILLAGE_12345678-1234-1234-1234-123456789abc_INFO"),
2068 BedrockDbKey::Village(_)
2069 ));
2070 assert!(matches!(
2071 BedrockDbKey::decode(b"LevelChunkMetaDataDictionary"),
2072 BedrockDbKey::Global(GlobalRecordKind::LevelChunkMetaDataDictionary)
2073 ));
2074 }
2075
2076 #[test]
2077 fn chunk_pos_matches_bedrock_level_height_ranges() {
2078 let overworld = ChunkPos {
2079 x: 0,
2080 z: 0,
2081 dimension: Dimension::Overworld,
2082 };
2083 assert_eq!(overworld.y_range(ChunkVersion::Old), (0, 255));
2084 assert_eq!(overworld.y_range(ChunkVersion::New), (-64, 319));
2085 assert_eq!(overworld.subchunk_index_range(ChunkVersion::New), (-4, 19));
2086 assert_eq!(
2087 BlockPos {
2088 x: -1,
2089 y: 64,
2090 z: -1
2091 }
2092 .to_chunk_pos(Dimension::Overworld),
2093 ChunkPos {
2094 x: -1,
2095 z: -1,
2096 dimension: Dimension::Overworld
2097 }
2098 );
2099 }
2100
2101 #[test]
2102 fn legacy_terrain_exposes_old_leveldb_arrays() {
2103 let mut bytes = vec![0; LEGACY_TERRAIN_VALUE_LEN];
2104 let block_index = LegacyTerrain::block_index(1, 2, 3).expect("block index");
2105 let column_index = 3 * 16 + 1;
2106 assert_eq!(block_index, 2_434);
2107 assert_eq!(LegacyTerrain::column_index(1, 3), Some(column_index));
2108 bytes[block_index] = 42;
2109 bytes[LEGACY_TERRAIN_BLOCK_DATA_OFFSET + block_index / 2] = 0xba;
2110 bytes[LEGACY_TERRAIN_SKY_LIGHT_OFFSET + block_index / 2] = 0xc7;
2111 bytes[LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET + block_index / 2] = 0xd5;
2112 bytes[LEGACY_TERRAIN_HEIGHTMAP_OFFSET + column_index] = 99;
2113 bytes[LEGACY_TERRAIN_BIOME_OFFSET + column_index * 4
2114 ..LEGACY_TERRAIN_BIOME_OFFSET + column_index * 4 + 4]
2115 .copy_from_slice(&[12, 0xab, 0xcd, 0xef]);
2116
2117 let terrain = LegacyTerrain::parse(Bytes::from(bytes)).expect("legacy terrain");
2118
2119 assert_eq!(terrain.block_id_at(1, 2, 3), Some(42));
2120 assert_eq!(terrain.block_data_at(1, 2, 3), Some(0x0a));
2121 assert_eq!(terrain.sky_light_at(1, 2, 3), Some(0x07));
2122 assert_eq!(terrain.block_light_at(1, 2, 3), Some(0x05));
2123 assert_eq!(terrain.height_at(1, 3), Some(99));
2124 assert_eq!(terrain.biome_color_at(1, 3), Some(0x00ab_cdef));
2125 assert_eq!(
2126 terrain.biome_sample_at(1, 3),
2127 Some(LegacyBiomeSample {
2128 biome_id: 12,
2129 red: 0xab,
2130 green: 0xcd,
2131 blue: 0xef,
2132 })
2133 );
2134 assert!(LegacyTerrain::parse(Bytes::from_static(b"short")).is_err());
2135 }
2136
2137 #[test]
2138 fn legacy_subchunk_decodes_block_ids_metadata_and_light() {
2139 let mut bytes = vec![0; LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN];
2140 bytes[0] = 2;
2141 let index = LegacySubChunk::block_index(4, 5, 6).expect("block index");
2142 assert_eq!(index, 1_125);
2143 bytes[1 + index] = 7;
2144 bytes[1 + LEGACY_SUBCHUNK_BLOCK_COUNT + index / 2] = 0xc0;
2145 bytes[1 + LEGACY_SUBCHUNK_BLOCK_COUNT + LEGACY_SUBCHUNK_BLOCK_COUNT / 2 + index / 2] = 0xe0;
2146 bytes[1 + LEGACY_SUBCHUNK_BLOCK_COUNT + LEGACY_SUBCHUNK_BLOCK_COUNT + index / 2] = 0xa0;
2147
2148 let subchunk = parse_subchunk(0, Bytes::from(bytes)).expect("parse legacy subchunk");
2149
2150 let SubChunkFormat::LegacySubChunk(legacy) = &subchunk.format else {
2151 panic!("expected legacy subchunk");
2152 };
2153 assert_eq!(legacy.version(), 2);
2154 assert_eq!(legacy.block_id_at(4, 5, 6), Some(7));
2155 assert_eq!(legacy.block_data_at(4, 5, 6), Some(0x0c));
2156 assert_eq!(legacy.sky_light_at(4, 5, 6), Some(0x0e));
2157 assert_eq!(legacy.block_light_at(4, 5, 6), Some(0x0a));
2158 assert_eq!(subchunk.legacy_block_id_at(4, 5, 6), Some(7));
2159 }
2160
2161 #[test]
2162 fn paletted_subchunk_v1_uses_single_storage_without_count_byte() {
2163 let mut bytes = build_paletted_subchunk(8, None, 4, 4);
2164 bytes.remove(1);
2165 bytes[0] = 1;
2166
2167 let subchunk = parse_subchunk(0, Bytes::from(bytes)).expect("parse v1 palette");
2168
2169 let SubChunkFormat::Paletted { version, storages } = subchunk.format else {
2170 panic!("expected v1 paletted subchunk");
2171 };
2172 assert_eq!(version, 1);
2173 assert_eq!(storages.len(), 1);
2174 assert_eq!(storages[0].indices.as_ref().expect("indices").len(), 4096);
2175 }
2176
2177 #[test]
2178 fn paletted_subchunk_decodes_supported_bits_per_block() {
2179 for bits_per_block in [0, 1, 2, 3, 4, 5, 6, 8, 16] {
2180 let bytes = build_paletted_subchunk(8, None, bits_per_block, 4);
2181
2182 let subchunk = parse_subchunk(0, Bytes::from(bytes)).expect("parse");
2183
2184 let SubChunkFormat::Paletted { storages, .. } = subchunk.format else {
2185 panic!("expected paletted subchunk for {bits_per_block} bits");
2186 };
2187 assert_eq!(storages.len(), 1);
2188 assert_eq!(storages[0].indices.as_ref().expect("indices").len(), 4096);
2189 assert_eq!(
2190 storages[0]
2191 .counts
2192 .as_ref()
2193 .expect("full indices retain counts")
2194 .iter()
2195 .sum::<u16>(),
2196 4096
2197 );
2198 }
2199 }
2200
2201 #[test]
2202 fn paletted_subchunk_counts_only_drops_indices_but_keeps_counts() {
2203 let bytes = build_paletted_subchunk(8, None, 4, 4);
2204
2205 let subchunk =
2206 parse_subchunk_with_mode(0, Bytes::from(bytes), SubChunkDecodeMode::CountsOnly)
2207 .expect("parse");
2208
2209 let SubChunkFormat::Paletted { storages, .. } = subchunk.format else {
2210 panic!("expected paletted subchunk");
2211 };
2212 assert!(storages[0].indices.is_none());
2213 assert_eq!(
2214 storages[0]
2215 .counts
2216 .as_ref()
2217 .expect("counts-only retains counts")
2218 .iter()
2219 .sum::<u16>(),
2220 4096
2221 );
2222 }
2223
2224 #[test]
2225 fn surface_columns_keep_random_access_without_full_indices() {
2226 let bytes = build_paletted_subchunk(8, None, 4, 4);
2227 let full = parse_subchunk_with_mode(
2228 0,
2229 Bytes::from(bytes.clone()),
2230 SubChunkDecodeMode::FullIndices,
2231 )
2232 .expect("parse full indices");
2233 let surface =
2234 parse_subchunk_with_mode(0, Bytes::from(bytes), SubChunkDecodeMode::SurfaceColumns)
2235 .expect("parse surface columns");
2236
2237 let SubChunkFormat::Paletted {
2238 storages: surface_storages,
2239 ..
2240 } = &surface.format
2241 else {
2242 panic!("expected surface paletted subchunk");
2243 };
2244 assert!(surface_storages[0].indices.is_none());
2245 assert!(surface_storages[0].packed_indices.is_some());
2246 assert!(surface_storages[0].counts.is_none());
2247
2248 for (x, y, z) in [(0, 0, 0), (1, 2, 3), (15, 15, 15), (7, 9, 4)] {
2249 assert_eq!(
2250 full.block_state_at(x, y, z),
2251 surface.block_state_at(x, y, z)
2252 );
2253 }
2254 }
2255
2256 #[test]
2257 fn paletted_subchunk_v9_accepts_embedded_y_byte() {
2258 let bytes = build_paletted_subchunk(9, Some(-4), 4, 4);
2259
2260 let subchunk = parse_subchunk(-4, Bytes::from(bytes)).expect("parse");
2261
2262 let SubChunkFormat::Paletted { storages, .. } = subchunk.format else {
2263 panic!("expected paletted v9 subchunk");
2264 };
2265 assert_eq!(storages[0].states.len(), 4);
2266 }
2267
2268 #[test]
2269 fn paletted_subchunk_v9_accepts_positive_embedded_y_that_looks_like_storage_header() {
2270 let bytes = build_paletted_subchunk(9, Some(8), 4, 4);
2271
2272 let subchunk = parse_subchunk(8, Bytes::from(bytes)).expect("parse");
2273
2274 let SubChunkFormat::Paletted { storages, .. } = &subchunk.format else {
2275 panic!("expected paletted v9 subchunk");
2276 };
2277 assert_eq!(storages[0].states.len(), 4);
2278 assert_eq!(
2279 subchunk
2280 .block_state_at(1, 2, 3)
2281 .expect("block state at x=1 y=2 z=3")
2282 .name,
2283 "minecraft:block_2"
2284 );
2285 }
2286
2287 #[test]
2288 fn paletted_subchunk_v9_falls_back_to_legacy_layout_without_embedded_y() {
2289 let bytes = build_paletted_subchunk(9, None, 4, 4);
2290
2291 let subchunk = parse_subchunk(8, Bytes::from(bytes)).expect("parse");
2292
2293 let SubChunkFormat::Paletted { storages, .. } = &subchunk.format else {
2294 panic!("expected paletted v9 subchunk");
2295 };
2296 assert_eq!(storages[0].states.len(), 4);
2297 assert_eq!(
2298 subchunk
2299 .block_state_at(1, 2, 3)
2300 .expect("block state at x=1 y=2 z=3")
2301 .name,
2302 "minecraft:block_2"
2303 );
2304 }
2305
2306 #[test]
2307 fn paletted_subchunk_rejects_trailing_bytes_after_storage_payload() {
2308 let mut bytes = build_paletted_subchunk(8, None, 4, 4);
2309 bytes.push(0);
2310
2311 let subchunk = parse_subchunk(0, Bytes::from(bytes)).expect("parse");
2312
2313 assert!(matches!(subchunk.format, SubChunkFormat::Raw { .. }));
2314 }
2315
2316 #[test]
2317 fn block_state_lookup_uses_xz_plane_storage_order() {
2318 let bytes = build_paletted_subchunk(8, None, 4, 8);
2319 let subchunk = parse_subchunk(0, Bytes::from(bytes)).expect("parse");
2320
2321 assert_eq!(block_storage_index(1, 2, 3), 306);
2322 let state = subchunk
2323 .block_state_at(1, 2, 3)
2324 .expect("block state at x=1 y=2 z=3");
2325
2326 assert_eq!(
2327 state.name,
2328 format!("minecraft:block_{}", block_storage_index(1, 2, 3) % 8)
2329 );
2330 }
2331
2332 #[test]
2333 fn visible_block_state_lookup_uses_top_non_air_storage() {
2334 let subchunk = parse_subchunk(
2335 0,
2336 Bytes::from(build_two_storage_paletted_subchunk(
2337 "minecraft:stone",
2338 "minecraft:copper_block",
2339 )),
2340 )
2341 .expect("parse layered subchunk");
2342
2343 assert_eq!(
2344 subchunk
2345 .block_state_at(1, 2, 3)
2346 .expect("storage zero state")
2347 .name,
2348 "minecraft:stone"
2349 );
2350 let visible = subchunk
2351 .visible_block_states_at(1, 2, 3)
2352 .map(|state| state.name.as_str())
2353 .collect::<Vec<_>>();
2354
2355 assert_eq!(visible, ["minecraft:copper_block", "minecraft:stone"]);
2356 assert_eq!(
2357 subchunk
2358 .visible_block_state_at(1, 2, 3)
2359 .expect("visible state")
2360 .name,
2361 "minecraft:copper_block"
2362 );
2363 }
2364
2365 #[test]
2366 fn visible_surface_state_iterator_reports_palette_positions() {
2367 let mut bytes = vec![8, 3];
2368 append_test_palette_storage(
2369 &mut bytes,
2370 &["minecraft:air", "minecraft:water"],
2371 |x, y, z| u16::from((x, y, z) == (1, 2, 3)),
2372 );
2373 append_test_palette_storage(
2374 &mut bytes,
2375 &["minecraft:air", "minecraft:short_grass"],
2376 |x, y, z| u16::from((x, y, z) == (1, 2, 3)),
2377 );
2378 append_test_palette_storage(
2379 &mut bytes,
2380 &["minecraft:air", "minecraft:stone"],
2381 |x, y, z| u16::from((x, y, z) == (1, 2, 3)),
2382 );
2383
2384 let subchunk = parse_subchunk(0, Bytes::from(bytes)).expect("parse layered subchunk");
2385 let SubChunkFormat::Paletted { storages, .. } = &subchunk.format else {
2386 panic!("expected paletted subchunk");
2387 };
2388
2389 assert_eq!(storages.len(), 3);
2390 let visible_entries = subchunk
2391 .visible_block_surface_states_at(1, 2, 3)
2392 .map(|entry| {
2393 (
2394 entry.storage_index,
2395 entry.palette_index,
2396 entry.state.name.as_str(),
2397 )
2398 })
2399 .collect::<Vec<_>>();
2400
2401 assert_eq!(
2402 visible_entries,
2403 [
2404 (2, 1, "minecraft:stone"),
2405 (1, 1, "minecraft:short_grass"),
2406 (0, 1, "minecraft:water")
2407 ]
2408 );
2409 }
2410
2411 #[test]
2412 fn paletted_subchunk_v9_decodes_zero_bit_secondary_storage_without_palette_len() {
2413 let mut bytes = vec![9, 2, 4];
2414 append_test_palette_storage(
2415 &mut bytes,
2416 &["minecraft:air", "minecraft:stone"],
2417 |x, y, z| u16::from((x, y, z) == (4, 2, 4)),
2418 );
2419 append_zero_bit_palette_storage(&mut bytes, "minecraft:gold_block");
2420
2421 let subchunk = parse_subchunk(4, Bytes::from(bytes)).expect("parse v9 layered subchunk");
2422
2423 let SubChunkFormat::Paletted { storages, .. } = &subchunk.format else {
2424 panic!("expected paletted subchunk");
2425 };
2426 assert_eq!(storages.len(), 2);
2427 assert_eq!(storages[1].states.len(), 1);
2428 assert_eq!(storages[1].counts.as_deref(), Some(&[4096][..]));
2429 assert_eq!(
2430 subchunk
2431 .block_state_at(4, 2, 4)
2432 .expect("storage zero state")
2433 .name,
2434 "minecraft:stone"
2435 );
2436 assert_eq!(
2437 subchunk
2438 .visible_block_state_at(4, 2, 4)
2439 .expect("visible state")
2440 .name,
2441 "minecraft:gold_block"
2442 );
2443 }
2444
2445 #[test]
2446 fn chunk_get_block_reads_decoded_paletted_subchunk() {
2447 let pos = ChunkPos {
2448 x: 0,
2449 z: 0,
2450 dimension: Dimension::Overworld,
2451 };
2452 let key = ChunkKey::subchunk(pos, 0);
2453 let chunk = Chunk {
2454 pos,
2455 version: Some(8),
2456 records: vec![ChunkRecord {
2457 key,
2458 value: Bytes::from(build_paletted_subchunk(8, None, 4, 8)),
2459 }],
2460 };
2461
2462 let state = chunk.get_block(1, 2, 3).expect("block state");
2463
2464 assert_eq!(state.name, "minecraft:block_2");
2465 }
2466
2467 fn build_paletted_subchunk(
2468 version: u8,
2469 embedded_y: Option<i8>,
2470 bits_per_block: u8,
2471 palette_len: usize,
2472 ) -> Vec<u8> {
2473 let palette_len = if bits_per_block == 0 { 1 } else { palette_len };
2474 let mut bytes = vec![version, 1];
2475 if let Some(y) = embedded_y {
2476 bytes.push(y as u8);
2477 }
2478 bytes.push(bits_per_block << 1);
2479 let values_per_word = 32_usize
2480 .checked_div(usize::from(bits_per_block))
2481 .unwrap_or(4096);
2482 let mut words = vec![0_u32; packed_word_count(bits_per_block)];
2483 if bits_per_block != 0 {
2484 for block_index in 0..4096 {
2485 let value = u32::try_from(block_index % palette_len).expect("palette index");
2486 let word_index = block_index / values_per_word;
2487 let bit_offset = (block_index % values_per_word) * usize::from(bits_per_block);
2488 words[word_index] |= value << bit_offset;
2489 }
2490 }
2491 for word in words {
2492 bytes.extend_from_slice(&word.to_le_bytes());
2493 }
2494 if bits_per_block != 0 {
2495 bytes.extend_from_slice(
2496 &i32::try_from(palette_len)
2497 .expect("palette length")
2498 .to_le_bytes(),
2499 );
2500 }
2501 for index in 0..palette_len {
2502 let tag = NbtTag::Compound(IndexMap::from([
2503 (
2504 "name".to_string(),
2505 NbtTag::String(format!("minecraft:block_{index}")),
2506 ),
2507 ("states".to_string(), NbtTag::Compound(IndexMap::new())),
2508 ("version".to_string(), NbtTag::Int(1)),
2509 ]));
2510 bytes.extend_from_slice(&serialize_root_nbt(&tag).expect("serialize palette"));
2511 }
2512 bytes
2513 }
2514
2515 fn append_zero_bit_palette_storage(bytes: &mut Vec<u8>, name: &str) {
2516 bytes.push(0);
2517 let tag = NbtTag::Compound(IndexMap::from([
2518 ("name".to_string(), NbtTag::String(name.to_string())),
2519 ("states".to_string(), NbtTag::Compound(IndexMap::new())),
2520 ("version".to_string(), NbtTag::Int(1)),
2521 ]));
2522 bytes.extend_from_slice(&serialize_root_nbt(&tag).expect("serialize palette"));
2523 }
2524
2525 fn build_two_storage_paletted_subchunk(lower_name: &str, upper_name: &str) -> Vec<u8> {
2526 let mut bytes = vec![8, 2];
2527 append_test_palette_storage(&mut bytes, &["minecraft:air", lower_name], |x, y, z| {
2528 u16::from((x, y, z) == (1, 2, 3))
2529 });
2530 append_test_palette_storage(&mut bytes, &["minecraft:air", upper_name], |x, y, z| {
2531 u16::from((x, y, z) == (1, 2, 3))
2532 });
2533 bytes
2534 }
2535
2536 fn append_test_palette_storage(
2537 bytes: &mut Vec<u8>,
2538 palette: &[&str],
2539 value_at: impl Fn(u8, u8, u8) -> u16,
2540 ) {
2541 let bits_per_block = 1_u8;
2542 let values_per_word = usize::from(32 / bits_per_block);
2543 let mut words = vec![0_u32; packed_word_count(bits_per_block)];
2544 for local_z in 0..16_u8 {
2545 for local_x in 0..16_u8 {
2546 for local_y in 0..16_u8 {
2547 let value = value_at(local_x, local_y, local_z);
2548 if value == 0 {
2549 continue;
2550 }
2551 let block_index = block_storage_index(local_x, local_y, local_z);
2552 let word_index = block_index / values_per_word;
2553 let bit_offset = (block_index % values_per_word) * usize::from(bits_per_block);
2554 words[word_index] |= u32::from(value) << bit_offset;
2555 }
2556 }
2557 }
2558 bytes.push(bits_per_block << 1);
2559 for word in words {
2560 bytes.extend_from_slice(&word.to_le_bytes());
2561 }
2562 bytes.extend_from_slice(
2563 &i32::try_from(palette.len())
2564 .expect("test palette length")
2565 .to_le_bytes(),
2566 );
2567 for name in palette {
2568 let tag = NbtTag::Compound(IndexMap::from([
2569 ("name".to_string(), NbtTag::String((*name).to_string())),
2570 ("states".to_string(), NbtTag::Compound(IndexMap::new())),
2571 ("version".to_string(), NbtTag::Int(1)),
2572 ]));
2573 bytes.extend_from_slice(&serialize_root_nbt(&tag).expect("serialize palette"));
2574 }
2575 }
2576}