1mod components;
2pub mod indexing;
3mod relative_updates;
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;
20pub use relative_updates::RelativeEntityUpdate;
21use tracing::debug;
22
23use crate::{
24 FluidOnEyes, LookDirection, Physics, Pose, Position,
25 dimensions::{EntityDimensions, calculate_dimensions},
26 metadata::Health,
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 relative_updates::debug_detect_updates_received_on_local_entities,
61 debug_new_entity,
62 add_dead,
63 clamp_look_direction,
64 update_on_climbable,
65 (update_dimensions, update_bounding_box).chain(),
66 update_crouching,
67 ),
68 ),
69 )
70 .add_systems(GameTick, (update_in_loaded_chunk, update_fluid_on_eyes))
71 .init_resource::<EntityUuidIndex>();
72 }
73}
74
75fn debug_new_entity(query: Query<(Entity, Option<&LocalEntity>), Added<MinecraftEntityId>>) {
76 for (entity, local) in query.iter() {
77 if local.is_some() {
78 debug!("new local entity: {:?}", entity);
79 } else {
80 debug!("new entity: {:?}", entity);
81 }
82 }
83}
84
85pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
92 for (entity, health) in query.iter() {
93 if **health <= 0.0 {
94 commands.entity(entity).insert(Dead);
95 }
96 }
97}
98
99pub fn update_fluid_on_eyes(
100 mut query: Query<(&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName)>,
101 worlds: Res<Worlds>,
102) {
103 for (mut fluid_on_eyes, position, dimensions, world_name) in query.iter_mut() {
104 let Some(world) = worlds.get(world_name) else {
105 continue;
106 };
107
108 let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
109 let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
110 let fluid_at_eye = world
111 .read()
112 .get_fluid_state(eye_block_pos)
113 .unwrap_or_default();
114 let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
115 if fluid_cutoff_y > adjusted_eye_y {
116 **fluid_on_eyes = fluid_at_eye.kind;
117 } else {
118 **fluid_on_eyes = FluidKind::Empty;
119 }
120 }
121}
122
123pub fn update_on_climbable(
124 mut query: Query<(&mut OnClimbable, &Position, &WorldName), With<LocalEntity>>,
125 worlds: Res<Worlds>,
126) {
127 for (mut on_climbable, position, world_name) in query.iter_mut() {
128 let Some(world) = worlds.get(world_name) else {
137 continue;
138 };
139
140 let world = world.read();
141
142 let block_pos = BlockPos::from(position);
143 let block_state_at_feet = world.get_block_state(block_pos).unwrap_or_default();
144 let block_at_feet = Box::<dyn BlockTrait>::from(block_state_at_feet);
145 let registry_block_at_feet = block_at_feet.as_registry_block();
146
147 **on_climbable = tags::blocks::CLIMBABLE.contains(®istry_block_at_feet)
148 || (tags::blocks::TRAPDOORS.contains(®istry_block_at_feet)
149 && is_trapdoor_usable_as_ladder(block_state_at_feet, block_pos, &world));
150 }
151}
152
153fn is_trapdoor_usable_as_ladder(
154 block_state: BlockState,
155 block_pos: BlockPos,
156 world: &azalea_world::World,
157) -> bool {
158 if !block_state
160 .property::<properties::Open>()
161 .unwrap_or_default()
162 {
163 return false;
164 }
165
166 let block_below = world.get_block_state(block_pos.down(1)).unwrap_or_default();
168 let registry_block_below = Box::<dyn BlockTrait>::from(block_below).as_registry_block();
169 if registry_block_below != BlockKind::Ladder {
170 return false;
171 }
172 let ladder_facing = block_below
174 .property::<properties::FacingCardinal>()
175 .expect("ladder block must have facing property");
176 let trapdoor_facing = block_state
177 .property::<properties::FacingCardinal>()
178 .expect("trapdoor block must have facing property");
179 if ladder_facing != trapdoor_facing {
180 return false;
181 }
182
183 true
184}
185
186#[derive(Clone, Component, Deref, DerefMut)]
191pub struct LoadedBy(pub HashSet<Entity>);
192
193pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
194 for mut look_direction in &mut query {
195 *look_direction = look_direction.clamped();
196 }
197}
198
199#[allow(clippy::type_complexity)]
208pub fn update_bounding_box(
209 mut query: Query<
210 (&mut Physics, &Position, &EntityDimensions),
211 Or<(Changed<Position>, Changed<EntityDimensions>)>,
212 >,
213) {
214 for (mut physics, position, dimensions) in query.iter_mut() {
215 let bounding_box = dimensions.make_bounding_box(**position);
216 physics.bounding_box = bounding_box;
217 }
218}
219
220#[allow(clippy::type_complexity)]
221pub fn update_dimensions(
222 mut query: Query<
223 (&mut EntityDimensions, &EntityKindComponent, &Pose),
224 Or<(Changed<EntityKindComponent>, Changed<Pose>)>,
225 >,
226) {
227 for (mut dimensions, kind, pose) in query.iter_mut() {
228 *dimensions = calculate_dimensions(**kind, *pose);
229 }
230}
231
232pub fn update_crouching(query: Query<(&mut Crouching, &Pose), Without<LocalEntity>>) {
233 for (mut crouching, pose) in query {
234 let new_crouching = *pose == Pose::Crouching;
235 if **crouching != new_crouching {
237 **crouching = new_crouching;
238 }
239 }
240}
241
242#[derive(Clone, Component, Copy, Debug)]
248pub struct InLoadedChunk;
249
250pub fn update_in_loaded_chunk(
252 mut commands: bevy_ecs::system::Commands,
253 query: Query<(Entity, &WorldName, &Position)>,
254 worlds: Res<Worlds>,
255) {
256 for (entity, world_name, position) in &query {
257 let player_chunk_pos = ChunkPos::from(position);
258 let Some(world_lock) = worlds.get(world_name) else {
259 commands.entity(entity).remove::<InLoadedChunk>();
260 continue;
261 };
262
263 let in_loaded_chunk = world_lock.read().chunks.get(&player_chunk_pos).is_some();
264 if in_loaded_chunk {
265 commands.entity(entity).insert(InLoadedChunk);
266 } else {
267 commands.entity(entity).remove::<InLoadedChunk>();
268 }
269 }
270}
271
272pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos {
274 on_pos(0.2, chunk_storage, position)
275}
276
277pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: Position) -> BlockPos {
290 let x = pos.x.floor() as i32;
291 let y = (pos.y - offset as f64).floor() as i32;
292 let z = pos.z.floor() as i32;
293 let pos = BlockPos { x, y, z };
294
295 let block_pos = pos.down(1);
297 let block_state = chunk_storage.get_block_state(block_pos);
298 if block_state == Some(BlockState::AIR) {
299 let block_pos_below = block_pos.down(1);
300 let block_state_below = chunk_storage.get_block_state(block_pos_below);
301 if let Some(_block_state_below) = block_state_below {
302 }
309 }
310
311 pos
312}
313
314#[cfg(test)]
315mod tests {
316 use azalea_block::{
317 blocks::{Ladder, OakTrapdoor},
318 properties::{FacingCardinal, TopBottom},
319 };
320 use azalea_core::position::{BlockPos, ChunkPos};
321 use azalea_registry::builtin::BlockKind;
322 use azalea_world::{Chunk, ChunkStorage, PartialWorld, World};
323
324 use super::is_trapdoor_usable_as_ladder;
325
326 #[test]
327 fn test_is_trapdoor_useable_as_ladder() {
328 let mut partial_world = PartialWorld::default();
329 let mut chunks = ChunkStorage::default();
330 partial_world.chunks.set(
331 &ChunkPos { x: 0, z: 0 },
332 Some(Chunk::default()),
333 &mut chunks,
334 );
335 partial_world.chunks.set_block_state(
336 BlockPos::new(0, 0, 0),
337 BlockKind::Stone.into(),
338 &chunks,
339 );
340
341 let ladder = Ladder {
342 facing: FacingCardinal::East,
343 waterlogged: false,
344 };
345 partial_world
346 .chunks
347 .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks);
348
349 let trapdoor = OakTrapdoor {
350 facing: FacingCardinal::East,
351 half: TopBottom::Bottom,
352 open: true,
353 powered: false,
354 waterlogged: false,
355 };
356 partial_world
357 .chunks
358 .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks);
359
360 let world = World::from(chunks);
361 let trapdoor_matches_ladder = is_trapdoor_usable_as_ladder(
362 world
363 .get_block_state(BlockPos::new(0, 1, 0))
364 .unwrap_or_default(),
365 BlockPos::new(0, 1, 0),
366 &world,
367 );
368
369 assert!(trapdoor_matches_ladder);
370 }
371}