azalea/pathfinder/execute/
simulation.rs

1//! An alternative execution engine for the pathfinder that attempts to skip
2//! nodes in the path by running simulations.
3//!
4//! See [`SimulationPathfinderExecutionPlugin`] for more information.
5
6use std::{borrow::Cow, time::Instant};
7
8use azalea_client::{
9    PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent,
10    local_player::WorldHolder,
11    mining::{Mining, MiningSystems, StartMiningBlockEvent},
12};
13use azalea_core::{position::BlockPos, tick::GameTick};
14use azalea_entity::{Attributes, LookDirection, Physics, Position, inventory::Inventory};
15use azalea_physics::PhysicsSystems;
16use bevy_app::{App, Plugin};
17use bevy_ecs::{prelude::*, system::SystemState};
18use tracing::{debug, trace};
19
20use crate::{
21    WalkDirection,
22    bot::{JumpEvent, LookAtEvent, direction_looking_at},
23    ecs::{
24        entity::Entity,
25        system::{Commands, Query},
26    },
27    pathfinder::{
28        ExecutingPath,
29        debug::debug_render_path_with_particles,
30        moves::{ExecuteCtx, IsReachedCtx},
31        simulation::{SimulatedPlayerBundle, Simulation},
32    },
33};
34
35/// An alternative execution engine for the pathfinder that attempts to skip
36/// nodes in the path by running simulations.
37///
38/// This allows it to smooth the path and sprint-jump without failing jumps or
39/// looking unnatural. However, this comes at the cost of execution being more
40/// expensive and potentially less stable.
41///
42/// To use it, simply add [`SimulationPathfinderExecutionPlugin`] as a plugin.
43///
44/// ```
45/// use azalea::{
46///     pathfinder::execute::simulation::SimulationPathfinderExecutionPlugin, swarm::prelude::*,
47/// };
48///
49/// let builder = SwarmBuilder::new().add_plugins(SimulationPathfinderExecutionPlugin);
50/// // ...
51/// ```
52///
53/// [`DefaultPathfinderExecutionPlugin`]: super::DefaultPathfinderExecutionPlugin
54pub struct SimulationPathfinderExecutionPlugin;
55impl Plugin for SimulationPathfinderExecutionPlugin {
56    fn build(&self, app: &mut App) {
57        app.add_systems(
58            GameTick,
59            (
60                super::timeout_movement,
61                super::patching::check_for_path_obstruction,
62                super::check_node_reached,
63                tick_execute_path,
64                super::recalculate_near_end_of_path,
65                super::recalculate_if_has_goal_but_no_path,
66            )
67                .chain()
68                .after(PhysicsSystems)
69                .after(azalea_client::movement::send_position)
70                .after(MiningSystems)
71                .after(debug_render_path_with_particles),
72        );
73    }
74}
75
76#[derive(Clone, Component, Debug)]
77pub enum SimulatingPathState {
78    Fail,
79    Simulated(SimulatingPathOpts),
80}
81#[derive(Clone, Component, Debug)]
82pub struct SimulatingPathOpts {
83    pub start: BlockPos,
84    pub target: BlockPos,
85    pub jumping: bool,
86    pub jump_until_target_distance: f64,
87    pub jump_after_start_distance: f64,
88    pub sprinting: bool,
89    pub y_rot: f32,
90}
91
92#[allow(clippy::type_complexity)]
93pub fn tick_execute_path(
94    mut commands: Commands,
95    mut query: Query<(
96        Entity,
97        &mut ExecutingPath,
98        &mut LookDirection,
99        &Position,
100        &Physics,
101        &PhysicsState,
102        Option<&Mining>,
103        &WorldHolder,
104        &Attributes,
105        &Inventory,
106        Option<&SimulatingPathState>,
107    )>,
108    mut look_at_events: MessageWriter<LookAtEvent>,
109    mut sprint_events: MessageWriter<StartSprintEvent>,
110    mut walk_events: MessageWriter<StartWalkEvent>,
111    mut jump_events: MessageWriter<JumpEvent>,
112    mut start_mining_events: MessageWriter<StartMiningBlockEvent>,
113) {
114    for (
115        entity,
116        mut executing_path,
117        mut look_direction,
118        position,
119        physics,
120        physics_state,
121        mining,
122        world_holder,
123        attributes,
124        inventory,
125        mut simulating_path_state,
126    ) in &mut query
127    {
128        executing_path.ticks_since_last_node_reached += 1;
129
130        if executing_path.ticks_since_last_node_reached == 1 {
131            if let Some(SimulatingPathState::Simulated(s)) = simulating_path_state {
132                // only reset the state if we just reached the end of the simulation path (for
133                // performance)
134                if s.target == executing_path.last_reached_node
135                    // or if the current simulation target isn't in the path, reset too
136                    || !executing_path
137                        .path
138                        .iter()
139                        .any(|e| e.movement.target == s.target)
140                {
141                    simulating_path_state = None;
142                }
143            } else {
144                simulating_path_state = None;
145            }
146        }
147
148        let simulating_path_state = if let Some(simulating_path_state) = simulating_path_state {
149            Cow::Borrowed(simulating_path_state)
150        } else {
151            let start = Instant::now();
152            let new_state = run_simulations(
153                &executing_path,
154                world_holder,
155                SimulatedPlayerBundle {
156                    position: *position,
157                    physics: physics.clone(),
158                    physics_state: physics_state.clone(),
159                    look_direction: *look_direction,
160                    attributes: attributes.clone(),
161                    inventory: inventory.clone(),
162                },
163            );
164            debug!("found sim in {:?}: {new_state:?}", start.elapsed());
165            commands.entity(entity).insert(new_state.clone());
166            Cow::Owned(new_state)
167        };
168
169        match &*simulating_path_state {
170            SimulatingPathState::Fail => {
171                if let Some(edge) = executing_path.path.front() {
172                    let mut ctx = ExecuteCtx {
173                        entity,
174                        target: edge.movement.target,
175                        position: **position,
176                        start: executing_path.last_reached_node,
177                        physics,
178                        is_currently_mining: mining.is_some(),
179                        can_mine: true,
180                        world: world_holder.shared.clone(),
181                        menu: inventory.inventory_menu.clone(),
182
183                        commands: &mut commands,
184                        look_at_events: &mut look_at_events,
185                        sprint_events: &mut sprint_events,
186                        walk_events: &mut walk_events,
187                        jump_events: &mut jump_events,
188                        start_mining_events: &mut start_mining_events,
189                    };
190                    ctx.on_tick_start();
191                    trace!(
192                        "executing move, position: {}, last_reached_node: {}",
193                        **position, executing_path.last_reached_node
194                    );
195                    (edge.movement.data.execute)(ctx);
196                }
197            }
198            SimulatingPathState::Simulated(SimulatingPathOpts {
199                start,
200                target,
201                jumping,
202                jump_until_target_distance,
203                jump_after_start_distance,
204                sprinting,
205                y_rot,
206            }) => {
207                look_direction.update(LookDirection::new(*y_rot, 0.));
208
209                if *sprinting {
210                    sprint_events.write(StartSprintEvent {
211                        entity,
212                        direction: SprintDirection::Forward,
213                    });
214                } else if physics_state.was_sprinting {
215                    walk_events.write(StartWalkEvent {
216                        entity,
217                        direction: WalkDirection::None,
218                    });
219                } else {
220                    walk_events.write(StartWalkEvent {
221                        entity,
222                        direction: WalkDirection::Forward,
223                    });
224                }
225                if *jumping
226                    && target.center().horizontal_distance_squared_to(**position)
227                        > jump_until_target_distance.powi(2)
228                    && start.center().horizontal_distance_squared_to(**position)
229                        > jump_after_start_distance.powi(2)
230                {
231                    jump_events.write(JumpEvent { entity });
232                }
233            }
234        }
235
236        //
237    }
238}
239
240fn run_simulations(
241    executing_path: &ExecutingPath,
242    world_holder: &WorldHolder,
243    player: SimulatedPlayerBundle,
244) -> SimulatingPathState {
245    let swimming = player.physics.is_in_water();
246
247    let mut sim = Simulation::new(world_holder.shared.read().chunks.clone(), player.clone());
248
249    for nodes_ahead in [20, 15, 10, 5, 4, 3, 2, 1, 0] {
250        if nodes_ahead + 1 >= executing_path.path.len() {
251            // don't simulate to the last node since it has stricter checks
252            continue;
253        }
254
255        let mut results = Vec::new();
256
257        if let Some(simulating_to) = executing_path.path.get(nodes_ahead) {
258            let y_rot =
259                direction_looking_at(*player.position, simulating_to.movement.target.center())
260                    .y_rot();
261
262            for jump_until_target_distance in [0., 1., 3.] {
263                for jump_after_start_distance in [0., 0.5] {
264                    for jumping in [true, false] {
265                        if !jumping
266                            && (jump_until_target_distance != 0. || jump_after_start_distance != 0.)
267                        {
268                            continue;
269                        }
270
271                        // this loop is left here in case you wanna try re-enabling walking, but
272                        // it doesn't seem that useful
273                        for sprinting in [true] {
274                            if !sprinting && nodes_ahead > 2 {
275                                continue;
276                            }
277                            if swimming {
278                                if !sprinting
279                                    || jump_until_target_distance > 0.
280                                    || jump_after_start_distance > 0.
281                                {
282                                    continue;
283                                }
284                            } else if jump_until_target_distance == 0. {
285                                continue;
286                            }
287
288                            let state = SimulatingPathOpts {
289                                start: BlockPos::from(player.position),
290                                target: simulating_to.movement.target,
291                                jumping,
292                                jump_until_target_distance,
293                                jump_after_start_distance,
294                                sprinting,
295                                y_rot,
296                            };
297                            let sim_res = run_one_simulation(
298                                &mut sim,
299                                player.clone(),
300                                state.clone(),
301                                executing_path,
302                                nodes_ahead,
303                                if swimming {
304                                    (nodes_ahead * 12) + 20
305                                } else {
306                                    (nodes_ahead * 4) + 20
307                                },
308                            );
309                            if sim_res.success {
310                                results.push((state, sim_res.ticks));
311                            }
312                        }
313                    }
314                }
315            }
316        }
317
318        if !results.is_empty() {
319            let fastest = results.iter().min_by_key(|r| r.1).unwrap().0.clone();
320            return SimulatingPathState::Simulated(fastest);
321        }
322    }
323
324    SimulatingPathState::Fail
325}
326
327struct SimulationResult {
328    success: bool,
329    ticks: usize,
330}
331fn run_one_simulation(
332    sim: &mut Simulation,
333    player: SimulatedPlayerBundle,
334    state: SimulatingPathOpts,
335    executing_path: &ExecutingPath,
336    nodes_ahead: usize,
337    timeout_ticks: usize,
338) -> SimulationResult {
339    let simulating_to = &executing_path.path[nodes_ahead];
340
341    let start = BlockPos::from(player.position);
342    sim.reset(player);
343
344    let simulating_to_block = simulating_to.movement.target;
345
346    let mut success = false;
347    let mut total_ticks = 0;
348
349    for ticks in 1..=timeout_ticks {
350        let position = sim.position();
351        let ecs = sim.app.world_mut();
352
353        ecs.get_mut::<LookDirection>(sim.entity)
354            .unwrap()
355            .update(LookDirection::new(state.y_rot, 0.));
356
357        if state.sprinting {
358            ecs.write_message(StartSprintEvent {
359                entity: sim.entity,
360                direction: SprintDirection::Forward,
361            });
362        } else if ecs
363            .get::<PhysicsState>(sim.entity)
364            .map(|p| p.trying_to_sprint)
365            .unwrap_or_default()
366        {
367            // have to let go for a tick to be able to start walking
368            ecs.write_message(StartWalkEvent {
369                entity: sim.entity,
370                direction: WalkDirection::None,
371            });
372        } else {
373            ecs.write_message(StartWalkEvent {
374                entity: sim.entity,
375                direction: WalkDirection::Forward,
376            });
377        }
378        if state.jumping
379            && simulating_to_block
380                .center()
381                .horizontal_distance_squared_to(position)
382                > state.jump_until_target_distance.powi(2)
383            && start.center().horizontal_distance_squared_to(position)
384                > state.jump_after_start_distance.powi(2)
385        {
386            ecs.write_message(JumpEvent { entity: sim.entity });
387        }
388
389        sim.tick();
390
391        let physics = sim.physics();
392        if physics.horizontal_collision
393            || physics.is_in_lava()
394            || (physics.velocity.y < -0.7 && !physics.is_in_water())
395        {
396            // fail
397            break;
398        }
399
400        if (simulating_to.movement.data.is_reached)(IsReachedCtx {
401            target: simulating_to_block,
402            start,
403            position: sim.position(),
404            physics: &physics,
405        }) {
406            success = true;
407            total_ticks = ticks;
408            break;
409        }
410    }
411
412    if success {
413        // now verify that the path is safe by continuing to the next node
414
415        let mut followup_success = false;
416
417        let next_node = &executing_path.path[nodes_ahead + 1];
418        for _ in 1..=30 {
419            // add ticks here so if we sort by ticks later it'll be more accurate
420            total_ticks += 1;
421
422            {
423                let mut system_state = SystemState::<(
424                    Commands,
425                    Query<(&Position, &Physics, Option<&Mining>, &Inventory)>,
426                    MessageWriter<LookAtEvent>,
427                    MessageWriter<StartSprintEvent>,
428                    MessageWriter<StartWalkEvent>,
429                    MessageWriter<JumpEvent>,
430                    MessageWriter<StartMiningBlockEvent>,
431                )>::new(sim.app.world_mut());
432                let (
433                    mut commands,
434                    query,
435                    mut look_at_events,
436                    mut sprint_events,
437                    mut walk_events,
438                    mut jump_events,
439                    mut start_mining_events,
440                ) = system_state.get_mut(sim.app.world_mut());
441
442                let (position, physics, mining, inventory) = query.get(sim.entity).unwrap();
443
444                if physics.horizontal_collision {
445                    // if the simulated move made us hit a wall then it's bad
446                    break;
447                }
448                if physics.velocity.y < -0.7 && !physics.is_in_water() {
449                    break;
450                }
451
452                (next_node.movement.data.execute)(ExecuteCtx {
453                    entity: sim.entity,
454                    target: next_node.movement.target,
455                    start: simulating_to_block,
456                    position: **position,
457                    physics,
458                    is_currently_mining: mining.is_some(),
459                    // don't modify the world from the simulation
460                    can_mine: false,
461                    world: sim.world.clone(),
462                    menu: inventory.inventory_menu.clone(),
463
464                    commands: &mut commands,
465                    look_at_events: &mut look_at_events,
466                    sprint_events: &mut sprint_events,
467                    walk_events: &mut walk_events,
468                    jump_events: &mut jump_events,
469                    start_mining_events: &mut start_mining_events,
470                });
471                system_state.apply(sim.app.world_mut());
472            }
473
474            sim.tick();
475
476            if (next_node.movement.data.is_reached)(IsReachedCtx {
477                target: next_node.movement.target,
478                start: simulating_to_block,
479                position: sim.position(),
480                physics: &sim.physics(),
481            }) {
482                followup_success = true;
483                break;
484            }
485        }
486
487        if !followup_success {
488            debug!("followup failed");
489            success = false;
490        }
491    }
492
493    SimulationResult {
494        success,
495        ticks: total_ticks,
496    }
497}