aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2023-07-23 22:17:17 -0500
committermat <git@matdoes.dev>2023-07-23 22:17:17 -0500
commitd99ba0da5500a1a4917bf9c8c9e93e2caa4ada99 (patch)
tree08af266f9ccc9a6dea7593198bfe50dab2a307eb
parent2ab16402de3ac12c04696833450cd8d2f7b39b85 (diff)
downloadazalea-drasl-d99ba0da5500a1a4917bf9c8c9e93e2caa4ada99.tar.xz
clean up azalea-entity a little
-rw-r--r--azalea-client/src/packet_handling.rs13
-rwxr-xr-xazalea-client/src/player.rs6
-rwxr-xr-xazalea-entity/src/dimensions.rs15
-rw-r--r--azalea-entity/src/info.rs306
-rw-r--r--azalea-entity/src/lib.rs36
-rw-r--r--azalea-entity/src/plugin/indexing.rs (renamed from azalea-entity/src/systems.rs)150
-rw-r--r--azalea-entity/src/plugin/mod.rs147
-rw-r--r--azalea-entity/src/plugin/relative_updates.rs122
-rw-r--r--azalea-physics/src/lib.rs9
-rw-r--r--azalea-world/src/container.rs2
-rw-r--r--azalea-world/src/world.rs2
-rw-r--r--azalea/src/pathfinder/astar.rs2
12 files changed, 397 insertions, 413 deletions
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<Option<&InstanceName>>,
Res<InstanceContainer>,
- ResMut<EntityInfos>,
+ ResMut<EntityUuidIndex>,
)> = 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<AddPlayerEvent>,
- entity_infos: Res<EntityInfos>,
+ entity_uuid_index: Res<EntityUuidIndex>,
) {
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<Position>>) {
- 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::<EntityInfos>();
- }
-}
-
-fn debug_new_entity(query: Query<(Entity, Option<&Local>), Added<MinecraftEntityId>>) {
- 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<RwLock<PartialInstance>>,
- // a function that takes the entity and updates it
- pub update: Box<dyn FnOnce(&mut EntityMut) + Send + Sync>,
-}
-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::<MinecraftEntityId>().unwrap();
- let Some(updates_received) = entity_mut.get_mut::<UpdatesReceived>() 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::<UpdatesReceived>().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<Uuid, Entity>,
-}
-
-impl EntityInfos {
- pub fn new() -> Self {
- Self {
- entity_by_uuid: HashMap::default(),
- }
- }
-
- pub fn get_entity_by_uuid(&self, uuid: &Uuid) -> Option<Entity> {
- 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<Position>>,
- instance_container: Res<InstanceContainer>,
-) {
- 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<Entity>);
-
-/// 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<MinecraftEntityId>,
- (Without<UpdatesReceived>, Without<Local>),
- ),
- >,
-) {
- 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<Entity, (With<Local>, With<UpdatesReceived>)>,
-) {
- 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<EntityInfos>,
- instance_container: Res<InstanceContainer>,
- query: Query<(Entity, &EntityUuid, &Position, &InstanceName, &LoadedBy), Changed<LoadedBy>>,
-) {
- 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<Health>>) {
- 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
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};
+//! Stuff related to entity indexes and keeping track of entities in the world.
+
+use azalea_core::ChunkPos;
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
-use bevy_ecs::prelude::*;
-use log::{debug, error, info};
+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<Uuid, Entity>,
+}
-use crate::{EntityInfos, EntityUuid, EyeHeight, FluidOnEyes, LoadedBy, Local, Position};
+impl EntityUuidIndex {
+ pub fn new() -> Self {
+ Self {
+ entity_by_uuid: HashMap::default(),
+ }
+ }
+
+ pub fn get(&self, uuid: &Uuid) -> Option<Entity> {
+ 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<EntityInfos>,
+ mut entity_infos: ResMut<EntityUuidIndex>,
query: Query<(Entity, &EntityUuid, Option<&Local>), Changed<EntityUuid>>,
) {
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<Position>>,
+ instance_container: Res<InstanceContainer>,
+) {
+ 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);
+ }
+ }
+}
+
+/// Despawn entities that aren't being loaded by anything.
+pub fn remove_despawned_entities_from_indexes(
+ mut commands: Commands,
+ mut entity_infos: ResMut<EntityUuidIndex>,
instance_container: Res<InstanceContainer>,
+ query: Query<(Entity, &EntityUuid, &Position, &InstanceName, &LoadedBy), Changed<LoadedBy>>,
) {
- for (mut fluid_on_eyes, position, eye_height, instance_name) in query.iter_mut() {
- let Some(instance) = instance_container.get(instance_name) else {
+ 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;
- };
-
- 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;
+ }
+
+ // 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 {
- **fluid_on_eyes = azalea_registry::Fluid::Empty;
+ 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::<EntityUuidIndex>();
+ }
+}
+
+fn debug_new_entity(query: Query<(Entity, Option<&Local>), Added<MinecraftEntityId>>) {
+ 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<Health>>) {
+ 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<InstanceContainer>,
+) {
+ 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<Entity>);
+
+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<Position>>) {
+ 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<RwLock<PartialInstance>>,
+ // a function that takes the entity and updates it
+ pub update: Box<dyn FnOnce(&mut EntityMut) + Send + Sync>,
+}
+
+/// 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::<MinecraftEntityId>().unwrap();
+ let Some(updates_received) = entity_mut.get_mut::<UpdatesReceived>() 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::<UpdatesReceived>().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<MinecraftEntityId>,
+ (Without<UpdatesReceived>, Without<Local>),
+ ),
+ >,
+) {
+ 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<Entity, (With<Local>, With<UpdatesReceived>)>,
+) {
+ 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<Entity>,
/// 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<W: PartialOrd + Debug> Ord for Weight<W> {
impl<W: PartialOrd + Debug> Eq for Weight<W> {}
impl<W: PartialOrd + Debug> PartialOrd for Weight<W> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- self.0.partial_cmp(&other.0)
+ Some(self.cmp(other))
}
}