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::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/// Item frame state needed by end-city structure markers.
24///
25/// This intentionally implements only placement, synced item/facing data,
26/// persistence, and comparator integration. Interaction, drops, map tracking,
27/// and support checks belong to the full item-frame entity implementation.
28#[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
36// SAFETY: This key is owned by Steel and uniquely identifies `ItemFrameEntity`.
37unsafe impl DowncastType for ItemFrameEntity {
38    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item_frame");
39}
40
41impl ItemFrameEntity {
42    /// Creates a fresh item frame from the generic entity factory path.
43    #[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    /// Creates a fresh item frame attached to `block_pos`.
59    #[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    /// Creates an item frame from persistent entity data.
92    #[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    /// Sets the framed item, matching vanilla by storing a single item.
108    pub fn set_item(&self, item: ItemStack) {
109        self.set_item_with_update(item, true);
110    }
111
112    /// Sets the framed item and optionally notifies nearby comparators.
113    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}