Skip to main content

azalea_block/
block_state.rs

1use std::{
2    fmt::{self, Debug},
3    hint::assert_unchecked,
4    io::{self, Cursor, Write},
5    ops::Deref,
6};
7
8use azalea_buf::{AzBuf, AzBufVar, BufReadError};
9use azalea_registry::builtin::BlockKind;
10
11use crate::BlockTrait;
12
13/// The type that's used internally to represent a block state ID.
14///
15/// This should be either `u16` or `u32`. If you choose to modify it, you must
16/// also change it in `azalea-block-macros/src/lib.rs`.
17///
18/// This does not affect protocol serialization, it just allows you to make the
19/// internal type smaller if you want.
20pub type BlockStateIntegerRepr = u16;
21
22/// A representation of a state a block can be in.
23///
24/// For example, a stone block only has one state but each possible stair
25/// rotation is a different state.
26///
27/// Note that this type is internally either a `u16` or `u32`, depending on
28/// [`BlockStateIntegerRepr`].
29#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
30pub struct BlockState {
31    id: BlockStateIntegerRepr,
32}
33
34impl BlockState {
35    /// A shortcut for getting air, since it always has an ID of `0`.
36    ///
37    /// # Note
38    ///
39    /// This does not include the other types of air like cave air.
40    pub const AIR: BlockState = BlockState { id: 0 };
41
42    /// Create a new [`BlockState`]
43    ///
44    /// # Panics
45    ///
46    /// Panics if the block is not a valid state.
47    ///
48    /// You should probably use [`BlockState::try_from`] instead.
49    #[inline]
50    pub(crate) const fn new_const(id: BlockStateIntegerRepr) -> Self {
51        assert!(Self::is_valid_state(id));
52        Self { id }
53    }
54
55    /// Convert the [`BlockState`] into a [`&'static dyn BlockTrait`].
56    #[inline]
57    #[must_use]
58    pub const fn to_trait(self) -> &'static dyn BlockTrait {
59        crate::generated::blocks::blockstate_to_blocktrait(self)
60    }
61
62    /// Whether the block state is possible to exist in vanilla Minecraft.
63    ///
64    /// It's equivalent to checking that the state ID is not greater than
65    /// [`Self::MAX_STATE`].
66    #[inline]
67    pub const fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
68        state_id <= Self::MAX_STATE
69    }
70
71    /// Returns true if the block is air.
72    ///
73    /// This only checks for normal air, not other types like cave air.
74    #[inline]
75    pub fn is_air(&self) -> bool {
76        *self == Self::AIR
77    }
78
79    /// Returns the protocol ID for the block state.
80    ///
81    /// These IDs may change across Minecraft versions, so you shouldn't
82    /// hard-code them or store them in databases.
83    #[inline]
84    pub const fn id(&self) -> BlockStateIntegerRepr {
85        // this unsafe assert allows us to skip bounds checks if the array has at least
86        // MAX_STATE items.
87        // SAFETY: BlockState::id is private and is always checked with is_valid_state
88        // before being constructed.
89        unsafe { assert_unchecked(Self::is_valid_state(self.id)) };
90
91        self.id
92    }
93}
94
95impl Deref for BlockState {
96    type Target = dyn BlockTrait;
97
98    #[inline]
99    fn deref(&self) -> &Self::Target {
100        self.to_trait()
101    }
102}
103
104impl TryFrom<u32> for BlockState {
105    type Error = ();
106
107    /// Safely converts a u32 state ID to a block state.
108    fn try_from(state_id: u32) -> Result<Self, Self::Error> {
109        let state_id = state_id as BlockStateIntegerRepr;
110        if Self::is_valid_state(state_id) {
111            Ok(BlockState { id: state_id })
112        } else {
113            Err(())
114        }
115    }
116}
117impl TryFrom<i32> for BlockState {
118    type Error = ();
119
120    fn try_from(state_id: i32) -> Result<Self, Self::Error> {
121        Self::try_from(state_id as u32)
122    }
123}
124
125impl TryFrom<u16> for BlockState {
126    type Error = ();
127
128    /// Safely converts a u16 state ID to a block state.
129    fn try_from(id: u16) -> Result<Self, Self::Error> {
130        let id = id as BlockStateIntegerRepr;
131        if !Self::is_valid_state(id) {
132            return Err(());
133        }
134        Ok(BlockState { id })
135    }
136}
137impl From<BlockState> for u32 {
138    /// See [`BlockState::id`].
139    fn from(value: BlockState) -> Self {
140        value.id as u32
141    }
142}
143
144impl AzBuf for BlockState {
145    fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
146        let state_id = u32::azalea_read_var(buf)?;
147        Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
148            id: state_id as i32,
149        })
150    }
151    fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
152        u32::azalea_write_var(&(self.id as u32), buf)
153    }
154}
155
156impl Debug for BlockState {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(f, "BlockState(id: {}, {:?})", self.id, self.to_trait())
159    }
160}
161
162impl From<BlockState> for BlockKind {
163    fn from(block_state: BlockState) -> Self {
164        block_state.as_block_kind()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_from_u32() {
174        assert_eq!(
175            BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
176            BlockState::AIR
177        );
178
179        assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
180        assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
181    }
182
183    #[test]
184    fn test_from_blockstate() {
185        let block = BlockState::AIR.to_trait();
186        assert_eq!(block.id(), "air");
187
188        let block = BlockState::from(BlockKind::FloweringAzalea).to_trait();
189        assert_eq!(block.id(), "flowering_azalea");
190    }
191
192    #[test]
193    fn test_debug_blockstate() {
194        let formatted = format!("{:?}", BlockState::from(BlockKind::FloweringAzalea));
195        assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
196
197        let formatted = format!("{:?}", BlockState::from(BlockKind::BigDripleafStem));
198        assert!(
199            formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
200            "{}",
201            formatted
202        );
203    }
204}