steel_core/behavior/blocks/decoration/skull/
mod.rs1mod 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
35pub type SkullTypeRef = &'static SkullType;
37
38steel_registry::impl_registry_entry_eq!(SkullType);
39
40#[derive(Debug)]
42pub struct SkullType {
43 pub key: Identifier,
45 pub block: BlockRef,
47 pub wall_block: BlockRef,
49}
50
51impl SkullType {
52 #[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#[derive(Clone, Default, PartialEq, Eq, Debug)]
64pub struct SkullData {
65 pub owner: Option<ResolvableProfile>,
67 pub note_block_sound: Option<Identifier>,
69 pub custom_name: Option<TextComponent>,
71}
72
73impl SkullData {
74 #[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 #[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 #[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#[derive(Debug, Clone, Copy)]
110pub enum SkullPlacement {
111 Floor {
115 rotation: u8,
117 },
118 Wall {
120 facing: Direction,
122 },
123}
124
125#[derive(Debug, PartialEq, Eq)]
127pub struct Skull {
128 skull_type: SkullTypeRef,
129 data: SkullData,
130}
131
132impl Skull {
133 #[must_use]
139 pub fn new(skull_type: SkullTypeRef) -> Self {
140 Self {
141 skull_type,
142 data: SkullData::default(),
143 }
144 }
145
146 #[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 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 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 #[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 #[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 #[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 #[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 #[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 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 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#[derive(Debug, thiserror::Error)]
290pub enum InvalidPlacementError {
291 #[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}