steel_core/fluid/spread_context.rs
1//! Spread calculation context for fluid flow optimization.
2//!
3//! Based on vanilla's FlowingFluid.SpreadContext, this provides local caching
4//! of block states and hole checks during fluid spread calculations.
5//!
6//! This avoids repeatedly querying the world for the same positions during
7//! the recursive slope-finding algorithm.
8
9use std::sync::Arc;
10
11use rustc_hash::FxHashMap;
12use steel_registry::fluid::FluidRef;
13use steel_utils::BlockPos;
14use steel_utils::BlockStateId;
15
16use crate::fluid::is_hole;
17use crate::world::World;
18/// Context for fluid spread calculations with local caching.
19///
20/// This is created fresh for each `get_spread()` call and caches:
21/// - `BlockState` lookups by relative position
22/// - Hole check results by relative position
23pub(super) struct SpreadContext<'a> {
24 /// Cache for block states by encoded relative position
25 state_cache: FxHashMap<i16, BlockStateId>,
26 /// Cache for hole check results by encoded relative position
27 hole_cache: FxHashMap<i16, bool>,
28 /// Reference to world for cache misses
29 world: &'a Arc<World>,
30 /// The block from which spreading originates — used to compute relative cache keys.
31 origin: BlockPos,
32}
33
34impl<'a> SpreadContext<'a> {
35 /// Creates a new `SpreadContext` anchored at `origin`.
36 ///
37 /// `origin` must be the block that triggered the spread (the block passed to
38 /// `get_spread()`), matching vanilla's `new FlowingFluid.SpreadContext(level, blockPos)`.
39 #[must_use]
40 pub(super) fn new(world: &'a Arc<World>, origin: BlockPos) -> Self {
41 Self {
42 state_cache: FxHashMap::default(),
43 hole_cache: FxHashMap::default(),
44 world,
45 origin,
46 }
47 }
48
49 /// Encodes a world position into a short cache key relative to the spread origin.
50 fn encode_key(&self, pos: BlockPos) -> i16 {
51 // Positions in the slope-finding algorithm stay within slopeFindDistance (<=4)
52 // of the origin, so the difference always fits in i8.
53 let dx = (pos.0.x - self.origin.0.x) as i8;
54 let dz = (pos.0.z - self.origin.0.z) as i8;
55 ((i16::from(dx) + 128) << 8) | (i16::from(dz) + 128)
56 }
57
58 /// Gets the cached block state at the given position, querying the world if not cached.
59 #[must_use]
60 pub fn get_block_state(&mut self, pos: BlockPos) -> BlockStateId {
61 let key = self.encode_key(pos);
62 *self
63 .state_cache
64 .entry(key)
65 .or_insert_with(|| self.world.get_block_state(pos))
66 }
67
68 /// Seeds a state already read by the outer spread calculation.
69 pub fn cache_block_state(&mut self, pos: BlockPos, state: BlockStateId) {
70 let key = self.encode_key(pos);
71 self.state_cache.insert(key, state);
72 }
73
74 /// Checks if the position is a hole (can fluid flow down into it?), with caching.
75 #[must_use]
76 pub fn is_hole(&mut self, pos: BlockPos, fluid_id: FluidRef) -> bool {
77 let key = self.encode_key(pos);
78 if let Some(is_hole) = self.hole_cache.get(&key) {
79 return *is_hole;
80 }
81
82 let state = self.get_block_state(pos);
83 let below = pos.below();
84 let below_state = self.world.get_block_state(below);
85 let result = is_hole(self.world, pos, state, below, below_state, fluid_id);
86 self.hole_cache.insert(key, result);
87 result
88 }
89
90 /// Returns a reference to the world.
91 #[must_use]
92 pub(super) const fn world(&self) -> &'a Arc<World> {
93 self.world
94 }
95}