diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2023-08-24 22:59:40 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-08-24 22:59:40 -0500 |
| commit | 11d14c74c53c07231c8ca33b622380df99bf9a59 (patch) | |
| tree | ea1d0c61a6d5f8af550a708ff3b71fbbaed5d122 | |
| parent | 57e5a0f0b96a38674bd18ac38d2d07e4f4ca2fd6 (diff) | |
| download | azalea-drasl-11d14c74c53c07231c8ca33b622380df99bf9a59.tar.xz | |
Support properly switching instances (#106)
* start implementing switching dimensions
* fix removeentity in shared worlds
* also store entity ids per local player
* uncomment a trace in pathfinder
* cleanup
---------
Co-authored-by: mat <git@matdoes.dev>
| -rw-r--r-- | azalea-client/src/client.rs | 13 | ||||
| -rw-r--r-- | azalea-client/src/entity_query.rs | 10 | ||||
| -rw-r--r-- | azalea-client/src/lib.rs | 1 | ||||
| -rw-r--r-- | azalea-client/src/local_player.rs | 9 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling.rs | 218 | ||||
| -rw-r--r-- | azalea-client/src/received_registries.rs | 7 | ||||
| -rw-r--r-- | azalea-entity/src/plugin/indexing.rs | 58 | ||||
| -rwxr-xr-x | azalea-protocol/src/packets/game/clientbound_respawn_packet.rs | 4 | ||||
| -rwxr-xr-x | azalea-world/src/chunk_storage.rs | 6 | ||||
| -rw-r--r-- | azalea-world/src/container.rs | 3 | ||||
| -rw-r--r-- | azalea-world/src/world.rs | 8 | ||||
| -rw-r--r-- | azalea/examples/testbot.rs | 313 | ||||
| -rw-r--r-- | azalea/src/pathfinder/mod.rs | 22 |
13 files changed, 411 insertions, 261 deletions
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index e2ca6c4e..39b86b98 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -21,7 +21,10 @@ use crate::{ use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError}; use azalea_chat::FormattedText; use azalea_core::Vec3; -use azalea_entity::{metadata::Health, EntityPlugin, EntityUpdateSet, EyeHeight, Local, Position}; +use azalea_entity::{ + indexing::EntityIdIndex, metadata::Health, EntityPlugin, EntityUpdateSet, EyeHeight, Local, + Position, +}; use azalea_physics::{PhysicsPlugin, PhysicsSet}; use azalea_protocol::{ connect::{Connection, ConnectionError}, @@ -306,8 +309,13 @@ impl Client { last_sent_direction: LastSentLookDirection::default(), abilities: PlayerAbilities::default(), permission_level: PermissionLevel::default(), + hunger: Hunger::default(), + + entity_id_index: EntityIdIndex::default(), + mining: mining::MineBundle::default(), attack: attack::AttackBundle::default(), + _local: Local, }); @@ -591,6 +599,9 @@ pub struct JoinedClientBundle { pub last_sent_direction: LastSentLookDirection, pub abilities: PlayerAbilities, pub permission_level: PermissionLevel, + pub hunger: Hunger, + + pub entity_id_index: EntityIdIndex, pub mining: mining::MineBundle, pub attack: attack::AttackBundle, diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs index d3fa522b..ca41c872 100644 --- a/azalea-client/src/entity_query.rs +++ b/azalea-client/src/entity_query.rs @@ -71,6 +71,16 @@ impl Client { .expect("Entity components must be present in Client::entity)components."); components.clone() } + + /// Get a component from an entity, if it exists. This is similar to + /// [`Self::entity_component`] but returns an `Option` instead of panicking + /// if the component isn't present. + pub fn get_entity_component<Q: Component + Clone>(&mut self, entity: Entity) -> Option<Q> { + let mut ecs = self.ecs.lock(); + let mut q = ecs.query::<&Q>(); + let components = q.get(&ecs, entity).ok(); + components.cloned() + } } pub trait EntityPredicate<Q: ReadOnlyWorldQuery, Filter: ReadOnlyWorldQuery> { diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 92a44f6c..0321a396 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -26,6 +26,7 @@ mod movement; pub mod packet_handling; pub mod ping; mod player; +pub mod received_registries; pub mod respawn; pub mod task_pool; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 27ac28dc..a66b7ad5 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -99,6 +99,15 @@ pub struct Hunger { pub saturation: f32, } +impl Default for Hunger { + fn default() -> Self { + Hunger { + food: 20, + saturation: 5., + } + } +} + impl LocalPlayer { /// Create a new `LocalPlayer`. pub fn new( diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index 5767d4d2..0b6e23b1 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -3,7 +3,7 @@ use std::{collections::HashSet, io::Cursor, sync::Arc}; use azalea_buf::McBufWritable; use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3}; use azalea_entity::{ - indexing::EntityUuidIndex, + indexing::{EntityIdIndex, EntityUuidIndex}, metadata::{apply_metadata, Health, PlayerMetadataBundle}, Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection, Physics, PlayerBundle, Position, RelativeEntityUpdate, @@ -47,6 +47,7 @@ use crate::{ SetContainerContentEvent, }, local_player::{GameProfileComponent, Hunger, LocalGameMode, LocalPlayer}, + received_registries::ReceivedRegistries, ClientInformation, PlayerInfo, }; @@ -208,22 +209,17 @@ pub fn process_packet_events(ecs: &mut World) { #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, - Query<( - &mut LocalPlayer, - Option<&mut InstanceName>, - &GameProfileComponent, - &ClientInformation, - )>, + Query<(&mut LocalPlayer, &GameProfileComponent, &ClientInformation)>, ResMut<InstanceContainer>, )> = SystemState::new(ecs); let (mut commands, mut query, mut instance_container) = system_state.get_mut(ecs); - let (mut local_player, world_name, game_profile, client_information) = + let (mut local_player, game_profile, client_information) = query.get_mut(player_entity).unwrap(); { - let dimension = &p - .registry_holder - .root + let received_registries = ReceivedRegistries(p.registry_holder.root); + + let dimension = &received_registries .dimension_type .value .iter() @@ -235,13 +231,6 @@ pub fn process_packet_events(ecs: &mut World) { let new_world_name = p.dimension.clone(); - if let Some(mut world_name) = world_name { - *world_name = world_name.clone(); - } else { - commands - .entity(player_entity) - .insert(InstanceName(new_world_name.clone())); - } // add this world to the instance_container (or don't if it's already // there) let weak_world = instance_container.insert( @@ -257,8 +246,7 @@ pub fn process_packet_events(ecs: &mut World) { azalea_world::calculate_chunk_storage_range( client_information.view_distance.into(), ), - // this argument makes it so other clients don't update this - // player entity + // this argument makes it so other clients don't update this player entity // in a shared world Some(player_entity), ); @@ -281,10 +269,7 @@ pub fn process_packet_events(ecs: &mut World) { previous: p.previous_game_type.into(), }, // this gets overwritten later by the SetHealth packet - Hunger { - food: 20, - saturation: 5., - }, + received_registries, player_bundle, )); } @@ -588,21 +573,22 @@ pub fn process_packet_events(ecs: &mut World) { #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, - Query<Option<&InstanceName>>, + Query<(&mut EntityIdIndex, Option<&InstanceName>)>, Res<InstanceContainer>, ResMut<EntityUuidIndex>, )> = SystemState::new(ecs); 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(); + let (mut entity_id_index, instance_name) = query.get_mut(player_entity).unwrap(); if let Some(instance_name) = instance_name { let bundle = p.as_entity_bundle((**instance_name).clone()); - let mut entity_commands = commands.spawn(( + let mut spawned = commands.spawn(( MinecraftEntityId(p.id), LoadedBy(HashSet::from([player_entity])), bundle, )); + entity_id_index.insert(MinecraftEntityId(p.id), spawned.id()); { // add it to the indexes immediately so if there's a packet that references @@ -611,13 +597,13 @@ pub fn process_packet_events(ecs: &mut World) { instance .write() .entity_by_id - .insert(MinecraftEntityId(p.id), entity_commands.id()); - entity_uuid_index.insert(p.uuid, entity_commands.id()); + .insert(MinecraftEntityId(p.id), spawned.id()); + entity_uuid_index.insert(p.uuid, spawned.id()); } // the bundle doesn't include the default entity metadata so we add that // separately - p.apply_metadata(&mut entity_commands); + p.apply_metadata(&mut spawned); } else { warn!("got add player packet but we haven't gotten a login packet yet"); } @@ -629,28 +615,26 @@ pub fn process_packet_events(ecs: &mut World) { let mut system_state: SystemState<( Commands, - Query<&mut LocalPlayer>, + Query<&EntityIdIndex>, Query<&EntityKind>, )> = SystemState::new(ecs); let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); + let entity_id_index = query.get_mut(player_entity).unwrap(); - let world = local_player.world.read(); - let entity = world.entity_by_id(&MinecraftEntityId(p.id)); - drop(world); + let entity = entity_id_index.get(&MinecraftEntityId(p.id)); - if let Some(entity) = entity { - let entity_kind = entity_kind_query.get(entity).unwrap(); - let mut entity_commands = commands.entity(entity); - if let Err(e) = apply_metadata( - &mut entity_commands, - **entity_kind, - (*p.packed_items).clone(), - ) { - warn!("{e}"); - } - } else { + let Some(entity) = entity else { warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id); + continue; + }; + let entity_kind = entity_kind_query.get(entity).unwrap(); + let mut entity_commands = commands.entity(entity); + if let Err(e) = apply_metadata( + &mut entity_commands, + **entity_kind, + (*p.packed_items).clone(), + ) { + warn!("{e}"); } system_state.apply(ecs); @@ -670,10 +654,11 @@ pub fn process_packet_events(ecs: &mut World) { #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, - Query<(&TabList, Option<&InstanceName>)>, + Query<(&mut EntityIdIndex, &TabList, Option<&InstanceName>)>, )> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); - let (tab_list, world_name) = query.get_mut(player_entity).unwrap(); + let (mut entity_id_index, tab_list, world_name) = + query.get_mut(player_entity).unwrap(); if let Some(InstanceName(world_name)) = world_name { let bundle = p.as_player_bundle(world_name.clone()); @@ -682,6 +667,7 @@ pub fn process_packet_events(ecs: &mut World) { LoadedBy(HashSet::from([player_entity])), bundle, )); + entity_id_index.insert(MinecraftEntityId(p.id), spawned.id()); if let Some(player_info) = tab_list.get(&p.uuid) { spawned.insert(GameProfileComponent(player_info.profile.clone())); @@ -720,14 +706,14 @@ pub fn process_packet_events(ecs: &mut World) { debug!("Got set experience packet {:?}", p); } ClientboundGamePacket::TeleportEntity(p) => { - let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = - SystemState::new(ecs); + let mut system_state: SystemState<( + Commands, + Query<(&EntityIdIndex, &LocalPlayer)>, + )> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); + let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - let world = local_player.world.read(); - let entity = world.entity_by_id(&MinecraftEntityId(p.id)); - drop(world); + let entity = entity_id_index.get(&MinecraftEntityId(p.id)); if let Some(entity) = entity { let new_position = p.position; @@ -751,14 +737,14 @@ pub fn process_packet_events(ecs: &mut World) { // debug!("Got rotate head packet {:?}", p); } ClientboundGamePacket::MoveEntityPos(p) => { - let mut system_state: SystemState<(Commands, Query<&LocalPlayer>)> = - SystemState::new(ecs); + let mut system_state: SystemState<( + Commands, + Query<(&EntityIdIndex, &LocalPlayer)>, + )> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); + let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - let world = local_player.world.read(); - let entity = world.entity_by_id(&MinecraftEntityId(p.entity_id)); - drop(world); + let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id)); if let Some(entity) = entity { let delta = p.delta.clone(); @@ -779,14 +765,14 @@ pub fn process_packet_events(ecs: &mut World) { system_state.apply(ecs); } ClientboundGamePacket::MoveEntityPosRot(p) => { - let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = - SystemState::new(ecs); + let mut system_state: SystemState<( + Commands, + Query<(&EntityIdIndex, &LocalPlayer)>, + )> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); + let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - let world = local_player.world.read(); - let entity = world.entity_by_id(&MinecraftEntityId(p.entity_id)); - drop(world); + let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id)); if let Some(entity) = entity { let delta = p.delta.clone(); @@ -832,29 +818,29 @@ pub fn process_packet_events(ecs: &mut World) { debug!("Got remove entities packet {:?}", p); let mut system_state: SystemState<( - Commands, - Query<&mut InstanceName>, - Res<InstanceContainer>, + Query<&mut EntityIdIndex>, + Query<&mut LoadedBy>, )> = SystemState::new(ecs); - let (mut commands, mut query, instance_container) = system_state.get_mut(ecs); - let Ok(instance_name) = query.get_mut(player_entity) else { + let (mut query, mut entity_query) = system_state.get_mut(ecs); + let Ok(mut entity_id_index) = query.get_mut(player_entity) else { + warn!("our local player doesn't have EntityIdIndex"); continue; }; - let Some(instance) = instance_container.get(&instance_name) else { - continue; - }; for &id in &p.entity_ids { - if let Some(entity) = - instance.write().entity_by_id.remove(&MinecraftEntityId(id)) - { - trace!("despawning entity"); - commands.entity(entity).despawn(); - } + let Some(entity) = entity_id_index.remove(&MinecraftEntityId(id)) else { + warn!("There is no entity with id {id:?}"); + continue; + }; + let Ok(mut loaded_by) = entity_query.get_mut(entity) else { + warn!( + "tried to despawn entity {id} but it doesn't have a LoadedBy component", + ); + continue; + }; + loaded_by.remove(&player_entity); } - - system_state.apply(ecs); } ClientboundGamePacket::PlayerChat(p) => { debug!("Got player chat packet {:?}", p); @@ -1114,8 +1100,72 @@ pub fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::Respawn(p) => { debug!("Got respawn packet {:?}", p); - let mut system_state: SystemState<Commands> = SystemState::new(ecs); - let mut commands = system_state.get(ecs); + #[allow(clippy::type_complexity)] + let mut system_state: SystemState<( + Commands, + Query<( + &mut LocalPlayer, + &GameProfileComponent, + &ClientInformation, + &ReceivedRegistries, + )>, + ResMut<InstanceContainer>, + )> = SystemState::new(ecs); + let (mut commands, mut query, mut instance_container) = system_state.get_mut(ecs); + let (mut local_player, game_profile, client_information, received_registries) = + query.get_mut(player_entity).unwrap(); + + { + let dimension = &received_registries + .dimension_type + .value + .iter() + .find(|t| t.name == p.dimension_type) + .unwrap_or_else(|| { + panic!("No dimension_type with name {}", p.dimension_type) + }) + .element; + + let new_world_name = p.dimension.clone(); + + // add this world to the instance_container (or don't if it's already + // there) + let weak_world = instance_container.insert( + new_world_name.clone(), + dimension.height, + dimension.min_y, + ); + + // set the partial_world to an empty world + // (when we add chunks or entities those will be in the + // instance_container) + *local_player.partial_instance.write() = PartialInstance::new( + azalea_world::calculate_chunk_storage_range( + client_information.view_distance.into(), + ), + Some(player_entity), + ); + local_player.world = weak_world; + + // this resets a bunch of our components like physics and stuff + let player_bundle = PlayerBundle { + entity: EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_world_name, + ), + metadata: PlayerMetadataBundle::default(), + }; + // update the local gamemode and metadata things + commands.entity(player_entity).insert(( + LocalGameMode { + current: p.game_type, + previous: p.previous_game_type.into(), + }, + player_bundle, + )); + } // Remove the Dead marker component from the player. commands.entity(player_entity).remove::<Dead>(); diff --git a/azalea-client/src/received_registries.rs b/azalea-client/src/received_registries.rs new file mode 100644 index 00000000..845527ae --- /dev/null +++ b/azalea-client/src/received_registries.rs @@ -0,0 +1,7 @@ +use azalea_protocol::packets::game::clientbound_login_packet::registry::RegistryRoot; +use bevy_ecs::component::Component; +use derive_more::Deref; + +/// The registries that the server sent us on login. +#[derive(Clone, Debug, Component, Deref)] +pub struct ReceivedRegistries(pub RegistryRoot); diff --git a/azalea-entity/src/plugin/indexing.rs b/azalea-entity/src/plugin/indexing.rs index f7dfe0fa..0f6232fa 100644 --- a/azalea-entity/src/plugin/indexing.rs +++ b/azalea-entity/src/plugin/indexing.rs @@ -3,11 +3,13 @@ use azalea_core::ChunkPos; use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId}; use bevy_ecs::{ + component::Component, entity::Entity, query::{Changed, With, Without}, system::{Commands, Query, Res, ResMut, Resource}, }; use log::{debug, error, info, warn}; +use nohash_hasher::IntMap; use std::{collections::HashMap, fmt::Debug}; use uuid::Uuid; @@ -21,6 +23,15 @@ pub struct EntityUuidIndex { entity_by_uuid: HashMap<Uuid, Entity>, } +/// An index of Minecraft entity IDs to Azalea ECS entities. This is a +/// `Component` so local players can keep track of entity IDs independently from +/// the instance. +#[derive(Component, Default)] +pub struct EntityIdIndex { + /// An index of entities by their MinecraftEntityId + entity_by_id: IntMap<MinecraftEntityId, Entity>, +} + impl EntityUuidIndex { pub fn new() -> Self { Self { @@ -41,6 +52,24 @@ impl EntityUuidIndex { } } +impl EntityIdIndex { + pub fn get(&self, id: &MinecraftEntityId) -> Option<Entity> { + self.entity_by_id.get(id).copied() + } + + pub fn contains_key(&self, id: &MinecraftEntityId) -> bool { + self.entity_by_id.contains_key(id) + } + + pub fn insert(&mut self, id: MinecraftEntityId, entity: Entity) { + self.entity_by_id.insert(id, entity); + } + + pub fn remove(&mut self, id: &MinecraftEntityId) -> Option<Entity> { + self.entity_by_id.remove(id) + } +} + impl Debug for EntityUuidIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EntityUuidIndex").finish() @@ -140,8 +169,7 @@ pub fn update_uuid_index( if local.is_none() { if let Some(old_entity) = entity_infos.entity_by_uuid.get(&uuid) { debug!( - "Entity with UUID {uuid:?} already existed in the world, not adding to - index (old ecs id: {old_entity:?} / new ecs id: {entity:?})" + "Entity with UUID {uuid:?} already existed in the world, not adding to index (old ecs id: {old_entity:?} / new ecs id: {entity:?})" ); continue; } @@ -164,8 +192,7 @@ pub fn update_entity_by_id_index( if local.is_none() { if let Some(old_entity) = world.entity_by_id.get(id) { debug!( - "Entity with ID {id:?} already existed in the world, not adding to - index (old ecs id: {old_entity:?} / new ecs id: {entity:?})" + "Entity with ID {id:?} already existed in the world, not adding to index (old ecs id: {old_entity:?} / new ecs id: {entity:?})" ); continue; } @@ -209,8 +236,23 @@ pub fn remove_despawned_entities_from_indexes( 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(); + let Some(instance_lock) = instance_container.get(world_name) else { + // the instance isn't even loaded by us, so we can safely delete the entity + debug!( + "Despawned entity {entity:?} because it's in an instance that isn't loaded anymore" + ); + 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(); + + continue; + }; + + let mut instance = instance_lock.write(); // if the entity has no references left, despawn it if !loaded_by.is_empty() { @@ -219,11 +261,11 @@ pub fn remove_despawned_entities_from_indexes( // 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 let Some(entities_in_chunk) = instance.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); + instance.entities_by_chunk.remove(&chunk); } } else { warn!("Tried to remove entity from chunk {chunk:?} but the entity was not there."); diff --git a/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs b/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs index 3518aa08..71ccfd18 100755 --- a/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs @@ -7,8 +7,8 @@ pub struct ClientboundRespawnPacket { pub dimension_type: ResourceLocation, pub dimension: ResourceLocation, pub seed: u64, - pub player_game_type: GameMode, - pub previous_player_game_type: OptionalGameType, + pub game_type: GameMode, + pub previous_game_type: OptionalGameType, pub is_debug: bool, pub is_flat: bool, pub data_to_keep: u8, diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index f0f053fa..133d522b 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -123,11 +123,7 @@ impl PartialChunkStorage { ) -> Result<(), BufReadError> { debug!("Replacing chunk at {:?}", pos); if !self.in_range(pos) { - trace!( - "Ignoring chunk since it's not in the view range: {}, {}", - pos.x, - pos.z - ); + warn!("Ignoring chunk since it's not in the view range: {pos:?}"); return Ok(()); } diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs index 895d8d2d..866ac157 100644 --- a/azalea-world/src/container.rs +++ b/azalea-world/src/container.rs @@ -37,7 +37,8 @@ impl InstanceContainer { } } - /// Get a world from the container. + /// Get a world from the container. Returns `None` if none of the clients + /// are in this world. pub fn get(&self, name: &InstanceName) -> Option<Arc<RwLock<Instance>>> { self.worlds.get(name).and_then(|world| world.upgrade()) } diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index ecd2e653..df462be4 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -82,16 +82,12 @@ pub struct Instance { /// An index of all the entities we know are in the chunks of the world pub entities_by_chunk: HashMap<ChunkPos, HashSet<Entity>>, - /// An index of Minecraft entity IDs to Azalea ECS entities. + /// An index of Minecraft entity IDs to Azalea ECS entities. You should + /// avoid using this and instead use `azalea_entity::EntityIdIndex` pub entity_by_id: IntMap<MinecraftEntityId, Entity>, } impl Instance { - /// Get an ECS [`Entity`] from a Minecraft entity ID. - pub fn entity_by_id(&self, entity_id: &MinecraftEntityId) -> Option<Entity> { - self.entity_by_id.get(entity_id).copied() - } - pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> { self.chunks.get_block_state(pos) } diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs index e2f1c436..3d566410 100644 --- a/azalea/examples/testbot.rs +++ b/azalea/examples/testbot.rs @@ -105,174 +105,183 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< }, ); println!("sender entity: {entity:?}"); - if let Some(entity) = entity { - match m.content().as_str() { - "whereami" => { - let pos = bot.entity_component::<Position>(entity); - bot.chat(&format!("You're at {pos:?}",)); - } - "whereareyou" => { - let pos = bot.position(); - bot.chat(&format!("I'm at {pos:?}",)); - } - "goto" => { - let entity_pos = bot.entity_component::<Position>(entity); - let target_pos: BlockPos = entity_pos.into(); - println!("going to {target_pos:?}"); - bot.goto(BlockPosGoal::from(target_pos)); - } - "look" => { - let entity_pos = bot - .entity_component::<Position>(entity) - .up(bot.entity_component::<EyeHeight>(entity).into()); - println!("entity_pos: {entity_pos:?}"); - bot.look_at(entity_pos); - } - "jump" => { - bot.set_jumping(true); - } - "walk" => { - bot.walk(WalkDirection::Forward); - } - "stop" => { - bot.set_jumping(false); - bot.walk(WalkDirection::None); - } - "lag" => { - std::thread::sleep(Duration::from_millis(1000)); - } - "inventory" => { - println!("inventory: {:?}", bot.menu()); - } - "findblock" => { - let target_pos = bot - .world() - .read() - .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); - bot.chat(&format!("target_pos: {target_pos:?}",)); - } - "gotoblock" => { - let target_pos = bot - .world() - .read() - .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); - if let Some(target_pos) = target_pos { - // +1 to stand on top of the block - bot.goto(BlockPosGoal::from(target_pos.up(1))); - } else { - bot.chat("no diamond block found"); - } - } - "mineblock" => { - let target_pos = bot - .world() - .read() - .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); - if let Some(target_pos) = target_pos { - // +1 to stand on top of the block - bot.chat("ok mining diamond block"); - bot.look_at(target_pos.center()); - bot.mine(target_pos).await; - bot.chat("finished mining"); - } else { - bot.chat("no diamond block found"); - } + match m.content().as_str() { + "whereami" => { + let Some(entity) = entity else { + bot.chat("I can't see you"); + return Ok(()); + }; + let pos = bot.entity_component::<Position>(entity); + bot.chat(&format!("You're at {pos:?}")); + } + "whereareyou" => { + let pos = bot.position(); + bot.chat(&format!("I'm at {pos:?}")); + } + "goto" => { + let Some(entity) = entity else { + bot.chat("I can't see you"); + return Ok(()); + }; + let entity_pos = bot.entity_component::<Position>(entity); + let target_pos: BlockPos = entity_pos.into(); + println!("going to {target_pos:?}"); + bot.goto(BlockPosGoal::from(target_pos)); + } + "look" => { + let Some(entity) = entity else { + bot.chat("I can't see you"); + return Ok(()); + }; + let entity_pos = bot + .entity_component::<Position>(entity) + .up(bot.entity_component::<EyeHeight>(entity).into()); + println!("entity_pos: {entity_pos:?}"); + bot.look_at(entity_pos); + } + "jump" => { + bot.set_jumping(true); + } + "walk" => { + bot.walk(WalkDirection::Forward); + } + "stop" => { + bot.set_jumping(false); + bot.walk(WalkDirection::None); + } + "lag" => { + std::thread::sleep(Duration::from_millis(1000)); + } + "inventory" => { + println!("inventory: {:?}", bot.menu()); + } + "findblock" => { + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); + bot.chat(&format!("target_pos: {target_pos:?}",)); + } + "gotoblock" => { + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); + if let Some(target_pos) = target_pos { + // +1 to stand on top of the block + bot.goto(BlockPosGoal::from(target_pos.up(1))); + } else { + bot.chat("no diamond block found"); } - "lever" => { - let target_pos = bot - .world() - .read() - .find_block(bot.position(), &azalea::Block::Lever.into()); - let Some(target_pos) = target_pos else { - bot.chat("no lever found"); - return Ok(()); - }; - bot.goto(BlockPosGoal::from(target_pos)); + } + "mineblock" => { + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); + if let Some(target_pos) = target_pos { + // +1 to stand on top of the block + bot.chat("ok mining diamond block"); bot.look_at(target_pos.center()); - bot.block_interact(target_pos); - } - "hitresult" => { - let hit_result = bot.get_component::<HitResultComponent>(); - bot.chat(&format!("hit_result: {hit_result:?}",)); + bot.mine(target_pos).await; + bot.chat("finished mining"); + } else { + bot.chat("no diamond block found"); } - "chest" => { - let target_pos = bot - .world() - .read() - .find_block(bot.position(), &azalea::Block::Chest.into()); - let Some(target_pos) = target_pos else { - bot.chat("no chest found"); - return Ok(()); - }; - bot.look_at(target_pos.center()); - let container = bot.open_container(target_pos).await; - println!("container: {:?}", container); - if let Some(container) = container { - if let Some(contents) = container.contents() { - for item in contents { - if let ItemSlot::Present(item) = item { - println!("item: {:?}", item); - } + } + "lever" => { + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::Lever.into()); + let Some(target_pos) = target_pos else { + bot.chat("no lever found"); + return Ok(()); + }; + bot.goto(BlockPosGoal::from(target_pos)); + bot.look_at(target_pos.center()); + bot.block_interact(target_pos); + } + "hitresult" => { + let hit_result = bot.get_component::<HitResultComponent>(); + bot.chat(&format!("hit_result: {hit_result:?}",)); + } + "chest" => { + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::Chest.into()); + let Some(target_pos) = target_pos else { + bot.chat("no chest found"); + return Ok(()); + }; + bot.look_at(target_pos.center()); + let container = bot.open_container(target_pos).await; + println!("container: {:?}", container); + if let Some(container) = container { + if let Some(contents) = container.contents() { + for item in contents { + if let ItemSlot::Present(item) = item { + println!("item: {:?}", item); } - } else { - println!("container was immediately closed"); } } else { - println!("no container found"); + println!("container was immediately closed"); } + } else { + println!("no container found"); } - "attack" => { - let mut nearest_entity = None; - let mut nearest_distance = f64::INFINITY; - let mut nearest_pos = Vec3::default(); - let bot_position = bot.position(); - let bot_entity = bot.entity; - let bot_instance_name = bot.component::<InstanceName>(); + } + "attack" => { + let mut nearest_entity = None; + let mut nearest_distance = f64::INFINITY; + let mut nearest_pos = Vec3::default(); + let bot_position = bot.position(); + let bot_entity = bot.entity; + let bot_instance_name = bot.component::<InstanceName>(); + { + let mut ecs = bot.ecs.lock(); + let mut query = ecs.query_filtered::<( + azalea::ecs::entity::Entity, + &MinecraftEntityId, + &Position, + &InstanceName, + &EyeHeight, + ), With<MinecraftEntityId>>(); + for (entity, &entity_id, position, instance_name, eye_height) in + query.iter(&ecs) { - let mut ecs = bot.ecs.lock(); - let mut query = ecs.query_filtered::<( - azalea::ecs::entity::Entity, - &MinecraftEntityId, - &Position, - &InstanceName, - &EyeHeight, - ), With<MinecraftEntityId>>( - ); - for (entity, &entity_id, position, instance_name, eye_height) in - query.iter(&ecs) - { - if entity == bot_entity { - continue; - } - if instance_name != &bot_instance_name { - continue; - } + if entity == bot_entity { + continue; + } + if instance_name != &bot_instance_name { + continue; + } - let distance = bot_position.distance_to(position); - if distance < 4.0 && distance < nearest_distance { - nearest_entity = Some(entity_id); - nearest_distance = distance; - nearest_pos = position.up(**eye_height as f64); - } + let distance = bot_position.distance_to(position); + if distance < 4.0 && distance < nearest_distance { + nearest_entity = Some(entity_id); + nearest_distance = distance; + nearest_pos = position.up(**eye_height as f64); } } - if let Some(nearest_entity) = nearest_entity { - bot.look_at(nearest_pos); - bot.attack(nearest_entity); - bot.chat("attacking"); - let mut ticks = bot.get_tick_broadcaster(); - while ticks.recv().await.is_ok() { - if !bot.has_attack_cooldown() { - break; - } + } + if let Some(nearest_entity) = nearest_entity { + bot.look_at(nearest_pos); + bot.attack(nearest_entity); + bot.chat("attacking"); + let mut ticks = bot.get_tick_broadcaster(); + while ticks.recv().await.is_ok() { + if !bot.has_attack_cooldown() { + break; } - bot.chat("finished attacking"); - } else { - bot.chat("no entities found"); } + bot.chat("finished attacking"); + } else { + bot.chat("no entities found"); } - _ => {} } + _ => {} } } Event::Packet(packet) => match *packet { diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 9237aa81..58b59fd4 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -24,10 +24,11 @@ use azalea_physics::PhysicsSet; use azalea_world::{InstanceContainer, InstanceName}; use bevy_app::{FixedUpdate, Update}; use bevy_ecs::prelude::Event; +use bevy_ecs::query::Changed; use bevy_ecs::schedule::IntoSystemConfigs; use bevy_tasks::{AsyncComputeTaskPool, Task}; use futures_lite::future; -use log::{debug, error}; +use log::{debug, error, trace}; use std::collections::VecDeque; use std::sync::Arc; @@ -49,6 +50,7 @@ impl Plugin for PathfinderPlugin { goto_listener, add_default_pathfinder, (handle_tasks, path_found_listener).chain(), + stop_pathfinding_on_instance_change, ), ); } @@ -249,7 +251,7 @@ fn tick_execute_path( entity, position: center, }); - debug!( + trace!( "tick: pathfinder {entity:?}; going to {:?}; currently at {position:?}", target.pos ); @@ -280,6 +282,22 @@ fn tick_execute_path( } } +fn stop_pathfinding_on_instance_change( + mut query: Query<(Entity, &mut Pathfinder), Changed<InstanceName>>, + mut walk_events: EventWriter<StartWalkEvent>, +) { + for (entity, mut pathfinder) in &mut query { + if !pathfinder.path.is_empty() { + debug!("instance changed, clearing path"); + pathfinder.path.clear(); + walk_events.send(StartWalkEvent { + entity, + direction: WalkDirection::None, + }); + } + } +} + /// Information about our vertical velocity #[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)] pub enum VerticalVel { |
