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, 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/// Vanilla pig entity.
57#[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
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    /// Vanilla `SpawnPlacements` predicate: `Animal.checkAnimalSpawnRules`.
76    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    /// Creates a new pig entity.
81    #[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    /// Creates a pig entity from saved base data.
90    #[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    /// Sets the current pig variant by registry entry.
151    pub fn set_variant(&self, variant: PigVariantRef) {
152        self.entity_data
153            .lock()
154            .variant
155            .set(RegistryReference::new(variant));
156    }
157
158    /// Returns the current pig variant.
159    #[must_use]
160    pub fn variant(&self) -> PigVariantRef {
161        self.entity_data.lock().variant.get().value()
162    }
163
164    /// Sets the current pig sound variant by registry entry.
165    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    /// Returns the current pig sound variant.
173    #[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    /// Returns whether the stack is vanilla pig food.
217    #[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;