Skip to main content

steel_core/entity/entities/objects/display_ui/
item_frame.rs

1//! Minimal persistent item-frame entity used by structure generation.
2
3use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
7use simdnbt::owned::NbtCompound;
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::nbt::NbtNumeric as _;
17use steel_utils::nbt::NbtValueInput as _;
18use steel_utils::{BlockPos, Direction, DowncastType, DowncastTypeKey, WorldAabb, axis::Axis};
19
20use crate::entity::block_attached_entity::{BlockAttachedEntity, BlockAttachedEntityBase};
21use crate::entity::damage::DamageSource;
22use crate::entity::{
23    Entity, EntityBase, EntityBaseLoad, EntityBaseState, EntityMoveError, EntitySyncedData,
24    ItemFrame, SharedEntity,
25};
26use crate::physics::{MoveResult, MoverType};
27use crate::world::{Explosion, World};
28
29/// Item frame state needed by end-city structure markers.
30///
31/// This intentionally implements only placement, synced item/facing data,
32/// persistence, and comparator integration.
33///
34/// TODO: Add interaction, drops, map tracking, and support checks.
35#[entity_behavior(class = "ItemFrame")]
36pub struct ItemFrameEntity {
37    base: EntityBase,
38    entity_type: EntityTypeRef,
39    entity_data: SyncMutex<ItemFrameEntityData>,
40    block_attached_entity_base: BlockAttachedEntityBase,
41}
42
43// SAFETY: This key is owned by Steel and uniquely identifies `ItemFrameEntity`.
44unsafe impl DowncastType for ItemFrameEntity {
45    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item_frame");
46}
47
48impl ItemFrameEntity {
49    /// Creates a fresh item frame from the generic entity factory path.
50    #[must_use]
51    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
52        Self::new_attached(
53            entity_type,
54            id,
55            BlockPos::new(
56                position.x.floor() as i32,
57                position.y.floor() as i32,
58                position.z.floor() as i32,
59            ),
60            Direction::South,
61            world,
62        )
63    }
64
65    /// Creates a fresh item frame attached to `block_pos`.
66    #[must_use]
67    pub fn new_attached(
68        entity_type: EntityTypeRef,
69        id: i32,
70        block_pos: BlockPos,
71        direction: Direction,
72        world: Weak<World>,
73    ) -> Self {
74        let entity = Self {
75            base: EntityBase::new_with_state(
76                id,
77                EntityBaseState::new_with_bounding_box(
78                    Self::frame_center(block_pos, direction),
79                    entity_type.dimensions,
80                    Self::frame_bounding_box(block_pos, direction, false),
81                )
82                .with_rotation(Self::rotation_for_direction(direction)),
83                world,
84            ),
85            entity_type,
86            entity_data: SyncMutex::new(ItemFrameEntityData::new()),
87            block_attached_entity_base: BlockAttachedEntityBase::new(block_pos),
88        };
89        entity
90            .entity_data
91            .lock()
92            .hanging_entity
93            .direction
94            .set(direction);
95        entity
96    }
97
98    /// Creates an item frame from persistent entity data.
99    #[must_use]
100    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
101        let position = load.position;
102        Self {
103            base: EntityBase::from_load(load, entity_type.dimensions),
104            entity_type,
105            entity_data: SyncMutex::new(ItemFrameEntityData::new()),
106            block_attached_entity_base: BlockAttachedEntityBase::new(BlockPos::new(
107                position.x.floor() as i32,
108                position.y.floor() as i32,
109                position.z.floor() as i32,
110            )),
111        }
112    }
113
114    /// Sets the framed item, matching vanilla by storing a single item.
115    pub fn set_item(&self, item: ItemStack) {
116        self.set_item_with_update(item, true);
117    }
118
119    /// Sets the framed item and optionally notifies nearby comparators.
120    pub(crate) fn set_item_with_update(&self, mut item: ItemStack, update_comparators: bool) {
121        if !item.is_empty() {
122            item.set_count(1);
123        }
124        self.entity_data.lock().item.set(item);
125        self.recalculate_position_or_warn();
126        if update_comparators && let Some(world) = self.level() {
127            world.update_neighbor_for_output_signal(
128                self.block_attached_entity_base.pos(),
129                &vanilla_blocks::AIR,
130            );
131        }
132    }
133
134    fn set_direction(&self, direction: Direction) {
135        self.entity_data
136            .lock()
137            .hanging_entity
138            .direction
139            .set(direction);
140        self.base
141            .set_rotation(Self::rotation_for_direction(direction));
142        self.recalculate_position_or_warn();
143    }
144
145    fn try_recalculate_position(&self) -> Result<(), EntityMoveError> {
146        let block_pos = self.block_attached_entity_base.pos();
147        let direction = *self.entity_data.lock().hanging_entity.direction.get();
148        let position = Self::frame_center(block_pos, direction);
149        self.base.try_set_position(position)?;
150        self.base.set_bounding_box(Self::frame_bounding_box(
151            block_pos,
152            direction,
153            self.has_framed_map(),
154        ));
155
156        Ok(())
157    }
158
159    fn recalculate_position_or_warn(&self) {
160        if let Err(error) = self.try_recalculate_position() {
161            log::warn!(
162                "failed to commit item frame {} position recalculation: {error}",
163                self.base.id()
164            );
165        }
166    }
167
168    fn has_framed_map(&self) -> bool {
169        self.entity_data.lock().item.get().has(MAP_ID)
170    }
171
172    fn frame_center(block_pos: BlockPos, direction: Direction) -> DVec3 {
173        let off = direction.offset_vec().as_dvec3() * 0.46875;
174        block_pos.0.as_dvec3() + DVec3::splat(0.5) - off
175    }
176
177    fn rotation_for_direction(direction: Direction) -> (f32, f32) {
178        if direction.is_horizontal() {
179            (
180                f32::from(direction_2d_data_value(direction)) * DEGREE_90,
181                0.0,
182            )
183        } else {
184            let pitch = match direction {
185                Direction::Up => -DEGREE_90,
186                Direction::Down => DEGREE_90,
187                Direction::North | Direction::South | Direction::West | Direction::East => 0.0,
188            };
189            (0.0, pitch)
190        }
191    }
192
193    fn frame_bounding_box(
194        block_pos: BlockPos,
195        direction: Direction,
196        has_framed_map: bool,
197    ) -> WorldAabb {
198        let center = Self::frame_center(block_pos, direction);
199        let size = if has_framed_map { 1.0 } else { 0.75 };
200        let x_size = if direction.axis() == Axis::X {
201            0.0625
202        } else {
203            size
204        };
205        let y_size = if direction.axis() == Axis::Y {
206            0.0625
207        } else {
208            size
209        };
210        let z_size = if direction.axis() == Axis::Z {
211            0.0625
212        } else {
213            size
214        };
215        WorldAabb::new(
216            center.x - x_size / 2.0,
217            center.y - y_size / 2.0,
218            center.z - z_size / 2.0,
219            center.x + x_size / 2.0,
220            center.y + y_size / 2.0,
221            center.z + z_size / 2.0,
222        )
223    }
224}
225
226impl ItemFrame for ItemFrameEntity {
227    fn direction(&self) -> Direction {
228        *self.entity_data.lock().hanging_entity.direction.get()
229    }
230
231    fn analog_output(&self) -> i32 {
232        let entity_data = self.entity_data.lock();
233        if entity_data.item.get().is_empty() {
234            0
235        } else {
236            *entity_data.rotation.get() % 8 + 1
237        }
238    }
239}
240
241impl Entity for ItemFrameEntity {
242    fn base(&self) -> &EntityBase {
243        &self.base
244    }
245
246    fn entity_type(&self) -> EntityTypeRef {
247        self.entity_type
248    }
249
250    fn spawn_data(&self) -> i32 {
251        direction_3d_data_value(*self.entity_data.lock().hanging_entity.direction.get())
252    }
253
254    fn spawn_position(&self) -> DVec3 {
255        let block_pos = self.block_attached_entity_base.pos();
256        DVec3::new(
257            f64::from(block_pos.x()),
258            f64::from(block_pos.y()),
259            f64::from(block_pos.z()),
260        )
261    }
262
263    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
264        Some(&self.entity_data)
265    }
266
267    fn tick(self: Arc<Self>) {
268        self.tick_block_attached_entity();
269    }
270
271    fn is_pickable(&self) -> bool {
272        self.is_pickable_block_attached_entity()
273    }
274
275    fn ignore_explosion(&self, explosion: &dyn Explosion) -> bool {
276        explosion
277            .direct_source_entity()
278            .is_some_and(Entity::is_in_water)
279            || !explosion.should_affect_blocklike_entities()
280    }
281
282    fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
283        self.hurt_block_attached_entity(world, source, amount)
284    }
285
286    fn skip_attack_interaction(&self, source: &SharedEntity) -> bool {
287        self.skip_attack_interaction_block_attached_entity(source)
288    }
289
290    fn push_impulse(&self, impulse: DVec3) {
291        self.push_impulse_block_attached_entity(impulse);
292    }
293
294    fn move_entity(self: Arc<Self>, mover_type: MoverType, delta: DVec3) -> Option<MoveResult> {
295        self.move_entity_block_attached_entity(mover_type, delta)
296    }
297
298    fn refresh_dimensions(&self) {
299        self.refresh_dimensions_block_attached_entity();
300    }
301
302    fn try_set_position(&self, pos: DVec3) -> Result<(), EntityMoveError> {
303        self.try_set_position_block_attached_entity(pos)
304    }
305
306    fn save_additional(&self, nbt: &mut NbtCompound) {
307        self.save_block_attached_entity(nbt);
308
309        let entity_data = self.entity_data.lock();
310        let item = entity_data.item.get();
311        if !item.is_empty() {
312            nbt.insert("Item", item.to_nbt_tag_ref());
313        }
314        nbt.insert("ItemRotation", *entity_data.rotation.get() as i8);
315        nbt.insert("ItemDropChance", 1.0_f32);
316        nbt.insert(
317            "Facing",
318            direction_3d_data_value(*entity_data.hanging_entity.direction.get()) as i8,
319        );
320        nbt.insert("Invisible", 0_i8);
321        nbt.insert("Fixed", 0_i8);
322    }
323
324    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
325        self.load_block_attached_entity(nbt);
326
327        if let Some(item_tag) = nbt.compound("Item")
328            && let Some(item) = ItemStack::from_borrowed_compound(&item_tag)
329        {
330            self.set_item_with_update(item, false);
331        }
332
333        if let Some(item_rotation) = nbt.byte_value("ItemRotation") {
334            self.entity_data
335                .lock()
336                .rotation
337                .set(i32::from(item_rotation) % 8);
338        }
339
340        let facing = nbt
341            .get("Facing")
342            .and_then(|tag| tag.codec_i8())
343            .map_or(Direction::Down, |value| {
344                Direction::from_3d_data_value(i32::from(value))
345            });
346        self.set_direction(facing);
347
348        self.recalculate_position_or_warn();
349    }
350}
351
352impl BlockAttachedEntity for ItemFrameEntity {
353    fn block_attached_entity_base(&self) -> &BlockAttachedEntityBase {
354        &self.block_attached_entity_base
355    }
356
357    fn survives(&self) -> bool {
358        true
359    }
360
361    fn drop_item(&self, _caused_by: Option<&dyn Entity>) {}
362
363    fn recalculate_bounding_box(&self) -> Result<(), EntityMoveError> {
364        self.try_recalculate_position()
365    }
366}
367
368const fn direction_3d_data_value(direction: Direction) -> i32 {
369    match direction {
370        Direction::Down => 0,
371        Direction::Up => 1,
372        Direction::North => 2,
373        Direction::South => 3,
374        Direction::West => 4,
375        Direction::East => 5,
376    }
377}
378
379const fn direction_2d_data_value(direction: Direction) -> u8 {
380    match direction {
381        Direction::South | Direction::Down | Direction::Up => 0,
382        Direction::West => 1,
383        Direction::North => 2,
384        Direction::East => 3,
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use simdnbt::borrow::read_compound;
392    use std::io::Cursor;
393    use std::string::ToString;
394    use steel_registry::{vanilla_entities, vanilla_items};
395
396    fn frame_loaded_from(nbt: &NbtCompound) -> ItemFrameEntity {
397        let frame = ItemFrameEntity::new_attached(
398            &vanilla_entities::ITEM_FRAME,
399            1,
400            BlockPos::new(12, 80, 14),
401            Direction::West,
402            Weak::new(),
403        );
404        let mut bytes = Vec::new();
405        nbt.write(&mut bytes);
406        let borrowed =
407            read_compound(&mut Cursor::new(bytes.as_slice())).expect("test NBT should reborrow");
408        frame.load_additional((&borrowed).into());
409        frame
410    }
411
412    #[test]
413    fn load_wraps_facing_and_keeps_rotation_sign_like_java() {
414        let mut nbt = NbtCompound::new();
415        nbt.insert("Facing", 7_i8);
416        nbt.insert("ItemRotation", -1_i8);
417        let frame = frame_loaded_from(&nbt);
418
419        let entity_data = frame.entity_data.lock();
420        assert_eq!(*entity_data.hanging_entity.direction.get(), Direction::Up);
421        assert_eq!(*entity_data.rotation.get(), -1);
422    }
423
424    #[test]
425    fn load_defaults_missing_facing_to_down() {
426        let frame = frame_loaded_from(&NbtCompound::new());
427
428        let entity_data = frame.entity_data.lock();
429        assert_eq!(*entity_data.hanging_entity.direction.get(), Direction::Down);
430    }
431
432    #[test]
433    fn item_frame_persists_structure_marker_state() {
434        let frame = ItemFrameEntity::new_attached(
435            &vanilla_entities::ITEM_FRAME,
436            1,
437            BlockPos::new(12, 80, 14),
438            Direction::West,
439            Weak::new(),
440        );
441        frame.set_item(ItemStack::new(&vanilla_items::ELYTRA));
442
443        let mut nbt = NbtCompound::new();
444        frame.save_additional(&mut nbt);
445
446        assert_eq!(nbt.byte("Facing"), Some(4));
447        assert_eq!(nbt.byte("ItemRotation"), Some(0));
448        assert_eq!(nbt.float("ItemDropChance"), Some(1.0));
449        assert_eq!(nbt.byte("Invisible"), Some(0));
450        assert_eq!(nbt.byte("Fixed"), Some(0));
451        let Some(item) = nbt.compound("Item") else {
452            panic!("item frame should save framed item");
453        };
454        assert_eq!(
455            item.string("id").map(ToString::to_string),
456            Some("minecraft:elytra".to_owned())
457        );
458        assert_eq!(item.int("count"), Some(1));
459    }
460
461    #[test]
462    fn item_frame_is_pickable_like_vanilla() {
463        let frame = ItemFrameEntity::new_attached(
464            &vanilla_entities::ITEM_FRAME,
465            1,
466            BlockPos::new(12, 80, 14),
467            Direction::West,
468            Weak::new(),
469        );
470
471        assert!(frame.is_pickable());
472    }
473
474    #[test]
475    fn analog_output_uses_item_presence_and_rotation() {
476        let frame = ItemFrameEntity::new_attached(
477            &vanilla_entities::ITEM_FRAME,
478            1,
479            BlockPos::new(12, 80, 14),
480            Direction::West,
481            Weak::new(),
482        );
483        assert_eq!(frame.analog_output(), 0);
484
485        frame.set_item_with_update(ItemStack::new(&vanilla_items::ELYTRA), false);
486        assert_eq!(frame.analog_output(), 1);
487        frame.entity_data.lock().rotation.set(7);
488        assert_eq!(frame.analog_output(), 8);
489    }
490}