From bb6b116cb81a64deeb5ee8c1d021f27dba1cbc58 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 19 Jun 2022 00:30:24 -0500 Subject: [PATCH] Improvements to azalea-world for entities --- Cargo.lock | 1 + azalea-client/src/client.rs | 187 ++++++++++++++++++++---------------- azalea-core/src/position.rs | 19 +++- azalea-entity/src/lib.rs | 17 ++-- azalea-world/Cargo.toml | 1 + azalea-world/src/chunk.rs | 33 ++++++- azalea-world/src/entity.rs | 47 ++++++++- azalea-world/src/lib.rs | 82 ++++++++++------ bot/src/main.rs | 12 ++- examples/pvp.rs | 2 +- 10 files changed, 263 insertions(+), 138 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74375949..a3ceba57 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,7 @@ dependencies = [ "azalea-entity", "azalea-nbt", "azalea-protocol", + "log", "nohash-hasher", ] diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index ff8729cb..dc2fe70f 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -21,9 +21,11 @@ use azalea_protocol::{ resolver, ServerAddress, }; use azalea_world::{ChunkStorage, EntityStorage, World}; -use std::{fmt::Debug, sync::Arc}; +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; -use tokio::sync::Mutex; #[derive(Default)] pub struct ClientState { @@ -55,7 +57,7 @@ pub enum ChatPacket { /// A player that you can control that is currently in a Minecraft server. pub struct Client { event_receiver: UnboundedReceiver, - pub conn: Arc>, + pub conn: Arc>, pub state: Arc>, // game_loop } @@ -63,6 +65,8 @@ pub struct Client { /// Whether we should ignore errors when decoding packets. const IGNORE_ERRORS: bool = !cfg!(debug_assertions); +struct HandleError(String); + impl Client { /// Connect to a Minecraft server with an account. pub async fn join(account: &Account, address: &ServerAddress) -> Result { @@ -137,7 +141,7 @@ impl Client { } }; - let conn = Arc::new(Mutex::new(conn)); + let conn = Arc::new(tokio::sync::Mutex::new(conn)); let (tx, rx) = mpsc::unbounded_channel(); @@ -161,14 +165,16 @@ impl Client { } async fn game_loop( - conn: Arc>, + conn: Arc>, tx: UnboundedSender, state: Arc>, ) { loop { let r = conn.lock().await.read().await; match r { - Ok(packet) => Self::handle(&packet, &tx, &state, &conn).await, + Ok(packet) => { + Self::handle(&packet, &tx, &state, &conn).await; + } Err(e) => { if IGNORE_ERRORS { println!("Error: {:?}", e); @@ -187,82 +193,79 @@ impl Client { packet: &GamePacket, tx: &UnboundedSender, state: &Arc>, - conn: &Arc>, - ) { + conn: &Arc>, + ) -> Result<(), HandleError> { match packet { GamePacket::ClientboundLoginPacket(p) => { println!("Got login packet {:?}", p); - let mut state = state.lock().await; + { + let mut state = state.lock()?; - // // write p into login.txt - // std::io::Write::write_all( - // &mut std::fs::File::create("login.txt").unwrap(), - // format!("{:#?}", p).as_bytes(), - // ) - // .unwrap(); + // // write p into login.txt + // std::io::Write::write_all( + // &mut std::fs::File::create("login.txt").unwrap(), + // format!("{:#?}", p).as_bytes(), + // ) + // .unwrap(); - state.player.entity.id = p.player_id; + state.player.entity.id = p.player_id; - // TODO: have registry_holder be a struct because this sucks rn - // best way would be to add serde support to azalea-nbt + // TODO: have registry_holder be a struct because this sucks rn + // best way would be to add serde support to azalea-nbt - let registry_holder = p - .registry_holder - .as_compound() - .expect("Registry holder is not a compound") - .get("") - .expect("No \"\" tag") - .as_compound() - .expect("\"\" tag is not a compound"); - let dimension_types = registry_holder - .get("minecraft:dimension_type") - .expect("No dimension_type tag") - .as_compound() - .expect("dimension_type is not a compound") - .get("value") - .expect("No dimension_type value") - .as_list() - .expect("dimension_type value is not a list"); - let dimension_type = dimension_types - .iter() - .find(|t| { - t.as_compound() - .expect("dimension_type value is not a compound") - .get("name") - .expect("No name tag") - .as_string() - .expect("name is not a string") - == p.dimension_type.to_string() - }) - .expect(&format!("No dimension_type with name {}", p.dimension_type)) - .as_compound() - .unwrap() - .get("element") - .expect("No element tag") - .as_compound() - .expect("element is not a compound"); - let height = (*dimension_type - .get("height") - .expect("No height tag") - .as_int() - .expect("height tag is not an int")) - .try_into() - .expect("height is not a u32"); - let min_y = (*dimension_type - .get("min_y") - .expect("No min_y tag") - .as_int() - .expect("min_y tag is not an int")) - .try_into() - .expect("min_y is not an i32"); + let registry_holder = p + .registry_holder + .as_compound() + .expect("Registry holder is not a compound") + .get("") + .expect("No \"\" tag") + .as_compound() + .expect("\"\" tag is not a compound"); + let dimension_types = registry_holder + .get("minecraft:dimension_type") + .expect("No dimension_type tag") + .as_compound() + .expect("dimension_type is not a compound") + .get("value") + .expect("No dimension_type value") + .as_list() + .expect("dimension_type value is not a list"); + let dimension_type = dimension_types + .iter() + .find(|t| { + t.as_compound() + .expect("dimension_type value is not a compound") + .get("name") + .expect("No name tag") + .as_string() + .expect("name is not a string") + == p.dimension_type.to_string() + }) + .expect(&format!("No dimension_type with name {}", p.dimension_type)) + .as_compound() + .unwrap() + .get("element") + .expect("No element tag") + .as_compound() + .expect("element is not a compound"); + let height = (*dimension_type + .get("height") + .expect("No height tag") + .as_int() + .expect("height tag is not an int")) + .try_into() + .expect("height is not a u32"); + let min_y = (*dimension_type + .get("min_y") + .expect("No min_y tag") + .as_int() + .expect("min_y tag is not an int")) + .try_into() + .expect("min_y is not an i32"); - state.world = Some(World { - height, - min_y, - storage: ChunkStorage::new(16), - entities: EntityStorage::new(), - }); + state.world = Some(World::new(16, height, min_y)); + } conn.lock() .await @@ -321,8 +324,7 @@ impl Client { GamePacket::ClientboundSetChunkCacheCenterPacket(p) => { println!("Got chunk cache center packet {:?}", p); state - .lock() - .await + .lock()? .world .as_mut() .unwrap() @@ -334,8 +336,7 @@ impl Client { // let chunk = Chunk::read_with_world_height(&mut p.chunk_data); // println("chunk {:?}") state - .lock() - .await + .lock()? .world .as_mut() .expect("World doesn't exist! We should've gotten a login packet by now.") @@ -349,13 +350,11 @@ impl Client { println!("Got add entity packet {:?}", p); let entity = Entity::from(p); state - .lock() - .await + .lock()? .world .as_mut() .expect("World doesn't exist! We should've gotten a login packet by now.") - .entities - .insert(entity); + .add_entity(entity); } GamePacket::ClientboundSetEntityDataPacket(p) => { // println!("Got set entity data packet {:?}", p); @@ -392,6 +391,18 @@ impl Client { } GamePacket::ClientboundTeleportEntityPacket(p) => { // println!("Got teleport entity packet {:?}", p); + let state_lock = state.lock()?; + + // let entity = state_lock + // .world + // .unwrap() + // .entity_by_id(p.id) + // .ok_or("Teleporting entity that doesn't exist.".to_string())?; + // state_lock + // .world + // .as_mut() + // .expect("World doesn't exist! We should've gotten a login packet by now.") + // .move_entity(&mut entity, new_pos) } GamePacket::ClientboundUpdateAdvancementsPacket(p) => { println!("Got update advancements packet {:?}", p); @@ -457,9 +468,23 @@ impl Client { } _ => panic!("Unexpected packet {:?}", packet), } + + Ok(()) } pub async fn next(&mut self) -> Option { self.event_receiver.recv().await } } + +impl From> for HandleError { + fn from(e: std::sync::PoisonError) -> Self { + HandleError(e.to_string()) + } +} + +impl From for HandleError { + fn from(e: String) -> Self { + HandleError(e) + } +} diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index 24be5f6a..43881f1c 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -27,7 +27,7 @@ impl Rem for BlockPos { } } -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct ChunkPos { pub x: i32, pub z: i32, @@ -164,6 +164,12 @@ impl From<&EntityPos> for BlockPos { } } +impl From<&EntityPos> for ChunkPos { + fn from(pos: &EntityPos) -> Self { + ChunkPos::from(&BlockPos::from(pos)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -181,4 +187,15 @@ mod tests { let chunk_block_pos = ChunkBlockPos::from(&block_pos); assert_eq!(chunk_block_pos, ChunkBlockPos::new(5, 78, 14)); } + + #[test] + fn test_from_entity_pos_to_chunk_pos() { + let entity_pos = EntityPos { + x: 33.5, + y: 77.0, + z: -19.6, + }; + let chunk_pos = ChunkPos::from(&entity_pos); + assert_eq!(chunk_pos, ChunkPos::new(2, -2)); + } } diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index f9d808c2..f776f16f 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -1,4 +1,4 @@ -use azalea_core::EntityPos; +use azalea_core::{ChunkPos, EntityPos}; #[cfg(feature = "protocol")] use azalea_protocol::packets::game::clientbound_add_entity_packet::ClientboundAddEntityPacket; use uuid::Uuid; @@ -16,19 +16,16 @@ impl Entity { &self.pos } - pub fn set_pos(&mut self, pos: EntityPos) { - // TODO: check if it moved to another chunk - self.pos = pos; + /// Sets the position of the entity. This doesn't update the cache in + /// azalea-world, and should only be used within azalea-world! + pub fn unsafe_move(&mut self, new_pos: EntityPos) { + self.pos = new_pos; } } #[cfg(feature = "protocol")] -impl From<&azalea_protocol::packets::game::clientbound_add_entity_packet::ClientboundAddEntityPacket> - for Entity -{ - fn from( - p: &azalea_protocol::packets::game::clientbound_add_entity_packet::ClientboundAddEntityPacket, - ) -> Self { +impl From<&ClientboundAddEntityPacket> for Entity { + fn from(p: &ClientboundAddEntityPacket) -> Self { Self { id: p.id, uuid: p.uuid, diff --git a/azalea-world/Cargo.toml b/azalea-world/Cargo.toml index e5e9da1d..96942138 100644 --- a/azalea-world/Cargo.toml +++ b/azalea-world/Cargo.toml @@ -11,6 +11,7 @@ azalea-core = {path = "../azalea-core"} azalea-entity = {path = "../azalea-entity"} azalea-nbt = {path = "../azalea-nbt"} azalea-protocol = {path = "../azalea-protocol"} +log = "0.4.17" nohash-hasher = "0.2.0" [profile.release] diff --git a/azalea-world/src/chunk.rs b/azalea-world/src/chunk.rs index 96bbd922..cbf77b20 100644 --- a/azalea-world/src/chunk.rs +++ b/azalea-world/src/chunk.rs @@ -1,4 +1,3 @@ -use crate::bit_storage::BitStorage; use crate::palette::PalettedContainer; use crate::palette::PalettedContainerType; use crate::World; @@ -13,10 +12,13 @@ use std::{ const SECTION_HEIGHT: u32 = 16; +#[derive(Debug)] pub struct ChunkStorage { pub view_center: ChunkPos, chunk_radius: u32, view_range: u32, + pub height: u32, + pub min_y: i32, // chunks is a list of size chunk_radius * chunk_radius chunks: Vec>>>, } @@ -32,12 +34,14 @@ fn floor_mod(x: i32, y: u32) -> u32 { } impl ChunkStorage { - pub fn new(chunk_radius: u32) -> Self { + pub fn new(chunk_radius: u32, height: u32, min_y: i32) -> Self { let view_range = chunk_radius * 2 + 1; ChunkStorage { view_center: ChunkPos::new(0, 0), chunk_radius, view_range, + height, + min_y, chunks: vec![None; (view_range * view_range) as usize], } } @@ -61,6 +65,29 @@ impl ChunkStorage { None => None, } } + + pub fn replace_with_packet_data( + &mut self, + pos: &ChunkPos, + data: &mut impl Read, + ) -> Result<(), String> { + if !self.in_range(pos) { + println!( + "Ignoring chunk since it's not in the view range: {}, {}", + pos.x, pos.z + ); + return Ok(()); + } + + let chunk = Arc::new(Mutex::new(Chunk::read_with_world_height( + data, + self.height, + )?)); + println!("Loaded chunk {:?}", pos); + self[pos] = Some(chunk); + + Ok(()) + } } impl Index<&ChunkPos> for ChunkStorage { @@ -84,7 +111,7 @@ pub struct Chunk { impl Chunk { pub fn read_with_world(buf: &mut impl Read, data: &World) -> Result { - Self::read_with_world_height(buf, data.height) + Self::read_with_world_height(buf, data.height()) } pub fn read_with_world_height(buf: &mut impl Read, world_height: u32) -> Result { diff --git a/azalea-world/src/entity.rs b/azalea-world/src/entity.rs index 49e1ae73..7077d0c4 100644 --- a/azalea-world/src/entity.rs +++ b/azalea-world/src/entity.rs @@ -1,14 +1,14 @@ -use std::collections::HashMap; - use azalea_core::ChunkPos; use azalea_entity::Entity; -use nohash_hasher::IntMap; +use log::warn; +use nohash_hasher::{IntMap, IntSet}; +use std::collections::HashMap; #[derive(Debug)] pub struct EntityStorage { by_id: IntMap, // TODO: this doesn't work yet (should be updated in the set_pos method in azalea-entity) - by_chunk: HashMap, + by_chunk: HashMap>, } impl EntityStorage { @@ -22,13 +22,24 @@ impl EntityStorage { /// Add an entity to the storage. #[inline] pub fn insert(&mut self, entity: Entity) { + self.by_chunk + .entry(ChunkPos::from(entity.pos())) + .or_default() + .insert(entity.id); self.by_id.insert(entity.id, entity); } /// Remove an entity from the storage by its id. #[inline] pub fn remove_by_id(&mut self, id: u32) { - self.by_id.remove(&id); + if let Some(entity) = self.by_id.remove(&id) { + let entity_chunk = ChunkPos::from(entity.pos()); + if let None = self.by_chunk.remove(&entity_chunk) { + warn!("Tried to remove entity with id {id} from chunk {entity_chunk:?} but it was not found."); + } + } else { + warn!("Tried to remove entity with id {id} but it was not found.") + } } /// Get a reference to an entity by its id. @@ -42,4 +53,30 @@ impl EntityStorage { pub fn get_mut_by_id(&mut self, id: u32) -> Option<&mut Entity> { self.by_id.get_mut(&id) } + + /// Clear all entities in a chunk. + pub fn clear_chunk(&mut self, chunk: &ChunkPos) { + if let Some(entities) = self.by_chunk.remove(chunk) { + for entity_id in entities { + self.by_id.remove(&entity_id); + } + } + } + + /// Updates an entity from its old chunk. + #[inline] + pub fn update_entity_chunk( + &mut self, + entity_id: u32, + old_chunk: &ChunkPos, + new_chunk: &ChunkPos, + ) { + if let Some(entities) = self.by_chunk.get_mut(old_chunk) { + entities.remove(&entity_id); + } + self.by_chunk + .entry(*new_chunk) + .or_default() + .insert(entity_id); + } } diff --git a/azalea-world/src/lib.rs b/azalea-world/src/lib.rs index b47126d4..746143c7 100644 --- a/azalea-world/src/lib.rs +++ b/azalea-world/src/lib.rs @@ -6,7 +6,8 @@ mod entity; mod palette; use azalea_block::BlockState; -use azalea_core::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos}; +use azalea_core::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos, EntityPos}; +use azalea_entity::Entity; use azalea_protocol::mc_buf::{McBufReadable, McBufWritable}; pub use bit_storage::BitStorage; pub use chunk::{Chunk, ChunkStorage}; @@ -26,61 +27,78 @@ mod tests { } } +#[derive(Debug)] pub struct World { - pub storage: ChunkStorage, - pub entities: EntityStorage, - pub height: u32, - pub min_y: i32, + chunk_storage: ChunkStorage, + entity_storage: EntityStorage, } impl World { + pub fn new(chunk_radius: u32, height: u32, min_y: i32) -> Self { + World { + chunk_storage: ChunkStorage::new(chunk_radius, height, min_y), + entity_storage: EntityStorage::new(), + } + } + pub fn replace_with_packet_data( &mut self, pos: &ChunkPos, data: &mut impl Read, ) -> Result<(), String> { - if !self.storage.in_range(pos) { - println!( - "Ignoring chunk since it's not in the view range: {}, {}", - pos.x, pos.z - ); - return Ok(()); - } - // let existing_chunk = &self.storage[pos]; - - let chunk = Arc::new(Mutex::new(Chunk::read_with_world(data, self)?)); - println!("Loaded chunk {:?}", pos); - self.storage[pos] = Some(chunk); - - Ok(()) + self.chunk_storage.replace_with_packet_data(pos, data) } pub fn update_view_center(&mut self, pos: &ChunkPos) { - self.storage.view_center = *pos; + self.chunk_storage.view_center = *pos; } pub fn get_block_state(&self, pos: &BlockPos) -> Option { - self.storage.get_block_state(pos, self.min_y) + self.chunk_storage.get_block_state(pos, self.min_y()) + } + + pub fn move_entity(&mut self, entity_id: u32, new_pos: EntityPos) -> Result<(), String> { + let entity = self + .entity_storage + .get_mut_by_id(entity_id) + .ok_or("Moving entity that doesn't exist".to_string())?; + let old_chunk = ChunkPos::from(entity.pos()); + let new_chunk = ChunkPos::from(&new_pos); + // this is fine because we update the chunk below + entity.unsafe_move(new_pos); + if old_chunk != new_chunk { + self.entity_storage + .update_entity_chunk(entity_id, &old_chunk, &new_chunk); + } + Ok(()) + } + + pub fn add_entity(&mut self, entity: Entity) { + self.entity_storage.insert(entity); + } + + pub fn height(&self) -> u32 { + self.chunk_storage.height + } + + pub fn min_y(&self) -> i32 { + self.chunk_storage.min_y + } + + pub fn entity_by_id(&self, id: u32) -> Option<&Entity> { + self.entity_storage.get_by_id(id) } } + impl Index<&ChunkPos> for World { type Output = Option>>; fn index(&self, pos: &ChunkPos) -> &Self::Output { - &self.storage[pos] + &self.chunk_storage[pos] } } impl IndexMut<&ChunkPos> for World { fn index_mut<'a>(&'a mut self, pos: &ChunkPos) -> &'a mut Self::Output { - &mut self.storage[pos] + &mut self.chunk_storage[pos] } } -// impl Index<&BlockPos> for World { -// type Output = Option>>; - -// fn index(&self, pos: &BlockPos) -> &Self::Output { -// let chunk = &self[ChunkPos::from(pos)]; -// // chunk. - -// } -// } diff --git a/bot/src/main.rs b/bot/src/main.rs index bfcba7f5..6b318157 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -1,7 +1,7 @@ use azalea_client::{Account, Event}; #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box> { println!("Hello, world!"); // let address = "95.111.249.143:10000"; @@ -15,18 +15,18 @@ async fn main() { let mut client = account.join(&address.try_into().unwrap()).await.unwrap(); println!("connected"); - while let Some(e) = client.next().await { + while let Some(e) = &client.next().await { match e { // TODO: have a "loaded" or "ready" event that fires when all chunks are loaded Event::Login => {} Event::Chat(_p) => { - let state = client.state.lock().await; + let state = &client.state.lock()?; let world = state.world.as_ref().unwrap(); - println!("{:?}", world.entities); + println!("{:?}", world); // world.get_block_state(state.player.entity.pos); // println!("{}", p.message.to_ansi(None)); // if p.message.to_ansi(None) == " ok" { - // let state = client.state.lock().await; + // let state = client.state.lock(); // let world = state.world.as_ref().unwrap(); // let c = world.get_block_state(&BlockPos::new(5, 78, -2)).unwrap(); // println!("block state: {:?}", c); @@ -36,4 +36,6 @@ async fn main() { } println!("done"); + + Ok(()) } diff --git a/examples/pvp.rs b/examples/pvp.rs index 61382ecd..5febdd45 100644 --- a/examples/pvp.rs +++ b/examples/pvp.rs @@ -21,7 +21,7 @@ async fn main() { if bot.entity.can_reach(target.bounding_box) { bot.swing(); } - if !h.using_held_item() && bot.state.lock().await.hunger <= 17 { + if !h.using_held_item() && bot.state.lock().hunger <= 17 { bot.hold(azalea::ItemGroup::Food); tokio::task::spawn(bot.use_held_item()); }