Skip to main content

steel_registry/fluid/
mod.rs

1//! Fluid registry for Minecraft fluids.
2
3use crate::{TaggedRegistryExt, vanilla_fluid_tags::FluidTag, vanilla_fluids};
4use rustc_hash::FxHashMap;
5use steel_utils::Identifier;
6
7/// A fluid type definition (e.g., water, lava, empty).
8#[derive(Debug)]
9pub struct Fluid {
10    /// The identifier for this fluid (e.g., "minecraft:water").
11    pub key: Identifier,
12    /// Whether this fluid is empty (air).
13    pub is_empty: bool,
14    /// Whether this is a source fluid (vs flowing).
15    pub is_source: bool,
16    /// The block this fluid places.
17    pub block: Identifier,
18    /// The bucket item for this fluid.
19    pub bucket_item: Identifier,
20    /// The source fluid identifier (for flowing fluids).
21    pub source_fluid: Option<Identifier>,
22    /// The flowing fluid identifier (for source fluids).
23    pub flowing_fluid: Option<Identifier>,
24    /// Tick delay for fluid updates.
25    pub tick_delay: u32,
26    /// Explosion resistance.
27    pub explosion_resistance: f32,
28}
29
30impl Fluid {
31    /// Returns `true` if this fluid is tagged with the given tag.
32    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
33        REGISTRY.fluids.is_in_tag(self, tag)
34    }
35}
36
37pub type FluidRef = &'static Fluid;
38
39impl PartialEq for FluidRef {
40    fn eq(&self, other: &Self) -> bool {
41        self.key == other.key
42    }
43}
44
45impl Eq for FluidRef {}
46
47/// A fluid state instance with amount and falling properties.
48///
49/// This is computed on-demand from block states rather than stored.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct FluidState {
52    /// The fluid type (water, lava, empty).
53    pub fluid_id: FluidRef,
54    /// The fluid amount (1-8, where 8 is a full block/source).
55    pub amount: u8,
56    /// Whether the fluid is falling (flows downward faster).
57    pub falling: bool,
58}
59
60impl FluidState {
61    /// The empty fluid state.
62    pub const EMPTY: Self = Self {
63        fluid_id: &vanilla_fluids::EMPTY,
64        amount: 0,
65        falling: false,
66    };
67
68    /// Creates a new fluid state.
69    #[must_use]
70    pub const fn new(fluid: FluidRef, amount: u8, falling: bool) -> Self {
71        Self {
72            fluid_id: fluid,
73            amount,
74            falling,
75        }
76    }
77
78    /// Creates a source fluid state (amount=8, not falling).
79    #[must_use]
80    pub const fn source(fluid: FluidRef) -> Self {
81        Self {
82            fluid_id: fluid,
83            amount: 8,
84            falling: false,
85        }
86    }
87
88    /// Creates a flowing fluid state.
89    #[must_use]
90    pub const fn flowing(fluid: FluidRef, amount: u8, falling: bool) -> Self {
91        Self {
92            fluid_id: fluid,
93            amount,
94            falling,
95        }
96    }
97
98    /// Returns true if this is the empty fluid.
99    #[must_use]
100    pub const fn is_empty(&self) -> bool {
101        self.fluid_id.is_empty || self.amount == 0
102    }
103
104    /// Returns true if this is a source block (full fluid, not falling).
105    ///
106    /// Checks both the registry `fluid_id.is_source` flag (primary discriminator,
107    /// equivalent to vanilla checking if the type is a `SourceFluid`) and the
108    /// data invariant `amount == 8 && !falling`, guarding against malformed chunk data.
109    #[must_use]
110    pub const fn is_source(&self) -> bool {
111        self.fluid_id.is_source && self.amount == 8 && !self.falling
112    }
113
114    /// Returns the fluid's own height (0.0 to ~0.89).
115    #[must_use]
116    pub fn own_height(&self) -> f32 {
117        if self.is_empty() {
118            0.0
119        } else {
120            self.amount as f32 / 9.0
121        }
122    }
123
124    /// Decodes a fluid state from a liquid block's LEVEL property (0-15).
125    ///
126    /// - LEVEL 0 = source (amount=8, falling=false)
127    /// - LEVEL 1-7 = flowing levels 7-1 (amount = 8 - level)
128    /// - LEVEL 8-15 = falling fluid (amount=8, falling=true, but clamped)
129    #[must_use]
130    pub const fn from_block_level(fluid: FluidRef, level: u8) -> Self {
131        if level == 0 {
132            // Source block
133            Self::source(fluid)
134        } else if level <= 7 {
135            // Flowing fluid: level 1 = amount 7, level 7 = amount 1
136            Self::flowing(fluid, 8 - level, false)
137        } else {
138            // Falling fluid (level 8-15): vanilla encodes as 8 + (8 - amount)
139            // so amount = 16 - level. In practice only level=8 (amount=8) is used.
140            let amount = 16u8.saturating_sub(level).max(1);
141            Self::flowing(fluid, amount, true)
142        }
143    }
144
145    /// Encodes this fluid state to a liquid block's LEVEL property (0-15).
146    #[must_use]
147    pub const fn to_block_level(self) -> u8 {
148        if self.is_source() {
149            0
150        } else if self.falling {
151            8
152        } else {
153            // amount 7 -> level 1, amount 1 -> level 7
154            8 - self.amount
155        }
156    }
157}
158
159/// Registry for all fluids.
160pub struct FluidRegistry {
161    fluids_by_id: Vec<FluidRef>,
162    fluids_by_key: FxHashMap<Identifier, usize>,
163    tags: FxHashMap<Identifier, Vec<Identifier>>,
164    allows_registering: bool,
165}
166
167impl Default for FluidRegistry {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl FluidRegistry {
174    /// Creates a new, empty fluid registry.
175    #[must_use]
176    pub fn new() -> Self {
177        Self {
178            fluids_by_id: Vec::new(),
179            fluids_by_key: FxHashMap::default(),
180            tags: FxHashMap::default(),
181            allows_registering: true,
182        }
183    }
184
185    /// Registers a fluid and returns its ID.
186    pub fn register(&mut self, fluid: FluidRef) -> usize {
187        assert!(
188            self.allows_registering,
189            "Cannot register fluids after the registry has been frozen"
190        );
191
192        let id = self.fluids_by_id.len();
193        self.fluids_by_key.insert(fluid.key.clone(), id);
194        self.fluids_by_id.push(fluid);
195        id
196    }
197
198    /// Iterates over all fluids with their IDs.
199    pub fn iter(&self) -> impl Iterator<Item = (usize, FluidRef)> + '_ {
200        self.fluids_by_id
201            .iter()
202            .enumerate()
203            .map(|(id, &fluid)| (id, fluid))
204    }
205}
206
207crate::impl_registry!(FluidRegistry, Fluid, fluids_by_id, fluids_by_key, fluids);
208crate::impl_tagged_registry!(FluidRegistry, fluids_by_key, "fluid");
209
210use crate::REGISTRY;
211
212/// Returns true if the given `FluidRef` is water (including flowing water).
213#[must_use]
214pub fn is_water_fluid(fluid: FluidRef) -> bool {
215    !fluid.is_empty && fluid.has_tag(&FluidTag::WATER)
216}
217
218/// Returns true if the given `FluidRef` is lava (including flowing lava).
219#[must_use]
220pub fn is_lava_fluid(fluid: FluidRef) -> bool {
221    !fluid.is_empty && fluid.has_tag(&FluidTag::LAVA)
222}
223
224/// Extension trait for `FluidState` type-checking methods.
225pub trait FluidStateExt {
226    /// Returns true if this fluid state contains water.
227    fn is_water(&self) -> bool;
228    /// Returns true if this fluid state contains lava.
229    fn is_lava(&self) -> bool;
230}
231
232impl FluidStateExt for FluidState {
233    fn is_water(&self) -> bool {
234        is_water_fluid(self.fluid_id)
235    }
236    fn is_lava(&self) -> bool {
237        is_lava_fluid(self.fluid_id)
238    }
239}