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
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 }
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 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 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 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 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 let mut followup_success = false;
417
418 let next_node = &executing_path.path[nodes_ahead + 1];
419 for _ in 1..=30 {
420 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 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 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}