Skip to main content

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