azalea/pathfinder/
debug.rs

1use azalea_client::{InstanceHolder, chat::SendChatEvent};
2use azalea_core::position::Vec3;
3use bevy_ecs::prelude::*;
4
5use super::ExecutingPath;
6
7/// A component that makes bots run /particle commands while pathfinding to show
8/// where they're going. This requires the bots to have server operator
9/// permissions, and it'll make them spam *a lot* of commands.
10///
11/// ```
12/// # use azalea::prelude::*;
13/// # use azalea::pathfinder::debug::PathfinderDebugParticles;
14/// # #[derive(Component, Clone, Default)]
15/// # pub struct State;
16///
17/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
18///     match event {
19///         azalea::Event::Init => {
20///             bot.ecs
21///                 .lock()
22///                 .entity_mut(bot.entity)
23///                 .insert(PathfinderDebugParticles);
24///         }
25///         _ => {}
26///     }
27///     Ok(())
28/// }
29/// ```
30#[derive(Component)]
31pub struct PathfinderDebugParticles;
32
33pub fn debug_render_path_with_particles(
34    mut query: Query<(Entity, &ExecutingPath, &InstanceHolder), With<PathfinderDebugParticles>>,
35    // chat_events is Option because the tests don't have SendChatEvent
36    // and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter>
37    chat_events: Option<ResMut<Events<SendChatEvent>>>,
38    mut tick_count: Local<usize>,
39) {
40    let Some(mut chat_events) = chat_events else {
41        return;
42    };
43    if *tick_count >= 2 {
44        *tick_count = 0;
45    } else {
46        *tick_count += 1;
47        return;
48    }
49    for (entity, executing_path, instance_holder) in &mut query {
50        if executing_path.path.is_empty() {
51            continue;
52        }
53
54        let chunks = &instance_holder.instance.read().chunks;
55
56        let mut start = executing_path.last_reached_node;
57        for (i, edge) in executing_path.path.iter().enumerate() {
58            let movement = &edge.movement;
59            let end = movement.target;
60
61            let start_vec3 = start.center();
62            let end_vec3 = end.center();
63
64            let step_count = (start_vec3.distance_squared_to(&end_vec3).sqrt() * 4.0) as usize;
65
66            let target_block_state = chunks.get_block_state(&movement.target).unwrap_or_default();
67            let above_target_block_state = chunks
68                .get_block_state(&movement.target.up(1))
69                .unwrap_or_default();
70            // this isn't foolproof, there might be another block that could be mined
71            // depending on the move, but it's good enough for debugging
72            // purposes
73            let is_mining = !super::world::is_block_state_passable(target_block_state)
74                || !super::world::is_block_state_passable(above_target_block_state);
75
76            let (r, g, b): (f64, f64, f64) = if i == 0 {
77                (0., 1., 0.)
78            } else if is_mining {
79                (1., 0., 0.)
80            } else {
81                (0., 1., 1.)
82            };
83
84            // interpolate between the start and end positions
85            for i in 0..step_count {
86                let percent = i as f64 / step_count as f64;
87                let pos = Vec3 {
88                    x: start_vec3.x + (end_vec3.x - start_vec3.x) * percent,
89                    y: start_vec3.y + (end_vec3.y - start_vec3.y) * percent,
90                    z: start_vec3.z + (end_vec3.z - start_vec3.z) * percent,
91                };
92                let particle_command = format!(
93                    "/particle dust{{color:[{r},{g},{b}],scale:{size}}} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
94                    size = 1,
95                    start_x = pos.x,
96                    start_y = pos.y,
97                    start_z = pos.z,
98                    delta_x = 0,
99                    delta_y = 0,
100                    delta_z = 0,
101                    count = 1
102                );
103                chat_events.send(SendChatEvent {
104                    entity,
105                    content: particle_command,
106                });
107            }
108
109            start = movement.target;
110        }
111    }
112}