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