azalea_entity/plugin/
mod.rs

1pub mod indexing;
2mod relative_updates;
3
4use std::collections::HashSet;
5
6use azalea_block::{BlockState, fluid_state::FluidKind};
7use azalea_core::{
8    position::{BlockPos, ChunkPos, Vec3},
9    tick::GameTick,
10};
11use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
12use bevy_app::{App, Plugin, PostUpdate, Update};
13use bevy_ecs::prelude::*;
14use derive_more::{Deref, DerefMut};
15use indexing::EntityUuidIndex;
16pub use relative_updates::RelativeEntityUpdate;
17use tracing::debug;
18
19use crate::{
20    Crouching, Dead, EntityKindComponent, FluidOnEyes, LocalEntity, LookDirection, OnClimbable,
21    Physics, Pose, Position,
22    dimensions::{EntityDimensions, calculate_dimensions},
23    metadata::Health,
24};
25
26/// A Bevy [`SystemSet`] for various types of entity updates.
27#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
28pub enum EntityUpdateSystems {
29    /// Create search indexes for entities.
30    Index,
31    /// Remove despawned entities from search indexes.
32    Deindex,
33}
34
35/// Plugin handling some basic entity functionality.
36pub struct EntityPlugin;
37impl Plugin for EntityPlugin {
38    fn build(&self, app: &mut App) {
39        // entities get added pre-update
40        // added to indexes during update (done by this plugin)
41        // modified during update
42        // despawned post-update (done by this plugin)
43        app.add_systems(
44            PostUpdate,
45            indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSystems::Deindex),
46        )
47        .add_systems(
48            Update,
49            (
50                (
51                    indexing::update_entity_chunk_positions,
52                    indexing::insert_entity_chunk_position,
53                )
54                    .chain()
55                    .in_set(EntityUpdateSystems::Index),
56                (
57                    relative_updates::debug_detect_updates_received_on_local_entities,
58                    debug_new_entity,
59                    add_dead,
60                    clamp_look_direction,
61                    update_on_climbable,
62                    (update_dimensions, update_bounding_box, update_fluid_on_eyes).chain(),
63                    update_crouching,
64                ),
65            ),
66        )
67        .add_systems(GameTick, update_in_loaded_chunk)
68        .init_resource::<EntityUuidIndex>();
69    }
70}
71
72fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
73    for (entity, local) in query.iter() {
74        if local.is_some() {
75            debug!("new local entity: {:?}", entity);
76        } else {
77            debug!("new entity: {:?}", entity);
78        }
79    }
80}
81
82/// System that adds the [`Dead`] marker component if an entity's health is set
83/// to 0 (or less than 0).
84///
85/// This will be present if an entity is doing the death animation.
86///
87/// Entities that are dead cannot be revived.
88pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
89    for (entity, health) in query.iter() {
90        if **health <= 0.0 {
91            commands.entity(entity).insert(Dead);
92        }
93    }
94}
95
96pub fn update_fluid_on_eyes(
97    mut query: Query<(
98        &mut FluidOnEyes,
99        &Position,
100        &EntityDimensions,
101        &InstanceName,
102    )>,
103    instance_container: Res<InstanceContainer>,
104) {
105    for (mut fluid_on_eyes, position, dimensions, instance_name) in query.iter_mut() {
106        let Some(instance) = instance_container.get(instance_name) else {
107            continue;
108        };
109
110        let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
111        let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z));
112        let fluid_at_eye = instance
113            .read()
114            .get_fluid_state(eye_block_pos)
115            .unwrap_or_default();
116        let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
117        if fluid_cutoff_y > adjusted_eye_y {
118            **fluid_on_eyes = fluid_at_eye.kind;
119        } else {
120            **fluid_on_eyes = FluidKind::Empty;
121        }
122    }
123}
124
125pub fn update_on_climbable(
126    mut query: Query<(&mut OnClimbable, &Position, &InstanceName), With<LocalEntity>>,
127    instance_container: Res<InstanceContainer>,
128) {
129    for (mut on_climbable, position, instance_name) in query.iter_mut() {
130        // TODO: there's currently no gamemode component that can be accessed from here,
131        // maybe LocalGameMode should be replaced with two components, maybe called
132        // EntityGameMode and PreviousGameMode?
133
134        // if game_mode == GameMode::Spectator {
135        //     continue;
136        // }
137
138        let Some(instance) = instance_container.get(instance_name) else {
139            continue;
140        };
141
142        let instance = instance.read();
143
144        let block_pos = BlockPos::from(position);
145        let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default();
146        let block_at_feet = Box::<dyn azalea_block::BlockTrait>::from(block_state_at_feet);
147        let registry_block_at_feet = block_at_feet.as_registry_block();
148
149        **on_climbable = azalea_registry::tags::blocks::CLIMBABLE.contains(&registry_block_at_feet)
150            || (azalea_registry::tags::blocks::TRAPDOORS.contains(&registry_block_at_feet)
151                && is_trapdoor_useable_as_ladder(block_state_at_feet, block_pos, &instance));
152    }
153}
154
155fn is_trapdoor_useable_as_ladder(
156    block_state: BlockState,
157    block_pos: BlockPos,
158    instance: &azalea_world::Instance,
159) -> bool {
160    // trapdoor must be open
161    if !block_state
162        .property::<azalea_block::properties::Open>()
163        .unwrap_or_default()
164    {
165        return false;
166    }
167
168    // block below must be a ladder
169    let block_below = instance
170        .get_block_state(block_pos.down(1))
171        .unwrap_or_default();
172    let registry_block_below =
173        Box::<dyn azalea_block::BlockTrait>::from(block_below).as_registry_block();
174    if registry_block_below != azalea_registry::Block::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::<azalea_block::properties::FacingCardinal>()
180        .expect("ladder block must have facing property");
181    let trapdoor_facing = block_state
182        .property::<azalea_block::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(Component, Clone, 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(Component, Clone, Debug, Copy)]
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_world::{Chunk, ChunkStorage, Instance, PartialInstance};
296
297    use super::is_trapdoor_useable_as_ladder;
298
299    #[test]
300    fn test_is_trapdoor_useable_as_ladder() {
301        let mut partial_instance = PartialInstance::default();
302        let mut chunks = ChunkStorage::default();
303        partial_instance.chunks.set(
304            &ChunkPos { x: 0, z: 0 },
305            Some(Chunk::default()),
306            &mut chunks,
307        );
308        partial_instance.chunks.set_block_state(
309            BlockPos::new(0, 0, 0),
310            azalea_registry::Block::Stone.into(),
311            &chunks,
312        );
313
314        let ladder = Ladder {
315            facing: FacingCardinal::East,
316            waterlogged: false,
317        };
318        partial_instance
319            .chunks
320            .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
321
322        let trapdoor = OakTrapdoor {
323            facing: FacingCardinal::East,
324            half: TopBottom::Bottom,
325            open: true,
326            powered: false,
327            waterlogged: false,
328        };
329        partial_instance
330            .chunks
331            .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
332
333        let instance = Instance::from(chunks);
334        let trapdoor_matches_ladder = is_trapdoor_useable_as_ladder(
335            instance
336                .get_block_state(BlockPos::new(0, 1, 0))
337                .unwrap_or_default(),
338            BlockPos::new(0, 1, 0),
339            &instance,
340        );
341
342        assert!(trapdoor_matches_ladder);
343    }
344}