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#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
32pub enum EntityUpdateSystems {
33 Index,
35 Deindex,
37}
38
39pub struct EntityPlugin;
41impl Plugin for EntityPlugin {
42 fn build(&self, app: &mut App) {
43 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
87pub 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(®istry_block_at_feet)
151 || (tags::blocks::TRAPDOORS.contains(®istry_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 if !block_state
163 .property::<properties::Open>()
164 .unwrap_or_default()
165 {
166 return false;
167 }
168
169 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 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#[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#[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 if **crouching != new_crouching {
243 **crouching = new_crouching;
244 }
245 }
246}
247
248#[derive(Clone, Component, Copy, Debug)]
254pub struct InLoadedChunk;
255
256pub 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
282pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos {
284 on_pos(0.2, chunk_storage, position)
285}
286
287pub 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 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 }
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}