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#[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#[derive(Debug, Clone, Copy)]
63pub(crate) enum EntitySpawnPlacement {
64 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#[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#[derive(Debug)]
107pub(crate) enum EntitySpawnError {
108 InvalidPosition,
109 Peaceful,
110 MissingFactory,
111 InvalidEntityData,
112 AddEntity,
113}
114
115pub(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
143pub(crate) fn add_spawned_entity(
145 world: &Arc<World>,
146 entity: SharedEntity,
147) -> Result<(), AddEntityError> {
148 world.try_add_entity(entity)
149}
150
151pub(crate) struct LoadedEntity {
153 pub(crate) root: SharedEntity,
154 passengers: Vec<Self>,
155}
156
157pub(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
169pub(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 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
329pub(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
342pub(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
373pub(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}