@@ -16,6 +16,7 @@ use std::collections::VecDeque;
1616use std:: sync:: atomic:: { self , AtomicUsize } ;
1717use std:: sync:: Arc ;
1818use std:: time:: { Duration , Instant } ;
19+ use std:: { cmp, thread} ;
1920
2021use astar:: PathfinderTimeout ;
2122use azalea_client:: inventory:: { Inventory , InventorySet , SetSelectedHotbarSlotEvent } ;
@@ -35,6 +36,7 @@ use bevy_ecs::query::Changed;
3536use bevy_ecs:: schedule:: IntoSystemConfigs ;
3637use bevy_tasks:: { AsyncComputeTaskPool , Task } ;
3738use futures_lite:: future;
39+ use goals:: BlockPosGoal ;
3840use parking_lot:: RwLock ;
3941use rel_block_pos:: RelBlockPos ;
4042use tracing:: { debug, error, info, trace, warn} ;
@@ -105,7 +107,8 @@ pub struct Pathfinder {
105107 pub is_calculating : bool ,
106108 pub allow_mining : bool ,
107109
108- pub deterministic_timeout : bool ,
110+ pub default_timeout : Option < PathfinderTimeout > ,
111+ pub max_timeout : Option < PathfinderTimeout > ,
109112
110113 pub goto_id : Arc < AtomicUsize > ,
111114}
@@ -138,13 +141,11 @@ pub struct GotoEvent {
138141 /// Whether the bot is allowed to break blocks while pathfinding.
139142 pub allow_mining : bool ,
140143
141- /// Whether the timeout should be based on number of nodes considered
142- /// instead of the time passed.
143- ///
144- /// Also see: [`PathfinderTimeout::Nodes`]
145- pub deterministic_timeout : bool ,
144+ /// Also see [`PathfinderTimeout::Nodes`]
145+ pub default_timeout : PathfinderTimeout ,
146+ pub max_timeout : PathfinderTimeout ,
146147}
147- #[ derive( Event , Clone ) ]
148+ #[ derive( Event , Clone , Debug ) ]
148149pub struct PathFoundEvent {
149150 pub entity : Entity ,
150151 pub start : BlockPos ,
@@ -186,7 +187,8 @@ impl PathfinderClientExt for azalea_client::Client {
186187 goal : Arc :: new ( goal) ,
187188 successors_fn : moves:: default_move,
188189 allow_mining : true ,
189- deterministic_timeout : false ,
190+ default_timeout : PathfinderTimeout :: Time ( Duration :: from_secs ( 1 ) ) ,
191+ max_timeout : PathfinderTimeout :: Time ( Duration :: from_secs ( 5 ) ) ,
190192 } ) ;
191193 }
192194
@@ -198,7 +200,8 @@ impl PathfinderClientExt for azalea_client::Client {
198200 goal : Arc :: new ( goal) ,
199201 successors_fn : moves:: default_move,
200202 allow_mining : false ,
201- deterministic_timeout : false ,
203+ default_timeout : PathfinderTimeout :: Time ( Duration :: from_secs ( 1 ) ) ,
204+ max_timeout : PathfinderTimeout :: Time ( Duration :: from_secs ( 5 ) ) ,
202205 } ) ;
203206 }
204207
@@ -247,6 +250,8 @@ pub fn goto_listener(
247250 pathfinder. successors_fn = Some ( event. successors_fn ) ;
248251 pathfinder. is_calculating = true ;
249252 pathfinder. allow_mining = event. allow_mining ;
253+ pathfinder. default_timeout = Some ( event. default_timeout ) ;
254+ pathfinder. max_timeout = Some ( event. max_timeout ) ;
250255
251256 let start = if let Some ( executing_path) = executing_path
252257 && let Some ( final_node) = executing_path. path . back ( )
@@ -279,19 +284,23 @@ pub fn goto_listener(
279284 None
280285 } ) ;
281286
282- let deterministic_timeout = event. deterministic_timeout ;
287+ let default_timeout = event. default_timeout ;
288+ let max_timeout = event. max_timeout ;
283289
284- let task = thread_pool. spawn ( calculate_path ( CalculatePathOpts {
285- entity,
286- start,
287- goal,
288- successors_fn,
289- world_lock,
290- goto_id_atomic,
291- allow_mining,
292- mining_cache,
293- deterministic_timeout,
294- } ) ) ;
290+ let task = thread_pool. spawn ( async move {
291+ calculate_path ( CalculatePathOpts {
292+ entity,
293+ start,
294+ goal,
295+ successors_fn,
296+ world_lock,
297+ goto_id_atomic,
298+ allow_mining,
299+ mining_cache,
300+ default_timeout,
301+ max_timeout,
302+ } )
303+ } ) ;
295304
296305 commands. entity ( event. entity ) . insert ( ComputePath ( task) ) ;
297306 }
@@ -306,8 +315,9 @@ pub struct CalculatePathOpts {
306315 pub goto_id_atomic : Arc < AtomicUsize > ,
307316 pub allow_mining : bool ,
308317 pub mining_cache : MiningCache ,
309- /// See [`GotoEvent::deterministic_timeout`]
310- pub deterministic_timeout : bool ,
318+ /// Also see [`GotoEvent::deterministic_timeout`]
319+ pub default_timeout : PathfinderTimeout ,
320+ pub max_timeout : PathfinderTimeout ,
311321}
312322
313323/// Calculate the [`PathFoundEvent`] for the given pathfinder options.
@@ -318,7 +328,7 @@ pub struct CalculatePathOpts {
318328/// You are expected to immediately send the `PathFoundEvent` you received after
319329/// calling this function. `None` will be returned if the pathfinding was
320330/// interrupted by another path calculation.
321- pub async fn calculate_path ( opts : CalculatePathOpts ) -> Option < PathFoundEvent > {
331+ pub fn calculate_path ( opts : CalculatePathOpts ) -> Option < PathFoundEvent > {
322332 debug ! ( "start: {:?}" , opts. start) ;
323333
324334 let goto_id = opts. goto_id_atomic . fetch_add ( 1 , atomic:: Ordering :: SeqCst ) + 1 ;
@@ -337,14 +347,10 @@ pub async fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
337347 ' calculate: loop {
338348 let start_time = Instant :: now ( ) ;
339349
340- let timeout = if opts. deterministic_timeout {
341- PathfinderTimeout :: Nodes ( if attempt_number == 0 {
342- 1_000_000
343- } else {
344- 5_000_000
345- } )
350+ let timeout = if attempt_number == 0 {
351+ opts. default_timeout
346352 } else {
347- PathfinderTimeout :: Time ( Duration :: from_secs ( if attempt_number == 0 { 1 } else { 5 } ) )
353+ opts . max_timeout
348354 } ;
349355
350356 let astar:: Path { movements, partial } = a_star (
@@ -364,7 +370,7 @@ pub async fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
364370 info ! ( "Pathfinder took {duration:?} (incomplete path)" ) ;
365371 }
366372 // wait a bit so it's not a busy loop
367- std :: thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
373+ thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
368374 } else {
369375 info ! ( "Pathfinder took {duration:?}" ) ;
370376 }
@@ -385,7 +391,7 @@ pub async fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
385391 }
386392
387393 if path. is_empty ( ) && partial {
388- if attempt_number == 0 {
394+ if attempt_number == 0 && opts . default_timeout != opts . max_timeout {
389395 debug ! ( "this path is empty, retrying with a higher timeout" ) ;
390396 attempt_number += 1 ;
391397 continue ' calculate;
@@ -660,10 +666,16 @@ pub fn check_node_reached(
660666}
661667
662668pub fn check_for_path_obstruction (
663- mut query : Query < ( & Pathfinder , & mut ExecutingPath , & InstanceName , & Inventory ) > ,
669+ mut query : Query < (
670+ Entity ,
671+ & Pathfinder ,
672+ & mut ExecutingPath ,
673+ & InstanceName ,
674+ & Inventory ,
675+ ) > ,
664676 instance_container : Res < InstanceContainer > ,
665677) {
666- for ( pathfinder, mut executing_path, instance_name, inventory) in & mut query {
678+ for ( entity , pathfinder, mut executing_path, instance_name, inventory) in & mut query {
667679 let Some ( successors_fn) = pathfinder. successors_fn else {
668680 continue ;
669681 } ;
@@ -693,8 +705,95 @@ pub fn check_for_path_obstruction(
693705 "path obstructed at index {obstructed_index} (starting at {:?}, path: {:?})" ,
694706 executing_path. last_reached_node, executing_path. path
695707 ) ;
696- executing_path. path . truncate ( obstructed_index) ;
697- executing_path. is_path_partial = true ;
708+ // if it's near the end, don't bother recalculating a patch, just truncate and
709+ // mark it as partial
710+ if obstructed_index + 5 > executing_path. path . len ( ) {
711+ debug ! (
712+ "obstruction is near the end of the path, truncating and marking path as partial"
713+ ) ;
714+ executing_path. path . truncate ( obstructed_index) ;
715+ executing_path. is_path_partial = true ;
716+ continue ;
717+ }
718+
719+ let Some ( successors_fn) = pathfinder. successors_fn else {
720+ error ! ( "got PatchExecutingPathEvent but the bot has no successors_fn" ) ;
721+ continue ;
722+ } ;
723+
724+ let world_lock = instance_container
725+ . get ( instance_name)
726+ . expect ( "Entity tried to pathfind but the entity isn't in a valid world" ) ;
727+
728+ let patch_start = if obstructed_index == 0 {
729+ executing_path. last_reached_node
730+ } else {
731+ executing_path. path [ obstructed_index - 1 ] . target
732+ } ;
733+
734+ // patch up to 20 nodes
735+ let patch_end_index = cmp:: min ( obstructed_index + 20 , executing_path. path . len ( ) - 1 ) ;
736+ let patch_end = executing_path. path [ patch_end_index] . target ;
737+
738+ // this doesn't override the main goal, it's just the goal for this A*
739+ // calculation
740+ let goal = Arc :: new ( BlockPosGoal ( patch_end) ) ;
741+
742+ let goto_id_atomic = pathfinder. goto_id . clone ( ) ;
743+
744+ let allow_mining = pathfinder. allow_mining ;
745+ let mining_cache = MiningCache :: new ( if allow_mining {
746+ Some ( inventory. inventory_menu . clone ( ) )
747+ } else {
748+ None
749+ } ) ;
750+
751+ // the timeout is small enough that this doesn't need to be async
752+ let path_found_event = calculate_path ( CalculatePathOpts {
753+ entity,
754+ start : patch_start,
755+ goal,
756+ successors_fn,
757+ world_lock,
758+ goto_id_atomic,
759+ allow_mining,
760+ mining_cache,
761+ default_timeout : PathfinderTimeout :: Nodes ( 10_000 ) ,
762+ max_timeout : PathfinderTimeout :: Nodes ( 10_000 ) ,
763+ } ) ;
764+ debug ! ( "obstruction patch: {path_found_event:?}" ) ;
765+
766+ let mut new_path = VecDeque :: new ( ) ;
767+ if obstructed_index > 0 {
768+ new_path. extend ( executing_path. path . iter ( ) . take ( obstructed_index) . cloned ( ) ) ;
769+ }
770+
771+ let mut is_patch_complete = false ;
772+ if let Some ( path_found_event) = path_found_event {
773+ if let Some ( found_path_patch) = path_found_event. path {
774+ if !found_path_patch. is_empty ( ) {
775+ new_path. extend ( found_path_patch) ;
776+
777+ if !path_found_event. is_partial {
778+ new_path
779+ . extend ( executing_path. path . iter ( ) . skip ( patch_end_index) . cloned ( ) ) ;
780+ is_patch_complete = true ;
781+ debug ! ( "the obstruction patch is not partial" ) ;
782+ } else {
783+ debug ! (
784+ "the obstruction patch is partial, throwing away rest of path :("
785+ ) ;
786+ }
787+ }
788+ }
789+ } else {
790+ // no path found, rip
791+ }
792+
793+ executing_path. path = new_path;
794+ if !is_patch_complete {
795+ executing_path. is_path_partial = true ;
796+ }
698797 }
699798 }
700799}
@@ -726,7 +825,10 @@ pub fn recalculate_near_end_of_path(
726825 goal,
727826 successors_fn,
728827 allow_mining : pathfinder. allow_mining ,
729- deterministic_timeout : pathfinder. deterministic_timeout ,
828+ default_timeout : pathfinder
829+ . default_timeout
830+ . expect ( "default_timeout should be set" ) ,
831+ max_timeout : pathfinder. max_timeout . expect ( "max_timeout should be set" ) ,
730832 } ) ;
731833 pathfinder. is_calculating = true ;
732834
@@ -823,7 +925,10 @@ pub fn recalculate_if_has_goal_but_no_path(
823925 goal,
824926 successors_fn : pathfinder. successors_fn . unwrap ( ) ,
825927 allow_mining : pathfinder. allow_mining ,
826- deterministic_timeout : pathfinder. deterministic_timeout ,
928+ default_timeout : pathfinder
929+ . default_timeout
930+ . expect ( "default_timeout should be set" ) ,
931+ max_timeout : pathfinder. max_timeout . expect ( "max_timeout should be set" ) ,
827932 } ) ;
828933 pathfinder. is_calculating = true ;
829934 }
@@ -950,6 +1055,7 @@ mod tests {
9501055 use azalea_world:: { Chunk , ChunkStorage , PartialChunkStorage } ;
9511056
9521057 use super :: {
1058+ astar:: PathfinderTimeout ,
9531059 goals:: BlockPosGoal ,
9541060 moves,
9551061 simulation:: { SimulatedPlayerBundle , Simulation } ,
@@ -976,7 +1082,8 @@ mod tests {
9761082 goal : Arc :: new ( BlockPosGoal ( end_pos) ) ,
9771083 successors_fn : moves:: default_move,
9781084 allow_mining : false ,
979- deterministic_timeout : true ,
1085+ default_timeout : PathfinderTimeout :: Nodes ( 1_000_000 ) ,
1086+ max_timeout : PathfinderTimeout :: Nodes ( 5_000_000 ) ,
9801087 } ) ;
9811088 simulation
9821089 }
0 commit comments