1mod components;
2pub mod indexing;
3
4use std::collections::HashSet;
5
6use azalea_block::{BlockState, BlockTrait, fluid_state::FluidKind, properties};
7use azalea_core::{
8 entity_id::MinecraftEntityId,
9 position::{BlockPos, ChunkPos},
10 tick::GameTick,
11};
12use azalea_registry::{builtin::BlockKind, tags};
13use azalea_world::{ChunkStorage, WorldName, Worlds};
14use bevy_app::{App, Plugin, PostUpdate, Update};
15use bevy_ecs::prelude::*;
16pub use components::*;
17use derive_more::{Deref, DerefMut};
18use indexing::EntityUuidIndex;
19use tracing::debug;
20
21use crate::{
22 FluidOnEyes, LookDirection, Physics, Pose, Position,
23 dimensions::{EntityDimensions, calculate_dimensions},
24 metadata::{self, Health, Player},
25};
26
27#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
29pub enum EntityUpdateSystems {
30 Index,
32 Deindex,
34}
35
36pub struct EntityPlugin;
38impl Plugin for EntityPlugin {
39 fn build(&self, app: &mut App) {
40 app.add_systems(
45 PostUpdate,
46 indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSystems::Deindex),
47 )
48 .add_systems(
49 Update,
50 (
51 (
52 indexing::update_entity_chunk_positions,
53 indexing::insert_entity_chunk_position,
54 )
55 .chain()
56 .in_set(EntityUpdateSystems::Index),
57 (
58 debug_new_entity,
59 add_dead,
60 clamp_look_direction,
61 update_on_climbable,
62 (update_dimensions, update_bounding_box).chain(),
63 update_crouching,
64 ),
65 ),
66 )
67 .add_systems(GameTick, (update_in_loaded_chunk, update_fluid_on_eyes))
68 .init_resource::<EntityUuidIndex>();
69 }
70}
71
72fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
73 for (entity, local) in query.iter() {
74 if local.is_some() {
75 debug!("new local entity: {:?}", entity);
76 } else {
77 debug!("new entity: {:?}", entity);
78 }
79 }
80}
81
82pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
89 for (entity, health) in query.iter() {
90 if **health <= 0.0 {
91 commands.entity(entity).insert(Dead);
92 }
93 }
94}
95
96pub fn update_fluid_on_eyes(
97 mut query: Query<
98 (&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName),
99 With<metadata::AbstractLiving>,
100 >,
101 worlds: Res<Worlds>,
102) {
103 query
104 .par_iter_mut()
105 .for_each(|(mut fluid_on_eyes, position, dimensions, world_name)| {
106 let Some(world) = worlds.get(world_name) else {
107 return;
108 };
109
110 let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
111 let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
112 let fluid_at_eye = world
113 .read()
114 .get_fluid_state(eye_block_pos)
115 .unwrap_or_default();
116 let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
117 if fluid_cutoff_y > adjusted_eye_y {
118 **fluid_on_eyes = fluid_at_eye.kind;
119 } else {
120 **fluid_on_eyes = FluidKind::Empty;
121 }
122 });
123}
124
125pub fn update_on_climbable(
126 mut query: Query<(&mut OnClimbable, &Position, &WorldName), With<LocalEntity>>,
127 worlds: Res<Worlds>,
128) {
129 for (mut on_climbable, position, world_name) in query.iter_mut() {
130 let Some(world) = worlds.get(world_name) else {
139 continue;
140 };
141
142 let world = world.read();
143
144 let block_pos = BlockPos::from(position);
145 let block_state_at_feet = world.get_block_state(block_pos).unwrap_or_default();
146 let block_at_feet = Box::<dyn BlockTrait>::from(block_state_at_feet);
147 let registry_block_at_feet = block_at_feet.as_registry_block();
148
149 **on_climbable = tags::blocks::CLIMBABLE.contains(®istry_block_at_feet)
150 || (tags::blocks::TRAPDOORS.contains(®istry_block_at_feet)
151 && is_trapdoor_usable_as_ladder(block_state_at_feet, block_pos, &world));
152 }
153}
154
155fn is_trapdoor_usable_as_ladder(
156 block_state: BlockState,
157 block_pos: BlockPos,
158 world: &azalea_world::World,
159) -> bool {
160 if !block_state
162 .property::<properties::Open>()
163 .unwrap_or_default()
164 {
165 return false;
166 }
167
168 let block_below = world.get_block_state(block_pos.down(1)).unwrap_or_default();
170 let registry_block_below = Box::<dyn BlockTrait>::from(block_below).as_registry_block();
171 if registry_block_below != BlockKind::Ladder {
172 return false;
173 }
174 let ladder_facing = block_below
176 .property::<properties::FacingCardinal>()
177 .expect("ladder block must have facing property");
178 let trapdoor_facing = block_state
179 .property::<properties::FacingCardinal>()
180 .expect("trapdoor block must have facing property");
181 if ladder_facing != trapdoor_facing {
182 return false;
183 }
184
185 true
186}
187
188#[derive(Clone, Component, Deref, DerefMut)]
193pub struct LoadedBy(pub HashSet<Entity>);
194
195pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
196 for mut look_direction in &mut query {
197 *look_direction = look_direction.clamped();
198 }
199}
200
201#[allow(clippy::type_complexity)]
210pub fn update_bounding_box(
211 mut query: Query<
212 (&mut Physics, &Position, &EntityDimensions),
213 Or<(Changed<Position>, Changed<EntityDimensions>)>,
214 >,
215) {
216 for (mut physics, position, dimensions) in query.iter_mut() {
217 let bounding_box = dimensions.make_bounding_box(**position);
218 physics.bounding_box = bounding_box;
219 }
220}
221
222#[allow(clippy::type_complexity)]
223pub fn update_dimensions(
224 mut query: Query<
225 (&mut EntityDimensions, &EntityKindComponent, &Pose),
226 Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
227 >,
228) {
229 for (mut dimensions, kind, pose) in query.iter_mut() {
230 *dimensions = calculate_dimensions(**kind, *pose);
231 }
232}
233
234#[allow(clippy::type_complexity)]
235pub fn update_crouching(
236 query: Query<(&mut Crouching, &Pose), (Without<LocalEntity>, With<Player>)>,
237) {
238 for (mut crouching, pose) in query {
239 let new_crouching = *pose == Pose::Crouching;
240 if **crouching != new_crouching {
242 **crouching = new_crouching;
243 }
244 }
245}
246
247#[derive(Clone, Component, Copy, Debug)]
253pub struct InLoadedChunk;
254
255pub fn update_in_loaded_chunk(
257 mut commands: bevy_ecs::system::Commands,
258 query: Query<(Entity, &WorldName, &Position, Option<&InLoadedChunk>)>,
259 worlds: Res<Worlds>,
260) {
261 for (entity, world_name, position, last_in_loaded_chunk) in &query {
262 let player_chunk_pos = ChunkPos::from(position);
263 let Some(world_lock) = worlds.get(world_name) else {
264 commands.entity(entity).remove::<InLoadedChunk>();
265 continue;
266 };
267
268 let in_loaded_chunk = world_lock.read().chunks.get(&player_chunk_pos).is_some();
269 if in_loaded_chunk {
270 if last_in_loaded_chunk.is_none() {
271 commands.entity(entity).insert(InLoadedChunk);
272 }
273 } else {
274 if last_in_loaded_chunk.is_some() {
275 commands.entity(entity).remove::<InLoadedChunk>();
276 }
277 }
278 }
279}
280
281pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos {
283 on_pos(0.2, chunk_storage, position)
284}
285
286pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: Position) -> BlockPos {
299 let x = pos.x.floor() as i32;
300 let y = (pos.y - offset as f64).floor() as i32;
301 let z = pos.z.floor() as i32;
302 let pos = BlockPos { x, y, z };
303
304 let block_pos = pos.down(1);
306 let block_state = chunk_storage.get_block_state(block_pos);
307 if block_state == Some(BlockState::AIR) {
308 let block_pos_below = block_pos.down(1);
309 let block_state_below = chunk_storage.get_block_state(block_pos_below);
310 if let Some(_block_state_below) = block_state_below {
311 }
318 }
319
320 pos
321}
322
323#[cfg(test)]
324mod tests {
325 use azalea_block::{
326 blocks::{Ladder, OakTrapdoor},
327 properties::{FacingCardinal, TopBottom},
328 };
329 use azalea_core::position::{BlockPos, ChunkPos};
330 use azalea_registry::builtin::BlockKind;
331 use azalea_world::{Chunk, ChunkStorage, PartialWorld, World};
332
333 use super::is_trapdoor_usable_as_ladder;
334
335 #[test]
336 fn test_is_trapdoor_useable_as_ladder() {
337 let mut partial_world = PartialWorld::default();
338 let mut chunks = ChunkStorage::default();
339 partial_world.chunks.set(
340 &ChunkPos { x: 0, z: 0 },
341 Some(Chunk::default()),
342 &mut chunks,
343 );
344 partial_world.chunks.set_block_state(
345 BlockPos::new(0, 0, 0),
346 BlockKind::Stone.into(),
347 &chunks,
348 );
349
350 let ladder = Ladder {
351 facing: FacingCardinal::East,
352 waterlogged: false,
353 };
354 partial_world
355 .chunks
356 .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
357
358 let trapdoor = OakTrapdoor {
359 facing: FacingCardinal::East,
360 half: TopBottom::Bottom,
361 open: true,
362 powered: false,
363 waterlogged: false,
364 };
365 partial_world
366 .chunks
367 .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
368
369 let world = World::from(chunks);
370 let trapdoor_matches_ladder = is_trapdoor_usable_as_ladder(
371 world
372 .get_block_state(BlockPos::new(0, 1, 0))
373 .unwrap_or_default(),
374 BlockPos::new(0, 1, 0),
375 &world,
376 );
377
378 assert!(trapdoor_matches_ladder);
379 }
380}