Skip to main content

steel_core/behavior/blocks/decoration/skull/
mod.rs

1mod abstract_skull_block;
2mod piglin_wall_skull_block;
3mod player_head_block;
4mod player_wall_head_block;
5mod skull_block;
6mod wall_skull_block;
7mod wither_skull_block;
8mod wither_wall_skull_block;
9
10use std::io;
11use std::sync::Arc;
12
13pub use piglin_wall_skull_block::PiglinWallSkullBlock;
14pub use player_head_block::PlayerHeadBlock;
15pub use player_wall_head_block::PlayerWallHeadBlock;
16pub use skull_block::SkullBlock;
17use steel_registry::blocks::block_state_ext::BlockStateExt;
18use steel_registry::blocks::properties::BlockStateProperties;
19use steel_registry::data_components::DataComponentPatch;
20use steel_registry::data_components::vanilla_components::{CUSTOM_NAME, NOTE_BLOCK_SOUND, PROFILE};
21use steel_registry::resolvable_profile::PartialProfile;
22use steel_registry::{PlayerSkinPatch, ProfileProperty};
23use steel_registry::{REGISTRY, ResolvableProfile, blocks::BlockRef, item_stack::ItemStack};
24use steel_utils::types::UpdateFlags;
25use steel_utils::{BlockPos, BlockStateId, Direction, Downcast, Identifier};
26use text_components::TextComponent;
27pub use wall_skull_block::WallSkullBlock;
28pub use wither_skull_block::WitherSkullBlock;
29pub use wither_wall_skull_block::WitherWallSkullBlock;
30
31use crate::behavior::skull_types;
32use crate::block_entity::entities::SkullBlockEntity;
33use crate::world::World;
34
35/// Static reference to a skull type
36pub type SkullTypeRef = &'static SkullType;
37
38steel_registry::impl_registry_entry_eq!(SkullType);
39
40/// A skull kind and its floor and wall blocks
41#[derive(Debug)]
42pub struct SkullType {
43    /// Key of this skull type
44    pub key: Identifier,
45    /// Block used when the skull is placed on the floor
46    pub block: BlockRef,
47    /// Block used when the skull is placed on a wall
48    pub wall_block: BlockRef,
49}
50
51impl SkullType {
52    /// Returns the vanilla skull type whose floor or wall block is `block`
53    #[must_use]
54    pub fn from_block(block: BlockRef) -> Option<SkullTypeRef> {
55        skull_types::ALL
56            .iter()
57            .copied()
58            .find(|skull_type| skull_type.block == block || skull_type.wall_block == block)
59    }
60}
61
62/// Data stored by a skull
63#[derive(Clone, Default, PartialEq, Eq, Debug)]
64pub struct SkullData {
65    /// Player whose skin the skull shows
66    pub owner: Option<ResolvableProfile>,
67    /// Sound a note block plays below this skull
68    pub note_block_sound: Option<Identifier>,
69    /// Custom name for skull items
70    pub custom_name: Option<TextComponent>,
71}
72
73impl SkullData {
74    /// Creates a component patch with all data of this skull
75    #[must_use]
76    pub fn component_patch(&self) -> DataComponentPatch {
77        let mut patch = self.custom_name_patch();
78        if let Some(owner) = self.owner.clone() {
79            patch.set(PROFILE, owner);
80        }
81        if let Some(sound) = self.note_block_sound.clone() {
82            patch.set(NOTE_BLOCK_SOUND, sound);
83        }
84        patch
85    }
86
87    /// Creates a component patch with only the custom name
88    #[must_use]
89    pub fn custom_name_patch(&self) -> DataComponentPatch {
90        let mut patch = DataComponentPatch::new();
91        if let Some(name) = self.custom_name.clone() {
92            patch.set(CUSTOM_NAME, name);
93        }
94        patch
95    }
96
97    /// Reads the `SkullData` from the `ItemStack`'s components
98    #[must_use]
99    pub fn from_item(item: &ItemStack) -> Self {
100        Self {
101            owner: item.get(PROFILE).cloned(),
102            note_block_sound: item.get(NOTE_BLOCK_SOUND).cloned(),
103            custom_name: item.get(CUSTOM_NAME).cloned(),
104        }
105    }
106}
107
108/// How a skull is attached when placed as a block
109#[derive(Debug, Clone, Copy)]
110pub enum SkullPlacement {
111    /// Skull placed on the floor (values above 15 wrap)
112    ///
113    /// See [`steel_math::convert_to_rotation_segment`]
114    Floor {
115        /// The rotation in 16 segments
116        rotation: u8,
117    },
118    /// Skull placed on a wall
119    Wall {
120        /// Cardinal direction
121        facing: Direction,
122    },
123}
124
125/// Builder for skull items
126#[derive(Debug, PartialEq, Eq)]
127pub struct Skull {
128    skull_type: SkullTypeRef,
129    data: SkullData,
130}
131
132impl Skull {
133    /// Creates a new `Skull` from the given `SkullTypeRef`
134    ///
135    /// See [`Skull::of`] for creating a player's skull from their gameprofile
136    /// See [`Skull::player_named`] for creating a player's skull from their name
137    /// See [`Skull::texture`] for creating a player skull from a texture base64 string
138    #[must_use]
139    pub fn new(skull_type: SkullTypeRef) -> Self {
140        Self {
141            skull_type,
142            data: SkullData::default(),
143        }
144    }
145
146    /// Creates a player head from the profile of a player
147    /// ```ignore
148    /// # use steel_core::behavior::blocks::Skull;
149    /// let skull = Skull::of(player.gameprofile.clone());
150    /// ```
151    #[must_use]
152    pub fn of(player: impl Into<ResolvableProfile>) -> Self {
153        Self {
154            skull_type: &skull_types::PLAYER,
155            data: SkullData {
156                owner: Some(player.into()),
157                ..SkullData::default()
158            },
159        }
160    }
161
162    /// Creates a player head that the client resolves by player name
163    /// ```
164    /// # use steel_core::behavior::blocks::Skull;
165    /// let skull = Skull::player_named("Notch");
166    /// ```
167    pub fn player_named(name: impl Into<String>) -> io::Result<Self> {
168        Ok(Self {
169            skull_type: &skull_types::PLAYER,
170            data: SkullData {
171                owner: Some(ResolvableProfile::dynamic_name(
172                    name.into(),
173                    PlayerSkinPatch::default(),
174                )?),
175                ..SkullData::default()
176            },
177        })
178    }
179
180    /// Creates a player head showing a texture from a base64 encoded string
181    /// ```
182    /// # use steel_core::behavior::blocks::Skull;
183    /// const TEXTURE: &str = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2M2OWQ0MTA3NmE4ZGVhNGYwNmQzZjFhOWFjNDdjYzk5Njk4OGI3NGEwOTEzYWIyYWMxYTc0Y2FmNzA4MTkxOCJ9fX0=";
184    /// let skull = Skull::texture(TEXTURE);
185    /// ```
186    pub fn texture(value: impl Into<String>) -> io::Result<Self> {
187        let property = ProfileProperty::new("textures".to_owned(), value.into(), None)?;
188        let profile = PartialProfile::new(None, None, vec![property])?;
189        Ok(Self {
190            skull_type: &skull_types::PLAYER,
191            data: SkullData {
192                owner: Some(ResolvableProfile::static_partial(
193                    profile,
194                    PlayerSkinPatch::default(),
195                )),
196                ..SkullData::default()
197            },
198        })
199    }
200
201    /// Sets the custom name
202    #[must_use]
203    pub fn custom_name(mut self, custom_name: impl Into<TextComponent>) -> Self {
204        self.data.custom_name = Some(custom_name.into());
205        self
206    }
207
208    /// Sets the custom note block sound
209    #[must_use]
210    pub fn note_block_sound(mut self, note_block_sound: Identifier) -> Self {
211        self.data.note_block_sound = Some(note_block_sound);
212        self
213    }
214
215    /// Builds the skull as an item stack
216    #[must_use]
217    pub fn to_item(&self) -> ItemStack {
218        let item = REGISTRY.items.by_block(self.skull_type.block);
219        ItemStack::with_count_and_patch(item, 1, self.data.component_patch())
220    }
221
222    /// Reads the skull data from an `ItemStack` and returns the skull
223    #[must_use]
224    pub fn from_item(item: &ItemStack) -> Option<Self> {
225        let skull_type = SkullType::from_block(item.item().block?)?;
226        Some(Self {
227            skull_type,
228            data: SkullData::from_item(item),
229        })
230    }
231
232    /// Reads the skull placed at `pos`
233    #[must_use]
234    pub fn from_world(world: &World, pos: BlockPos) -> Option<Self> {
235        let skull_type = SkullType::from_block(world.get_block_state(pos).get_block())?;
236        let block_entity = world.get_block_entity(pos)?;
237        let skull = block_entity.downcast_ref::<SkullBlockEntity>()?;
238        Some(Self {
239            skull_type,
240            data: skull.data(),
241        })
242    }
243
244    /// Returns the block state of this skull for the given placement
245    pub fn block_state(
246        &self,
247        placement: SkullPlacement,
248    ) -> Result<BlockStateId, InvalidPlacementError> {
249        match placement {
250            SkullPlacement::Floor { rotation } => Ok(self
251                .skull_type
252                .block
253                .default_state()
254                .set_value(&BlockStateProperties::ROTATION_16, rotation & 15)),
255            SkullPlacement::Wall { facing } if facing.is_horizontal() => Ok(self
256                .skull_type
257                .wall_block
258                .default_state()
259                .set_value(&BlockStateProperties::HORIZONTAL_FACING, facing)),
260            SkullPlacement::Wall { facing } => Err(InvalidPlacementError::InvalidFacing(facing)),
261        }
262    }
263
264    /// Places this skull as a block and block entity
265    ///
266    /// > must run in Steel’s serialized world-mutation phase
267    ///
268    /// See [`World::set_block`] for the warning
269    pub fn place(
270        &self,
271        world: &Arc<World>,
272        pos: BlockPos,
273        placement: SkullPlacement,
274        update_flags: UpdateFlags,
275    ) -> Result<(), InvalidPlacementError> {
276        world.set_block(pos, self.block_state(placement)?, update_flags);
277        let Some(block_entity) = world.get_block_entity(pos) else {
278            return Ok(());
279        };
280        let Some(skull) = block_entity.downcast_ref::<SkullBlockEntity>() else {
281            return Ok(());
282        };
283        skull.set_data(self.data.clone());
284        Ok(())
285    }
286}
287
288/// Error during Skull placement
289#[derive(Debug, thiserror::Error)]
290pub enum InvalidPlacementError {
291    /// Wall skulls can only face horizontally
292    #[error("wall skulls cannot face: {0:?}")]
293    InvalidFacing(Direction),
294}
295
296#[cfg(test)]
297mod tests {
298    use steel_registry::init_vanilla_registry;
299    use steel_utils::{BlockPos, ChunkPos, types::UpdateFlags};
300
301    use crate::{
302        behavior::{
303            blocks::{Skull, SkullPlacement},
304            init_behaviors,
305        },
306        block_entity::init_block_entities,
307        test_support::{fresh_test_world, insert_ready_full_chunk},
308    };
309
310    #[test]
311    fn roundtrip() {
312        init_vanilla_registry();
313        init_behaviors();
314        init_block_entities();
315        let world = &fresh_test_world("skull_place").world;
316        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
317        let pos = BlockPos::new(0, 0, 0);
318        let skull = Skull::player_named("Cuz_Im_Clicks").expect("valid player");
319        let skull = skull.custom_name("Cuz_Im_Clicks");
320        skull
321            .place(
322                world,
323                pos,
324                SkullPlacement::Floor { rotation: 0 },
325                UpdateFlags::UPDATE_ALL,
326            )
327            .expect("placement should work");
328
329        let s = Skull::from_world(world, pos).expect("should succeed");
330        assert_eq!(skull, s);
331    }
332}