Skip to main content

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