Skip to main content

steel_core/entity/entities/mobs/passive/cow/
mod.rs

1//! Vanilla Cow entity with variant + sound-variant parity.
2
3use crate::player::player_inventory::Prediction;
4use std::str::FromStr;
5use std::sync::{Arc, Weak};
6
7use glam::DVec3;
8use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
9use simdnbt::owned::NbtCompound;
10use steel_macros::entity_behavior;
11use steel_protocol::packets::game::SoundSource;
12use steel_registry::cow_sound_variant::CowSoundVariantRef;
13use steel_registry::cow_variant::CowVariantRef;
14use steel_registry::data_components::vanilla_components::{COW_SOUND_VARIANT, COW_VARIANT};
15use steel_registry::entity_type::{
16    EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef,
17};
18use steel_registry::item_stack::ItemStack;
19use steel_registry::sound_event::SoundEventRef;
20use steel_registry::vanilla_entity_data::CowEntityData;
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::{ITEM_BEHAVIORS, 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, LivingEntity,
40    LivingEntityBase, Mob, MobBase, PathfinderMob, SharedEntity, SpawnGroupData, SpawnRule,
41    sync_dirty_mob_effects,
42};
43use crate::physics::MoveResult;
44use crate::player::Player;
45use crate::world::World;
46
47const COW_BABY_PASSENGER_ATTACHMENTS: [EntityAttachmentPoint; 1] =
48    [EntityAttachmentPoint::new(0.0, 0.75, 0.0)];
49const COW_BABY_WIDTH: f32 = 0.45;
50const COW_BABY_HEIGHT: f32 = 0.7;
51const COW_BABY_EYE_HEIGHT: f32 = 0.69;
52
53const COW_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
54    COW_BABY_WIDTH,
55    COW_BABY_HEIGHT,
56    COW_BABY_EYE_HEIGHT,
57    EntityAttachments::new(&COW_BABY_PASSENGER_ATTACHMENTS, &[], &[], &[]),
58);
59const DEFAULT_STEP_HEIGHT: f32 = 0.6;
60
61#[entity_behavior(class = "Cow", spawn_rule)]
62/// Vanilla cow entity with synced variant and sound-variant state.
63pub struct CowEntity {
64    base: EntityBase,
65    entity_type: EntityTypeRef,
66    living_base: LivingEntityBase,
67    mob_base: MobBase,
68    ageable_base: AgeableMobBase,
69    animal_base: AnimalBase,
70    entity_data: SyncMutex<CowEntityData>,
71}
72
73// SAFETY: This key is owned by Steel and uniquely identifies `CowEntity`.
74unsafe impl DowncastType for CowEntity {
75    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/cow");
76}
77
78impl CowEntity {
79    /// Vanilla `SpawnPlacements` predicate: `Animal.checkAnimalSpawnRules`.
80    pub(crate) const SPAWN_RULE: SpawnRule = |entity_type, level, spawn_reason, pos, random| {
81        <Self as Animal>::check_animal_spawn_rules(entity_type, level, spawn_reason, pos, random)
82    };
83
84    /// Creates a new cow at runtime.
85    #[must_use]
86    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
87        Self::new_with_base(
88            EntityBase::new(id, position, entity_type.dimensions, world),
89            entity_type,
90        )
91    }
92
93    /// Reconstructs a cow from persisted base entity state.
94    #[must_use]
95    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
96        Self::new_with_base(
97            EntityBase::from_load(load, entity_type.dimensions),
98            entity_type,
99        )
100    }
101
102    fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
103        let living_base = LivingEntityBase::new(entity_type);
104        let mob_base = MobBase::new();
105        let ageable_base = AgeableMobBase::new();
106        let animal_base = AnimalBase::new();
107        AnimalBase::initialize_pathfinding_malus(&mob_base);
108        let mut entity_data = CowEntityData::new();
109        living_base.initialize_synced_data(&mut entity_data);
110
111        {
112            // Keep vanilla AbstractCow goal priorities and speeds in the same order.
113            let mut goal_selector = mob_base.goal_selector().lock();
114            goal_selector.add_goal(0, FloatGoal::new(&mob_base));
115            goal_selector.add_goal(1, PanicGoal::new(2.0));
116            goal_selector.add_goal(2, BreedGoal::new(1.0));
117            goal_selector.add_goal(
118                3,
119                TemptGoal::new(
120                    1.25,
121                    |item_stack| {
122                        REGISTRY
123                            .items
124                            .is_in_tag(item_stack.item(), &ItemTag::COW_FOOD)
125                    },
126                    false,
127                ),
128            );
129            goal_selector.add_goal(4, FollowParentGoal::new(1.25));
130            goal_selector.add_goal(5, WaterAvoidingRandomStrollGoal::new(1.0));
131            goal_selector.add_goal(6, LookAtPlayerGoal::new(6.0));
132            goal_selector.add_goal(7, RandomLookAroundGoal::new());
133        }
134
135        Self {
136            base,
137            entity_type,
138            living_base,
139            mob_base,
140            ageable_base,
141            animal_base,
142            entity_data: SyncMutex::new(entity_data),
143        }
144    }
145
146    /// Sets the active cow variant by registry entry.
147    pub fn set_variant(&self, variant: CowVariantRef) {
148        self.entity_data
149            .lock()
150            .variant
151            .set(RegistryReference::new(variant));
152    }
153
154    /// Returns the active cow variant, falling back to temperate when invalid.
155    #[must_use]
156    pub fn variant(&self) -> CowVariantRef {
157        self.entity_data.lock().variant.get().value()
158    }
159
160    /// Sets the active cow sound variant by registry entry.
161    pub fn set_sound_variant(&self, sound_variant: CowSoundVariantRef) {
162        self.entity_data
163            .lock()
164            .sound_variant
165            .set(RegistryReference::new(sound_variant));
166    }
167
168    /// Returns the active cow sound variant, falling back to classic when invalid.
169    #[must_use]
170    pub fn sound_variant(&self) -> CowSoundVariantRef {
171        self.entity_data.lock().sound_variant.get().value()
172    }
173
174    fn set_variant_by_key(&self, key: &Identifier) -> bool {
175        let Some(variant) = REGISTRY.cow_variants.by_key(key) else {
176            return false;
177        };
178        self.set_variant(variant);
179        true
180    }
181
182    fn set_sound_variant_by_key(&self, key: &Identifier) {
183        if let Some(sound_variant) = REGISTRY.cow_sound_variants.by_key(key) {
184            self.set_sound_variant(sound_variant);
185        }
186    }
187
188    fn update_dirty_mob_effect_entity_data(&self) {
189        if let Some(display) = sync_dirty_mob_effects(&self.living_base, &self.entity_data) {
190            self.entity_data
191                .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
192        }
193    }
194
195    /// Returns whether an item stack matches the vanilla cow food tag.
196    #[must_use]
197    pub fn is_food(item_stack: &ItemStack) -> bool {
198        REGISTRY
199            .items
200            .is_in_tag(item_stack.item(), &ItemTag::COW_FOOD)
201    }
202
203    fn try_milk(&self, player: &Player, hand: InteractionHand) -> bool {
204        if AgeableMob::is_baby(self) {
205            return false;
206        }
207
208        let is_bucket = {
209            let inventory = player.inventory.lock();
210            inventory.get_item_in_hand(hand).is(&vanilla_items::BUCKET)
211        };
212        if !is_bucket {
213            return false;
214        }
215
216        player.play_sound(&sound_events::ENTITY_COW_MILK, 1.0, 1.0);
217
218        let overflow = {
219            let mut inventory = player.inventory.lock();
220            inventory.apply_filled_result(
221                hand,
222                ITEM_BEHAVIORS.default_instance(&vanilla_items::MILK_BUCKET),
223                player.has_infinite_materials(),
224                true,
225            )
226        };
227
228        if !overflow.is_empty() {
229            let _ = player.drop_item(overflow, false, Prediction::Predicted);
230        }
231
232        true
233    }
234}
235
236impl DefaultEntityPush for CowEntity {}
237
238impl Entity for CowEntity {
239    fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
240        Some(self)
241    }
242
243    fn is_hard_collision_relevant(&self) -> bool {
244        false
245    }
246
247    fn base(&self) -> &EntityBase {
248        &self.base
249    }
250
251    fn entity_type(&self) -> EntityTypeRef {
252        self.entity_type
253    }
254
255    fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
256        if let Some(variant) = item_stack.get(COW_VARIANT) {
257            self.set_variant(variant.value());
258        }
259        if let Some(sound_variant) = item_stack.get(COW_SOUND_VARIANT) {
260            self.set_sound_variant(sound_variant.value());
261        }
262    }
263
264    fn base_tick(&self) {
265        Mob::base_tick_mob(self);
266    }
267
268    fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
269        let scale = LivingEntity::get_scale(self);
270        if AgeableMob::is_baby(self) {
271            COW_BABY_DIMENSIONS.scale(scale)
272        } else if self.entity_type.fixed {
273            self.entity_type.dimensions
274        } else {
275            self.entity_type.dimensions.scale(scale)
276        }
277    }
278
279    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
280        Some(&self.entity_data)
281    }
282
283    fn update_data_before_sync(&self) {
284        self.update_dirty_mob_effect_entity_data();
285    }
286
287    fn max_up_step(&self) -> f32 {
288        self.attributes()
289            .lock()
290            .get_value(vanilla_attributes::STEP_HEIGHT)
291            .unwrap_or(f64::from(DEFAULT_STEP_HEIGHT)) as f32
292    }
293
294    fn sound_source(&self) -> SoundSource {
295        SoundSource::Neutral
296    }
297
298    fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
299        self.play_sound(self.sound_variant().step_sound, 0.15, 1.0);
300    }
301
302    fn save_additional(&self, nbt: &mut NbtCompound) {
303        self.save_mob(nbt);
304        self.save_ageable_mob(nbt);
305        self.save_animal(nbt);
306        nbt.insert("variant", self.variant().key.to_string());
307        nbt.insert("sound_variant", self.sound_variant().key.to_string());
308    }
309
310    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
311        self.load_mob(nbt);
312        self.load_ageable_mob(nbt);
313        self.load_animal(nbt);
314
315        if let Some(variant) = nbt.string("variant")
316            && let Ok(key) = Identifier::from_str(variant.to_str().as_ref())
317        {
318            self.set_variant_by_key(&key);
319        }
320        if let Some(sound_variant) = nbt.string("sound_variant")
321            && let Ok(key) = Identifier::from_str(sound_variant.to_str().as_ref())
322        {
323            self.set_sound_variant_by_key(&key);
324        }
325    }
326}
327
328impl LivingEntity for CowEntity {
329    fn living_base(&self) -> &LivingEntityBase {
330        &self.living_base
331    }
332
333    fn get_health(&self) -> f32 {
334        *self.entity_data.lock().living_entity().health.get()
335    }
336
337    fn set_health(&self, health: f32) {
338        let max_health = self.get_max_health();
339        let clamped = health.clamp(0.0, max_health);
340        self.entity_data
341            .lock()
342            .living_entity_mut()
343            .health
344            .set(clamped);
345    }
346
347    fn sound_volume(&self) -> f32 {
348        0.4
349    }
350
351    fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
352        Some(self.sound_variant().hurt_sound)
353    }
354
355    fn death_sound(&self) -> Option<SoundEventRef> {
356        Some(self.sound_variant().death_sound)
357    }
358
359    fn server_ai_step(&self, entity: &SharedEntity) {
360        Mob::mob_server_ai_step(self, entity);
361    }
362
363    fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
364        let result = Mob::mob_ai_step(self, entity);
365
366        AgeableMob::tick_ageable_mob(self);
367        Animal::tick_animal_love(self);
368        result
369    }
370}
371
372impl AgeableMob for CowEntity {
373    fn ageable_base(&self) -> &AgeableMobBase {
374        &self.ageable_base
375    }
376
377    fn is_age_locked(&self) -> bool {
378        *self.entity_data.lock().ageable_mob().age_locked.get()
379    }
380
381    fn set_age_locked(&self, age_locked: bool) {
382        self.entity_data
383            .lock()
384            .ageable_mob_mut()
385            .age_locked
386            .set(age_locked);
387    }
388
389    fn set_synced_baby(&self, baby: bool) {
390        self.entity_data.lock().ageable_mob_mut().baby.set(baby);
391    }
392
393    fn age_boundary_changed(&self, _baby: bool) {
394        self.refresh_dimensions();
395    }
396
397    fn breed_variant_key(&self) -> Option<&Identifier> {
398        Some(&self.variant().key)
399    }
400
401    fn set_breed_variant_key(&self, key: &Identifier) -> bool {
402        self.set_variant_by_key(key)
403    }
404
405    fn initialize_breed_offspring(&self, partner: &dyn AgeableMob, offspring: &dyn AgeableMob) {
406        let use_self_variant = rand::random::<bool>();
407        let variant_key = if use_self_variant {
408            self.breed_variant_key()
409        } else {
410            partner.breed_variant_key()
411        };
412        let Some(variant_key) = variant_key else {
413            return;
414        };
415
416        if !offspring.set_breed_variant_key(variant_key) {
417            log::error!("cow offspring could not inherit breeding variant {variant_key}");
418        }
419    }
420}
421
422impl Animal for CowEntity {
423    fn animal_base(&self) -> &AnimalBase {
424        &self.animal_base
425    }
426
427    fn is_food(&self, item_stack: &ItemStack) -> bool {
428        CowEntity::is_food(item_stack)
429    }
430}
431
432impl Mob for CowEntity {
433    fn mob_base(&self) -> &MobBase {
434        &self.mob_base
435    }
436
437    fn tick_goal_selectors(&self, entity: &SharedEntity) {
438        PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
439    }
440
441    fn tick_path_navigation(&self) {
442        PathfinderMob::tick_pathfinder_path_navigation(self);
443    }
444
445    fn custom_server_ai_step(&self) {
446        Animal::custom_server_ai_step_animal(self);
447    }
448
449    fn ambient_sound(&self) -> Option<SoundEventRef> {
450        Some(self.sound_variant().ambient_sound)
451    }
452
453    fn finalize_spawn(
454        &self,
455        world: &Arc<World>,
456        spawn_reason: EntitySpawnReason,
457        group_data: Option<SpawnGroupData>,
458    ) -> Option<SpawnGroupData> {
459        let biome = world.biome_at(self.block_position());
460        let (variant, sound_variant) = {
461            let mut random = LegacyRandom::from_seed(rand::random());
462            let variant = biome.and_then(|biome| {
463                REGISTRY
464                    .cow_variants
465                    .select_spawn_variant(biome, &mut random)
466            });
467            let sound_variant = REGISTRY.cow_sound_variants.pick_random(&mut random);
468            (variant, sound_variant)
469        };
470
471        if let Some(variant) = variant {
472            self.set_variant(variant);
473        }
474
475        if let Some(sound_variant) = sound_variant {
476            self.set_sound_variant(sound_variant);
477        }
478
479        self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
480    }
481
482    fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
483        if self.try_milk(player, hand) {
484            return InteractionResult::Success;
485        }
486
487        Animal::mob_interact_animal(self, player, hand)
488    }
489
490    fn mob_flags(&self) -> i8 {
491        *self.entity_data.lock().mob().mob_flags.get()
492    }
493
494    fn set_mob_flags(&self, flags: i8) {
495        self.entity_data.lock().mob_mut().mob_flags.set(flags);
496    }
497}
498
499impl PathfinderMob for CowEntity {}
500
501#[cfg(test)]
502mod tests;