diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index 0522d7e5..25c9757d 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -9,6 +9,7 @@ use azalea_entity::{ Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection, Physics, PlayerBundle, Position, RelativeEntityUpdate, }; +use azalea_nbt::NbtCompound; use azalea_protocol::{ connect::{ReadConnection, WriteConnection}, packets::game::{ @@ -578,9 +579,20 @@ pub fn process_packet_events(ecs: &mut World) { } } + let heightmaps = p + .chunk_data + .heightmaps + .as_compound() + .and_then(|c| c.get("")) + .and_then(|c| c.as_compound()); + // necessary to make the unwrap_or work + let empty_nbt_compound = NbtCompound::default(); + let heightmaps = heightmaps.unwrap_or(&empty_nbt_compound); + if let Err(e) = partial_world.chunks.replace_with_packet_data( &pos, &mut Cursor::new(&p.chunk_data.data), + heightmaps, &mut world.chunks, ) { error!("Couldn't set chunk data: {}", e); @@ -632,6 +644,7 @@ pub fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::SetEntityData(p) => { debug!("Got set entity data packet {:?}", p); + #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, Query<(&EntityIdIndex, &LocalPlayer)>, diff --git a/azalea-core/src/math.rs b/azalea-core/src/math.rs index 6683dd10..01695a0a 100644 --- a/azalea-core/src/math.rs +++ b/azalea-core/src/math.rs @@ -9,17 +9,17 @@ pub static SIN: LazyLock<[f32; 65536]> = LazyLock::new(|| { }); /// A sine function that uses a lookup table. -pub fn sin(var0: f32) -> f32 { - let var0 = var0 * 10430.378; - let var0 = var0 as usize; - SIN[var0 & 65535] +pub fn sin(x: f32) -> f32 { + let x = x * 10430.378; + let x = x as usize; + SIN[x & 65535] } /// A cosine function that uses a lookup table. -pub fn cos(var0: f32) -> f32 { - let var0 = var0 * 10430.378 + 16384.0; - let var0 = var0 as usize; - SIN[var0 & 65535] +pub fn cos(x: f32) -> f32 { + let x = x * 10430.378 + 16384.0; + let x = x as usize; + SIN[x & 65535] } // TODO: make this generic @@ -56,6 +56,10 @@ pub fn lerp(amount: T, a: T, b: T) -> T { a + amount * (b - a) } +pub fn ceil_log2(x: u32) -> u32 { + u32::BITS - x.leading_zeros() +} + #[cfg(test)] mod tests { use super::*; diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index eb6bcd89..f8072fa4 100755 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -346,6 +346,17 @@ impl From for ChunkPos { } } +impl From<&Vec3> for ChunkBlockPos { + fn from(pos: &Vec3) -> Self { + ChunkBlockPos::from(&BlockPos::from(pos)) + } +} +impl From for ChunkBlockPos { + fn from(pos: Vec3) -> Self { + ChunkBlockPos::from(&pos) + } +} + const PACKED_X_LENGTH: u64 = 1 + 25; // minecraft does something a bit more complicated to get this 25 const PACKED_Z_LENGTH: u64 = PACKED_X_LENGTH; const PACKED_Y_LENGTH: u64 = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH; diff --git a/azalea-protocol/src/packets/game/clientbound_level_chunk_with_light_packet.rs b/azalea-protocol/src/packets/game/clientbound_level_chunk_with_light_packet.rs index 2fc00f83..c10fa737 100755 --- a/azalea-protocol/src/packets/game/clientbound_level_chunk_with_light_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_level_chunk_with_light_packet.rs @@ -27,3 +27,34 @@ pub struct BlockEntity { pub kind: azalea_registry::BlockEntityKind, pub data: Nbt, } + +// Compound(NbtCompound { +// inner: [("", Compound(NbtCompound { +// inner: [ +// ("MOTION_BLOCKING", LongArray([2310355422147575936, +// 2292305770412047999, 2310355422147575423, 2292305770412310656, +// 2310355422013095551, 2292305839266005120, 2310320168921529983, +// 2310355422147575936, 2292305770412048512, 2310355422147575935, +// 2292305839266005120, 2310355422147313279, 2292305770546528384, +// 2310355353293618815, 2292305839266005120, 2292305770412047999, +// 2310355422147575936, 2292305770412047999, 2310355422147575423, +// 2292305770412048512, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 17079008895])), +// ("WORLD_SURFACE", LongArray([2310355422147575936, +// 2292340954784136831, 2310355422147575423, 2292305770412310656, +// 2310355422013095551, 2292305839266005120, 2310320168921529983, +// 2310355422147575936, 2292305770412048512, 2310355422147575935, +// 2292305839266005120, 2310355422147313279, 2292305770546528384, +// 2310355353293618815, 2292305839266005120, 2292305770412047999, +// 2310355422147575936, 2292305770412047999, 2310355422147575423, +// 2292305770412048512, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 2292305770412047999, +// 2292305770412047999, 2292305770412047999, 17079008895]))] }))] +// }) diff --git a/azalea-world/src/bit_storage.rs b/azalea-world/src/bit_storage.rs index 09b68fae..9f9b7abf 100755 --- a/azalea-world/src/bit_storage.rs +++ b/azalea-world/src/bit_storage.rs @@ -106,7 +106,7 @@ impl BitStorage { // 0 bit storage if data.is_empty() { return Ok(BitStorage { - data: Vec::with_capacity(0), + data: Vec::new(), bits, size, ..Default::default() diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index 133d522b..8d894d9a 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -1,10 +1,14 @@ +use crate::heightmap::Heightmap; +use crate::heightmap::HeightmapKind; use crate::palette::PalettedContainer; use crate::palette::PalettedContainerKind; use azalea_block::BlockState; use azalea_buf::{BufReadError, McBufReadable, McBufWritable}; use azalea_core::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos}; +use azalea_nbt::NbtCompound; use log::{debug, trace, warn}; use parking_lot::RwLock; +use std::str::FromStr; use std::{ collections::HashMap, fmt::Debug, @@ -43,6 +47,10 @@ pub struct ChunkStorage { #[derive(Debug)] pub struct Chunk { pub sections: Vec
, + /// Heightmaps are used for identifying the surface blocks in a chunk. + /// Usually for clients only `WorldSurface` and `MotionBlocking` are + /// present. + pub heightmaps: HashMap, } /// A section of a chunk, i.e. a 16*16*16 block area. @@ -73,6 +81,7 @@ impl Default for Chunk { fn default() -> Self { Chunk { sections: vec![Section::default(); (384 / 16) as usize], + heightmaps: HashMap::new(), } } } @@ -119,6 +128,7 @@ impl PartialChunkStorage { &mut self, pos: &ChunkPos, data: &mut Cursor<&[u8]>, + heightmaps: &NbtCompound, chunk_storage: &mut ChunkStorage, ) -> Result<(), BufReadError> { debug!("Replacing chunk at {:?}", pos); @@ -127,7 +137,12 @@ impl PartialChunkStorage { return Ok(()); } - let chunk = Chunk::read_with_dimension_height(data, chunk_storage.height)?; + let chunk = Chunk::read_with_dimension_height( + data, + chunk_storage.height, + chunk_storage.min_y, + heightmaps, + )?; trace!("Loaded chunk {:?}", pos); self.set(pos, Some(chunk), chunk_storage); @@ -229,6 +244,8 @@ impl Chunk { pub fn read_with_dimension_height( buf: &mut Cursor<&[u8]>, dimension_height: u32, + min_y: i32, + heightmaps_nbt: &NbtCompound, ) -> Result { let section_count = dimension_height / SECTION_HEIGHT; let mut sections = Vec::with_capacity(section_count as usize); @@ -236,23 +253,30 @@ impl Chunk { let section = Section::read_from(buf)?; sections.push(section); } - Ok(Chunk { sections }) + + let mut heightmaps = HashMap::new(); + for (name, heightmap) in heightmaps_nbt.iter() { + let Ok(kind) = HeightmapKind::from_str(name) else { + warn!("Unknown heightmap kind: {name}"); + continue; + }; + let Some(data) = heightmap.as_long_array() else { + warn!("Heightmap {name} is not a long array"); + continue; + }; + let data: Vec = data.iter().map(|x| *x as u64).collect(); + let heightmap = Heightmap::new(kind, dimension_height, min_y, data); + heightmaps.insert(kind, heightmap); + } + + Ok(Chunk { + sections, + heightmaps, + }) } pub fn get(&self, pos: &ChunkBlockPos, min_y: i32) -> Option { - if pos.y < min_y { - // y position is out of bounds - return None; - } - let section_index = section_index(pos.y, min_y) as usize; - if section_index >= self.sections.len() { - // y position is out of bounds - return None; - }; - // TODO: make sure the section exists - let section = &self.sections[section_index]; - let chunk_section_pos = ChunkSectionBlockPos::from(pos); - Some(section.get(chunk_section_pos)) + get_block_state_from_sections(&self.sections, pos, min_y) } pub fn get_and_set( @@ -265,7 +289,13 @@ impl Chunk { // TODO: make sure the section exists let section = &mut self.sections[section_index as usize]; let chunk_section_pos = ChunkSectionBlockPos::from(pos); - section.get_and_set(chunk_section_pos, state) + let previous_state = section.get_and_set(chunk_section_pos, state); + + for heightmap in self.heightmaps.values_mut() { + heightmap.update(pos, state, &self.sections); + } + + previous_state } pub fn set(&mut self, pos: &ChunkBlockPos, state: BlockState, min_y: i32) { @@ -274,9 +304,35 @@ impl Chunk { let section = &mut self.sections[section_index as usize]; let chunk_section_pos = ChunkSectionBlockPos::from(pos); section.set(chunk_section_pos, state); + + for heightmap in self.heightmaps.values_mut() { + heightmap.update(pos, state, &self.sections); + } } } +/// Get the block state at the given position from a list of sections. Returns +/// `None` if the position is out of bounds. +pub fn get_block_state_from_sections( + sections: &[Section], + pos: &ChunkBlockPos, + min_y: i32, +) -> Option { + if pos.y < min_y { + // y position is out of bounds + return None; + } + let section_index = section_index(pos.y, min_y) as usize; + if section_index >= sections.len() { + // y position is out of bounds + return None; + }; + // TODO: make sure the section exists + let section = §ions[section_index]; + let chunk_section_pos = ChunkSectionBlockPos::from(pos); + Some(section.get(chunk_section_pos)) +} + impl McBufWritable for Chunk { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { for section in &self.sections { diff --git a/azalea-world/src/heightmap.rs b/azalea-world/src/heightmap.rs new file mode 100644 index 00000000..ec73adf9 --- /dev/null +++ b/azalea-world/src/heightmap.rs @@ -0,0 +1,151 @@ +use std::{fmt::Display, str::FromStr}; + +use azalea_block::BlockState; +use azalea_core::{math, ChunkBlockPos}; +use azalea_registry::tags::blocks::LEAVES; + +use crate::{chunk_storage::get_block_state_from_sections, BitStorage, Section}; + +// (wg stands for worldgen) + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum HeightmapKind { + WorldSurfaceWg, + WorldSurface, + OceanFloorWg, + OceanFloor, + MotionBlocking, + MotionBlockingNoLeaves, +} + +#[derive(Clone, Debug)] +pub struct Heightmap { + pub data: BitStorage, + pub min_y: i32, + pub kind: HeightmapKind, +} + +fn blocks_motion(block_state: BlockState) -> bool { + // TODO + !block_state.is_air() +} + +fn motion_blocking(block_state: BlockState) -> bool { + // TODO + !block_state.is_air() || block_state.waterlogged() +} + +impl HeightmapKind { + pub fn is_opaque(self, block_state: BlockState) -> bool { + let block = Box::::from(block_state); + let registry_block = block.as_registry_block(); + match self { + HeightmapKind::WorldSurfaceWg => !block_state.is_air(), + HeightmapKind::WorldSurface => !block_state.is_air(), + HeightmapKind::OceanFloorWg => blocks_motion(block_state), + HeightmapKind::OceanFloor => blocks_motion(block_state), + HeightmapKind::MotionBlocking => motion_blocking(block_state), + HeightmapKind::MotionBlockingNoLeaves => { + motion_blocking(block_state) && !LEAVES.contains(®istry_block) + } + } + } +} + +impl Heightmap { + pub fn new(kind: HeightmapKind, dimension_height: u32, min_y: i32, data: Vec) -> Self { + let bits = math::ceil_log2(dimension_height + 1); + let data = BitStorage::new(bits as usize, 16 * 16, Some(data)).unwrap(); + Self { kind, data, min_y } + } + + pub fn get_index(x: u8, z: u8) -> usize { + (x as usize) + (z as usize) * 16 + } + + pub fn get_first_available_at_index(&self, index: usize) -> i32 { + self.data.get(index) as i32 + self.min_y + } + + pub fn get_first_available(&self, x: u8, z: u8) -> i32 { + self.get_first_available_at_index(Self::get_index(x, z)) + } + + pub fn get_highest_taken(&self, x: u8, z: u8) -> i32 { + self.get_first_available(x, z) - 1 + } + + pub fn set_height(&mut self, x: u8, z: u8, height: i32) { + self.data + .set(Self::get_index(x, z), (height - self.min_y) as u64); + } + + /// Updates the heightmap with the given block state at the given position. + pub fn update( + &mut self, + pos: &ChunkBlockPos, + block_state: BlockState, + sections: &[Section], + ) -> bool { + let first_available_y = self.get_first_available(pos.x, pos.z); + if pos.y <= first_available_y - 2 { + return false; + } + if self.kind.is_opaque(block_state) { + // increase y + if pos.y >= first_available_y { + self.set_height(pos.x, pos.z, pos.y + 1); + return true; + } + } else if first_available_y - 1 == pos.y { + // decrease y + for y in (self.min_y..pos.y).rev() { + if self.kind.is_opaque( + get_block_state_from_sections( + sections, + &ChunkBlockPos::new(pos.x, y, pos.z), + self.min_y, + ) + .unwrap_or_default(), + ) { + self.set_height(pos.x, pos.z, y + 1); + return true; + } + } + + self.set_height(pos.x, pos.z, self.min_y); + return true; + } + + false + } +} + +impl FromStr for HeightmapKind { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "WORLD_SURFACE_WG" => Ok(HeightmapKind::WorldSurfaceWg), + "WORLD_SURFACE" => Ok(HeightmapKind::WorldSurface), + "OCEAN_FLOOR_WG" => Ok(HeightmapKind::OceanFloorWg), + "OCEAN_FLOOR" => Ok(HeightmapKind::OceanFloor), + "MOTION_BLOCKING" => Ok(HeightmapKind::MotionBlocking), + "MOTION_BLOCKING_NO_LEAVES" => Ok(HeightmapKind::MotionBlockingNoLeaves), + _ => Err(()), + } + } +} + +impl Display for HeightmapKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HeightmapKind::WorldSurfaceWg => write!(f, "WORLD_SURFACE_WG"), + HeightmapKind::WorldSurface => write!(f, "WORLD_SURFACE"), + HeightmapKind::OceanFloorWg => write!(f, "OCEAN_FLOOR_WG"), + HeightmapKind::OceanFloor => write!(f, "OCEAN_FLOOR"), + HeightmapKind::MotionBlocking => write!(f, "MOTION_BLOCKING"), + HeightmapKind::MotionBlockingNoLeaves => write!(f, "MOTION_BLOCKING_NO_LEAVES"), + } + } +} diff --git a/azalea-world/src/lib.rs b/azalea-world/src/lib.rs index adaafa1f..0d80f75d 100644 --- a/azalea-world/src/lib.rs +++ b/azalea-world/src/lib.rs @@ -5,6 +5,7 @@ mod bit_storage; mod chunk_storage; mod container; +pub mod heightmap; pub mod iterators; pub mod palette; mod world; diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs index c562041e..eaa36832 100644 --- a/azalea/examples/testbot.rs +++ b/azalea/examples/testbot.rs @@ -3,17 +3,18 @@ #![feature(type_alias_impl_trait)] use azalea::ecs::query::With; -use azalea::entity::metadata::Player; -use azalea::entity::{EyeHeight, Position}; +use azalea::entity::{metadata::Player, EyeHeight, Position}; use azalea::interact::HitResultComponent; use azalea::inventory::ItemSlot; use azalea::pathfinder::goals::BlockPosGoal; use azalea::protocol::packets::game::ClientboundGamePacket; -use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection}; -use azalea::{Account, Client, Event}; -use azalea_client::SprintDirection; -use azalea_core::Vec3; -use azalea_world::{InstanceName, MinecraftEntityId}; +use azalea::world::{heightmap::HeightmapKind, InstanceName, MinecraftEntityId}; +use azalea::SprintDirection; +use azalea::{prelude::*, swarm::prelude::*}; +use azalea::{ + Account, BlockPos, ChunkPos, Client, Event, GameProfileComponent, Vec3, WalkDirection, +}; +use azalea_core::ChunkBlockPos; use std::time::Duration; #[derive(Default, Clone, Component)] @@ -288,6 +289,26 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< bot.chat("no entities found"); } } + "heightmap" => { + let position = bot.position(); + let chunk_pos = ChunkPos::from(position); + let chunk_block_pos = ChunkBlockPos::from(position); + let chunk = bot.world().read().chunks.get(&chunk_pos); + if let Some(chunk) = chunk { + let heightmaps = &chunk.read().heightmaps; + let Some(world_surface_heightmap) = + heightmaps.get(&HeightmapKind::WorldSurface) + else { + bot.chat("no world surface heightmap"); + return Ok(()); + }; + let highest_y = world_surface_heightmap + .get_highest_taken(chunk_block_pos.x, chunk_block_pos.z); + bot.chat(&format!("highest_y: {highest_y}",)); + } else { + bot.chat("no chunk found"); + } + } _ => {} } } diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 9e2cfb13..397e6f90 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -20,7 +20,7 @@ pub use azalea_block as blocks; pub use azalea_brigadier as brigadier; pub use azalea_chat::FormattedText; pub use azalea_client::*; -pub use azalea_core::{BlockPos, Vec3}; +pub use azalea_core::{BlockPos, ChunkPos, Vec3}; pub use azalea_entity as entity; pub use azalea_protocol as protocol; pub use azalea_registry::{Block, EntityKind, Item};