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 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;
20use tracing::debug;
21
22use crate::{
23 FluidOnEyes, LookDirection, Physics, Pose, Position,
24 dimensions::{EntityDimensions, calculate_dimensions},
25 metadata::{self, Health, Player},
26 plugin::effect_events::{handle_add_effect, handle_remove_effects},
27};
28
29#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
31pub enum EntityUpdateSystems {
32 Index,
34 Deindex,
36}
37
38pub struct EntityPlugin;
40impl Plugin for EntityPlugin {
41 fn build(&self, app: &mut App) {
42 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 debug_new_entity,
61 add_dead,
62 clamp_look_direction,
63 update_on_climbable,
64 (update_dimensions, update_bounding_box).chain(),
65 update_crouching,
66 ),
67 ),
68 )
69 .add_systems(GameTick, (update_in_loaded_chunk, update_fluid_on_eyes))
70 .add_observer(handle_add_effect)
71 .add_observer(handle_remove_effects)
72 .init_resource::<EntityUuidIndex>();
73 }
74}
75
76fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
77 for (entity, local) in query.iter() {
78 if local.is_some() {
79 debug!("new local entity: {:?}", entity);
80 } else {
81 debug!("new entity: {:?}", entity);
82 }
83 }
84}
85
86pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
93 for (entity, health) in query.iter() {
94 if **health <= 0.0 {
95 commands.entity(entity).insert(Dead);
96 }
97 }
98}
99
100pub fn update_fluid_on_eyes(
101 mut query: Query<
102 (&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName),
103 With<metadata::AbstractLiving>,
104 >,
105 worlds: Res<Worlds>,
106) {
107 query
108 .par_iter_mut()
109 .for_each(|(mut fluid_on_eyes, position, dimensions, world_name)| {
110 let Some(world) = worlds.get(world_name) else {
111 return;
112 };
113
114 let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
115 let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
116 let fluid_at_eye = world
117 .read()
118 .get_fluid_state(eye_block_pos)
119 .unwrap_or_default();
120 let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
121 if fluid_cutoff_y > adjusted_eye_y {
122 **fluid_on_eyes = fluid_at_eye.kind;
123 } else {
124 **fluid_on_eyes = FluidKind::Empty;
125 }
126 });
127}
128
129pub fn update_on_climbable(
130 mut query: Query<(&mut OnClimbable, &Position, &WorldName), With<LocalEntity>>,
131 worlds: Res<Worlds>,
132) {
133 for (mut on_climbable, position, world_name) in query.iter_mut() {
134 let Some(world) = worlds.get(world_name) else {
143 continue;
144 };
145
146 let world = world.read();
147
148 let block_pos = BlockPos::from(position);
149 let block_state_at_feet = world.get_block_state(block_pos).unwrap_or_default();
150 let block_at_feet = Box::<dyn BlockTrait>::from(block_state_at_feet);
151 let registry_block_at_feet = block_at_feet.as_block_kind();
152
153 **on_climbable = tags::blocks::CLIMBABLE.contains(®istry_block_at_feet)
154 || (tags::blocks::TRAPDOORS.contains(®istry_block_at_feet)
155 && is_trapdoor_usable_as_ladder(block_state_at_feet, block_pos, &world));
156 }
157}
158
159fn is_trapdoor_usable_as_ladder(
160 block_state: BlockState,
161 block_pos: BlockPos,
162 world: &azalea_world::World,
163) -> bool {
164 if !block_state
166 .property::<properties::Open>()
167 .unwrap_or_default()
168 {
169 return false;
170 }
171
172 let block_below = world.get_block_state(block_pos.down(1)).unwrap_or_default();
174 let registry_block_below = block_below.as_block_kind();
175 if registry_block_below != BlockKind::Ladder {
176 return false;
177 }
178 let ladder_facing = block_below
180 .property::<properties::FacingCardinal>()
181 .expect("ladder block must have facing property");
182 let trapdoor_facing = block_state
183 .property::<properties::FacingCardinal>()
184 .expect("trapdoor block must have facing property");
185 if ladder_facing != trapdoor_facing {
186 return false;
187 }
188
189 true
190}
191
192#[derive(Clone, Component, Deref, DerefMut)]
197pub struct LoadedBy(pub HashSet<Entity>);
198
199pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
200 for mut look_direction in &mut query {
201 *look_direction = look_direction.clamped();
202 }
203}
204
205#[allow(clippy::type_complexity)]
214pub fn update_bounding_box(
215 mut query: Query<
216 (&mut Physics, &Position, &EntityDimensions),
217 Or<(Changed<Position>, Changed<EntityDimensions>)>,
218 >,
219) {
220 for (mut physics, position, dimensions) in query.iter_mut() {
221 let bounding_box = dimensions.make_bounding_box(**position);
222 physics.bounding_box = bounding_box;
223 }
224}
225
226#[allow(clippy::type_complexity)]
227pub fn update_dimensions(
228 mut query: Query<
229 (&mut EntityDimensions, &EntityKindComponent, &Pose),
230 Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
231 >,
232) {
233 for (mut dimensions, kind, pose) in query.iter_mut() {
234 *dimensions = calculate_dimensions(**kind, *pose);
235 }
236}
237
238#[allow(clippy::type_complexity)]
239pub fn update_crouching(
240 query: Query<(&mut Crouching, &Pose), (Without<LocalEntity>, With<Player>)>,
241) {
242 for (mut crouching, pose) in query {
243 let new_crouching = *pose == Pose::Crouching;
244 if **crouching != new_crouching {
246 **crouching = new_crouching;
247 }
248 }
249}
250
251#[derive(Clone, Component, Copy, Debug)]
257pub struct InLoadedChunk;
258
259pub fn update_in_loaded_chunk(
261 mut commands: bevy_ecs::system::Commands,
262 query: Query<(Entity, &WorldName, &Position, Option<&InLoadedChunk>)>,
263 worlds: Res<Worlds>,
264) {
265 for (entity, world_name, position, last_in_loaded_chunk) in &query {
266 let player_chunk_pos = ChunkPos::from(position);
267 let Some(world_lock) = worlds.get(world_name) else {
268 commands.entity(entity).remove::<InLoadedChunk>();
269 continue;
270 };
271
272 let in_loaded_chunk = world_lock.read().chunks.get(&player_chunk_pos).is_some();
273 if in_loaded_chunk {
274 if last_in_loaded_chunk.is_none() {
275 commands.entity(entity).insert(InLoadedChunk);
276 }
277 } else {
278 if last_in_loaded_chunk.is_some() {
279 commands.entity(entity).remove::<InLoadedChunk>();
280 }
281 }
282 }
283}
284
285pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos {
287 on_pos(0.2, chunk_storage, position)
288}
289
290pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: Position) -> BlockPos {
303 let x = pos.x.floor() as i32;
304 let y = (pos.y - offset as f64).floor() as i32;
305 let z = pos.z.floor() as i32;
306 let pos = BlockPos { x, y, z };
307
308 let block_pos = pos.down(1);
310 let block_state = chunk_storage.get_block_state(block_pos);
311 if block_state == Some(BlockState::AIR) {
312 let block_pos_below = block_pos.down(1);
313 let block_state_below = chunk_storage.get_block_state(block_pos_below);
314 if let Some(_block_state_below) = block_state_below {
315 }
322 }
323
324 pos
325}
326
327#[cfg(test)]
328mod tests {
329 use azalea_block::{
330 blocks::{Ladder, OakTrapdoor},
331 properties::{FacingCardinal, TopBottom},
332 };
333 use azalea_core::position::{BlockPos, ChunkPos};
334 use azalea_registry::builtin::BlockKind;
335 use azalea_world::{Chunk, ChunkStorage, PartialWorld, World};
336
337 use super::is_trapdoor_usable_as_ladder;
338
339 #[test]
340 fn test_is_trapdoor_useable_as_ladder() {
341 let mut partial_world = PartialWorld::default();
342 let mut chunks = ChunkStorage::default();
343 partial_world.chunks.set(
344 &ChunkPos { x: 0, z: 0 },
345 Some(Chunk::default()),
346 &mut chunks,
347 );
348 partial_world.chunks.set_block_state(
349 BlockPos::new(0, 0, 0),
350 BlockKind::Stone.into(),
351 &chunks,
352 );
353
354 let ladder = Ladder {
355 facing: FacingCardinal::East,
356 waterlogged: false,
357 };
358 partial_world
359 .chunks
360 .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
361
362 let trapdoor = OakTrapdoor {
363 facing: FacingCardinal::East,
364 half: TopBottom::Bottom,
365 open: true,
366 powered: false,
367 waterlogged: false,
368 };
369 partial_world
370 .chunks
371 .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
372
373 let world = World::from(chunks);
374 let trapdoor_matches_ladder = is_trapdoor_usable_as_ladder(
375 world
376 .get_block_state(BlockPos::new(0, 1, 0))
377 .unwrap_or_default(),
378 BlockPos::new(0, 1, 0),
379 &world,
380 );
381
382 assert!(trapdoor_matches_ladder);
383 }
384}