steel_core/entity/entities/
leash_fence_knot.rs1use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use steel_macros::entity_behavior;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::entity_type::EntityTypeRef;
9use steel_registry::sound_events;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::vanilla_entities;
12use steel_utils::locks::SyncMutex;
13use steel_utils::{BlockPos, Downcast as _, DowncastType, DowncastTypeKey, WorldAabb};
14
15use crate::entity::{
16 Entity, EntityBase, EntityBaseLoad, EntityBaseState, RemovalReason, SharedEntity,
17 next_entity_id,
18};
19use crate::world::World;
20
21#[entity_behavior(class = "LeashFenceKnotEntity")]
23pub struct LeashFenceKnotEntity {
24 base: EntityBase,
25 entity_type: EntityTypeRef,
26 block_pos: SyncMutex<BlockPos>,
27 check_interval: SyncMutex<i32>,
28}
29
30unsafe impl DowncastType for LeashFenceKnotEntity {
32 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/leash_fence_knot");
33}
34
35impl LeashFenceKnotEntity {
36 #[must_use]
38 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
39 Self::new_attached(
40 entity_type,
41 id,
42 BlockPos::new(
43 position.x.floor() as i32,
44 position.y.floor() as i32,
45 position.z.floor() as i32,
46 ),
47 world,
48 )
49 }
50
51 #[must_use]
53 pub fn new_attached(
54 entity_type: EntityTypeRef,
55 id: i32,
56 block_pos: BlockPos,
57 world: Weak<World>,
58 ) -> Self {
59 Self {
60 base: EntityBase::new_with_state(
61 id,
62 EntityBaseState::new_with_bounding_box(
63 Self::knot_center(block_pos),
64 entity_type.dimensions,
65 Self::knot_bounding_box(entity_type, block_pos),
66 ),
67 world,
68 ),
69 entity_type,
70 block_pos: SyncMutex::new(block_pos),
71 check_interval: SyncMutex::new(0),
72 }
73 }
74
75 #[must_use]
77 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
78 let position = load.position;
79 let block_pos = BlockPos::new(
80 position.x.floor() as i32,
81 position.y.floor() as i32,
82 position.z.floor() as i32,
83 );
84 Self {
85 base: EntityBase::from_load(load, entity_type.dimensions),
86 entity_type,
87 block_pos: SyncMutex::new(block_pos),
88 check_interval: SyncMutex::new(0),
89 }
90 }
91
92 #[must_use]
94 pub fn block_pos(&self) -> BlockPos {
95 *self.block_pos.lock()
96 }
97
98 #[must_use]
100 pub fn survives(&self) -> bool {
101 let Some(world) = self.level() else {
102 return false;
103 };
104 world
105 .get_block_state(self.block_pos())
106 .get_block()
107 .has_tag(&BlockTag::FENCES)
108 }
109
110 #[must_use]
112 pub fn get_knot(world: &World, pos: BlockPos) -> Option<SharedEntity> {
113 let search_box = WorldAabb::new(
114 f64::from(pos.x()) - 1.0,
115 f64::from(pos.y()) - 1.0,
116 f64::from(pos.z()) - 1.0,
117 f64::from(pos.x()) + 1.0,
118 f64::from(pos.y()) + 1.0,
119 f64::from(pos.z()) + 1.0,
120 );
121 world
122 .get_entities_in_aabb_matching(&search_box, |entity| {
123 entity
124 .downcast_ref::<Self>()
125 .is_some_and(|knot| knot.block_pos() == pos)
126 })
127 .into_iter()
128 .next()
129 }
130
131 #[must_use]
133 pub fn get_or_create_knot(world: &Arc<World>, pos: BlockPos) -> Option<SharedEntity> {
134 if let Some(knot) = Self::get_knot(world.as_ref(), pos) {
135 return Some(knot);
136 }
137
138 let knot: SharedEntity = Arc::new(Self::new_attached(
139 &vanilla_entities::LEASH_KNOT,
140 next_entity_id(),
141 pos,
142 Arc::downgrade(world),
143 ));
144 if let Err(error) = world.try_add_entity(Arc::clone(&knot)) {
145 log::warn!("Failed to spawn leash knot entity: {error}");
146 return None;
147 }
148
149 Some(knot)
150 }
151
152 fn should_check_survival(&self) -> bool {
153 let mut check_interval = self.check_interval.lock();
154 if *check_interval == 100 {
155 *check_interval = 0;
156 true
157 } else {
158 *check_interval += 1;
159 false
160 }
161 }
162
163 fn play_drop_sound(&self) {
164 self.play_sound(&sound_events::ITEM_LEAD_UNTIED, 1.0, 1.0);
165 }
166
167 fn knot_center(block_pos: BlockPos) -> DVec3 {
168 DVec3::new(
169 f64::from(block_pos.x()) + 0.5,
170 f64::from(block_pos.y()) + 0.375,
171 f64::from(block_pos.z()) + 0.5,
172 )
173 }
174
175 fn knot_bounding_box(entity_type: EntityTypeRef, block_pos: BlockPos) -> WorldAabb {
176 let center = Self::knot_center(block_pos);
177 let half_width = f64::from(entity_type.dimensions.width) / 2.0;
178 let height = f64::from(entity_type.dimensions.height);
179 WorldAabb::new(
180 center.x - half_width,
181 center.y,
182 center.z - half_width,
183 center.x + half_width,
184 center.y + height,
185 center.z + half_width,
186 )
187 }
188}
189
190impl Entity for LeashFenceKnotEntity {
191 fn base(&self) -> &EntityBase {
192 &self.base
193 }
194
195 fn entity_type(&self) -> EntityTypeRef {
196 self.entity_type
197 }
198
199 fn spawn_position(&self) -> DVec3 {
200 let block_pos = self.block_pos();
201 DVec3::new(
202 f64::from(block_pos.x()),
203 f64::from(block_pos.y()),
204 f64::from(block_pos.z()),
205 )
206 }
207
208 fn notify_leashee_removed(&self, _leashable: &dyn Entity) {
209 if self.level().is_some() && self.leashables_leashed_to().is_empty() {
210 self.set_removed(RemovalReason::Discarded);
211 }
212 }
213
214 fn tick(&self) {
215 if self.level().is_none() {
216 return;
217 }
218 self.check_below_world();
219 if self.should_check_survival() && !self.is_removed() && !self.survives() {
220 self.set_removed(RemovalReason::Discarded);
221 self.play_drop_sound();
222 }
223 }
224
225 fn is_pickable(&self) -> bool {
226 true
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use simdnbt::owned::NbtCompound;
234
235 #[test]
236 fn leash_knot_uses_vanilla_position_and_bounding_box() {
237 let knot = LeashFenceKnotEntity::new_attached(
238 &vanilla_entities::LEASH_KNOT,
239 1,
240 BlockPos::new(4, 65, -9),
241 Weak::new(),
242 );
243
244 assert_eq!(knot.position(), DVec3::new(4.5, 65.375, -8.5));
245 assert_eq!(
246 knot.bounding_box(),
247 LeashFenceKnotEntity::knot_bounding_box(
248 &vanilla_entities::LEASH_KNOT,
249 BlockPos::new(4, 65, -9)
250 )
251 );
252 }
253
254 #[test]
255 fn leash_knot_spawn_packet_uses_attached_block_pos() {
256 let knot = LeashFenceKnotEntity::new_attached(
257 &vanilla_entities::LEASH_KNOT,
258 1,
259 BlockPos::new(4, 65, -9),
260 Weak::new(),
261 );
262
263 assert_eq!(knot.spawn_position(), DVec3::new(4.0, 65.0, -9.0));
264 }
265
266 #[test]
267 fn leash_knot_saves_no_type_specific_block_pos() {
268 let knot = LeashFenceKnotEntity::new_attached(
269 &vanilla_entities::LEASH_KNOT,
270 1,
271 BlockPos::new(4, 65, -9),
272 Weak::new(),
273 );
274
275 let mut nbt = NbtCompound::new();
276 knot.save_additional(&mut nbt);
277
278 assert!(nbt.is_empty());
279 }
280
281 #[test]
282 fn leash_knot_survival_check_matches_vanilla_interval() {
283 let knot = LeashFenceKnotEntity::new_attached(
284 &vanilla_entities::LEASH_KNOT,
285 1,
286 BlockPos::new(4, 65, -9),
287 Weak::new(),
288 );
289
290 for _ in 0..100 {
291 assert!(!knot.should_check_survival());
292 }
293 assert!(knot.should_check_survival());
294 assert!(!knot.should_check_survival());
295 }
296}