Skip to main content

bedrock_world/
chunk.rs

1//! Bedrock chunk keys, coordinates, and subchunk payload parsing.
2//!
3//! This module decodes LevelDB key shapes used by Minecraft Bedrock worlds and
4//! exposes conservative parsers for modern paletted subchunks plus older
5//! LevelDB-era terrain arrays. Unsupported payloads are preserved as raw bytes
6//! where possible so inspection tools can keep scanning mixed-version worlds.
7
8use 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;
17/// Number of block ID entries in an old 16x128x16 `LegacyTerrain` value.
18pub const LEGACY_TERRAIN_BLOCK_COUNT: usize = 16 * 128 * 16;
19/// Exact byte length of an old `LevelDB` `LegacyTerrain` value.
20pub const LEGACY_TERRAIN_VALUE_LEN: usize = 83_200;
21/// Number of block entries in a 16x16x16 legacy subchunk.
22pub const LEGACY_SUBCHUNK_BLOCK_COUNT: usize = 16 * 16 * 16;
23/// Minimum byte length of a legacy subchunk without light arrays.
24pub const LEGACY_SUBCHUNK_MIN_VALUE_LEN: usize =
25    1 + LEGACY_SUBCHUNK_BLOCK_COUNT + LEGACY_SUBCHUNK_BLOCK_COUNT / 2;
26/// Byte length of a legacy subchunk with sky and block light arrays.
27pub 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)]
40/// Bedrock dimension identifier.
41pub enum Dimension {
42    /// The overworld dimension, encoded as `0`.
43    Overworld,
44    /// The Nether dimension, encoded as `1`.
45    Nether,
46    /// The End dimension, encoded as `2`.
47    End,
48    /// A dimension id not recognized by this crate.
49    Unknown(i32),
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53/// Vertical build-height generation used for chunk bounds.
54pub enum ChunkVersion {
55    /// Pre-Caves-and-Cliffs vertical range.
56    Old,
57    /// Modern extended vertical range.
58    New,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62/// Absolute block position within a world.
63pub struct BlockPos {
64    /// Absolute X block coordinate.
65    pub x: i32,
66    /// Absolute Y block coordinate.
67    pub y: i32,
68    /// Absolute Z block coordinate.
69    pub z: i32,
70}
71
72impl Dimension {
73    #[must_use]
74    /// Returns the numeric Bedrock dimension id.
75    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    /// Decodes a numeric Bedrock dimension id.
86    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)]
97/// Chunk position and dimension.
98pub struct ChunkPos {
99    /// Chunk X coordinate.
100    pub x: i32,
101    /// Chunk Z coordinate.
102    pub z: i32,
103    /// Dimension containing this chunk.
104    pub dimension: Dimension,
105}
106
107impl ChunkPos {
108    #[must_use]
109    /// Returns the inclusive block Y range for this chunk and version.
110    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    /// Returns the inclusive subchunk Y-index range for this chunk and version.
124    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    /// Returns the minimum block position covered by this chunk.
138    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    /// Returns the maximum block position covered by this chunk.
149    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    /// Converts this block position to a chunk position in the given dimension.
162    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    /// Returns local chunk X/Z offsets and the absolute Y coordinate.
170    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]
184/// Returns the Bedrock X-major storage index for local 16x16x16 coordinates.
185pub 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)]
190/// Bedrock chunk record tag byte used in `LevelDB` chunk keys.
191pub enum ChunkRecordTag {
192    /// Modern `Data3D` terrain and biome record.
193    Data3D,
194    /// Modern `Data2D` heightmap and biome record.
195    Data2D,
196    /// Legacy `Data2D` heightmap and biome record.
197    Data2DLegacy,
198    /// Subchunk payload record.
199    SubChunkPrefix,
200    /// Old LevelDB-era terrain record.
201    LegacyTerrain,
202    /// Block-entity NBT record.
203    BlockEntity,
204    /// Legacy inline entity NBT record.
205    Entity,
206    /// Pending tick NBT record.
207    PendingTicks,
208    /// Block extra-data record.
209    BlockExtraData,
210    /// Biome state record.
211    BiomeState,
212    /// Finalized state record.
213    FinalizedState,
214    /// Chunk conversion data record.
215    ConversionData,
216    /// Border blocks record.
217    BorderBlocks,
218    /// Hardcoded spawn-area record.
219    HardcodedSpawners,
220    /// Random tick record.
221    RandomTicks,
222    /// Checksums record.
223    Checksums,
224    /// Generation seed record.
225    GenerationSeed,
226    /// Metadata hash record.
227    MetaDataHash,
228    /// Pre-Caves-and-Cliffs blending marker.
229    GeneratedPreCavesAndCliffsBlending,
230    /// Blending biome-height record.
231    BlendingBiomeHeight,
232    /// Blending data record.
233    BlendingData,
234    /// Actor digest version record.
235    ActorDigestVersion,
236    /// Current chunk version record.
237    Version,
238    /// Old chunk version record.
239    VersionOld,
240    /// Legacy chunk version record.
241    LegacyVersion,
242    /// Unknown value preserved for forward compatibility.
243    Unknown(u8),
244}
245
246impl ChunkRecordTag {
247    #[must_use]
248    /// Returns the raw chunk record tag byte.
249    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    /// Decodes a raw chunk record tag byte.
282    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    /// Returns whether this tag can contribute renderable terrain data.
315    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)]
328/// Classified Bedrock `LevelDB` key.
329///
330/// Chunk keys carry coordinate/tag structure. Non-chunk variants model the
331/// documented global, player, map, village, actor, and string-key records while
332/// preserving unknown bytes for forward compatibility.
333pub enum BedrockDbKey {
334    /// Chunk-scoped record such as subchunk terrain, block entities, or HSA.
335    Chunk(ChunkKey),
336    /// Local-player key, accepting both `LocalPlayer` and `~local_player`.
337    LocalPlayer,
338    /// Remote-player key using the `player_` prefix.
339    RemotePlayer(String),
340    /// Modern actor payload key `actorprefix<uid>`.
341    ActorPrefix {
342        /// Actor id encoded in an `actorprefix` key.
343        actor_id: i64,
344    },
345    /// Modern actor digest key `digp<x><z>[dimension]`.
346    ActorDigest {
347        /// Chunk position encoded in a `digp` actor digest key.
348        pos: ChunkPos,
349    },
350    /// Map data key with the `map_` prefix.
351    Map(String),
352    /// Village record key.
353    Village(ParsedVillageKey),
354    /// Known global record key.
355    Global(GlobalRecordKind),
356    /// Nether/end portal tracking record.
357    Portals,
358    /// Scheduler write tracking record.
359    SchedulerWt,
360    /// Structure-template record.
361    StructureTemplate(String),
362    /// Ticking-area record.
363    TickingArea(String),
364    /// Flat-world layer settings record.
365    GameFlatWorldLayers,
366    /// Other UTF-8 key not matched by a more specific classifier.
367    PlainString(String),
368    /// Non-UTF-8 or otherwise unknown key bytes.
369    Unknown(Bytes),
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
373/// Known village record suffix kind.
374pub enum VillageRecordKind {
375    /// Village info record.
376    Info,
377    /// Village dwellers record.
378    Dwellers,
379    /// Village players record.
380    Players,
381    /// Village point-of-interest record.
382    Poi,
383    /// Unknown value preserved for forward compatibility.
384    Unknown,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Hash)]
388/// Parsed village storage key components.
389pub struct ParsedVillageKey {
390    /// Original raw value retained for inspection or roundtrip preservation.
391    pub raw: String,
392    /// Bedrock dimension encoded in the village key, when present.
393    pub dimension: Option<Dimension>,
394    /// Village UUID component decoded from the key.
395    pub uuid: String,
396    /// Classified kind for this record.
397    pub kind: VillageRecordKind,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
401/// Validated map record identifier without the `map_` storage prefix.
402pub struct MapRecordId(String);
403
404impl MapRecordId {
405    /// Creates a map record id from a printable ASCII suffix.
406    ///
407    /// # Errors
408    ///
409    /// Returns [`BedrockWorldError::Validation`] when the id is empty or
410    /// contains non-printable/non-ASCII bytes.
411    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    /// Creates a map record id without validation.
423    ///
424    /// Use this only when preserving an already-decoded storage key.
425    pub fn unchecked(id: impl Into<String>) -> Self {
426        Self(id.into())
427    }
428
429    #[must_use]
430    /// Returns the id suffix without the `map_` storage prefix.
431    pub fn as_str(&self) -> &str {
432        &self.0
433    }
434
435    #[must_use]
436    /// Encodes this id as the `LevelDB` key `map_<id>`.
437    pub fn storage_key(&self) -> Bytes {
438        Bytes::from(format!("map_{}", self.0))
439    }
440
441    #[must_use]
442    /// Decodes a `LevelDB` map key into an id suffix.
443    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)]
461/// Actor unique id used by modern `actorprefix` records.
462pub struct ActorUid(pub i64);
463
464impl ActorUid {
465    #[must_use]
466    /// Encodes this actor id as `actorprefix<little-endian i64>`.
467    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    /// Decodes an `actorprefix` storage key into an actor id.
476    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)]
482/// Chunk actor digest key used by modern Bedrock entity storage.
483pub struct ActorDigestKey {
484    /// Chunk whose digest lists actor ids for the chunk.
485    pub pos: ChunkPos,
486}
487
488impl ActorDigestKey {
489    #[must_use]
490    /// Creates a digest key for a chunk.
491    pub const fn new(pos: ChunkPos) -> Self {
492        Self { pos }
493    }
494
495    #[must_use]
496    /// Encodes this digest as `digp<x><z>[dimension]`.
497    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    /// Decodes a `digp` storage key into a digest key.
514    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)]
520/// Known non-chunk global records in a Bedrock `LevelDB` world.
521pub enum GlobalRecordKind {
522    /// `mobevents` global NBT record.
523    MobEvents,
524    /// Dimension metadata record: `Overworld`, `Nether`, or `TheEnd`.
525    Dimension(Dimension),
526    /// `scoreboard` global NBT record.
527    Scoreboard,
528    /// `LocalPlayer` global/player record.
529    LocalPlayer,
530    /// Autonomous entity tracking record.
531    AutonomousEntities,
532    /// Global biome metadata dictionary.
533    BiomeData,
534    /// Level chunk metadata dictionary.
535    LevelChunkMetaDataDictionary,
536    /// World clock metadata.
537    WorldClocks,
538    /// Preserved UTF-8 global key not recognized by this crate.
539    Other(String),
540}
541
542impl GlobalRecordKind {
543    #[must_use]
544    /// Classifies an exact storage key as a known global record.
545    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    /// Returns the canonical storage name for this global record.
564    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    /// Encodes this global kind as an exact `LevelDB` key.
583    pub fn storage_key(&self) -> Bytes {
584        Bytes::from(self.name())
585    }
586}
587
588impl BedrockDbKey {
589    #[must_use]
590    /// Decodes this value from Bedrock storage bytes.
591    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    /// Returns a stable human-readable key category.
642    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    /// Encodes this value into Bedrock storage bytes.
664    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)]
686/// Decoded chunk storage key with position, tag, and optional subchunk index.
687pub struct ChunkKey {
688    /// Chunk position encoded in the storage key.
689    pub pos: ChunkPos,
690    /// Chunk record tag byte decoded from the key.
691    pub tag: ChunkRecordTag,
692    /// Optional subchunk Y index for `SubChunkPrefix` records.
693    pub subchunk_y: Option<i8>,
694}
695
696impl ChunkKey {
697    #[must_use]
698    /// Creates a non-subchunk chunk key for the given position and record tag.
699    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    /// Creates a `SubChunkPrefix` key for the given vertical subchunk index.
709    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    /// Encodes this value into Bedrock storage bytes.
719    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    /// Decodes this value from Bedrock storage bytes.
738    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)]
773/// Raw chunk record paired with its decoded chunk key.
774pub struct ChunkRecord {
775    /// Decoded storage key for this record.
776    pub key: ChunkKey,
777    /// Parsed or raw value associated with this record.
778    pub value: Bytes,
779}
780
781#[derive(Debug, Clone, PartialEq)]
782/// Block state decoded from a Bedrock palette entry.
783pub struct BlockState {
784    /// Named Bedrock value or identifier.
785    pub name: String,
786    /// Palette block states in storage order.
787    pub states: BTreeMap<String, NbtTag>,
788    /// Bedrock format or payload version.
789    pub version: Option<i32>,
790}
791
792#[derive(Debug, Clone, PartialEq)]
793/// Block palette and optional unpacked indices for a subchunk storage.
794pub struct BlockPalette {
795    /// Palette block states in storage order.
796    pub states: Vec<BlockState>,
797    /// Optional unpacked palette indices in Bedrock storage order.
798    pub indices: Option<Vec<u16>>,
799    /// Packed palette indices retained for exact surface-column sampling.
800    packed_indices: Option<PackedPaletteIndices>,
801    /// Per-palette-entry usage counts when the decode request needs them.
802    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    /// Creates a palette backed by already unpacked block indices.
839    ///
840    /// This is intended for callers that construct decoded subchunks, including
841    /// tests and format adapters. Normal storage decoding retains a packed
842    /// representation when unpacking is unnecessary.
843    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    /// Returns the decoded palette index at local subchunk coordinates.
858    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    /// Returns the block state at local subchunk coordinates.
871    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)]
900/// Controls whether subchunk parsing keeps full indices or counts only.
901pub enum SubChunkDecodeMode {
902    /// Decode palette counts without retaining all block indices.
903    CountsOnly,
904    /// Retain packed palette indices for exact surface-column sampling.
905    SurfaceColumns,
906    #[default]
907    /// Decode and retain full block index arrays.
908    FullIndices,
909}
910
911#[derive(Debug, Clone, PartialEq)]
912/// Decoded subchunk payload family.
913pub enum SubChunkFormat {
914    /// Legacy pre-paletted subchunk payload.
915    LegacySubChunk(LegacySubChunk),
916    /// Old LevelDB-era terrain record.
917    LegacyTerrain,
918    /// Old fixed-array v1 subchunk payload.
919    FixedArrayV1,
920    /// Modern paletted subchunk payload.
921    Paletted {
922        /// Bedrock format or payload version.
923        version: u8,
924        /// Biome or block storages decoded from the record.
925        storages: Vec<BlockPalette>,
926    },
927    /// Raw bytes preserved because the payload was not decoded.
928    Raw {
929        /// Bedrock format or payload version.
930        version: Option<u8>,
931        /// Raw payload bytes preserved for unsupported formats.
932        bytes: Bytes,
933    },
934}
935
936#[derive(Debug, Clone, PartialEq)]
937/// Decoded subchunk at a vertical subchunk index.
938pub struct SubChunk {
939    /// Vertical subchunk index encoded by the storage key.
940    pub y: i8,
941    /// Decoded payload family for this value.
942    pub format: SubChunkFormat,
943}
944
945impl SubChunk {
946    #[must_use]
947    /// Returns the primary block state at local subchunk coordinates.
948    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    /// Returns the first visible block state at local subchunk coordinates.
959    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    /// Iterates visible block states at local subchunk coordinates from top storage to bottom.
971    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    /// Legacy block id at.
1009    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    /// Legacy block data at.
1020    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
1030/// Iterator over visible block states at a local coordinate.
1031pub 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)]
1085/// Legacy biome sample containing biome id and saved RGB components.
1086pub struct LegacyBiomeSample {
1087    /// Biome id associated with the sampled column.
1088    pub biome_id: u8,
1089    /// Red color component saved by legacy biome data.
1090    pub red: u8,
1091    /// Green color component saved by legacy biome data.
1092    pub green: u8,
1093    /// Blue color component saved by legacy biome data.
1094    pub blue: u8,
1095}
1096
1097impl LegacyBiomeSample {
1098    #[must_use]
1099    /// Rgb u32.
1100    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)]
1106/// Decoded view over an old LevelDB-era terrain value.
1107pub struct LegacyTerrain {
1108    bytes: Bytes,
1109}
1110
1111impl LegacyTerrain {
1112    /// Parses this value from Bedrock storage bytes.
1113    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    /// Returns the complete raw `LegacyTerrain` value bytes.
1125    pub fn raw(&self) -> &Bytes {
1126        &self.bytes
1127    }
1128
1129    #[must_use]
1130    /// Returns the 16x128x16 block id array.
1131    pub fn block_ids(&self) -> &[u8] {
1132        &self.bytes[..LEGACY_TERRAIN_BLOCK_COUNT]
1133    }
1134
1135    #[must_use]
1136    /// Returns packed 4-bit block data values.
1137    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    /// Returns packed 4-bit sky-light values.
1143    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    /// Returns packed 4-bit block-light values.
1149    pub fn block_light(&self) -> &[u8] {
1150        &self.bytes[LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET..LEGACY_TERRAIN_HEIGHTMAP_OFFSET]
1151    }
1152
1153    #[must_use]
1154    /// Returns raw heightmap bytes in `z * 16 + x` column order.
1155    pub fn heightmap(&self) -> &[u8] {
1156        &self.bytes[LEGACY_TERRAIN_HEIGHTMAP_OFFSET..LEGACY_TERRAIN_BIOME_OFFSET]
1157    }
1158
1159    #[must_use]
1160    /// Returns legacy biome samples as `[biome_id, red, green, blue]` columns.
1161    pub fn biomes(&self) -> &[u8] {
1162        &self.bytes[LEGACY_TERRAIN_BIOME_OFFSET..LEGACY_TERRAIN_VALUE_LEN]
1163    }
1164
1165    #[must_use]
1166    /// Returns the legacy terrain block-array index for local coordinates.
1167    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    /// Returns the horizontal column index in `z * 16 + x` order.
1177    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    /// Returns the legacy numeric block id at local coordinates.
1187    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    /// Returns the 4-bit block data value at local coordinates.
1194    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    /// Returns the 4-bit sky-light value at local coordinates.
1201    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    /// Returns the 4-bit block-light value at local coordinates.
1208    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    /// Returns the raw terrain heightmap value for a local column.
1215    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    /// Returns the legacy biome sample for a local column.
1221    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    /// Returns the legacy RGB biome color for a local column.
1234    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)]
1241/// Decoded view over a legacy pre-paletted subchunk payload.
1242pub struct LegacySubChunk {
1243    version: u8,
1244    bytes: Bytes,
1245}
1246
1247impl LegacySubChunk {
1248    /// Parses this value from Bedrock storage bytes.
1249    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    /// Returns the legacy subchunk payload version byte.
1274    pub const fn version(&self) -> u8 {
1275        self.version
1276    }
1277
1278    #[must_use]
1279    /// Returns the complete raw legacy subchunk payload.
1280    pub fn raw(&self) -> &Bytes {
1281        &self.bytes
1282    }
1283
1284    #[must_use]
1285    /// Returns the 16x16x16 block id array.
1286    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    /// Returns packed 4-bit block data values.
1294    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    /// Returns packed 4-bit sky-light values when present.
1302    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    /// Returns packed 4-bit block-light values when present.
1313    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    /// Returns the legacy subchunk block-array index for local coordinates.
1323    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    /// Returns the legacy numeric block id at local subchunk coordinates.
1333    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    /// Returns the 4-bit block data value at local subchunk coordinates.
1340    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    /// Returns the 4-bit sky-light value at local subchunk coordinates.
1347    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    /// Returns the 4-bit block-light value at local subchunk coordinates.
1354    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)]
1361/// Entity data data model.
1362pub struct EntityData {
1363    /// Root NBT tag for the entity payload.
1364    pub tag: NbtTag,
1365}
1366
1367#[derive(Debug, Clone, PartialEq)]
1368/// Parsed chunk with records grouped by position.
1369pub struct Chunk {
1370    /// Chunk position represented by this parsed chunk.
1371    pub pos: ChunkPos,
1372    /// Bedrock format or payload version.
1373    pub version: Option<u8>,
1374    /// Records included in this result.
1375    pub records: Vec<ChunkRecord>,
1376}
1377
1378impl Chunk {
1379    /// Returns a decoded subchunk by vertical index, when the record is present.
1380    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    /// Returns the decoded legacy terrain record, when present.
1390    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    /// Get block.
1402    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    /// Set block.
1465    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    /// Get entities.
1472    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    /// Get block entities.
1485    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
1498/// Parse subchunk.
1499pub fn parse_subchunk(y: i8, bytes: Bytes) -> Result<SubChunk> {
1500    parse_subchunk_with_mode(y, bytes, SubChunkDecodeMode::FullIndices)
1501}
1502
1503/// Parse subchunk with mode.
1504pub 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}