azalea_entity/plugin/
mod.rs

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