Skip to main content

azalea_entity/plugin/
mod.rs

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