Files
Pumpkin/crates/pumpkin-protocol/src/bedrock/client/level_chunk.rs
2026-08-11 12:37:08 +02:00

244 lines
8.0 KiB
Rust

use std::io::{Error, Write};
use xxhash_rust::xxh64::xxh64;
use pumpkin_macros::packet;
use pumpkin_world::chunk::{ChunkData, palette::NetworkPalette};
use crate::{
codec::{var_int::VarInt, var_uint::VarUInt},
serial::PacketWrite,
};
const VERSION: u8 = 9;
#[packet(58)]
pub struct CLevelChunk<'a> {
// https://mojang.github.io/bedrock-protocol-docs/html/LevelChunkPacket.html
pub dimension: i32,
pub cache_enabled: bool,
// https://gist.github.com/Tomcc/a96af509e275b1af483b25c543cfbf37
// https://github.com/Mojang/bedrock-protocol-docs/blob/main/additional_docs/SubChunk%20Request%20System%20v1.18.10.md
pub chunk: &'a ChunkData,
}
pub type ChunkBlob = (u64, Vec<u8>);
pub type EncodedChunk = (Vec<u8>, Vec<ChunkBlob>);
impl CLevelChunk<'_> {
pub fn encode_chunk(
chunk: &ChunkData,
dimension: i32,
cache_enabled: bool,
) -> Result<EncodedChunk, Error> {
let mut writer = Vec::new();
VarInt(chunk.x).write(&mut writer)?;
VarInt(chunk.z).write(&mut writer)?;
VarInt(dimension).write(&mut writer)?;
let sub_chunk_count = chunk.section.count as u32;
VarUInt(sub_chunk_count).write(&mut writer)?;
// Optional sub-chunk request limit. Pumpkin sends complete chunks.
false.write(&mut writer)?;
cache_enabled.write(&mut writer)?;
let mut blobs = Vec::new();
let block_sections = chunk
.section
.block_sections
.read()
.map_err(|_| Error::other("block_sections read lock poisoned"))?;
let min_y_section = (chunk.section.min_y >> 4) as i8;
let mut subchunk_bytes_list = Vec::with_capacity(block_sections.len());
for (i, block_palette) in block_sections.iter().enumerate() {
let mut subchunk_buf = Vec::new();
// Version 9: [version:byte][num_storages:byte][sub_chunk_index:byte]
let y = (i as i8) + min_y_section;
let num_storages = 1;
subchunk_buf.write_all(&[VERSION, num_storages, y as u8])?;
let network_repr = block_palette.convert_be_network();
(network_repr.bits_per_entry << 1 | 1).write(&mut subchunk_buf)?;
for data in network_repr.packed_data {
data.write(&mut subchunk_buf)?;
}
match network_repr.palette {
NetworkPalette::Single(id) => {
VarInt(i32::from(id)).write(&mut subchunk_buf)?;
}
NetworkPalette::Indirect(palette) => {
VarInt(palette.len() as i32).write(&mut subchunk_buf)?;
for id in palette {
VarInt(i32::from(id)).write(&mut subchunk_buf)?;
}
}
NetworkPalette::Direct => (),
}
subchunk_bytes_list.push(subchunk_buf);
}
let biome_sections = chunk
.section
.biome_sections
.read()
.map_err(|_| Error::other("biome_sections read lock poisoned"))?;
let mut biome_buf = Vec::new();
for biome_palette in biome_sections.iter() {
let network_repr = biome_palette.convert_be_network();
(network_repr.bits_per_entry << 1 | 1).write(&mut biome_buf)?;
for data in network_repr.packed_data {
data.write(&mut biome_buf)?;
}
match network_repr.palette {
NetworkPalette::Single(id) => {
VarInt(i32::from(id)).write(&mut biome_buf)?;
}
NetworkPalette::Indirect(palette) => {
VarInt(palette.len() as i32).write(&mut biome_buf)?;
for id in palette {
VarInt(i32::from(id)).write(&mut biome_buf)?;
}
}
NetworkPalette::Direct => (),
}
}
if cache_enabled {
for subchunk_buf in subchunk_bytes_list {
let hash = xxh64(&subchunk_buf, 0);
blobs.push((hash, subchunk_buf));
}
let biome_hash = xxh64(&biome_buf, 0);
blobs.push((biome_hash, biome_buf));
VarUInt(blobs.len() as u32).write(&mut writer)?;
for (hash, _) in &blobs {
writer.write_all(&hash.to_le_bytes())?;
}
// Chunk data payload when cache_enabled: only border block count byte (0).
VarUInt(1).write(&mut writer)?;
writer.write_all(&[0])?;
} else {
VarUInt(0).write(&mut writer)?;
let mut chunk_data = Vec::new();
for subchunk_buf in subchunk_bytes_list {
chunk_data.write_all(&subchunk_buf)?;
}
chunk_data.write_all(&biome_buf)?;
chunk_data.write_all(&[0])?;
VarUInt(chunk_data.len() as u32).write(&mut writer)?;
writer.write_all(&chunk_data)?;
}
Ok((writer, blobs))
}
}
impl PacketWrite for CLevelChunk<'_> {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
let (encoded, _) = Self::encode_chunk(self.chunk, self.dimension, self.cache_enabled)?;
writer.write_all(&encoded)
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Mutex,
atomic::{AtomicBool, AtomicU64},
};
use pumpkin_data::chunk::ChunkStatus;
use pumpkin_world::{
chunk::{ChunkData, ChunkHeightmaps, ChunkLight, ChunkSections},
tick::scheduler::ChunkTickScheduler,
};
use super::CLevelChunk;
use crate::serial::PacketWrite;
fn read_var_uint(data: &[u8], offset: &mut usize) -> u32 {
let mut value = 0;
for shift in (0..35).step_by(7) {
let byte = data[*offset];
*offset += 1;
value |= u32::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return value;
}
}
panic!("VarUInt is too long");
}
#[test]
fn biomes_follow_subchunks_without_subchunk_headers() {
let chunk = ChunkData {
section: ChunkSections::new(24, -64),
heightmap: Mutex::new(ChunkHeightmaps::default()),
x: 0,
z: 0,
block_ticks: ChunkTickScheduler::default(),
fluid_ticks: ChunkTickScheduler::default(),
pending_block_entities: Mutex::default(),
light_engine: Mutex::new(ChunkLight::default()),
light_populated: AtomicBool::new(false),
status: ChunkStatus::Full,
blending_data: None,
dirty: AtomicBool::new(false),
inhabited_time: AtomicU64::new(0),
};
let mut encoded = Vec::new();
CLevelChunk {
dimension: 0,
cache_enabled: false,
chunk: &chunk,
}
.write(&mut encoded)
.unwrap();
let mut offset = 0;
for _ in 0..3 {
read_var_uint(&encoded, &mut offset);
}
assert_eq!(read_var_uint(&encoded, &mut offset), 24);
assert_eq!(encoded[offset], 0); // No sub-chunk request limit.
assert_eq!(encoded[offset + 1], 0); // Cache disabled.
offset += 2;
assert_eq!(read_var_uint(&encoded, &mut offset), 0);
let raw_len = read_var_uint(&encoded, &mut offset) as usize;
let raw = &encoded[offset..];
assert_eq!(raw.len(), raw_len);
let mut raw_offset = 0;
for y in -4i8..20 {
assert_eq!(&raw[raw_offset..raw_offset + 3], &[9, 1, y as u8]);
raw_offset += 3;
assert_eq!(raw[raw_offset], 1); // Single-value block palette.
raw_offset += 1;
read_var_uint(raw, &mut raw_offset);
}
for _ in 0..24 {
assert_eq!(raw[raw_offset], 1); // No version/storage/Y prefix.
raw_offset += 1;
read_var_uint(raw, &mut raw_offset);
}
assert_eq!(raw[raw_offset], 0); // Border block count.
assert_eq!(raw_offset + 1, raw.len());
}
}