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