Skip to main content

azalea/pathfinder/
debug.rs

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