1use std::cmp::Ordering;
4
5use steel_utils::BlockPos;
6
7use crate::entity::ai::node::{Node, NodeHeap, Target};
8use crate::entity::ai::path::{Path, PathfindingContext};
9use crate::entity::ai::walk::{WalkNodeCollision, WalkNodeEvaluator};
10
11const FUDGING: f32 = 1.5;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct PathRequest<'a> {
15 pub targets: &'a [BlockPos],
16 pub max_path_length: f32,
17 pub reach_range: i32,
18 pub max_visited_nodes_multiplier: f32,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct PathFinder {
23 max_visited_nodes: i32,
24 open_set: NodeHeap,
25}
26
27impl PathFinder {
28 #[must_use]
29 pub const fn new(max_visited_nodes: i32) -> Self {
30 Self {
31 max_visited_nodes,
32 open_set: NodeHeap::new(),
33 }
34 }
35
36 #[must_use]
37 pub const fn max_visited_nodes(&self) -> i32 {
38 self.max_visited_nodes
39 }
40
41 pub const fn set_max_visited_nodes(&mut self, max_visited_nodes: i32) {
42 self.max_visited_nodes = max_visited_nodes;
43 }
44
45 #[must_use]
46 pub fn find_path(
47 &mut self,
48 evaluator: &mut WalkNodeEvaluator,
49 context: &mut PathfindingContext<'_>,
50 collision: &mut impl WalkNodeCollision,
51 request: PathRequest<'_>,
52 ) -> Option<Path> {
53 let from = evaluator.get_start(context);
54 let (from_point, mut target_entries) =
55 self.prepare_search(evaluator, from, request.targets)?;
56 let max_visited_nodes_adjusted =
57 (self.max_visited_nodes as f32 * request.max_visited_nodes_multiplier) as i32;
58 let mut reached_targets = Vec::new();
59 let mut count = 0;
60 while !self.open_set.is_empty() {
61 count += 1;
62 if count >= max_visited_nodes_adjusted {
63 break;
64 }
65
66 let Some(current_hash) = self.open_set.pop(evaluator.nodes_mut()) else {
67 break;
68 };
69 {
70 let Some(current) = evaluator.node_mut(current_hash) else {
71 continue;
72 };
73 current.closed = true;
74 }
75 let Some(current_data) = evaluator.node(current_hash).map(NodeSearchData::from_node)
76 else {
77 continue;
78 };
79
80 for (index, target) in target_entries.iter_mut().enumerate() {
81 if current_data.point.manhattan_to_node(target.target.node())
82 <= request.reach_range as f32
83 {
84 target.target.set_reached();
85 if !reached_targets.contains(&index) {
86 reached_targets.push(index);
87 }
88 }
89 }
90 if !reached_targets.is_empty() {
91 break;
92 }
93
94 if current_data.point.distance_to(from_point) >= request.max_path_length {
95 continue;
96 }
97
98 let neighbors = evaluator.get_neighbors(context, collision, current_hash);
99 for neighbor_hash in neighbors.iter() {
100 let Some(neighbor_data) =
101 evaluator.node(neighbor_hash).map(NodeSearchData::from_node)
102 else {
103 continue;
104 };
105 let distance = current_data.point.distance_to(neighbor_data.point);
106 let walked_distance = current_data.walked_distance + distance;
107 let tentative_g_score = current_data.g + distance + neighbor_data.cost_malus;
108 if walked_distance >= request.max_path_length {
109 continue;
110 }
111 if neighbor_data.in_open_set && tentative_g_score >= neighbor_data.g {
112 continue;
113 }
114
115 let h =
116 Self::get_best_h(evaluator.node(neighbor_hash)?, &mut target_entries) * FUDGING;
117 let f = tentative_g_score + h;
118 {
119 let Some(neighbor) = evaluator.node_mut(neighbor_hash) else {
120 continue;
121 };
122 neighbor.came_from = Some(current_hash);
123 neighbor.g = tentative_g_score;
124 neighbor.h = h;
125 neighbor.walked_distance = walked_distance;
126 }
127
128 if neighbor_data.in_open_set {
129 if !self
130 .open_set
131 .change_cost(evaluator.nodes_mut(), neighbor_hash, f)
132 {
133 return None;
134 }
135 } else {
136 let Some(neighbor) = evaluator.node_mut(neighbor_hash) else {
137 continue;
138 };
139 neighbor.f = f;
140 if !self.open_set.insert(evaluator.nodes_mut(), neighbor_hash) {
141 return None;
142 }
143 }
144 }
145 }
146
147 if reached_targets.is_empty() {
148 Self::best_unreached_path(evaluator, &target_entries)
149 } else {
150 Self::best_reached_path(evaluator, &target_entries, &reached_targets)
151 }
152 }
153
154 fn prepare_search(
155 &mut self,
156 evaluator: &mut WalkNodeEvaluator,
157 from: i32,
158 targets: &[BlockPos],
159 ) -> Option<(NodePoint, Vec<PathTarget>)> {
160 if targets.is_empty() {
161 return None;
162 }
163
164 evaluator.reset_search_state();
165 self.open_set.clear(evaluator.nodes_mut());
166 let mut target_entries = targets
167 .iter()
168 .copied()
169 .map(PathTarget::new)
170 .collect::<Vec<_>>();
171 let from_point = NodePoint::from_node(evaluator.node(from)?);
172 let from_h = Self::get_best_h(evaluator.node(from)?, &mut target_entries);
173 {
174 let from_node = evaluator.node_mut(from)?;
175 from_node.g = 0.0;
176 from_node.h = from_h;
177 from_node.f = from_h;
178 from_node.walked_distance = 0.0;
179 from_node.came_from = None;
180 from_node.closed = false;
181 }
182 if !self.open_set.insert(evaluator.nodes_mut(), from) {
183 return None;
184 }
185
186 Some((from_point, target_entries))
187 }
188
189 fn get_best_h(from: &Node, targets: &mut [PathTarget]) -> f32 {
190 let mut best_h = f32::MAX;
191 for target in targets {
192 let h = from.distance_to(target.target.node());
193 target.target.update_best(h, from);
194 best_h = best_h.min(h);
195 }
196 best_h
197 }
198
199 fn best_reached_path(
200 evaluator: &WalkNodeEvaluator,
201 targets: &[PathTarget],
202 reached_targets: &[usize],
203 ) -> Option<Path> {
204 let mut best = None;
205 for index in reached_targets {
206 let Some(target) = targets.get(*index) else {
207 continue;
208 };
209 let Some(path) =
210 Self::reconstruct_path(evaluator, target.target.best_node(), target.pos, true)
211 else {
212 continue;
213 };
214 if best
215 .as_ref()
216 .is_none_or(|best_path: &Path| path.node_count() < best_path.node_count())
217 {
218 best = Some(path);
219 }
220 }
221 best
222 }
223
224 fn best_unreached_path(evaluator: &WalkNodeEvaluator, targets: &[PathTarget]) -> Option<Path> {
225 let mut best = None;
226 for target in targets {
227 let Some(path) =
228 Self::reconstruct_path(evaluator, target.target.best_node(), target.pos, false)
229 else {
230 continue;
231 };
232 if best
233 .as_ref()
234 .is_none_or(|best_path: &Path| compare_unreached_paths(&path, best_path).is_lt())
235 {
236 best = Some(path);
237 }
238 }
239 best
240 }
241
242 fn reconstruct_path(
243 evaluator: &WalkNodeEvaluator,
244 closest: Option<i32>,
245 target: BlockPos,
246 reached: bool,
247 ) -> Option<Path> {
248 let mut hashes = Vec::new();
249 let mut current_hash = closest?;
250 loop {
251 hashes.push(current_hash);
252 let node = evaluator.node(current_hash)?;
253 let Some(came_from) = node.came_from else {
254 break;
255 };
256 current_hash = came_from;
257 }
258 hashes.reverse();
259
260 let mut nodes = Vec::with_capacity(hashes.len());
261 for hash in hashes {
262 nodes.push(path_node_from(evaluator.node(hash)?));
263 }
264 Some(Path::new(nodes, target, reached))
265 }
266}
267
268impl Default for PathFinder {
269 fn default() -> Self {
270 Self::new(200)
271 }
272}
273
274#[derive(Debug, Clone, PartialEq)]
275struct PathTarget {
276 target: Target,
277 pos: BlockPos,
278}
279
280impl PathTarget {
281 const fn new(pos: BlockPos) -> Self {
282 Self {
283 target: Target::new(Node::new(pos.x(), pos.y(), pos.z())),
284 pos,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290struct NodePoint {
291 x: i32,
292 y: i32,
293 z: i32,
294}
295
296impl NodePoint {
297 const fn from_node(node: &Node) -> Self {
298 Self {
299 x: node.x,
300 y: node.y,
301 z: node.z,
302 }
303 }
304
305 fn distance_to(self, other: Self) -> f32 {
306 let xd = (other.x - self.x) as f32;
307 let yd = (other.y - self.y) as f32;
308 let zd = (other.z - self.z) as f32;
309 xd.mul_add(xd, yd.mul_add(yd, zd * zd)).sqrt()
310 }
311
312 fn manhattan_to_node(self, other: &Node) -> f32 {
313 (other.x - self.x).abs() as f32
314 + (other.y - self.y).abs() as f32
315 + (other.z - self.z).abs() as f32
316 }
317}
318
319#[derive(Debug, Clone, Copy, PartialEq)]
320struct NodeSearchData {
321 point: NodePoint,
322 g: f32,
323 walked_distance: f32,
324 cost_malus: f32,
325 in_open_set: bool,
326}
327
328impl NodeSearchData {
329 const fn from_node(node: &Node) -> Self {
330 Self {
331 point: NodePoint::from_node(node),
332 g: node.g,
333 walked_distance: node.walked_distance,
334 cost_malus: node.cost_malus,
335 in_open_set: node.in_open_set(),
336 }
337 }
338}
339
340fn compare_unreached_paths(left: &Path, right: &Path) -> Ordering {
341 left.dist_to_target()
342 .total_cmp(&right.dist_to_target())
343 .then_with(|| left.node_count().cmp(&right.node_count()))
344}
345
346const fn path_node_from(node: &Node) -> Node {
347 let mut path_node = Node::new(node.x, node.y, node.z);
348 path_node.g = node.g;
349 path_node.h = node.h;
350 path_node.f = node.f;
351 path_node.came_from = node.came_from;
352 path_node.closed = node.closed;
353 path_node.walked_distance = node.walked_distance;
354 path_node.cost_malus = node.cost_malus;
355 path_node.path_type = node.path_type;
356 path_node
357}
358
359#[cfg(test)]
360mod tests {
361 use std::ops::RangeInclusive;
362
363 use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_blocks};
364 use steel_utils::{BlockPos, BlockStateId, WorldAabb};
365
366 use super::{PathFinder, PathRequest};
367 use crate::behavior::init_behaviors;
368 use crate::entity::ai::node::Node;
369 use crate::entity::ai::path::{PathfindingContext, PathfindingMalus};
370 use crate::entity::ai::walk::{MobPathSettings, WalkNodeEvaluator};
371 use crate::world::LevelReader;
372
373 struct GridLevel {
374 default_state: BlockStateId,
375 states: Vec<(BlockPos, BlockStateId)>,
376 }
377
378 impl GridLevel {
379 fn new(default_state: BlockStateId) -> Self {
380 Self {
381 default_state,
382 states: Vec::new(),
383 }
384 }
385
386 fn with(mut self, pos: BlockPos, state: BlockStateId) -> Self {
387 self.states.push((pos, state));
388 self
389 }
390 }
391
392 impl LevelReader for GridLevel {
393 fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
394 self.states
395 .iter()
396 .find_map(|(state_pos, state)| (*state_pos == pos).then_some(*state))
397 .unwrap_or(self.default_state)
398 }
399
400 fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
401 0
402 }
403
404 fn min_y(&self) -> i32 {
405 -64
406 }
407
408 fn height(&self) -> i32 {
409 384
410 }
411 }
412
413 #[test]
414 fn pathfinder_finds_direct_walkable_path() {
415 init_vanilla_registry();
416 init_behaviors();
417
418 let level = flat_level(0..=2, -1..=1);
419 let mut context = PathfindingContext::new(&level, BlockPos::new(0, 64, 0));
420 let mut evaluator = WalkNodeEvaluator::new(test_settings(1, 1, 1));
421 let mut no_collision = |_aabb: WorldAabb| false;
422 let mut pathfinder = PathFinder::new(128);
423
424 let path = pathfinder.find_path(
425 &mut evaluator,
426 &mut context,
427 &mut no_collision,
428 PathRequest {
429 targets: &[BlockPos::new(2, 64, 0)],
430 max_path_length: 16.0,
431 reach_range: 0,
432 max_visited_nodes_multiplier: 1.0,
433 },
434 );
435
436 let Some(path) = path else {
437 panic!("path should be found");
438 };
439 assert!(path.can_reach());
440 assert_eq!(path.node_count(), 3);
441 assert_eq!(
442 path.nodes()
443 .iter()
444 .map(Node::as_block_pos)
445 .collect::<Vec<_>>(),
446 vec![
447 BlockPos::new(0, 64, 0),
448 BlockPos::new(1, 64, 0),
449 BlockPos::new(2, 64, 0)
450 ]
451 );
452 }
453
454 #[test]
455 fn pathfinder_returns_closest_path_when_target_is_not_reached() {
456 init_vanilla_registry();
457 init_behaviors();
458
459 let level = flat_level(0..=4, -1..=1);
460 let mut context = PathfindingContext::new(&level, BlockPos::new(0, 64, 0));
461 let mut evaluator = WalkNodeEvaluator::new(test_settings(1, 1, 1));
462 let mut no_collision = |_aabb: WorldAabb| false;
463 let mut pathfinder = PathFinder::new(128);
464
465 let path = pathfinder.find_path(
466 &mut evaluator,
467 &mut context,
468 &mut no_collision,
469 PathRequest {
470 targets: &[BlockPos::new(4, 64, 0)],
471 max_path_length: 1.5,
472 reach_range: 0,
473 max_visited_nodes_multiplier: 1.0,
474 },
475 );
476
477 let Some(path) = path else {
478 panic!("closest path should be returned");
479 };
480 assert!(!path.can_reach());
481 assert_eq!(
482 path.end_node().map(Node::as_block_pos),
483 Some(BlockPos::new(1, 64, 0))
484 );
485 assert_eq!(path.dist_to_target().to_bits(), 3.0_f32.to_bits());
486 }
487
488 fn flat_level(x_range: RangeInclusive<i32>, z_range: RangeInclusive<i32>) -> GridLevel {
489 let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
490 let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
491 let mut level = GridLevel::new(air);
492 for x in x_range {
493 for z in z_range.clone() {
494 level = level.with(BlockPos::new(x, 63, z), stone);
495 }
496 }
497 level
498 }
499
500 fn test_settings(entity_width: i32, entity_height: i32, entity_depth: i32) -> MobPathSettings {
501 MobPathSettings::new(
502 entity_width,
503 entity_height,
504 entity_depth,
505 BlockPos::new(0, 64, 0),
506 &PathfindingMalus::new(),
507 )
508 }
509}