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.
///
/// The original block state can be recovered with [`BlockState::from_raw`].
@ -337,6 +342,11 @@ pub fn build() -> anyhow::Result<()> {
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.
///
/// If this block does not have the property, then `None` is returned.

View file

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

View file

@ -201,7 +201,7 @@ fn terrain_column(
2.0,
0.5,
) * 6.0)
.floor() as i64;
.floor() as i64;
if *in_terrain {
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 {
fn from([x, y, z]: [i32; 3]) -> Self {
BlockPos::new(x, y, z)
}
}
impl From<BlockPos> for [i32; 3] {
fn from(pos: BlockPos) -> Self {
[pos.x, pos.y, pos.z]
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -15,20 +15,21 @@ use crate::packets::play::s2c::{
};
use crate::protocol::{Encode, Nbt};
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 {
chunks: HashMap<ChunkPos, Chunk>,
server: Server,
section_count: u32,
dimension: DimensionId,
}
impl Chunks {
pub(crate) fn new(server: Server, section_count: u32) -> Self {
pub(crate) fn new(server: Server, dimension: DimensionId) -> Self {
Self {
chunks: HashMap::new(),
server,
section_count,
dimension,
}
}
@ -36,8 +37,8 @@ impl Chunks {
self.chunks.len()
}
pub fn get(&self, pos: ChunkPos) -> Option<&Chunk> {
self.chunks.get(&pos)
pub fn get(&self, pos: impl Into<ChunkPos>) -> Option<&Chunk> {
self.chunks.get(&pos.into())
}
pub fn clear(&mut self) {
@ -51,6 +52,27 @@ impl Chunks {
pub fn par_iter(&self) -> impl ParallelIterator<Item = (ChunkPos, &Chunk)> + Clone + '_ {
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> {
@ -59,7 +81,8 @@ impl<'a> ChunksMut<'a> {
}
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()
}
@ -67,8 +90,8 @@ impl<'a> ChunksMut<'a> {
self.0.chunks.remove(&pos).is_some()
}
pub fn get_mut(&mut self, pos: ChunkPos) -> Option<ChunkMut> {
self.0.chunks.get_mut(&pos).map(ChunkMut)
pub fn get_mut(&mut self, pos: impl Into<ChunkPos>) -> Option<ChunkMut> {
self.0.chunks.get_mut(&pos.into()).map(ChunkMut)
}
pub fn iter_mut(&mut self) -> impl FusedIterator<Item = (ChunkPos, ChunkMut)> + '_ {
@ -84,6 +107,29 @@ impl<'a> ChunksMut<'a> {
.par_iter_mut()
.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);
@ -101,23 +147,21 @@ pub struct Chunk {
// TODO block_entities: HashMap<u32, BlockEntity>,
/// The MOTION_BLOCKING heightmap
heightmap: Vec<i64>,
modified: bool,
created_tick: Ticks,
}
impl Chunk {
pub(crate) fn new(section_count: u32, current_tick: Ticks) -> Self {
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],
compact_data: Vec::new(),
modified: true,
};
let mut chunk = Self {
sections: vec![sect; section_count as usize].into(),
heightmap: Vec::new(),
modified: true,
created_tick: current_tick,
};
@ -135,7 +179,9 @@ impl Chunk {
pub fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
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 {
BlockState::AIR
}
@ -178,14 +224,60 @@ impl Chunk {
}
}
/// Gets the unapplied changes to this chunk as a block change packet.
pub(crate) fn block_change_packet(&self, pos: ChunkPos) -> Option<BlockChangePacket> {
if !self.modified {
return None;
}
/// Returns unapplied changes to this chunk as block change packets through
/// the provided closure.
pub(crate) fn block_change_packets(
&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
None
let global_x = pos.x * 16 + (idx % 16) as i32;
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> {
pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) {
if x < 16 && y < self.height() && z < 16 {
let sec = &mut self.0.sections[y / 16];
let idx = x + z * 16 + y % 16 * 16 * 16;
if block != sec.blocks[idx] {
sec.blocks[idx] = block;
// TODO: set the modified bit.
sec.modified = true;
self.0.modified = true;
// TODO: update block entity if b could have block entity data.
assert!(
x < 16 && y < self.height() && z < 16,
"the chunk block coordinates must be within bounds"
);
let sect = &mut self.0.sections[y / 16];
let idx = x + z * 16 + y % 16 * 16 * 16;
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) {
if x < 4 && y < self.height() / 4 && z < 4 {
self.0.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b;
}
assert!(
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) {
if self.modified {
self.0.modified = false;
let mut any_modified = false;
for sect in self.0.sections.iter_mut() {
if sect.modified {
sect.modified = false;
for sect in self.0.sections.iter_mut() {
if sect.modified_count > 0 {
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)
.encode(&mut sect.compact_data)
.unwrap();
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();
for b in &mut sect.blocks {
*b &= BLOCK_STATE_MASK;
if !BlockState::from_raw_unchecked(*b).is_air() {
non_air_block_count += 1;
}
}
}
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);
}
}
@ -323,14 +432,25 @@ impl Into<[i32; 2]> for ChunkPos {
/// A 16x16x16 section of blocks, biomes, and light in a chunk.
#[derive(Clone)]
struct ChunkSection {
/// The blocks in this section, stored in x, z, y order.
blocks: [BlockState; 4096],
/// The block states in this section, stored in x, z, y order.
/// 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],
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.
fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
let height = sections.len() * 16;
@ -344,7 +464,10 @@ fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
for x in 0..16 {
for z in 0..16 {
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.
if !block.is_air() {
let column_height = y as u64;
@ -438,3 +561,38 @@ fn encode_paletted_container(
fn log2_ceil(n: usize) -> 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
// that have been overwritten also need to be unloaded.
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)
&& chunk.created_tick() != current_tick
{
if let Some(pkt) = chunk.block_change_packet(pos) {
send_packet(&mut self.0.send, pkt);
}
chunk.block_change_packets(pos, dimension.min_y, |pkt| {
send_packet(&mut self.0.send, pkt)
});
return true;
}
}
@ -518,9 +520,7 @@ impl<'a> ClientMut<'a> {
if let Some(chunk) = chunks.get(pos) {
if self.0.loaded_chunks.insert(pos) {
self.send_packet(chunk.chunk_data_packet(pos));
if let Some(pkt) = chunk.block_change_packet(pos) {
self.send_packet(pkt);
}
chunk.block_change_packets(pos, dimension.min_y, |pkt| self.send_packet(pkt));
}
}
}

View file

@ -1028,9 +1028,9 @@ pub mod play {
def_struct! {
MultiBlockChange 0x3d {
chunk_section_position: u64,
chunk_section_position: i64,
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};
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct VarLong(i64);
pub struct VarLong(pub(crate) i64);
impl VarLong {
/// 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(),
entities: Entities::new(),
spatial_index: SpatialIndex::new(),
chunks: Chunks::new(
self.server.clone(),
(self.server.dimension(dim).height / 16) as u32,
),
chunks: Chunks::new(self.server.clone(), dim),
meta: WorldMeta {
dimension: dim,
is_flat: false,