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 EntityUpdateSet {
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(EntityUpdateSet::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(EntityUpdateSet::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>>) {
88 for (entity, health) in query.iter() {
89 if **health <= 0.0 {
90 commands.entity(entity).insert(Dead);
91 }
92 }
93}
94
95pub fn update_fluid_on_eyes(
96 mut query: Query<(
97 &mut FluidOnEyes,
98 &Position,
99 &EntityDimensions,
100 &InstanceName,
101 )>,
102 instance_container: Res<InstanceContainer>,
103) {
104 for (mut fluid_on_eyes, position, dimensions, instance_name) in query.iter_mut() {
105 let Some(instance) = instance_container.get(instance_name) else {
106 continue;
107 };
108
109 let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
110 let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z));
111 let fluid_at_eye = instance
112 .read()
113 .get_fluid_state(eye_block_pos)
114 .unwrap_or_default();
115 let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
116 if fluid_cutoff_y > adjusted_eye_y {
117 **fluid_on_eyes = fluid_at_eye.kind;
118 } else {
119 **fluid_on_eyes = FluidKind::Empty;
120 }
121 }
122}
123
124pub fn update_on_climbable(
125 mut query: Query<(&mut OnClimbable, &Position, &InstanceName), With<LocalEntity>>,
126 instance_container: Res<InstanceContainer>,
127) {
128 for (mut on_climbable, position, instance_name) in query.iter_mut() {
129 let Some(instance) = instance_container.get(instance_name) else {
138 continue;
139 };
140
141 let instance = instance.read();
142
143 let block_pos = BlockPos::from(position);
144 let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default();
145 let block_at_feet = Box::<dyn azalea_block::BlockTrait>::from(block_state_at_feet);
146 let registry_block_at_feet = block_at_feet.as_registry_block();
147
148 **on_climbable = azalea_registry::tags::blocks::CLIMBABLE.contains(®istry_block_at_feet)
149 || (azalea_registry::tags::blocks::TRAPDOORS.contains(®istry_block_at_feet)
150 && is_trapdoor_useable_as_ladder(block_state_at_feet, block_pos, &instance));
151 }
152}
153
154fn is_trapdoor_useable_as_ladder(
155 block_state: BlockState,
156 block_pos: BlockPos,
157 instance: &azalea_world::Instance,
158) -> bool {
159 if !block_state
161 .property::<azalea_block::properties::Open>()
162 .unwrap_or_default()
163 {
164 return false;
165 }
166
167 let block_below = instance
169 .get_block_state(block_pos.down(1))
170 .unwrap_or_default();
171 let registry_block_below =
172 Box::<dyn azalea_block::BlockTrait>::from(block_below).as_registry_block();
173 if registry_block_below != azalea_registry::Block::Ladder {
174 return false;
175 }
176 let ladder_facing = block_below
178 .property::<azalea_block::properties::FacingCardinal>()
179 .expect("ladder block must have facing property");
180 let trapdoor_facing = block_state
181 .property::<azalea_block::properties::FacingCardinal>()
182 .expect("trapdoor block must have facing property");
183 if ladder_facing != trapdoor_facing {
184 return false;
185 }
186
187 true
188}
189
190#[derive(Component, Clone, 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 = apply_clamp_look_direction(*look_direction);
198 }
199}
200pub fn apply_clamp_look_direction(mut look_direction: LookDirection) -> LookDirection {
201 look_direction.x_rot = look_direction.x_rot.clamp(-90., 90.);
202
203 look_direction
204}
205
206#[allow(clippy::type_complexity)]
212pub fn update_bounding_box(
213 mut query: Query<
214 (&mut Physics, &Position, &EntityDimensions),
215 Or<(Changed<Position>, Changed<EntityDimensions>)>,
216 >,
217) {
218 for (mut physics, position, dimensions) in query.iter_mut() {
219 let bounding_box = dimensions.make_bounding_box(**position);
220 physics.bounding_box = bounding_box;
221 }
222}
223
224#[allow(clippy::type_complexity)]
225pub fn update_dimensions(
226 mut query: Query<
227 (&mut EntityDimensions, &EntityKindComponent, &Pose),
228 Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
229 >,
230) {
231 for (mut dimensions, kind, pose) in query.iter_mut() {
232 *dimensions = calculate_dimensions(**kind, *pose);
233 }
234}
235
236pub fn update_crouching(query: Query<(&mut Crouching, &Pose), Without<LocalEntity>>) {
237 for (mut crouching, pose) in query {
238 let new_crouching = *pose == Pose::Crouching;
239 if **crouching != new_crouching {
241 **crouching = new_crouching;
242 }
243 }
244}
245
246#[derive(Component, Clone, Debug, Copy)]
252pub struct InLoadedChunk;
253
254pub fn update_in_loaded_chunk(
256 mut commands: bevy_ecs::system::Commands,
257 query: Query<(Entity, &InstanceName, &Position)>,
258 instance_container: Res<InstanceContainer>,
259) {
260 for (entity, instance_name, position) in &query {
261 let player_chunk_pos = ChunkPos::from(position);
262 let Some(instance_lock) = instance_container.get(instance_name) else {
263 commands.entity(entity).remove::<InLoadedChunk>();
264 continue;
265 };
266
267 let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
268 if in_loaded_chunk {
269 commands.entity(entity).insert(InLoadedChunk);
270 } else {
271 commands.entity(entity).remove::<InLoadedChunk>();
272 }
273 }
274}
275
276#[derive(Component)]
280pub struct HasClientLoaded;
281
282#[cfg(test)]
283mod tests {
284 use azalea_block::{
285 blocks::{Ladder, OakTrapdoor},
286 properties::{FacingCardinal, TopBottom},
287 };
288 use azalea_core::position::{BlockPos, ChunkPos};
289 use azalea_world::{Chunk, ChunkStorage, Instance, PartialInstance};
290
291 use super::is_trapdoor_useable_as_ladder;
292
293 #[test]
294 fn test_is_trapdoor_useable_as_ladder() {
295 let mut partial_instance = PartialInstance::default();
296 let mut chunks = ChunkStorage::default();
297 partial_instance.chunks.set(
298 &ChunkPos { x: 0, z: 0 },
299 Some(Chunk::default()),
300 &mut chunks,
301 );
302 partial_instance.chunks.set_block_state(
303 BlockPos::new(0, 0, 0),
304 azalea_registry::Block::Stone.into(),
305 &chunks,
306 );
307
308 let ladder = Ladder {
309 facing: FacingCardinal::East,
310 waterlogged: false,
311 };
312 partial_instance
313 .chunks
314 .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
315
316 let trapdoor = OakTrapdoor {
317 facing: FacingCardinal::East,
318 half: TopBottom::Bottom,
319 open: true,
320 powered: false,
321 waterlogged: false,
322 };
323 partial_instance
324 .chunks
325 .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
326
327 let instance = Instance::from(chunks);
328 let trapdoor_matches_ladder = is_trapdoor_useable_as_ladder(
329 instance
330 .get_block_state(BlockPos::new(0, 1, 0))
331 .unwrap_or_default(),
332 BlockPos::new(0, 1, 0),
333 &instance,
334 );
335
336 assert!(trapdoor_matches_ladder);
337 }
338}