Skip to main content

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

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