Skip to main content

azalea/pathfinder/
world.rs

1use std::{
2    array,
3    cell::{RefCell, UnsafeCell},
4    fmt::Debug,
5    mem,
6    sync::Arc,
7};
8
9use azalea_block::{
10    BlockState,
11    properties::{self, SlabKind, StairShape},
12};
13use azalea_core::{
14    bitset::FastFixedBitSet,
15    position::{BlockPos, ChunkPos, ChunkSectionBlockPos},
16};
17use azalea_physics::collision::BlockWithShape;
18use azalea_registry::builtin::BlockKind;
19use azalea_world::{World, palette::PalettedContainer};
20use parking_lot::RwLock;
21use rustc_hash::FxHashMap;
22
23use super::{mining::MiningCache, positions::RelBlockPos};
24use crate::pathfinder::positions::SmallChunkSectionPos;
25
26const MAX_VIEW_DISTANCE: usize = 32;
27
28/// An efficient representation of the world used for the pathfinder.
29pub struct CachedWorld {
30    /// The origin that the [`RelBlockPos`] types will be relative to.
31    ///
32    /// This is for an optimization that reduces the size of the block positions
33    /// that are used by the pathfinder.
34    origin: BlockPos,
35
36    min_y: i32,
37    world_lock: Arc<RwLock<World>>,
38
39    // we use the bounded cache by default and then switch if it gets too big
40    bounded_chunk_cache: RefCell<[(ChunkPos, CachedChunk); MAX_VIEW_DISTANCE * MAX_VIEW_DISTANCE]>,
41    unbounded_chunk_cache: RefCell<FxHashMap<ChunkPos, CachedChunk>>,
42
43    cached_blocks: UnsafeCell<CachedSections>,
44
45    #[allow(clippy::type_complexity)]
46    cached_mining_costs: UnsafeCell<Option<Box<[(RelBlockPos, f32)]>>>,
47}
48
49// we store `PalettedContainer`s instead of `Chunk`s or `Section`s because it
50// doesn't contain any unnecessary data like heightmaps or biomes.
51type CachedChunk = Box<[PalettedContainer<BlockState>]>;
52
53pub struct CachedSections {
54    pub fast_sections: Box<[Option<CachedSection>; FAST_SECTIONS_CACHE_SIZE]>,
55    pub fallback_sections: Vec<CachedSection>,
56}
57
58const FAST_SECTIONS_CACHE_SIZE: usize = 16 * 16 * 16;
59fn fast_section_idx(pos: SmallChunkSectionPos) -> usize {
60    (pos.y as usize % 16) + (pos.x as usize % 16) * 16 + (pos.z as usize % 16) * 16 * 16
61}
62
63impl CachedSections {
64    pub fn get_mut(&mut self, pos: SmallChunkSectionPos) -> Option<&mut CachedSection> {
65        let idx = fast_section_idx(pos);
66
67        if let Some(fast_item) = &mut self.fast_sections[idx]
68            && fast_item.pos == pos
69        {
70            return Some(fast_item);
71        }
72
73        if let Some(item) = self.fallback_sections.iter_mut().find(|s| s.pos == pos) {
74            return Some(item);
75        }
76
77        None
78    }
79
80    #[inline]
81    pub fn insert(&mut self, section: CachedSection) {
82        let idx = fast_section_idx(section.pos);
83
84        if let item @ None = &mut self.fast_sections[idx] {
85            *item = Some(section);
86            return;
87        }
88
89        // this benchmarks better than pushing even when we linear search later. i guess
90        // it has better cache locality?
91        let index = self
92            .fallback_sections
93            .binary_search_by(|s| s.pos.cmp(&section.pos))
94            .unwrap_or_else(|e| e);
95        self.fallback_sections.insert(index, section);
96    }
97}
98impl Default for CachedSections {
99    fn default() -> Self {
100        Self {
101            fast_sections: (0..FAST_SECTIONS_CACHE_SIZE)
102                .map(|_| None)
103                .collect::<Box<[_]>>()
104                .try_into()
105                .unwrap(),
106            fallback_sections: Default::default(),
107        }
108    }
109}
110
111pub struct CachedSection {
112    pub pos: SmallChunkSectionPos,
113    pub bitsets: Box<SectionBitsets>,
114}
115impl Debug for CachedSection {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("CachedSection")
118            .field("pos", &self.pos)
119            .finish()
120    }
121}
122
123#[derive(Default)]
124pub struct SectionBitsets {
125    /// Blocks that we can fully pass through (like air).
126    pub passable: FastFixedBitSet<4096>,
127    /// Blocks that we can stand on and do parkour from.
128    pub solid: FastFixedBitSet<4096>,
129    /// Blocks that we can stand on but might not be able to parkour from.
130    pub standable: FastFixedBitSet<4096>,
131    /// Water source blocks.
132    pub water: FastFixedBitSet<4096>,
133}
134
135impl CachedWorld {
136    pub fn new(world_lock: Arc<RwLock<World>>, origin: BlockPos) -> Self {
137        let min_y = world_lock.read().chunks.min_y();
138        Self {
139            origin,
140            min_y,
141            world_lock,
142            bounded_chunk_cache: RefCell::new(array::from_fn(|_| {
143                (ChunkPos::new(i32::MAX, i32::MAX), Default::default())
144            })),
145            unbounded_chunk_cache: Default::default(),
146            cached_blocks: Default::default(),
147            cached_mining_costs: UnsafeCell::new(None),
148        }
149    }
150
151    // ```
152    // fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
153    //     self.with_section(ChunkSectionPos::from(pos), |section| {
154    //         let state = section.get(pos.x as usize, pos.y as usize, pos.z as usize);
155    //         BlockState::try_from(state).unwrap_or(BlockState::AIR)
156    //     })
157    // }
158    // ```
159
160    fn with_section<T>(
161        &self,
162        section_pos: SmallChunkSectionPos,
163        f: impl FnOnce(&azalea_world::palette::PalettedContainer<BlockState>) -> T,
164    ) -> Option<T> {
165        if section_pos.y * 16 < self.min_y {
166            // y position is out of bounds
167            return None;
168        }
169
170        let chunk_pos = ChunkPos::new(section_pos.x as i32, section_pos.z as i32);
171        let section_index =
172            azalea_world::chunk::section_index(section_pos.y * 16, self.min_y) as usize;
173
174        let mut cache_idx = 0;
175
176        let mut unbounded_chunk_cache = self.unbounded_chunk_cache.borrow_mut();
177        let mut bounded_chunk_cache = self.bounded_chunk_cache.borrow_mut();
178        if unbounded_chunk_cache.is_empty() {
179            const D: i32 = MAX_VIEW_DISTANCE as i32;
180            let cache_x = i32::rem_euclid(chunk_pos.x, D) * D;
181            let cache_z = i32::rem_euclid(chunk_pos.z, D);
182            cache_idx = (cache_x + cache_z) as usize;
183
184            // get section from cache
185            if !bounded_chunk_cache[cache_idx].1.is_empty() {
186                if bounded_chunk_cache[cache_idx].0 != chunk_pos {
187                    // switch to the unbounded cache :(
188
189                    for (moving_chunk_pos, moving_chunk) in bounded_chunk_cache.iter_mut() {
190                        if !moving_chunk.is_empty() {
191                            unbounded_chunk_cache
192                                .insert(*moving_chunk_pos, mem::take(moving_chunk));
193                        }
194                    }
195                }
196
197                let sections = &bounded_chunk_cache[cache_idx].1;
198                if section_index >= sections.len() {
199                    // y position is out of bounds
200                    return None;
201                };
202                let section = &sections[section_index];
203                return Some(f(section));
204            }
205        } else if let Some(sections) = unbounded_chunk_cache.get(&chunk_pos) {
206            if section_index >= sections.len() {
207                // y position is out of bounds
208                return None;
209            };
210            let section = &sections[section_index];
211            return Some(f(section));
212        }
213
214        let world = self.world_lock.read();
215        let chunk = world.chunks.get(&chunk_pos)?;
216        let chunk = chunk.read();
217
218        let sections = chunk
219            .sections
220            .iter()
221            .map(|section| section.states.clone())
222            .collect::<Box<[PalettedContainer<BlockState>]>>();
223
224        if section_index >= sections.len() {
225            // y position is out of bounds
226            return None;
227        };
228
229        let section = &sections[section_index];
230        let r = f(section);
231
232        // add the sections to the chunk cache
233        if unbounded_chunk_cache.is_empty() {
234            bounded_chunk_cache[cache_idx] = (chunk_pos, sections);
235        } else {
236            unbounded_chunk_cache.insert(chunk_pos, sections);
237        }
238
239        Some(r)
240    }
241
242    fn calculate_bitsets_for_section(&self, section_pos: SmallChunkSectionPos) -> CachedSection {
243        let bitsets = self
244            .with_section(section_pos, |section| {
245                let mut bitsets = SectionBitsets {
246                    passable: FastFixedBitSet::<4096>::new(),
247                    solid: FastFixedBitSet::<4096>::new(),
248                    standable: FastFixedBitSet::<4096>::new(),
249                    water: FastFixedBitSet::<4096>::new(),
250                };
251
252                for i in 0..4096 {
253                    let block_state = section.get_at_index(i);
254                    if is_block_state_passable(block_state) {
255                        bitsets.passable.set(i);
256                    }
257                    if is_block_state_solid(block_state) {
258                        bitsets.solid.set(i);
259                    }
260                    if is_block_state_standable(block_state) {
261                        bitsets.standable.set(i);
262                    }
263                    if is_block_state_water(block_state) {
264                        bitsets.water.set(i);
265                    }
266                }
267                Box::new(bitsets)
268            })
269            .unwrap_or_default();
270
271        CachedSection {
272            pos: section_pos,
273            bitsets,
274        }
275    }
276
277    fn check_bitset_for_block(
278        &self,
279        pos: BlockPos,
280        cb: impl FnOnce(&SectionBitsets, usize) -> bool,
281    ) -> bool {
282        let (section_pos, section_block_pos) = (
283            SmallChunkSectionPos::from(pos),
284            ChunkSectionBlockPos::from(pos),
285        );
286        let index = u16::from(section_block_pos) as usize;
287        // SAFETY: we're only accessing this from one thread
288        let cached_blocks = unsafe { &mut *self.cached_blocks.get() };
289        if let Some(cached) = cached_blocks.get_mut(section_pos) {
290            return cb(&cached.bitsets, index);
291        }
292
293        let cached = self.calculate_bitsets_for_section(section_pos);
294        let passable = cb(&cached.bitsets, index);
295        cached_blocks.insert(cached);
296        passable
297    }
298
299    pub fn is_block_passable(&self, pos: RelBlockPos) -> bool {
300        self.is_block_pos_passable(pos.apply(self.origin))
301    }
302    fn is_block_pos_passable(&self, pos: BlockPos) -> bool {
303        self.check_bitset_for_block(pos, |bitsets, index| bitsets.passable.index(index))
304    }
305
306    pub fn is_block_water(&self, pos: RelBlockPos) -> bool {
307        self.is_block_pos_water(pos.apply(self.origin))
308    }
309    fn is_block_pos_water(&self, pos: BlockPos) -> bool {
310        self.check_bitset_for_block(pos, |bitsets, index| bitsets.water.index(index))
311    }
312
313    /// Get the block state at the given position.
314    ///
315    /// This is relatively slow, so you should avoid it whenever possible.
316    pub fn get_block_state(&self, pos: RelBlockPos) -> BlockState {
317        self.get_block_state_at_pos(pos.apply(self.origin))
318    }
319
320    fn get_block_state_at_pos(&self, pos: BlockPos) -> BlockState {
321        let (section_pos, section_block_pos) = (
322            SmallChunkSectionPos::from(pos),
323            ChunkSectionBlockPos::from(pos),
324        );
325        let index = u16::from(section_block_pos) as usize;
326
327        self.with_section(section_pos, |section| section.get_at_index(index))
328            .unwrap_or_default()
329    }
330
331    pub fn is_block_solid(&self, pos: RelBlockPos) -> bool {
332        self.is_block_pos_solid(pos.apply(self.origin))
333    }
334    pub fn is_block_standable(&self, pos: RelBlockPos) -> bool {
335        self.is_block_pos_standable(pos.apply(self.origin))
336    }
337
338    fn is_block_pos_solid(&self, pos: BlockPos) -> bool {
339        self.check_bitset_for_block(pos, |bitsets, index| bitsets.solid.index(index))
340    }
341    fn is_block_pos_standable(&self, pos: BlockPos) -> bool {
342        self.check_bitset_for_block(pos, |bitsets, index| bitsets.standable.index(index))
343    }
344
345    /// Returns how much it costs to break this block.
346    ///
347    /// Returns 0 if the block is already passable.
348    pub fn cost_for_breaking_block(&self, pos: RelBlockPos, mining_cache: &MiningCache) -> f32 {
349        let cached_mining_costs = self.cached_mining_costs();
350
351        let hash_index = calculate_cached_mining_costs_index(pos);
352        let &(cached_pos, potential_cost) =
353            unsafe { cached_mining_costs.get_unchecked(hash_index) };
354        if cached_pos == pos {
355            return potential_cost;
356        }
357
358        let cost = self.uncached_cost_for_breaking_block(pos, mining_cache);
359        unsafe {
360            *cached_mining_costs.get_unchecked_mut(hash_index) = (pos, cost);
361        };
362
363        cost
364    }
365
366    // this is fine because pathfinding is single-threaded
367    #[allow(clippy::mut_from_ref)]
368    fn cached_mining_costs(&self) -> &mut [(RelBlockPos, f32)] {
369        // SAFETY: again, pathfinding is single-threaded
370        let cached_mining_costs = unsafe { &mut *self.cached_mining_costs.get() };
371        if let Some(cached_mining_costs) = cached_mining_costs {
372            return cached_mining_costs;
373        }
374        // delay initialization so we don't have to create this if it's unused
375
376        // this uses about 2mb of memory. it *really* helps though.
377        *cached_mining_costs = Some(
378            vec![(RelBlockPos::new(i16::MAX, i32::MAX, i16::MAX), 0.); CACHED_MINING_COSTS_SIZE]
379                .into(),
380        );
381
382        cached_mining_costs.as_mut().unwrap()
383    }
384
385    fn uncached_cost_for_breaking_block(
386        &self,
387        pos: RelBlockPos,
388        mining_cache: &MiningCache,
389    ) -> f32 {
390        if self.is_block_passable(pos) {
391            // if the block is passable then it doesn't need to be broken
392            return 0.;
393        }
394
395        let rel_pos = pos;
396        let pos = pos.apply(self.origin);
397
398        let (section_pos, section_block_pos) = (
399            SmallChunkSectionPos::from(pos),
400            ChunkSectionBlockPos::from(pos),
401        );
402
403        // we use this as an optimization to avoid getting the section again if the
404        // block is in the same section
405        let up_is_in_same_section = section_block_pos.y != 15;
406        let north_is_in_same_section = section_block_pos.z != 0;
407        let east_is_in_same_section = section_block_pos.x != 15;
408        let south_is_in_same_section = section_block_pos.z != 15;
409        let west_is_in_same_section = section_block_pos.x != 0;
410
411        let mut is_falling_block_above = false;
412
413        let Some(mut mining_cost) = self.with_section(section_pos, |section| {
414            let block_state = section.get_at_index(u16::from(section_block_pos) as usize);
415            let mining_cost = mining_cache.cost_for(block_state);
416
417            if mining_cost == f32::INFINITY {
418                // the block is unbreakable
419                return f32::INFINITY;
420            }
421
422            // if there's a falling block or liquid above this block, abort
423            if up_is_in_same_section {
424                let up_block = section.get_at_index(u16::from(section_block_pos.up(1)) as usize);
425                if mining_cache.is_liquid(up_block) {
426                    return f32::INFINITY;
427                }
428                if mining_cache.is_falling_block(up_block) {
429                    is_falling_block_above = true;
430                }
431            }
432
433            // if there's a liquid to the north of this block, abort
434            if north_is_in_same_section {
435                let north_block =
436                    section.get_at_index(u16::from(section_block_pos.north(1)) as usize);
437                if mining_cache.is_liquid(north_block) {
438                    return f32::INFINITY;
439                }
440            }
441
442            // liquid to the east
443            if east_is_in_same_section {
444                let east_block =
445                    section.get_at_index(u16::from(section_block_pos.east(1)) as usize);
446                if mining_cache.is_liquid(east_block) {
447                    return f32::INFINITY;
448                }
449            }
450
451            // liquid to the south
452            if south_is_in_same_section {
453                let south_block =
454                    section.get_at_index(u16::from(section_block_pos.south(1)) as usize);
455                if mining_cache.is_liquid(south_block) {
456                    return f32::INFINITY;
457                }
458            }
459
460            // liquid to the west
461            if west_is_in_same_section {
462                let west_block =
463                    section.get_at_index(u16::from(section_block_pos.west(1)) as usize);
464                if mining_cache.is_liquid(west_block) {
465                    return f32::INFINITY;
466                }
467            }
468
469            // the block is probably safe to break, we'll have to check the adjacent blocks
470            // that weren't in the same section next though
471            mining_cost
472        }) else {
473            // the chunk isn't loaded
474            let cost = if self.is_block_pos_solid(pos) {
475                // assume it's unbreakable if it's solid and out of render distance
476                f32::INFINITY
477            } else {
478                0.
479            };
480            return cost;
481        };
482
483        if mining_cost == f32::INFINITY {
484            // the block is unbreakable
485            return f32::INFINITY;
486        }
487
488        fn check_should_avoid_this_block(
489            world: &CachedWorld,
490            pos: BlockPos,
491            check: impl FnOnce(BlockState) -> bool,
492        ) -> bool {
493            let block_state = world
494                .with_section(SmallChunkSectionPos::from(pos), |section| {
495                    section.get_at_index(u16::from(ChunkSectionBlockPos::from(pos)) as usize)
496                })
497                .unwrap_or_default();
498            check(block_state)
499        }
500
501        // check the adjacent blocks that weren't in the same section
502        if !up_is_in_same_section
503            && check_should_avoid_this_block(self, pos.up(1), |b| {
504                if mining_cache.is_falling_block(b) {
505                    is_falling_block_above = true;
506                }
507                mining_cache.is_liquid(b)
508            })
509        {
510            return f32::INFINITY;
511        }
512        if !north_is_in_same_section
513            && check_should_avoid_this_block(self, pos.north(1), |b| mining_cache.is_liquid(b))
514        {
515            return f32::INFINITY;
516        }
517        if !east_is_in_same_section
518            && check_should_avoid_this_block(self, pos.east(1), |b| mining_cache.is_liquid(b))
519        {
520            return f32::INFINITY;
521        }
522        if !south_is_in_same_section
523            && check_should_avoid_this_block(self, pos.south(1), |b| mining_cache.is_liquid(b))
524        {
525            return f32::INFINITY;
526        }
527        if !west_is_in_same_section
528            && check_should_avoid_this_block(self, pos.west(1), |b| mining_cache.is_liquid(b))
529        {
530            return f32::INFINITY;
531        }
532
533        if is_falling_block_above {
534            mining_cost += self.cost_for_breaking_block(rel_pos.up(1), mining_cache);
535        }
536
537        mining_cost
538    }
539
540    /// Whether this block and the block above are passable
541    pub fn is_passable(&self, pos: RelBlockPos) -> bool {
542        self.is_passable_at_block_pos(pos.apply(self.origin))
543    }
544    fn is_passable_at_block_pos(&self, pos: BlockPos) -> bool {
545        self.is_block_pos_passable(pos) && self.is_block_pos_passable(pos.up(1))
546    }
547
548    pub fn cost_for_passing(&self, pos: RelBlockPos, mining_cache: &MiningCache) -> f32 {
549        self.cost_for_breaking_block(pos, mining_cache)
550            + self.cost_for_breaking_block(pos.up(1), mining_cache)
551    }
552
553    /// Whether we can stand in this position.
554    ///
555    /// Checks if the block below is solid, and that the two blocks above that
556    /// are passable.
557    pub fn is_standable(&self, pos: RelBlockPos) -> bool {
558        self.is_standable_at_block_pos(pos.apply(self.origin))
559    }
560    fn is_standable_at_block_pos(&self, pos: BlockPos) -> bool {
561        self.is_block_pos_standable(pos.down(1)) && self.is_passable_at_block_pos(pos)
562    }
563
564    pub fn cost_for_standing(&self, pos: RelBlockPos, mining_cache: &MiningCache) -> f32 {
565        if !self.is_block_standable(pos.down(1)) {
566            return f32::INFINITY;
567        }
568        self.cost_for_passing(pos, mining_cache)
569    }
570
571    /// Get the amount of air/passable blocks until the next non-passable block
572    /// below this one.
573    pub fn fall_distance(&self, pos: RelBlockPos) -> u32 {
574        let mut distance = 0;
575        let mut current_pos = pos.down(1);
576        while self.is_block_passable(current_pos) {
577            distance += 1;
578            current_pos = current_pos.down(1);
579
580            if current_pos.y < self.min_y {
581                return u32::MAX;
582            }
583        }
584        distance
585    }
586
587    pub fn origin(&self) -> BlockPos {
588        self.origin
589    }
590}
591
592const CACHED_MINING_COSTS_SIZE: usize = 2usize.pow(18);
593fn calculate_cached_mining_costs_index(pos: RelBlockPos) -> usize {
594    // create an 18-bit index by taking the bottom bits from each axis
595
596    const X_BITS: usize = 6;
597    const Y_BITS: usize = 6;
598    const Z_BITS: usize = 6;
599
600    const X_MASK: usize = (1 << X_BITS) - 1;
601    const Y_MASK: usize = (1 << Y_BITS) - 1;
602    const Z_MASK: usize = (1 << Z_BITS) - 1;
603
604    let hash_index = ((pos.x as usize & X_MASK) << (Y_BITS + Z_BITS))
605        | ((pos.z as usize & Z_MASK) << Y_BITS)
606        | (pos.y as usize & Y_MASK);
607    debug_assert!(hash_index < CACHED_MINING_COSTS_SIZE);
608    hash_index
609}
610
611/// Whether our client could pass through this block.
612pub fn is_block_state_passable(block_state: BlockState) -> bool {
613    // i already tried optimizing this by having it cache in an IntMap/FxHashMap but
614    // it wasn't measurably faster
615
616    if block_state.is_air() {
617        // fast path
618        return true;
619    }
620    if !block_state.is_collision_shape_empty() {
621        return false;
622    }
623    let registry_block = BlockKind::from(block_state);
624    if registry_block == BlockKind::Water {
625        return false;
626    }
627    if block_state
628        .property::<azalea_block::properties::Waterlogged>()
629        .unwrap_or_default()
630    {
631        return false;
632    }
633    if registry_block == BlockKind::Lava {
634        return false;
635    }
636    // block.waterlogged currently doesn't account for seagrass and some other water
637    // blocks
638    if block_state == BlockKind::Seagrass.into() {
639        return false;
640    }
641
642    // don't walk into fire
643    if registry_block == BlockKind::Fire || registry_block == BlockKind::SoulFire {
644        return false;
645    }
646
647    if registry_block == BlockKind::PowderSnow {
648        // we can't jump out of powder snow
649        return false;
650    }
651
652    if registry_block == BlockKind::SweetBerryBush {
653        // these hurt us
654        return false;
655    }
656
657    true
658}
659
660/// Whether this block has a solid hitbox at the top (i.e. we can stand on it
661/// and do parkour from it).
662#[inline]
663#[expect(clippy::needless_bool, reason = "Kept for future improvements")]
664pub fn is_block_state_solid(block_state: BlockState) -> bool {
665    if block_state.is_air() {
666        // fast path
667        return false;
668    }
669
670    if block_state.is_collision_shape_full() {
671        // hazard
672        if block_state == BlockState::from(BlockKind::MagmaBlock) {
673            return false;
674        };
675
676        return true;
677    }
678
679    if matches!(
680        block_state.property::<properties::SlabKind>(),
681        Some(properties::SlabKind::Top | properties::SlabKind::Double)
682    ) {
683        // top slabs
684        return true;
685    }
686
687    let block = BlockKind::from(block_state);
688    // solid enough
689    if matches!(block, BlockKind::DirtPath | BlockKind::Farmland) {
690        return true;
691    }
692
693    false
694}
695
696/// Whether we can stand on this block (but not necessarily do parkour jumps
697/// from it).
698pub fn is_block_state_standable(block_state: BlockState) -> bool {
699    if block_state.is_air() {
700        // fast path
701        return false;
702    }
703
704    if is_block_state_solid(block_state) {
705        return true;
706    }
707
708    if block_state.property::<SlabKind>().is_some()
709        || block_state.property::<StairShape>().is_some()
710    {
711        return true;
712    }
713
714    false
715}
716
717pub fn is_block_state_water(block_state: BlockState) -> bool {
718    // only the default blockstate, which is source blocks
719    block_state == BlockState::from(BlockKind::Water)
720}
721
722#[cfg(test)]
723mod tests {
724    use azalea_world::{Chunk, ChunkStorage, PartialWorld};
725
726    use super::*;
727
728    #[test]
729    fn test_is_passable() {
730        let mut partial_world = PartialWorld::default();
731        let mut world = ChunkStorage::default();
732
733        partial_world
734            .chunks
735            .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
736        partial_world.chunks.set_block_state(
737            BlockPos::new(0, 0, 0),
738            BlockKind::Stone.into(),
739            &world,
740        );
741        partial_world
742            .chunks
743            .set_block_state(BlockPos::new(0, 1, 0), BlockState::AIR, &world);
744
745        let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())), BlockPos::default());
746        assert!(!ctx.is_block_pos_passable(BlockPos::new(0, 0, 0)));
747        assert!(ctx.is_block_pos_passable(BlockPos::new(0, 1, 0)));
748    }
749
750    #[test]
751    fn test_is_solid() {
752        let mut partial_world = PartialWorld::default();
753        let mut world = ChunkStorage::default();
754        partial_world
755            .chunks
756            .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
757        partial_world.chunks.set_block_state(
758            BlockPos::new(0, 0, 0),
759            BlockKind::Stone.into(),
760            &world,
761        );
762        partial_world
763            .chunks
764            .set_block_state(BlockPos::new(0, 1, 0), BlockState::AIR, &world);
765
766        let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())), BlockPos::default());
767        assert!(ctx.is_block_pos_solid(BlockPos::new(0, 0, 0)));
768        assert!(!ctx.is_block_pos_solid(BlockPos::new(0, 1, 0)));
769    }
770
771    #[test]
772    fn test_is_standable() {
773        let mut partial_world = PartialWorld::default();
774        let mut world = ChunkStorage::default();
775        partial_world
776            .chunks
777            .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
778        partial_world.chunks.set_block_state(
779            BlockPos::new(0, 0, 0),
780            BlockKind::Stone.into(),
781            &world,
782        );
783        partial_world
784            .chunks
785            .set_block_state(BlockPos::new(0, 1, 0), BlockState::AIR, &world);
786        partial_world
787            .chunks
788            .set_block_state(BlockPos::new(0, 2, 0), BlockState::AIR, &world);
789        partial_world
790            .chunks
791            .set_block_state(BlockPos::new(0, 3, 0), BlockState::AIR, &world);
792
793        let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())), BlockPos::default());
794        assert!(ctx.is_standable_at_block_pos(BlockPos::new(0, 1, 0)));
795        assert!(!ctx.is_standable_at_block_pos(BlockPos::new(0, 0, 0)));
796        assert!(!ctx.is_standable_at_block_pos(BlockPos::new(0, 2, 0)));
797    }
798}