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, 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 if s.target == executing_path.last_reached_node
136 || !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 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 }
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 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 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 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 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 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 let mut followup_success = false;
424
425 let next_node = &executing_path.path[nodes_ahead + 1];
426 for _ in 1..=30 {
427 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 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 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}