Skip to main content

steel_core/entity/
spawn.rs

1use std::{collections::BTreeSet, io::Cursor, sync::Arc};
2
3use glam::DVec3;
4use simdnbt::borrow::{BaseNbtCompound, NbtCompound as BorrowedNbtCompoundView, read_compound};
5use simdnbt::owned::NbtCompound;
6use steel_math::{DEGREE_360, wrap_degrees};
7use steel_registry::data_components::vanilla_components::{CUSTOM_DATA, CUSTOM_NAME, ENTITY_DATA};
8use steel_registry::entity_data::EntityPose;
9use steel_registry::entity_type::EntityTypeRef;
10use steel_registry::item_stack::ItemStack;
11use steel_registry::{REGISTRY, RegistryExt};
12use steel_utils::nbt::NbtValueInput as _;
13use steel_utils::nbt::{NbtNumeric, merge_nbt_compounds, nbt_collection_values};
14use steel_utils::{BlockPos, Identifier, UuidExt, WorldAabb, axis::Axis, types::Difficulty};
15use text_components::TextComponent;
16use uuid::Uuid;
17
18use super::{AddEntityError, ENTITIES, SharedEntity, next_entity_id};
19use crate::entity::entity::{read_nbt_dvec3, read_nbt_rotation, sanitize_nbt_motion};
20use crate::entity::{EntityBaseSaveData, EntityFireFreezeState, EntityLoadRequest};
21use crate::physics::{CollisionWorld, WorldCollisionProvider, collide};
22use crate::world::World;
23
24/// Reason an entity is being spawned, affecting spawn-time initialization
25/// such as equipment and group data.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum EntitySpawnReason {
28    Natural,
29    ChunkGeneration,
30    Spawner,
31    Structure,
32    Breeding,
33    MobSummoned,
34    Jockey,
35    Event,
36    Conversion,
37    Reinforcement,
38    Triggered,
39    Bucket,
40    SpawnItemUse,
41    Command,
42    Dispenser,
43    Patrol,
44    TrialSpawner,
45    Load,
46    DimensionTravel,
47}
48
49impl EntitySpawnReason {
50    #[must_use]
51    pub const fn is_spawner(self) -> bool {
52        matches!(self, Self::Spawner | Self::TrialSpawner)
53    }
54
55    #[must_use]
56    pub const fn ignores_light_requirements(self) -> bool {
57        matches!(self, Self::TrialSpawner)
58    }
59}
60
61/// Placement modes used by the shared vanilla entity-spawn coordinator.
62#[derive(Debug, Clone, Copy)]
63pub(crate) enum EntitySpawnPlacement {
64    /// Spawn at a block center, optionally applying vanilla's downward offset.
65    Block {
66        pos: BlockPos,
67        try_move_down: bool,
68        moved_up: bool,
69    },
70}
71
72impl EntitySpawnPlacement {
73    fn factory_position(self) -> DVec3 {
74        match self {
75            Self::Block {
76                pos,
77                try_move_down: true,
78                ..
79            } => DVec3::new(
80                f64::from(pos.x()) + 0.5,
81                f64::from(pos.y()) + 1.0,
82                f64::from(pos.z()) + 0.5,
83            ),
84            Self::Block { pos, .. } => DVec3::new(
85                f64::from(pos.x()) + 0.5,
86                f64::from(pos.y()),
87                f64::from(pos.z()) + 0.5,
88            ),
89        }
90    }
91}
92
93/// Request for a complete, world-inserting entity spawn.
94#[derive(Debug, Clone, Copy)]
95pub(crate) struct EntitySpawnRequest<'a> {
96    pub entity_type: EntityTypeRef,
97    pub placement: EntitySpawnPlacement,
98    pub reason: EntitySpawnReason,
99    pub finalize_spawn: bool,
100    pub play_ambient_sound: bool,
101    pub item_stack: Option<&'a ItemStack>,
102    pub user_is_operator: bool,
103}
104
105/// Failure from the shared spawn coordinator.
106#[derive(Debug)]
107pub(crate) enum EntitySpawnError {
108    InvalidPosition,
109    Peaceful,
110    MissingFactory,
111    InvalidEntityData,
112    AddEntity,
113}
114
115/// Creates an entity instance through the generated entity factory registry.
116pub(crate) fn create_entity_instance(
117    world: &Arc<World>,
118    entity_type: EntityTypeRef,
119    position: DVec3,
120) -> Result<SharedEntity, EntitySpawnError> {
121    if !position.is_finite() || !World::is_in_spawnable_bounds(BlockPos::from(position)) {
122        return Err(EntitySpawnError::InvalidPosition);
123    }
124
125    if world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful {
126        return Err(EntitySpawnError::Peaceful);
127    }
128
129    if !ENTITIES.has_factory(entity_type) {
130        return Err(EntitySpawnError::MissingFactory);
131    }
132
133    ENTITIES
134        .create(
135            entity_type,
136            next_entity_id(),
137            position,
138            Arc::downgrade(world),
139        )
140        .ok_or(EntitySpawnError::MissingFactory)
141}
142
143/// Inserts a fully initialized entity into the live world entity manager.
144pub(crate) fn add_spawned_entity(
145    world: &Arc<World>,
146    entity: SharedEntity,
147) -> Result<(), AddEntityError> {
148    world.try_add_entity(entity)
149}
150
151/// Keeps freshly loaded passengers alive until the world takes ownership.
152pub(crate) struct LoadedEntity {
153    pub(crate) root: SharedEntity,
154    passengers: Vec<Self>,
155}
156
157/// Loads a complete entity tree from vanilla entity NBT without publishing it
158/// to the world. The caller owns the atomic insertion boundary.
159pub(crate) fn load_entity_recursive(
160    world: &Arc<World>,
161    nbt: &BaseNbtCompound<'_>,
162    reason: EntitySpawnReason,
163    post_load: impl Fn(&SharedEntity),
164) -> Option<LoadedEntity> {
165    let nbt: BorrowedNbtCompoundView<'_, '_> = nbt.into();
166    load_entity_recursive_inner(world, &nbt, reason, &post_load)
167}
168
169/// Reborrows an owned compound and loads it through the recursive entity path.
170pub(crate) fn load_entity_recursive_owned(
171    world: &Arc<World>,
172    nbt: &NbtCompound,
173    reason: EntitySpawnReason,
174    post_load: impl Fn(&SharedEntity),
175) -> Option<LoadedEntity> {
176    let mut bytes = Vec::new();
177    nbt.write(&mut bytes);
178    let borrowed = read_compound(&mut Cursor::new(bytes.as_slice())).ok()?;
179    load_entity_recursive(world, &borrowed, reason, post_load)
180}
181
182#[expect(
183    clippy::only_used_in_recursion,
184    reason = "The spawn reason must be propagated to every nested passenger load."
185)]
186fn load_entity_recursive_inner(
187    world: &Arc<World>,
188    nbt: &BorrowedNbtCompoundView<'_, '_>,
189    reason: EntitySpawnReason,
190    post_load: &impl Fn(&SharedEntity),
191) -> Option<LoadedEntity> {
192    let entity_type = entity_type_from_nbt(nbt)?;
193    if world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful {
194        return None;
195    }
196    let position = read_nbt_dvec3(nbt, "Pos").unwrap_or(DVec3::ZERO);
197    if !position.is_finite() {
198        return None;
199    }
200
201    let motion = read_nbt_dvec3(nbt, "Motion").unwrap_or(DVec3::ZERO);
202    let rotation = read_nbt_rotation(nbt, "Rotation").unwrap_or((0.0, 0.0));
203    if !rotation.0.is_finite() || !rotation.1.is_finite() {
204        return None;
205    }
206
207    let uuid = nbt
208        .get("UUID")
209        .and_then(|tag| nbt_collection_values(&tag.to_owned()))
210        .and_then(|values| {
211            values
212                .iter()
213                .map(NbtNumeric::codec_i32)
214                .collect::<Option<Vec<_>>>()
215        })
216        .and_then(|values| Uuid::from_int_array(values.get(..4)?))
217        .unwrap_or_else(Uuid::new_v4);
218    let save_data = load_entity_save_data(nbt);
219    let request = EntityLoadRequest {
220        entity_type,
221        position: super::clamp_loaded_entity_position(position),
222        uuid,
223        velocity: sanitize_nbt_motion(motion),
224        rotation,
225        fall_distance: nbt
226            .get("fall_distance")
227            .and_then(|tag| tag.codec_f64())
228            .or_else(|| nbt.double_value("FallDistance"))
229            .unwrap_or(0.0),
230        fire_freeze: EntityFireFreezeState::from_parts(
231            nbt.get("Fire")
232                .and_then(|tag| tag.short_value())
233                .map_or(0, i32::from),
234            nbt.get("TicksFrozen")
235                .and_then(|tag| tag.int_value())
236                .unwrap_or(0),
237            false,
238            false,
239            nbt.get("HasVisualFire")
240                .and_then(|tag| tag.byte_value())
241                .is_some_and(|value| value != 0),
242        ),
243        on_ground: nbt
244            .get("OnGround")
245            .and_then(|tag| tag.byte_value())
246            .is_some_and(|value| value != 0),
247        save_data,
248        world: Arc::downgrade(world),
249    };
250
251    let entity = ENTITIES.create_and_load_for_spawn_view(request, nbt)?;
252    post_load(&entity);
253    let mut loaded = LoadedEntity {
254        root: entity,
255        passengers: Vec::new(),
256    };
257
258    let Some(passengers) = nbt.list("Passengers") else {
259        return Some(loaded);
260    };
261    let Some(passengers) = passengers.compounds() else {
262        return Some(loaded);
263    };
264    for passenger_nbt in passengers {
265        let Some(passenger) = load_entity_recursive_inner(world, &passenger_nbt, reason, post_load)
266        else {
267            continue;
268        };
269        if loaded.root.could_accept_passenger() && loaded.root.entity_type().can_serialize {
270            // Freshly created trees cannot cycle. NBT loading forces boarding in vanilla.
271            passenger.root.set_pose(EntityPose::Standing);
272            super::EntityBase::restore_passenger_relationship(&loaded.root, &passenger.root);
273            loaded.passengers.push(passenger);
274        }
275    }
276
277    Some(loaded)
278}
279
280fn entity_type_from_nbt(nbt: &BorrowedNbtCompoundView<'_, '_>) -> Option<EntityTypeRef> {
281    let id = nbt.string("id")?.to_str().parse::<Identifier>().ok()?;
282    REGISTRY.entity_types.by_key(&id)
283}
284
285fn load_entity_save_data(nbt: &BorrowedNbtCompoundView<'_, '_>) -> EntityBaseSaveData {
286    let mut save_data = EntityBaseSaveData::new();
287    save_data.air_supply = nbt
288        .get("Air")
289        .and_then(|tag| tag.int_value())
290        .unwrap_or(save_data.air_supply);
291    save_data.portal_cooldown = nbt
292        .get("PortalCooldown")
293        .and_then(|tag| tag.int_value())
294        .unwrap_or(0);
295    save_data.no_gravity = nbt
296        .get("NoGravity")
297        .and_then(|tag| tag.byte_value())
298        .is_some_and(|value| value != 0);
299    save_data.invulnerable = nbt
300        .get("Invulnerable")
301        .and_then(|tag| tag.byte_value())
302        .is_some_and(|value| value != 0);
303    save_data.custom_name = nbt
304        .get("CustomName")
305        .and_then(|tag| TextComponent::from_nbt(&tag.to_owned()));
306    save_data.custom_name_visible = nbt
307        .get("CustomNameVisible")
308        .and_then(|tag| tag.byte_value())
309        .is_some_and(|value| value != 0);
310    save_data.silent = nbt
311        .get("Silent")
312        .and_then(|tag| tag.byte_value())
313        .is_some_and(|value| value != 0);
314    save_data.glowing = nbt
315        .get("Glowing")
316        .and_then(|tag| tag.byte_value())
317        .is_some_and(|value| value != 0);
318    if let Some(tags) = nbt.list("Tags").and_then(|list| list.strings()) {
319        save_data.tags = tags
320            .iter()
321            .take(super::MAX_ENTITY_TAGS)
322            .map(|tag| tag.to_str().into_owned())
323            .collect::<BTreeSet<_>>();
324    }
325    save_data.custom_data = nbt.compound("data").map_or_default(|data| data.to_owned());
326    save_data
327}
328
329/// Applies the implicit entity data carried by an item stack.
330pub(crate) fn apply_implicit_item_stack_components(entity: &SharedEntity, item_stack: &ItemStack) {
331    entity.apply_implicit_item_components(item_stack);
332
333    if let Some(custom_name) = item_stack.get(CUSTOM_NAME) {
334        entity.set_custom_name(Some(custom_name.clone()));
335    }
336
337    if let Some(custom_data) = item_stack.get(CUSTOM_DATA) {
338        entity.set_custom_data(custom_data.copy_tag());
339    }
340}
341
342/// Applies all entity data carried by an item stack during a normal item spawn.
343pub(crate) fn apply_item_stack_components(
344    entity: &SharedEntity,
345    item_stack: &ItemStack,
346    user_is_operator: bool,
347) -> Result<(), EntitySpawnError> {
348    apply_implicit_item_stack_components(entity, item_stack);
349
350    let Some(entity_data) = item_stack.get(ENTITY_DATA) else {
351        return Ok(());
352    };
353
354    if entity_data.entity_type() != entity.entity_type() {
355        return Ok(());
356    }
357
358    if entity.entity_type().only_op_can_set_nbt && !user_is_operator {
359        return Ok(());
360    }
361
362    let mut merged = entity.nbt_for_data_compare();
363    merge_nbt_compounds(&mut merged, &entity_data.data().copy_tag());
364
365    let mut bytes = Vec::new();
366    merged.write(&mut bytes);
367    let mut cursor = Cursor::new(bytes.as_slice());
368    let borrowed = read_compound(&mut cursor).map_err(|_| EntitySpawnError::InvalidEntityData)?;
369    entity.apply_spawn_data((&borrowed).into());
370    Ok(())
371}
372
373/// Mirrors vanilla `EntityType.spawn` for server-side entity creation.
374pub(crate) fn spawn_entity(
375    world: &Arc<World>,
376    request: EntitySpawnRequest<'_>,
377) -> Result<SharedEntity, EntitySpawnError> {
378    let entity = create_entity_instance(
379        world,
380        request.entity_type,
381        request.placement.factory_position(),
382    )?;
383
384    let EntitySpawnPlacement::Block {
385        pos,
386        try_move_down,
387        moved_up,
388    } = request.placement;
389    let position_above = DVec3::new(
390        f64::from(pos.x()) + 0.5,
391        f64::from(pos.y()) + 1.0,
392        f64::from(pos.z()) + 0.5,
393    );
394    if try_move_down {
395        entity.base().set_position_local(position_above);
396    }
397
398    let y_offset = if try_move_down {
399        entity_y_offset(world, pos, moved_up, entity.bounding_box())
400    } else {
401        0.0
402    };
403    let position = DVec3::new(
404        f64::from(pos.x()) + 0.5,
405        f64::from(pos.y()) + y_offset,
406        f64::from(pos.z()) + 0.5,
407    );
408    let rotation = (wrap_degrees(rand::random::<f32>() * DEGREE_360), 0.0);
409
410    entity.base().set_position_local(position);
411    entity.set_rotation(rotation);
412    entity.set_old_position_to_current();
413    entity.base().set_old_rotation_to_current();
414
415    if request.finalize_spawn
416        && let Some(mob) = entity.as_mob()
417    {
418        mob.set_y_head_rot(rotation.0);
419        mob.set_y_body_rot(rotation.0);
420        let _ = mob.finalize_spawn(world, request.reason, None);
421    }
422
423    if let Some(item_stack) = request.item_stack {
424        apply_item_stack_components(&entity, item_stack, request.user_is_operator)?;
425    }
426
427    add_spawned_entity(world, Arc::clone(&entity)).map_err(|_| EntitySpawnError::AddEntity)?;
428
429    if request.play_ambient_sound
430        && let Some(mob) = entity.as_mob()
431    {
432        mob.play_ambient_sound();
433    }
434
435    Ok(entity)
436}
437
438fn entity_y_offset(
439    world: &Arc<World>,
440    spawn_pos: BlockPos,
441    moved_up: bool,
442    entity_box: WorldAabb,
443) -> f64 {
444    let min_y = f64::from(spawn_pos.y()) - if moved_up { 1.0 } else { 0.0 };
445    let collision_box = WorldAabb::new(
446        f64::from(spawn_pos.x()),
447        min_y,
448        f64::from(spawn_pos.z()),
449        f64::from(spawn_pos.x() + 1),
450        f64::from(spawn_pos.y() + 1),
451        f64::from(spawn_pos.z() + 1),
452    );
453    let shapes = WorldCollisionProvider::new(world).get_block_collisions(&collision_box);
454    1.0 + collide(
455        Axis::Y,
456        &entity_box,
457        &shapes,
458        if moved_up { -2.0 } else { -1.0 },
459    )
460}
461
462#[derive(Debug, Clone, Copy, PartialEq)]
463pub enum SpawnGroupData {
464    AgeableMob(AgeableMobGroupData),
465}
466
467#[derive(Debug, Clone, Copy, PartialEq)]
468pub struct AgeableMobGroupData {
469    group_size: i32,
470    should_spawn_baby: bool,
471    baby_spawn_chance: f32,
472}
473
474impl AgeableMobGroupData {
475    pub const DEFAULT_BABY_SPAWN_CHANCE: f32 = 0.05;
476
477    #[must_use]
478    pub const fn new(should_spawn_baby: bool, baby_spawn_chance: f32) -> Self {
479        Self {
480            group_size: 0,
481            should_spawn_baby,
482            baby_spawn_chance,
483        }
484    }
485
486    #[must_use]
487    pub const fn with_should_spawn_baby(should_spawn_baby: bool) -> Self {
488        Self::new(should_spawn_baby, Self::DEFAULT_BABY_SPAWN_CHANCE)
489    }
490
491    #[must_use]
492    pub const fn with_baby_spawn_chance(baby_spawn_chance: f32) -> Self {
493        Self::new(true, baby_spawn_chance)
494    }
495
496    #[must_use]
497    pub const fn group_size(self) -> i32 {
498        self.group_size
499    }
500
501    #[must_use]
502    pub const fn should_spawn_baby(self) -> bool {
503        self.should_spawn_baby
504    }
505
506    #[must_use]
507    pub const fn baby_spawn_chance(self) -> f32 {
508        self.baby_spawn_chance
509    }
510
511    pub const fn increase_group_size_by_one(&mut self) {
512        self.group_size += 1;
513    }
514
515    #[must_use]
516    pub const fn needs_baby_spawn_roll(self) -> bool {
517        self.should_spawn_baby && self.group_size > 0
518    }
519
520    pub fn finalize_ageable_spawn(&mut self, baby_roll: impl FnOnce() -> f32) -> bool {
521        let spawn_baby = self.needs_baby_spawn_roll() && baby_roll() <= self.baby_spawn_chance;
522        self.increase_group_size_by_one();
523        spawn_baby
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use std::sync::{Arc, Weak};
530
531    use glam::DVec3;
532    use simdnbt::owned::{NbtCompound, NbtList};
533    use steel_registry::data_components::CustomData;
534    use steel_registry::data_components::components::EntityData;
535    use steel_registry::data_components::vanilla_components::{
536        CHICKEN_SOUND_VARIANT, CHICKEN_VARIANT, COW_SOUND_VARIANT, COW_VARIANT, ENTITY_DATA,
537        PIG_VARIANT, SHEEP_COLOR,
538    };
539    use steel_registry::init_vanilla_registry;
540    use steel_registry::item_stack::ItemStack;
541    use steel_registry::{
542        DyeColor, RegistryReference, vanilla_chicken_sound_variants, vanilla_chicken_variants,
543        vanilla_cow_sound_variants, vanilla_cow_variants, vanilla_entities, vanilla_items,
544        vanilla_pig_variants,
545    };
546    use steel_utils::types::Difficulty;
547    use text_components::TextComponent;
548
549    use crate::entity::entities::{ChickenEntity, CowEntity, PigEntity, SheepEntity};
550    use crate::entity::{
551        AgeableMob, Entity, EntitySpawnReason, SharedEntity, init_entities,
552        load_entity_recursive_owned,
553    };
554    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
555
556    use super::{AgeableMobGroupData, apply_item_stack_components};
557
558    fn entity_nbt(id: &str) -> NbtCompound {
559        let mut entity = NbtCompound::new();
560        entity.insert("id", id);
561        entity
562    }
563
564    #[test]
565    fn recursive_spawner_load_rejects_unknown_and_unimplemented_entities() {
566        init_vanilla_registry();
567        init_entities();
568        let world_fixture = fresh_test_world("spawner_recursive_load_rejects_unsupported");
569        let world = &world_fixture.world;
570
571        let unknown = entity_nbt("minecraft:not_an_entity");
572        assert!(
573            load_entity_recursive_owned(world, &unknown, EntitySpawnReason::Spawner, |_| {},)
574                .is_none()
575        );
576
577        let malformed = entity_nbt("not an entity identifier");
578        assert!(
579            load_entity_recursive_owned(world, &malformed, EntitySpawnReason::Spawner, |_| {},)
580                .is_none()
581        );
582
583        let unimplemented = entity_nbt("minecraft:blaze");
584        assert!(
585            load_entity_recursive_owned(world, &unimplemented, EntitySpawnReason::Spawner, |_| {},)
586                .is_none()
587        );
588    }
589
590    #[test]
591    fn recursive_spawner_load_skips_failed_passengers_and_keeps_valid_siblings() {
592        init_vanilla_registry();
593        init_entities();
594        let world_fixture = fresh_test_world("spawner_recursive_load_rejects_passenger");
595        let world = &world_fixture.world;
596
597        let mut root = entity_nbt("minecraft:pig");
598        root.insert(
599            "Passengers",
600            NbtList::Compound(vec![
601                entity_nbt("minecraft:not_an_entity"),
602                entity_nbt("minecraft:blaze"),
603                entity_nbt("minecraft:chicken"),
604                entity_nbt("minecraft:pig"),
605            ]),
606        );
607
608        let loaded = load_entity_recursive_owned(world, &root, EntitySpawnReason::Spawner, |_| {})
609            .expect("failed passengers must not discard the root");
610        let root = Arc::clone(&loaded.root);
611        assert_eq!(root.entity_type(), &vanilla_entities::PIG);
612        let passengers = root.passengers();
613        assert_eq!(passengers.len(), 2);
614        assert_eq!(passengers[0].entity_type(), &vanilla_entities::CHICKEN);
615        assert_eq!(
616            passengers[0].vehicle().map(|vehicle| vehicle.id()),
617            Some(root.id())
618        );
619        drop(passengers);
620        insert_ready_full_chunk(world, steel_utils::ChunkPos::new(0, 0));
621        world
622            .try_add_fresh_entity_with_passengers(Arc::clone(&root))
623            .expect("loaded passengers should be inserted with the root");
624        drop(loaded);
625        assert_eq!(root.passengers().len(), 2);
626    }
627
628    #[test]
629    fn recursive_spawner_load_rejects_hostile_entities_in_peaceful() {
630        init_vanilla_registry();
631        init_entities();
632        let world_fixture = fresh_test_world("spawner_recursive_load_peaceful");
633        let world = &world_fixture.world;
634        world.set_difficulty(Difficulty::Peaceful);
635
636        assert!(
637            load_entity_recursive_owned(
638                world,
639                &entity_nbt("minecraft:endermite"),
640                EntitySpawnReason::Spawner,
641                |_| {},
642            )
643            .is_none()
644        );
645
646        let mut root = entity_nbt("minecraft:pig");
647        root.insert(
648            "Passengers",
649            NbtList::Compound(vec![
650                entity_nbt("minecraft:endermite"),
651                entity_nbt("minecraft:chicken"),
652            ]),
653        );
654        let loaded = load_entity_recursive_owned(world, &root, EntitySpawnReason::Spawner, |_| {})
655            .expect("a peaceful-safe root should still load");
656        let passengers = loaded.root.passengers();
657        assert_eq!(passengers.len(), 1);
658        assert_eq!(passengers[0].entity_type(), &vanilla_entities::CHICKEN);
659    }
660
661    #[test]
662    fn ageable_group_data_increments_before_later_baby_rolls_can_apply() {
663        let mut group_data = AgeableMobGroupData::with_should_spawn_baby(true);
664
665        assert!(!group_data.finalize_ageable_spawn(|| {
666            panic!("first group member should not roll for baby spawn")
667        }));
668        assert_eq!(group_data.group_size(), 1);
669
670        assert!(group_data.finalize_ageable_spawn(|| 0.05));
671        assert_eq!(group_data.group_size(), 2);
672    }
673
674    #[test]
675    fn ageable_group_data_can_disable_baby_spawns() {
676        let mut group_data = AgeableMobGroupData::with_should_spawn_baby(false);
677
678        assert!(
679            !group_data
680                .finalize_ageable_spawn(|| { panic!("disabled baby spawning should not roll") })
681        );
682        assert!(
683            !group_data
684                .finalize_ageable_spawn(|| { panic!("disabled baby spawning should not roll") })
685        );
686        assert_eq!(group_data.group_size(), 2);
687    }
688
689    #[test]
690    fn item_entity_data_overrides_entity_state_without_replacing_defaults() {
691        init_vanilla_registry();
692
693        let pig = Arc::new(PigEntity::new(
694            &vanilla_entities::PIG,
695            1,
696            DVec3::ZERO,
697            Weak::new(),
698        ));
699        let entity: SharedEntity = Arc::<PigEntity>::clone(&pig);
700        pig.set_custom_name(Some(TextComponent::plain("Existing")));
701
702        let mut payload = NbtCompound::new();
703        payload.insert("Age", pig.get_baby_start_age());
704        payload.insert("variant", vanilla_pig_variants::WARM.key.to_string());
705        let entity_data = EntityData::new(
706            &vanilla_entities::PIG,
707            CustomData::try_from_compound(payload).expect("test entity data should be valid"),
708        );
709        let mut spawn_egg = ItemStack::new(&vanilla_items::PIG_SPAWN_EGG);
710        spawn_egg.set(ENTITY_DATA, entity_data);
711
712        apply_item_stack_components(&entity, &spawn_egg, false)
713            .expect("valid typed entity data should load");
714
715        assert_eq!(pig.get_age(), pig.get_baby_start_age());
716        assert_eq!(pig.variant().key, vanilla_pig_variants::WARM.key);
717        assert!(AgeableMob::is_baby(pig.as_ref()));
718        assert_eq!(pig.custom_name(), Some(TextComponent::plain("Existing")));
719    }
720
721    #[test]
722    fn item_pig_variant_overrides_spawn_variant() {
723        init_vanilla_registry();
724
725        let pig = Arc::new(PigEntity::new(
726            &vanilla_entities::PIG,
727            1,
728            DVec3::ZERO,
729            Weak::new(),
730        ));
731        pig.set_variant(&vanilla_pig_variants::WARM);
732        let entity: SharedEntity = Arc::<PigEntity>::clone(&pig);
733
734        let mut spawn_egg = ItemStack::new(&vanilla_items::PIG_SPAWN_EGG);
735        spawn_egg.set(
736            PIG_VARIANT,
737            RegistryReference::new(&vanilla_pig_variants::COLD),
738        );
739
740        apply_item_stack_components(&entity, &spawn_egg, false)
741            .expect("valid pig variant component should apply");
742
743        assert_eq!(pig.variant().key, vanilla_pig_variants::COLD.key);
744    }
745
746    #[test]
747    fn item_cow_components_override_spawn_state() {
748        init_vanilla_registry();
749
750        let cow = Arc::new(CowEntity::new(
751            &vanilla_entities::COW,
752            1,
753            DVec3::ZERO,
754            Weak::new(),
755        ));
756        let entity: SharedEntity = Arc::<CowEntity>::clone(&cow);
757
758        let mut spawn_egg = ItemStack::new(&vanilla_items::COW_SPAWN_EGG);
759        spawn_egg.set(
760            COW_VARIANT,
761            RegistryReference::new(&vanilla_cow_variants::COLD),
762        );
763        spawn_egg.set(
764            COW_SOUND_VARIANT,
765            RegistryReference::new(&vanilla_cow_sound_variants::MOODY),
766        );
767
768        apply_item_stack_components(&entity, &spawn_egg, false)
769            .expect("valid cow components should apply");
770
771        assert_eq!(cow.variant().key, vanilla_cow_variants::COLD.key);
772        assert_eq!(
773            cow.sound_variant().key,
774            vanilla_cow_sound_variants::MOODY.key
775        );
776    }
777
778    #[test]
779    fn item_chicken_components_override_spawn_state() {
780        init_vanilla_registry();
781
782        let chicken = Arc::new(ChickenEntity::new(
783            &vanilla_entities::CHICKEN,
784            1,
785            DVec3::ZERO,
786            Weak::new(),
787        ));
788        let entity: SharedEntity = Arc::<ChickenEntity>::clone(&chicken);
789
790        let mut spawn_egg = ItemStack::new(&vanilla_items::CHICKEN_SPAWN_EGG);
791        spawn_egg.set(
792            CHICKEN_VARIANT,
793            RegistryReference::new(&vanilla_chicken_variants::COLD),
794        );
795        spawn_egg.set(
796            CHICKEN_SOUND_VARIANT,
797            RegistryReference::new(&vanilla_chicken_sound_variants::PICKY),
798        );
799
800        apply_item_stack_components(&entity, &spawn_egg, false)
801            .expect("valid chicken components should apply");
802
803        assert_eq!(chicken.variant().key, vanilla_chicken_variants::COLD.key);
804        assert_eq!(
805            chicken.sound_variant().key,
806            vanilla_chicken_sound_variants::PICKY.key
807        );
808    }
809
810    #[test]
811    fn item_sheep_color_overrides_spawn_state() {
812        init_vanilla_registry();
813
814        let sheep = Arc::new(SheepEntity::new(
815            &vanilla_entities::SHEEP,
816            1,
817            DVec3::ZERO,
818            Weak::new(),
819        ));
820        let entity: SharedEntity = Arc::<SheepEntity>::clone(&sheep);
821
822        let mut spawn_egg = ItemStack::new(&vanilla_items::SHEEP_SPAWN_EGG);
823        spawn_egg.set(SHEEP_COLOR, DyeColor::Pink);
824
825        apply_item_stack_components(&entity, &spawn_egg, false)
826            .expect("valid sheep color should apply");
827
828        assert_eq!(sheep.color(), DyeColor::Pink);
829    }
830}