@@ -20,22 +20,26 @@ use crate::ecs::{
20
20
system:: { Commands , Query , Res } ,
21
21
} ;
22
22
use crate :: pathfinder:: moves:: PathfinderCtx ;
23
+ use azalea_client:: chat:: SendChatEvent ;
23
24
use azalea_client:: movement:: walk_listener;
24
25
use azalea_client:: { StartSprintEvent , StartWalkEvent } ;
25
- use azalea_core:: position:: BlockPos ;
26
+ use azalea_core:: position:: { BlockPos , Vec3 } ;
26
27
use azalea_entity:: metadata:: Player ;
27
28
use azalea_entity:: LocalEntity ;
28
29
use azalea_entity:: { Physics , Position } ;
29
30
use azalea_physics:: PhysicsSet ;
30
31
use azalea_world:: { InstanceContainer , InstanceName } ;
31
32
use bevy_app:: { FixedUpdate , PreUpdate , Update } ;
33
+ use bevy_ecs:: event:: Events ;
32
34
use bevy_ecs:: prelude:: Event ;
33
35
use bevy_ecs:: query:: Changed ;
34
36
use bevy_ecs:: schedule:: IntoSystemConfigs ;
37
+ use bevy_ecs:: system:: { Local , ResMut } ;
35
38
use bevy_tasks:: { AsyncComputeTaskPool , Task } ;
36
39
use futures_lite:: future;
37
40
use log:: { debug, error, info, trace, warn} ;
38
41
use std:: collections:: VecDeque ;
42
+ use std:: sync:: atomic:: { self , AtomicUsize } ;
39
43
use std:: sync:: Arc ;
40
44
use std:: time:: { Duration , Instant } ;
41
45
@@ -51,9 +55,13 @@ impl Plugin for PathfinderPlugin {
51
55
FixedUpdate ,
52
56
// putting systems in the FixedUpdate schedule makes them run every Minecraft tick
53
57
// (every 50 milliseconds).
54
- tick_execute_path
55
- . after ( PhysicsSet )
56
- . after ( azalea_client:: movement:: send_position) ,
58
+ (
59
+ tick_execute_path
60
+ . after ( PhysicsSet )
61
+ . after ( azalea_client:: movement:: send_position) ,
62
+ debug_render_path_with_particles,
63
+ )
64
+ . chain ( ) ,
57
65
)
58
66
. add_systems ( PreUpdate , add_default_pathfinder)
59
67
. add_systems (
@@ -81,6 +89,8 @@ pub struct Pathfinder {
81
89
pub goal : Option < Arc < dyn Goal + Send + Sync > > ,
82
90
pub successors_fn : Option < SuccessorsFn > ,
83
91
pub is_calculating : bool ,
92
+
93
+ pub goto_id : Arc < AtomicUsize > ,
84
94
}
85
95
#[ derive( Event ) ]
86
96
pub struct GotoEvent {
@@ -157,7 +167,7 @@ fn goto_listener(
157
167
// if we're currently pathfinding and got a goto event, start a little ahead
158
168
pathfinder
159
169
. path
160
- . get ( 5 )
170
+ . get ( 20 )
161
171
. unwrap_or_else ( || pathfinder. path . back ( ) . unwrap ( ) )
162
172
. target
163
173
} ;
@@ -175,6 +185,9 @@ fn goto_listener(
175
185
let goal = event. goal . clone ( ) ;
176
186
let entity = event. entity ;
177
187
188
+ let goto_id_atomic = pathfinder. goto_id . clone ( ) ;
189
+ let goto_id = goto_id_atomic. fetch_add ( 1 , atomic:: Ordering :: Relaxed ) + 1 ;
190
+
178
191
let task = thread_pool. spawn ( async move {
179
192
debug ! ( "start: {start:?}" ) ;
180
193
@@ -204,6 +217,8 @@ fn goto_listener(
204
217
let duration = end_time - start_time;
205
218
if partial {
206
219
info ! ( "Pathfinder took {duration:?} (timed out)" ) ;
220
+ // wait a bit so it's not a busy loop
221
+ std:: thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
207
222
} else {
208
223
info ! ( "Pathfinder took {duration:?}" ) ;
209
224
}
@@ -216,6 +231,13 @@ fn goto_listener(
216
231
path = movements. into_iter ( ) . collect :: < VecDeque < _ > > ( ) ;
217
232
is_partial = partial;
218
233
234
+ let goto_id_now = goto_id_atomic. load ( atomic:: Ordering :: Relaxed ) ;
235
+ if goto_id != goto_id_now {
236
+ // we must've done another goto while calculating this path, so throw it away
237
+ warn ! ( "finished calculating a path, but it's outdated" ) ;
238
+ return None ;
239
+ }
240
+
219
241
if path. is_empty ( ) && partial {
220
242
if attempt_number == 0 {
221
243
debug ! ( "this path is empty, retrying with a higher timeout" ) ;
@@ -275,6 +297,7 @@ fn path_found_listener(
275
297
pathfinder. path = path. to_owned ( ) ;
276
298
debug ! ( "set path to {:?}" , path. iter( ) . take( 10 ) . collect:: <Vec <_>>( ) ) ;
277
299
pathfinder. last_reached_node = Some ( event. start ) ;
300
+ pathfinder. last_node_reached_at = Some ( Instant :: now ( ) ) ;
278
301
} else {
279
302
let mut new_path = VecDeque :: new ( ) ;
280
303
@@ -313,7 +336,6 @@ fn path_found_listener(
313
336
) ;
314
337
pathfinder. queued_path = Some ( new_path) ;
315
338
}
316
- pathfinder. last_node_reached_at = Some ( Instant :: now ( ) ) ;
317
339
} else {
318
340
error ! ( "No path found" ) ;
319
341
pathfinder. path . clear ( ) ;
@@ -353,6 +375,9 @@ fn tick_execute_path(
353
375
if last_node_reached_at. elapsed ( ) > Duration :: from_secs ( 2 ) {
354
376
warn ! ( "pathfinder timeout" ) ;
355
377
pathfinder. path . clear ( ) ;
378
+ pathfinder. queued_path = None ;
379
+ pathfinder. last_reached_node = None ;
380
+ pathfinder. goto_id . fetch_add ( 1 , atomic:: Ordering :: Relaxed ) ;
356
381
// set partial to true to make sure that the recalculation happens
357
382
pathfinder. is_path_partial = true ;
358
383
}
@@ -386,8 +411,10 @@ fn tick_execute_path(
386
411
// this is to make sure we don't fall off immediately after finishing the path
387
412
physics. on_ground
388
413
&& BlockPos :: from ( position) == movement. target
389
- && x_difference_from_center. abs ( ) < 0.2
390
- && z_difference_from_center. abs ( ) < 0.2
414
+ // adding the delta like this isn't a perfect solution but it helps to make
415
+ // sure we don't keep going if our delta is high
416
+ && ( x_difference_from_center + physics. delta . x ) . abs ( ) < 0.2
417
+ && ( z_difference_from_center + physics. delta . z ) . abs ( ) < 0.2
391
418
} else {
392
419
true
393
420
} ;
@@ -470,13 +497,16 @@ fn tick_execute_path(
470
497
{
471
498
warn ! ( "path obstructed at index {obstructed_index} (starting at {last_reached_node:?}, path: {:?})" , pathfinder. path) ;
472
499
pathfinder. path . truncate ( obstructed_index) ;
500
+ pathfinder. is_path_partial = true ;
473
501
}
474
502
}
475
503
}
476
504
477
505
{
478
506
// start recalculating if the path ends soon
479
- if pathfinder. path . len ( ) < 5 && !pathfinder. is_calculating && pathfinder. is_path_partial
507
+ if ( pathfinder. path . len ( ) == 20 || pathfinder. path . len ( ) < 5 )
508
+ && !pathfinder. is_calculating
509
+ && pathfinder. is_path_partial
480
510
{
481
511
if let Some ( goal) = pathfinder. goal . as_ref ( ) . cloned ( ) {
482
512
debug ! ( "Recalculating path because it ends soon" ) ;
@@ -507,6 +537,12 @@ fn tick_execute_path(
507
537
} ) ;
508
538
}
509
539
}
540
+ } else if pathfinder. path . is_empty ( ) {
541
+ // idk when this can happen but stop moving just in case
542
+ walk_events. send ( StartWalkEvent {
543
+ entity,
544
+ direction : WalkDirection :: None ,
545
+ } ) ;
510
546
}
511
547
}
512
548
}
@@ -529,6 +565,79 @@ fn stop_pathfinding_on_instance_change(
529
565
}
530
566
}
531
567
568
+ /// A component that makes bots run /particle commands while pathfinding to show
569
+ /// where they're going. This requires the bots to have op permissions, and
570
+ /// it'll make them spam *a lot* of commands.
571
+ #[ derive( Component ) ]
572
+ pub struct PathfinderDebugParticles ;
573
+
574
+ fn debug_render_path_with_particles (
575
+ mut query : Query < ( Entity , & Pathfinder ) , With < PathfinderDebugParticles > > ,
576
+ // chat_events is Option because the tests don't have SendChatEvent
577
+ // and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter>
578
+ chat_events : Option < ResMut < Events < SendChatEvent > > > ,
579
+ mut tick_count : Local < usize > ,
580
+ ) {
581
+ let Some ( mut chat_events) = chat_events else {
582
+ return ;
583
+ } ;
584
+ if * tick_count >= 2 {
585
+ * tick_count = 0 ;
586
+ } else {
587
+ * tick_count += 1 ;
588
+ return ;
589
+ }
590
+ for ( entity, pathfinder) in & mut query {
591
+ if pathfinder. path . is_empty ( ) {
592
+ continue ;
593
+ }
594
+
595
+ let mut start = pathfinder
596
+ . last_reached_node
597
+ . unwrap_or_else ( || pathfinder. path . front ( ) . unwrap ( ) . target ) ;
598
+ for movement in & pathfinder. path {
599
+ // /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
600
+
601
+ let end = movement. target ;
602
+
603
+ let start_vec3 = start. center ( ) ;
604
+ let end_vec3 = end. center ( ) ;
605
+
606
+ let step_count = ( start_vec3. distance_to_sqr ( & end_vec3) . sqrt ( ) * 4.0 ) as usize ;
607
+
608
+ // interpolate between the start and end positions
609
+ for i in 0 ..step_count {
610
+ let percent = i as f64 / step_count as f64 ;
611
+ let pos = Vec3 {
612
+ x : start_vec3. x + ( end_vec3. x - start_vec3. x ) * percent,
613
+ y : start_vec3. y + ( end_vec3. y - start_vec3. y ) * percent,
614
+ z : start_vec3. z + ( end_vec3. z - start_vec3. z ) * percent,
615
+ } ;
616
+ let particle_command = format ! (
617
+ "/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}" ,
618
+ r = 0 ,
619
+ g = 1 ,
620
+ b = 1 ,
621
+ size = 1 ,
622
+ start_x = pos. x,
623
+ start_y = pos. y,
624
+ start_z = pos. z,
625
+ delta_x = 0 ,
626
+ delta_y = 0 ,
627
+ delta_z = 0 ,
628
+ count = 1
629
+ ) ;
630
+ chat_events. send ( SendChatEvent {
631
+ entity,
632
+ content : particle_command,
633
+ } ) ;
634
+ }
635
+
636
+ start = movement. target ;
637
+ }
638
+ }
639
+ }
640
+
532
641
pub trait Goal {
533
642
fn heuristic ( & self , n : BlockPos ) -> f32 ;
534
643
fn success ( & self , n : BlockPos ) -> bool ;
@@ -729,6 +838,29 @@ mod tests {
729
838
) ;
730
839
}
731
840
841
+ #[ test]
842
+ fn test_small_descend_and_parkour_2_block_gap ( ) {
843
+ let mut partial_chunks = PartialChunkStorage :: default ( ) ;
844
+ let mut simulation = setup_simulation (
845
+ & mut partial_chunks,
846
+ BlockPos :: new ( 0 , 71 , 0 ) ,
847
+ BlockPos :: new ( 0 , 70 , 5 ) ,
848
+ vec ! [
849
+ BlockPos :: new( 0 , 70 , 0 ) ,
850
+ BlockPos :: new( 0 , 70 , 1 ) ,
851
+ BlockPos :: new( 0 , 69 , 2 ) ,
852
+ BlockPos :: new( 0 , 69 , 5 ) ,
853
+ ] ,
854
+ ) ;
855
+ for _ in 0 ..40 {
856
+ simulation. tick ( ) ;
857
+ }
858
+ assert_eq ! (
859
+ BlockPos :: from( simulation. position( ) ) ,
860
+ BlockPos :: new( 0 , 70 , 5 )
861
+ ) ;
862
+ }
863
+
732
864
#[ test]
733
865
fn test_quickly_descend ( ) {
734
866
let mut partial_chunks = PartialChunkStorage :: default ( ) ;
0 commit comments