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 Crouching, Dead, EntityKindComponent, FluidOnEyes, LocalEntity, LookDirection, OnClimbable,
21 Physics, Pose, Position,
22 dimensions::{EntityDimensions, calculate_dimensions},
23 metadata::Health,
24};
25
26#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
28pub enum EntityUpdateSystems {
29 Index,
31 Deindex,
33}
34
35pub struct EntityPlugin;
37impl Plugin for EntityPlugin {
38 fn build(&self, app: &mut App) {
39 app.add_systems(
44 PostUpdate,
45 indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSystems::Deindex),
46 )
47 .add_systems(
48 Update,
49 (
50 (
51 indexing::update_entity_chunk_positions,
52 indexing::insert_entity_chunk_position,
53 )
54 .chain()
55 .in_set(EntityUpdateSystems::Index),
56 (
57 relative_updates::debug_detect_updates_received_on_local_entities,
58 debug_new_entity,
59 add_dead,
60 clamp_look_direction,
61 update_on_climbable,
62 (update_dimensions, update_bounding_box, update_fluid_on_eyes).chain(),
63 update_crouching,
64 ),
65 ),
66 )
67 .add_systems(GameTick, update_in_loaded_chunk)
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,
99 &Position,
100 &EntityDimensions,
101 &InstanceName,
102 )>,
103 instance_container: Res<InstanceContainer>,
104) {
105 for (mut fluid_on_eyes, position, dimensions, instance_name) in query.iter_mut() {
106 let Some(instance) = instance_container.get(instance_name) else {
107 continue;
108 };
109
110 let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
111 let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z));
112 let fluid_at_eye = instance
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, &InstanceName), With<LocalEntity>>,
127 instance_container: Res<InstanceContainer>,
128) {
129 for (mut on_climbable, position, instance_name) in query.iter_mut() {
130 let Some(instance) = instance_container.get(instance_name) else {
139 continue;
140 };
141
142 let instance = instance.read();
143
144 let block_pos = BlockPos::from(position);
145 let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default();
146 let block_at_feet = Box::<dyn azalea_block::BlockTrait>::from(block_state_at_feet);
147 let registry_block_at_feet = block_at_feet.as_registry_block();
148
149 **on_climbable = azalea_registry::tags::blocks::CLIMBABLE.contains(®istry_block_at_feet)
150 || (azalea_registry::tags::blocks::TRAPDOORS.contains(®istry_block_at_feet)
151 && is_trapdoor_useable_as_ladder(block_state_at_feet, block_pos, &instance));
152 }
153}
154
155fn is_trapdoor_useable_as_ladder(
156 block_state: BlockState,
157 block_pos: BlockPos,
158 instance: &azalea_world::Instance,
159) -> bool {
160 if !block_state
162 .property::<azalea_block::properties::Open>()
163 .unwrap_or_default()
164 {
165 return false;
166 }
167
168 let block_below = instance
170 .get_block_state(block_pos.down(1))
171 .unwrap_or_default();
172 let registry_block_below =
173 Box::<dyn azalea_block::BlockTrait>::from(block_below).as_registry_block();
174 if registry_block_below != azalea_registry::Block::Ladder {
175 return false;
176 }
177 let ladder_facing = block_below
179 .property::<azalea_block::properties::FacingCardinal>()
180 .expect("ladder block must have facing property");
181 let trapdoor_facing = block_state
182 .property::<azalea_block::properties::FacingCardinal>()
183 .expect("trapdoor block must have facing property");
184 if ladder_facing != trapdoor_facing {
185 return false;
186 }
187
188 true
189}
190
191#[derive(Component, Clone, Deref, DerefMut)]
196pub struct LoadedBy(pub HashSet<Entity>);
197
198pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
199 for mut look_direction in &mut query {
200 *look_direction = apply_clamp_look_direction(*look_direction);
201 }
202}
203pub fn apply_clamp_look_direction(mut look_direction: LookDirection) -> LookDirection {
204 look_direction.x_rot = look_direction.x_rot.clamp(-90., 90.);
205
206 look_direction
207}
208
209#[allow(clippy::type_complexity)]
218pub fn update_bounding_box(
219 mut query: Query<
220 (&mut Physics, &Position, &EntityDimensions),
221 Or<(Changed<Position>, Changed<EntityDimensions>)>,
222 >,
223) {
224 for (mut physics, position, dimensions) in query.iter_mut() {
225 let bounding_box = dimensions.make_bounding_box(**position);
226 physics.bounding_box = bounding_box;
227 }
228}
229
230#[allow(clippy::type_complexity)]
231pub fn update_dimensions(
232 mut query: Query<
233 (&mut EntityDimensions, &EntityKindComponent, &Pose),
234 Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
235 >,
236) {
237 for (mut dimensions, kind, pose) in query.iter_mut() {
238 *dimensions = calculate_dimensions(**kind, *pose);
239 }
240}
241
242pub fn update_crouching(query: Query<(&mut Crouching, &Pose), Without<LocalEntity>>) {
243 for (mut crouching, pose) in query {
244 let new_crouching = *pose == Pose::Crouching;
245 if **crouching != new_crouching {
247 **crouching = new_crouching;
248 }
249 }
250}
251
252#[derive(Component, Clone, Debug, Copy)]
258pub struct InLoadedChunk;
259
260pub fn update_in_loaded_chunk(
262 mut commands: bevy_ecs::system::Commands,
263 query: Query<(Entity, &InstanceName, &Position)>,
264 instance_container: Res<InstanceContainer>,
265) {
266 for (entity, instance_name, position) in &query {
267 let player_chunk_pos = ChunkPos::from(position);
268 let Some(instance_lock) = instance_container.get(instance_name) else {
269 commands.entity(entity).remove::<InLoadedChunk>();
270 continue;
271 };
272
273 let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
274 if in_loaded_chunk {
275 commands.entity(entity).insert(InLoadedChunk);
276 } else {
277 commands.entity(entity).remove::<InLoadedChunk>();
278 }
279 }
280}
281
282#[derive(Component)]
286pub struct HasClientLoaded;
287
288#[cfg(test)]
289mod tests {
290 use azalea_block::{
291 blocks::{Ladder, OakTrapdoor},
292 properties::{FacingCardinal, TopBottom},
293 };
294 use azalea_core::position::{BlockPos, ChunkPos};
295 use azalea_world::{Chunk, ChunkStorage, Instance, PartialInstance};
296
297 use super::is_trapdoor_useable_as_ladder;
298
299 #[test]
300 fn test_is_trapdoor_useable_as_ladder() {
301 let mut partial_instance = PartialInstance::default();
302 let mut chunks = ChunkStorage::default();
303 partial_instance.chunks.set(
304 &ChunkPos { x: 0, z: 0 },
305 Some(Chunk::default()),
306 &mut chunks,
307 );
308 partial_instance.chunks.set_block_state(
309 BlockPos::new(0, 0, 0),
310 azalea_registry::Block::Stone.into(),
311 &chunks,
312 );
313
314 let ladder = Ladder {
315 facing: FacingCardinal::East,
316 waterlogged: false,
317 };
318 partial_instance
319 .chunks
320 .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
321
322 let trapdoor = OakTrapdoor {
323 facing: FacingCardinal::East,
324 half: TopBottom::Bottom,
325 open: true,
326 powered: false,
327 waterlogged: false,
328 };
329 partial_instance
330 .chunks
331 .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
332
333 let instance = Instance::from(chunks);
334 let trapdoor_matches_ladder = is_trapdoor_useable_as_ladder(
335 instance
336 .get_block_state(BlockPos::new(0, 1, 0))
337 .unwrap_or_default(),
338 BlockPos::new(0, 1, 0),
339 &instance,
340 );
341
342 assert!(trapdoor_matches_ladder);
343 }
344}