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