diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index 547b1b4b..d72a06b2 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -2,9 +2,10 @@ use std::{collections::HashSet, io::Cursor, sync::Arc}; use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3}; use azalea_entity::{ + indexing::EntityUuidIndex, metadata::{apply_metadata, Health, PlayerMetadataBundle}, - Dead, EntityBundle, EntityInfos, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, - LookDirection, Physics, PlayerBundle, Position, RelativeEntityUpdate, + Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection, + Physics, PlayerBundle, Position, RelativeEntityUpdate, }; use azalea_protocol::{ connect::{ReadConnection, WriteConnection}, @@ -579,9 +580,9 @@ fn process_packet_events(ecs: &mut World) { Commands, Query>, Res, - ResMut, + ResMut, )> = SystemState::new(ecs); - let (mut commands, mut query, instance_container, mut entity_infos) = + let (mut commands, mut query, instance_container, mut entity_uuid_index) = system_state.get_mut(ecs); let instance_name = query.get_mut(player_entity).unwrap(); @@ -601,9 +602,7 @@ fn process_packet_events(ecs: &mut World) { .write() .entity_by_id .insert(MinecraftEntityId(p.id), entity_commands.id()); - entity_infos - .entity_by_uuid - .insert(p.uuid, entity_commands.id()); + entity_uuid_index.insert(p.uuid, entity_commands.id()); } // the bundle doesn't include the default entity metadata so we add that diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index 25ba0d8c..a94340ab 100755 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -1,7 +1,7 @@ use azalea_auth::game_profile::GameProfile; use azalea_chat::FormattedText; use azalea_core::GameMode; -use azalea_entity::EntityInfos; +use azalea_entity::indexing::EntityUuidIndex; use bevy_ecs::{ event::EventReader, system::{Commands, Res}, @@ -35,10 +35,10 @@ pub struct PlayerInfo { pub fn retroactively_add_game_profile_component( mut commands: Commands, mut events: EventReader, - entity_infos: Res, + entity_uuid_index: Res, ) { for event in events.iter() { - if let Some(entity) = entity_infos.get_entity_by_uuid(&event.info.uuid) { + if let Some(entity) = entity_uuid_index.get(&event.info.uuid) { commands .entity(entity) .insert(GameProfileComponent(event.info.profile.clone())); diff --git a/azalea-entity/src/dimensions.rs b/azalea-entity/src/dimensions.rs index 5e716307..1d013d10 100755 --- a/azalea-entity/src/dimensions.rs +++ b/azalea-entity/src/dimensions.rs @@ -1,7 +1,4 @@ use azalea_core::{Vec3, AABB}; -use bevy_ecs::{query::Changed, system::Query}; - -use super::{Physics, Position}; #[derive(Debug, Default)] pub struct EntityDimensions { @@ -24,15 +21,3 @@ impl EntityDimensions { } } } - -/// Sets the position of the entity. This doesn't update the cache in -/// azalea-world, and should only be used within azalea-world! -/// -/// # Safety -/// Cached position in the world must be updated. -pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed>) { - for (position, mut physics) in query.iter_mut() { - let bounding_box = physics.dimensions.make_bounding_box(position); - physics.bounding_box = bounding_box; - } -} diff --git a/azalea-entity/src/info.rs b/azalea-entity/src/info.rs deleted file mode 100644 index 0c5fd3d3..00000000 --- a/azalea-entity/src/info.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Implement things relating to entity datas, like an index of uuids to -//! entities. - -use azalea_core::ChunkPos; -use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; -use bevy_app::{App, Plugin, PostUpdate, PreUpdate, Update}; -use bevy_ecs::{ - component::Component, - entity::Entity, - query::{Added, Changed, With, Without}, - schedule::{IntoSystemConfigs, SystemSet}, - system::{Commands, EntityCommand, Query, Res, ResMut, Resource}, - world::{EntityMut, World}, -}; -use derive_more::{Deref, DerefMut}; -use log::{debug, warn}; -use parking_lot::RwLock; -use std::{ - collections::{HashMap, HashSet}, - fmt::Debug, - sync::Arc, -}; -use uuid::Uuid; - -use crate::{ - add_dead, - systems::{ - deduplicate_entities, deduplicate_local_entities, update_entity_by_id_index, - update_fluid_on_eyes, update_uuid_index, - }, - update_bounding_box, EntityUuid, LastSentPosition, Position, -}; - -use super::{Local, LookDirection}; - -/// A Bevy [`SystemSet`] for various types of entity updates. -#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)] -pub enum EntityUpdateSet { - /// Remove ECS entities that refer to an entity that was already in the ECS - /// before. - Deduplicate, - /// Create search indexes for entities. - Index, - /// Remove despawned entities from search indexes. - Deindex, -} - -/// Plugin handling some basic entity functionality. -pub struct EntityPlugin; -impl Plugin for EntityPlugin { - fn build(&self, app: &mut App) { - // entities get added pre-update - // added to indexes during update (done by this plugin) - // modified during update - // despawned post-update (done by this plugin) - app.add_systems( - PreUpdate, - remove_despawned_entities_from_indexes.in_set(EntityUpdateSet::Deindex), - ) - .add_systems( - PostUpdate, - (deduplicate_entities, deduplicate_local_entities).in_set(EntityUpdateSet::Deduplicate), - ) - .add_systems( - Update, - ( - ( - update_entity_chunk_positions, - update_uuid_index, - update_entity_by_id_index, - ) - .in_set(EntityUpdateSet::Index), - ( - add_updates_received, - debug_new_entity, - debug_detect_updates_received_on_local_entities, - add_dead, - update_bounding_box, - clamp_look_direction, - update_fluid_on_eyes, - ), - ), - ) - .init_resource::(); - } -} - -fn debug_new_entity(query: Query<(Entity, Option<&Local>), Added>) { - for (entity, local) in query.iter() { - if local.is_some() { - debug!("new local entity: {:?}", entity); - } else { - debug!("new entity: {:?}", entity); - } - } -} - -// How entity updates are processed (to avoid issues with shared worlds) -// - each bot contains a map of { entity id: updates received } -// - the shared world also contains a canonical "true" updates received for each -// entity -// - when a client loads an entity, its "updates received" is set to the same as -// the global "updates received" -// - when the shared world sees an entity for the first time, the "updates -// received" is set to 1. -// - clients can force the shared "updates received" to 0 to make it so certain -// entities (i.e. other bots in our swarm) don't get confused and updated by -// other bots -// - when a client gets an update to an entity, we check if our "updates -// received" is the same as the shared world's "updates received": if it is, -// then process the update and increment the client's and shared world's -// "updates received" if not, then we simply increment our local "updates -// received" and do nothing else - -/// An [`EntityCommand`] that applies a "relative update" to an entity, which -/// means this update won't be run multiple times by different clients in the -/// same world. -/// -/// This is used to avoid a bug where when there's multiple clients in the same -/// world and an entity sends a relative move packet to all clients, its -/// position gets desynced since the relative move is applied multiple times. -/// -/// Don't use this unless you actually got an entity update packet that all -/// other clients within render distance will get too. You usually don't need -/// this when the change isn't relative either. -pub struct RelativeEntityUpdate { - pub partial_world: Arc>, - // a function that takes the entity and updates it - pub update: Box, -} -impl EntityCommand for RelativeEntityUpdate { - fn apply(self, entity: Entity, world: &mut World) { - let partial_entity_infos = &mut self.partial_world.write().entity_infos; - - let mut entity_mut = world.entity_mut(entity); - - if Some(entity) == partial_entity_infos.owner_entity { - // if the entity owns this partial world, it's always allowed to update itself - (self.update)(&mut entity_mut); - return; - }; - - let entity_id = *entity_mut.get::().unwrap(); - let Some(updates_received) = entity_mut.get_mut::() else { - // a client tried to update another client, which isn't allowed - return; - }; - - let this_client_updates_received = partial_entity_infos - .updates_received - .get(&entity_id) - .copied(); - - let can_update = this_client_updates_received.unwrap_or(1) == **updates_received; - if can_update { - let new_updates_received = this_client_updates_received.unwrap_or(0) + 1; - partial_entity_infos - .updates_received - .insert(entity_id, new_updates_received); - - **entity_mut.get_mut::().unwrap() = new_updates_received; - - let mut entity = world.entity_mut(entity); - (self.update)(&mut entity); - } - } -} - -/// Things that are shared between all the partial worlds. -#[derive(Resource, Default)] -pub struct EntityInfos { - /// An index of entities by their UUIDs - pub entity_by_uuid: HashMap, -} - -impl EntityInfos { - pub fn new() -> Self { - Self { - entity_by_uuid: HashMap::default(), - } - } - - pub fn get_entity_by_uuid(&self, uuid: &Uuid) -> Option { - self.entity_by_uuid.get(uuid).copied() - } -} - -/// Update the chunk position indexes in [`EntityInfos`]. -fn update_entity_chunk_positions( - mut query: Query<(Entity, &Position, &mut LastSentPosition, &InstanceName), Changed>, - instance_container: Res, -) { - for (entity, pos, last_pos, world_name) in query.iter_mut() { - let world_lock = instance_container.get(world_name).unwrap(); - let mut world = world_lock.write(); - - let old_chunk = ChunkPos::from(*last_pos); - let new_chunk = ChunkPos::from(*pos); - - if old_chunk != new_chunk { - // move the entity from the old chunk to the new one - if let Some(entities) = world.entities_by_chunk.get_mut(&old_chunk) { - entities.remove(&entity); - } - world - .entities_by_chunk - .entry(new_chunk) - .or_default() - .insert(entity); - } - } -} -/// A component that lists all the local player entities that have this entity -/// loaded. If this is empty, the entity will be removed from the ECS. -#[derive(Component, Clone, Deref, DerefMut)] -pub struct LoadedBy(pub HashSet); - -/// A component that counts the number of times this entity has been modified. -/// This is used for making sure two clients don't do the same relative update -/// on an entity. -/// -/// If an entity is local (i.e. it's a client/localplayer), this component -/// should NOT be present in the entity. -#[derive(Component, Debug, Deref, DerefMut)] -pub struct UpdatesReceived(u32); - -#[allow(clippy::type_complexity)] -pub fn add_updates_received( - mut commands: Commands, - query: Query< - Entity, - ( - Changed, - (Without, Without), - ), - >, -) { - for entity in query.iter() { - // entities always start with 1 update received - commands.entity(entity).insert(UpdatesReceived(1)); - } -} - -/// The [`UpdatesReceived`] component should never be on [`Local`] entities. -/// This warns if an entity has both components. -fn debug_detect_updates_received_on_local_entities( - query: Query, With)>, -) { - for entity in &query { - warn!("Entity {:?} has both Local and UpdatesReceived", entity); - } -} - -/// Despawn entities that aren't being loaded by anything. -fn remove_despawned_entities_from_indexes( - mut commands: Commands, - mut entity_infos: ResMut, - instance_container: Res, - query: Query<(Entity, &EntityUuid, &Position, &InstanceName, &LoadedBy), Changed>, -) { - for (entity, uuid, position, world_name, loaded_by) in &query { - let world_lock = instance_container.get(world_name).unwrap(); - let mut world = world_lock.write(); - - // if the entity has no references left, despawn it - if !loaded_by.is_empty() { - continue; - } - - // remove the entity from the chunk index - let chunk = ChunkPos::from(*position); - if let Some(entities_in_chunk) = world.entities_by_chunk.get_mut(&chunk) { - if entities_in_chunk.remove(&entity) { - // remove the chunk if there's no entities in it anymore - if entities_in_chunk.is_empty() { - world.entities_by_chunk.remove(&chunk); - } - } else { - warn!("Tried to remove entity from chunk {chunk:?} but the entity was not there."); - } - } else { - warn!("Tried to remove entity from chunk {chunk:?} but the chunk was not found."); - } - // remove it from the uuid index - if entity_infos.entity_by_uuid.remove(uuid).is_none() { - warn!("Tried to remove entity {entity:?} from the uuid index but it was not there."); - } - // and now remove the entity from the ecs - commands.entity(entity).despawn(); - debug!("Despawned entity {entity:?} because it was not loaded by anything."); - return; - } -} - -pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) { - for mut look_direction in &mut query { - look_direction.y_rot %= 360.0; - look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0; - } -} - -impl Debug for EntityInfos { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EntityInfos").finish() - } -} diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 76c5220a..bc12e64c 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -5,33 +5,24 @@ mod data; mod dimensions; mod effects; mod enchantments; -mod info; pub mod metadata; pub mod mining; -mod systems; +mod plugin; -use self::{attributes::AttributeInstance, metadata::Health}; +use self::attributes::AttributeInstance; pub use attributes::Attributes; use azalea_block::BlockState; use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB}; use azalea_world::{ChunkStorage, InstanceName}; -use bevy_ecs::{ - bundle::Bundle, - component::Component, - entity::Entity, - query::Changed, - system::{Commands, Query}, -}; +use bevy_ecs::{bundle::Bundle, component::Component}; pub use data::*; use derive_more::{Deref, DerefMut}; -pub use dimensions::{update_bounding_box, EntityDimensions}; -pub use info::{ - clamp_look_direction, EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, - RelativeEntityUpdate, -}; +pub use dimensions::EntityDimensions; use std::fmt::Debug; use uuid::Uuid; +pub use crate::plugin::*; + pub fn move_relative( physics: &mut Physics, direction: &LookDirection, @@ -234,21 +225,6 @@ pub struct Physics { #[derive(Component, Copy, Clone, Default)] pub struct Dead; -/// System that adds the [`Dead`] marker component if an entity's health is set -/// to 0 (or less than 0). This will be present if an entity is doing the death -/// animation. -/// -/// Entities that are dead can not be revived. -/// TODO: fact check this in-game by setting an entity's health to 0 and then -/// not 0 -pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed>) { - for (entity, health) in query.iter() { - if **health <= 0.0 { - commands.entity(entity).insert(Dead); - } - } -} - /// A component that contains the offset of the entity's eyes from the entity /// coordinates. /// diff --git a/azalea-entity/src/systems.rs b/azalea-entity/src/plugin/indexing.rs similarity index 57% rename from azalea-entity/src/systems.rs rename to azalea-entity/src/plugin/indexing.rs index 3fa8c4d5..f7dfe0fa 100644 --- a/azalea-entity/src/systems.rs +++ b/azalea-entity/src/plugin/indexing.rs @@ -1,9 +1,51 @@ -use azalea_core::{BlockPos, Vec3}; -use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId}; -use bevy_ecs::prelude::*; -use log::{debug, error, info}; +//! Stuff related to entity indexes and keeping track of entities in the world. -use crate::{EntityInfos, EntityUuid, EyeHeight, FluidOnEyes, LoadedBy, Local, Position}; +use azalea_core::ChunkPos; +use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId}; +use bevy_ecs::{ + entity::Entity, + query::{Changed, With, Without}, + system::{Commands, Query, Res, ResMut, Resource}, +}; +use log::{debug, error, info, warn}; +use std::{collections::HashMap, fmt::Debug}; +use uuid::Uuid; + +use crate::{EntityUuid, LastSentPosition, Local, Position}; + +use super::LoadedBy; + +#[derive(Resource, Default)] +pub struct EntityUuidIndex { + /// An index of entities by their UUIDs + entity_by_uuid: HashMap, +} + +impl EntityUuidIndex { + pub fn new() -> Self { + Self { + entity_by_uuid: HashMap::default(), + } + } + + pub fn get(&self, uuid: &Uuid) -> Option { + self.entity_by_uuid.get(uuid).copied() + } + + pub fn contains_key(&self, uuid: &Uuid) -> bool { + self.entity_by_uuid.contains_key(uuid) + } + + pub fn insert(&mut self, uuid: Uuid, entity: Entity) { + self.entity_by_uuid.insert(uuid, entity); + } +} + +impl Debug for EntityUuidIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EntityUuidIndex").finish() + } +} /// Remove new entities that have the same id as an existing entity, and /// increase the reference counts. @@ -89,7 +131,7 @@ pub fn deduplicate_local_entities( } pub fn update_uuid_index( - mut entity_infos: ResMut, + mut entity_infos: ResMut, query: Query<(Entity, &EntityUuid, Option<&Local>), Changed>, ) { for (entity, &uuid, local) in query.iter() { @@ -108,29 +150,6 @@ pub fn update_uuid_index( } } -// /// Clear all entities in a chunk. This will not clear them from the -// /// shared storage unless there are no other references to them. -// pub fn clear_entities_in_chunk( -// mut commands: Commands, -// partial_entity_infos: &mut PartialEntityInfos, -// chunk: &ChunkPos, -// instance_container: &WorldContainer, -// world_name: &InstanceName, -// mut query: Query<(&MinecraftEntityId, &mut ReferenceCount)>, -// ) { let world_lock = instance_container.get(world_name).unwrap(); let world = -// world_lock.read(); - -// if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() { -// for &entity in &entities { -// let (id, mut reference_count) = query.get_mut(entity).unwrap(); -// if partial_entity_infos.loaded_entity_ids.remove(id) { -// // decrease the reference count -// **reference_count -= 1; -// } -// } -// } -// } - /// System to keep the entity_by_id index up-to-date. pub fn update_entity_by_id_index( mut query: Query< @@ -156,26 +175,69 @@ pub fn update_entity_by_id_index( } } -pub fn update_fluid_on_eyes( - mut query: Query<(&mut FluidOnEyes, &Position, &EyeHeight, &InstanceName)>, +/// Update the chunk position indexes in [`EntityUuidIndex`]. +pub fn update_entity_chunk_positions( + mut query: Query<(Entity, &Position, &mut LastSentPosition, &InstanceName), Changed>, instance_container: Res, ) { - for (mut fluid_on_eyes, position, eye_height, instance_name) in query.iter_mut() { - let Some(instance) = instance_container.get(instance_name) else { - continue; - }; + for (entity, pos, last_pos, world_name) in query.iter_mut() { + let world_lock = instance_container.get(world_name).unwrap(); + let mut world = world_lock.write(); - let adjusted_eye_y = position.y + (**eye_height as f64) - 0.1111111119389534; - let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z)); - let fluid_at_eye = instance - .read() - .get_fluid_state(&eye_block_pos) - .unwrap_or_default(); - let fluid_cutoff_y = eye_block_pos.y as f64 + (fluid_at_eye.height as f64 / 16f64); - if fluid_cutoff_y > adjusted_eye_y { - **fluid_on_eyes = fluid_at_eye.fluid; - } else { - **fluid_on_eyes = azalea_registry::Fluid::Empty; + let old_chunk = ChunkPos::from(*last_pos); + let new_chunk = ChunkPos::from(*pos); + + if old_chunk != new_chunk { + // move the entity from the old chunk to the new one + if let Some(entities) = world.entities_by_chunk.get_mut(&old_chunk) { + entities.remove(&entity); + } + world + .entities_by_chunk + .entry(new_chunk) + .or_default() + .insert(entity); } } } + +/// Despawn entities that aren't being loaded by anything. +pub fn remove_despawned_entities_from_indexes( + mut commands: Commands, + mut entity_infos: ResMut, + instance_container: Res, + query: Query<(Entity, &EntityUuid, &Position, &InstanceName, &LoadedBy), Changed>, +) { + for (entity, uuid, position, world_name, loaded_by) in &query { + let world_lock = instance_container.get(world_name).unwrap(); + let mut world = world_lock.write(); + + // if the entity has no references left, despawn it + if !loaded_by.is_empty() { + continue; + } + + // remove the entity from the chunk index + let chunk = ChunkPos::from(*position); + if let Some(entities_in_chunk) = world.entities_by_chunk.get_mut(&chunk) { + if entities_in_chunk.remove(&entity) { + // remove the chunk if there's no entities in it anymore + if entities_in_chunk.is_empty() { + world.entities_by_chunk.remove(&chunk); + } + } else { + warn!("Tried to remove entity from chunk {chunk:?} but the entity was not there."); + } + } else { + warn!("Tried to remove entity from chunk {chunk:?} but the chunk was not found."); + } + // remove it from the uuid index + if entity_infos.entity_by_uuid.remove(uuid).is_none() { + warn!("Tried to remove entity {entity:?} from the uuid index but it was not there."); + } + // and now remove the entity from the ecs + commands.entity(entity).despawn(); + debug!("Despawned entity {entity:?} because it was not loaded by anything."); + return; + } +} diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs new file mode 100644 index 00000000..7b514fcc --- /dev/null +++ b/azalea-entity/src/plugin/mod.rs @@ -0,0 +1,147 @@ +pub mod indexing; +mod relative_updates; + +use std::collections::HashSet; + +use azalea_core::{BlockPos, Vec3}; +use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId}; +use bevy_app::{App, Plugin, PostUpdate, PreUpdate, Update}; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; +use log::debug; + +use crate::{ + metadata::Health, Dead, EyeHeight, FluidOnEyes, Local, LookDirection, Physics, Position, +}; + +use indexing::EntityUuidIndex; +pub use relative_updates::RelativeEntityUpdate; + +/// A Bevy [`SystemSet`] for various types of entity updates. +#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)] +pub enum EntityUpdateSet { + /// Remove ECS entities that refer to an entity that was already in the ECS + /// before. + Deduplicate, + /// Create search indexes for entities. + Index, + /// Remove despawned entities from search indexes. + Deindex, +} + +/// Plugin handling some basic entity functionality. +pub struct EntityPlugin; +impl Plugin for EntityPlugin { + fn build(&self, app: &mut App) { + // entities get added pre-update + // added to indexes during update (done by this plugin) + // modified during update + // despawned post-update (done by this plugin) + app.add_systems( + PreUpdate, + indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSet::Deindex), + ) + .add_systems( + PostUpdate, + ( + indexing::deduplicate_entities, + indexing::deduplicate_local_entities, + ) + .in_set(EntityUpdateSet::Deduplicate), + ) + .add_systems( + Update, + ( + ( + indexing::update_entity_chunk_positions, + indexing::update_uuid_index, + indexing::update_entity_by_id_index, + ) + .in_set(EntityUpdateSet::Index), + ( + relative_updates::add_updates_received, + relative_updates::debug_detect_updates_received_on_local_entities, + debug_new_entity, + add_dead, + update_bounding_box, + clamp_look_direction, + update_fluid_on_eyes, + ), + ), + ) + .init_resource::(); + } +} + +fn debug_new_entity(query: Query<(Entity, Option<&Local>), Added>) { + for (entity, local) in query.iter() { + if local.is_some() { + debug!("new local entity: {:?}", entity); + } else { + debug!("new entity: {:?}", entity); + } + } +} + +/// System that adds the [`Dead`] marker component if an entity's health is set +/// to 0 (or less than 0). This will be present if an entity is doing the death +/// animation. +/// +/// Entities that are dead can not be revived. +/// TODO: fact check this in-game by setting an entity's health to 0 and then +/// not 0 +pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed>) { + for (entity, health) in query.iter() { + if **health <= 0.0 { + commands.entity(entity).insert(Dead); + } + } +} + +pub fn update_fluid_on_eyes( + mut query: Query<(&mut FluidOnEyes, &Position, &EyeHeight, &InstanceName)>, + instance_container: Res, +) { + for (mut fluid_on_eyes, position, eye_height, instance_name) in query.iter_mut() { + let Some(instance) = instance_container.get(instance_name) else { + continue; + }; + + let adjusted_eye_y = position.y + (**eye_height as f64) - 0.1111111119389534; + let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z)); + let fluid_at_eye = instance + .read() + .get_fluid_state(&eye_block_pos) + .unwrap_or_default(); + let fluid_cutoff_y = eye_block_pos.y as f64 + (fluid_at_eye.height as f64 / 16f64); + if fluid_cutoff_y > adjusted_eye_y { + **fluid_on_eyes = fluid_at_eye.fluid; + } else { + **fluid_on_eyes = azalea_registry::Fluid::Empty; + } + } +} + +/// A component that lists all the local player entities that have this entity +/// loaded. If this is empty, the entity will be removed from the ECS. +#[derive(Component, Clone, Deref, DerefMut)] +pub struct LoadedBy(pub HashSet); + +pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) { + for mut look_direction in &mut query { + look_direction.y_rot %= 360.0; + look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0; + } +} + +/// Sets the position of the entity. This doesn't update the cache in +/// azalea-world, and should only be used within azalea-world! +/// +/// # Safety +/// Cached position in the world must be updated. +pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed>) { + for (position, mut physics) in query.iter_mut() { + let bounding_box = physics.dimensions.make_bounding_box(position); + physics.bounding_box = bounding_box; + } +} diff --git a/azalea-entity/src/plugin/relative_updates.rs b/azalea-entity/src/plugin/relative_updates.rs new file mode 100644 index 00000000..7d01feda --- /dev/null +++ b/azalea-entity/src/plugin/relative_updates.rs @@ -0,0 +1,122 @@ +// How entity updates are processed (to avoid issues with shared worlds) +// - each bot contains a map of { entity id: updates received } +// - the shared world also contains a canonical "true" updates received for each +// entity +// - when a client loads an entity, its "updates received" is set to the same as +// the global "updates received" +// - when the shared world sees an entity for the first time, the "updates +// received" is set to 1. +// - clients can force the shared "updates received" to 0 to make it so certain +// entities (i.e. other bots in our swarm) don't get confused and updated by +// other bots +// - when a client gets an update to an entity, we check if our "updates +// received" is the same as the shared world's "updates received": if it is, +// then process the update and increment the client's and shared world's +// "updates received" if not, then we simply increment our local "updates +// received" and do nothing else + +use std::sync::Arc; + +use azalea_world::{MinecraftEntityId, PartialInstance}; +use bevy_ecs::{ + prelude::{Component, Entity}, + query::{Changed, With, Without}, + system::{Commands, EntityCommand, Query}, + world::{EntityMut, World}, +}; +use derive_more::{Deref, DerefMut}; +use log::warn; +use parking_lot::RwLock; + +use crate::Local; + +/// An [`EntityCommand`] that applies a "relative update" to an entity, which +/// means this update won't be run multiple times by different clients in the +/// same world. +/// +/// This is used to avoid a bug where when there's multiple clients in the same +/// world and an entity sends a relative move packet to all clients, its +/// position gets desynced since the relative move is applied multiple times. +/// +/// Don't use this unless you actually got an entity update packet that all +/// other clients within render distance will get too. You usually don't need +/// this when the change isn't relative either. +pub struct RelativeEntityUpdate { + pub partial_world: Arc>, + // a function that takes the entity and updates it + pub update: Box, +} + +/// A component that counts the number of times this entity has been modified. +/// This is used for making sure two clients don't do the same relative update +/// on an entity. +/// +/// If an entity is local (i.e. it's a client/localplayer), this component +/// should NOT be present in the entity. +#[derive(Component, Debug, Deref, DerefMut)] +pub struct UpdatesReceived(u32); + +impl EntityCommand for RelativeEntityUpdate { + fn apply(self, entity: Entity, world: &mut World) { + let partial_entity_infos = &mut self.partial_world.write().entity_infos; + + let mut entity_mut = world.entity_mut(entity); + + if Some(entity) == partial_entity_infos.owner_entity { + // if the entity owns this partial world, it's always allowed to update itself + (self.update)(&mut entity_mut); + return; + }; + + let entity_id = *entity_mut.get::().unwrap(); + let Some(updates_received) = entity_mut.get_mut::() else { + // a client tried to update another client, which isn't allowed + return; + }; + + let this_client_updates_received = partial_entity_infos + .updates_received + .get(&entity_id) + .copied(); + + let can_update = this_client_updates_received.unwrap_or(1) == **updates_received; + if can_update { + let new_updates_received = this_client_updates_received.unwrap_or(0) + 1; + partial_entity_infos + .updates_received + .insert(entity_id, new_updates_received); + + **entity_mut.get_mut::().unwrap() = new_updates_received; + + let mut entity = world.entity_mut(entity); + (self.update)(&mut entity); + } + } +} + +#[allow(clippy::type_complexity)] +pub fn add_updates_received( + mut commands: Commands, + query: Query< + Entity, + ( + Changed, + (Without, Without), + ), + >, +) { + for entity in query.iter() { + // entities always start with 1 update received + commands.entity(entity).insert(UpdatesReceived(1)); + } +} + +/// The [`UpdatesReceived`] component should never be on [`Local`] entities. +/// This warns if an entity has both components. +pub fn debug_detect_updates_received_on_local_entities( + query: Query, With)>, +) { + for entity in &query { + warn!("Entity {:?} has both Local and UpdatesReceived", entity); + } +} diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index 6b451dd6..9c16caef 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -6,10 +6,9 @@ pub mod collision; use azalea_block::{Block, BlockState}; use azalea_core::{BlockPos, Vec3}; -use azalea_entity::update_bounding_box; use azalea_entity::{ - clamp_look_direction, metadata::Sprinting, move_relative, Attributes, Jumping, Local, - LookDirection, Physics, Position, + metadata::Sprinting, move_relative, Attributes, Jumping, Local, LookDirection, Physics, + Position, }; use azalea_world::{Instance, InstanceContainer, InstanceName}; use bevy_app::{App, FixedUpdate, Plugin, Update}; @@ -34,8 +33,8 @@ impl Plugin for PhysicsPlugin { .add_systems( Update, force_jump_listener - .before(update_bounding_box) - .after(clamp_look_direction), + .before(azalea_entity::update_bounding_box) + .after(azalea_entity::clamp_look_direction), ) .add_systems(FixedUpdate, (ai_step, travel).chain().in_set(PhysicsSet)); } diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs index f1884265..895d8d2d 100644 --- a/azalea-world/src/container.rs +++ b/azalea-world/src/container.rs @@ -19,7 +19,7 @@ pub struct InstanceContainer { // cases where we'd want to get every entity in the world (just getting the entities in chunks // should work fine). - // Entities are garbage collected (by manual reference counting in EntityInfos) so we don't + // Entities are garbage collected (by manual reference counting in EntityUuidIndex) so we don't // need to worry about them here. // If it looks like we're relying on the server giving us unique world names, that's because we diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index c88b0929..8f4251bc 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -53,7 +53,7 @@ pub struct PartialEntityInfos { // note: using MinecraftEntityId for entity ids is acceptable here since // there's no chance of collisions here /// The entity id of the player that owns this partial world. This will - /// make `RelativeEntityUpdate` pretend the entity doesn't exist so + /// make `RelativeEntityUpdate` pretend this entity doesn't exist so /// it doesn't get modified from outside sources. pub owner_entity: Option, /// A counter for each entity that tracks how many updates we've observed diff --git a/azalea/src/pathfinder/astar.rs b/azalea/src/pathfinder/astar.rs index 65caf337..0bdd0f17 100644 --- a/azalea/src/pathfinder/astar.rs +++ b/azalea/src/pathfinder/astar.rs @@ -106,6 +106,6 @@ impl Ord for Weight { impl Eq for Weight {} impl PartialOrd for Weight { fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) + Some(self.cmp(other)) } }