Skip to main content

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