azalea/pathfinder/moves/
mod.rs

1pub mod basic;
2pub mod parkour;
3
4use std::{
5    fmt::{self, Debug},
6    sync::Arc,
7};
8
9use azalea_block::BlockState;
10use azalea_client::{
11    SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
12    inventory::SetSelectedHotbarSlotEvent, mining::StartMiningBlockEvent,
13};
14use azalea_core::position::{BlockPos, Vec3};
15use azalea_inventory::Menu;
16use azalea_world::Instance;
17use bevy_ecs::{entity::Entity, message::MessageWriter, system::Commands};
18use parking_lot::RwLock;
19use tracing::debug;
20
21use super::{
22    astar,
23    custom_state::CustomPathfinderStateRef,
24    mining::MiningCache,
25    rel_block_pos::RelBlockPos,
26    world::{CachedWorld, is_block_state_passable},
27};
28use crate::{
29    auto_tool::best_tool_in_hotbar_for_block,
30    bot::{JumpEvent, LookAtEvent},
31    pathfinder::player_pos_to_block_pos,
32};
33
34type Edge = astar::Edge<RelBlockPos, MoveData>;
35
36pub type SuccessorsFn = fn(&mut PathfinderCtx, RelBlockPos);
37
38pub fn default_move(ctx: &mut PathfinderCtx, node: RelBlockPos) {
39    basic::basic_move(ctx, node);
40    parkour::parkour_move(ctx, node);
41}
42
43#[derive(Clone)]
44pub struct MoveData {
45    /// Use the context to determine what events should be sent to complete this
46    /// movement.
47    pub execute: &'static (dyn Fn(ExecuteCtx) + Send + Sync),
48    /// Whether we've reached the target.
49    pub is_reached: &'static (dyn Fn(IsReachedCtx) -> bool + Send + Sync),
50}
51impl Debug for MoveData {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.debug_struct("MoveData")
54            // .field("move_kind", &self.move_kind)
55            .finish()
56    }
57}
58
59pub struct ExecuteCtx<'s, 'w1, 'w2, 'w3, 'w4, 'w5, 'w6, 'a> {
60    pub entity: Entity,
61    /// The node that we're trying to reach.
62    pub target: BlockPos,
63    /// The last node that we reached.
64    pub start: BlockPos,
65    pub position: Vec3,
66    pub physics: &'a azalea_entity::Physics,
67    pub is_currently_mining: bool,
68    pub instance: Arc<RwLock<Instance>>,
69    pub menu: Menu,
70
71    pub commands: &'a mut Commands<'w1, 's>,
72    pub look_at_events: &'a mut MessageWriter<'w2, LookAtEvent>,
73    pub sprint_events: &'a mut MessageWriter<'w3, StartSprintEvent>,
74    pub walk_events: &'a mut MessageWriter<'w4, StartWalkEvent>,
75    pub jump_events: &'a mut MessageWriter<'w5, JumpEvent>,
76    pub start_mining_events: &'a mut MessageWriter<'w6, StartMiningBlockEvent>,
77}
78
79impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_, '_> {
80    pub fn look_at(&mut self, position: Vec3) {
81        self.look_at_events.write(LookAtEvent {
82            entity: self.entity,
83            position: Vec3 {
84                x: position.x,
85                // look forward
86                y: self.position.up(1.53).y,
87                z: position.z,
88            },
89        });
90    }
91
92    pub fn look_at_exact(&mut self, position: Vec3) {
93        self.look_at_events.write(LookAtEvent {
94            entity: self.entity,
95            position,
96        });
97    }
98
99    pub fn sprint(&mut self, direction: SprintDirection) {
100        self.sprint_events.write(StartSprintEvent {
101            entity: self.entity,
102            direction,
103        });
104    }
105
106    pub fn walk(&mut self, direction: WalkDirection) {
107        self.walk_events.write(StartWalkEvent {
108            entity: self.entity,
109            direction,
110        });
111    }
112
113    pub fn jump(&mut self) {
114        self.jump_events.write(JumpEvent {
115            entity: self.entity,
116        });
117    }
118
119    pub fn jump_if_in_water(&mut self) {
120        if self.physics.is_in_water() {
121            self.jump();
122        }
123    }
124
125    /// Returns whether this block could be mined.
126    pub fn should_mine(&mut self, block: BlockPos) -> bool {
127        let block_state = self
128            .instance
129            .read()
130            .get_block_state(block)
131            .unwrap_or_default();
132        if is_block_state_passable(block_state) {
133            // block is already passable, no need to mine it
134            return false;
135        }
136
137        true
138    }
139
140    /// Mine the block at the given position. Returns whether the block is being
141    /// mined.
142    pub fn mine(&mut self, block: BlockPos) -> bool {
143        let block_state = self
144            .instance
145            .read()
146            .get_block_state(block)
147            .unwrap_or_default();
148        if is_block_state_passable(block_state) {
149            // block is already passable, no need to mine it
150            return false;
151        }
152
153        let best_tool_result = best_tool_in_hotbar_for_block(block_state, &self.menu);
154        debug!("best tool for {block_state:?}: {best_tool_result:?}");
155
156        self.commands.trigger(SetSelectedHotbarSlotEvent {
157            entity: self.entity,
158            slot: best_tool_result.index as u8,
159        });
160
161        self.is_currently_mining = true;
162
163        self.walk(WalkDirection::None);
164        self.look_at_exact(block.center());
165        self.start_mining_events.write(StartMiningBlockEvent {
166            entity: self.entity,
167            position: block,
168            force: true,
169        });
170
171        true
172    }
173
174    /// Mine the given block, but make sure the player is standing at the start
175    /// of the current node first.
176    pub fn mine_while_at_start(&mut self, block: BlockPos) -> bool {
177        let horizontal_distance_from_start = (self.start.center() - self.position)
178            .horizontal_distance_squared()
179            .sqrt();
180        let at_start_position = player_pos_to_block_pos(self.position) == self.start
181            && horizontal_distance_from_start < 0.25;
182
183        if self.should_mine(block) {
184            if at_start_position {
185                self.look_at(block.center());
186                self.mine(block);
187            } else {
188                self.look_at(self.start.center());
189                self.walk(WalkDirection::Forward);
190            }
191            true
192        } else {
193            false
194        }
195    }
196
197    pub fn get_block_state(&self, block: BlockPos) -> BlockState {
198        self.instance
199            .read()
200            .get_block_state(block)
201            .unwrap_or_default()
202    }
203}
204
205pub struct IsReachedCtx<'a> {
206    /// The node that we're trying to reach.
207    pub target: BlockPos,
208    /// The last node that we reached.
209    pub start: BlockPos,
210    pub position: Vec3,
211    pub physics: &'a azalea_entity::Physics,
212}
213
214/// Returns whether the entity is at the node and should start going to the
215/// next node.
216#[must_use]
217pub fn default_is_reached(
218    IsReachedCtx {
219        position,
220        target,
221        physics,
222        ..
223    }: IsReachedCtx,
224) -> bool {
225    if BlockPos::from(position) == target {
226        return true;
227    }
228
229    // this is to make it handle things like slabs correctly, if we're on the block
230    // below the target but on_ground
231    BlockPos::from(position).up(1) == target && physics.on_ground()
232}
233
234pub struct PathfinderCtx<'a> {
235    pub edges: &'a mut Vec<Edge>,
236    pub world: &'a CachedWorld,
237    pub mining_cache: &'a MiningCache,
238
239    pub custom_state: &'a CustomPathfinderStateRef,
240}