Skip to main content

steel_registry/fluid/
mod.rs

1//! Fluid registry for Minecraft fluids.
2
3use crate::{
4    RegistryExt, RegistryTags, TaggedRegistryExt,
5    blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
6    vanilla_blocks,
7    vanilla_fluid_tags::FluidTag,
8    vanilla_fluids,
9};
10use rustc_hash::FxHashMap;
11use steel_utils::{BlockStateId, Identifier};
12
13/// A fluid type definition (e.g., water, lava, empty).
14#[derive(Debug)]
15pub struct Fluid {
16    /// The identifier for this fluid (e.g., "minecraft:water").
17    pub key: Identifier,
18    /// Whether this fluid is empty (air).
19    pub is_empty: bool,
20    /// Whether this is a source fluid (vs flowing).
21    pub is_source: bool,
22    /// Whether this fluid receives random ticks.
23    pub is_randomly_ticking: bool,
24    /// The block this fluid places.
25    pub block: Identifier,
26    /// The bucket item for this fluid.
27    pub bucket_item: Identifier,
28    /// The source fluid identifier (for flowing fluids).
29    pub source_fluid: Option<Identifier>,
30    /// The flowing fluid identifier (for source fluids).
31    pub flowing_fluid: Option<Identifier>,
32    /// Tick delay for fluid updates.
33    pub tick_delay: u32,
34    /// Explosion resistance.
35    pub explosion_resistance: f32,
36}
37
38impl Fluid {
39    /// Returns `true` if this fluid is tagged with the given tag.
40    #[must_use]
41    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
42        REGISTRY.fluids.is_in_tag(self, tag)
43    }
44
45    /// Returns this fluid's source variant.
46    ///
47    /// Vanilla's source and flowing fluids are distinct registry entries. Liquid
48    /// blocks store a single block id plus a level property, so state decoding
49    /// resolves the correct fluid variant from extracted fluid relationship data.
50    #[must_use]
51    pub fn source_variant(&'static self) -> FluidRef {
52        let Some(source_key) = &self.source_fluid else {
53            return self;
54        };
55
56        match REGISTRY.fluids.by_key(source_key) {
57            Some(fluid) => fluid,
58            None => panic!(
59                "fluid `{}` references missing source fluid `{source_key}`",
60                self.key
61            ),
62        }
63    }
64
65    /// Returns this fluid's flowing variant.
66    #[must_use]
67    pub fn flowing_variant(&'static self) -> FluidRef {
68        let Some(flowing_key) = &self.flowing_fluid else {
69            return self;
70        };
71
72        match REGISTRY.fluids.by_key(flowing_key) {
73            Some(fluid) => fluid,
74            None => panic!(
75                "fluid `{}` references missing flowing fluid `{flowing_key}`",
76                self.key
77            ),
78        }
79    }
80}
81
82pub type FluidRef = &'static Fluid;
83
84/// A fluid state instance with amount and falling properties.
85///
86/// Registered block states cache this value in their immutable state metadata.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct FluidState {
89    /// The fluid type (water, lava, empty).
90    pub fluid_id: FluidRef,
91    /// The fluid amount (1-8, where 8 is a full block/source).
92    pub amount: u8,
93    /// Whether the fluid is falling (flows downward faster).
94    pub falling: bool,
95}
96
97const LEGACY_SOURCE_LEVEL: u8 = 0;
98const LEGACY_MAX_LEVEL: u8 = 8;
99const LEGACY_FALLING_OFFSET: u8 = 8;
100
101impl FluidState {
102    /// The empty fluid state.
103    pub const EMPTY: Self = Self {
104        fluid_id: &vanilla_fluids::EMPTY,
105        amount: 0,
106        falling: false,
107    };
108
109    /// Creates a new fluid state.
110    #[must_use]
111    pub const fn new(fluid: FluidRef, amount: u8, falling: bool) -> Self {
112        Self {
113            fluid_id: fluid,
114            amount,
115            falling,
116        }
117    }
118
119    /// Creates a source fluid state (amount=8, not falling).
120    #[must_use]
121    pub const fn source(fluid: FluidRef) -> Self {
122        Self {
123            fluid_id: fluid,
124            amount: 8,
125            falling: false,
126        }
127    }
128
129    /// Creates a flowing fluid state.
130    #[must_use]
131    pub const fn flowing(fluid: FluidRef, amount: u8, falling: bool) -> Self {
132        Self {
133            fluid_id: fluid,
134            amount,
135            falling,
136        }
137    }
138
139    /// Returns true if this is the empty fluid.
140    #[must_use]
141    pub const fn is_empty(&self) -> bool {
142        self.fluid_id.is_empty || self.amount == 0
143    }
144
145    /// Returns true if this state is owned by a source fluid type.
146    ///
147    /// Vanilla `FluidState.isSource()` delegates to the owning fluid. Source
148    /// fluids can still carry `FALLING=true` through `FlowingFluid.getSource`.
149    #[must_use]
150    pub const fn is_source(&self) -> bool {
151        self.fluid_id.is_source
152    }
153
154    /// Returns true if this fluid has vanilla's full amount (`8`).
155    ///
156    /// This intentionally does not require a source fluid type. Vanilla
157    /// `FluidState.isFull()` is `getAmount() == 8`, so a falling full fluid is
158    /// full without being a source.
159    #[must_use]
160    pub const fn is_full(&self) -> bool {
161        self.amount == 8
162    }
163
164    /// Returns whether this fluid receives random ticks.
165    #[must_use]
166    pub const fn is_randomly_ticking(&self) -> bool {
167        self.fluid_id.is_randomly_ticking
168    }
169
170    /// Returns the fluid's own height (0.0 to ~0.89).
171    #[must_use]
172    pub fn own_height(&self) -> f32 {
173        if self.is_empty() {
174            0.0
175        } else {
176            f32::from(self.amount) / 9.0
177        }
178    }
179
180    /// Decodes a fluid state from a liquid block's LEVEL property (0-15).
181    ///
182    /// - LEVEL 0 = source (amount=8, falling=false)
183    /// - LEVEL 1-7 = flowing levels 7-1 (amount = 8 - level)
184    /// - LEVEL 8-15 = falling fluid (amount=8, falling=true, but clamped)
185    #[must_use]
186    pub fn from_block_level(fluid: FluidRef, level: u8) -> Self {
187        if level == 0 {
188            // Source block
189            Self::source(fluid.source_variant())
190        } else if level <= 7 {
191            // Flowing fluid: level 1 = amount 7, level 7 = amount 1
192            Self::flowing(fluid.flowing_variant(), 8 - level, false)
193        } else {
194            // LiquidBlock clamps LEVEL 8-15 to the single cached falling state.
195            Self::flowing(fluid.flowing_variant(), 8, true)
196        }
197    }
198
199    /// Encodes this fluid state to a liquid block's LEVEL property (0-15).
200    #[must_use]
201    pub const fn to_block_level(self) -> u8 {
202        if self.is_source() {
203            0
204        } else if self.falling {
205            8
206        } else {
207            // amount 7 -> level 1, amount 1 -> level 7
208            8 - self.amount
209        }
210    }
211
212    pub fn create_legacy_block(self) -> BlockStateId {
213        vanilla_blocks::WATER
214            .default_state()
215            .set_value(&BlockStateProperties::LEVEL, Self::get_legacy_level(self))
216    }
217
218    const fn get_legacy_level(self) -> u8 {
219        if self.is_source() {
220            return LEGACY_SOURCE_LEVEL;
221        }
222
223        let falling_offset = if self.falling {
224            LEGACY_FALLING_OFFSET
225        } else {
226            0
227        };
228
229        LEGACY_MAX_LEVEL - self.amount.min(LEGACY_MAX_LEVEL) + falling_offset
230    }
231}
232
233/// Registry for all fluids.
234pub struct FluidRegistry {
235    fluids_by_id: Vec<FluidRef>,
236    fluids_by_key: FxHashMap<Identifier, usize>,
237    tags: RegistryTags,
238    allows_registering: bool,
239}
240
241impl Default for FluidRegistry {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl FluidRegistry {
248    /// Creates a new, empty fluid registry.
249    #[must_use]
250    pub fn new() -> Self {
251        Self {
252            fluids_by_id: Vec::new(),
253            fluids_by_key: FxHashMap::default(),
254            tags: RegistryTags::default(),
255            allows_registering: true,
256        }
257    }
258
259    /// Registers a fluid and returns its ID.
260    pub fn register(&mut self, fluid: FluidRef) -> usize {
261        assert!(
262            self.allows_registering,
263            "Cannot register fluids after the registry has been frozen"
264        );
265
266        let id = self.fluids_by_id.len();
267        self.fluids_by_key.insert(fluid.key.clone(), id);
268        self.fluids_by_id.push(fluid);
269        id
270    }
271
272    /// Iterates over all fluids with their IDs.
273    pub fn iter(&self) -> impl Iterator<Item = (usize, FluidRef)> + '_ {
274        self.fluids_by_id
275            .iter()
276            .enumerate()
277            .map(|(id, &fluid)| (id, fluid))
278    }
279}
280
281crate::impl_registry!(FluidRegistry, Fluid, fluids_by_id, fluids_by_key, fluids);
282crate::impl_tagged_registry!(FluidRegistry, fluids_by_key, "fluid");
283
284use crate::REGISTRY;
285
286/// Returns true if the given `FluidRef` is water (including flowing water).
287#[must_use]
288pub fn is_water_fluid(fluid: FluidRef) -> bool {
289    !fluid.is_empty && fluid.has_tag(&FluidTag::WATER)
290}
291
292/// Returns true if the given `FluidRef` is lava (including flowing lava).
293#[must_use]
294pub fn is_lava_fluid(fluid: FluidRef) -> bool {
295    !fluid.is_empty && fluid.has_tag(&FluidTag::LAVA)
296}
297
298/// Extension trait for `FluidState` type-checking methods.
299pub trait FluidStateExt {
300    /// Returns true if this fluid state contains water.
301    fn is_water(&self) -> bool;
302    /// Returns true if this fluid state contains lava.
303    fn is_lava(&self) -> bool;
304}
305
306impl FluidStateExt for FluidState {
307    fn is_water(&self) -> bool {
308        is_water_fluid(self.fluid_id)
309    }
310    fn is_lava(&self) -> bool {
311        is_lava_fluid(self.fluid_id)
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use crate::{init_vanilla_registry, vanilla_fluids};
318
319    use super::*;
320
321    #[test]
322    fn from_block_level_uses_source_variant_for_level_zero() {
323        init_vanilla_registry();
324
325        let water = FluidState::from_block_level(&vanilla_fluids::WATER, 0);
326        let lava = FluidState::from_block_level(&vanilla_fluids::LAVA, 0);
327
328        assert_eq!(water.fluid_id, &vanilla_fluids::WATER);
329        assert_eq!(lava.fluid_id, &vanilla_fluids::LAVA);
330        assert!(water.is_source());
331        assert!(lava.is_source());
332    }
333
334    #[test]
335    fn from_block_level_uses_flowing_variant_for_non_source_levels() {
336        init_vanilla_registry();
337
338        let water = FluidState::from_block_level(&vanilla_fluids::WATER, 1);
339        let lava = FluidState::from_block_level(&vanilla_fluids::LAVA, 8);
340
341        assert_eq!(water.fluid_id, &vanilla_fluids::FLOWING_WATER);
342        assert_eq!(lava.fluid_id, &vanilla_fluids::FLOWING_LAVA);
343        assert!(!water.is_source());
344        assert!(!lava.is_source());
345        assert!(lava.falling);
346    }
347
348    #[test]
349    fn from_block_level_clamps_all_falling_liquid_levels_to_full_amount() {
350        init_vanilla_registry();
351
352        for level in 8..=15 {
353            let water = FluidState::from_block_level(&vanilla_fluids::WATER, level);
354
355            assert_eq!(water.fluid_id, &vanilla_fluids::FLOWING_WATER);
356            assert_eq!(water.amount, 8);
357            assert!(water.falling);
358            assert!(water.is_full());
359            assert!(!water.is_source());
360        }
361    }
362
363    #[test]
364    fn source_fluid_type_is_source_even_when_falling() {
365        init_vanilla_registry();
366
367        let falling_source = FluidState::new(&vanilla_fluids::WATER, 8, true);
368
369        assert!(falling_source.is_source());
370        assert!(falling_source.is_full());
371    }
372}