azalea/pathfinder/execute/
patching.rs

1use std::{cmp, collections::VecDeque, ops::RangeInclusive, sync::Arc};
2
3use azalea_core::position::BlockPos;
4use azalea_entity::inventory::Inventory;
5use azalea_world::{WorldName, Worlds};
6use bevy_ecs::{
7    entity::Entity,
8    system::{Query, Res},
9};
10use parking_lot::RwLock;
11use tracing::{debug, error, warn};
12
13use crate::pathfinder::{
14    CalculatePathCtx, ExecutingPath, Pathfinder, PathfinderOpts,
15    astar::{self, PathfinderTimeout},
16    calculate_path, call_successors_fn,
17    custom_state::CustomPathfinderState,
18    goals::BlockPosGoal,
19    mining::MiningCache,
20    moves,
21    positions::RelBlockPos,
22    world::CachedWorld,
23};
24
25#[allow(clippy::type_complexity)]
26pub fn check_for_path_obstruction(
27    mut query: Query<(
28        Entity,
29        &mut Pathfinder,
30        &mut ExecutingPath,
31        &WorldName,
32        &Inventory,
33        Option<&CustomPathfinderState>,
34    )>,
35    worlds: Res<Worlds>,
36) {
37    for (entity, mut pathfinder, mut executing_path, world_name, inventory, custom_state) in
38        &mut query
39    {
40        let Some(opts) = pathfinder.opts.clone() else {
41            continue;
42        };
43
44        let world_lock = worlds
45            .get(world_name)
46            .expect("Entity tried to pathfind but the entity isn't in a valid world");
47
48        // obstruction check (the path we're executing isn't possible anymore)
49        let origin = executing_path.last_reached_node;
50        let cached_world = CachedWorld::new(world_lock, origin);
51        let mining_cache = MiningCache::new(if opts.allow_mining {
52            Some(inventory.inventory_menu.clone())
53        } else {
54            None
55        });
56        let custom_state = custom_state.cloned().unwrap_or_default();
57        let custom_state_ref = custom_state.0.read();
58        let successors = |pos: RelBlockPos| {
59            call_successors_fn(
60                &cached_world,
61                &mining_cache,
62                &custom_state_ref,
63                opts.successors_fn,
64                pos,
65            )
66        };
67
68        let Some(obstructed_index) = check_path_obstructed(
69            origin,
70            RelBlockPos::from_origin(origin, executing_path.last_reached_node),
71            &executing_path.path,
72            successors,
73        ) else {
74            continue;
75        };
76
77        drop(custom_state_ref);
78
79        warn!(
80            "path obstructed at index {obstructed_index} (starting at {:?})",
81            executing_path.last_reached_node,
82        );
83        debug!("obstructed path: {:?}", executing_path.path);
84        // if it's near the end, don't bother recalculating a patch, just truncate and
85        // mark it as partial
86        if obstructed_index + 5 > executing_path.path.len() {
87            debug!(
88                "obstruction is near the end of the path, truncating and marking path as partial"
89            );
90            executing_path.path.truncate(obstructed_index);
91            executing_path.is_path_partial = true;
92            continue;
93        }
94
95        let Some(opts) = pathfinder.opts.clone() else {
96            error!("got PatchExecutingPathEvent but the bot has no pathfinder opts");
97            continue;
98        };
99
100        let world_lock = worlds
101            .get(world_name)
102            .expect("Entity tried to pathfind but the entity isn't in a valid world");
103
104        // patch up to 20 nodes
105        let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1);
106
107        patch_path(
108            obstructed_index..=patch_end_index,
109            &mut executing_path,
110            &mut pathfinder,
111            inventory,
112            entity,
113            world_lock,
114            custom_state.clone(),
115            opts,
116        );
117    }
118}
119
120/// Update the given [`ExecutingPath`] to recalculate the path of the nodes in
121/// the given index range.
122///
123/// You should avoid making the range too large, since the timeout for the A*
124/// calculation is very low. About 20 nodes is a good amount.
125#[allow(clippy::too_many_arguments)]
126pub fn patch_path(
127    patch_nodes: RangeInclusive<usize>,
128    executing_path: &mut ExecutingPath,
129    pathfinder: &mut Pathfinder,
130    inventory: &Inventory,
131    entity: Entity,
132    world_lock: Arc<RwLock<azalea_world::World>>,
133    custom_state: CustomPathfinderState,
134    opts: PathfinderOpts,
135) {
136    let patch_start = if *patch_nodes.start() == 0 {
137        executing_path.last_reached_node
138    } else {
139        executing_path.path[*patch_nodes.start() - 1]
140            .movement
141            .target
142    };
143
144    let patch_end = executing_path.path[*patch_nodes.end()].movement.target;
145
146    // this doesn't override the main goal, it's just the goal for this A*
147    // calculation
148    let goal = Arc::new(BlockPosGoal(patch_end));
149
150    let goto_id_atomic = pathfinder.goto_id.clone();
151    let allow_mining = opts.allow_mining;
152
153    let mining_cache = MiningCache::new(if allow_mining {
154        Some(inventory.inventory_menu.clone())
155    } else {
156        None
157    });
158
159    // the timeout is small enough that this doesn't need to be async
160    let path_found_event = calculate_path(CalculatePathCtx {
161        entity,
162        start: patch_start,
163        goal,
164        world_lock,
165        goto_id_atomic,
166        mining_cache,
167        custom_state,
168        opts: PathfinderOpts {
169            min_timeout: PathfinderTimeout::Nodes(10_000),
170            max_timeout: PathfinderTimeout::Nodes(10_000),
171            ..opts
172        },
173    });
174
175    // this is necessary in case we interrupted another ongoing path calculation
176    pathfinder.is_calculating = false;
177
178    debug!("obstruction patch: {path_found_event:?}");
179
180    let mut new_path = VecDeque::new();
181    if *patch_nodes.start() > 0 {
182        new_path.extend(
183            executing_path
184                .path
185                .iter()
186                .take(*patch_nodes.start())
187                .cloned(),
188        );
189    }
190
191    let mut is_patch_complete = false;
192    if let Some(path_found_event) = path_found_event {
193        if let Some(found_path_patch) = path_found_event.path
194            && !found_path_patch.is_empty()
195        {
196            new_path.extend(found_path_patch);
197
198            if !path_found_event.is_partial {
199                new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
200                is_patch_complete = true;
201                debug!("the patch is not partial :)");
202            } else {
203                debug!("the patch is partial, throwing away rest of path :(");
204            }
205        }
206    } else {
207        // no path found, rip
208    }
209
210    executing_path.path = new_path;
211    if !is_patch_complete {
212        executing_path.is_path_partial = true;
213    }
214}
215
216/// Checks whether the path has been obstructed, and returns Some(index) if it
217/// has been.
218///
219/// The index is of the first obstructed node.
220pub fn check_path_obstructed<SuccessorsFn>(
221    origin: BlockPos,
222    mut current_position: RelBlockPos,
223    path: &VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
224    successors_fn: SuccessorsFn,
225) -> Option<usize>
226where
227    SuccessorsFn: Fn(RelBlockPos) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>>,
228{
229    for (i, edge) in path.iter().enumerate() {
230        let movement_target = RelBlockPos::from_origin(origin, edge.movement.target);
231
232        let mut found_edge = None;
233        for candidate_edge in successors_fn(current_position) {
234            if candidate_edge.movement.target == movement_target {
235                found_edge = Some(candidate_edge);
236                break;
237            }
238        }
239
240        current_position = movement_target;
241        // if found_edge is None or the cost increased, then return the index
242        if found_edge
243            .map(|found_edge| found_edge.cost > edge.cost)
244            .unwrap_or(true)
245        {
246            // if the node that we're currently executing was obstructed then it's often too
247            // late to change the path, so it's usually better to just ignore this case :/
248            if i == 0 {
249                warn!("path obstructed at index 0 ({edge:?}), ignoring");
250                continue;
251            }
252
253            return Some(i);
254        }
255    }
256
257    None
258}