1use std::str::FromStr;
6use std::sync::{Arc, Weak};
7
8use glam::DVec3;
9use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
10use simdnbt::owned::NbtCompound;
11use steel_macros::entity_behavior;
12use steel_registry::data_components::vanilla_components::{PIG_SOUND_VARIANT, PIG_VARIANT};
13use steel_registry::entity_type::{
14 EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef,
15};
16use steel_registry::item_stack::ItemStack;
17use steel_registry::pig_sound_variant::{PigAge, PigSoundVariantRef};
18use steel_registry::pig_variant::PigVariantRef;
19use steel_registry::sound_event::SoundEventRef;
20use steel_registry::vanilla_entity_data::PigEntityData;
21use steel_registry::vanilla_item_tags::ItemTag;
22use steel_registry::{
23 REGISTRY, RegistryExt, RegistryReference, TaggedRegistryExt, sound_events, vanilla_attributes,
24 vanilla_items,
25};
26use steel_utils::locks::SyncMutex;
27use steel_utils::random::legacy_random::LegacyRandom;
28use steel_utils::types::InteractionHand;
29use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, Identifier};
30
31use crate::behavior::InteractionResult;
32use crate::entity::ai::goal::{
33 BreedGoal, FloatGoal, FollowParentGoal, LookAtPlayerGoal, PanicGoal, RandomLookAroundGoal,
34 TemptGoal, WaterAvoidingRandomStrollGoal,
35};
36use crate::entity::damage::DamageSource;
37use crate::entity::{
38 AgeableMob, AgeableMobBase, Animal, AnimalBase, DefaultEntityPush, Entity, EntityBase,
39 EntityBaseLoad, EntityPose, EntitySpawnReason, EntitySyncedData, ItemBasedSteering,
40 ItemSteerable, LivingEntity, LivingEntityBase, LivingEntitySyncedData, Mob, MobBase,
41 MoveResult, PathfinderMob, SharedEntity, SpawnGroupData, SpawnRule, sync_dirty_mob_effects,
42};
43use crate::inventory::equipment::EquipmentSlot;
44use crate::player::Player;
45use crate::world::World;
46
47const PIG_BABY_PASSENGER_ATTACHMENTS: [EntityAttachmentPoint; 1] =
48 [EntityAttachmentPoint::new(0.0, 0.5, 0.0)];
49const PIG_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
50 0.45,
51 0.45,
52 0.40625,
53 EntityAttachments::new(&PIG_BABY_PASSENGER_ATTACHMENTS, &[], &[], &[]),
54);
55
56#[entity_behavior(class = "Pig", spawn_rule)]
58pub struct PigEntity {
59 base: EntityBase,
60 entity_type: EntityTypeRef,
61 living_base: LivingEntityBase,
62 mob_base: MobBase,
63 ageable_base: AgeableMobBase,
64 animal_base: AnimalBase,
65 steering: SyncMutex<ItemBasedSteering>,
66 entity_data: SyncMutex<PigEntityData>,
67}
68
69unsafe impl DowncastType for PigEntity {
71 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/pig");
72}
73
74impl PigEntity {
75 pub(crate) const SPAWN_RULE: SpawnRule = |entity_type, level, spawn_reason, pos, random| {
77 <Self as Animal>::check_animal_spawn_rules(entity_type, level, spawn_reason, pos, random)
78 };
79
80 #[must_use]
82 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
83 Self::new_with_base(
84 EntityBase::new(id, position, entity_type.dimensions, world),
85 entity_type,
86 )
87 }
88
89 #[must_use]
91 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
92 Self::new_with_base(
93 EntityBase::from_load(load, entity_type.dimensions),
94 entity_type,
95 )
96 }
97
98 fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
99 let living_base = LivingEntityBase::new(entity_type);
100 let mob_base = MobBase::new();
101 let ageable_base = AgeableMobBase::new();
102 let animal_base = AnimalBase::new();
103 AnimalBase::initialize_pathfinding_malus(&mob_base);
104 let steering = SyncMutex::new(ItemBasedSteering::new());
105 let mut entity_data = PigEntityData::new();
106 living_base.initialize_synced_data(&mut entity_data);
107 {
108 let mut goal_selector = mob_base.goal_selector().lock();
109 goal_selector.add_goal(0, FloatGoal::new(&mob_base));
110 goal_selector.add_goal(1, PanicGoal::new(1.25));
111 goal_selector.add_goal(3, BreedGoal::new(1.0));
112 goal_selector.add_goal(
113 4,
114 TemptGoal::new(
115 1.2,
116 |item_stack| item_stack.is(&vanilla_items::CARROT_ON_A_STICK),
117 false,
118 ),
119 );
120 goal_selector.add_goal(
121 4,
122 TemptGoal::new(
123 1.2,
124 |item_stack| {
125 REGISTRY
126 .items
127 .is_in_tag(item_stack.item(), &ItemTag::PIG_FOOD)
128 },
129 false,
130 ),
131 );
132 goal_selector.add_goal(5, FollowParentGoal::new(1.1));
133 goal_selector.add_goal(6, WaterAvoidingRandomStrollGoal::new(1.0));
134 goal_selector.add_goal(7, LookAtPlayerGoal::new(6.0));
135 goal_selector.add_goal(8, RandomLookAroundGoal::new());
136 }
137
138 Self {
139 base,
140 entity_type,
141 living_base,
142 mob_base,
143 ageable_base,
144 animal_base,
145 steering,
146 entity_data: SyncMutex::new(entity_data),
147 }
148 }
149
150 pub fn set_variant(&self, variant: PigVariantRef) {
152 self.entity_data
153 .lock()
154 .variant
155 .set(RegistryReference::new(variant));
156 }
157
158 #[must_use]
160 pub fn variant(&self) -> PigVariantRef {
161 self.entity_data.lock().variant.get().value()
162 }
163
164 pub fn set_sound_variant(&self, sound_variant: PigSoundVariantRef) {
166 self.entity_data
167 .lock()
168 .sound_variant
169 .set(RegistryReference::new(sound_variant));
170 }
171
172 #[must_use]
174 pub fn sound_variant(&self) -> PigSoundVariantRef {
175 self.entity_data.lock().sound_variant.get().value()
176 }
177
178 fn set_variant_by_key(&self, key: &Identifier) -> bool {
179 let Some(variant) = REGISTRY.pig_variants.by_key(key) else {
180 return false;
181 };
182 self.set_variant(variant);
183 true
184 }
185
186 fn set_sound_variant_by_key(&self, key: &Identifier) {
187 if let Some(sound_variant) = REGISTRY.pig_sound_variants.by_key(key) {
188 self.set_sound_variant(sound_variant);
189 }
190 }
191
192 fn current_sound_set(&self) -> &'static PigAge {
193 let sound_variant = self.sound_variant();
194 if AgeableMob::is_baby(self) {
195 &sound_variant.baby_sounds
196 } else {
197 &sound_variant.adult_sounds
198 }
199 }
200
201 fn set_ridden_rotation(&self, controller_yaw: f32, controller_pitch: f32) {
202 self.set_rotation((controller_yaw, controller_pitch * 0.5));
203 self.base.set_old_yaw_to_current();
204 let yaw = self.rotation().0;
205 self.set_y_body_rot(yaw);
206 self.set_y_head_rot(yaw);
207 }
208
209 fn update_dirty_mob_effect_entity_data(&self) {
210 if let Some(display) = sync_dirty_mob_effects(&self.living_base, &self.entity_data) {
211 self.entity_data
212 .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
213 }
214 }
215
216 #[must_use]
218 pub fn is_food(item_stack: &ItemStack) -> bool {
219 REGISTRY
220 .items
221 .is_in_tag(item_stack.item(), &ItemTag::PIG_FOOD)
222 }
223}
224
225impl DefaultEntityPush for PigEntity {}
226
227impl Entity for PigEntity {
228 fn is_hard_collision_relevant(&self) -> bool {
229 false
230 }
231
232 fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
233 Some(self)
234 }
235
236 fn base(&self) -> &EntityBase {
237 &self.base
238 }
239
240 fn entity_type(&self) -> EntityTypeRef {
241 self.entity_type
242 }
243
244 fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
245 if let Some(variant) = item_stack.get(PIG_VARIANT) {
246 self.set_variant(variant.value());
247 }
248 if let Some(sound_variant) = item_stack.get(PIG_SOUND_VARIANT) {
249 self.set_sound_variant(sound_variant.value());
250 }
251 }
252
253 fn base_tick(&self) {
254 Mob::base_tick_mob(self);
255 }
256
257 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
258 let scale = LivingEntity::get_scale(self);
259 if AgeableMob::is_baby(self) {
260 PIG_BABY_DIMENSIONS.scale(scale)
261 } else if self.entity_type.fixed {
262 self.entity_type.dimensions
263 } else {
264 self.entity_type.dimensions.scale(scale)
265 }
266 }
267
268 fn controlling_passenger(&self) -> Option<SharedEntity> {
269 if self.is_saddled()
270 && let Some(passenger) = self.first_passenger()
271 && passenger.as_player().is_some_and(|player| {
272 let mut is_holding_carrot_on_a_stick =
273 |item_stack: &ItemStack| item_stack.is(&vanilla_items::CARROT_ON_A_STICK);
274 player.is_holding(&mut is_holding_carrot_on_a_stick)
275 })
276 {
277 return Some(passenger);
278 }
279
280 self.controlling_passenger_mob()
281 }
282
283 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
284 Some(&self.entity_data)
285 }
286
287 fn update_data_before_sync(&self) {
288 self.update_dirty_mob_effect_entity_data();
289 }
290
291 fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
292 self.play_sound(self.current_sound_set().step_sound, 0.15, 1.0);
293 }
294
295 fn save_additional(&self, nbt: &mut NbtCompound) {
296 self.save_mob(nbt);
297 self.save_ageable_mob(nbt);
298 self.save_animal(nbt);
299 nbt.insert("variant", self.variant().key.to_string());
300 nbt.insert("sound_variant", self.sound_variant().key.to_string());
301 }
302
303 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
304 self.load_mob(nbt);
305 self.load_ageable_mob(nbt);
306 self.load_animal(nbt);
307
308 if let Some(variant) = nbt.string("variant")
309 && let Ok(key) = Identifier::from_str(variant.to_str().as_ref())
310 {
311 self.set_variant_by_key(&key);
312 }
313 if let Some(sound_variant) = nbt.string("sound_variant")
314 && let Ok(key) = Identifier::from_str(sound_variant.to_str().as_ref())
315 {
316 self.set_sound_variant_by_key(&key);
317 }
318 }
319}
320
321impl LivingEntity for PigEntity {
322 fn living_base(&self) -> &LivingEntityBase {
323 &self.living_base
324 }
325
326 fn living_synced_data(&self) -> Option<&dyn LivingEntitySyncedData> {
327 Some(&self.entity_data)
328 }
329
330 fn get_health(&self) -> f32 {
331 *self.entity_data.lock().living_entity().health.get()
332 }
333
334 fn set_health(&self, health: f32) {
335 let max_health = self.get_max_health();
336 let clamped = health.clamp(0.0, max_health);
337 self.entity_data
338 .lock()
339 .living_entity_mut()
340 .health
341 .set(clamped);
342 }
343
344 fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
345 Some(self.current_sound_set().hurt_sound)
346 }
347
348 fn death_sound(&self) -> Option<SoundEventRef> {
349 Some(self.current_sound_set().death_sound)
350 }
351
352 fn can_use_slot(&self, slot: EquipmentSlot) -> bool {
353 slot != EquipmentSlot::Saddle || (Entity::is_alive(self) && !AgeableMob::is_baby(self))
354 }
355
356 fn can_dispenser_equip_into_slot(&self, slot: EquipmentSlot) -> bool {
357 slot == EquipmentSlot::Saddle || Mob::can_pick_up_loot(self)
358 }
359
360 fn equip_sound(&self, slot: EquipmentSlot, _stack: &ItemStack) -> Option<SoundEventRef> {
361 (slot == EquipmentSlot::Saddle).then_some(&sound_events::ENTITY_PIG_SADDLE)
362 }
363
364 fn server_ai_step(&self, entity: &SharedEntity) {
365 Mob::mob_server_ai_step(self, entity);
366 }
367
368 fn tick_ridden(&self, controller: &Player, _ridden_input: DVec3) {
369 let (yaw, pitch) = controller.rotation();
370 self.set_ridden_rotation(yaw, pitch);
371 ItemSteerable::tick_boost(self);
372 }
373
374 fn ridden_input(&self, _controller: &Player, _self_input: DVec3) -> DVec3 {
375 DVec3::new(0.0, 0.0, 1.0)
376 }
377
378 fn ridden_speed(&self, _controller: &Player) -> f32 {
379 let movement_speed = self
380 .attributes()
381 .lock()
382 .required_value(vanilla_attributes::MOVEMENT_SPEED) as f32;
383 movement_speed * 0.225 * ItemSteerable::boost_factor(self)
384 }
385
386 fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
387 let result = Mob::mob_ai_step(self, entity);
388 AgeableMob::tick_ageable_mob(self);
389 Animal::tick_animal_love(self);
390 result
391 }
392}
393
394impl AgeableMob for PigEntity {
395 fn ageable_base(&self) -> &AgeableMobBase {
396 &self.ageable_base
397 }
398
399 fn is_age_locked(&self) -> bool {
400 *self.entity_data.lock().ageable_mob().age_locked.get()
401 }
402
403 fn set_age_locked(&self, age_locked: bool) {
404 self.entity_data
405 .lock()
406 .ageable_mob_mut()
407 .age_locked
408 .set(age_locked);
409 }
410
411 fn set_synced_baby(&self, baby: bool) {
412 self.entity_data.lock().ageable_mob_mut().baby.set(baby);
413 }
414
415 fn breed_variant_key(&self) -> Option<&Identifier> {
416 Some(&self.variant().key)
417 }
418
419 fn set_breed_variant_key(&self, key: &Identifier) -> bool {
420 self.set_variant_by_key(key)
421 }
422
423 fn initialize_breed_offspring(&self, partner: &dyn AgeableMob, offspring: &dyn AgeableMob) {
424 let use_self_variant = rand::random::<bool>();
425 let variant_key = if use_self_variant {
426 self.breed_variant_key()
427 } else {
428 partner.breed_variant_key()
429 };
430 let Some(variant_key) = variant_key else {
431 return;
432 };
433
434 if !offspring.set_breed_variant_key(variant_key) {
435 log::error!("pig offspring could not inherit breeding variant {variant_key}");
436 }
437 }
438}
439
440impl Animal for PigEntity {
441 fn animal_base(&self) -> &AnimalBase {
442 &self.animal_base
443 }
444
445 fn is_food(&self, item_stack: &ItemStack) -> bool {
446 PigEntity::is_food(item_stack)
447 }
448
449 fn play_eating_sound(&self) {
450 self.play_sound(self.current_sound_set().eat_sound, 1.0, 1.0);
451 }
452}
453
454impl ItemSteerable for PigEntity {
455 fn item_based_steering(&self) -> &SyncMutex<ItemBasedSteering> {
456 &self.steering
457 }
458
459 fn boost_time_total(&self) -> i32 {
460 *self.entity_data.lock().boost_time.get()
461 }
462
463 fn set_boost_time_total(&self, boost_time_total: i32) {
464 self.entity_data.lock().boost_time.set(boost_time_total);
465 }
466}
467
468impl Mob for PigEntity {
469 fn mob_base(&self) -> &MobBase {
470 &self.mob_base
471 }
472
473 fn tick_goal_selectors(&self, entity: &SharedEntity) {
474 PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
475 }
476
477 fn tick_path_navigation(&self) {
478 PathfinderMob::tick_pathfinder_path_navigation(self);
479 }
480
481 fn custom_server_ai_step(&self) {
482 Animal::custom_server_ai_step_animal(self);
483 }
484
485 fn ambient_sound(&self) -> Option<SoundEventRef> {
486 Some(self.current_sound_set().ambient_sound)
487 }
488
489 fn finalize_spawn(
490 &self,
491 world: &Arc<World>,
492 spawn_reason: EntitySpawnReason,
493 group_data: Option<SpawnGroupData>,
494 ) -> Option<SpawnGroupData> {
495 let biome = world.biome_at(self.block_position());
496 let (variant, sound_variant) = {
497 let mut random = LegacyRandom::from_seed(rand::random());
498 let variant = biome.and_then(|biome| {
499 REGISTRY
500 .pig_variants
501 .select_spawn_variant(biome, &mut random)
502 });
503 let sound_variant = REGISTRY.pig_sound_variants.pick_random(&mut random);
504 (variant, sound_variant)
505 };
506
507 if let Some(variant) = variant {
508 self.set_variant(variant);
509 }
510
511 if let Some(sound_variant) = sound_variant {
512 self.set_sound_variant(sound_variant);
513 }
514
515 self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
516 }
517
518 fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
519 let item_stack = {
520 let inventory = player.inventory.lock();
521 let item_stack = inventory.get_item_in_hand(hand);
522 item_stack.copy_with_count(item_stack.count())
523 };
524 let has_food = PigEntity::is_food(&item_stack);
525
526 if !has_food && self.is_saddled() && !self.is_vehicle() && !player.is_secondary_use_active()
527 {
528 if let Some(world) = self.level()
529 && let Some(vehicle) = world.get_entity_by_id(self.id())
530 {
531 player.start_riding(&vehicle);
532 }
533 return InteractionResult::Success;
534 }
535
536 let interaction_result = Animal::mob_interact_animal(self, player, hand);
537 if interaction_result.consumes_action() {
538 return interaction_result;
539 }
540
541 if LivingEntity::is_equippable_in_slot(self, &item_stack, EquipmentSlot::Saddle) {
542 return LivingEntity::interact_living_entity_with_equippable(self, player, hand);
543 }
544
545 InteractionResult::Pass
546 }
547
548 fn mob_flags(&self) -> i8 {
549 *self.entity_data.lock().mob().mob_flags.get()
550 }
551
552 fn set_mob_flags(&self, flags: i8) {
553 self.entity_data.lock().mob_mut().mob_flags.set(flags);
554 }
555}
556
557impl PathfinderMob for PigEntity {}
558
559#[cfg(test)]
560mod tests;