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