azalea_block/
block_state.rs

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