mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
make entities have a reference to WeakWorlds instead
... and other exciting bug fixes
This commit is contained in:
parent
2d6737b247
commit
37b9f10b3b
17 changed files with 531 additions and 335 deletions
|
@ -292,6 +292,11 @@ impl From<String> 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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<tokio::sync::Mutex<WriteConnection<ServerboundGamePacket>>>,
|
||||
pub entity_id: Arc<RwLock<u32>>,
|
||||
/// The world that this client has access to. This supports shared worlds.
|
||||
pub world: Arc<RwLock<World>>,
|
||||
pub world: Arc<RwLock<PartialWorld>>,
|
||||
/// 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<WeakWorld> {
|
||||
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<RwLockWriteGuard<World>> {
|
||||
pub fn entity(&self) -> Entity<Arc<WeakWorld>> {
|
||||
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<RwLockReadGuard<World>> {
|
||||
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() };
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<D: Deref<Target = WeakWorld>> 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<D: DerefMut<Target = World>> MovableEntity for Entity<'_, D> {
|
||||
impl<D: Deref<Target = WeakWorld>> 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<VoxelShape>,
|
||||
) -> Vec3 {
|
||||
let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
|
||||
|
|
|
@ -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<Arc<Mutex<Chunk>>> {
|
||||
fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<Arc<RwLock<Chunk>>> {
|
||||
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);
|
||||
|
||||
|
|
|
@ -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<D: DerefMut<Target = World>> HasPhysics for Entity<'_, D> {
|
||||
impl<D: Deref<Target = WeakWorld>> 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<D: DerefMut<Target = World>>(
|
||||
fn handle_relative_friction_and_calculate_movement<D: Deref<Target = WeakWorld>>(
|
||||
entity: &mut Entity<D>,
|
||||
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<D: DerefMut<Target = World>>(entity: &Entity<D>) -> f32 {
|
||||
fn block_jump_factor<D: Deref<Target = WeakWorld>>(entity: &Entity<D>) -> 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<D: DerefMut<Target = World>>(entity: &Entity<D>) -> f32 {
|
|||
// public double getJumpBoostPower() {
|
||||
// return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F *
|
||||
// (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; }
|
||||
fn jump_power<D: DerefMut<Target = World>>(entity: &Entity<D>) -> f32 {
|
||||
fn jump_power<D: Deref<Target = WeakWorld>>(entity: &Entity<D>) -> f32 {
|
||||
0.42 * block_jump_factor(entity)
|
||||
}
|
||||
|
||||
fn jump_boost_power<D: DerefMut<Target = World>>(_entity: &Entity<D>) -> f64 {
|
||||
fn jump_boost_power<D: Deref<Target = WeakWorld>>(_entity: &Entity<D>) -> 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();
|
||||
|
|
|
@ -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<Option<Arc<Mutex<Chunk>>>>,
|
||||
chunks: Vec<Option<Arc<RwLock<Chunk>>>>,
|
||||
}
|
||||
|
||||
/// 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<ChunkPos, Weak<Mutex<Chunk>>>,
|
||||
pub chunks: HashMap<ChunkPos, Weak<RwLock<Chunk>>>,
|
||||
}
|
||||
|
||||
/// 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<ChunkPos, Arc<Mutex<Chunk>>>,
|
||||
pub chunks: HashMap<ChunkPos, Arc<RwLock<Chunk>>>,
|
||||
}
|
||||
|
||||
/// 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<BlockState> {
|
||||
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<BlockState> {
|
||||
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<Mutex<Chunk>>> {
|
||||
pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> {
|
||||
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<Arc<Mutex<Chunk>>>> {
|
||||
pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> {
|
||||
if !self.in_range(pos) {
|
||||
return None;
|
||||
}
|
||||
|
@ -183,7 +172,7 @@ impl PartialChunkStorage {
|
|||
}
|
||||
|
||||
/// Get a chunk,
|
||||
pub fn get(&self, pos: &ChunkPos) -> Option<Arc<Mutex<Chunk>>> {
|
||||
pub fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
|
||||
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<Arc<Mutex<Chunk>>>) {
|
||||
pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) {
|
||||
if let Some(chunk) = &chunk {
|
||||
self.shared
|
||||
.write()
|
||||
|
@ -219,16 +208,30 @@ impl WeakChunkStorage {
|
|||
chunks: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
|
||||
self.chunks.get(pos).and_then(|chunk| chunk.upgrade())
|
||||
}
|
||||
|
||||
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
|
||||
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<BlockState> {
|
||||
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, BufReadError> {
|
||||
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<BlockState> {
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -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<Target = World>> Entity<'d, D> {
|
||||
impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
|
||||
pub fn new(world: D, id: u32, data: NonNull<EntityData>) -> Self {
|
||||
// TODO: have this be based on the entity type
|
||||
Self {
|
||||
|
@ -38,7 +38,7 @@ impl<'d, D: Deref<Target = World>> Entity<'d, D> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'d, D: DerefMut<Target = World>> Entity<'d, D> {
|
||||
impl<'d, D: Deref<Target = WeakWorld>> 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<Target = World>> Entity<'d, D> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'d, D: Deref<Target = World>> Entity<'d, D> {
|
||||
impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
|
||||
#[inline]
|
||||
pub fn pos(&self) -> &Vec3 {
|
||||
&self.pos
|
||||
|
@ -150,8 +150,8 @@ impl<'d, D: Deref<Target = World>> Entity<'d, D> {
|
|||
|
||||
// impl<
|
||||
// 'd,
|
||||
// D: DerefMut<Target = World> + Deref<Target = World>,
|
||||
// D2: Deref<Target = World>,
|
||||
// D: Deref<Target = WeakWorld> + Deref<Target = WeakWorld>,
|
||||
// D2: Deref<Target = WeakWorld>,
|
||||
// > From<Entity<'d, D>> for Entity<'d, D2>
|
||||
// {
|
||||
// fn from(entity: Entity<'d, D>) -> Entity<'d, D> {
|
||||
|
@ -164,13 +164,13 @@ impl<'d, D: Deref<Target = World>> Entity<'d, D> {
|
|||
// }
|
||||
// }
|
||||
|
||||
impl<D: DerefMut<Target = World>> DerefMut for Entity<'_, D> {
|
||||
impl<D: Deref<Target = WeakWorld>> DerefMut for Entity<'_, D> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
unsafe { self.data.as_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Deref<Target = World>> Deref for Entity<'_, D> {
|
||||
impl<D: Deref<Target = WeakWorld>> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,11 @@ use uuid::Uuid;
|
|||
pub struct PartialEntityStorage {
|
||||
pub shared: Arc<RwLock<WeakEntityStorage>>,
|
||||
|
||||
/// 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<u32, u32>,
|
||||
/// 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<Arc<EntityData>> {
|
||||
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<Arc<EntityData>> {
|
||||
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<F>(&self, mut f: F) -> Option<Arc<EntityData>>
|
||||
pub fn find_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
|
||||
where
|
||||
F: FnMut(&Arc<EntityData>) -> 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<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<Arc<EntityData>>
|
||||
pub fn find_entity_in_chunk<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<Arc<EntityData>>
|
||||
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<Arc<EntityData>> {
|
||||
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<Arc<EntityData>> {
|
||||
self.id_by_uuid
|
||||
.get(uuid)
|
||||
.and_then(|id| self.data_by_id.get(id).and_then(|e| e.upgrade()))
|
||||
}
|
||||
|
||||
pub fn find_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
|
||||
where
|
||||
F: FnMut(&Arc<EntityData>) -> 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<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<Arc<EntityData>>
|
||||
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)]
|
||||
|
|
|
@ -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<WeakWorld>,
|
||||
pub shared: Arc<WeakWorld>,
|
||||
|
||||
pub chunk_storage: PartialChunkStorage,
|
||||
pub entity_storage: PartialEntityStorage,
|
||||
|
@ -31,10 +37,10 @@ pub struct WeakWorld {
|
|||
pub entity_storage: Arc<RwLock<WeakEntityStorage>>,
|
||||
}
|
||||
|
||||
impl World {
|
||||
impl PartialWorld {
|
||||
pub fn new(chunk_radius: u32, shared: Arc<WeakWorld>, 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<Arc<Mutex<Chunk>>> {
|
||||
pub fn get_chunk(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
|
||||
self.chunk_storage.get(pos)
|
||||
}
|
||||
|
||||
pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option<Chunk>) -> 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<BlockState> {
|
||||
self.chunk_storage.get_block_state(pos)
|
||||
}
|
||||
|
||||
pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
|
||||
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<Entity<'_, &WeakWorld>> {
|
||||
// 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<Arc<EntityData>> {
|
||||
self.entity_storage.get_by_id(id)
|
||||
}
|
||||
|
||||
pub fn entity(&self, id: u32) -> Option<Entity<&World>> {
|
||||
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<Entity<'_, &mut World>> {
|
||||
// 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<Arc<EntityData>> {
|
||||
self.entity_storage.get_by_uuid(uuid)
|
||||
}
|
||||
|
||||
pub fn find_one_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
|
||||
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<Arc<EntityData>> {
|
||||
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<Entity<&WeakWorld>> {
|
||||
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<Arc<EntityData>> {
|
||||
self.entity_storage.read().get_by_uuid(uuid)
|
||||
}
|
||||
|
||||
pub fn find_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
|
||||
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<BlockState> {
|
||||
self.chunk_storage.read().get_block_state(pos)
|
||||
}
|
||||
|
||||
pub fn get_chunk(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
|
||||
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)
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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<Vec<Node>> to a VecDeque<Node>
|
||||
*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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ChatPacket>,
|
||||
pub tx: Sender<ChatPacket>,
|
||||
}
|
||||
|
||||
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<Mutex<usize>>,
|
||||
pub tx: UnboundedSender<ChatPacket>,
|
||||
pub chat_index: Arc<Mutex<usize>>,
|
||||
pub tx: Sender<ChatPacket>,
|
||||
pub swarm_state: SwarmState,
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,49 @@ pub struct State {
|
|||
pub struct SwarmState {
|
||||
pub chat_queue: Arc<Mutex<VecDeque<ChatPacket>>>,
|
||||
pub chat_min_index: Arc<Mutex<usize>>,
|
||||
pub rx: Arc<tokio::sync::Mutex<UnboundedReceiver<ChatPacket>>>,
|
||||
pub rx: Arc<tokio::sync::Mutex<Receiver<ChatPacket>>>,
|
||||
}
|
||||
|
||||
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<Self>, 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<S>(swarm: Swarm<S>) -> (Self, UnboundedSender<ChatPacket>)
|
||||
pub fn new<S>(swarm: Swarm<S>) -> (Self, Sender<ChatPacket>)
|
||||
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::<State>()
|
||||
.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::<State>()
|
||||
.expect("Chat plugin not installed")
|
||||
.clone()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue