diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index ea485049..d889f3ef 100755 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -292,6 +292,11 @@ impl From for Component { }) } } +impl From<&str> for Component { + fn from(s: &str) -> Self { + Self::from(s.to_string()) + } +} impl Display for Component { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/azalea-client/src/chat.rs b/azalea-client/src/chat.rs index 510bf043..fa88daf7 100755 --- a/azalea-client/src/chat.rs +++ b/azalea-client/src/chat.rs @@ -78,6 +78,15 @@ impl ChatPacket { pub fn content(&self) -> String { self.split_sender_and_content().1 } + + /// Create a new ChatPacket from a string. This is meant to be used as a + /// convenience function for testing. + pub fn new(message: &str) -> Self { + ChatPacket::System(Arc::new(ClientboundSystemChatPacket { + content: Component::from(message), + overlay: false, + })) + } } impl Client { diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 3aad96c4..44c0f8cc 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -30,10 +30,10 @@ use azalea_protocol::{ }; use azalea_world::{ entity::{metadata, Entity, EntityData, EntityMetadata}, - WeakWorld, WeakWorldContainer, World, + PartialWorld, WeakWorld, WeakWorldContainer, }; use log::{debug, error, info, trace, warn}; -use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use parking_lot::{Mutex, RwLock}; use std::{ any, backtrace::Backtrace, @@ -92,7 +92,7 @@ pub struct Client { pub write_conn: Arc>>, pub entity_id: Arc>, /// The world that this client has access to. This supports shared worlds. - pub world: Arc>, + pub world: Arc>, /// A container of world names to worlds. If we're not using a shared world /// (i.e. not a swarm), then this will only contain data about the world /// we're currently in. @@ -180,7 +180,7 @@ impl Client { write_conn, // default our id to 0, it'll be set later entity_id: Arc::new(RwLock::new(0)), - world: Arc::new(RwLock::new(World::default())), + world: Arc::new(RwLock::new(PartialWorld::default())), world_container: world_container .unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))), world_name: Arc::new(RwLock::new(None)), @@ -509,16 +509,18 @@ impl Client { .as_int() .expect("min_y tag is not an int"); + let world_name = p.dimension.clone(); + + *client.world_name.write() = Some(world_name.clone()); // add this world to the world_container (or don't if it's already there) - let weak_world = - client - .world_container - .write() - .insert(p.dimension.clone(), height, min_y); + let weak_world = client + .world_container + .write() + .insert(world_name, height, min_y); // set the loaded_world to an empty world // (when we add chunks or entities those will be in the world_container) let mut world_lock = client.world.write(); - *world_lock = World::new( + *world_lock = PartialWorld::new( client.client_information.read().view_distance.into(), weak_world, p.player_id, @@ -1011,7 +1013,7 @@ impl Client { // return if there's no chunk at the player's position { - let world_lock = client.world.read(); + let world_lock = client.world(); let player_entity_id = *client.entity_id.read(); let player_entity = world_lock.entity(player_entity_id); let Some(player_entity) = player_entity else { @@ -1037,46 +1039,24 @@ impl Client { // TODO: minecraft does ambient sounds here } - /// Get a [`WeakWorld`] from our world container. If it's a normal client, - /// then it'll be the same as the world the client has loaded. If the - /// client using a shared world, then the shared world will be a superset - /// of the client's world. + /// Get a reference to our (potentially shared) world. /// - /// # Panics - /// Panics if the client has not received the login packet yet. You can - /// check this with [`Client::logged_in`]. + /// This gets the [`WeakWorld`] from our world container. If it's a normal + /// client, then it'll be the same as the world the client has loaded. + /// If the client using a shared world, then the shared world will be a + /// superset of the client's world. pub fn world(&self) -> Arc { - let world_name = self.world_name.read(); - let world_name = world_name - .as_ref() - .expect("Client has not received login packet yet"); - if let Some(world) = self.world_container.read().get(world_name) { - world - } else { - unreachable!("The world name must be in the world container"); - } + self.world.read().shared.clone() } /// Returns the entity associated to the player. - pub fn entity_mut(&self) -> Entity> { + pub fn entity(&self) -> Entity> { let entity_id = *self.entity_id.read(); - let world = self.world.write(); - - let entity_data = world - .entity_storage - .get_by_id(entity_id) - .expect("Player entity should exist"); - let entity_ptr = unsafe { entity_data.as_ptr() }; - Entity::new(world, entity_id, entity_ptr) - } - /// Returns the entity associated to the player. - pub fn entity(&self) -> Entity> { - let entity_id = *self.entity_id.read(); - let world = self.world.read(); - + let world = self.world(); let entity_data = world .entity_storage + .read() .get_by_id(entity_id) .expect("Player entity should be in the given world"); let entity_ptr = unsafe { entity_data.as_ptr() }; diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 5a638883..d9cab1d4 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -107,7 +107,7 @@ impl Client { }; drop(player_entity); - let mut player_entity = self.entity_mut(); + let mut player_entity = self.entity(); if sending_position { player_entity.last_pos = *player_entity.pos(); @@ -192,7 +192,7 @@ impl Client { // server ai step { - let mut player_entity = self.entity_mut(); + let mut player_entity = self.entity(); let physics_state = self.physics_state.lock(); player_entity.xxa = physics_state.left_impulse; @@ -222,7 +222,7 @@ impl Client { self.set_sprinting(true); } - let mut player_entity = self.entity_mut(); + let mut player_entity = self.entity(); player_entity.ai_step(); } @@ -318,7 +318,7 @@ impl Client { /// player. You should use the [`walk`] and [`sprint`] methods instead. /// Returns if the operation was successful. fn set_sprinting(&mut self, sprinting: bool) -> bool { - let mut player_entity = self.entity_mut(); + let mut player_entity = self.entity(); player_entity.metadata.sprinting = sprinting; if sprinting { player_entity @@ -341,14 +341,13 @@ impl Client { /// If you're making a realistic client, calling this function every tick is /// recommended. pub fn set_jumping(&mut self, jumping: bool) { - let mut player_entity = self.entity_mut(); + let mut player_entity = self.entity(); player_entity.jumping = jumping; } /// Returns whether the player will try to jump next tick. pub fn jumping(&self) -> bool { let player_entity = self.entity(); - player_entity.jumping } @@ -357,7 +356,7 @@ impl Client { /// f3 screen. /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90. pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) { - let mut player_entity = self.entity_mut(); + let mut player_entity = self.entity(); player_entity.set_rotation(y_rot, x_rot); } diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index 1b4f052b..650fb58c 100755 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -1,12 +1,12 @@ use azalea_auth::game_profile::GameProfile; use azalea_chat::Component; use azalea_core::GameType; -use azalea_world::World; +use azalea_world::PartialWorld; use uuid::Uuid; /// Something that has a world associated to it. this is usually a `Client`. pub trait WorldHaver { - fn world(&self) -> &World; + fn world(&self) -> &PartialWorld; } /// A player in the tab list. diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index c057f8d7..458303c5 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -6,11 +6,11 @@ mod world_collisions; use azalea_core::{Axis, Vec3, AABB, EPSILON}; use azalea_world::entity::{Entity, EntityData}; -use azalea_world::{MoveEntityError, World}; +use azalea_world::{MoveEntityError, WeakWorld}; pub use blocks::BlockWithShape; pub use discrete_voxel_shape::*; pub use shape::*; -use std::ops::DerefMut; +use std::ops::Deref; use world_collisions::CollisionGetter; pub enum MoverType { @@ -33,7 +33,7 @@ pub trait MovableEntity { ) -> Result<(), MoveEntityError>; } -impl HasCollision for World { +impl> HasCollision for D { // private Vec3 collide(Vec3 var1) { // AABB var2 = this.getBoundingBox(); // List var3 = this.level.getEntityCollisions(this, @@ -87,7 +87,7 @@ impl HasCollision for World { } } -impl> MovableEntity for Entity<'_, D> { +impl> MovableEntity for Entity<'_, D> { /// Move an entity by a given delta, checking for collisions. fn move_colliding( &mut self, @@ -206,7 +206,7 @@ fn collide_bounding_box( entity: Option<&EntityData>, movement: &Vec3, entity_bounding_box: &AABB, - world: &World, + world: &WeakWorld, entity_collisions: Vec, ) -> Vec3 { let mut collision_boxes: Vec = Vec::with_capacity(entity_collisions.len() + 1); diff --git a/azalea-physics/src/collision/world_collisions.rs b/azalea-physics/src/collision/world_collisions.rs index a28bce14..9e238bf9 100644 --- a/azalea-physics/src/collision/world_collisions.rs +++ b/azalea-physics/src/collision/world_collisions.rs @@ -2,8 +2,8 @@ use crate::collision::{BlockWithShape, VoxelShape, AABB}; use azalea_block::BlockState; use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON}; use azalea_world::entity::EntityData; -use azalea_world::{Chunk, World}; -use parking_lot::Mutex; +use azalea_world::{Chunk, WeakWorld}; +use parking_lot::RwLock; use std::sync::Arc; use super::Shapes; @@ -16,7 +16,7 @@ pub trait CollisionGetter { ) -> BlockCollisions<'a>; } -impl CollisionGetter for World { +impl CollisionGetter for WeakWorld { fn get_block_collisions<'a>( &'a self, entity: Option<&EntityData>, @@ -27,7 +27,7 @@ impl CollisionGetter for World { } pub struct BlockCollisions<'a> { - pub world: &'a World, + pub world: &'a WeakWorld, // context: CollisionContext, pub aabb: AABB, pub entity_shape: VoxelShape, @@ -37,7 +37,7 @@ pub struct BlockCollisions<'a> { impl<'a> BlockCollisions<'a> { // TODO: the entity is stored in the context - pub fn new(world: &'a World, _entity: Option<&EntityData>, aabb: AABB) -> Self { + pub fn new(world: &'a WeakWorld, _entity: Option<&EntityData>, aabb: AABB) -> Self { let origin_x = (aabb.min_x - EPSILON) as i32 - 1; let origin_y = (aabb.min_y - EPSILON) as i32 - 1; let origin_z = (aabb.min_z - EPSILON) as i32 - 1; @@ -57,7 +57,7 @@ impl<'a> BlockCollisions<'a> { } } - fn get_chunk(&self, block_x: i32, block_z: i32) -> Option>> { + fn get_chunk(&self, block_x: i32, block_z: i32) -> Option>> { let chunk_x = ChunkSectionPos::block_to_section_coord(block_x); let chunk_z = ChunkSectionPos::block_to_section_coord(block_z); let chunk_pos = ChunkPos::new(chunk_x, chunk_z); @@ -96,7 +96,7 @@ impl<'a> Iterator for BlockCollisions<'a> { let pos = item.pos; let block_state: BlockState = chunk - .lock() + .read() .get(&(&pos).into(), self.world.min_y()) .unwrap_or(BlockState::Air); diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index d419535e..76ae3990 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -2,15 +2,14 @@ pub mod collision; -use std::ops::DerefMut; - use azalea_block::{Block, BlockState}; use azalea_core::{BlockPos, Vec3}; use azalea_world::{ entity::{Entity, EntityData}, - World, + WeakWorld, }; use collision::{MovableEntity, MoverType}; +use std::ops::Deref; pub trait HasPhysics { fn travel(&mut self, acceleration: &Vec3); @@ -19,7 +18,7 @@ pub trait HasPhysics { fn jump_from_ground(&mut self); } -impl> HasPhysics for Entity<'_, D> { +impl> HasPhysics for Entity<'_, D> { /// Move the entity with the given acceleration while handling friction, /// gravity, collisions, and some other stuff. fn travel(&mut self, acceleration: &Vec3) { @@ -143,7 +142,7 @@ fn get_block_pos_below_that_affects_movement(entity: &EntityData) -> BlockPos { ) } -fn handle_relative_friction_and_calculate_movement>( +fn handle_relative_friction_and_calculate_movement>( entity: &mut Entity, acceleration: &Vec3, block_friction: f32, @@ -183,7 +182,7 @@ fn get_friction_influenced_speed(entity: &EntityData, friction: f32) -> f32 { /// Returns the what the entity's jump should be multiplied by based on the /// block they're standing on. -fn block_jump_factor>(entity: &Entity) -> f32 { +fn block_jump_factor>(entity: &Entity) -> f32 { let block_at_pos = entity.world.get_block_state(&entity.pos().into()); let block_below = entity .world @@ -211,11 +210,11 @@ fn block_jump_factor>(entity: &Entity) -> f32 { // public double getJumpBoostPower() { // return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F * // (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; } -fn jump_power>(entity: &Entity) -> f32 { +fn jump_power>(entity: &Entity) -> f32 { 0.42 * block_jump_factor(entity) } -fn jump_boost_power>(_entity: &Entity) -> f64 { +fn jump_boost_power>(_entity: &Entity) -> f64 { // TODO: potion effects // if let Some(effects) = entity.effects() { // if let Some(jump_effect) = effects.get(&Effect::Jump) { @@ -235,13 +234,13 @@ mod tests { use azalea_core::ChunkPos; use azalea_world::{ entity::{metadata, EntityMetadata}, - Chunk, World, + Chunk, PartialWorld, }; use uuid::Uuid; #[test] fn test_gravity() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world.add_entity( 0, @@ -272,7 +271,7 @@ mod tests { } #[test] fn test_collision() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); @@ -305,7 +304,7 @@ mod tests { #[test] fn test_slab_collision() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); @@ -339,7 +338,7 @@ mod tests { #[test] fn test_top_slab_collision() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); @@ -373,7 +372,7 @@ mod tests { #[test] fn test_weird_wall_collision() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index 7d4486e4..6d3907f0 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -1,20 +1,16 @@ use crate::palette::PalettedContainer; use crate::palette::PalettedContainerType; -use crate::World; use azalea_block::BlockState; -use azalea_buf::BufReadError; -use azalea_buf::{McBufReadable, McBufWritable}; +use azalea_buf::{BufReadError, McBufReadable, McBufWritable}; use azalea_core::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos}; -use log::debug; -use log::trace; -use log::warn; -use parking_lot::Mutex; +use log::{debug, trace, warn}; use parking_lot::RwLock; -use std::collections::HashMap; -use std::fmt::Debug; -use std::io::Cursor; -use std::sync::Weak; -use std::{io::Write, sync::Arc}; +use std::{ + collections::HashMap, + fmt::Debug, + io::{Cursor, Write}, + sync::{Arc, Weak}, +}; const SECTION_HEIGHT: u32 = 16; @@ -30,7 +26,7 @@ pub struct PartialChunkStorage { chunk_radius: u32, view_range: u32, // chunks is a list of size chunk_radius * chunk_radius - chunks: Vec>>>, + chunks: Vec>>>, } /// A storage for chunks where they're only stored weakly, so if they're not @@ -39,7 +35,7 @@ pub struct PartialChunkStorage { pub struct WeakChunkStorage { pub height: u32, pub min_y: i32, - pub chunks: HashMap>>, + pub chunks: HashMap>>, } /// A storage of potentially infinite chunks in a world. Chunks are stored as @@ -47,7 +43,7 @@ pub struct WeakChunkStorage { pub struct ChunkStorage { pub height: u32, pub min_y: i32, - pub chunks: HashMap>>, + pub chunks: HashMap>>, } /// A single chunk in a world (16*?*16 blocks). This only contains the blocks @@ -113,20 +109,13 @@ impl PartialChunkStorage { && (chunk_pos.z - self.view_center.z).unsigned_abs() <= self.chunk_radius } - pub fn get_block_state(&self, pos: &BlockPos) -> Option { - let chunk_pos = ChunkPos::from(pos); - let chunk = self.get(&chunk_pos)?; - let chunk = chunk.lock(); - chunk.get(&ChunkBlockPos::from(pos), self.min_y()) - } - pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option { if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) { return None; } let chunk_pos = ChunkPos::from(pos); let chunk = self.get(&chunk_pos)?; - let mut chunk = chunk.lock(); + let mut chunk = chunk.write(); Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y())) } @@ -145,7 +134,7 @@ impl PartialChunkStorage { return Ok(()); } - let chunk = Arc::new(Mutex::new(Chunk::read_with_dimension_height( + let chunk = Arc::new(RwLock::new(Chunk::read_with_dimension_height( data, self.height(), )?)); @@ -158,7 +147,7 @@ impl PartialChunkStorage { /// Get a [`Chunk`] within render distance, or `None` if it's not loaded. /// Use [`PartialChunkStorage::get`] to get a chunk from the shared storage. - pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc>> { + pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc>> { if !self.in_range(pos) { warn!( "Chunk at {:?} is not in the render distance (center: {:?}, {} chunks)", @@ -173,7 +162,7 @@ impl PartialChunkStorage { /// Get a mutable reference to a [`Chunk`] within render distance, or /// `None` if it's not loaded. Use [`PartialChunkStorage::get`] to get /// a chunk from the shared storage. - pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option>>> { + pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option>>> { if !self.in_range(pos) { return None; } @@ -183,7 +172,7 @@ impl PartialChunkStorage { } /// Get a chunk, - pub fn get(&self, pos: &ChunkPos) -> Option>> { + pub fn get(&self, pos: &ChunkPos) -> Option>> { self.shared .read() .chunks @@ -196,7 +185,7 @@ impl PartialChunkStorage { /// /// # Panics /// If the chunk is not in the render distance. - pub fn set(&mut self, pos: &ChunkPos, chunk: Option>>) { + pub fn set(&mut self, pos: &ChunkPos, chunk: Option>>) { if let Some(chunk) = &chunk { self.shared .write() @@ -219,16 +208,30 @@ impl WeakChunkStorage { chunks: HashMap::new(), } } + + pub fn get(&self, pos: &ChunkPos) -> Option>> { + self.chunks.get(pos).and_then(|chunk| chunk.upgrade()) + } + + pub fn get_block_state(&self, pos: &BlockPos) -> Option { + let chunk_pos = ChunkPos::from(pos); + let chunk = self.get(&chunk_pos)?; + let chunk = chunk.read(); + chunk.get(&ChunkBlockPos::from(pos), self.min_y) + } + + pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option { + if pos.y < self.min_y || pos.y >= (self.min_y + self.height as i32) { + return None; + } + let chunk_pos = ChunkPos::from(pos); + let chunk = self.get(&chunk_pos)?; + let mut chunk = chunk.write(); + Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y)) + } } impl Chunk { - pub fn read_with_dimension( - buf: &mut Cursor<&[u8]>, - data: &World, - ) -> Result { - Self::read_with_dimension_height(buf, data.height()) - } - pub fn read_with_dimension_height( buf: &mut Cursor<&[u8]>, dimension_height: u32, @@ -242,18 +245,12 @@ impl Chunk { Ok(Chunk { sections }) } - pub fn section_index(&self, y: i32, min_y: i32) -> u32 { - assert!(y >= min_y, "y ({y}) must be at least {min_y}"); - let min_section_index = min_y.div_floor(16); - (y.div_floor(16) - min_section_index) as u32 - } - 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 = self.section_index(pos.y, min_y) as usize; + 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; @@ -270,7 +267,7 @@ impl Chunk { state: BlockState, min_y: i32, ) -> BlockState { - let section_index = self.section_index(pos.y, min_y); + let section_index = section_index(pos.y, min_y); // TODO: make sure the section exists let section = &mut self.sections[section_index as usize]; let chunk_section_pos = ChunkSectionBlockPos::from(pos); @@ -278,7 +275,7 @@ impl Chunk { } pub fn set(&mut self, pos: &ChunkBlockPos, state: BlockState, min_y: i32) { - let section_index = self.section_index(pos.y, min_y); + let section_index = section_index(pos.y, min_y); // TODO: make sure the section exists let section = &mut self.sections[section_index as usize]; let chunk_section_pos = ChunkSectionBlockPos::from(pos); @@ -384,21 +381,28 @@ impl Default for WeakChunkStorage { } } +/// Get the index of where a section is in a chunk based on its y coordinate +/// and the minimum y coordinate of the world. +pub fn section_index(y: i32, min_y: i32) -> u32 { + assert!(y >= min_y, "y ({y}) must be at least {min_y}"); + let min_section_index = min_y.div_floor(16); + (y.div_floor(16) - min_section_index) as u32 +} + #[cfg(test)] mod tests { use super::*; #[test] fn test_section_index() { - let chunk = Chunk::default(); - assert_eq!(chunk.section_index(0, 0), 0); - assert_eq!(chunk.section_index(128, 0), 8); - assert_eq!(chunk.section_index(127, 0), 7); - assert_eq!(chunk.section_index(0, -64), 4); - assert_eq!(chunk.section_index(-64, -64), 0); - assert_eq!(chunk.section_index(-49, -64), 0); - assert_eq!(chunk.section_index(-48, -64), 1); - assert_eq!(chunk.section_index(128, -64), 12); + assert_eq!(section_index(0, 0), 0); + assert_eq!(section_index(128, 0), 8); + assert_eq!(section_index(127, 0), 7); + assert_eq!(section_index(0, -64), 4); + assert_eq!(section_index(-64, -64), 0); + assert_eq!(section_index(-49, -64), 0); + assert_eq!(section_index(-48, -64), 1); + assert_eq!(section_index(128, -64), 12); } #[test] @@ -406,21 +410,31 @@ mod tests { let mut chunk_storage = PartialChunkStorage::default(); chunk_storage.set( &ChunkPos { x: 0, z: 0 }, - Some(Arc::new(Mutex::new(Chunk::default()))), + Some(Arc::new(RwLock::new(Chunk::default()))), ); assert!(chunk_storage + .shared + .read() .get_block_state(&BlockPos { x: 0, y: 319, z: 0 }) .is_some()); assert!(chunk_storage + .shared + .read() .get_block_state(&BlockPos { x: 0, y: 320, z: 0 }) .is_none()); assert!(chunk_storage + .shared + .read() .get_block_state(&BlockPos { x: 0, y: 338, z: 0 }) .is_none()); assert!(chunk_storage + .shared + .read() .get_block_state(&BlockPos { x: 0, y: -64, z: 0 }) .is_some()); assert!(chunk_storage + .shared + .read() .get_block_state(&BlockPos { x: 0, y: -65, z: 0 }) .is_none()); } diff --git a/azalea-world/src/entity/mod.rs b/azalea-world/src/entity/mod.rs index 814ef6d2..94362f2f 100644 --- a/azalea-world/src/entity/mod.rs +++ b/azalea-world/src/entity/mod.rs @@ -5,7 +5,7 @@ pub mod metadata; use self::attributes::{AttributeInstance, AttributeModifiers}; pub use self::metadata::EntityMetadata; -use crate::World; +use crate::WeakWorld; use azalea_block::BlockState; use azalea_core::{BlockPos, Vec3, AABB}; pub use data::*; @@ -17,7 +17,7 @@ use uuid::Uuid; /// A reference to an entity in a world. #[derive(Debug)] -pub struct Entity<'d, D = &'d mut World> { +pub struct Entity<'d, D = &'d WeakWorld> { /// The world this entity is in. pub world: D, /// The incrementing numerical id of the entity. @@ -26,7 +26,7 @@ pub struct Entity<'d, D = &'d mut World> { _marker: PhantomData<&'d ()>, } -impl<'d, D: Deref> Entity<'d, D> { +impl<'d, D: Deref> Entity<'d, D> { pub fn new(world: D, id: u32, data: NonNull) -> Self { // TODO: have this be based on the entity type Self { @@ -38,7 +38,7 @@ impl<'d, D: Deref> Entity<'d, D> { } } -impl<'d, D: DerefMut> Entity<'d, D> { +impl<'d, D: Deref> Entity<'d, D> { /// Sets the position of the entity. This doesn't update the cache in /// azalea-world, and should only be used within azalea-world! /// @@ -95,7 +95,7 @@ impl<'d, D: DerefMut> Entity<'d, D> { } } -impl<'d, D: Deref> Entity<'d, D> { +impl<'d, D: Deref> Entity<'d, D> { #[inline] pub fn pos(&self) -> &Vec3 { &self.pos @@ -150,8 +150,8 @@ impl<'d, D: Deref> Entity<'d, D> { // impl< // 'd, -// D: DerefMut + Deref, -// D2: Deref, +// D: Deref + Deref, +// D2: Deref, // > From> for Entity<'d, D2> // { // fn from(entity: Entity<'d, D>) -> Entity<'d, D> { @@ -164,13 +164,13 @@ impl<'d, D: Deref> Entity<'d, D> { // } // } -impl> DerefMut for Entity<'_, D> { +impl> DerefMut for Entity<'_, D> { fn deref_mut(&mut self) -> &mut Self::Target { unsafe { self.data.as_mut() } } } -impl> Deref for Entity<'_, D> { +impl> Deref for Entity<'_, D> { type Target = EntityData; fn deref(&self) -> &Self::Target { @@ -287,10 +287,11 @@ impl EntityData { #[cfg(test)] mod tests { use super::*; + use crate::PartialWorld; #[test] fn from_mut_entity_to_ref_entity() { - let mut world = World::default(); + let mut world = PartialWorld::default(); let uuid = Uuid::from_u128(100); world.add_entity( 0, @@ -301,7 +302,6 @@ mod tests { ), ); let entity: Entity = world.entity_mut(0).unwrap(); - let entity_ref = Entity::from(entity); - assert_eq!(entity_ref.uuid, uuid); + assert_eq!(entity.uuid, uuid); } } diff --git a/azalea-world/src/entity_storage.rs b/azalea-world/src/entity_storage.rs index 0e8fc0b5..71c5413c 100755 --- a/azalea-world/src/entity_storage.rs +++ b/azalea-world/src/entity_storage.rs @@ -34,7 +34,11 @@ use uuid::Uuid; pub struct PartialEntityStorage { pub shared: Arc>, - /// The entity id of the player that owns this struct. + /// The entity id of the player that owns this partial world. This will + /// make [`PartialWorld::entity_mut`] pretend the entity doesn't exist so + /// it doesn't get modified from outside sources. + /// + /// [`PartialWorld::entity_mut`]: crate::PartialWorld::entity_mut pub owner_entity_id: u32, pub updates_received: IntMap, /// Strong references to the entities we have loaded. @@ -172,11 +176,7 @@ impl PartialEntityStorage { /// Get an entity in the shared storage by its id, if it exists. #[inline] pub fn get_by_id(&self, id: u32) -> Option> { - self.shared - .read() - .data_by_id - .get(&id) - .and_then(|e| e.upgrade()) + self.shared.read().get_by_id(id) } /// Get a reference to an entity by its UUID, if it's being loaded by this @@ -204,13 +204,7 @@ impl PartialEntityStorage { /// Get an entity in the shared storage by its UUID, if it exists. #[inline] pub fn get_by_uuid(&self, uuid: &Uuid) -> Option> { - self.shared.read().id_by_uuid.get(uuid).and_then(|id| { - self.shared - .read() - .data_by_id - .get(id) - .and_then(|e| e.upgrade()) - }) + self.shared.read().get_by_uuid(uuid) } /// Clear all entities in a chunk. This will not clear them from the @@ -241,46 +235,23 @@ impl PartialEntityStorage { old_chunk: &ChunkPos, new_chunk: &ChunkPos, ) { - if let Some(entities) = self.shared.write().ids_by_chunk.get_mut(old_chunk) { - entities.remove(&entity_id); - } self.shared .write() - .ids_by_chunk - .entry(*new_chunk) - .or_default() - .insert(entity_id); + .update_entity_chunk(entity_id, old_chunk, new_chunk); } - pub fn find_one_entity(&self, mut f: F) -> Option> + pub fn find_entity(&self, mut f: F) -> Option> where F: FnMut(&Arc) -> bool, { - for entity in self.shared.read().entities() { - if let Some(entity) = entity.upgrade() { - if f(&entity) { - return Some(entity); - } - } - } - None + self.shared.read().find_entity(|e| f(e)) } - pub fn find_one_entity_in_chunk(&self, chunk: &ChunkPos, mut f: F) -> Option> + pub fn find_entity_in_chunk(&self, chunk: &ChunkPos, mut f: F) -> Option> where F: FnMut(&EntityData) -> bool, { - let shared = self.shared.read(); - if let Some(entities) = shared.ids_by_chunk.get(chunk) { - for entity_id in entities { - if let Some(entity) = shared.data_by_id.get(entity_id).and_then(|e| e.upgrade()) { - if f(&entity) { - return Some(entity); - } - } - } - } - None + self.shared.read().find_entity_in_chunk(chunk, |e| f(e)) } } @@ -366,6 +337,67 @@ impl WeakEntityStorage { pub fn contains_id(&self, id: &u32) -> bool { self.data_by_id.contains_key(id) } + + /// Get an entity by its id, if it exists. + #[inline] + pub fn get_by_id(&self, id: u32) -> Option> { + self.data_by_id.get(&id).and_then(|e| e.upgrade()) + } + + /// Get an entity in the shared storage by its UUID, if it exists. + #[inline] + pub fn get_by_uuid(&self, uuid: &Uuid) -> Option> { + self.id_by_uuid + .get(uuid) + .and_then(|id| self.data_by_id.get(id).and_then(|e| e.upgrade())) + } + + pub fn find_entity(&self, mut f: F) -> Option> + where + F: FnMut(&Arc) -> bool, + { + for entity in self.entities() { + if let Some(entity) = entity.upgrade() { + if f(&entity) { + return Some(entity); + } + } + } + None + } + + pub fn find_entity_in_chunk(&self, chunk: &ChunkPos, mut f: F) -> Option> + where + F: FnMut(&EntityData) -> bool, + { + if let Some(entities) = self.ids_by_chunk.get(chunk) { + for entity_id in entities { + if let Some(entity) = self.data_by_id.get(entity_id).and_then(|e| e.upgrade()) { + if f(&entity) { + return Some(entity); + } + } + } + } + None + } + + /// Move an entity from its old chunk to a new chunk. + #[inline] + pub fn update_entity_chunk( + &mut self, + entity_id: u32, + old_chunk: &ChunkPos, + new_chunk: &ChunkPos, + ) { + if let Some(entities) = self.ids_by_chunk.get_mut(old_chunk) { + entities.remove(&entity_id); + } + self.ids_by_chunk + .entry(*new_chunk) + .or_default() + .insert(entity_id); + } } #[cfg(test)] diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index 65d001c3..648613b9 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -6,18 +6,24 @@ use crate::{ use azalea_block::BlockState; use azalea_buf::BufReadError; use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3}; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use std::{backtrace::Backtrace, fmt::Debug}; use std::{fmt::Formatter, io::Cursor, sync::Arc}; use uuid::Uuid; -/// A world is a collection of chunks and entities. They're called "levels" in -/// Minecraft's source code. +/// PartialWorlds are usually owned by clients, and hold strong references to +/// chunks and entities in [`WeakWorld`]s. +/// +/// Basically, they hold the chunks and entities that are within render +/// distance but can still access chunks and entities owned by other +/// `PartialWorld`s that have the same `WeakWorld`. +/// +/// This is primarily useful for having multiple clients in the same world. #[derive(Default)] -pub struct World { +pub struct PartialWorld { // we just need to keep a strong reference to `shared` so it doesn't get // dropped, we don't need to do anything with it - _shared: Arc, + pub shared: Arc, pub chunk_storage: PartialChunkStorage, pub entity_storage: PartialEntityStorage, @@ -31,10 +37,10 @@ pub struct WeakWorld { pub entity_storage: Arc>, } -impl World { +impl PartialWorld { pub fn new(chunk_radius: u32, shared: Arc, owner_entity_id: u32) -> Self { - World { - _shared: shared.clone(), + PartialWorld { + shared: shared.clone(), chunk_storage: PartialChunkStorage::new(chunk_radius, shared.chunk_storage.clone()), entity_storage: PartialEntityStorage::new( shared.entity_storage.clone(), @@ -51,13 +57,13 @@ impl World { self.chunk_storage.replace_with_packet_data(pos, data) } - pub fn get_chunk(&self, pos: &ChunkPos) -> Option>> { + pub fn get_chunk(&self, pos: &ChunkPos) -> Option>> { self.chunk_storage.get(pos) } pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option) -> Result<(), BufReadError> { self.chunk_storage - .set(pos, chunk.map(|c| Arc::new(Mutex::new(c)))); + .set(pos, chunk.map(|c| Arc::new(RwLock::new(c)))); Ok(()) } @@ -65,14 +71,24 @@ impl World { self.chunk_storage.view_center = *pos; } - pub fn get_block_state(&self, pos: &BlockPos) -> Option { - self.chunk_storage.get_block_state(pos) - } - pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option { self.chunk_storage.set_block_state(pos, state) } + /// Returns a mutable reference to the entity with the given ID. + pub fn entity_mut(&mut self, id: u32) -> Option> { + // no entity for you (we're processing this entity somewhere else) + if id != self.entity_storage.owner_entity_id && !self.entity_storage.maybe_update(id) { + return None; + } + + self.shared.entity(id) + } + + pub fn add_entity(&mut self, id: u32, entity: EntityData) { + self.entity_storage.insert(id, entity); + } + pub fn set_entity_pos(&mut self, entity_id: u32, new_pos: Vec3) -> Result<(), MoveEntityError> { let mut entity = self .entity_mut(entity_id) @@ -109,51 +125,6 @@ impl World { } Ok(()) } - - pub fn add_entity(&mut self, id: u32, entity: EntityData) { - self.entity_storage.insert(id, entity); - } - - pub fn height(&self) -> u32 { - self.chunk_storage.height() - } - - pub fn min_y(&self) -> i32 { - self.chunk_storage.min_y() - } - - pub fn entity_data_by_id(&self, id: u32) -> Option> { - self.entity_storage.get_by_id(id) - } - - pub fn entity(&self, id: u32) -> Option> { - let entity_data = self.entity_storage.get_by_id(id)?; - let entity_ptr = unsafe { entity_data.as_ptr() }; - Some(Entity::new(self, id, entity_ptr)) - } - - /// Returns a mutable reference to the entity with the given ID. - pub fn entity_mut(&mut self, id: u32) -> Option> { - // no entity for you (we're processing this entity somewhere else) - if id != self.entity_storage.owner_entity_id && !self.entity_storage.maybe_update(id) { - return None; - } - - let entity_data = self.entity_storage.get_by_id(id)?; - let entity_ptr = unsafe { entity_data.as_ptr() }; - Some(Entity::new(self, id, entity_ptr)) - } - - pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option> { - self.entity_storage.get_by_uuid(uuid) - } - - pub fn find_one_entity(&self, mut f: F) -> Option> - where - F: FnMut(&EntityData) -> bool, - { - self.entity_storage.find_one_entity(|entity| f(entity)) - } } impl WeakWorld { @@ -164,16 +135,68 @@ impl WeakWorld { } } + /// Read the total height of the world. You can add this to [`Self::min_y`] + /// to get the highest possible y coordinate a block can be placed at. pub fn height(&self) -> u32 { self.chunk_storage.read().height } + /// Get the lowest possible y coordinate a block can be placed at. pub fn min_y(&self) -> i32 { self.chunk_storage.read().min_y } + + pub fn entity_data_by_id(&self, id: u32) -> Option> { + self.entity_storage.read().get_by_id(id) + } + + /// Returns a entity with the given ID. + /// + /// The returned Entity can technically be mutated, but you should avoid + /// doing any relative mutations. + pub fn entity(&self, id: u32) -> Option> { + let entity_data = self.entity_storage.read().get_by_id(id)?; + let entity_ptr = unsafe { entity_data.as_ptr() }; + Some(Entity::new(self, id, entity_ptr)) + } + + pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option> { + self.entity_storage.read().get_by_uuid(uuid) + } + + pub fn find_entity(&self, mut f: F) -> Option> + where + F: FnMut(&EntityData) -> bool, + { + self.entity_storage.read().find_entity(|entity| f(entity)) + } + + pub fn set_entity_pos(&self, entity_id: u32, new_pos: Vec3) -> Result<(), MoveEntityError> { + let mut entity = self + .entity(entity_id) + .ok_or_else(|| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?; + let old_chunk = ChunkPos::from(entity.pos()); + let new_chunk = ChunkPos::from(&new_pos); + // this is fine because we update the chunk below + unsafe { entity.move_unchecked(new_pos) }; + if old_chunk != new_chunk { + self.entity_storage + .write() + .update_entity_chunk(entity_id, &old_chunk, &new_chunk); + } + Ok(()) + } + + pub fn get_block_state(&self, pos: &BlockPos) -> Option { + self.chunk_storage.read().get_block_state(pos) + } + + pub fn get_chunk(&self, pos: &ChunkPos) -> Option>> { + self.chunk_storage.read().get(pos) + } } -impl Debug for World { +impl Debug for PartialWorld { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("World") .field("chunk_storage", &self.chunk_storage) diff --git a/azalea/examples/pvp.rs b/azalea/examples/pvp.rs index 711806de..76507e64 100755 --- a/azalea/examples/pvp.rs +++ b/azalea/examples/pvp.rs @@ -50,7 +50,7 @@ async fn swarm_handle( if let Some(target) = swarm .worlds .read() - .find_one_entity(|e| e.id == "minecraft:player") + .find_entity(|e| e.id == "minecraft:player") { for (bot, bot_state) in swarm { bot.tick_goto_goal(pathfinder::Goals::Reach(target.bounding_box)); diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 93f4825f..49506a17 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -85,6 +85,7 @@ pub mod prelude; mod start; mod swarm; +pub use azalea_block::*; pub use azalea_client::*; pub use azalea_core::{BlockPos, Vec3}; pub use start::{start, Options}; diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 8a9d7540..a1619c41 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -6,7 +6,7 @@ use crate::{Client, Event}; use async_trait::async_trait; use azalea_core::{BlockPos, CardinalDirection}; use azalea_world::entity::EntityData; -use log::debug; +use log::{debug, error}; use mtdstarlite::Edge; pub use mtdstarlite::MTDStarLite; use parking_lot::Mutex; @@ -80,7 +80,7 @@ impl Trait for azalea_client::Client { let successors = |node: &Node| { let mut edges = Vec::new(); - let world = self.world.read(); + let world = &self.world.read().shared; for possible_move in possible_moves.iter() { edges.push(Edge { target: possible_move.next_node(node), @@ -111,7 +111,11 @@ impl Trait for azalea_client::Client { .expect("Pathfinder plugin not installed!") .clone(); // convert the Option> to a VecDeque - *state.path.lock() = p.expect("no path").into_iter().collect(); + if let Some(p) = p { + *state.path.lock() = p.into_iter().collect(); + } else { + error!("no path found"); + } } } diff --git a/azalea/src/pathfinder/moves.rs b/azalea/src/pathfinder/moves.rs index ac2137d3..ccf8ba1a 100644 --- a/azalea/src/pathfinder/moves.rs +++ b/azalea/src/pathfinder/moves.rs @@ -1,10 +1,10 @@ use super::{Node, VerticalVel}; use azalea_core::{BlockPos, CardinalDirection}; use azalea_physics::collision::{self, BlockWithShape}; -use azalea_world::World; +use azalea_world::WeakWorld; /// whether this block is passable -fn is_block_passable(pos: &BlockPos, world: &World) -> bool { +fn is_block_passable(pos: &BlockPos, world: &WeakWorld) -> bool { if let Some(block) = world.get_block_state(pos) { block.shape() == &collision::empty_shape() } else { @@ -13,7 +13,7 @@ fn is_block_passable(pos: &BlockPos, world: &World) -> bool { } /// whether this block has a solid hitbox (i.e. we can stand on it) -fn is_block_solid(pos: &BlockPos, world: &World) -> bool { +fn is_block_solid(pos: &BlockPos, world: &WeakWorld) -> bool { if let Some(block) = world.get_block_state(pos) { block.shape() == &collision::block_shape() } else { @@ -22,14 +22,14 @@ fn is_block_solid(pos: &BlockPos, world: &World) -> bool { } /// Whether this block and the block above are passable -fn is_passable(pos: &BlockPos, world: &World) -> bool { +fn is_passable(pos: &BlockPos, world: &WeakWorld) -> bool { is_block_passable(pos, world) && is_block_passable(&pos.up(1), world) } /// Whether we can stand in this position. Checks if the block below is solid, /// and that the two blocks above that are passable. -fn is_standable(pos: &BlockPos, world: &World) -> bool { +fn is_standable(pos: &BlockPos, world: &WeakWorld) -> bool { is_block_solid(&pos.down(1), world) && is_passable(pos, world) } @@ -37,7 +37,7 @@ const JUMP_COST: f32 = 0.5; const WALK_ONE_BLOCK_COST: f32 = 1.0; pub trait Move { - fn cost(&self, world: &World, node: &Node) -> f32; + fn cost(&self, world: &WeakWorld, node: &Node) -> f32; /// Returns by how much the entity's position should be changed when this /// move is executed. fn offset(&self) -> BlockPos; @@ -51,7 +51,7 @@ pub trait Move { pub struct ForwardMove(pub CardinalDirection); impl Move for ForwardMove { - fn cost(&self, world: &World, node: &Node) -> f32 { + fn cost(&self, world: &WeakWorld, node: &Node) -> f32 { if is_standable(&(node.pos + self.offset()), world) && node.vertical_vel == VerticalVel::None { @@ -67,7 +67,7 @@ impl Move for ForwardMove { pub struct AscendMove(pub CardinalDirection); impl Move for AscendMove { - fn cost(&self, world: &World, node: &Node) -> f32 { + fn cost(&self, world: &WeakWorld, node: &Node) -> f32 { if node.vertical_vel == VerticalVel::None && is_block_passable(&node.pos.up(2), world) && is_standable(&(node.pos + self.offset()), world) @@ -89,7 +89,7 @@ impl Move for AscendMove { } pub struct DescendMove(pub CardinalDirection); impl Move for DescendMove { - fn cost(&self, world: &World, node: &Node) -> f32 { + fn cost(&self, world: &WeakWorld, node: &Node) -> f32 { // check whether 3 blocks vertically forward are passable if node.vertical_vel == VerticalVel::None && is_standable(&(node.pos + self.offset()), world) @@ -112,7 +112,7 @@ impl Move for DescendMove { } pub struct DiagonalMove(pub CardinalDirection); impl Move for DiagonalMove { - fn cost(&self, world: &World, node: &Node) -> f32 { + fn cost(&self, world: &WeakWorld, node: &Node) -> f32 { if node.vertical_vel != VerticalVel::None { return f32::INFINITY; } @@ -151,37 +151,46 @@ mod tests { use super::*; use azalea_block::BlockState; use azalea_core::ChunkPos; - use azalea_world::Chunk; + use azalea_world::{Chunk, PartialWorld}; #[test] fn test_is_passable() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); world.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone); world.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air); - assert_eq!(is_block_passable(&BlockPos::new(0, 0, 0), &world), false); - assert_eq!(is_block_passable(&BlockPos::new(0, 1, 0), &world), true); + assert_eq!( + is_block_passable(&BlockPos::new(0, 0, 0), &world.shared), + false + ); + assert_eq!( + is_block_passable(&BlockPos::new(0, 1, 0), &world.shared), + true + ); } #[test] fn test_is_solid() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); world.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone); world.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air); - assert_eq!(is_block_solid(&BlockPos::new(0, 0, 0), &world), true); - assert_eq!(is_block_solid(&BlockPos::new(0, 1, 0), &world), false); + assert_eq!(is_block_solid(&BlockPos::new(0, 0, 0), &world.shared), true); + assert_eq!( + is_block_solid(&BlockPos::new(0, 1, 0), &world.shared), + false + ); } #[test] fn test_is_standable() { - let mut world = World::default(); + let mut world = PartialWorld::default(); world .set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) .unwrap(); @@ -190,8 +199,8 @@ mod tests { world.set_block_state(&BlockPos::new(0, 2, 0), BlockState::Air); world.set_block_state(&BlockPos::new(0, 3, 0), BlockState::Air); - assert!(is_standable(&BlockPos::new(0, 1, 0), &world)); - assert!(!is_standable(&BlockPos::new(0, 0, 0), &world)); - assert!(!is_standable(&BlockPos::new(0, 2, 0), &world)); + assert!(is_standable(&BlockPos::new(0, 1, 0), &world.shared)); + assert!(!is_standable(&BlockPos::new(0, 0, 0), &world.shared)); + assert!(!is_standable(&BlockPos::new(0, 2, 0), &world.shared)); } } diff --git a/azalea/src/swarm/chat.rs b/azalea/src/swarm/chat.rs index 6c51ba33..4582c59e 100644 --- a/azalea/src/swarm/chat.rs +++ b/azalea/src/swarm/chat.rs @@ -18,12 +18,12 @@ use async_trait::async_trait; use azalea_client::{ChatPacket, Client, Event}; use parking_lot::Mutex; use std::{collections::VecDeque, sync::Arc}; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::broadcast::{Receiver, Sender}; #[derive(Clone)] pub struct Plugin { pub swarm_state: SwarmState, - pub tx: UnboundedSender, + pub tx: Sender, } impl crate::Plugin for Plugin { @@ -31,7 +31,7 @@ impl crate::Plugin for Plugin { fn build(&self) -> State { State { - farthest_chat_index: Arc::new(Mutex::new(0)), + chat_index: Arc::new(Mutex::new(0)), swarm_state: self.swarm_state.clone(), tx: self.tx.clone(), } @@ -40,8 +40,8 @@ impl crate::Plugin for Plugin { #[derive(Clone)] pub struct State { - pub farthest_chat_index: Arc>, - pub tx: UnboundedSender, + pub chat_index: Arc>, + pub tx: Sender, pub swarm_state: SwarmState, } @@ -49,7 +49,49 @@ pub struct State { pub struct SwarmState { pub chat_queue: Arc>>, pub chat_min_index: Arc>, - pub rx: Arc>>, + pub rx: Arc>>, +} + +impl State { + pub fn handle_chat(&self, message: ChatPacket) { + // When a bot receives a chat messages, it looks into the queue to find the + // earliest instance of the message content that's after the bot's chat index. + // If it finds it, then its personal index is simply updated. Otherwise, fire + // the event and add to the queue. + + let mut chat_queue = self.swarm_state.chat_queue.lock(); + let chat_min_index = self.swarm_state.chat_min_index.lock(); + let mut chat_index = self.chat_index.lock(); + + if *chat_min_index > *chat_index { + // if this happens it's because this bot just logged in, so + // ignore it and let another bot handle it + println!("chat_min_index ({chat_min_index}) > chat_index ({chat_index})"); + *chat_index = *chat_min_index; + return; + } + let actual_vec_index = *chat_index - *chat_min_index; + + // go through the queue and find the first message that's after the bot's index + let mut found = false; + for (i, past_message) in chat_queue.iter().enumerate().skip(actual_vec_index) { + if past_message == &message { + // found the message, update the index + *chat_index = i + *chat_min_index + 1; + found = true; + break; + } + } + + if !found { + // didn't find the message, so fire the swarm event and add to the queue + self.tx + .send(message.clone()) + .expect("failed to send chat message to swarm"); + chat_queue.push_back(message); + *chat_index = chat_queue.len() + *chat_min_index; + } + } } #[async_trait] @@ -57,46 +99,17 @@ impl crate::PluginState for State { async fn handle(self: Box, event: Event, _bot: Client) { // we're allowed to access Plugin::swarm_state since it's shared for every bot if let Event::Chat(m) = event { - // When a bot receives a chat messages, it looks into the queue to find the - // earliest instance of the message content that's after the bot's chat index. - // If it finds it, then its personal index is simply updated. Otherwise, fire - // the event and add to the queue. - - let mut chat_queue = self.swarm_state.chat_queue.lock(); - let chat_min_index = self.swarm_state.chat_min_index.lock(); - let mut farthest_chat_index = self.farthest_chat_index.lock(); - - let actual_vec_index = *farthest_chat_index - *chat_min_index; - - // go through the queue and find the first message that's after the bot's index - let mut found = false; - for (i, msg) in chat_queue.iter().enumerate().skip(actual_vec_index) { - if msg == &m { - // found the message, update the index - *farthest_chat_index = i + *chat_min_index + 1; - found = true; - break; - } - } - - if !found { - // didn't find the message, so fire the swarm event and add to the queue - self.tx - .send(m.clone()) - .expect("failed to send chat message to swarm"); - chat_queue.push_back(m); - *farthest_chat_index = chat_queue.len() - 1 + *chat_min_index; - } + self.handle_chat(m); } } } impl SwarmState { - pub fn new(swarm: Swarm) -> (Self, UnboundedSender) + pub fn new(swarm: Swarm) -> (Self, Sender) where S: Send + Sync + Clone + 'static, { - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let (tx, rx) = tokio::sync::broadcast::channel(1); let swarm_state = SwarmState { chat_queue: Arc::new(Mutex::new(VecDeque::new())), @@ -114,35 +127,143 @@ impl SwarmState { // it should never be locked unless we reused the same plugin for two swarms // (bad) let mut rx = self.rx.lock().await; - while let Some(m) = rx.recv().await { + while let Ok(m) = rx.recv().await { swarm.swarm_tx.send(SwarmEvent::Chat(m)).unwrap(); - - // To make sure the queue doesn't grow too large, we keep a `chat_min_index` - // in Swarm that's set to the smallest index of all the bots, and we remove all - // messages from the queue that are before that index. - - let chat_min_index = *self.chat_min_index.lock(); - let mut new_chat_min_index = usize::MAX; - for (bot, _) in swarm.bot_datas.lock().iter() { - let this_farthest_chat_index = *bot - .plugins - .get::() - .expect("Chat plugin not installed") - .farthest_chat_index - .lock(); - if this_farthest_chat_index < new_chat_min_index { - new_chat_min_index = this_farthest_chat_index; - } - } - - let mut chat_queue = self.chat_queue.lock(); - // remove all messages from the queue that are before the min index - for _ in 0..(new_chat_min_index - chat_min_index) { - chat_queue.pop_front(); - } - - // update the min index - *self.chat_min_index.lock() = new_chat_min_index; + let bot_states = swarm + .bot_datas + .lock() + .iter() + .map(|(bot, _)| { + bot.plugins + .get::() + .expect("Chat plugin not installed") + .clone() + }) + .collect::>(); + self.handle_new_chat_message(&bot_states); } } } + +impl SwarmState { + pub fn handle_new_chat_message(&self, bot_states: &[State]) { + // To make sure the queue doesn't grow too large, we keep a `chat_min_index` + // in Swarm that's set to the smallest index of all the bots, and we remove all + // messages from the queue that are before that index. + + let chat_min_index = *self.chat_min_index.lock(); + let mut new_chat_min_index = usize::MAX; + for bot_state in bot_states { + let this_chat_index = *bot_state.chat_index.lock(); + if this_chat_index < new_chat_min_index { + new_chat_min_index = this_chat_index; + } + } + + let mut chat_queue = self.chat_queue.lock(); + if chat_min_index > new_chat_min_index { + println!( + "chat_min_index ({chat_min_index}) > new_chat_min_index ({new_chat_min_index})" + ); + return; + } + // remove all messages from the queue that are before the min index + for _ in 0..(new_chat_min_index - chat_min_index) { + chat_queue.pop_front(); + } + + // update the min index + *self.chat_min_index.lock() = new_chat_min_index; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_swarm_chat() { + let (tx, mut rx) = tokio::sync::broadcast::channel(1); + let swarm_state = SwarmState { + chat_queue: Arc::new(Mutex::new(VecDeque::new())), + chat_min_index: Arc::new(Mutex::new(0)), + rx: Arc::new(tokio::sync::Mutex::new(rx)), + }; + let mut bot_states = vec![]; + let bot0 = State { + swarm_state: swarm_state.clone(), + chat_index: Arc::new(Mutex::new(0)), + tx: tx.clone(), + }; + let bot1 = State { + swarm_state: swarm_state.clone(), + chat_index: Arc::new(Mutex::new(0)), + tx: tx.clone(), + }; + bot_states.push(bot0.clone()); + bot_states.push(bot1.clone()); + + bot0.handle_chat(ChatPacket::new("a")); + // the swarm should get the event immediately after the bot gets it + assert_eq!( + swarm_state.rx.lock().await.try_recv(), + Ok(ChatPacket::new("a")) + ); + assert_eq!(*bot0.chat_index.lock(), 1); + // and a second bot sending the event shouldn't do anything + bot1.handle_chat(ChatPacket::new("a")); + assert!(swarm_state.rx.lock().await.try_recv().is_err()); + assert_eq!(*bot1.chat_index.lock(), 1); + + // but if the first one gets it again, it should sent it again + bot0.handle_chat(ChatPacket::new("a")); + assert_eq!( + swarm_state.rx.lock().await.try_recv(), + Ok(ChatPacket::new("a")) + ); + + // alright and now the second bot got a different chat message and it should be + // sent + bot1.handle_chat(ChatPacket::new("b")); + assert_eq!( + swarm_state.rx.lock().await.try_recv(), + Ok(ChatPacket::new("b")) + ); + } + + #[tokio::test] + async fn test_new_bot() { + let (tx, mut rx) = tokio::sync::broadcast::channel(1); + let swarm_state = SwarmState { + chat_queue: Arc::new(Mutex::new(VecDeque::new())), + chat_min_index: Arc::new(Mutex::new(0)), + rx: Arc::new(tokio::sync::Mutex::new(rx)), + }; + let mut bot_states = vec![]; + let bot0 = State { + swarm_state: swarm_state.clone(), + chat_index: Arc::new(Mutex::new(0)), + tx: tx.clone(), + }; + bot_states.push(bot0.clone()); + + // bot0 gets a chat message + bot0.handle_chat(ChatPacket::new("a")); + assert_eq!( + swarm_state.rx.lock().await.try_recv(), + Ok(ChatPacket::new("a")) + ); + // now a second bot joined and got a different chat message + let bot1 = State { + swarm_state: swarm_state.clone(), + chat_index: Arc::new(Mutex::new(0)), + tx: tx.clone(), + }; + bot_states.push(bot1.clone()); + bot1.handle_chat(ChatPacket::new("b")); + assert_eq!( + swarm_state.rx.lock().await.try_recv(), + Ok(ChatPacket::new("b")) + ); + } +}