Skip to main content

azalea_entity/plugin/
mod.rs

1mod components;
2pub mod indexing;
3
4use std::collections::HashSet;
5
6use azalea_block::{BlockState, BlockTrait, fluid_state::FluidKind, properties};
7use azalea_core::{
8    entity_id::MinecraftEntityId,
9    position::{BlockPos, ChunkPos},
10    tick::GameTick,
11};
12use azalea_registry::{builtin::BlockKind, tags};
13use azalea_world::{ChunkStorage, WorldName, Worlds};
14use bevy_app::{App, Plugin, PostUpdate, Update};
15use bevy_ecs::prelude::*;
16pub use components::*;
17use derive_more::{Deref, DerefMut};
18use indexing::EntityUuidIndex;
19use tracing::debug;
20
21use crate::{
22    FluidOnEyes, LookDirection, Physics, Pose, Position,
23    dimensions::{EntityDimensions, calculate_dimensions},
24    metadata::{self, Health, Player},
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                    debug_new_entity,
59                    add_dead,
60                    clamp_look_direction,
61                    update_on_climbable,
62                    (update_dimensions, update_bounding_box).chain(),
63                    update_crouching,
64                ),
65            ),
66        )
67        .add_systems(GameTick, (update_in_loaded_chunk, update_fluid_on_eyes))
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, &Position, &EntityDimensions, &WorldName),
99        With<metadata::AbstractLiving>,
100    >,
101    worlds: Res<Worlds>,
102) {
103    query
104        .par_iter_mut()
105        .for_each(|(mut fluid_on_eyes, position, dimensions, world_name)| {
106            let Some(world) = worlds.get(world_name) else {
107                return;
108            };
109
110            let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
111            let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
112            let fluid_at_eye = world
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, &WorldName), With<LocalEntity>>,
127    worlds: Res<Worlds>,
128) {
129    for (mut on_climbable, position, world_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(world) = worlds.get(world_name) else {
139            continue;
140        };
141
142        let world = world.read();
143
144        let block_pos = BlockPos::from(position);
145        let block_state_at_feet = world.get_block_state(block_pos).unwrap_or_default();
146        let block_at_feet = Box::<dyn BlockTrait>::from(block_state_at_feet);
147        let registry_block_at_feet = block_at_feet.as_registry_block();
148
149        **on_climbable = tags::blocks::CLIMBABLE.contains(&registry_block_at_feet)
150            || (tags::blocks::TRAPDOORS.contains(&registry_block_at_feet)
151                && is_trapdoor_usable_as_ladder(block_state_at_feet, block_pos, &world));
152    }
153}
154
155fn is_trapdoor_usable_as_ladder(
156    block_state: BlockState,
157    block_pos: BlockPos,
158    world: &azalea_world::World,
159) -> bool {
160    // trapdoor must be open
161    if !block_state
162        .property::<properties::Open>()
163        .unwrap_or_default()
164    {
165        return false;
166    }
167
168    // block below must be a ladder
169    let block_below = world.get_block_state(block_pos.down(1)).unwrap_or_default();
170    let registry_block_below = Box::<dyn BlockTrait>::from(block_below).as_registry_block();
171    if registry_block_below != BlockKind::Ladder {
172        return false;
173    }
174    // and the ladder must be facing the same direction as the trapdoor
175    let ladder_facing = block_below
176        .property::<properties::FacingCardinal>()
177        .expect("ladder block must have facing property");
178    let trapdoor_facing = block_state
179        .property::<properties::FacingCardinal>()
180        .expect("trapdoor block must have facing property");
181    if ladder_facing != trapdoor_facing {
182        return false;
183    }
184
185    true
186}
187
188/// A component that lists all the local player entities that have this entity
189/// loaded.
190///
191/// If this is empty, the entity will be removed from the ECS.
192#[derive(Clone, Component, Deref, DerefMut)]
193pub struct LoadedBy(pub HashSet<Entity>);
194
195pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
196    for mut look_direction in &mut query {
197        *look_direction = look_direction.clamped();
198    }
199}
200
201/// Sets the position of the entity.
202///
203/// This doesn't update the cache in azalea-world, and should only be used
204/// within azalea-world.
205///
206/// # Safety
207///
208/// Cached position in the world must be updated.
209#[allow(clippy::type_complexity)]
210pub fn update_bounding_box(
211    mut query: Query<
212        (&mut Physics, &Position, &EntityDimensions),
213        Or<(Changed<Position>, Changed<EntityDimensions>)>,
214    >,
215) {
216    for (mut physics, position, dimensions) in query.iter_mut() {
217        let bounding_box = dimensions.make_bounding_box(**position);
218        physics.bounding_box = bounding_box;
219    }
220}
221
222#[allow(clippy::type_complexity)]
223pub fn update_dimensions(
224    mut query: Query<
225        (&mut EntityDimensions, &EntityKindComponent, &Pose),
226        Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
227    >,
228) {
229    for (mut dimensions, kind, pose) in query.iter_mut() {
230        *dimensions = calculate_dimensions(**kind, *pose);
231    }
232}
233
234#[allow(clippy::type_complexity)]
235pub fn update_crouching(
236    query: Query<(&mut Crouching, &Pose), (Without<LocalEntity>, With<Player>)>,
237) {
238    for (mut crouching, pose) in query {
239        let new_crouching = *pose == Pose::Crouching;
240        // avoid triggering change detection
241        if **crouching != new_crouching {
242            **crouching = new_crouching;
243        }
244    }
245}
246
247/// Marks an entity that's in a loaded chunk. This is updated at the beginning
248/// of every tick.
249///
250/// Internally, this is only used for player physics. Not to be confused with
251/// the somewhat similarly named [`LoadedBy`].
252#[derive(Clone, Component, Copy, Debug)]
253pub struct InLoadedChunk;
254
255/// Update the [`InLoadedChunk`] component for all entities in the world.
256pub fn update_in_loaded_chunk(
257    mut commands: bevy_ecs::system::Commands,
258    query: Query<(Entity, &WorldName, &Position, Option<&InLoadedChunk>)>,
259    worlds: Res<Worlds>,
260) {
261    for (entity, world_name, position, last_in_loaded_chunk) in &query {
262        let player_chunk_pos = ChunkPos::from(position);
263        let Some(world_lock) = worlds.get(world_name) else {
264            commands.entity(entity).remove::<InLoadedChunk>();
265            continue;
266        };
267
268        let in_loaded_chunk = world_lock.read().chunks.get(&player_chunk_pos).is_some();
269        if in_loaded_chunk {
270            if last_in_loaded_chunk.is_none() {
271                commands.entity(entity).insert(InLoadedChunk);
272            }
273        } else {
274            if last_in_loaded_chunk.is_some() {
275                commands.entity(entity).remove::<InLoadedChunk>();
276            }
277        }
278    }
279}
280
281/// Get the position of the block below the entity, but a little lower.
282pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos {
283    on_pos(0.2, chunk_storage, position)
284}
285
286// int x = Mth.floor(this.position.x);
287// int y = Mth.floor(this.position.y - (double)var1);
288// int z = Mth.floor(this.position.z);
289// BlockPos var5 = new BlockPos(x, y, z);
290// if (this.level.getBlockState(var5).isAir()) {
291//    BlockPos var6 = var5.below();
292//    BlockState var7 = this.level.getBlockState(var6);
293//    if (var7.is(BlockTags.FENCES) || var7.is(BlockTags.WALLS) ||
294// var7.getBlock() instanceof FenceGateBlock) {       return var6;
295//    }
296// }
297// return var5;
298pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: Position) -> BlockPos {
299    let x = pos.x.floor() as i32;
300    let y = (pos.y - offset as f64).floor() as i32;
301    let z = pos.z.floor() as i32;
302    let pos = BlockPos { x, y, z };
303
304    // TODO: check if block below is a fence, wall, or fence gate
305    let block_pos = pos.down(1);
306    let block_state = chunk_storage.get_block_state(block_pos);
307    if block_state == Some(BlockState::AIR) {
308        let block_pos_below = block_pos.down(1);
309        let block_state_below = chunk_storage.get_block_state(block_pos_below);
310        if let Some(_block_state_below) = block_state_below {
311            // if block_state_below.is_fence()
312            //     || block_state_below.is_wall()
313            //     || block_state_below.is_fence_gate()
314            // {
315            //     return block_pos_below;
316            // }
317        }
318    }
319
320    pos
321}
322
323#[cfg(test)]
324mod tests {
325    use azalea_block::{
326        blocks::{Ladder, OakTrapdoor},
327        properties::{FacingCardinal, TopBottom},
328    };
329    use azalea_core::position::{BlockPos, ChunkPos};
330    use azalea_registry::builtin::BlockKind;
331    use azalea_world::{Chunk, ChunkStorage, PartialWorld, World};
332
333    use super::is_trapdoor_usable_as_ladder;
334
335    #[test]
336    fn test_is_trapdoor_useable_as_ladder() {
337        let mut partial_world = PartialWorld::default();
338        let mut chunks = ChunkStorage::default();
339        partial_world.chunks.set(
340            &ChunkPos { x: 0, z: 0 },
341            Some(Chunk::default()),
342            &mut chunks,
343        );
344        partial_world.chunks.set_block_state(
345            BlockPos::new(0, 0, 0),
346            BlockKind::Stone.into(),
347            &chunks,
348        );
349
350        let ladder = Ladder {
351            facing: FacingCardinal::East,
352            waterlogged: false,
353        };
354        partial_world
355            .chunks
356            .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
357
358        let trapdoor = OakTrapdoor {
359            facing: FacingCardinal::East,
360            half: TopBottom::Bottom,
361            open: true,
362            powered: false,
363            waterlogged: false,
364        };
365        partial_world
366            .chunks
367            .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
368
369        let world = World::from(chunks);
370        let trapdoor_matches_ladder = is_trapdoor_usable_as_ladder(
371            world
372                .get_block_state(BlockPos::new(0, 1, 0))
373                .unwrap_or_default(),
374            BlockPos::new(0, 1, 0),
375            &world,
376        );
377
378        assert!(trapdoor_matches_ladder);
379    }
380}