Read and Write light data using Anvil

Now we are reading and writing light data to and from the anvil format and send it in the chunk and light packet
This commit is contained in:
Alexander Medvedev 2025-04-10 19:09:27 +02:00
parent f0636bb924
commit fbd95cb693
9 changed files with 79 additions and 43 deletions

View File

@ -60,4 +60,4 @@ serde_json = "1.0"
async-trait = "0.1"
tempfile = "3.17.1"
tempfile = "3.19.1"

View File

@ -29,11 +29,11 @@ aes = "0.8"
cfb8 = "0.8"
# compression
async-compression = { version = "0.4.20", features = ["tokio", "zlib"] }
async-compression = { version = "0.4.22", features = ["tokio", "zlib"] }
take_mut = "0.2.2"
bitflags = "2.9.0"
[dev-dependencies]
# Validate correctness
flate2 = "1.1.0"
flate2 = "1.1.1"

View File

@ -8,10 +8,7 @@ use crate::{
use pumpkin_data::packet::clientbound::PLAY_LEVEL_CHUNK_WITH_LIGHT;
use pumpkin_macros::packet;
use pumpkin_world::chunk::{
ChunkData,
palette::{BlockPalette, NetworkPalette},
};
use pumpkin_world::chunk::{ChunkData, palette::NetworkPalette};
#[packet(PLAY_LEVEL_CHUNK_WITH_LIGHT)]
pub struct CChunkData<'a>(pub &'a ChunkData);
@ -45,21 +42,40 @@ impl ClientPacket for CChunkData<'_> {
}
let mut data_buf = Vec::new();
let mut light_buf = Vec::new();
for section in self.0.section.sections.iter() {
// 2 blocks per byte for block lights
let chunk_light_len = BlockPalette::VOLUME / 2;
// TODO: Implement, currently default to full bright
let chunk_light = vec![0xFFu8; chunk_light_len];
let mut sky_light_buf = Vec::new();
let mut sky_light_empty_mask = 0;
let mut sky_light_mask = 0;
let mut block_light_buf = Vec::new();
let mut block_light_empty_mask = 0;
let mut block_light_mask = 0;
light_buf.write_var_int(&chunk_light_len.try_into().map_err(|_| {
WritingError::Message(format!(
"{} is not representable as a VarInt!",
chunk_light_len
))
})?)?;
light_buf.write_slice(&chunk_light)?;
for (i, section) in self.0.section.sections.iter().enumerate() {
// Write sky light
if let Some(sky_light) = &section.sky_light {
let mut buf = Vec::new();
buf.write_var_int(&sky_light.len().try_into().map_err(|_| {
WritingError::Message("sky_light not representable as a VarInt!".to_string())
})?)?;
buf.write_slice(sky_light)?;
sky_light_buf.push(buf);
sky_light_mask |= 1 << i;
} else {
sky_light_empty_mask |= 1 << i;
}
// Write block light
if let Some(block_light) = &section.block_light {
let mut buf = Vec::new();
buf.write_var_int(&block_light.len().try_into().map_err(|_| {
WritingError::Message("block_light not representable as a VarInt!".to_string())
})?)?;
buf.write_slice(block_light)?;
block_light_buf.push(buf);
block_light_mask |= 1 << i;
} else {
block_light_empty_mask |= 1 << i;
}
// Block count
let non_empty_block_count = section.block_states.non_air_block_count() as i16;
@ -131,27 +147,29 @@ impl ClientPacket for CChunkData<'_> {
// TODO: block entities
write.write_var_int(&VarInt(0))?;
// Sky Light Mask
// All of the chunks, this is not optimal and uses way more data than needed but will be
// overhauled with a full lighting system.
write.write_bitset(&BitSet(Box::new([0b01111111111111111111111110])))?;
// Sky Light Mask
write.write_bitset(&BitSet(Box::new([sky_light_mask])))?;
// Block Light Mask
write.write_bitset(&BitSet(Box::new([0])))?;
write.write_bitset(&BitSet(Box::new([block_light_mask])))?;
// Empty Sky Light Mask
write.write_bitset(&BitSet(Box::new([0])))?;
write.write_bitset(&BitSet(Box::new([sky_light_empty_mask])))?;
// Empty Block Light Mask
write.write_bitset(&BitSet(Box::new([0])))?;
write.write_bitset(&BitSet(Box::new([block_light_empty_mask])))?;
// Sky light
write.write_var_int(&self.0.section.sections.len().try_into().map_err(|_| {
WritingError::Message(format!(
"{} is not representable as a VarInt!",
self.0.section.sections.len()
))
})?)?;
write.write_slice(&light_buf)?;
write.write_var_int(&VarInt(sky_light_buf.len() as i32))?;
for sky_buf in sky_light_buf {
write.write_slice(&sky_buf)?;
}
// Block Lighting
write.write_var_int(&VarInt(0))
// Block Light
write.write_var_int(&VarInt(block_light_buf.len() as i32))?;
for block_buf in block_light_buf {
write.write_slice(&block_buf)?;
}
Ok(())
}
}

