azalea/pathfinder/execute/
patching.rs1use std::{
2 cmp,
3 collections::VecDeque,
4 ops::RangeInclusive,
5 sync::Arc,
6 time::{Duration, Instant},
7};
8
9use azalea_core::position::BlockPos;
10use azalea_entity::inventory::Inventory;
11use azalea_world::{WorldName, Worlds};
12use bevy_ecs::{
13 entity::Entity,
14 system::{Query, Res},
15};
16use parking_lot::RwLock;
17use tracing::{debug, error, warn};
18
19use crate::pathfinder::{
20 CalculatePathCtx, ExecutingPath, Pathfinder, PathfinderOpts,
21 astar::{self, PathfinderTimeout},
22 calculate_path, call_successors_fn,
23 custom_state::CustomPathfinderState,
24 goals::BlockPosGoal,
25 mining::MiningCache,
26 moves,
27 positions::RelBlockPos,
28 world::CachedWorld,
29};
30
31#[allow(clippy::type_complexity)]
32pub fn check_for_path_obstruction(
33 mut query: Query<(
34 Entity,
35 &mut Pathfinder,
36 &mut ExecutingPath,
37 &WorldName,
38 &Inventory,
39 Option<&CustomPathfinderState>,
40 )>,
41 worlds: Res<Worlds>,
42) {
43 query.par_iter_mut().for_each(|(entity, mut pathfinder, mut executing_path, world_name, inventory, custom_state)| {
44 let Some(opts) = pathfinder.opts.clone() else {
45 return;
46 };
47
48 let world_lock = worlds
49 .get(world_name)
50 .expect("Entity tried to pathfind but the entity isn't in a valid world");
51
52 let origin = executing_path.last_reached_node;
54 let cached_world = CachedWorld::new(world_lock, origin);
55 let mining_cache = MiningCache::new(if opts.allow_mining {
56 Some(inventory.inventory_menu.clone())
57 } else {
58 None
59 });
60 let custom_state = custom_state.cloned().unwrap_or_default();
61 let custom_state_ref = custom_state.0.read();
62 let successors = |pos: RelBlockPos| {
63 call_successors_fn(
64 &cached_world,
65 &mining_cache,
66 &custom_state_ref,
67 opts.successors_fn,
68 pos,
69 )
70 };
71
72 let timeout = Duration::from_millis(10);
74
75 let Some(obstructed_index) = check_path_obstructed(
76 origin,
77 RelBlockPos::from_origin(origin, executing_path.last_reached_node),
78 &executing_path.path,
79 successors,
80 timeout
81 ) else {
82 return;
83 };
84
85 drop(custom_state_ref);
86
87 warn!(
88 "path obstructed at index {obstructed_index} (starting at {:?})",
89 executing_path.last_reached_node,
90 );
91 debug!("obstructed path: {:?}", executing_path.path);
92 if obstructed_index + 5 > executing_path.path.len() {
95 debug!(
96 "obstruction is near the end of the path, truncating and marking path as partial"
97 );
98 executing_path.path.truncate(obstructed_index);
99 executing_path.is_path_partial = true;
100 return;
101 }
102
103 let Some(opts) = pathfinder.opts.clone() else {
104 error!("got PatchExecutingPathEvent but the bot has no pathfinder opts");
105 return;
106 };
107
108 let world_lock = worlds
109 .get(world_name)
110 .expect("Entity tried to pathfind but the entity isn't in a valid world");
111
112 let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1);
114
115 patch_path(
116 obstructed_index..=patch_end_index,
117 &mut executing_path,
118 &mut pathfinder,
119 inventory,
120 entity,
121 world_lock,
122 custom_state.clone(),
123 opts,
124 );
125 });
126}
127
128#[allow(clippy::too_many_arguments)]
134pub fn patch_path(
135 patch_nodes: RangeInclusive<usize>,
136 executing_path: &mut ExecutingPath,
137 pathfinder: &mut Pathfinder,
138 inventory: &Inventory,
139 entity: Entity,
140 world_lock: Arc<RwLock<azalea_world::World>>,
141 custom_state: CustomPathfinderState,
142 opts: PathfinderOpts,
143) {
144 let patch_start = if *patch_nodes.start() == 0 {
145 executing_path.last_reached_node
146 } else {
147 executing_path.path[*patch_nodes.start() - 1]
148 .movement
149 .target
150 };
151
152 let patch_end = executing_path.path[*patch_nodes.end()].movement.target;
153
154 let goal = Arc::new(BlockPosGoal(patch_end));
157
158 let goto_id_atomic = pathfinder.goto_id.clone();
159 let allow_mining = opts.allow_mining;
160
161 let mining_cache = MiningCache::new(if allow_mining {
162 Some(inventory.inventory_menu.clone())
163 } else {
164 None
165 });
166
167 let path_found_event = calculate_path(CalculatePathCtx {
169 entity,
170 start: patch_start,
171 goal,
172 world_lock,
173 goto_id_atomic,
174 mining_cache,
175 custom_state,
176 opts: PathfinderOpts {
177 min_timeout: PathfinderTimeout::Nodes(10_000),
178 max_timeout: PathfinderTimeout::Nodes(10_000),
179 ..opts
180 },
181 });
182
183 pathfinder.is_calculating = false;
185
186 debug!("obstruction patch: {path_found_event:?}");
187
188 let mut new_path = VecDeque::new();
189 if *patch_nodes.start() > 0 {
190 new_path.extend(
191 executing_path
192 .path
193 .iter()
194 .take(*patch_nodes.start())
195 .cloned(),
196 );
197 }
198
199 let mut is_patch_complete = false;
200 if let Some(path_found_event) = path_found_event {
201 if let Some(found_path_patch) = path_found_event.path
202 && !found_path_patch.is_empty()
203 {
204 new_path.extend(found_path_patch);
205
206 if !path_found_event.is_partial {
207 new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
208 is_patch_complete = true;
209 debug!("the patch is not partial :)");
210 } else {
211 debug!("the patch is partial, throwing away rest of path :(");
212 }
213 }
214 } else {
215 }
217
218 executing_path.path = new_path;
219 if !is_patch_complete {
220 executing_path.is_path_partial = true;
221 }
222}
223
224pub fn check_path_obstructed<SuccessorsFn>(
229 origin: BlockPos,
230 mut current_position: RelBlockPos,
231 path: &VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
232 successors_fn: SuccessorsFn,
233 timeout: Duration,
234) -> Option<usize>
235where
236 SuccessorsFn: Fn(RelBlockPos) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>>,
237{
238 let start_time = Instant::now();
239
240 for (i, edge) in path.iter().enumerate() {
241 if start_time.elapsed() > timeout {
242 break;
243 }
244
245 let movement_target = RelBlockPos::from_origin(origin, edge.movement.target);
246
247 let mut found_edge = None;
248 for candidate_edge in successors_fn(current_position) {
249 if candidate_edge.movement.target == movement_target {
250 found_edge = Some(candidate_edge);
251 break;
252 }
253 }
254
255 current_position = movement_target;
256 if found_edge
258 .map(|found_edge| found_edge.cost > edge.cost)
259 .unwrap_or(true)
260 {
261 if i == 0 {
264 warn!("path obstructed at index 0 ({edge:?}), ignoring");
265 continue;
266 }
267
268 return Some(i);
269 }
270 }
271
272 None
273}