azalea_entity/plugin/
mod.rs

1pub mod indexing;
2mod relative_updates;
3
4use std::collections::HashSet;
5
6use azalea_block::{BlockState, BlockTrait, fluid_state::FluidKind, properties};
7use azalea_core::{
8    position::{BlockPos, ChunkPos},
9    tick::GameTick,
10};
11use azalea_registry::{builtin::BlockKind, tags};
12use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
13use bevy_app::{App, Plugin, PostUpdate, Update};
14use bevy_ecs::prelude::*;
15use derive_more::{Deref, DerefMut};
16use indexing::EntityUuidIndex;
17pub use relative_updates::RelativeEntityUpdate;
18use tracing::debug;
19
20use crate::{
21    Crouching, Dead, EntityKindComponent, FluidOnEyes, LocalEntity, LookDirection, OnClimbable,
22    Physics, Pose, Position,
23    dimensions::{EntityDimensions, calculate_dimensions},
24    metadata::Health,
25};
26
27/// A Bevy [`SystemSet`] for various types of entity updates.
28#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
29pub enum EntityUpdateSystems {
30    /// Create search indexes for entities.
31    Index,
32    /// Remove despawned entities from search indexes.
33    Deindex,
34}
35
36/// Plugin handling some basic entity functionality.
37pub struct EntityPlugin;
38impl Plugin for EntityPlugin {
39    fn build(&self, app: &mut App) {
40        // entities get added pre-update
41        // added to indexes during update (done by this plugin)
42        // modified during update
43        // despawned post-update (done by this plugin)
44        app.add_systems(
45            PostUpdate,
46            indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSystems::Deindex),
47        )
48        .add_systems(
49            Update,
50            (
51                (
52                    indexing::update_entity_chunk_positions,
53                    indexing::insert_entity_chunk_position,
54                )
55                    .chain()
56                    .in_set(EntityUpdateSystems::Index),
57                (
58                    relative_updates::debug_detect_updates_received_on_local_entities,
59                    debug_new_entity,
60                    add_dead,
61                    clamp_look_direction,
62                    update_on_climbable,
63                    (update_dimensions, update_bounding_box).chain(),
64                    update_crouching,
65                ),
66            ),
67        )
68        .add_systems(GameTick, (update_in_loaded_chunk, update_fluid_on_eyes))
69        .init_resource::<EntityUuidIndex>();
70    }
71}
72
73fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
74    for (entity, local) in query.iter() {
75        if local.is_some() {
76            debug!("new local entity: {:?}", entity);
77        } else {
78            debug!("new entity: {:?}", entity);
79        }
80    }
81}
82
83/// System that adds the [`Dead`] marker component if an entity's health is set
84/// to 0 (or less than 0).
85///
86/// This will be present if an entity is doing the death animation.
87///
88/// Entities that are dead cannot be revived.
89pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
90    for (entity, health) in query.iter() {
91        if **health <= 0.0 {
92            commands.entity(entity).insert(Dead);
93        }
94    }
95}
96
97pub fn update_fluid_on_eyes(
98    mut query: Query<(
99        &mut FluidOnEyes,
100        &Position,
101        &EntityDimensions,
102        &InstanceName,
103    )>,
104    instance_container: Res<InstanceContainer>,
105) {
106    for (mut fluid_on_eyes, position, dimensions, instance_name) in query.iter_mut() {
107        let Some(instance) = instance_container.get(instance_name) else {
108            continue;
109        };
110
111        let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
112        let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
113        let fluid_at_eye = instance
114            .read()
115            .get_fluid_state(eye_block_pos)
116            .unwrap_or_default();
117        let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
118        if fluid_cutoff_y > adjusted_eye_y {
119            **fluid_on_eyes = fluid_at_eye.kind;
120        } else {
121            **fluid_on_eyes = FluidKind::Empty;
122        }
123    }
124}
125
126pub fn update_on_climbable(
127    mut query: Query<(&mut OnClimbable, &Position, &InstanceName), With<LocalEntity>>,
128    instance_container: Res<InstanceContainer>,
129) {
130    for (mut on_climbable, position, instance_name) in query.iter_mut() {
131        // TODO: there's currently no gamemode component that can be accessed from here,
132        // maybe LocalGameMode should be replaced with two components, maybe called
133        // EntityGameMode and PreviousGameMode?
134
135        // if game_mode == GameMode::Spectator {
136        //     continue;
137        // }
138
139        let Some(instance) = instance_container.get(instance_name) else {
140            continue;
141        };
142
143        let instance = instance.read();
144
145        let block_pos = BlockPos::from(position);
146        let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default();
147        let block_at_feet = Box::<dyn BlockTrait>::from(block_state_at_feet);
148        let registry_block_at_feet = block_at_feet.as_registry_block();
149
150        **on_climbable = tags::blocks::CLIMBABLE.contains(&registry_block_at_feet)
151            || (tags::blocks::TRAPDOORS.contains(&registry_block_at_feet)
152                && is_trapdoor_useable_as_ladder(block_state_at_feet, block_pos, &instance));
153    }
154}
155
156fn is_trapdoor_useable_as_ladder(
157    block_state: BlockState,
158    block_pos: BlockPos,
159    instance: &azalea_world::Instance,
160) -> bool {
161    // trapdoor must be open
162    if !block_state
163        .property::<properties::Open>()
164        .unwrap_or_default()
165    {
166        return false;
167    }
168
169    // block below must be a ladder
170    let block_below = instance
171        .get_block_state(block_pos.down(1))
172        .unwrap_or_default();
173    let registry_block_below = Box::<dyn BlockTrait>::from(block_below).as_registry_block();
174    if registry_block_below != BlockKind::Ladder {
175        return false;
176    }
177    // and the ladder must be facing the same direction as the trapdoor
178    let ladder_facing = block_below
179        .property::<properties::FacingCardinal>()
180        .expect("ladder block must have facing property");
181    let trapdoor_facing = block_state
182        .property::<properties::FacingCardinal>()
183        .expect("trapdoor block must have facing property");
184    if ladder_facing != trapdoor_facing {
185        return false;
186    }
187
188    true
189}
190
191/// A component that lists all the local player entities that have this entity
192/// loaded.
193///
194/// If this is empty, the entity will be removed from the ECS.
195#[derive(Clone, Component, Deref, DerefMut)]
196pub struct LoadedBy(pub HashSet<Entity>);
197
198pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
199    for mut look_direction in &mut query {
200        *look_direction = apply_clamp_look_direction(*look_direction);
201    }
202}
203pub fn apply_clamp_look_direction(mut look_direction: LookDirection) -> LookDirection {
204    look_direction.x_rot = look_direction.x_rot.clamp(-90., 90.);
205
206    look_direction
207}
208
209/// Sets the position of the entity.
210///
211/// This doesn't update the cache in azalea-world, and should only be used
212/// within azalea-world.
213///
214/// # Safety
215///
216/// Cached position in the world must be updated.
217#[allow(clippy::type_complexity)]
218pub fn update_bounding_box(
219    mut query: Query<
220        (&mut Physics, &Position, &EntityDimensions),
221        Or<(Changed<Position>, Changed<EntityDimensions>)>,
222    >,
223) {
224    for (mut physics, position, dimensions) in query.iter_mut() {
225        let bounding_box = dimensions.make_bounding_box(**position);
226        physics.bounding_box = bounding_box;
227    }
228}
229
230#[allow(clippy::type_complexity)]
231pub fn update_dimensions(
232    mut query: Query<
233        (&mut EntityDimensions, &EntityKindComponent, &Pose),
234        Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
235    >,
236) {
237    for (mut dimensions, kind, pose) in query.iter_mut() {
238        *dimensions = calculate_dimensions(**kind, *pose);
239    }
240}
241
242pub fn update_crouching(query: Query<(&mut Crouching, &Pose), Without<LocalEntity>>) {
243    for (mut crouching, pose) in query {
244        let new_crouching = *pose == Pose::Crouching;
245        // avoid triggering change detection
246        if **crouching != new_crouching {
247            **crouching = new_crouching;
248        }
249    }
250}
251
252/// Marks an entity that's in a loaded chunk. This is updated at the beginning
253/// of every tick.
254///
255/// Internally, this is only used for player physics. Not to be confused with
256/// the somewhat similarly named [`LoadedBy`].
257#[derive(Clone, Component, Copy, Debug)]
258pub struct InLoadedChunk;
259
260/// Update the [`InLoadedChunk`] component for all entities in the world.
261pub fn update_in_loaded_chunk(
262    mut commands: bevy_ecs::system::Commands,
263    query: Query<(Entity, &InstanceName, &Position)>,
264    instance_container: Res<InstanceContainer>,
265) {
266    for (entity, instance_name, position) in &query {
267        let player_chunk_pos = ChunkPos::from(position);
268        let Some(instance_lock) = instance_container.get(instance_name) else {
269            commands.entity(entity).remove::<InLoadedChunk>();
270            continue;
271        };
272
273        let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
274        if in_loaded_chunk {
275            commands.entity(entity).insert(InLoadedChunk);
276        } else {
277            commands.entity(entity).remove::<InLoadedChunk>();
278        }
279    }
280}
281
282/// A component that indicates whether the client has loaded.
283///
284/// This is updated by a system in `azalea-client`.
285#[derive(Component)]
286pub struct HasClientLoaded;
287
288#[cfg(test)]
289mod tests {
290    use azalea_block::{
291        blocks::{Ladder, OakTrapdoor},
292        properties::{FacingCardinal, TopBottom},
293    };
294    use azalea_core::position::{BlockPos, ChunkPos};
295    use azalea_registry::builtin::BlockKind;
296    use azalea_world::{Chunk, ChunkStorage, Instance, PartialInstance};
297
298    use super::is_trapdoor_useable_as_ladder;
299
300    #[test]
301    fn test_is_trapdoor_useable_as_ladder() {
302        let mut partial_instance = PartialInstance::default();
303        let mut chunks = ChunkStorage::default();
304        partial_instance.chunks.set(
305            &ChunkPos { x: 0, z: 0 },
306            Some(Chunk::default()),
307            &mut chunks,
308        );
309        partial_instance.chunks.set_block_state(
310            BlockPos::new(0, 0, 0),
311            BlockKind::Stone.into(),
312            &chunks,
313        );
314
315        let ladder = Ladder {
316            facing: FacingCardinal::East,
317            waterlogged: false,
318        };
319        partial_instance
320            .chunks
321            .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
322
323        let trapdoor = OakTrapdoor {
324            facing: FacingCardinal::East,
325            half: TopBottom::Bottom,
326            open: true,
327            powered: false,
328            waterlogged: false,
329        };
330        partial_instance
331            .chunks
332            .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
333
334        let instance = Instance::from(chunks);
335        let trapdoor_matches_ladder = is_trapdoor_useable_as_ladder(
336            instance
337                .get_block_state(BlockPos::new(0, 1, 0))
338                .unwrap_or_default(),
339            BlockPos::new(0, 1, 0),
340            &instance,
341        );
342
343        assert!(trapdoor_matches_ladder);
344    }
345}