Skip to main content

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