1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

patch path on timeout instead of recalculating everything

This commit is contained in:
mat 2024-12-26 12:36:41 +00:00
parent 344834c724
commit 33e1a1326a
4 changed files with 185 additions and 89 deletions

View file

@ -137,12 +137,16 @@ fn run_pathfinder_benchmark(
azalea::pathfinder::call_successors_fn(&cached_world, &mining_cache, successors_fn, pos) azalea::pathfinder::call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
}; };
let astar::Path { movements, partial } = a_star( let astar::Path {
movements,
is_partial: partial,
} = a_star(
RelBlockPos::get_origin(origin), RelBlockPos::get_origin(origin),
|n| goal.heuristic(n.apply(origin)), |n| goal.heuristic(n.apply(origin)),
successors, successors,
|n| goal.success(n.apply(origin)), |n| goal.success(n.apply(origin)),
PathfinderTimeout::Time(Duration::MAX), PathfinderTimeout::Time(Duration::MAX),
PathfinderTimeout::Time(Duration::MAX),
); );
assert!(!partial); assert!(!partial);

View file

@ -4,6 +4,7 @@ use azalea::{
brigadier::prelude::*, brigadier::prelude::*,
entity::{LookDirection, Position}, entity::{LookDirection, Position},
interact::HitResultComponent, interact::HitResultComponent,
pathfinder::{ExecutingPath, Pathfinder},
world::MinecraftEntityId, world::MinecraftEntityId,
BlockPos, BlockPos,
}; };
@ -117,4 +118,34 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
1 1
})), })),
))); )));
commands.register(literal("pathfinderstate").executes(|ctx: &Ctx| {
let source = ctx.source.lock();
let pathfinder = source.bot.get_component::<Pathfinder>();
let Some(pathfinder) = pathfinder else {
source.reply("I don't have the Pathfinder ocmponent");
return 1;
};
source.reply(&format!(
"pathfinder.is_calculating: {}",
pathfinder.is_calculating
));
let executing_path = source.bot.get_component::<ExecutingPath>();
let Some(executing_path) = executing_path else {
source.reply("I'm not executing a path");
return 1;
};
source.reply(&format!(
"is_path_partial: {}, path.len: {}, queued_path.len: {}",
executing_path.is_path_partial,
executing_path.path.len(),
if let Some(queued) = executing_path.queued_path {
queued.len().to_string()
} else {
"n/a".to_string()
},
));
1
}));
} }

View file

@ -16,7 +16,7 @@ where
P: Eq + Hash + Copy + Debug, P: Eq + Hash + Copy + Debug,
{ {
pub movements: Vec<Movement<P, M>>, pub movements: Vec<Movement<P, M>>,
pub partial: bool, pub is_partial: bool,
} }
// used for better results when timing out // used for better results when timing out
@ -90,7 +90,7 @@ where
return Path { return Path {
movements: reconstruct_path(nodes, index), movements: reconstruct_path(nodes, index),
partial: false, is_partial: false,
}; };
} }
@ -190,7 +190,7 @@ where
Path { Path {
movements: reconstruct_path(nodes, best_path), movements: reconstruct_path(nodes, best_path),
partial: true, is_partial: true,
} }
} }

View file

