steel_core/entity/entities/pig/
mod.rs1use 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#[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
67unsafe impl DowncastType for PigEntity {
69 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/pig");
70}
71
72impl PigEntity {
73 #[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 #[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 pub fn set_variant(&self, variant: PigVariantRef) {
145 self.entity_data
146 .lock()
147 .variant
148 .set(RegistryReference::new(variant));
149 }
150
151 #[must_use]
153 pub fn variant(&self) -> PigVariantRef {
154 self.entity_data.lock().variant.get().value()
155 }
156
157 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 #[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 #[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;