1mod components;
2pub mod effect_events;
3pub mod indexing;
4
5use std::collections::HashSet;
6
7use azalea_block::{BlockState, 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
39#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
41pub struct EntityGeometryUpdateSystems;
42
43pub struct EntityPlugin;
45impl Plugin for EntityPlugin {
46 fn build(&self, app: &mut App) {
47 app.add_systems(
52 PostUpdate,
53 indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSystems::Deindex),
54 )
55 .add_systems(
56 Update,
57 (
58 (
59 indexing::update_entity_chunk_positions,
60 indexing::insert_entity_chunk_position,
61 )
62 .chain()
63 .in_set(EntityUpdateSystems::Index),
64 (
65 debug_new_entity,
66 add_dead,
67 clamp_look_direction,
68 update_on_climbable,
69 (update_dimensions, update_bounding_box).chain(),
70 update_crouching,
71 ),
72 ),
73 )
74 .add_systems(
75 GameTick,
76 (
77 update_in_loaded_chunk,
78 update_fluid_on_eyes,
79 (update_dimensions, update_bounding_box)
80 .chain()
81 .in_set(EntityGeometryUpdateSystems),
82 ),
83 )
84 .add_observer(handle_add_effect)
85 .add_observer(handle_remove_effects)
86 .init_resource::<EntityUuidIndex>();
87 }
88}
89
90fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
91 for (entity, local) in query.iter() {
92 if local.is_some() {
93 debug!("new local entity: {:?}", entity);
94 } else {
95 debug!("new entity: {:?}", entity);
96 }
97 }
98}
99
100pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
107 for (entity, health) in query.iter() {
108 if **health <= 0.0 {
109 commands.entity(entity).insert(Dead);
110 }
111 }
112}
113
114pub fn update_fluid_on_eyes(
115 mut query: Query<
116 (&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName),
117 With<metadata::AbstractLiving>,
118 >,
119 worlds: Res<Worlds>,
120) {
121 query
122 .par_iter_mut()
123 .for_each(|(mut fluid_on_eyes, position, dimensions, world_name)| {
124 let Some(world) = worlds.get(world_name) else {
125 return;
126 };
127
128 let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
129 let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
130 let fluid_at_eye = world
131 .read()
132 .get_fluid_state(eye_block_pos)
133 .unwrap_or_default();
134 let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
135 if fluid_cutoff_y > adjusted_eye_y {
136 **fluid_on_eyes = fluid_at_eye.kind;
137 } else {
138 **fluid_on_eyes = FluidKind::Empty;
139 }
140 });
141}
142
143pub fn update_on_climbable(
144 mut query: Query<(&mut OnClimbable, &Position, &WorldName, &GameMode), With<LocalEntity>>,
145 worlds: Res<Worlds>,
146) {
147 for (mut on_climbable, position, world_name, &game_mode) in query.iter_mut() {
148 if game_mode == GameMode::Spectator {
149 continue;
150 }
151
152 let Some(world) = worlds.get(world_name) else {
153 continue;
154 };
155
156 let world = world.read();
157
158 let block_pos = BlockPos::from(position);
159 let block_at_feet = world.get_block_state(block_pos).unwrap_or_default();
160 let registry_block_at_feet = block_at_feet.as_block_kind();
161
162 **on_climbable = tags::blocks::CLIMBABLE.contains(®istry_block_at_feet)
163 || (tags::blocks::TRAPDOORS.contains(®istry_block_at_feet)
164 && is_trapdoor_usable_as_ladder(block_at_feet, block_pos, &world));
165 }
166}
167
168#[expect(clippy::needless_bool, reason = "Kept for future improvements")]
169fn is_trapdoor_usable_as_ladder(
170 block_state: BlockState,
171 block_pos: BlockPos,
172 world: &azalea_world::World,
173) -> bool {
174 if !block_state
176 .property::<properties::Open>()
177 .unwrap_or_default()
178 {
179 return false;
180 }
181
182 let block_below = world.get_block_state(block_pos.down(1)).unwrap_or_default();
184 let registry_block_below = block_below.as_block_kind();
185 if registry_block_below != BlockKind::Ladder {
186 return false;
187 }
188 let ladder_facing = block_below
190 .property::<properties::FacingCardinal>()
191 .expect("ladder block must have facing property");
192 let trapdoor_facing = block_state
193 .property::<properties::FacingCardinal>()
194 .expect("trapdoor block must have facing property");
195 if ladder_facing != trapdoor_facing {
196 return false;
197 }
198
199 true
200}
201
202#[derive(Clone, Component, Deref, DerefMut)]
207pub struct LoadedBy(pub HashSet<Entity>);
208
209pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
210 for mut look_direction in &mut query {
211 *look_direction = look_direction.clamped();
212 }
213}
214
215#[allow(clippy::type_complexity)]
224pub fn update_bounding_box(
225 mut query: Query<
226 (&mut Physics, &Position, &EntityDimensions),
227 Or<(Changed<Position>, Changed<EntityDimensions>)>,
228 >,
229) {
230 for (mut physics, position, dimensions) in query.iter_mut() {
231 let bounding_box = dimensions.make_bounding_box(**position);
232 physics.bounding_box = bounding_box;
233 }
234}
235
236#[allow(clippy::type_complexity)]
237pub fn update_dimensions(
238 mut query: Query<
239 (&mut EntityDimensions, &EntityKindComponent, &Pose),
240 Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
241 >,
242) {
243 for (mut dimensions, kind, pose) in query.iter_mut() {
244 *dimensions = calculate_dimensions(**kind, *pose);
245 }
246}
247
248#[allow(clippy::type_complexity)]
249pub fn update_crouching(
250 query: Query<(&mut Crouching, &Pose), (Without<LocalEntity>, With<Player>)>,
251) {
252 for (mut crouching, pose) in query {
253 let new_crouching = *pose == Pose::Crouching;
254 if **crouching != new_crouching {
256 **crouching = new_crouching;
257 }
258 }
259}
260
261#[derive(Clone, Component, Copy, Debug)]
267pub struct InLoadedChunk;
268
269pub fn update_in_loaded_chunk(
271 mut commands: bevy_ecs::system::Commands,
272 query: Query<(Entity, &WorldName, &Position, Option<&InLoadedChunk>)>,
273 worlds: Res<Worlds>,
274) {
275 for (entity, world_name, position, last_in_loaded_chunk) in &query {
276 let player_chunk_pos = ChunkPos::from(position);
277 let Some(world_lock) = worlds.get(world_name) else {
278 commands.entity(entity).remove::<InLoadedChunk>();
279 continue;
280 };
281
282 let in_loaded_chunk = world_lock.read().chunks.get(&player_chunk_pos).is_some();
283 if in_loaded_chunk {
284 if last_in_loaded_chunk.is_none() {
285 commands.entity(entity).insert(InLoadedChunk);
286 }
287 } else {
288 if last_in_loaded_chunk.is_some() {
289 commands.entity(entity).remove::<InLoadedChunk>();
290 }
291 }
292 }
293}
294
295pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos {
297 on_pos(0.2, chunk_storage, position)
298}
299
300pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: Position) -> BlockPos {
313 let x = pos.x.floor() as i32;
314 let y = (pos.y - offset as f64).floor() as i32;
315 let z = pos.z.floor() as i32;
316 let pos = BlockPos { x, y, z };
317
318 let block_pos = pos.down(1);
320 let block_state = chunk_storage.get_block_state(block_pos);
321 if block_state == Some(BlockState::AIR) {
322 let block_pos_below = block_pos.down(1);
323 let block_state_below = chunk_storage.get_block_state(block_pos_below);
324 if let Some(_block_state_below) = block_state_below {
325 }
332 }
333
334 pos
335}
336
337#[cfg(test)]
338mod tests {
339 use azalea_block::{
340 blocks::{Ladder, OakTrapdoor},
341 properties::{FacingCardinal, TopBottom},
342 };
343 use azalea_core::position::{BlockPos, ChunkPos};
344 use azalea_registry::builtin::BlockKind;
345 use azalea_world::{Chunk, ChunkStorage, PartialWorld, World};
346
347 use super::is_trapdoor_usable_as_ladder;
348
349 #[test]
350 fn test_is_trapdoor_useable_as_ladder() {
351 let mut partial_world = PartialWorld::default();
352 let mut chunks = ChunkStorage::default();
353 partial_world.chunks.set(
354 &ChunkPos { x: 0, z: 0 },
355 Some(Chunk::default()),
356 &mut chunks,
357 );
358 partial_world.chunks.set_block_state(
359 BlockPos::new(0, 0, 0),
360 BlockKind::Stone.into(),
361 &chunks,
362 );
363
364 let ladder = Ladder {
365 facing: FacingCardinal::East,
366 waterlogged: false,
367 };
368 partial_world
369 .chunks
370 .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
371
372 let trapdoor = OakTrapdoor {
373 facing: FacingCardinal::East,
374 half: TopBottom::Bottom,
375 open: true,
376 powered: false,
377 waterlogged: false,
378 };
379 partial_world
380 .chunks
381 .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
382
383 let world = World::from(chunks);
384 let trapdoor_matches_ladder = is_trapdoor_usable_as_ladder(
385 world
386 .get_block_state(BlockPos::new(0, 1, 0))
387 .unwrap_or_default(),
388 BlockPos::new(0, 1, 0),
389 &world,
390 );
391
392 assert!(trapdoor_matches_ladder);
393 }
394}