@ -13,6 +13,7 @@ pub mod simulation;
pub mod world; pub mod world;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ops::RangeInclusive;
use std::sync::atomic::{self, AtomicUsize}; use std::sync::atomic::{self, AtomicUsize};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -71,9 +72,9 @@ impl Plugin for PathfinderPlugin {
GameTick, GameTick,
( (
timeout_movement, timeout_movement,
check_for_path_obstruction,
check_node_reached, check_node_reached,
tick_execute_path, tick_execute_path,
check_for_path_obstruction,
debug_render_path_with_particles, debug_render_path_with_particles,
recalculate_near_end_of_path, recalculate_near_end_of_path,
recalculate_if_has_goal_but_no_path, recalculate_if_has_goal_but_no_path,
@ -115,7 +116,7 @@ pub struct Pathfinder {
/// A component that's present on clients that are actively following a /// A component that's present on clients that are actively following a
/// pathfinder path. /// pathfinder path.
#[derive(Component)] #[derive(Component, Clone)]
pub struct ExecutingPath { pub struct ExecutingPath {
pub path: VecDeque<astar::Movement<BlockPos, moves::MoveData>>, pub path: VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
pub queued_path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>, pub queued_path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
@ -257,7 +258,7 @@ pub fn goto_listener(
&& let Some(final_node) = executing_path.path.back() && let Some(final_node) = executing_path.path.back()
{ {
// if we're currently pathfinding and got a goto event, start a little ahead // if we're currently pathfinding and got a goto event, start a little ahead
executing_path.path.get(20).unwrap_or(final_node).target executing_path.path.get(50).unwrap_or(final_node).target
} else { } else {
BlockPos::from(position) BlockPos::from(position)
}; };
@ -339,14 +340,14 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
call_successors_fn(&cached_world, &opts.mining_cache, opts.successors_fn, pos) call_successors_fn(&cached_world, &opts.mining_cache, opts.successors_fn, pos)
}; };
let mut attempt_number = 0; let path;
let mut path;
let mut is_partial: bool;
let start_time = Instant::now(); let start_time = Instant::now();
let astar::Path { movements, partial } = a_star( let astar::Path {
movements,
is_partial,
} = a_star(
RelBlockPos::get_origin(origin), RelBlockPos::get_origin(origin),
|n| opts.goal.heuristic(n.apply(origin)), |n| opts.goal.heuristic(n.apply(origin)),
successors, successors,
@ -355,9 +356,9 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
opts.max_timeout, opts.max_timeout,
); );
let end_time = Instant::now(); let end_time = Instant::now();
debug!("partial: {partial:?}"); debug!("partial: {is_partial:?}");
let duration = end_time - start_time; let duration = end_time - start_time;
if partial { if is_partial {
if movements.is_empty() { if movements.is_empty() {
info!("Pathfinder took {duration:?} (empty path)"); info!("Pathfinder took {duration:?} (empty path)");
} else { } else {
@ -375,7 +376,6 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
} }
path = movements.into_iter().collect::<VecDeque<_>>(); path = movements.into_iter().collect::<VecDeque<_>>();
is_partial = partial;
let goto_id_now = opts.goto_id_atomic.load(atomic::Ordering::SeqCst); let goto_id_now = opts.goto_id_atomic.load(atomic::Ordering::SeqCst);
if goto_id != goto_id_now { if goto_id != goto_id_now {
@ -384,7 +384,7 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
return None; return None;
} }
if path.is_empty() && partial { if path.is_empty() && is_partial {
debug!("this path is empty, we might be stuck :("); debug!("this path is empty, we might be stuck :(");
} }
@ -521,9 +521,20 @@ pub fn path_found_listener(
} }
pub fn timeout_movement( pub fn timeout_movement(
mut query: Query<(&Pathfinder, &mut ExecutingPath, &Position, Option<&Mining>)>, mut query: Query<(
Entity,
&mut Pathfinder,
&mut ExecutingPath,
&Position,
Option<&Mining>,
&InstanceName,
&Inventory,
)>,
instance_container: Res<InstanceContainer>,
) { ) {
for (pathfinder, mut executing_path, position, mining) in &mut query { for (entity, mut pathfinder, mut executing_path, position, mining, instance_name, inventory) in
&mut query
{
// don't timeout if we're mining // don't timeout if we're mining
if let Some(mining) = mining { if let Some(mining) = mining {
// also make sure we're close enough to the block that's being mined // also make sure we're close enough to the block that's being mined
@ -539,18 +550,29 @@ pub fn timeout_movement(
&& !pathfinder.is_calculating && !pathfinder.is_calculating
&& !executing_path.path.is_empty() && !executing_path.path.is_empty()
{ {
warn!("pathfinder timeout"); warn!("pathfinder timeout, trying to patch path");
// the path wasn't being followed anyways, so clearing it is fine
executing_path.path.clear();
executing_path.queued_path = None; executing_path.queued_path = None;
executing_path.last_reached_node = BlockPos::from(position); executing_path.last_reached_node = BlockPos::from(position);
// invalidate whatever calculation we were just doing, if any
pathfinder.goto_id.fetch_add(1, atomic::Ordering::Relaxed);
// set partial to true to make sure that a recalculation will happen
executing_path.is_path_partial = true;
// the path will get recalculated automatically because the path is let world_lock = instance_container
// empty .get(instance_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let successors_fn: moves::SuccessorsFn = pathfinder.successors_fn.unwrap();
// try to fix the path without recalculating everything.
// (though, it'll still get fully recalculated by `recalculate_near_end_of_path`
// if the new path is too short)
patch_path(
0..=cmp::min(20, executing_path.path.len() - 1),
&mut executing_path,
&mut pathfinder,
inventory,
entity,
successors_fn,
world_lock,
);
// reset last_node_reached_at so we don't immediately try to patch again
executing_path.last_node_reached_at = Instant::now();
} }
} }
} }
@ -653,14 +675,14 @@ pub fn check_node_reached(
pub fn check_for_path_obstruction( pub fn check_for_path_obstruction(
mut query: Query<( mut query: Query<(
Entity, Entity,
&Pathfinder, &mut Pathfinder,
&mut ExecutingPath, &mut ExecutingPath,
&InstanceName, &InstanceName,
&Inventory, &Inventory,
)>, )>,
instance_container: Res<InstanceContainer>, instance_container: Res<InstanceContainer>,
) { ) {
for (entity, pathfinder, mut executing_path, instance_name, inventory) in &mut query { for (entity, mut pathfinder, mut executing_path, instance_name, inventory) in &mut query {
let Some(successors_fn) = pathfinder.successors_fn else { let Some(successors_fn) = pathfinder.successors_fn else {
continue; continue;
}; };
@ -710,76 +732,109 @@ pub fn check_for_path_obstruction(
.get(instance_name) .get(instance_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world"); .expect("Entity tried to pathfind but the entity isn't in a valid world");
let patch_start = if obstructed_index == 0 {
executing_path.last_reached_node
} else {
executing_path.path[obstructed_index - 1].target
};
// patch up to 20 nodes // patch up to 20 nodes
let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1); let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1);
let patch_end = executing_path.path[patch_end_index].target;
// this doesn't override the main goal, it's just the goal for this A* patch_path(
// calculation obstructed_index..=patch_end_index,
let goal = Arc::new(BlockPosGoal(patch_end)); &mut executing_path,
&mut pathfinder,
let goto_id_atomic = pathfinder.goto_id.clone(); inventory,
let allow_mining = pathfinder.allow_mining;
let mining_cache = MiningCache::new(if allow_mining {
Some(inventory.inventory_menu.clone())
} else {
None
});
// the timeout is small enough that this doesn't need to be async
let path_found_event = calculate_path(CalculatePathOpts {
entity, entity,
start: patch_start,
goal,
successors_fn, successors_fn,
world_lock, world_lock,
goto_id_atomic, );
allow_mining, }
mining_cache, }
min_timeout: PathfinderTimeout::Nodes(10_000), }
max_timeout: PathfinderTimeout::Nodes(10_000),
});
debug!("obstruction patch: {path_found_event:?}");
let mut new_path = VecDeque::new(); /// update the given [`ExecutingPath`] to recalculate the path of the nodes in
if obstructed_index > 0 { /// the given index range.
new_path.extend(executing_path.path.iter().take(obstructed_index).cloned()); ///
} /// You should avoid making the range too large, since the timeout for the A*
/// calculation is very low. About 20 nodes is a good amount.
fn patch_path(
patch_nodes: RangeInclusive<usize>,
executing_path: &mut ExecutingPath,
pathfinder: &mut Pathfinder,
inventory: &Inventory,
entity: Entity,
successors_fn: SuccessorsFn,
world_lock: Arc<RwLock<azalea_world::Instance>>,
) {
let patch_start = if *patch_nodes.start() == 0 {
executing_path.last_reached_node
} else {
executing_path.path[*patch_nodes.start() - 1].target
};
let mut is_patch_complete = false; let patch_end = executing_path.path[*patch_nodes.end()].target;
if let Some(path_found_event) = path_found_event {
if let Some(found_path_patch) = path_found_event.path {
if !found_path_patch.is_empty() {
new_path.extend(found_path_patch);
if !path_found_event.is_partial { // this doesn't override the main goal, it's just the goal for this A*
new_path // calculation
.extend(executing_path.path.iter().skip(patch_end_index).cloned()); let goal = Arc::new(BlockPosGoal(patch_end));
is_patch_complete = true;
debug!("the obstruction patch is not partial :)"); let goto_id_atomic = pathfinder.goto_id.clone();
} else {
debug!( let allow_mining = pathfinder.allow_mining;
"the obstruction patch is partial, throwing away rest of path :(" let mining_cache = MiningCache::new(if allow_mining {
); Some(inventory.inventory_menu.clone())
} } else {
} None
});
// the timeout is small enough that this doesn't need to be async
let path_found_event = calculate_path(CalculatePathOpts {
entity,
start: patch_start,
goal,
successors_fn,
world_lock,
goto_id_atomic,
allow_mining,
mining_cache,
min_timeout: PathfinderTimeout::Nodes(10_000),
max_timeout: PathfinderTimeout::Nodes(10_000),
});
// this is necessary in case we interrupted another ongoing path calculation
pathfinder.is_calculating = false;
debug!("obstruction patch: {path_found_event:?}");
let mut new_path = VecDeque::new();
if *patch_nodes.start() > 0 {
new_path.extend(
executing_path
.path
.iter()
.take(*patch_nodes.start())
.cloned(),
);
}
let mut is_patch_complete = false;
if let Some(path_found_event) = path_found_event {
if let Some(found_path_patch) = path_found_event.path {
if !found_path_patch.is_empty() {
new_path.extend(found_path_patch);
if !path_found_event.is_partial {
new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
is_patch_complete = true;
debug!("the patch is not partial :)");
} else {
debug!("the patch is partial, throwing away rest of path :(");
} }
} else {
// no path found, rip
}
executing_path.path = new_path;
if !is_patch_complete {
executing_path.is_path_partial = true;
} }
} }
} else {
// no path found, rip
}
executing_path.path = new_path;
if !is_patch_complete {
executing_path.is_path_partial = true;
} }
} }
@ -795,7 +850,7 @@ pub fn recalculate_near_end_of_path(
}; };
// start recalculating if the path ends soon // start recalculating if the path ends soon
if (executing_path.path.len() == 20 || executing_path.path.len() < 5) if (executing_path.path.len() == 50 || executing_path.path.len() < 5)
&& !pathfinder.is_calculating && !pathfinder.is_calculating
&& executing_path.is_path_partial && executing_path.is_path_partial
{ {
@ -810,7 +865,13 @@ pub fn recalculate_near_end_of_path(
goal, goal,
successors_fn, successors_fn,
allow_mining: pathfinder.allow_mining, allow_mining: pathfinder.allow_mining,
min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"), min_timeout: if executing_path.path.len() == 50 {
// we have quite some time until the node is reached, soooo we might as well
// burn some cpu cycles to get a good path
PathfinderTimeout::Time(Duration::from_secs(5))
} else {
PathfinderTimeout::Time(Duration::from_secs(1))
},
max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"), max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
}); });
pathfinder.is_calculating = true; pathfinder.is_calculating = true;