Skip to main content

steel_core/entity/ai/
pathfinder.rs

1//! Vanilla-shaped A* path search.
2
3use 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}