Implement the block change packets

This commit is contained in:
Ryan 2022-06-22 08:06:54 -07:00
parent 09b434f298
commit 69ba704352
9 changed files with 262 additions and 85 deletions

View file

@ -330,6 +330,11 @@ pub fn build() -> anyhow::Result<()> {
} }
} }
pub(crate) const fn from_raw_unchecked(id: u16) -> Self {
debug_assert!(Self::from_raw(id).is_some());
Self(id)
}
/// Converts this block state to its underlying raw block state ID. /// Converts this block state to its underlying raw block state ID.
/// ///
/// The original block state can be recovered with [`BlockState::from_raw`]. /// The original block state can be recovered with [`BlockState::from_raw`].
@ -337,6 +342,11 @@ pub fn build() -> anyhow::Result<()> {
self.0 self.0
} }
/// Returns the maximum block state ID.
pub const fn max_raw() -> u16 {
#max_block_state
}
/// Gets the value of the property with the given name from this block. /// Gets the value of the property with the given name from this block.
/// ///
/// If this block does not have the property, then `None` is returned. /// If this block does not have the property, then `None` is returned.

View file

@ -938,7 +938,7 @@ const ENTITIES: &[Class] = &[
Field { Field {
name: "global_position", name: "global_position",
typ: Type::OptGlobalPosition, typ: Type::OptGlobalPosition,
} },
], ],
}, },
Class { Class {
@ -1232,7 +1232,7 @@ const ENTITIES: &[Class] = &[
Field { Field {
name: "tongue_target", name: "tongue_target",
typ: Type::VarInt(0), typ: Type::VarInt(0),
} },
], ],
}, },
Class { Class {
@ -1399,7 +1399,7 @@ const ENTITIES: &[Class] = &[
Field { Field {
name: "right_horn", name: "right_horn",
typ: Type::Bool(true), typ: Type::Bool(true),
} },
], ],
}, },
Class { Class {

View file

@ -201,7 +201,7 @@ fn terrain_column(
2.0, 2.0,
0.5, 0.5,
) * 6.0) ) * 6.0)
.floor() as i64; .floor() as i64;
if *in_terrain { if *in_terrain {
if *depth > 0 { if *depth > 0 {

View file

@ -53,12 +53,24 @@ impl From<(i32, i32, i32)> for BlockPos {
} }
} }
impl From<BlockPos> for (i32, i32, i32) {
fn from(pos: BlockPos) -> Self {
(pos.x, pos.y, pos.z)
}
}
impl From<[i32; 3]> for BlockPos { impl From<[i32; 3]> for BlockPos {
fn from([x, y, z]: [i32; 3]) -> Self { fn from([x, y, z]: [i32; 3]) -> Self {
BlockPos::new(x, y, z) BlockPos::new(x, y, z)
} }
} }
impl From<BlockPos> for [i32; 3] {
fn from(pos: BlockPos) -> Self {
[pos.x, pos.y, pos.z]
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -15,20 +15,21 @@ use crate::packets::play::s2c::{
}; };
use crate::protocol::{Encode, Nbt}; use crate::protocol::{Encode, Nbt};
use crate::var_int::VarInt; use crate::var_int::VarInt;
use crate::{BiomeId, Server, Ticks}; use crate::var_long::VarLong;
use crate::{BiomeId, BlockPos, DimensionId, Server, Ticks};
pub struct Chunks { pub struct Chunks {
chunks: HashMap<ChunkPos, Chunk>, chunks: HashMap<ChunkPos, Chunk>,
server: Server, server: Server,
section_count: u32, dimension: DimensionId,
} }
impl Chunks { impl Chunks {
pub(crate) fn new(server: Server, section_count: u32) -> Self { pub(crate) fn new(server: Server, dimension: DimensionId) -> Self {
Self { Self {
chunks: HashMap::new(), chunks: HashMap::new(),
server, server,
section_count, dimension,
} }
} }
@ -36,8 +37,8 @@ impl Chunks {
self.chunks.len() self.chunks.len()
} }
pub fn get(&self, pos: ChunkPos) -> Option<&Chunk> { pub fn get(&self, pos: impl Into<ChunkPos>) -> Option<&Chunk> {
self.chunks.get(&pos) self.chunks.get(&pos.into())
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
@ -51,6 +52,27 @@ impl Chunks {
pub fn par_iter(&self) -> impl ParallelIterator<Item = (ChunkPos, &Chunk)> + Clone + '_ { pub fn par_iter(&self) -> impl ParallelIterator<Item = (ChunkPos, &Chunk)> + Clone + '_ {
self.chunks.par_iter().map(|(&pos, chunk)| (pos, chunk)) self.chunks.par_iter().map(|(&pos, chunk)| (pos, chunk))
} }
pub fn get_block_state(&self, pos: impl Into<BlockPos>) -> Option<BlockState> {
let pos = pos.into();
let chunk_pos = ChunkPos::new(pos.x / 16, pos.z / 16);
let chunk = self.get(chunk_pos)?;
let min_y = self.server.dimension(self.dimension).min_y;
let y = pos.y.checked_sub(min_y)?.try_into().ok()?;
if y < chunk.height() {
Some(chunk.get_block_state(
pos.x.rem_euclid(16) as usize,
y,
pos.z.rem_euclid(16) as usize,
))
} else {
None
}
}
} }
impl<'a> ChunksMut<'a> { impl<'a> ChunksMut<'a> {
@ -59,7 +81,8 @@ impl<'a> ChunksMut<'a> {
} }
pub fn create(&mut self, pos: impl Into<ChunkPos>) -> bool { pub fn create(&mut self, pos: impl Into<ChunkPos>) -> bool {
let chunk = Chunk::new(self.section_count, self.server.current_tick()); let section_count = (self.server.dimension(self.dimension).height / 16) as u32;
let chunk = Chunk::new(section_count, self.server.current_tick());
self.0.chunks.insert(pos.into(), chunk).is_none() self.0.chunks.insert(pos.into(), chunk).is_none()
} }
@ -67,8 +90,8 @@ impl<'a> ChunksMut<'a> {
self.0.chunks.remove(&pos).is_some() self.0.chunks.remove(&pos).is_some()
} }
pub fn get_mut(&mut self, pos: ChunkPos) -> Option<ChunkMut> { pub fn get_mut(&mut self, pos: impl Into<ChunkPos>) -> Option<ChunkMut> {
self.0.chunks.get_mut(&pos).map(ChunkMut) self.0.chunks.get_mut(&pos.into()).map(ChunkMut)
} }
pub fn iter_mut(&mut self) -> impl FusedIterator<Item = (ChunkPos, ChunkMut)> + '_ { pub fn iter_mut(&mut self) -> impl FusedIterator<Item = (ChunkPos, ChunkMut)> + '_ {
@ -84,6 +107,29 @@ impl<'a> ChunksMut<'a> {
.par_iter_mut() .par_iter_mut()
.map(|(&pos, chunk)| (pos, ChunkMut(chunk))) .map(|(&pos, chunk)| (pos, ChunkMut(chunk)))
} }
pub fn set_block_state(&mut self, pos: impl Into<BlockPos>, block: BlockState) -> bool {
let pos = pos.into();
let chunk_pos = ChunkPos::new(pos.x / 16, pos.z / 16);
if let Some(chunk) = self.0.chunks.get_mut(&chunk_pos) {
let min_y = self.0.server.dimension(self.0.dimension).min_y;
if let Some(y) = pos.y.checked_sub(min_y).and_then(|y| y.try_into().ok()) {
if y < chunk.height() {
ChunkMut(chunk).set_block_state(
pos.x.rem_euclid(16) as usize,
y,
pos.z.rem_euclid(16) as usize,
block,
);
return true;
}
}
}
false
}
} }
pub struct ChunksMut<'a>(&'a mut Chunks); pub struct ChunksMut<'a>(&'a mut Chunks);
@ -101,23 +147,21 @@ pub struct Chunk {
// TODO block_entities: HashMap<u32, BlockEntity>, // TODO block_entities: HashMap<u32, BlockEntity>,
/// The MOTION_BLOCKING heightmap /// The MOTION_BLOCKING heightmap
heightmap: Vec<i64>, heightmap: Vec<i64>,
modified: bool,
created_tick: Ticks, created_tick: Ticks,
} }
impl Chunk { impl Chunk {
pub(crate) fn new(section_count: u32, current_tick: Ticks) -> Self { pub(crate) fn new(section_count: u32, current_tick: Ticks) -> Self {
let sect = ChunkSection { let sect = ChunkSection {
blocks: [BlockState::default(); 4096], blocks: [BlockState::AIR.to_raw(); 4096],
modified_count: 1, // Must be >0 so the chunk is initialized.
biomes: [BiomeId::default(); 64], biomes: [BiomeId::default(); 64],
compact_data: Vec::new(), compact_data: Vec::new(),
modified: true,
}; };
let mut chunk = Self { let mut chunk = Self {
sections: vec![sect; section_count as usize].into(), sections: vec![sect; section_count as usize].into(),
heightmap: Vec::new(), heightmap: Vec::new(),
modified: true,
created_tick: current_tick, created_tick: current_tick,
}; };
@ -135,7 +179,9 @@ impl Chunk {
pub fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState { pub fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
if x < 16 && y < self.height() && z < 16 { if x < 16 && y < self.height() && z < 16 {
self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] BlockState::from_raw_unchecked(
self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK,
)
} else { } else {
BlockState::AIR BlockState::AIR
} }
@ -178,14 +224,60 @@ impl Chunk {
} }
} }
/// Gets the unapplied changes to this chunk as a block change packet. /// Returns unapplied changes to this chunk as block change packets through
pub(crate) fn block_change_packet(&self, pos: ChunkPos) -> Option<BlockChangePacket> { /// the provided closure.
if !self.modified { pub(crate) fn block_change_packets(
return None; &self,
} pos: ChunkPos,
min_y: i32,
mut packet: impl FnMut(BlockChangePacket),
) {
for (sect_y, sect) in self.sections.iter().enumerate() {
if sect.modified_count == 1 {
let (idx, &block) = sect
.blocks
.iter()
.enumerate()
.find(|&(_, &b)| b & !BLOCK_STATE_MASK != 0)
.expect("invalid modified count");
// TODO let global_x = pos.x * 16 + (idx % 16) as i32;
None let global_y = sect_y as i32 * 16 + (idx / (16 * 16)) as i32 + min_y;
let global_z = pos.z * 16 + (idx / 16 % 16) as i32;
packet(BlockChangePacket::Single(BlockChange {
location: BlockPos::new(global_x, global_y, global_z),
block_id: VarInt((block & BLOCK_STATE_MASK).into()),
}));
} else if sect.modified_count > 1 {
let mut blocks = Vec::new();
for y in 0..16 {
for z in 0..16 {
for x in 0..16 {
let block =
sect.blocks[x as usize + z as usize * 16 + y as usize * 16 * 16];
if block & !BLOCK_STATE_MASK != 0 {
blocks.push(VarLong(
((block & BLOCK_STATE_MASK) as i64) << 12
| (x << 8 | z << 4 | y),
))
}
}
}
}
let chunk_section_position = (pos.x as i64) << 42
| (pos.z as i64 & 0x3fffff) << 20
| (sect_y as i64) & 0xfffff;
packet(BlockChangePacket::Multi(MultiBlockChange {
chunk_section_position,
invert_trust_edges: false,
blocks,
}));
}
}
} }
} }
@ -201,62 +293,79 @@ impl<'a> Deref for ChunkMut<'a> {
impl<'a> ChunkMut<'a> { impl<'a> ChunkMut<'a> {
pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) {
if x < 16 && y < self.height() && z < 16 { assert!(
let sec = &mut self.0.sections[y / 16]; x < 16 && y < self.height() && z < 16,
let idx = x + z * 16 + y % 16 * 16 * 16; "the chunk block coordinates must be within bounds"
if block != sec.blocks[idx] { );
sec.blocks[idx] = block;
// TODO: set the modified bit. let sect = &mut self.0.sections[y / 16];
sec.modified = true; let idx = x + z * 16 + y % 16 * 16 * 16;
self.0.modified = true;
// TODO: update block entity if b could have block entity data. if block.to_raw() != sect.blocks[idx] & BLOCK_STATE_MASK {
if sect.blocks[idx] & !BLOCK_STATE_MASK == 0 {
sect.modified_count += 1;
} }
sect.blocks[idx] = block.to_raw() | !BLOCK_STATE_MASK;
// TODO: if the block type was modified and the old block type
// could be a block entity, then the block entity at this
// position must be cleared.
} }
} }
pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) { pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) {
if x < 4 && y < self.height() / 4 && z < 4 { assert!(
self.0.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b; x < 4 && y < self.height() / 4 && z < 4,
} "the chunk biome coordinates must be within bounds"
);
self.0.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b;
} }
pub(crate) fn apply_modifications(&mut self) { pub(crate) fn apply_modifications(&mut self) {
if self.modified { let mut any_modified = false;
self.0.modified = false;
for sect in self.0.sections.iter_mut() { for sect in self.0.sections.iter_mut() {
if sect.modified { if sect.modified_count > 0 {
sect.modified = false; sect.modified_count = 0;
any_modified = true;
sect.compact_data.clear(); sect.compact_data.clear();
let non_air_block_count = sect.blocks.iter().filter(|&&b| !b.is_air()).count(); let mut non_air_block_count: i16 = 0;
(non_air_block_count as i16) for b in &mut sect.blocks {
.encode(&mut sect.compact_data) *b &= BLOCK_STATE_MASK;
.unwrap(); if !BlockState::from_raw_unchecked(*b).is_air() {
non_air_block_count += 1;
encode_paletted_container( }
sect.blocks.iter().map(|b| b.to_raw()),
4,
9,
15,
&mut sect.compact_data,
)
.unwrap();
// TODO: The direct bits per idx changes depending on the number of biomes in
// the biome registry.
encode_paletted_container(
sect.biomes.iter().map(|b| b.0),
0,
4,
6,
&mut sect.compact_data,
)
.unwrap();
} }
}
non_air_block_count.encode(&mut sect.compact_data).unwrap();
encode_paletted_container(
sect.blocks.iter().cloned(),
4,
9,
log2_ceil((BlockState::max_raw() + 1) as usize),
&mut sect.compact_data,
)
.unwrap();
// TODO: The direct bits per idx changes depending on the number of biomes in
// the biome registry.
encode_paletted_container(
sect.biomes.iter().map(|b| b.0),
0,
4,
6,
&mut sect.compact_data,
)
.unwrap();
}
}
if any_modified {
build_heightmap(&self.0.sections, &mut self.0.heightmap); build_heightmap(&self.0.sections, &mut self.0.heightmap);
} }
} }
@ -323,14 +432,25 @@ impl Into<[i32; 2]> for ChunkPos {
/// A 16x16x16 section of blocks, biomes, and light in a chunk. /// A 16x16x16 section of blocks, biomes, and light in a chunk.
#[derive(Clone)] #[derive(Clone)]
struct ChunkSection { struct ChunkSection {
/// The blocks in this section, stored in x, z, y order. /// The block states in this section, stored in x, z, y order.
blocks: [BlockState; 4096], /// The most significant bit is used to indicate if this block has been
/// modified.
blocks: [u16; 4096],
/// The number of modified blocks
modified_count: u16,
biomes: [BiomeId; 64], biomes: [BiomeId; 64],
compact_data: Vec<u8>, compact_data: Vec<u8>,
/// If the blocks or biomes were modified.
modified: bool,
} }
const BLOCK_STATE_MASK: u16 = 0x7fff;
const _: () = assert!(
BlockState::max_raw() <= BLOCK_STATE_MASK,
"There is not enough space in the block state type to store the modified bit. A bit array \
separate from the block state array should be created to keep track of modified blocks in \
the chunk section."
);
/// Builds the MOTION_BLOCKING heightmap. /// Builds the MOTION_BLOCKING heightmap.
fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) { fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
let height = sections.len() * 16; let height = sections.len() * 16;
@ -344,7 +464,10 @@ fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
for x in 0..16 { for x in 0..16 {
for z in 0..16 { for z in 0..16 {
for y in (0..height).rev() { for y in (0..height).rev() {
let block = sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16]; let block = BlockState::from_raw_unchecked(
sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK,
);
// TODO: is_solid || is_fluid heuristic for motion blocking. // TODO: is_solid || is_fluid heuristic for motion blocking.
if !block.is_air() { if !block.is_air() {
let column_height = y as u64; let column_height = y as u64;
@ -438,3 +561,38 @@ fn encode_paletted_container(
fn log2_ceil(n: usize) -> usize { fn log2_ceil(n: usize) -> usize {
n.next_power_of_two().trailing_zeros() as usize n.next_power_of_two().trailing_zeros() as usize
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_get() {
let mut chunk = Chunk::new(16, 0);
let mut chunk = ChunkMut(&mut chunk);
chunk.set_block_state(1, 2, 3, BlockState::CAKE);
assert_eq!(chunk.get_block_state(1, 2, 3), BlockState::CAKE);
chunk.set_biome(1, 2, 3, BiomeId(7));
assert_eq!(chunk.get_biome(1, 2, 3), BiomeId(7));
}
#[test]
#[should_panic]
fn block_state_oob() {
let mut chunk = Chunk::new(16, 0);
let mut chunk = ChunkMut(&mut chunk);
chunk.set_block_state(16, 0, 0, BlockState::CAKE);
}
#[test]
#[should_panic]
fn biome_oob() {
let mut chunk = Chunk::new(16, 0);
let mut chunk = ChunkMut(&mut chunk);
chunk.set_biome(4, 0, 0, BiomeId(0));
}
}

View file

@ -485,6 +485,8 @@ impl<'a> ClientMut<'a> {
} }
} }
let dimension = server.dimension(meta.dimension());
// Update existing chunks and unload those outside the view distance. Chunks // Update existing chunks and unload those outside the view distance. Chunks
// that have been overwritten also need to be unloaded. // that have been overwritten also need to be unloaded.
self.0.loaded_chunks.retain(|&pos| { self.0.loaded_chunks.retain(|&pos| {
@ -496,9 +498,9 @@ impl<'a> ClientMut<'a> {
if is_chunk_in_view_distance(center, pos, view_dist + cache) if is_chunk_in_view_distance(center, pos, view_dist + cache)
&& chunk.created_tick() != current_tick && chunk.created_tick() != current_tick
{ {
if let Some(pkt) = chunk.block_change_packet(pos) { chunk.block_change_packets(pos, dimension.min_y, |pkt| {
send_packet(&mut self.0.send, pkt); send_packet(&mut self.0.send, pkt)
} });
return true; return true;
} }
} }
@ -518,9 +520,7 @@ impl<'a> ClientMut<'a> {
if let Some(chunk) = chunks.get(pos) { if let Some(chunk) = chunks.get(pos) {
if self.0.loaded_chunks.insert(pos) { if self.0.loaded_chunks.insert(pos) {
self.send_packet(chunk.chunk_data_packet(pos)); self.send_packet(chunk.chunk_data_packet(pos));
if let Some(pkt) = chunk.block_change_packet(pos) { chunk.block_change_packets(pos, dimension.min_y, |pkt| self.send_packet(pkt));
self.send_packet(pkt);
}
} }
} }
} }

View file

@ -1028,9 +1028,9 @@ pub mod play {
def_struct! { def_struct! {
MultiBlockChange 0x3d { MultiBlockChange 0x3d {
chunk_section_position: u64, chunk_section_position: i64,
invert_trust_edges: bool, invert_trust_edges: bool,
blocks: Vec<u64>, blocks: Vec<VarLong>,
} }
} }

View file

@ -6,7 +6,7 @@ use byteorder::{ReadBytesExt, WriteBytesExt};
use crate::protocol::{Decode, Encode}; use crate::protocol::{Decode, Encode};
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct VarLong(i64); pub struct VarLong(pub(crate) i64);
impl VarLong { impl VarLong {
/// The maximum number of bytes a `VarLong` can occupy when read from and /// The maximum number of bytes a `VarLong` can occupy when read from and

View file

@ -62,10 +62,7 @@ impl<'a> WorldsMut<'a> {
clients: Clients::new(), clients: Clients::new(),
entities: Entities::new(), entities: Entities::new(),
spatial_index: SpatialIndex::new(), spatial_index: SpatialIndex::new(),
chunks: Chunks::new( chunks: Chunks::new(self.server.clone(), dim),
self.server.clone(),
(self.server.dimension(dim).height / 16) as u32,
),
meta: WorldMeta { meta: WorldMeta {
dimension: dim, dimension: dim,
is_flat: false, is_flat: false,