steel_core/entity/entities/
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_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#[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
35unsafe impl DowncastType for ItemFrameEntity {
37 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item_frame");
38}
39
40impl ItemFrameEntity {
41 #[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 #[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 #[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 pub fn set_item(&self, item: ItemStack) {
108 self.set_item_with_update(item, true);
109 }
110
111 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}