1use crate::behavior::InteractionResult;
2use crate::entity::damage::DamageSource;
3use crate::entity::{BorrowedNbtCompoundView, SharedEntity};
4use crate::entity::{Entity, EntityBase, EntityBaseLoad, EntitySyncedData};
5use crate::player::Player;
6use crate::world::World;
7use glam::DVec3;
8use parking_lot::MutexGuard;
9use simdnbt::owned::{NbtCompound, NbtTag};
10use simdnbt::{FromNbtTag, ToNbtTag};
11use std::sync::Arc;
12use std::sync::Weak;
13use steel_macros::entity_behavior;
14use steel_registry::blocks::behavior::PushReaction;
15use steel_registry::entity_data::EntityPose;
16use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
17use steel_registry::vanilla_entity_data::InteractionEntityData;
18use steel_utils::locks::SyncMutex;
19use steel_utils::nbt::NbtNumeric as _;
20use steel_utils::nbt::NbtValueInput as _;
21use steel_utils::types::InteractionHand;
22use steel_utils::{DowncastType, DowncastTypeKey, UuidExt, WorldAabb};
23use uuid::Uuid;
24
25const DEFAULT_WIDTH: f32 = 1.0;
26const DEFAULT_HEIGHT: f32 = 1.0;
27const DEFAULT_RESPONSE: bool = false;
28
29const DEFAULT_DIMENSIONS: EntityDimensions =
30 EntityDimensions::with_default_eye_height(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31
32const TAG_WIDTH: &str = "width";
33const TAG_HEIGHT: &str = "height";
34const TAG_ATTACK: &str = "attack";
35const TAG_INTERACTION: &str = "interaction";
36const TAG_RESPONSE: &str = "response";
37
38const TAG_PLAYER: &str = "player";
39const TAG_TIMESTAMP: &str = "timestamp";
40
41#[entity_behavior(class = "Interaction")]
47pub struct InteractionEntity {
48 base: EntityBase,
49 entity_type: EntityTypeRef,
50 entity_data: SyncMutex<InteractionEntityData>,
51 interaction: SyncMutex<Option<PlayerAction>>,
52 attack: SyncMutex<Option<PlayerAction>>,
53}
54
55unsafe impl DowncastType for InteractionEntity {
57 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/interaction");
58}
59
60impl InteractionEntity {
61 #[must_use]
63 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
64 Self {
65 base: EntityBase::new(id, position, DEFAULT_DIMENSIONS, world),
66 entity_type,
67 entity_data: SyncMutex::new(InteractionEntityData::new()),
68 attack: SyncMutex::new(None),
69 interaction: SyncMutex::new(None),
70 }
71 }
72
73 #[must_use]
75 pub fn with_uuid(
76 entity_type: EntityTypeRef,
77 id: i32,
78 position: DVec3,
79 uuid: Uuid,
80 world: Weak<World>,
81 ) -> Self {
82 Self {
83 base: EntityBase::with_uuid(id, uuid, position, DEFAULT_DIMENSIONS, world),
84 entity_type,
85 entity_data: SyncMutex::new(InteractionEntityData::new()),
86 attack: SyncMutex::new(None),
87 interaction: SyncMutex::new(None),
88 }
89 }
90
91 #[must_use]
93 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
94 Self {
95 base: EntityBase::from_load(load, DEFAULT_DIMENSIONS),
96 entity_type,
97 entity_data: SyncMutex::new(InteractionEntityData::new()),
98 attack: SyncMutex::new(None),
99 interaction: SyncMutex::new(None),
100 }
101 }
102
103 pub fn with_entity_data<R>(
109 &self,
110 f: impl FnOnce(&mut InteractionEntityDataView<'_>) -> R,
111 ) -> R {
112 let (value, dimensions_changed) = {
113 let mut view = InteractionEntityDataView {
114 guard: self.entity_data.lock(),
115 dimensions_changed: false,
116 };
117 let value = f(&mut view);
118 (value, view.dimensions_changed)
119 };
120 if dimensions_changed {
121 self.refresh_dimensions();
122 }
123 value
124 }
125
126 pub fn last_attack(&self) -> Option<PlayerAction> {
128 *self.attack.lock()
129 }
130
131 pub fn last_interaction(&self) -> Option<PlayerAction> {
133 *self.interaction.lock()
134 }
135}
136
137impl Entity for InteractionEntity {
138 fn base(&self) -> &EntityBase {
139 &self.base
140 }
141
142 fn entity_type(&self) -> EntityTypeRef {
143 self.entity_type
144 }
145
146 fn is_pickable(&self) -> bool {
147 true
148 }
149
150 fn skip_attack_interaction(&self, source: &SharedEntity) -> bool {
151 let Some(player) = source.as_player() else {
152 return false;
153 };
154 if let Some(world) = self.level() {
155 *self.attack.lock() = Some(PlayerAction {
156 player: player.uuid(),
157 timestamp: world.game_time(),
158 });
159 }
161 !self.entity_data.lock().response.get()
162 }
163
164 fn is_ignoring_block_triggers(&self) -> bool {
165 true
166 }
167
168 fn piston_push_reaction(&self) -> PushReaction {
169 PushReaction::IgnoreEntity
170 }
171
172 fn can_be_hit_by_projectile(&self) -> bool {
173 false
174 }
175
176 fn make_bounding_box_at(&self, position: DVec3) -> WorldAabb {
177 let guard = self.entity_data.lock();
178 WorldAabb::entity_box(
179 position.x,
180 position.y,
181 position.z,
182 f64::from(*guard.width.get() / 2.0),
183 f64::from(*guard.height.get()),
184 )
185 }
186
187 fn tick(self: Arc<Self>) {}
188
189 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
190 Some(&self.entity_data)
191 }
192
193 fn interact(
194 &self,
195 player: &Player,
196 _hand: InteractionHand,
197 _location: DVec3,
198 ) -> InteractionResult {
199 if let Some(world) = self.level() {
200 *self.interaction.lock() = Some(PlayerAction {
201 player: player.uuid(),
202 timestamp: world.game_time(),
203 });
204 }
205 InteractionResult::Consume
206 }
207
208 fn no_physics(&self) -> bool {
209 true
210 }
211
212 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
213 let guard = self.entity_data.lock();
214 EntityDimensions::with_default_eye_height(*guard.width.get(), *guard.height.get())
215 }
216
217 fn save_additional(&self, nbt: &mut NbtCompound) {
218 {
219 let guard = self.entity_data.lock();
220 nbt.insert(TAG_WIDTH, *guard.width.get());
221 nbt.insert(TAG_HEIGHT, *guard.height.get());
222 nbt.insert(TAG_RESPONSE, *guard.response.get());
223 }
224 {
225 let guard = self.attack.lock();
226 if let Some(attack) = guard.as_ref() {
227 nbt.insert(TAG_ATTACK, attack);
228 }
229 }
230 {
231 let guard = self.interaction.lock();
232 if let Some(interaction) = guard.as_ref() {
233 nbt.insert(TAG_INTERACTION, interaction);
234 }
235 }
236 }
237
238 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
239 let dimensions_changed = {
240 let mut guard = self.entity_data.lock();
241
242 guard
243 .width
244 .set(nbt.float_value(TAG_WIDTH).unwrap_or(DEFAULT_WIDTH));
245 guard
246 .height
247 .set(nbt.float_value(TAG_HEIGHT).unwrap_or(DEFAULT_HEIGHT));
248 guard.response.set(
249 nbt.byte_value(TAG_RESPONSE)
250 .map_or(DEFAULT_RESPONSE, |b| b != 0),
251 );
252
253 guard.width.is_dirty() || guard.height.is_dirty()
254 };
255 *self.attack.lock() = nbt.get(TAG_ATTACK).and_then(PlayerAction::from_nbt_tag);
256 *self.interaction.lock() = nbt
257 .get(TAG_INTERACTION)
258 .and_then(PlayerAction::from_nbt_tag);
259 if dimensions_changed {
260 self.refresh_dimensions();
261 }
262 self.base
263 .set_bounding_box(self.make_bounding_box_at(self.position()));
264 }
265
266 fn hurt(&self, _world: &World, _source: &DamageSource, _amount: f32) -> bool {
267 false
268 }
269}
270
271#[derive(Debug, Copy, Clone, PartialEq, Eq)]
274pub struct PlayerAction {
275 player: Uuid,
277
278 timestamp: i64,
280}
281
282impl PlayerAction {
283 #[must_use]
285 pub const fn player(&self) -> Uuid {
286 self.player
287 }
288
289 #[must_use]
291 pub const fn timestamp(&self) -> i64 {
292 self.timestamp
293 }
294}
295
296impl FromNbtTag for PlayerAction {
297 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
298 let compound = tag.compound()?;
299 Some(Self {
300 player: Uuid::from_int_array(&compound.int_array(TAG_PLAYER)?)?,
301 timestamp: compound.get(TAG_TIMESTAMP)?.codec_i64()?,
302 })
303 }
304}
305
306impl ToNbtTag for &PlayerAction {
307 fn to_nbt_tag(self) -> NbtTag {
308 let mut compound = NbtCompound::new();
309 compound.insert(
310 TAG_PLAYER,
311 NbtTag::IntArray(self.player.to_int_array().to_vec()),
312 );
313 compound.insert(TAG_TIMESTAMP, self.timestamp);
314 NbtTag::Compound(compound)
315 }
316}
317
318pub struct InteractionEntityDataView<'a> {
321 guard: MutexGuard<'a, InteractionEntityData>,
322 dimensions_changed: bool,
323}
324
325impl InteractionEntityDataView<'_> {
326 #[must_use]
328 pub fn width(&self) -> f32 {
329 *self.guard.width.get()
330 }
331
332 pub fn set_width(&mut self, width: f32) {
334 self.guard.width.set(width);
335 if self.guard.width.is_dirty() {
336 self.dimensions_changed = true;
337 }
338 }
339
340 #[must_use]
342 pub fn height(&self) -> f32 {
343 *self.guard.height.get()
344 }
345
346 pub fn set_height(&mut self, height: f32) {
348 self.guard.height.set(height);
349 if self.guard.height.is_dirty() {
350 self.dimensions_changed = true;
351 }
352 }
353
354 #[must_use]
359 pub fn response(&self) -> bool {
360 *self.guard.response.get()
361 }
362
363 pub fn set_response(&mut self, response: bool) {
368 self.guard.response.set(response);
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use crate::entity::entities::InteractionEntity;
375 use crate::entity::entities::objects::technical::interaction::{
376 DEFAULT_HEIGHT, DEFAULT_WIDTH, PlayerAction, TAG_HEIGHT, TAG_WIDTH,
377 };
378 use crate::entity::{Entity, SharedEntity};
379 use crate::player::Player;
380 use crate::test_support::tick_test_world;
381 use crate::test_support::{TestPlayerBuilder, fresh_test_world};
382 use glam::DVec3;
383 use simdnbt::borrow::read_compound;
384 use simdnbt::owned::NbtCompound;
385 use std::io::Cursor;
386 use std::sync::Arc;
387 use steel_registry::vanilla_entities;
388 use steel_utils::types::InteractionHand;
389
390 const TEST_POSITION: DVec3 = DVec3::new(-8.0, 128.0, 4.0);
391
392 #[test]
393 fn skip_attack_interaction_when_required() {
394 let world_fixture = fresh_test_world("skip_interaction_when_required");
395 let world = &world_fixture.world;
396 let player = TestPlayerBuilder::new(Arc::clone(world), "InteractPlayer", 0).build();
397
398 let response_false_interaction = InteractionEntity::new(
399 &vanilla_entities::INTERACTION,
400 1,
401 TEST_POSITION,
402 Arc::downgrade(world),
403 );
404 let response_true_interaction = InteractionEntity::new(
405 &vanilla_entities::INTERACTION,
406 2,
407 TEST_POSITION,
408 Arc::downgrade(world),
409 );
410 response_true_interaction.with_entity_data(|data| data.set_response(true));
411
412 let shared_player: SharedEntity = Arc::<Player>::clone(&player);
413 assert!(response_false_interaction.skip_attack_interaction(&shared_player));
414 assert!(!response_true_interaction.skip_attack_interaction(&shared_player));
415 }
416
417 #[test]
418 fn record_player_actions() {
419 let world_fixture = fresh_test_world("interaction_records_player_actions");
420 let world = &world_fixture.world;
421 let player = TestPlayerBuilder::new(Arc::clone(world), "InteractPlayer", 0).build();
422
423 let interaction = InteractionEntity::new(
424 &vanilla_entities::INTERACTION,
425 1,
426 TEST_POSITION,
427 Arc::downgrade(world),
428 );
429
430 assert_eq!(interaction.last_attack(), None);
431 assert_eq!(interaction.last_interaction(), None);
432
433 tick_test_world(world, 0, true);
434 tick_test_world(world, 1, true);
435 tick_test_world(world, 2, true);
436
437 let shared_player: SharedEntity = Arc::<Player>::clone(&player);
438 interaction.skip_attack_interaction(&shared_player);
439 assert_eq!(
440 interaction.last_attack(),
441 Some(PlayerAction {
442 player: player.uuid(),
443 timestamp: 3
444 })
445 );
446 assert_eq!(interaction.last_interaction(), None);
447
448 tick_test_world(world, 3, true);
449 tick_test_world(world, 4, true);
450
451 interaction.interact(player.as_ref(), InteractionHand::MainHand, TEST_POSITION);
452 assert_eq!(
453 interaction.last_attack(),
454 Some(PlayerAction {
455 player: player.uuid(),
456 timestamp: 3
457 })
458 );
459 assert_eq!(
460 interaction.last_interaction(),
461 Some(PlayerAction {
462 player: player.uuid(),
463 timestamp: 5
464 })
465 );
466
467 tick_test_world(world, 5, true);
468
469 interaction.skip_attack_interaction(&shared_player);
470 assert_eq!(
471 interaction.last_attack(),
472 Some(PlayerAction {
473 player: player.uuid(),
474 timestamp: 6
475 })
476 );
477 }
478
479 #[test]
480 fn update_dimensions_on_edit() {
481 let world_fixture = fresh_test_world("interaction_updates_dimensions_on_edit");
482 let world = &world_fixture.world;
483
484 let interaction = InteractionEntity::new(
485 &vanilla_entities::INTERACTION,
486 0,
487 TEST_POSITION,
488 Arc::downgrade(world),
489 );
490
491 let check_dimensions_and_bounding_box = |expected_width: f32, expected_height: f32| {
492 let dimensions = interaction.base.dimensions();
493 assert_eq!(dimensions.width, expected_width);
494 assert_eq!(dimensions.height, expected_height);
495
496 let bounding_box = interaction.base.bounding_box();
497 assert_eq!(bounding_box.width(), f64::from(expected_width));
498 assert_eq!(bounding_box.height(), f64::from(expected_height));
499 };
500
501 check_dimensions_and_bounding_box(DEFAULT_WIDTH, DEFAULT_HEIGHT);
502
503 interaction.with_entity_data(|data| {
504 data.set_width(2.0);
505 });
506 check_dimensions_and_bounding_box(2.0, DEFAULT_HEIGHT);
507
508 interaction.with_entity_data(|data| {
509 data.set_height(4.0);
510 });
511 check_dimensions_and_bounding_box(2.0, 4.0);
512
513 interaction.with_entity_data(|data| {
514 data.set_width(0.5);
515 data.set_height(0.25);
516 });
517 check_dimensions_and_bounding_box(0.5, 0.25);
518
519 let bytes = {
520 let mut compound = NbtCompound::new();
521 compound.insert(TAG_WIDTH, 2.0f32);
522 compound.remove(TAG_HEIGHT);
523 let mut bytes = Vec::new();
524 compound.write(&mut bytes);
525 bytes
526 };
527 let borrowed = read_compound(&mut Cursor::new(&bytes))
528 .unwrap_or_else(|error| panic!("test nbt should reborrow: {error}"));
529 interaction.load_additional((&borrowed).into());
530 check_dimensions_and_bounding_box(2.0, DEFAULT_HEIGHT);
531 }
532}