azalea/pathfinder/
debug.rs

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