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
28pub struct CachedWorld {
30 origin: BlockPos,
35
36 min_y: i32,
37 world_lock: Arc<RwLock<World>>,
38
39 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
49type 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 let index = self
92 .fallback_sections
93 .binary_search_by(|s| s.pos.cmp(§ion.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 pub passable: FastFixedBitSet<4096>,
127 pub solid: FastFixedBitSet<4096>,
129 pub standable: FastFixedBitSet<4096>,
131 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 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 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 if !bounded_chunk_cache[cache_idx].1.is_empty() {
186 if bounded_chunk_cache[cache_idx].0 != chunk_pos {
187 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 return None;
201 };
202 let section = §ions[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 return None;
209 };
210 let section = §ions[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 return None;
227 };
228
229 let section = §ions[section_index];
230 let r = f(section);
231
232 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 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 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 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 #[allow(clippy::mut_from_ref)]
368 fn cached_mining_costs(&self) -> &mut [(RelBlockPos, f32)] {
369 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 *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 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 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 return f32::INFINITY;
420 }
421
422 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 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 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 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 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 mining_cost
472 }) else {
473 let cost = if self.is_block_pos_solid(pos) {
475 f32::INFINITY
477 } else {
478 0.
479 };
480 return cost;
481 };
482
483 if mining_cost == f32::INFINITY {
484 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 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 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 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 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 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
611pub fn is_block_state_passable(block_state: BlockState) -> bool {
613 if block_state.is_air() {
617 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 if block_state == BlockKind::Seagrass.into() {
639 return false;
640 }
641
642 if registry_block == BlockKind::Fire || registry_block == BlockKind::SoulFire {
644 return false;
645 }
646
647 if registry_block == BlockKind::PowderSnow {
648 return false;
650 }
651
652 if registry_block == BlockKind::SweetBerryBush {
653 return false;
655 }
656
657 true
658}
659
660#[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 return false;
668 }
669
670 if block_state.is_collision_shape_full() {
671 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 return true;
685 }
686
687 let block = BlockKind::from(block_state);
688 if matches!(block, BlockKind::DirtPath | BlockKind::Farmland) {
690 return true;
691 }
692
693 false
694}
695
696pub fn is_block_state_standable(block_state: BlockState) -> bool {
699 if block_state.is_air() {
700 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 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}