View File

@ -9,7 +9,7 @@ pumpkin-protocol = { path = "../pumpkin-protocol" }
pumpkin-nbt = { path = "../pumpkin-nbt" }
pumpkin-util = { path = "../pumpkin-util" }
indexmap = { version = "2.8", features = ["serde"] }
indexmap = { version = "2.9", features = ["serde"] }
serde.workspace = true
serde_json.workspace = true

View File

@ -40,7 +40,7 @@ zstd = "0.13.3"
itertools = "0.14.0"
file-guard = "0.2"
indexmap = "2.8"
indexmap = "2.9"
enum_dispatch = "0.3"
noise = "0.9"
derive-getters = "0.5.0"
@ -54,7 +54,7 @@ tokio-util = { version = "0.7.14", features = ["rt"] }
criterion = { version = "0.5", features = ["html_reports", "async_tokio"] }
temp-dir = "0.1.14"
# Print log info inside tests when needed
env_logger = "0.11.7"
env_logger = "0.11.8"
# Allows us to modify the config
pumpkin-config = { path = "../pumpkin-config", features = ["test_helper"] }
# Deserialize NaN and Inf

View File

@ -827,6 +827,8 @@ pub fn chunk_to_bytes(chunk_data: &ChunkData) -> Result<Vec<u8>, ChunkSerializin
y: i as i8 + section_coords::block_to_section(chunk_data.section.min_y) as i8,
block_states,
biomes,
block_light: section.block_light.clone(), // :c
sky_light: section.sky_light.clone(), // :c
});
}

View File

@ -54,6 +54,8 @@ impl ChunkData {
.map(|section| SubChunk {
block_states: BlockPalette::from_disk_nbt(section.block_states),
biomes: BiomePalette::from_disk_nbt(section.biomes),
block_light: section.block_light,
sky_light: section.sky_light,
})
.collect();
let min_y = section_coords::section_to_block(chunk_data.min_y_section);
@ -101,11 +103,10 @@ impl ChunkData {
struct ChunkSectionNBT {
block_states: ChunkSectionBlockStates,
biomes: ChunkSectionBiomes,
// TODO
// #[serde(rename = "BlockLight", skip_serializing_if = "Option::is_none")]
// block_light: Option<Box<[u8]>>,
// #[serde(rename = "SkyLight", skip_serializing_if = "Option::is_none")]
// sky_light: Option<Box<[u8]>>,
#[serde(rename = "BlockLight", skip_serializing_if = "Option::is_none")]
block_light: Option<Box<[u8]>>,
#[serde(rename = "SkyLight", skip_serializing_if = "Option::is_none")]
sky_light: Option<Box<[u8]>>,
#[serde(rename = "Y")]
y: i8,
}

View File

@ -129,6 +129,7 @@ pub struct ChunkSections {
impl ChunkSections {
#[cfg(test)]
pub fn dump_blocks(&self) -> Vec<u16> {
// TODO: this is not optimal, we could use rust iters
let mut dump = Vec::new();
for section in self.sections.iter() {
section.block_states.for_each(|raw_id| {
@ -140,6 +141,7 @@ impl ChunkSections {
#[cfg(test)]
pub fn dump_biomes(&self) -> Vec<u8> {
// TODO: this is not optimal, we could use rust iters
let mut dump = Vec::new();
for section in self.sections.iter() {
section.biomes.for_each(|raw_id| {
@ -154,6 +156,19 @@ impl ChunkSections {
pub struct SubChunk {
pub block_states: BlockPalette,
pub biomes: BiomePalette,
pub block_light: Option<Box<[u8]>>,
pub sky_light: Option<Box<[u8]>>,
}
impl SubChunk {
pub fn max_light() -> Self {
let chunk_light_len = BlockPalette::VOLUME / 2;
Self {
block_light: Some(vec![0xFFu8; chunk_light_len].into_boxed_slice()),
sky_light: Some(vec![0xFFu8; chunk_light_len].into_boxed_slice()),
..Default::default()
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]

View File

@ -43,7 +43,7 @@ impl WorldGenerator for VanillaGenerator {
.unwrap();
let sub_chunks = generation_settings.shape.height as usize / BlockPalette::SIZE;
let sections = (0..sub_chunks).map(|_| SubChunk::default()).collect();
let sections = (0..sub_chunks).map(|_| SubChunk::max_light()).collect();
let mut sections = ChunkSections::new(sections, generation_settings.shape.min_y as i32);
let mut proto_chunk = ProtoChunk::new(