Skip to main content

steel_core/entity/entities/pig/
mod.rs

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