Skip to main content

steel_core/entity/
spawn.rs

1use std::{io::Cursor, sync::Arc};
2
3use glam::DVec3;
4use simdnbt::borrow::read_compound;
5use steel_math::{DEGREE_360, wrap_degrees};
6use steel_registry::data_components::vanilla_components::{CUSTOM_DATA, CUSTOM_NAME, ENTITY_DATA};
7use steel_registry::entity_type::EntityTypeRef;
8use steel_registry::item_stack::ItemStack;
9use steel_utils::nbt::merge_nbt_compounds;
10use steel_utils::{BlockPos, WorldAabb, axis::Axis, types::Difficulty};
11
12use super::{AddEntityError, ENTITIES, SharedEntity, next_entity_id};
13use crate::physics::{CollisionWorld, WorldCollisionProvider, collide};
14use crate::world::World;
15
16/// Vanilla `EntitySpawnReason`.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EntitySpawnReason {
19    Natural,
20    ChunkGeneration,
21    Spawner,
22    Structure,
23    Breeding,
24    MobSummoned,
25    Jockey,
26    Event,
27    Conversion,
28    Reinforcement,
29    Triggered,
30    Bucket,
31    SpawnItemUse,
32    Command,
33    Dispenser,
34    Patrol,
35    TrialSpawner,
36    Load,
37    DimensionTravel,
38}
39
40impl EntitySpawnReason {
41    #[must_use]
42    pub const fn is_spawner(self) -> bool {
43        matches!(self, Self::Spawner | Self::TrialSpawner)
44    }
45
46    #[must_use]
47    pub const fn ignores_light_requirements(self) -> bool {
48        matches!(self, Self::TrialSpawner)
49    }
50}
51
52/// Placement modes used by the shared vanilla entity-spawn coordinator.
53#[derive(Debug, Clone, Copy)]
54pub(crate) enum EntitySpawnPlacement {
55    /// Spawn at a block center, optionally applying vanilla's downward offset.
56    Block {
57        pos: BlockPos,
58        try_move_down: bool,
59        moved_up: bool,
60    },
61}
62
63impl EntitySpawnPlacement {
64    fn factory_position(self) -> DVec3 {
65        match self {
66            Self::Block {
67                pos,
68                try_move_down: true,
69                ..
70            } => DVec3::new(
71                f64::from(pos.x()) + 0.5,
72                f64::from(pos.y()) + 1.0,
73                f64::from(pos.z()) + 0.5,
74            ),
75            Self::Block { pos, .. } => DVec3::new(
76                f64::from(pos.x()) + 0.5,
77                f64::from(pos.y()),
78                f64::from(pos.z()) + 0.5,
79            ),
80        }
81    }
82}
83
84/// Request for a complete, world-inserting entity spawn.
85#[derive(Debug, Clone, Copy)]
86pub(crate) struct EntitySpawnRequest<'a> {
87    pub entity_type: EntityTypeRef,
88    pub placement: EntitySpawnPlacement,
89    pub reason: EntitySpawnReason,
90    pub finalize_spawn: bool,
91    pub play_ambient_sound: bool,
92    pub item_stack: Option<&'a ItemStack>,
93    pub user_is_operator: bool,
94}
95
96/// Failure from the shared spawn coordinator.
97#[derive(Debug)]
98pub(crate) enum EntitySpawnError {
99    InvalidPosition,
100    Peaceful,
101    MissingFactory,
102    InvalidEntityData,
103    AddEntity,
104}
105
106/// Creates an entity instance through the generated entity factory registry.
107pub(crate) fn create_entity_instance(
108    world: &Arc<World>,
109    entity_type: EntityTypeRef,
110    position: DVec3,
111) -> Result<SharedEntity, EntitySpawnError> {
112    if !position.is_finite() || !World::is_in_spawnable_bounds(BlockPos::from(position)) {
113        return Err(EntitySpawnError::InvalidPosition);
114    }
115
116    if world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful {
117        return Err(EntitySpawnError::Peaceful);
118    }
119
120    if !ENTITIES.has_factory(entity_type) {
121        return Err(EntitySpawnError::MissingFactory);
122    }
123
124    ENTITIES
125        .create(
126            entity_type,
127            next_entity_id(),
128            position,
129            Arc::downgrade(world),
130        )
131        .ok_or(EntitySpawnError::MissingFactory)
132}
133
134/// Inserts a fully initialized entity into the live world entity manager.
135pub(crate) fn add_spawned_entity(
136    world: &Arc<World>,
137    entity: SharedEntity,
138) -> Result<(), AddEntityError> {
139    world.try_add_entity(entity)
140}
141
142/// Applies the implicit entity data carried by an item stack.
143pub(crate) fn apply_implicit_item_stack_components(entity: &SharedEntity, item_stack: &ItemStack) {
144    entity.apply_implicit_item_components(item_stack);
145
146    if let Some(custom_name) = item_stack.get(CUSTOM_NAME) {
147        entity.set_custom_name(Some(custom_name.clone()));
148    }
149
150    if let Some(custom_data) = item_stack.get(CUSTOM_DATA) {
151        entity.set_custom_data(custom_data.copy_tag());
152    }
153}
154
155/// Applies all entity data carried by an item stack during a normal item spawn.
156pub(crate) fn apply_item_stack_components(
157    entity: &SharedEntity,
158    item_stack: &ItemStack,
159    user_is_operator: bool,
160) -> Result<(), EntitySpawnError> {
161    apply_implicit_item_stack_components(entity, item_stack);
162
163    let Some(entity_data) = item_stack.get(ENTITY_DATA) else {
164        return Ok(());
165    };
166
167    if entity_data.entity_type() != entity.entity_type() {
168        return Ok(());
169    }
170
171    if entity.entity_type().only_op_can_set_nbt && !user_is_operator {
172        return Ok(());
173    }
174
175    let mut merged = entity.nbt_for_data_compare();
176    merge_nbt_compounds(&mut merged, &entity_data.data().copy_tag());
177
178    let mut bytes = Vec::new();
179    merged.write(&mut bytes);
180    let mut cursor = Cursor::new(bytes.as_slice());
181    let borrowed = read_compound(&mut cursor).map_err(|_| EntitySpawnError::InvalidEntityData)?;
182    entity.apply_spawn_data((&borrowed).into());
183    Ok(())
184}
185
186/// Mirrors vanilla `EntityType.spawn` for server-side entity creation.
187pub(crate) fn spawn_entity(
188    world: &Arc<World>,
189    request: EntitySpawnRequest<'_>,
190) -> Result<SharedEntity, EntitySpawnError> {
191    let entity = create_entity_instance(
192        world,
193        request.entity_type,
194        request.placement.factory_position(),
195    )?;
196
197    let EntitySpawnPlacement::Block {
198        pos,
199        try_move_down,
200        moved_up,
201    } = request.placement;
202    let position_above = DVec3::new(
203        f64::from(pos.x()) + 0.5,
204        f64::from(pos.y()) + 1.0,
205        f64::from(pos.z()) + 0.5,
206    );
207    if try_move_down {
208        entity.base().set_position_local(position_above);
209    }
210
211    let y_offset = if try_move_down {
212        entity_y_offset(world, pos, moved_up, entity.bounding_box())
213    } else {
214        0.0
215    };
216    let position = DVec3::new(
217        f64::from(pos.x()) + 0.5,
218        f64::from(pos.y()) + y_offset,
219        f64::from(pos.z()) + 0.5,
220    );
221    let rotation = (wrap_degrees(rand::random::<f32>() * DEGREE_360), 0.0);
222
223    entity.base().set_position_local(position);
224    entity.set_rotation(rotation);
225    entity.set_old_position_to_current();
226    entity.base().set_old_rotation_to_current();
227
228    if request.finalize_spawn
229        && let Some(mob) = entity.as_mob()
230    {
231        mob.set_y_head_rot(rotation.0);
232        mob.set_y_body_rot(rotation.0);
233        let _ = mob.finalize_spawn(world, request.reason, None);
234    }
235
236    if let Some(item_stack) = request.item_stack {
237        apply_item_stack_components(&entity, item_stack, request.user_is_operator)?;
238    }
239
240    add_spawned_entity(world, Arc::clone(&entity)).map_err(|_| EntitySpawnError::AddEntity)?;
241
242    if request.play_ambient_sound
243        && let Some(mob) = entity.as_mob()
244    {
245        mob.play_ambient_sound();
246    }
247
248    Ok(entity)
249}
250
251fn entity_y_offset(
252    world: &Arc<World>,
253    spawn_pos: BlockPos,
254    moved_up: bool,
255    entity_box: WorldAabb,
256) -> f64 {
257    let min_y = f64::from(spawn_pos.y()) - if moved_up { 1.0 } else { 0.0 };
258    let collision_box = WorldAabb::new(
259        f64::from(spawn_pos.x()),
260        min_y,
261        f64::from(spawn_pos.z()),
262        f64::from(spawn_pos.x() + 1),
263        f64::from(spawn_pos.y() + 1),
264        f64::from(spawn_pos.z() + 1),
265    );
266    let shapes = WorldCollisionProvider::new(world).get_block_collisions(&collision_box);
267    1.0 + collide(
268        Axis::Y,
269        &entity_box,
270        &shapes,
271        if moved_up { -2.0 } else { -1.0 },
272    )
273}
274
275#[derive(Debug, Clone, Copy, PartialEq)]
276pub enum SpawnGroupData {
277    AgeableMob(AgeableMobGroupData),
278}
279
280#[derive(Debug, Clone, Copy, PartialEq)]
281pub struct AgeableMobGroupData {
282    group_size: i32,
283    should_spawn_baby: bool,
284    baby_spawn_chance: f32,
285}
286
287impl AgeableMobGroupData {
288    pub const DEFAULT_BABY_SPAWN_CHANCE: f32 = 0.05;
289
290    #[must_use]
291    pub const fn new(should_spawn_baby: bool, baby_spawn_chance: f32) -> Self {
292        Self {
293            group_size: 0,
294            should_spawn_baby,
295            baby_spawn_chance,
296        }
297    }
298
299    #[must_use]
300    pub const fn with_should_spawn_baby(should_spawn_baby: bool) -> Self {
301        Self::new(should_spawn_baby, Self::DEFAULT_BABY_SPAWN_CHANCE)
302    }
303
304    #[must_use]
305    pub const fn with_baby_spawn_chance(baby_spawn_chance: f32) -> Self {
306        Self::new(true, baby_spawn_chance)
307    }
308
309    #[must_use]
310    pub const fn group_size(self) -> i32 {
311        self.group_size
312    }
313
314    #[must_use]
315    pub const fn should_spawn_baby(self) -> bool {
316        self.should_spawn_baby
317    }
318
319    #[must_use]
320    pub const fn baby_spawn_chance(self) -> f32 {
321        self.baby_spawn_chance
322    }
323
324    pub const fn increase_group_size_by_one(&mut self) {
325        self.group_size += 1;
326    }
327
328    #[must_use]
329    pub const fn needs_baby_spawn_roll(self) -> bool {
330        self.should_spawn_baby && self.group_size > 0
331    }
332
333    pub fn finalize_ageable_spawn(&mut self, baby_roll: impl FnOnce() -> f32) -> bool {
334        let spawn_baby = self.needs_baby_spawn_roll() && baby_roll() <= self.baby_spawn_chance;
335        self.increase_group_size_by_one();
336        spawn_baby
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use std::sync::{Arc, Weak};
343
344    use glam::DVec3;
345    use simdnbt::owned::NbtCompound;
346    use steel_registry::data_components::CustomData;
347    use steel_registry::data_components::components::EntityData;
348    use steel_registry::data_components::vanilla_components::{
349        CHICKEN_SOUND_VARIANT, CHICKEN_VARIANT, COW_SOUND_VARIANT, COW_VARIANT, ENTITY_DATA,
350        PIG_VARIANT, SHEEP_COLOR,
351    };
352    use steel_registry::init_vanilla_registry;
353    use steel_registry::item_stack::ItemStack;
354    use steel_registry::{
355        DyeColor, RegistryReference, vanilla_chicken_sound_variants, vanilla_chicken_variants,
356        vanilla_cow_sound_variants, vanilla_cow_variants, vanilla_entities, vanilla_items,
357        vanilla_pig_variants,
358    };
359    use text_components::TextComponent;
360
361    use crate::entity::entities::{ChickenEntity, CowEntity, PigEntity, SheepEntity};
362    use crate::entity::{AgeableMob, Entity, SharedEntity};
363
364    use super::{AgeableMobGroupData, apply_item_stack_components};
365
366    #[test]
367    fn ageable_group_data_increments_before_later_baby_rolls_can_apply() {
368        let mut group_data = AgeableMobGroupData::with_should_spawn_baby(true);
369
370        assert!(!group_data.finalize_ageable_spawn(|| {
371            panic!("first group member should not roll for baby spawn")
372        }));
373        assert_eq!(group_data.group_size(), 1);
374
375        assert!(group_data.finalize_ageable_spawn(|| 0.05));
376        assert_eq!(group_data.group_size(), 2);
377    }
378
379    #[test]
380    fn ageable_group_data_can_disable_baby_spawns() {
381        let mut group_data = AgeableMobGroupData::with_should_spawn_baby(false);
382
383        assert!(
384            !group_data
385                .finalize_ageable_spawn(|| { panic!("disabled baby spawning should not roll") })
386        );
387        assert!(
388            !group_data
389                .finalize_ageable_spawn(|| { panic!("disabled baby spawning should not roll") })
390        );
391        assert_eq!(group_data.group_size(), 2);
392    }
393
394    #[test]
395    fn item_entity_data_overrides_entity_state_without_replacing_defaults() {
396        init_vanilla_registry();
397
398        let pig = Arc::new(PigEntity::new(
399            &vanilla_entities::PIG,
400            1,
401            DVec3::ZERO,
402            Weak::new(),
403        ));
404        let entity: SharedEntity = pig.clone();
405        pig.set_custom_name(Some(TextComponent::plain("Existing")));
406
407        let mut payload = NbtCompound::new();
408        payload.insert("Age", pig.get_baby_start_age());
409        payload.insert("variant", vanilla_pig_variants::WARM.key.to_string());
410        let entity_data = EntityData::new(
411            &vanilla_entities::PIG,
412            CustomData::try_from_compound(payload).expect("test entity data should be valid"),
413        );
414        let mut spawn_egg = ItemStack::new(&vanilla_items::PIG_SPAWN_EGG);
415        spawn_egg.set(ENTITY_DATA, entity_data);
416
417        apply_item_stack_components(&entity, &spawn_egg, false)
418            .expect("valid typed entity data should load");
419
420        assert_eq!(pig.get_age(), pig.get_baby_start_age());
421        assert_eq!(pig.variant().key, vanilla_pig_variants::WARM.key);
422        assert!(AgeableMob::is_baby(pig.as_ref()));
423        assert_eq!(pig.custom_name(), Some(TextComponent::plain("Existing")));
424    }
425
426    #[test]
427    fn item_pig_variant_overrides_spawn_variant() {
428        init_vanilla_registry();
429
430        let pig = Arc::new(PigEntity::new(
431            &vanilla_entities::PIG,
432            1,
433            DVec3::ZERO,
434            Weak::new(),
435        ));
436        pig.set_variant(&vanilla_pig_variants::WARM);
437        let entity: SharedEntity = pig.clone();
438
439        let mut spawn_egg = ItemStack::new(&vanilla_items::PIG_SPAWN_EGG);
440        spawn_egg.set(
441            PIG_VARIANT,
442            RegistryReference::new(&vanilla_pig_variants::COLD),
443        );
444
445        apply_item_stack_components(&entity, &spawn_egg, false)
446            .expect("valid pig variant component should apply");
447
448        assert_eq!(pig.variant().key, vanilla_pig_variants::COLD.key);
449    }
450
451    #[test]
452    fn item_cow_components_override_spawn_state() {
453        init_vanilla_registry();
454
455        let cow = Arc::new(CowEntity::new(
456            &vanilla_entities::COW,
457            1,
458            DVec3::ZERO,
459            Weak::new(),
460        ));
461        let entity: SharedEntity = cow.clone();
462
463        let mut spawn_egg = ItemStack::new(&vanilla_items::COW_SPAWN_EGG);
464        spawn_egg.set(
465            COW_VARIANT,
466            RegistryReference::new(&vanilla_cow_variants::COLD),
467        );
468        spawn_egg.set(
469            COW_SOUND_VARIANT,
470            RegistryReference::new(&vanilla_cow_sound_variants::MOODY),
471        );
472
473        apply_item_stack_components(&entity, &spawn_egg, false)
474            .expect("valid cow components should apply");
475
476        assert_eq!(cow.variant().key, vanilla_cow_variants::COLD.key);
477        assert_eq!(
478            cow.sound_variant().key,
479            vanilla_cow_sound_variants::MOODY.key
480        );
481    }
482
483    #[test]
484    fn item_chicken_components_override_spawn_state() {
485        init_vanilla_registry();
486
487        let chicken = Arc::new(ChickenEntity::new(
488            &vanilla_entities::CHICKEN,
489            1,
490            DVec3::ZERO,
491            Weak::new(),
492        ));
493        let entity: SharedEntity = chicken.clone();
494
495        let mut spawn_egg = ItemStack::new(&vanilla_items::CHICKEN_SPAWN_EGG);
496        spawn_egg.set(
497            CHICKEN_VARIANT,
498            RegistryReference::new(&vanilla_chicken_variants::COLD),
499        );
500        spawn_egg.set(
501            CHICKEN_SOUND_VARIANT,
502            RegistryReference::new(&vanilla_chicken_sound_variants::PICKY),
503        );
504
505        apply_item_stack_components(&entity, &spawn_egg, false)
506            .expect("valid chicken components should apply");
507
508        assert_eq!(chicken.variant().key, vanilla_chicken_variants::COLD.key);
509        assert_eq!(
510            chicken.sound_variant().key,
511            vanilla_chicken_sound_variants::PICKY.key
512        );
513    }
514
515    #[test]
516    fn item_sheep_color_overrides_spawn_state() {
517        init_vanilla_registry();
518
519        let sheep = Arc::new(SheepEntity::new(
520            &vanilla_entities::SHEEP,
521            1,
522            DVec3::ZERO,
523            Weak::new(),
524        ));
525        let entity: SharedEntity = sheep.clone();
526
527        let mut spawn_egg = ItemStack::new(&vanilla_items::SHEEP_SPAWN_EGG);
528        spawn_egg.set(SHEEP_COLOR, DyeColor::Pink);
529
530        apply_item_stack_components(&entity, &spawn_egg, false)
531            .expect("valid sheep color should apply");
532
533        assert_eq!(sheep.color(), DyeColor::Pink);
534    }
535}