1use steel_utils::{BlockPos, BlockStateId, PackedBlockPos};
4
5use crate::entity::ai::node::Node;
6use crate::entity::ai::walk::WalkPathEvaluator;
7use crate::world::LevelReader;
8
9const PATH_TYPE_CACHE_SIZE: usize = 4096;
10const PATH_TYPE_CACHE_MASK: usize = PATH_TYPE_CACHE_SIZE - 1;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[repr(u8)]
19pub enum PathType {
20 Blocked,
21 Open,
22 Walkable,
23 WalkableDoor,
24 Trapdoor,
25 PowderSnow,
26 OnTopOfPowderSnow,
27 Fence,
28 Lava,
29 Water,
30 WaterBorder,
31 Rail,
32 UnpassableRail,
33 FireInNeighbor,
34 Fire,
35 DamagingInNeighbor,
36 Damaging,
37 DoorOpen,
38 DoorWoodClosed,
39 DoorIronClosed,
40 Breach,
41 Leaves,
42 StickyHoney,
43 Cocoa,
44 DamageCautious,
45 OnTopOfTrapdoor,
46 BigMobsCloseToDanger,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum PathComputationType {
52 Land,
53 Water,
54 Air,
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct Path {
59 nodes: Vec<Node>,
60 next_node_index: usize,
61 target: BlockPos,
62 dist_to_target: f32,
63 reached: bool,
64}
65
66impl Path {
67 #[must_use]
68 pub fn new(nodes: Vec<Node>, target: BlockPos, reached: bool) -> Self {
69 let dist_to_target = nodes
70 .last()
71 .map_or(f32::MAX, |node| node.distance_manhattan_to_pos(target));
72 Self {
73 nodes,
74 next_node_index: 0,
75 target,
76 dist_to_target,
77 reached,
78 }
79 }
80
81 pub const fn advance(&mut self) {
82 self.next_node_index += 1;
83 }
84
85 #[must_use]
86 pub const fn not_started(&self) -> bool {
87 self.next_node_index == 0
88 }
89
90 #[must_use]
91 pub const fn is_done(&self) -> bool {
92 self.next_node_index >= self.nodes.len()
93 }
94
95 #[must_use]
96 pub fn end_node(&self) -> Option<&Node> {
97 self.nodes.last()
98 }
99
100 #[must_use]
101 pub fn node(&self, index: usize) -> Option<&Node> {
102 self.nodes.get(index)
103 }
104
105 pub fn truncate_nodes(&mut self, index: usize) {
106 if self.nodes.len() > index {
107 self.nodes.truncate(index);
108 }
109 }
110
111 pub fn replace_node(&mut self, index: usize, replace_with: Node) -> bool {
112 let Some(node) = self.nodes.get_mut(index) else {
113 return false;
114 };
115 *node = replace_with;
116 true
117 }
118
119 #[must_use]
120 pub const fn node_count(&self) -> usize {
121 self.nodes.len()
122 }
123
124 #[must_use]
125 pub const fn next_node_index(&self) -> usize {
126 self.next_node_index
127 }
128
129 pub const fn set_next_node_index(&mut self, next_node_index: usize) {
130 self.next_node_index = next_node_index;
131 }
132
133 #[must_use]
134 pub fn node_pos(&self, index: usize) -> Option<BlockPos> {
135 self.node(index).map(Node::as_block_pos)
136 }
137
138 #[must_use]
139 pub fn next_node_pos(&self) -> Option<BlockPos> {
140 self.node_pos(self.next_node_index)
141 }
142
143 #[must_use]
144 pub fn next_node(&self) -> Option<&Node> {
145 self.node(self.next_node_index)
146 }
147
148 #[must_use]
149 pub fn previous_node(&self) -> Option<&Node> {
150 self.next_node_index
151 .checked_sub(1)
152 .and_then(|index| self.node(index))
153 }
154
155 #[must_use]
156 pub fn same_as(&self, path: &Self) -> bool {
157 self.nodes.len() == path.nodes.len()
158 && self
159 .nodes
160 .iter()
161 .zip(path.nodes.iter())
162 .all(|(left, right)| left.hash() == right.hash())
163 }
164
165 #[must_use]
166 pub const fn can_reach(&self) -> bool {
167 self.reached
168 }
169
170 #[must_use]
171 pub const fn target(&self) -> BlockPos {
172 self.target
173 }
174
175 #[must_use]
176 pub const fn dist_to_target(&self) -> f32 {
177 self.dist_to_target
178 }
179
180 #[must_use]
181 pub fn nodes(&self) -> &[Node] {
182 &self.nodes
183 }
184}
185
186impl PathType {
187 pub const ALL: [Self; Self::COUNT] = [
188 Self::Blocked,
189 Self::Open,
190 Self::Walkable,
191 Self::WalkableDoor,
192 Self::Trapdoor,
193 Self::PowderSnow,
194 Self::OnTopOfPowderSnow,
195 Self::Fence,
196 Self::Lava,
197 Self::Water,
198 Self::WaterBorder,
199 Self::Rail,
200 Self::UnpassableRail,
201 Self::FireInNeighbor,
202 Self::Fire,
203 Self::DamagingInNeighbor,
204 Self::Damaging,
205 Self::DoorOpen,
206 Self::DoorWoodClosed,
207 Self::DoorIronClosed,
208 Self::Breach,
209 Self::Leaves,
210 Self::StickyHoney,
211 Self::Cocoa,
212 Self::DamageCautious,
213 Self::OnTopOfTrapdoor,
214 Self::BigMobsCloseToDanger,
215 ];
216 pub const COUNT: usize = 27;
217
218 #[must_use]
219 pub const fn index(self) -> usize {
220 self as usize
221 }
222
223 #[must_use]
224 #[expect(
225 clippy::match_same_arms,
226 reason = "one arm per vanilla PathType keeps the default table auditable"
227 )]
228 pub const fn default_malus(self) -> f32 {
229 match self {
230 Self::Blocked => -1.0,
231 Self::Open => 0.0,
232 Self::Walkable => 0.0,
233 Self::WalkableDoor => 0.0,
234 Self::Trapdoor => 0.0,
235 Self::PowderSnow => -1.0,
236 Self::OnTopOfPowderSnow => 0.0,
237 Self::Fence => -1.0,
238 Self::Lava => -1.0,
239 Self::Water => 8.0,
240 Self::WaterBorder => 8.0,
241 Self::Rail => 0.0,
242 Self::UnpassableRail => -1.0,
243 Self::FireInNeighbor => 8.0,
244 Self::Fire => 16.0,
245 Self::DamagingInNeighbor => 8.0,
246 Self::Damaging => -1.0,
247 Self::DoorOpen => 0.0,
248 Self::DoorWoodClosed => -1.0,
249 Self::DoorIronClosed => -1.0,
250 Self::Breach => 4.0,
251 Self::Leaves => -1.0,
252 Self::StickyHoney => 8.0,
253 Self::Cocoa => 0.0,
254 Self::DamageCautious => 0.0,
255 Self::OnTopOfTrapdoor => 0.0,
256 Self::BigMobsCloseToDanger => 4.0,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
262pub struct PathTypeSet {
263 bits: u32,
264}
265
266impl PathTypeSet {
267 #[must_use]
268 pub const fn new() -> Self {
269 Self { bits: 0 }
270 }
271
272 #[must_use]
273 pub const fn from_path_type(path_type: PathType) -> Self {
274 Self {
275 bits: Self::bit(path_type),
276 }
277 }
278
279 pub const fn insert(&mut self, path_type: PathType) {
280 self.bits |= Self::bit(path_type);
281 }
282
283 #[must_use]
284 pub const fn contains(self, path_type: PathType) -> bool {
285 self.bits & Self::bit(path_type) != 0
286 }
287
288 #[must_use]
289 pub const fn len(self) -> u32 {
290 self.bits.count_ones()
291 }
292
293 #[must_use]
294 pub const fn is_empty(self) -> bool {
295 self.bits == 0
296 }
297
298 #[must_use]
299 pub const fn single(self) -> Option<PathType> {
300 if self.bits.is_power_of_two() {
301 Some(PathType::ALL[self.bits.trailing_zeros() as usize])
302 } else {
303 None
304 }
305 }
306
307 pub fn iter(self) -> impl Iterator<Item = PathType> {
308 PathType::ALL
309 .into_iter()
310 .filter(move |path_type| self.contains(*path_type))
311 }
312
313 const fn bit(path_type: PathType) -> u32 {
314 1 << path_type.index()
315 }
316}
317
318#[derive(Debug, Clone)]
319pub struct PathfindingMalus {
320 overrides: [Option<f32>; PathType::COUNT],
321}
322
323impl PathfindingMalus {
324 #[must_use]
325 pub const fn new() -> Self {
326 Self {
327 overrides: [None; PathType::COUNT],
328 }
329 }
330
331 #[must_use]
332 pub fn get(&self, path_type: PathType) -> f32 {
333 self.overrides[path_type.index()].unwrap_or_else(|| path_type.default_malus())
334 }
335
336 pub const fn set(&mut self, path_type: PathType, malus: f32) {
337 self.overrides[path_type.index()] = Some(malus);
338 }
339}
340
341impl Default for PathfindingMalus {
342 fn default() -> Self {
343 Self::new()
344 }
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct PathTypeCache {
349 positions: Box<[i64]>,
350 path_types: Box<[Option<PathType>]>,
351}
352
353impl PathTypeCache {
354 #[must_use]
355 pub fn new() -> Self {
356 Self {
357 positions: vec![0; PATH_TYPE_CACHE_SIZE].into_boxed_slice(),
358 path_types: vec![None; PATH_TYPE_CACHE_SIZE].into_boxed_slice(),
359 }
360 }
361
362 #[must_use]
363 pub fn get_or_compute(
364 &mut self,
365 level: &(impl LevelReader + ?Sized),
366 pos: BlockPos,
367 ) -> PathType {
368 let key = PackedBlockPos::from(pos).as_raw();
369 let index = Self::index(key);
370 if self.positions[index] == key
371 && let Some(path_type) = self.path_types[index]
372 {
373 return path_type;
374 }
375
376 let path_type = WalkPathEvaluator::path_type_from_state(level, pos);
377 self.positions[index] = key;
378 self.path_types[index] = Some(path_type);
379 path_type
380 }
381
382 pub fn invalidate(&mut self, pos: BlockPos) {
383 let key = PackedBlockPos::from(pos).as_raw();
384 let index = Self::index(key);
385 if self.positions[index] == key {
386 self.path_types[index] = None;
387 }
388 }
389
390 const fn index(pos: i64) -> usize {
391 (fastutil_mix(pos as u64) as usize) & PATH_TYPE_CACHE_MASK
392 }
393}
394
395impl Default for PathTypeCache {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401pub struct PathfindingContext<'a> {
402 level: &'a dyn LevelReader,
403 cache: Option<&'a mut PathTypeCache>,
404 mob_position: BlockPos,
405}
406
407impl<'a> PathfindingContext<'a> {
408 #[must_use]
409 pub const fn new(level: &'a dyn LevelReader, mob_position: BlockPos) -> Self {
410 Self {
411 level,
412 cache: None,
413 mob_position,
414 }
415 }
416
417 #[must_use]
418 pub fn with_cache(
419 level: &'a dyn LevelReader,
420 mob_position: BlockPos,
421 cache: &'a mut PathTypeCache,
422 ) -> Self {
423 Self {
424 level,
425 cache: Some(cache),
426 mob_position,
427 }
428 }
429
430 #[must_use]
431 pub fn get_path_type_from_state(&mut self, x: i32, y: i32, z: i32) -> PathType {
432 let pos = BlockPos::new(x, y, z);
433 match self.cache.as_deref_mut() {
434 Some(cache) => cache.get_or_compute(self.level, pos),
435 None => WalkPathEvaluator::path_type_from_state(self.level, pos),
436 }
437 }
438
439 #[must_use]
440 pub fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
441 self.level.get_block_state(pos)
442 }
443
444 #[must_use]
445 pub const fn level(&self) -> &dyn LevelReader {
446 self.level
447 }
448
449 #[must_use]
450 pub const fn mob_position(&self) -> BlockPos {
451 self.mob_position
452 }
453}
454
455const fn fastutil_mix(value: u64) -> u64 {
456 let mixed = value.wrapping_mul(0x9E37_79B9_7F4A_7C15);
457 let mixed = mixed ^ (mixed >> 32);
458 mixed ^ (mixed >> 16)
459}
460
461#[cfg(test)]
462mod tests {
463 use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_blocks};
464 use steel_utils::{BlockPos, BlockStateId};
465 use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
466
467 use super::{
468 PATH_TYPE_CACHE_MASK, Path, PathType, PathTypeCache, PathTypeSet, PathfindingContext,
469 PathfindingMalus,
470 };
471 use crate::entity::ai::node::Node;
472 use crate::world::LevelReader;
473
474 struct SingleBlockLevel {
475 state: BlockStateId,
476 }
477
478 impl LevelReader for SingleBlockLevel {
479 fn get_block_state(&self, _pos: steel_utils::BlockPos) -> BlockStateId {
480 self.state
481 }
482
483 fn raw_brightness(&self, _pos: steel_utils::BlockPos, _sky_darkening: u8) -> u8 {
484 0
485 }
486
487 fn min_y(&self) -> i32 {
488 OverworldNoiseSettings::MIN_Y
489 }
490
491 fn height(&self) -> i32 {
492 OverworldNoiseSettings::HEIGHT
493 }
494
495 fn sea_level(&self) -> i32 {
496 OverworldNoiseSettings::SEA_LEVEL
497 }
498 }
499
500 #[test]
501 fn default_malus_matches_vanilla_path_types() {
502 assert_eq!(
503 PathType::Blocked.default_malus().to_bits(),
504 (-1.0_f32).to_bits()
505 );
506 assert_eq!(
507 PathType::Walkable.default_malus().to_bits(),
508 0.0_f32.to_bits()
509 );
510 assert_eq!(PathType::Water.default_malus().to_bits(), 8.0_f32.to_bits());
511 assert_eq!(PathType::Fire.default_malus().to_bits(), 16.0_f32.to_bits());
512 assert_eq!(
513 PathType::BigMobsCloseToDanger.default_malus().to_bits(),
514 4.0_f32.to_bits()
515 );
516 }
517
518 #[test]
519 fn malus_overrides_are_indexed_by_path_type() {
520 let mut malus = PathfindingMalus::new();
521 assert_eq!(malus.get(PathType::Fire).to_bits(), 16.0_f32.to_bits());
522
523 malus.set(PathType::Fire, -1.0);
524
525 assert_eq!(malus.get(PathType::Fire).to_bits(), (-1.0_f32).to_bits());
526 assert_eq!(malus.get(PathType::Water).to_bits(), 8.0_f32.to_bits());
527 }
528
529 #[test]
530 fn path_type_set_is_bit_indexed_by_vanilla_path_type_order() {
531 let mut set = PathTypeSet::new();
532 assert!(set.is_empty());
533 set.insert(PathType::Water);
534 set.insert(PathType::Fence);
535
536 assert_eq!(set.len(), 2);
537 assert!(set.contains(PathType::Water));
538 assert!(set.contains(PathType::Fence));
539 assert!(!set.contains(PathType::Open));
540 assert_eq!(
541 PathTypeSet::from_path_type(PathType::Rail).single(),
542 Some(PathType::Rail)
543 );
544 assert_eq!(
545 set.iter().collect::<Vec<_>>(),
546 vec![PathType::Fence, PathType::Water]
547 );
548 }
549
550 #[test]
551 fn path_type_cache_uses_vanilla_direct_mapped_size() {
552 assert_eq!(PATH_TYPE_CACHE_MASK, 4095);
553 }
554
555 #[test]
556 fn path_tracks_progress_and_target_distance() {
557 let mut path = Path::new(
558 vec![Node::new(0, 64, 0), Node::new(2, 64, 1)],
559 BlockPos::new(4, 64, 1),
560 false,
561 );
562
563 assert!(path.not_started());
564 assert!(!path.is_done());
565 assert_eq!(path.node_count(), 2);
566 assert_eq!(path.next_node_pos(), Some(BlockPos::new(0, 64, 0)));
567 assert_eq!(path.dist_to_target().to_bits(), 2.0_f32.to_bits());
568 assert!(!path.can_reach());
569
570 path.advance();
571
572 assert_eq!(
573 path.previous_node().map(Node::as_block_pos),
574 Some(BlockPos::new(0, 64, 0))
575 );
576 assert_eq!(path.next_node_pos(), Some(BlockPos::new(2, 64, 1)));
577 }
578
579 #[test]
580 fn path_same_as_compares_vanilla_node_identity() {
581 let left = Path::new(vec![Node::new(0, 64, 0)], BlockPos::new(1, 64, 0), false);
582 let mut right_node = Node::new(0, 64, 0);
583 right_node.cost_malus = 8.0;
584 let right = Path::new(vec![right_node], BlockPos::new(2, 64, 0), true);
585
586 assert!(left.same_as(&right));
587 }
588
589 #[test]
590 fn path_type_cache_invalidates_matching_position() {
591 init_vanilla_registry();
592
593 let level = SingleBlockLevel {
594 state: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR),
595 };
596 let pos = steel_utils::BlockPos::new(1, 64, 1);
597 let mut cache = PathTypeCache::new();
598
599 assert_eq!(cache.get_or_compute(&level, pos), PathType::Open);
600 cache.invalidate(pos);
601 assert_eq!(cache.get_or_compute(&level, pos), PathType::Open);
602 }
603
604 #[test]
605 fn pathfinding_context_uses_cache_when_supplied() {
606 init_vanilla_registry();
607
608 let level = SingleBlockLevel {
609 state: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR),
610 };
611 let mut cache = PathTypeCache::new();
612 let mut context = PathfindingContext::with_cache(
613 &level,
614 steel_utils::BlockPos::new(0, 64, 0),
615 &mut cache,
616 );
617
618 assert_eq!(context.get_path_type_from_state(0, 64, 0), PathType::Open);
619 }
620}