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