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