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#[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#[derive(Debug, Clone, Copy)]
54pub(crate) enum EntitySpawnPlacement {
55 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#[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#[derive(Debug)]
98pub(crate) enum EntitySpawnError {
99 InvalidPosition,
100 Peaceful,
101 MissingFactory,
102 InvalidEntityData,
103 AddEntity,
104}
105
106pub(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
134pub(crate) fn add_spawned_entity(
136 world: &Arc<World>,
137 entity: SharedEntity,
138) -> Result<(), AddEntityError> {
139 world.try_add_entity(entity)
140}
141
142pub(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
155pub(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
186pub(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}