steel_core/entity/entities/objects/display_ui/
item_frame.rs1use std::sync::Weak;
4
5use glam::DVec3;
6use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
7use simdnbt::owned::{NbtCompound, NbtTag};
8use steel_macros::entity_behavior;
9use steel_math::DEGREE_90;
10use steel_registry::data_components::vanilla_components::MAP_ID;
11use steel_registry::entity_type::EntityTypeRef;
12use steel_registry::item_stack::ItemStack;
13use steel_registry::vanilla_blocks;
14use steel_registry::vanilla_entity_data::ItemFrameEntityData;
15use steel_utils::locks::SyncMutex;
16use steel_utils::{BlockPos, Direction, DowncastType, DowncastTypeKey, WorldAabb, axis::Axis};
17
18use crate::entity::{
19 Entity, EntityBase, EntityBaseLoad, EntityBaseState, EntitySyncedData, ItemFrame,
20};
21use crate::world::World;
22
23#[entity_behavior(class = "ItemFrame")]
29pub struct ItemFrameEntity {
30 base: EntityBase,
31 entity_type: EntityTypeRef,
32 entity_data: SyncMutex<ItemFrameEntityData>,
33 block_pos: SyncMutex<BlockPos>,
34}
35
36unsafe impl DowncastType for ItemFrameEntity {
38 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item_frame");
39}
40
41impl ItemFrameEntity {
42 #[must_use]
44 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
45 Self::new_attached(
46 entity_type,
47 id,
48 BlockPos::new(
49 position.x.floor() as i32,
50 position.y.floor() as i32,
51 position.z.floor() as i32,
52 ),
53 Direction::South,
54 world,
55 )
56 }
57
58 #[must_use]
60 pub fn new_attached(
61 entity_type: EntityTypeRef,
62 id: i32,
63 block_pos: BlockPos,
64 direction: Direction,
65 world: Weak<World>,
66 ) -> Self {
67 let entity = Self {
68 base: EntityBase::new_with_state(
69 id,
70 EntityBaseState::new_with_bounding_box(
71 Self::frame_center(block_pos, direction),
72 entity_type.dimensions,
73 Self::frame_bounding_box(block_pos, direction, false),
74 )
75 .with_rotation(Self::rotation_for_direction(direction)),
76 world,
77 ),
78 entity_type,
79 entity_data: SyncMutex::new(ItemFrameEntityData::new()),
80 block_pos: SyncMutex::new(block_pos),
81 };
82 entity
83 .entity_data
84 .lock()
85 .hanging_entity
86 .direction
87 .set(direction);
88 entity
89 }
90
91 #[must_use]
93 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
94 let position = load.position;
95 Self {
96 base: EntityBase::from_load(load, entity_type.dimensions),
97 entity_type,
98 entity_data: SyncMutex::new(ItemFrameEntityData::new()),
99 block_pos: SyncMutex::new(BlockPos::new(
100 position.x.floor() as i32,
101 position.y.floor() as i32,
102 position.z.floor() as i32,
103 )),
104 }
105 }
106
107 pub fn set_item(&self, item: ItemStack) {
109 self.set_item_with_update(item, true);
110 }
111
112 pub(crate) fn set_item_with_update(&self, mut item: ItemStack, update_comparators: bool) {
114 if !item.is_empty() {
115 item.set_count(1);
116 }
117 self.entity_data.lock().item.set(item);
118 self.recalculate_position();
119 if update_comparators && let Some(world) = self.level() {
120 world.update_neighbor_for_output_signal(*self.block_pos.lock(), &vanilla_blocks::AIR);
121 }
122 }
123
124 fn set_direction(&self, direction: Direction) {
125 self.entity_data
126 .lock()
127 .hanging_entity
128 .direction
129 .set(direction);
130 self.base
131 .set_rotation(Self::rotation_for_direction(direction));
132 self.recalculate_position();
133 }
134
135 fn recalculate_position(&self) {
136 let block_pos = *self.block_pos.lock();
137 let direction = *self.entity_data.lock().hanging_entity.direction.get();
138 let position = Self::frame_center(block_pos, direction);
139 if let Err(error) = self.base.try_set_position(position) {
140 panic!(
141 "failed to commit item frame {} position recalculation: {error}",
142 self.base.id()
143 );
144 }
145 self.base.set_bounding_box(Self::frame_bounding_box(
146 block_pos,
147 direction,
148 self.has_framed_map(),
149 ));
150 }
151
152 fn has_framed_map(&self) -> bool {
153 self.entity_data.lock().item.get().has(MAP_ID)
154 }
155
156 fn frame_center(block_pos: BlockPos, direction: Direction) -> DVec3 {
157 let off = direction.offset_vec().as_dvec3() * 0.46875;
158 block_pos.0.as_dvec3() + DVec3::splat(0.5) - off
159 }
160
161 fn rotation_for_direction(direction: Direction) -> (f32, f32) {
162 if direction.is_horizontal() {
163 (
164 f32::from(direction_2d_data_value(direction)) * DEGREE_90,
165 0.0,
166 )
167 } else {
168 let pitch = match direction {
169 Direction::Up => -DEGREE_90,
170 Direction::Down => DEGREE_90,
171 Direction::North | Direction::South | Direction::West | Direction::East => 0.0,
172 };
173 (0.0, pitch)
174 }
175 }
176
177 fn frame_bounding_box(
178 block_pos: BlockPos,
179 direction: Direction,
180 has_framed_map: bool,
181 ) -> WorldAabb {
182 let center = Self::frame_center(block_pos, direction);
183 let size = if has_framed_map { 1.0 } else { 0.75 };
184 let x_size = if direction.axis() == Axis::X {
185 0.0625
186 } else {
187 size
188 };
189 let y_size = if direction.axis() == Axis::Y {
190 0.0625
191 } else {
192 size
193 };
194 let z_size = if direction.axis() == Axis::Z {
195 0.0625
196 } else {
197 size
198 };
199 WorldAabb::new(
200 center.x - x_size / 2.0,
201 center.y - y_size / 2.0,
202 center.z - z_size / 2.0,
203 center.x + x_size / 2.0,
204 center.y + y_size / 2.0,
205 center.z + z_size / 2.0,
206 )
207 }
208}
209
210impl ItemFrame for ItemFrameEntity {
211 fn direction(&self) -> Direction {
212 *self.entity_data.lock().hanging_entity.direction.get()
213 }
214
215 fn analog_output(&self) -> i32 {
216 let entity_data = self.entity_data.lock();
217 if entity_data.item.get().is_empty() {
218 0
219 } else {
220 *entity_data.rotation.get() % 8 + 1
221 }
222 }
223}
224
225impl Entity for ItemFrameEntity {
226 fn base(&self) -> &EntityBase {
227 &self.base
228 }
229
230 fn entity_type(&self) -> EntityTypeRef {
231 self.entity_type
232 }
233
234 fn spawn_data(&self) -> i32 {
235 direction_3d_data_value(*self.entity_data.lock().hanging_entity.direction.get())
236 }
237
238 fn spawn_position(&self) -> DVec3 {
239 let block_pos = *self.block_pos.lock();
240 DVec3::new(
241 f64::from(block_pos.x()),
242 f64::from(block_pos.y()),
243 f64::from(block_pos.z()),
244 )
245 }
246
247 fn is_pickable(&self) -> bool {
248 true
249 }
250
251 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
252 Some(&self.entity_data)
253 }
254
255 fn save_additional(&self, nbt: &mut NbtCompound) {
256 let block_pos = *self.block_pos.lock();
257 nbt.insert(
258 "block_pos",
259 NbtTag::IntArray(vec![block_pos.x(), block_pos.y(), block_pos.z()]),
260 );
261
262 let entity_data = self.entity_data.lock();
263 let item = entity_data.item.get();
264 if !item.is_empty() {
265 nbt.insert("Item", item.to_nbt_tag_ref());
266 }
267 nbt.insert("ItemRotation", *entity_data.rotation.get() as i8);
268 nbt.insert("ItemDropChance", 1.0_f32);
269 nbt.insert(
270 "Facing",
271 direction_3d_data_value(*entity_data.hanging_entity.direction.get()) as i8,
272 );
273 nbt.insert("Invisible", 0_i8);
274 nbt.insert("Fixed", 0_i8);
275 }
276
277 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
278 if let Some(block_pos) = nbt.int_array("block_pos")
279 && block_pos.len() == 3
280 {
281 *self.block_pos.lock() = BlockPos::new(block_pos[0], block_pos[1], block_pos[2]);
282 }
283
284 if let Some(item_tag) = nbt.compound("Item")
285 && let Some(item) = ItemStack::from_borrowed_compound(&item_tag)
286 {
287 self.set_item_with_update(item, false);
288 }
289
290 if let Some(item_rotation) = nbt.byte("ItemRotation") {
291 self.entity_data
292 .lock()
293 .rotation
294 .set(i32::from(item_rotation).rem_euclid(8));
295 }
296
297 let facing = nbt
298 .byte("Facing")
299 .and_then(|value| direction_from_3d_data_value(i32::from(value)))
300 .or_else(|| nbt.int("Facing").and_then(direction_from_3d_data_value));
301 if let Some(direction) = facing {
302 self.set_direction(direction);
303 }
304
305 self.recalculate_position();
306 }
307}
308
309const fn direction_3d_data_value(direction: Direction) -> i32 {
310 match direction {
311 Direction::Down => 0,
312 Direction::Up => 1,
313 Direction::North => 2,
314 Direction::South => 3,
315 Direction::West => 4,
316 Direction::East => 5,
317 }
318}
319
320const fn direction_from_3d_data_value(value: i32) -> Option<Direction> {
321 match value {
322 0 => Some(Direction::Down),
323 1 => Some(Direction::Up),
324 2 => Some(Direction::North),
325 3 => Some(Direction::South),
326 4 => Some(Direction::West),
327 5 => Some(Direction::East),
328 _ => None,
329 }
330}
331
332const fn direction_2d_data_value(direction: Direction) -> u8 {
333 match direction {
334 Direction::South | Direction::Down | Direction::Up => 0,
335 Direction::West => 1,
336 Direction::North => 2,
337 Direction::East => 3,
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use std::string::ToString;
345 use steel_registry::{vanilla_entities, vanilla_items};
346
347 #[test]
348 fn item_frame_persists_structure_marker_state() {
349 let frame = ItemFrameEntity::new_attached(
350 &vanilla_entities::ITEM_FRAME,
351 1,
352 BlockPos::new(12, 80, 14),
353 Direction::West,
354 Weak::new(),
355 );
356 frame.set_item(ItemStack::new(&vanilla_items::ELYTRA));
357
358 let mut nbt = NbtCompound::new();
359 frame.save_additional(&mut nbt);
360
361 assert_eq!(nbt.byte("Facing"), Some(4));
362 assert_eq!(nbt.byte("ItemRotation"), Some(0));
363 assert_eq!(nbt.float("ItemDropChance"), Some(1.0));
364 assert_eq!(nbt.byte("Invisible"), Some(0));
365 assert_eq!(nbt.byte("Fixed"), Some(0));
366 let Some(item) = nbt.compound("Item") else {
367 panic!("item frame should save framed item");
368 };
369 assert_eq!(
370 item.string("id").map(ToString::to_string),
371 Some("minecraft:elytra".to_owned())
372 );
373 assert_eq!(item.int("count"), Some(1));
374 }
375
376 #[test]
377 fn item_frame_is_pickable_like_vanilla() {
378 let frame = ItemFrameEntity::new_attached(
379 &vanilla_entities::ITEM_FRAME,
380 1,
381 BlockPos::new(12, 80, 14),
382 Direction::West,
383 Weak::new(),
384 );
385
386 assert!(frame.is_pickable());
387 }
388
389 #[test]
390 fn analog_output_uses_item_presence_and_rotation() {
391 let frame = ItemFrameEntity::new_attached(
392 &vanilla_entities::ITEM_FRAME,
393 1,
394 BlockPos::new(12, 80, 14),
395 Direction::West,
396 Weak::new(),
397 );
398 assert_eq!(frame.analog_output(), 0);
399
400 frame.set_item_with_update(ItemStack::new(&vanilla_items::ELYTRA), false);
401 assert_eq!(frame.analog_output(), 1);
402 frame.entity_data.lock().rotation.set(7);
403 assert_eq!(frame.analog_output(), 8);
404 }
405}