steel_core/entity/entities/objects/display_ui/
leash_fence_knot.rs1use std::sync::{Arc, Weak};
4
5use crate::behavior::InteractionResult;
6use crate::entity::block_attached_entity::{BlockAttachedEntity, BlockAttachedEntityBase};
7use crate::entity::damage::DamageSource;
8use crate::entity::{
9 Entity, EntityBase, EntityBaseLoad, EntityBaseState, EntityMoveError, RemovalReason,
10 SharedEntity, next_entity_id,
11};
12use crate::physics::{MoveResult, MoverType};
13use crate::player::Player;
14use crate::world::{Explosion, World};
15use glam::DVec3;
16use steel_macros::entity_behavior;
17use steel_registry::blocks::block_state_ext::BlockStateExt as _;
18use steel_registry::entity_type::EntityTypeRef;
19use steel_registry::item_stack::ItemStack;
20use steel_registry::sound_events::ITEM_LEAD_TIED;
21use steel_registry::vanilla_block_tags::BlockTag;
22use steel_registry::vanilla_entities;
23use steel_registry::vanilla_game_events::BLOCK_ATTACH;
24use steel_registry::{sound_events, vanilla_items};
25use steel_utils::types::InteractionHand;
26use steel_utils::{BlockPos, Downcast as _, DowncastType, DowncastTypeKey, WorldAabb};
27
28#[entity_behavior(class = "LeashFenceKnotEntity")]
30pub struct LeashFenceKnotEntity {
31 base: EntityBase,
32 entity_type: EntityTypeRef,
33 block_attached_entity_base: BlockAttachedEntityBase,
34}
35
36unsafe impl DowncastType for LeashFenceKnotEntity {
38 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/leash_fence_knot");
39}
40
41impl LeashFenceKnotEntity {
42 #[must_use]
44 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
45 Self::new_attached(
46 entity_type,
47 id,
48 BlockPos::new(
49 position.x.floor() as i32,
50 position.y.floor() as i32,
51 position.z.floor() as i32,
52 ),
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 world: Weak<World>,
64 ) -> Self {
65 Self {
66 base: EntityBase::new_with_state(
67 id,
68 EntityBaseState::new_with_bounding_box(
69 Self::knot_center(block_pos),
70 entity_type.dimensions,
71 Self::knot_bounding_box(entity_type, block_pos),
72 ),
73 world,
74 ),
75 entity_type,
76 block_attached_entity_base: BlockAttachedEntityBase::new(block_pos),
77 }
78 }
79
80 #[must_use]
82 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
83 let position = load.position;
84 let block_pos = BlockPos::new(
85 position.x.floor() as i32,
86 position.y.floor() as i32,
87 position.z.floor() as i32,
88 );
89 Self {
90 base: EntityBase::from_load(load, entity_type.dimensions),
91 entity_type,
92 block_attached_entity_base: BlockAttachedEntityBase::new(block_pos),
93 }
94 }
95
96 #[must_use]
98 pub fn block_pos(&self) -> BlockPos {
99 self.block_attached_entity_base.pos()
100 }
101
102 #[must_use]
104 pub fn get_knot(world: &World, pos: BlockPos) -> Option<SharedEntity> {
105 let search_box = WorldAabb::new(
106 f64::from(pos.x()) - 1.0,
107 f64::from(pos.y()) - 1.0,
108 f64::from(pos.z()) - 1.0,
109 f64::from(pos.x()) + 1.0,
110 f64::from(pos.y()) + 1.0,
111 f64::from(pos.z()) + 1.0,
112 );
113 world
114 .get_entities_in_aabb_matching(&search_box, |entity| {
115 entity
116 .downcast_ref::<Self>()
117 .is_some_and(|knot| knot.block_pos() == pos)
118 })
119 .into_iter()
120 .next()
121 }
122
123 #[must_use]
125 pub fn get_or_create_knot(world: &Arc<World>, pos: BlockPos) -> Option<SharedEntity> {
126 if let Some(knot) = Self::get_knot(world.as_ref(), pos) {
127 return Some(knot);
128 }
129
130 let knot: SharedEntity = Arc::new(Self::new_attached(
131 &vanilla_entities::LEASH_KNOT,
132 next_entity_id(),
133 pos,
134 Arc::downgrade(world),
135 ));
136 if let Err(error) = world.try_add_entity(Arc::clone(&knot)) {
137 log::warn!("Failed to spawn leash knot entity: {error}");
138 return None;
139 }
140
141 Some(knot)
142 }
143
144 fn knot_center(block_pos: BlockPos) -> DVec3 {
145 DVec3::new(
146 f64::from(block_pos.x()) + 0.5,
147 f64::from(block_pos.y()) + 0.375,
148 f64::from(block_pos.z()) + 0.5,
149 )
150 }
151
152 fn knot_bounding_box(entity_type: EntityTypeRef, block_pos: BlockPos) -> WorldAabb {
153 let center = Self::knot_center(block_pos);
154 let half_width = f64::from(entity_type.dimensions.width) / 2.0;
155 let height = f64::from(entity_type.dimensions.height);
156 WorldAabb::new(
157 center.x - half_width,
158 center.y,
159 center.z - half_width,
160 center.x + half_width,
161 center.y + height,
162 center.z + half_width,
163 )
164 }
165}
166
167impl Entity for LeashFenceKnotEntity {
168 fn base(&self) -> &EntityBase {
169 &self.base
170 }
171
172 fn entity_type(&self) -> EntityTypeRef {
173 self.entity_type
174 }
175
176 fn spawn_position(&self) -> DVec3 {
177 let block_pos = self.block_pos();
178 DVec3::new(
179 f64::from(block_pos.x()),
180 f64::from(block_pos.y()),
181 f64::from(block_pos.z()),
182 )
183 }
184
185 fn notify_leashee_removed(&self, _leashable: &dyn Entity) {
186 if self.level().is_some() && self.leashables_leashed_to().is_empty() {
187 self.set_removed(RemovalReason::Discarded);
188 }
189 }
190
191 fn interact(
192 &self,
193 player: &Player,
194 hand: InteractionHand,
195 location: DVec3,
196 ) -> InteractionResult {
197 let Some(world) = self.level() else {
198 return InteractionResult::Pass;
199 };
200
201 let holding_shears = {
202 let inventory = player.inventory.lock();
203 inventory.get_item_in_hand(hand).is(&vanilla_items::SHEARS)
204 };
205 if holding_shears {
206 let result = self.interact_entity(player, hand, location);
207 if result == InteractionResult::Success {
208 return result;
209 }
210 }
211
212 let mut attached_mob = false;
213 let Some(knot) = world.get_entity_by_id(self.id()) else {
214 return InteractionResult::Pass;
215 };
216 for entity in player.leashables_leashed_to() {
217 if let Some(leashable) = entity.as_leashable()
218 && leashable.can_have_a_leash_attached_to(self)
219 {
220 leashable.set_leashed_to(&knot);
221 attached_mob = true;
222 }
223 }
224
225 let mut any_dropped = false;
226 let Some(player_entity) = world.get_entity_by_id(player.id()) else {
227 return InteractionResult::Pass;
228 };
229 if !attached_mob && !player.is_secondary_use_active() {
230 for entity in knot.leashables_leashed_to() {
231 if let Some(leashable) = entity.as_leashable()
232 && leashable.can_have_a_leash_attached_to(player)
233 {
234 leashable.set_leashed_to(&player_entity);
235 any_dropped = true;
236 }
237 }
238 }
239
240 if !attached_mob && !any_dropped {
241 return self.interact_entity(player, hand, location);
242 }
243
244 self.game_event_with_source_entity(&BLOCK_ATTACH, Some(player));
245 self.play_sound(&ITEM_LEAD_TIED, 1.0, 1.0);
246
247 InteractionResult::Success
248 }
249
250 fn tick(self: Arc<Self>) {
251 self.tick_block_attached_entity();
252 }
253
254 fn is_pickable(&self) -> bool {
255 self.is_pickable_block_attached_entity()
256 }
257
258 fn ignore_explosion(&self, explosion: &dyn Explosion) -> bool {
259 explosion
260 .direct_source_entity()
261 .is_some_and(Entity::is_in_water)
262 || !explosion.should_affect_blocklike_entities()
263 }
264
265 fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
266 self.hurt_block_attached_entity(world, source, amount)
267 }
268
269 fn skip_attack_interaction(&self, source: &SharedEntity) -> bool {
270 self.skip_attack_interaction_block_attached_entity(source)
271 }
272
273 fn refresh_dimensions(&self) {
274 self.refresh_dimensions_block_attached_entity();
275 }
276
277 fn push_impulse(&self, impulse: DVec3) {
278 self.push_impulse_block_attached_entity(impulse);
279 }
280
281 fn move_entity(self: Arc<Self>, mover_type: MoverType, delta: DVec3) -> Option<MoveResult> {
282 self.move_entity_block_attached_entity(mover_type, delta)
283 }
284
285 fn try_set_position(&self, pos: DVec3) -> Result<(), EntityMoveError> {
286 self.try_set_position_block_attached_entity(pos)
287 }
288}
289
290impl BlockAttachedEntity for LeashFenceKnotEntity {
291 fn block_attached_entity_base(&self) -> &BlockAttachedEntityBase {
292 &self.block_attached_entity_base
293 }
294
295 fn survives(&self) -> bool {
296 let Some(world) = self.level() else {
297 return false;
298 };
299 world
300 .get_block_state(self.block_pos())
301 .get_block()
302 .has_tag(&BlockTag::FENCES)
303 }
304
305 fn drop_item(&self, _caused_by: Option<&dyn Entity>) {
306 self.play_sound(&sound_events::ITEM_LEAD_UNTIED, 1.0, 1.0);
307
308 for entity in self.leashables_leashed_to() {
313 entity.spawn_at_location(ItemStack::new(&vanilla_items::LEAD), 0.0);
314 }
315 }
316
317 fn recalculate_bounding_box(&self) -> Result<(), EntityMoveError> {
318 let pos = self.block_attached_entity_base.pos();
319 self.try_set_position_base(Self::knot_center(pos))?;
320 self.base()
321 .set_bounding_box(Self::knot_bounding_box(self.entity_type, pos));
322 Ok(())
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use simdnbt::owned::NbtCompound;
330
331 #[test]
332 fn leash_knot_uses_vanilla_position_and_bounding_box() {
333 let knot = LeashFenceKnotEntity::new_attached(
334 &vanilla_entities::LEASH_KNOT,
335 1,
336 BlockPos::new(4, 65, -9),
337 Weak::new(),
338 );
339
340 assert_eq!(knot.position(), DVec3::new(4.5, 65.375, -8.5));
341 assert_eq!(
342 knot.bounding_box(),
343 LeashFenceKnotEntity::knot_bounding_box(
344 &vanilla_entities::LEASH_KNOT,
345 BlockPos::new(4, 65, -9)
346 )
347 );
348 }
349
350 #[test]
351 fn leash_knot_spawn_packet_uses_attached_block_pos() {
352 let knot = LeashFenceKnotEntity::new_attached(
353 &vanilla_entities::LEASH_KNOT,
354 1,
355 BlockPos::new(4, 65, -9),
356 Weak::new(),
357 );
358
359 assert_eq!(knot.spawn_position(), DVec3::new(4.0, 65.0, -9.0));
360 }
361
362 #[test]
363 fn leash_knot_saves_no_type_specific_block_pos() {
364 let knot = LeashFenceKnotEntity::new_attached(
365 &vanilla_entities::LEASH_KNOT,
366 1,
367 BlockPos::new(4, 65, -9),
368 Weak::new(),
369 );
370
371 let mut nbt = NbtCompound::new();
372 knot.save_additional(&mut nbt);
373
374 assert!(nbt.is_empty());
375 }
376
377 #[test]
378 fn leash_knot_survival_check_matches_vanilla_interval() {
379 let knot = LeashFenceKnotEntity::new_attached(
380 &vanilla_entities::LEASH_KNOT,
381 1,
382 BlockPos::new(4, 65, -9),
383 Weak::new(),
384 );
385
386 for _ in 0..100 {
387 assert!(!knot.block_attached_entity_base().should_check_survival());
388 }
389 assert!(knot.block_attached_entity_base().should_check_survival());
390 assert!(!knot.block_attached_entity_base().should_check_survival());
391 }
392}