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