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    Dead, EyeHeight, FluidOnEyes, LocalEntity, LookDirection, OnClimbable, Physics, Position,
21    metadata::Health,
22};
23
24/// A Bevy [`SystemSet`] for various types of entity updates.
25#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
26pub enum EntityUpdateSet {
27    /// Create search indexes for entities.
28    Index,
29    /// Remove despawned entities from search indexes.
30    Deindex,
31}
32
33/// Plugin handling some basic entity functionality.
34pub struct EntityPlugin;
35impl Plugin for EntityPlugin {
36    fn build(&self, app: &mut App) {
37        // entities get added pre-update
38        // added to indexes during update (done by this plugin)
39        // modified during update
40        // despawned post-update (done by this plugin)
41        app.add_systems(
42            PostUpdate,
43            indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSet::Deindex),
44        )
45        .add_systems(
46            Update,
47            (
48                (
49                    indexing::update_entity_chunk_positions,
50                    indexing::insert_entity_chunk_position,
51                )
52                    .chain()
53                    .in_set(EntityUpdateSet::Index),
54                (
55                    relative_updates::debug_detect_updates_received_on_local_entities,
56                    debug_new_entity,
57                    add_dead,
58                    clamp_look_direction,
59                    update_fluid_on_eyes,
60                    update_on_climbable,
61                ),
62            ),
63        )
64        .add_systems(Update, update_bounding_box)
65        .add_systems(GameTick, update_in_loaded_chunk)
66        .init_resource::<EntityUuidIndex>();
67    }
68}
69
70fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
71    for (entity, local) in query.iter() {
72        if local.is_some() {
73            debug!("new local entity: {:?}", entity);
74        } else {
75            debug!("new entity: {:?}", entity);
76        }
77    }
78}
79
80/// System that adds the [`Dead`] marker component if an entity's health is set
81/// to 0 (or less than 0). This will be present if an entity is doing the death
82/// animation.
83///
84/// Entities that are dead cannot be revived.
85pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
86    for (entity, health) in query.iter() {
87        if **health <= 0.0 {
88            commands.entity(entity).insert(Dead);
89        }
90    }
91}
92
93pub fn update_fluid_on_eyes(
94    mut query: Query<(&mut FluidOnEyes, &Position, &EyeHeight, &InstanceName)>,
95    instance_container: Res<InstanceContainer>,
96) {
97    for (mut fluid_on_eyes, position, eye_height, instance_name) in query.iter_mut() {
98        let Some(instance) = instance_container.get(instance_name) else {
99            continue;
100        };
101
102        let adjusted_eye_y = position.y + (**eye_height as f64) - 0.1111111119389534;
103        let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z));
104        let fluid_at_eye = instance
105            .read()
106            .get_fluid_state(eye_block_pos)
107            .unwrap_or_default();
108        let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
109        if fluid_cutoff_y > adjusted_eye_y {
110            **fluid_on_eyes = fluid_at_eye.kind;
111        } else {
112            **fluid_on_eyes = FluidKind::Empty;
113        }
114    }
115}
116
117pub fn update_on_climbable(
118    mut query: Query<(&mut OnClimbable, &Position, &InstanceName), With<LocalEntity>>,
119    instance_container: Res<InstanceContainer>,
120) {
121    for (mut on_climbable, position, instance_name) in query.iter_mut() {
122        // TODO: there's currently no gamemode component that can be accessed from here,
123        // maybe LocalGameMode should be replaced with two components, maybe called
124        // EntityGameMode and PreviousGameMode?
125
126        // if game_mode == GameMode::Spectator {
127        //     continue;
128        // }
129
130        let Some(instance) = instance_container.get(instance_name) else {
131            continue;
132        };
133
134        let instance = instance.read();
135
136        let block_pos = BlockPos::from(position);
137        let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default();
138        let block_at_feet = Box::<dyn azalea_block::BlockTrait>::from(block_state_at_feet);
139        let registry_block_at_feet = block_at_feet.as_registry_block();
140
141        **on_climbable = azalea_registry::tags::blocks::CLIMBABLE.contains(&registry_block_at_feet)
142            || (azalea_registry::tags::blocks::TRAPDOORS.contains(&registry_block_at_feet)
143                && is_trapdoor_useable_as_ladder(block_state_at_feet, block_pos, &instance));
144    }
145}
146
147fn is_trapdoor_useable_as_ladder(
148    block_state: BlockState,
149    block_pos: BlockPos,
150    instance: &azalea_world::Instance,
151) -> bool {
152    // trapdoor must be open
153    if !block_state
154        .property::<azalea_block::properties::Open>()
155        .unwrap_or_default()
156    {
157        return false;
158    }
159
160    // block below must be a ladder
161    let block_below = instance
162        .get_block_state(block_pos.down(1))
163        .unwrap_or_default();
164    let registry_block_below =
165        Box::<dyn azalea_block::BlockTrait>::from(block_below).as_registry_block();
166    if registry_block_below != azalea_registry::Block::Ladder {
167        return false;
168    }
169    // and the ladder must be facing the same direction as the trapdoor
170    let ladder_facing = block_below
171        .property::<azalea_block::properties::FacingCardinal>()
172        .expect("ladder block must have facing property");
173    let trapdoor_facing = block_state
174        .property::<azalea_block::properties::FacingCardinal>()
175        .expect("trapdoor block must have facing property");
176    if ladder_facing != trapdoor_facing {
177        return false;
178    }
179
180    true
181}
182
183/// A component that lists all the local player entities that have this entity
184/// loaded. If this is empty, the entity will be removed from the ECS.
185#[derive(Component, Clone, Deref, DerefMut)]
186pub struct LoadedBy(pub HashSet<Entity>);
187
188pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
189    for mut look_direction in &mut query {
190        *look_direction = apply_clamp_look_direction(*look_direction);
191    }
192}
193pub fn apply_clamp_look_direction(mut look_direction: LookDirection) -> LookDirection {
194    look_direction.y_rot = look_direction.y_rot.rem_euclid(360.0);
195    look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0;
196    look_direction
197}
198
199/// Sets the position of the entity. This doesn't update the cache in
200/// azalea-world, and should only be used within azalea-world!
201///
202/// # Safety
203/// Cached position in the world must be updated.
204pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed<Position>>) {
205    for (position, mut physics) in query.iter_mut() {
206        let bounding_box = physics.dimensions.make_bounding_box(**position);
207        physics.bounding_box = bounding_box;
208    }
209}
210
211/// Marks an entity that's in a loaded chunk. This is updated at the beginning
212/// of every tick.
213///
214/// Internally, this is only used for player physics. Not to be confused with
215/// the somewhat similarly named [`LoadedBy`].
216#[derive(Component, Clone, Debug, Copy)]
217pub struct InLoadedChunk;
218
219/// Update the [`InLoadedChunk`] component for all entities in the world.
220pub fn update_in_loaded_chunk(
221    mut commands: bevy_ecs::system::Commands,
222    query: Query<(Entity, &InstanceName, &Position)>,
223    instance_container: Res<InstanceContainer>,
224) {
225    for (entity, instance_name, position) in &query {
226        let player_chunk_pos = ChunkPos::from(position);
227        let Some(instance_lock) = instance_container.get(instance_name) else {
228            commands.entity(entity).remove::<InLoadedChunk>();
229            continue;
230        };
231
232        let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
233        if in_loaded_chunk {
234            commands.entity(entity).insert(InLoadedChunk);
235        } else {
236            commands.entity(entity).remove::<InLoadedChunk>();
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use azalea_block::{
244        blocks::{Ladder, OakTrapdoor},
245        properties::{FacingCardinal, TopBottom},
246    };
247    use azalea_core::position::{BlockPos, ChunkPos};
248    use azalea_world::{Chunk, ChunkStorage, Instance, PartialInstance};
249
250    use super::is_trapdoor_useable_as_ladder;
251
252    #[test]
253    fn test_is_trapdoor_useable_as_ladder() {
254        let mut partial_instance = PartialInstance::default();
255        let mut chunks = ChunkStorage::default();
256        partial_instance.chunks.set(
257            &ChunkPos { x: 0, z: 0 },
258            Some(Chunk::default()),
259            &mut chunks,
260        );
261        partial_instance.chunks.set_block_state(
262            BlockPos::new(0, 0, 0),
263            azalea_registry::Block::Stone.into(),
264            &chunks,
265        );
266
267        let ladder = Ladder {
268            facing: FacingCardinal::East,
269            waterlogged: false,
270        };
271        partial_instance
272            .chunks
273            .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
274
275        let trapdoor = OakTrapdoor {
276            facing: FacingCardinal::East,
277            half: TopBottom::Bottom,
278            open: true,
279            powered: false,
280            waterlogged: false,
281        };
282        partial_instance
283            .chunks
284            .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
285
286        let instance = Instance::from(chunks);
287        let trapdoor_matches_ladder = is_trapdoor_useable_as_ladder(
288            instance
289                .get_block_state(BlockPos::new(0, 1, 0))
290                .unwrap_or_default(),
291            BlockPos::new(0, 1, 0),
292            &instance,
293        );
294
295        assert!(trapdoor_matches_ladder);
296    }
297}