use crate::error::{LevelDbError, Result};
pub const LEGACY_TERRAIN_BLOCK_COUNT: usize = 16 * 128 * 16;
pub const LEGACY_TERRAIN_VALUE_LEN: usize = 83_200;
pub const SUBCHUNK_BLOCK_COUNT: usize = 16 * 16 * 16;
pub const LEGACY_SUBCHUNK_MIN_VALUE_LEN: usize =
1 + SUBCHUNK_BLOCK_COUNT + SUBCHUNK_BLOCK_COUNT / 2;
pub const LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN: usize =
LEGACY_SUBCHUNK_MIN_VALUE_LEN + SUBCHUNK_BLOCK_COUNT;
const LEGACY_TERRAIN_BLOCK_DATA_OFFSET: usize = LEGACY_TERRAIN_BLOCK_COUNT;
const LEGACY_TERRAIN_SKY_LIGHT_OFFSET: usize =
LEGACY_TERRAIN_BLOCK_DATA_OFFSET + LEGACY_TERRAIN_BLOCK_COUNT / 2;
const LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET: usize =
LEGACY_TERRAIN_SKY_LIGHT_OFFSET + LEGACY_TERRAIN_BLOCK_COUNT / 2;
const LEGACY_TERRAIN_HEIGHTMAP_OFFSET: usize =
LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET + LEGACY_TERRAIN_BLOCK_COUNT / 2;
const LEGACY_TERRAIN_BIOME_OFFSET: usize = LEGACY_TERRAIN_HEIGHTMAP_OFFSET + 16 * 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BedrockKey<'a> {
Chunk(ChunkKey),
Other(&'a [u8]),
}
impl<'a> BedrockKey<'a> {
#[must_use]
pub fn parse(bytes: &'a [u8]) -> Self {
ChunkKey::parse(bytes).map_or(Self::Other(bytes), Self::Chunk)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dimension {
Overworld,
Nether,
End,
Other(i32),
}
impl Dimension {
#[must_use]
pub const fn as_i32(self) -> i32 {
match self {
Self::Overworld => 0,
Self::Nether => 1,
Self::End => 2,
Self::Other(value) => value,
}
}
#[must_use]
pub const fn is_encoded(self) -> bool {
!matches!(self, Self::Overworld)
}
}
impl From<i32> for Dimension {
fn from(value: i32) -> Self {
match value {
0 => Self::Overworld,
1 => Self::Nether,
2 => Self::End,
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChunkCoordinates {
pub x: i32,
pub z: i32,
}
impl ChunkCoordinates {
#[must_use]
pub const fn new(x: i32, z: i32) -> Self {
Self { x, z }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SubChunkIndex {
raw: i8,
}
impl SubChunkIndex {
#[must_use]
pub const fn from_raw(raw: i8) -> Self {
Self { raw }
}
#[must_use]
pub const fn from_u8(raw: u8) -> Self {
Self {
raw: i8::from_ne_bytes([raw]),
}
}
#[must_use]
pub const fn raw(self) -> i8 {
self.raw
}
#[must_use]
pub const fn as_u8(self) -> u8 {
self.raw.to_ne_bytes()[0]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ChunkRecordTag {
Data3D,
Data2D,
Data2DLegacy,
SubChunkPrefix,
LegacyTerrain,
Unknown(u8),
}
impl ChunkRecordTag {
#[must_use]
pub const fn from_byte(byte: u8) -> Self {
match byte {
0x2b => Self::Data3D,
0x2d => Self::Data2D,
0x2e => Self::Data2DLegacy,
0x2f => Self::SubChunkPrefix,
0x30 => Self::LegacyTerrain,
other => Self::Unknown(other),
}
}
#[must_use]
pub const fn as_byte(self) -> u8 {
match self {
Self::Data3D => 0x2b,
Self::Data2D => 0x2d,
Self::Data2DLegacy => 0x2e,
Self::SubChunkPrefix => 0x2f,
Self::LegacyTerrain => 0x30,
Self::Unknown(byte) => byte,
}
}
#[must_use]
pub const fn is_render_chunk_record(self) -> bool {
matches!(
self,
Self::Data3D
| Self::Data2D
| Self::Data2DLegacy
| Self::SubChunkPrefix
| Self::LegacyTerrain
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChunkKey {
pub coordinates: ChunkCoordinates,
pub dimension: Dimension,
pub tag: ChunkRecordTag,
pub subchunk: Option<SubChunkIndex>,
}
impl ChunkKey {
#[must_use]
pub const fn new(
coordinates: ChunkCoordinates,
dimension: Dimension,
tag: ChunkRecordTag,
) -> Self {
Self {
coordinates,
dimension,
tag,
subchunk: None,
}
}
#[must_use]
pub const fn new_subchunk(
coordinates: ChunkCoordinates,
dimension: Dimension,
subchunk: SubChunkIndex,
) -> Self {
Self {
coordinates,
dimension,
tag: ChunkRecordTag::SubChunkPrefix,
subchunk: Some(subchunk),
}
}
#[must_use]
pub fn parse(bytes: &[u8]) -> Option<Self> {
let explicit_dimension = matches!(bytes.len(), 13 | 14);
let has_subchunk = matches!(bytes.len(), 10 | 14);
if !matches!(bytes.len(), 9 | 10 | 13 | 14) {
return None;
}
let x = read_i32_le(bytes.get(0..4)?)?;
let z = read_i32_le(bytes.get(4..8)?)?;
let (dimension, tag_offset) = if explicit_dimension {
(Dimension::from(read_i32_le(bytes.get(8..12)?)?), 12)
} else {
(Dimension::Overworld, 8)
};
let tag = ChunkRecordTag::from_byte(*bytes.get(tag_offset)?);
if matches!(tag, ChunkRecordTag::Unknown(_)) {
return None;
}
let subchunk = has_subchunk.then(|| SubChunkIndex::from_u8(bytes[tag_offset + 1]));
if matches!(tag, ChunkRecordTag::SubChunkPrefix) != subchunk.is_some() {
return None;
}
Some(Self {
coordinates: ChunkCoordinates { x, z },
dimension,
tag,
subchunk,
})
}
#[must_use]
pub fn encode(self) -> Vec<u8> {
let mut out = Vec::with_capacity(match (self.dimension.is_encoded(), self.subchunk) {
(false, None) => 9,
(false, Some(_)) => 10,
(true, None) => 13,
(true, Some(_)) => 14,
});
out.extend_from_slice(&self.coordinates.x.to_le_bytes());
out.extend_from_slice(&self.coordinates.z.to_le_bytes());
if self.dimension.is_encoded() {
out.extend_from_slice(&self.dimension.as_i32().to_le_bytes());
}
out.push(self.tag.as_byte());
if let Some(subchunk) = self.subchunk {
out.push(subchunk.as_u8());
}
out
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LegacyBiomeSample {
pub biome_id: u8,
pub red: u8,
pub green: u8,
pub blue: u8,
}
impl LegacyBiomeSample {
#[must_use]
pub const fn rgb_u32(self) -> u32 {
((self.red as u32) << 16) | ((self.green as u32) << 8) | self.blue as u32
}
}
#[derive(Debug, Clone, Copy)]
pub struct LegacyTerrain<'a> {
bytes: &'a [u8],
}
impl<'a> LegacyTerrain<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self> {
if bytes.len() != LEGACY_TERRAIN_VALUE_LEN {
return Err(LevelDbError::corruption(format!(
"LegacyTerrain value must be {LEGACY_TERRAIN_VALUE_LEN} bytes, got {}",
bytes.len()
)));
}
Ok(Self { bytes })
}
#[must_use]
pub const fn raw(self) -> &'a [u8] {
self.bytes
}
#[must_use]
pub fn block_ids(self) -> &'a [u8] {
&self.bytes[..LEGACY_TERRAIN_BLOCK_COUNT]
}
#[must_use]
pub fn block_data(self) -> &'a [u8] {
&self.bytes[LEGACY_TERRAIN_BLOCK_DATA_OFFSET..LEGACY_TERRAIN_SKY_LIGHT_OFFSET]
}
#[must_use]
pub fn sky_light(self) -> &'a [u8] {
&self.bytes[LEGACY_TERRAIN_SKY_LIGHT_OFFSET..LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET]
}
#[must_use]
pub fn block_light(self) -> &'a [u8] {
&self.bytes[LEGACY_TERRAIN_BLOCK_LIGHT_OFFSET..LEGACY_TERRAIN_HEIGHTMAP_OFFSET]
}
#[must_use]
pub fn heightmap(self) -> &'a [u8] {
&self.bytes[LEGACY_TERRAIN_HEIGHTMAP_OFFSET..LEGACY_TERRAIN_BIOME_OFFSET]
}
#[must_use]
pub fn biomes(self) -> &'a [u8] {
&self.bytes[LEGACY_TERRAIN_BIOME_OFFSET..LEGACY_TERRAIN_VALUE_LEN]
}
#[must_use]
pub const fn block_index(x: u8, y: u8, z: u8) -> Option<usize> {
if x < 16 && y < 128 && z < 16 {
Some(((x as usize) << 11) | ((z as usize) << 7) | y as usize)
} else {
None
}
}
#[must_use]
pub const fn column_index(x: u8, z: u8) -> Option<usize> {
if x < 16 && z < 16 {
Some(x as usize * 16 + z as usize)
} else {
None
}
}
#[must_use]
pub fn block_id(self, x: u8, y: u8, z: u8) -> Option<u8> {
Self::block_index(x, y, z).and_then(|index| self.block_ids().get(index).copied())
}
#[must_use]
pub fn block_data_at(self, x: u8, y: u8, z: u8) -> Option<u8> {
Self::block_index(x, y, z).and_then(|index| nibble_at(self.block_data(), index))
}
#[must_use]
pub fn height_at(self, x: u8, z: u8) -> Option<u8> {
Self::column_index(x, z).and_then(|index| self.heightmap().get(index).copied())
}
#[must_use]
pub fn biome_sample_at(self, x: u8, z: u8) -> Option<LegacyBiomeSample> {
let offset = Self::column_index(x, z)?.checked_mul(4)?;
let bytes = self.biomes().get(offset..offset + 4)?;
Some(LegacyBiomeSample {
biome_id: bytes[0],
red: bytes[1],
green: bytes[2],
blue: bytes[3],
})
}
#[must_use]
pub fn biome_color_at(self, x: u8, z: u8) -> Option<u32> {
self.biome_sample_at(x, z).map(LegacyBiomeSample::rgb_u32)
}
}
#[derive(Debug, Clone, Copy)]
pub enum SubChunkPayload<'a> {
Legacy(LegacySubChunk<'a>),
Paletted {
version: u8,
storage_count: Option<u8>,
payload: &'a [u8],
},
Unknown {
version: u8,
payload: &'a [u8],
},
}
impl<'a> SubChunkPayload<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self> {
let Some((&version, payload)) = bytes.split_first() else {
return Err(LevelDbError::corruption("subchunk value is empty"));
};
match version {
0 | 2..=7 => Ok(Self::Legacy(LegacySubChunk::parse(version, payload)?)),
1 => Ok(Self::Paletted {
version,
storage_count: None,
payload,
}),
8..=u8::MAX => {
let Some((&storage_count, payload)) = payload.split_first() else {
return Err(LevelDbError::corruption(
"paletted subchunk is missing storage count",
));
};
Ok(Self::Paletted {
version,
storage_count: Some(storage_count),
payload,
})
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LegacySubChunk<'a> {
version: u8,
block_ids: &'a [u8],
block_data: &'a [u8],
sky_light: Option<&'a [u8]>,
block_light: Option<&'a [u8]>,
}
impl<'a> LegacySubChunk<'a> {
fn parse(version: u8, payload: &'a [u8]) -> Result<Self> {
if !matches!(
payload.len() + 1,
LEGACY_SUBCHUNK_MIN_VALUE_LEN | LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN
) {
return Err(LevelDbError::corruption(format!(
"legacy subchunk value has invalid length {}",
payload.len() + 1
)));
}
let block_ids = &payload[..SUBCHUNK_BLOCK_COUNT];
let block_data =
&payload[SUBCHUNK_BLOCK_COUNT..SUBCHUNK_BLOCK_COUNT + SUBCHUNK_BLOCK_COUNT / 2];
let light_offset = SUBCHUNK_BLOCK_COUNT + SUBCHUNK_BLOCK_COUNT / 2;
let (sky_light, block_light) = if payload.len() > light_offset {
(
Some(&payload[light_offset..light_offset + SUBCHUNK_BLOCK_COUNT / 2]),
Some(&payload[light_offset + SUBCHUNK_BLOCK_COUNT / 2..]),
)
} else {
(None, None)
};
Ok(Self {
version,
block_ids,
block_data,
sky_light,
block_light,
})
}
#[must_use]
pub const fn version(self) -> u8 {
self.version
}
#[must_use]
pub const fn block_ids(self) -> &'a [u8] {
self.block_ids
}
#[must_use]
pub const fn block_data(self) -> &'a [u8] {
self.block_data
}
#[must_use]
pub const fn sky_light(self) -> Option<&'a [u8]> {
self.sky_light
}
#[must_use]
pub const fn block_light(self) -> Option<&'a [u8]> {
self.block_light
}
#[must_use]
pub const fn block_index(x: u8, y: u8, z: u8) -> Option<usize> {
if x < 16 && y < 16 && z < 16 {
Some(x as usize * 256 + z as usize * 16 + y as usize)
} else {
None
}
}
#[must_use]
pub fn block_id(self, x: u8, y: u8, z: u8) -> Option<u8> {
Self::block_index(x, y, z).and_then(|index| self.block_ids.get(index).copied())
}
#[must_use]
pub fn block_data_at(self, x: u8, y: u8, z: u8) -> Option<u8> {
Self::block_index(x, y, z).and_then(|index| nibble_at(self.block_data, index))
}
#[must_use]
pub fn sky_light_at(self, x: u8, y: u8, z: u8) -> Option<u8> {
Self::block_index(x, y, z).and_then(|index| nibble_at(self.sky_light?, index))
}
#[must_use]
pub fn block_light_at(self, x: u8, y: u8, z: u8) -> Option<u8> {
Self::block_index(x, y, z).and_then(|index| nibble_at(self.block_light?, index))
}
}
fn read_i32_le(bytes: &[u8]) -> Option<i32> {
Some(i32::from_le_bytes(bytes.try_into().ok()?))
}
fn nibble_at(bytes: &[u8], index: usize) -> Option<u8> {
let byte = *bytes.get(index / 2)?;
Some(if index.is_multiple_of(2) {
byte & 0x0f
} else {
byte >> 4
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chunk_keys_roundtrip_old_and_dimension_layouts() {
let legacy = ChunkKey::new(
ChunkCoordinates::new(-1, 2),
Dimension::Overworld,
ChunkRecordTag::LegacyTerrain,
);
assert_eq!(legacy.encode().len(), 9);
assert_eq!(ChunkKey::parse(&legacy.encode()), Some(legacy));
let subchunk = ChunkKey::new_subchunk(
ChunkCoordinates::new(3, -4),
Dimension::Nether,
SubChunkIndex::from_raw(-2),
);
assert_eq!(subchunk.encode().len(), 14);
assert_eq!(ChunkKey::parse(&subchunk.encode()), Some(subchunk));
}
#[test]
fn render_chunk_record_tags_are_recognized() {
for (byte, tag) in [
(0x2b, ChunkRecordTag::Data3D),
(0x2d, ChunkRecordTag::Data2D),
(0x2e, ChunkRecordTag::Data2DLegacy),
(0x2f, ChunkRecordTag::SubChunkPrefix),
(0x30, ChunkRecordTag::LegacyTerrain),
] {
assert_eq!(ChunkRecordTag::from_byte(byte), tag);
assert_eq!(tag.as_byte(), byte);
assert!(tag.is_render_chunk_record());
}
assert!(!ChunkRecordTag::Unknown(0x31).is_render_chunk_record());
}
#[test]
fn malformed_chunk_keys_are_not_classified_as_chunk_keys() {
let mut missing_subchunk = Vec::new();
missing_subchunk.extend_from_slice(&1_i32.to_le_bytes());
missing_subchunk.extend_from_slice(&2_i32.to_le_bytes());
missing_subchunk.push(ChunkRecordTag::SubChunkPrefix.as_byte());
assert_eq!(ChunkKey::parse(&missing_subchunk), None);
assert!(matches!(
BedrockKey::parse(b"~local_player"),
BedrockKey::Other(b"~local_player")
));
}
#[test]
fn legacy_terrain_exposes_documented_slices_and_nibbles() {
let mut bytes = vec![0; LEGACY_TERRAIN_VALUE_LEN];
let index = LegacyTerrain::block_index(1, 2, 3).expect("index");
let column = LegacyTerrain::column_index(1, 3).expect("column");
assert_eq!(index, 2_434);
assert_eq!(column, 19);
bytes[index] = 42;
bytes[LEGACY_TERRAIN_BLOCK_DATA_OFFSET + index / 2] = 0xba;
bytes[LEGACY_TERRAIN_HEIGHTMAP_OFFSET + column] = 99;
bytes[LEGACY_TERRAIN_BIOME_OFFSET + column * 4
..LEGACY_TERRAIN_BIOME_OFFSET + column * 4 + 4]
.copy_from_slice(&[12, 0xab, 0xcd, 0xef]);
let terrain = LegacyTerrain::parse(&bytes).expect("legacy terrain");
assert_eq!(terrain.block_id(1, 2, 3), Some(42));
assert_eq!(terrain.block_data_at(1, 2, 3), Some(0x0a));
assert_eq!(terrain.height_at(1, 3), Some(99));
assert_eq!(terrain.biome_color_at(1, 3), Some(0x00ab_cdef));
assert_eq!(
terrain.biome_sample_at(1, 3),
Some(LegacyBiomeSample {
biome_id: 12,
red: 0xab,
green: 0xcd,
blue: 0xef,
})
);
assert_eq!(terrain.heightmap().len(), 256);
assert_eq!(terrain.biomes().len(), 1024);
assert!(LegacyTerrain::parse(&bytes[..10]).is_err());
}
#[test]
fn subchunk_payload_classifies_legacy_and_paletted_layouts() {
let mut legacy = vec![0; LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN];
legacy[0] = 2;
let index = LegacySubChunk::block_index(4, 5, 6).expect("index");
assert_eq!(index, 1_125);
legacy[1 + index] = 7;
legacy[1 + SUBCHUNK_BLOCK_COUNT + index / 2] = 0xc0;
legacy[1 + SUBCHUNK_BLOCK_COUNT + SUBCHUNK_BLOCK_COUNT / 2 + index / 2] = 0xe0;
legacy[1 + SUBCHUNK_BLOCK_COUNT + SUBCHUNK_BLOCK_COUNT + index / 2] = 0xa0;
let SubChunkPayload::Legacy(subchunk) =
SubChunkPayload::parse(&legacy).expect("legacy subchunk")
else {
panic!("expected legacy subchunk");
};
assert_eq!(subchunk.version(), 2);
assert_eq!(subchunk.block_id(4, 5, 6), Some(7));
assert_eq!(subchunk.block_data_at(4, 5, 6), Some(0x0c));
assert_eq!(subchunk.sky_light_at(4, 5, 6), Some(0x0e));
assert_eq!(subchunk.block_light_at(4, 5, 6), Some(0x0a));
assert!(subchunk.sky_light().is_some());
assert!(subchunk.block_light().is_some());
let paletted = [8, 1, 0xaa, 0xbb];
assert!(matches!(
SubChunkPayload::parse(&paletted).expect("paletted"),
SubChunkPayload::Paletted {
version: 8,
storage_count: Some(1),
payload: [0xaa, 0xbb],
}
));
}
}