1use 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
35pub 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 if s.target == executing_path.last_reached_node
135 || !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 }
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 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 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 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 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 let mut followup_success = false;
416
417 let next_node = &executing_path.path[nodes_ahead + 1];
418 for _ in 1..=30 {
419 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 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 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}