1use 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#[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
43unsafe impl DowncastType for ItemFrameEntity {
45 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item_frame");
46}
47
48impl ItemFrameEntity {
49 #[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 #[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 #[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 pub fn set_item(&self, item: ItemStack) {
116 self.set_item_with_update(item, true);
117 }
118
119 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}