Skip to content

Commit e11a902

Browse files
committed
patch pathfinder obstructions instead of just truncating the path
1 parent 2f1fe5f commit e11a902

File tree

4 files changed

+149
-46
lines changed

4 files changed

+149
-46
lines changed

azalea-protocol/src/read.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ fn parse_frame(buffer: &mut Cursor<Vec<u8>>) -> Result<Box<[u8]>, FrameSplitterE
105105

106106
// the length of the varint that says the length of the whole packet
107107
let varint_length = buffer.remaining() - buffer_copy.remaining();
108-
drop(buffer_copy);
109108

110109
buffer.advance(varint_length);
111110
let data =

azalea/examples/testbot/main.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,7 @@ async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Resu
188188

189189
Ok(())
190190
}
191-
async fn swarm_handle(
192-
mut swarm: Swarm,
193-
event: SwarmEvent,
194-
_state: SwarmState,
195-
) -> anyhow::Result<()> {
191+
async fn swarm_handle(swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> {
196192
match &event {
197193
SwarmEvent::Disconnect(account, join_opts) => {
198194
println!("bot got kicked! {}", account.username);

azalea/src/pathfinder/astar.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const COEFFICIENTS: [f32; 7] = [1.5, 2., 2.5, 3., 4., 5., 10.];
2424

2525
const MIN_IMPROVEMENT: f32 = 0.01;
2626

27+
#[derive(Debug, Clone, Copy, PartialEq)]
2728
pub enum PathfinderTimeout {
2829
/// Time out after a certain duration has passed. This is a good default so
2930
/// you don't waste too much time calculating a path if you're on a slow

azalea/src/pathfinder/mod.rs

Lines changed: 147 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::collections::VecDeque;
1616
use std::sync::atomic::{self, AtomicUsize};
1717
use std::sync::Arc;
1818
use std::time::{Duration, Instant};
19+
use std::{cmp, thread};
1920

2021
use astar::PathfinderTimeout;
2122
use azalea_client::inventory::{Inventory, InventorySet, SetSelectedHotbarSlotEvent};
@@ -35,6 +36,7 @@ use bevy_ecs::query::Changed;
3536
use bevy_ecs::schedule::IntoSystemConfigs;
3637
use bevy_tasks::{AsyncComputeTaskPool, Task};
3738
use futures_lite::future;
39+
use goals::BlockPosGoal;
3840
use parking_lot::RwLock;
3941
use rel_block_pos::RelBlockPos;
4042
use 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)]
148149
pub 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

662668
pub 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

Comments
 (0)