1use std::mem;
4use std::sync::Arc;
5
6use glam::DVec3;
7use rand::{Rng, RngExt, rng};
8use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
9use simdnbt::owned::{NbtCompound, NbtList};
10use steel_math::DEGREE_360;
11use steel_registry::data_components::components::EntityData;
12use steel_registry::entity_type::{EntityTypeRef, MobCategory};
13use steel_registry::loot_table::{LootContext, LootTableRef};
14use steel_registry::{
15 REGISTRY, RegistryExt, level_events, vanilla_blocks, vanilla_game_events, vanilla_game_rules,
16};
17use steel_utils::entity_events::EntityStatus;
18use steel_utils::locks::SyncMutex;
19use steel_utils::nbt::{NbtNumeric, merge_nbt_compounds};
20use steel_utils::types::Difficulty;
21use steel_utils::{BlockPos, Identifier, WorldAabb};
22
23use super::spawn_data::{EquipmentTable, SpawnData, WeightedSpawnData};
24use crate::entity::{
25 Entity, EntitySpawnReason, Mob, SpawnPlacements, entity_loot_ref, living_loot_equipment,
26 load_entity_recursive_owned, nbt_dvec3,
27};
28use crate::inventory::equipment::EquipmentSlot;
29use crate::physics::{WorldCollisionProvider, has_collision};
30use crate::world::World;
31
32const EVENT_SPAWN: i32 = 1;
33const DEFAULT_SPAWN_DELAY: i32 = 20;
34const DEFAULT_MIN_SPAWN_DELAY: i32 = 200;
35const DEFAULT_MAX_SPAWN_DELAY: i32 = 800;
36const DEFAULT_SPAWN_COUNT: i32 = 4;
37const DEFAULT_MAX_NEARBY_ENTITIES: i32 = 6;
38const DEFAULT_REQUIRED_PLAYER_RANGE: i32 = 16;
39const DEFAULT_SPAWN_RANGE: i32 = 4;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct BaseSpawnerConfig {
46 pub min_spawn_delay: i32,
48 pub max_spawn_delay: i32,
50 pub spawn_count: i32,
52 pub max_nearby_entities: i32,
54 pub required_player_range: i32,
56 pub spawn_range: i32,
58}
59
60impl Default for BaseSpawnerConfig {
61 fn default() -> Self {
62 Self {
63 min_spawn_delay: DEFAULT_MIN_SPAWN_DELAY,
64 max_spawn_delay: DEFAULT_MAX_SPAWN_DELAY,
65 spawn_count: DEFAULT_SPAWN_COUNT,
66 max_nearby_entities: DEFAULT_MAX_NEARBY_ENTITIES,
67 required_player_range: DEFAULT_REQUIRED_PLAYER_RANGE,
68 spawn_range: DEFAULT_SPAWN_RANGE,
69 }
70 }
71}
72
73#[derive(Debug)]
78enum NextSpawnData {
79 Potential(usize),
81 Own(SpawnData),
83}
84
85#[derive(Debug)]
86struct SpawnerState {
87 spawn_delay: i32,
88 spawn_potentials: Vec<WeightedSpawnData>,
89 next_spawn_data: Option<NextSpawnData>,
90 config: BaseSpawnerConfig,
91}
92
93#[derive(Debug, Default)]
94pub(super) struct SpawnerTickResult {
95 pub(super) state_changed: bool,
96 pub(super) next_spawn_data_changed: bool,
97}
98
99impl Default for SpawnerState {
100 fn default() -> Self {
101 Self {
102 spawn_delay: DEFAULT_SPAWN_DELAY,
103 spawn_potentials: Vec::new(),
104 next_spawn_data: None,
105 config: BaseSpawnerConfig::default(),
106 }
107 }
108}
109
110impl SpawnerState {
111 fn next_spawn_data(&self) -> Option<&SpawnData> {
112 match self.next_spawn_data.as_ref()? {
113 NextSpawnData::Potential(index) => Some(&self.spawn_potentials[*index].data),
114 NextSpawnData::Own(data) => Some(data),
115 }
116 }
117
118 fn get_or_create_next_spawn_data<R: Rng + ?Sized>(
119 &mut self,
120 random: &mut R,
121 ) -> (&mut SpawnData, bool) {
122 let created = self.next_spawn_data.is_none();
123 let next = self.next_spawn_data.get_or_insert_with(|| {
124 choose_weighted(&self.spawn_potentials, random).map_or_else(
125 || NextSpawnData::Own(SpawnData::default()),
126 NextSpawnData::Potential,
127 )
128 });
129 let data = match next {
130 NextSpawnData::Potential(index) => &mut self.spawn_potentials[*index].data,
131 NextSpawnData::Own(data) => data,
132 };
133 (data, created)
134 }
135
136 fn replace_spawn_potentials(&mut self, potentials: Vec<WeightedSpawnData>) {
137 let mut old_potentials = mem::replace(&mut self.spawn_potentials, potentials);
138 if let Some(NextSpawnData::Potential(index)) = self.next_spawn_data {
139 self.next_spawn_data = Some(NextSpawnData::Own(old_potentials.swap_remove(index).data));
140 }
141 }
142}
143
144#[derive(Debug)]
146pub struct BaseSpawner {
147 state: SyncMutex<SpawnerState>,
148}
149
150impl Default for BaseSpawner {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl BaseSpawner {
157 #[must_use]
159 pub fn new() -> Self {
160 Self {
161 state: SyncMutex::new(SpawnerState::default()),
162 }
163 }
164
165 pub fn load(&self, nbt: &BorrowedNbtCompound<'_>) {
167 let nbt: NbtCompoundView<'_, '_> = nbt.into();
168 let mut state = self.state.lock();
169 state.spawn_delay = nbt
170 .get("Delay")
171 .and_then(|tag| tag.short_value())
172 .map_or(DEFAULT_SPAWN_DELAY, i32::from);
173 if let Some(data) = nbt
174 .compound("SpawnData")
175 .and_then(|data| SpawnData::from_nbt(&data))
176 {
177 state.next_spawn_data = Some(NextSpawnData::Own(data));
178 }
179 let loaded_spawn_potentials = nbt.list("SpawnPotentials").map(|entries| {
180 entries
181 .compounds()
182 .unwrap_or_default()
183 .into_iter()
184 .filter_map(|entry| {
185 let weight = entry.get("weight")?.codec_i32()?;
186 if weight < 0 {
187 return None;
188 }
189 let data = entry.compound("data")?;
190 Some(WeightedSpawnData {
191 weight,
192 data: SpawnData::from_nbt(&data)?,
193 })
194 })
195 .collect::<Vec<_>>()
196 });
197 if let Some(potentials) = loaded_spawn_potentials {
198 state.replace_spawn_potentials(potentials);
199 } else {
200 state.replace_spawn_potentials(vec![WeightedSpawnData {
202 weight: 1,
203 data: SpawnData::default(),
204 }]);
205 if let Some(NextSpawnData::Own(data)) = state.next_spawn_data.take() {
206 state.spawn_potentials[0].data = data;
207 state.next_spawn_data = Some(NextSpawnData::Potential(0));
208 }
209 }
210 state.config.min_spawn_delay = nbt
211 .get("MinSpawnDelay")
212 .and_then(|tag| tag.int_value())
213 .unwrap_or(DEFAULT_MIN_SPAWN_DELAY);
214 state.config.max_spawn_delay = nbt
215 .get("MaxSpawnDelay")
216 .and_then(|tag| tag.int_value())
217 .unwrap_or(DEFAULT_MAX_SPAWN_DELAY);
218 state.config.spawn_count = nbt
219 .get("SpawnCount")
220 .and_then(|tag| tag.int_value())
221 .unwrap_or(DEFAULT_SPAWN_COUNT);
222 state.config.max_nearby_entities = nbt
223 .get("MaxNearbyEntities")
224 .and_then(|tag| tag.int_value())
225 .unwrap_or(DEFAULT_MAX_NEARBY_ENTITIES);
226 state.config.required_player_range = nbt
227 .get("RequiredPlayerRange")
228 .and_then(|tag| tag.int_value())
229 .unwrap_or(DEFAULT_REQUIRED_PLAYER_RANGE);
230 state.config.spawn_range = nbt
231 .get("SpawnRange")
232 .and_then(|tag| tag.int_value())
233 .unwrap_or(DEFAULT_SPAWN_RANGE);
234 }
235
236 pub fn save(&self, nbt: &mut NbtCompound) {
238 let state = self.state.lock();
239 nbt.insert("Delay", state.spawn_delay as i16);
240 nbt.insert("MinSpawnDelay", state.config.min_spawn_delay as i16);
241 nbt.insert("MaxSpawnDelay", state.config.max_spawn_delay as i16);
242 nbt.insert("SpawnCount", state.config.spawn_count as i16);
243 nbt.insert("MaxNearbyEntities", state.config.max_nearby_entities as i16);
244 nbt.insert(
245 "RequiredPlayerRange",
246 state.config.required_player_range as i16,
247 );
248 nbt.insert("SpawnRange", state.config.spawn_range as i16);
249 if let Some(next_spawn_data) = state.next_spawn_data() {
250 nbt.insert("SpawnData", next_spawn_data.to_nbt());
251 }
252 let potentials = state
253 .spawn_potentials
254 .iter()
255 .map(|entry| {
256 let mut nbt = NbtCompound::new();
257 nbt.insert("data", entry.data.to_nbt());
258 nbt.insert("weight", entry.weight);
259 nbt
260 })
261 .collect::<Vec<_>>();
262 nbt.insert("SpawnPotentials", NbtList::Compound(potentials));
263 }
264
265 #[cfg(test)]
267 pub(super) fn get_or_create_next_spawn_data(&self) -> SpawnData {
268 let mut random = rng();
269 self.get_or_create_next_spawn_data_with_rng(&mut random).0
270 }
271
272 fn get_or_create_next_spawn_data_with_rng<R: Rng + ?Sized>(
273 &self,
274 random: &mut R,
275 ) -> (SpawnData, bool) {
276 let mut state = self.state.lock();
277 let (data, created) = state.get_or_create_next_spawn_data(random);
278 (data.clone(), created)
279 }
280
281 pub fn set_entity_id(&self, entity_type: EntityTypeRef) {
283 let mut random = rng();
284 let mut state = self.state.lock();
285 let (data, _) = state.get_or_create_next_spawn_data(&mut random);
286 replace_entity_id(&mut data.entity_to_spawn, entity_type);
287 }
288
289 pub fn set_entity_data(&self, entity_data: &EntityData) {
291 let mut random = rng();
292 let mut state = self.state.lock();
293 let (data, _) = state.get_or_create_next_spawn_data(&mut random);
294 replace_entity_id(&mut data.entity_to_spawn, entity_data.entity_type());
295 merge_nbt_compounds(&mut data.entity_to_spawn, entity_data.data().as_compound());
296 }
297
298 #[must_use]
300 #[expect(
301 clippy::unused_self,
302 reason = "The event method mirrors the vanilla BaseSpawner delegation API."
303 )]
304 pub const fn on_event_triggered(&self, event: i32) -> bool {
305 event == EVENT_SPAWN
306 }
307
308 #[must_use]
311 pub fn spawn_delay(&self) -> i32 {
312 self.state.lock().spawn_delay
313 }
314
315 pub fn set_spawn_delay(&self, delay: i32) {
317 self.state.lock().spawn_delay = delay;
318 }
319
320 #[must_use]
322 pub fn config(&self) -> BaseSpawnerConfig {
323 self.state.lock().config
324 }
325
326 pub fn set_config(&self, config: BaseSpawnerConfig) {
328 self.state.lock().config = config;
329 }
330
331 #[must_use]
333 pub fn spawn_potentials(&self) -> Vec<WeightedSpawnData> {
334 self.state.lock().spawn_potentials.clone()
335 }
336
337 pub fn set_spawn_potentials(&self, potentials: Vec<WeightedSpawnData>) {
340 self.state.lock().replace_spawn_potentials(potentials);
341 }
342
343 #[must_use]
345 pub fn next_spawn_data(&self) -> Option<SpawnData> {
346 self.state.lock().next_spawn_data().cloned()
347 }
348
349 pub fn set_next_spawn_data(&self, data: SpawnData) {
351 self.state.lock().next_spawn_data = Some(NextSpawnData::Own(data));
352 }
353
354 #[expect(
355 clippy::too_many_lines,
356 reason = "The order of this method mirrors vanilla BaseSpawner.serverTick."
357 )]
358 pub(super) fn server_tick(&self, world: &Arc<World>, pos: BlockPos) -> SpawnerTickResult {
359 let center = DVec3::from(pos.get_center());
360 let config = self.config();
361 let Some(_) =
362 world.nearest_player(center, f64::from(config.required_player_range), |player| {
363 player.is_alive() && !player.is_spectator()
364 })
365 else {
366 return SpawnerTickResult::default();
367 };
368 if !world.get_game_rule(&vanilla_game_rules::SPAWNER_BLOCKS_WORK) {
369 return SpawnerTickResult::default();
370 }
371
372 let mut random = rng();
373 let mut result = SpawnerTickResult::default();
374 if self.spawn_delay() == -1 {
375 result = self.delay(world, pos, &mut random);
376 }
377
378 if self.decrement_spawn_delay() {
379 return result;
380 }
381
382 let (next_spawn_data, selected_data_changed) =
383 self.get_or_create_next_spawn_data_with_rng(&mut random);
384 result.next_spawn_data_changed |= selected_data_changed;
385 result.state_changed |= selected_data_changed;
386 let BaseSpawnerConfig {
387 spawn_count,
388 spawn_range,
389 max_nearby_entities,
390 ..
391 } = config;
392 let mut spawned_any = false;
393
394 for _ in 0..spawn_count {
395 let Some(entity_type) = entity_type_from_owned_nbt(&next_spawn_data.entity_to_spawn)
396 else {
397 return self.delay_result(world, pos, &mut random, result);
398 };
399
400 let spawn_position = configured_or_random_position(
401 &next_spawn_data.entity_to_spawn,
402 pos,
403 spawn_range,
404 &mut random,
405 );
406 let dimensions = entity_type.dimensions;
407 let spawn_box = WorldAabb::entity_box(
408 spawn_position.x,
409 spawn_position.y,
410 spawn_position.z,
411 f64::from(dimensions.half_width()),
412 f64::from(dimensions.height),
413 );
414 if has_collision(&WorldCollisionProvider::new(world), spawn_box) {
415 continue;
416 }
417
418 let spawn_block_pos = BlockPos::from(spawn_position);
419 if let Some(custom_rules) = next_spawn_data.custom_spawn_rules {
420 if entity_type.mob_category == MobCategory::Monster
421 && world.difficulty() == Difficulty::Peaceful
422 {
423 continue;
424 }
425 if !custom_rules.is_valid_position(world, spawn_block_pos) {
426 continue;
427 }
428 } else if !SpawnPlacements::check_spawner_spawn_rules(
429 entity_type,
430 world,
431 spawn_block_pos,
432 &mut random,
433 ) {
434 continue;
435 }
436
437 let Some(loaded) = load_entity_recursive_owned(
438 world,
439 &next_spawn_data.entity_to_spawn,
440 EntitySpawnReason::Spawner,
441 |entity| {
442 entity.snap_to(spawn_position, entity.rotation().0, entity.rotation().1);
443 },
444 ) else {
445 return self.delay_result(world, pos, &mut random, result);
446 };
447 let entity = &loaded.root;
448
449 let entity_class = entity.downcast_type_key();
450 let nearby_box = WorldAabb::new(
451 f64::from(pos.x()),
452 f64::from(pos.y()),
453 f64::from(pos.z()),
454 f64::from(pos.x() + 1),
455 f64::from(pos.y() + 1),
456 f64::from(pos.z() + 1),
457 )
458 .inflate(f64::from(spawn_range));
459 let nearby_count = world
460 .get_entities_in_aabb_matching(&nearby_box, |candidate| {
461 !candidate.is_spectator() && candidate.downcast_type_key() == entity_class
462 })
463 .len();
464 if nearby_count as i32 >= max_nearby_entities {
465 return self.delay_result(world, pos, &mut random, result);
466 }
467
468 let yaw = random.random::<f32>() * DEGREE_360;
469 entity.snap_to(entity.position(), yaw, 0.0);
470 if let Some(mob) = entity.as_mob() {
471 if (next_spawn_data.custom_spawn_rules.is_none()
472 && !mob.check_spawn_rules(world, EntitySpawnReason::Spawner))
473 || !mob.check_spawn_obstruction(world)
474 {
475 continue;
476 }
477
478 if next_spawn_data.entity_to_spawn.len() == 1 {
479 let _ = mob.finalize_spawn(world, EntitySpawnReason::Spawner, None);
480 }
481 if let Some(equipment) = &next_spawn_data.equipment {
482 equip_from_table(mob, equipment);
483 }
484 }
485
486 if world
487 .try_add_fresh_entity_with_passengers(Arc::clone(entity))
488 .is_err()
489 {
490 return self.delay_result(world, pos, &mut random, result);
491 }
492
493 world.level_event(level_events::PARTICLES_MOBBLOCK_SPAWN, pos, 0, None);
494 world.game_event(
495 &vanilla_game_events::ENTITY_PLACE,
496 spawn_block_pos,
497 &crate::world::game_event::GameEventContext::new(Some(entity.as_ref()), None),
498 );
499 if entity.as_mob().is_some() {
500 entity.broadcast_entity_event(EntityStatus::SilverfishMergeAnim);
501 }
502 spawned_any = true;
503 }
504
505 if spawned_any {
506 result = self.delay_result(world, pos, &mut random, result);
507 }
508 result
509 }
510
511 fn delay_result<R: Rng + ?Sized>(
512 &self,
513 world: &Arc<World>,
514 pos: BlockPos,
515 random: &mut R,
516 mut result: SpawnerTickResult,
517 ) -> SpawnerTickResult {
518 let delay_result = self.delay(world, pos, random);
519 result.state_changed |= delay_result.state_changed;
520 result.next_spawn_data_changed |= delay_result.next_spawn_data_changed;
521 result
522 }
523
524 fn delay<R: Rng + ?Sized>(
525 &self,
526 world: &Arc<World>,
527 pos: BlockPos,
528 random: &mut R,
529 ) -> SpawnerTickResult {
530 let mut result = SpawnerTickResult {
531 state_changed: true,
532 next_spawn_data_changed: false,
533 };
534 {
535 let mut state = self.state.lock();
536 let BaseSpawnerConfig {
537 min_spawn_delay,
538 max_spawn_delay,
539 ..
540 } = state.config;
541 let delay = max_spawn_delay.saturating_sub(min_spawn_delay);
542 state.spawn_delay = if delay <= 0 {
543 min_spawn_delay
544 } else {
545 min_spawn_delay + random.random_range(0..delay)
546 };
547 if let Some(index) = choose_weighted(&state.spawn_potentials, random) {
548 result.next_spawn_data_changed =
549 state.next_spawn_data() != Some(&state.spawn_potentials[index].data);
550 state.next_spawn_data = Some(NextSpawnData::Potential(index));
551 }
552 }
553 world.block_event(pos, &vanilla_blocks::SPAWNER, EVENT_SPAWN, 0);
554 result
555 }
556
557 fn decrement_spawn_delay(&self) -> bool {
558 let mut state = self.state.lock();
559 if state.spawn_delay > 0 {
560 state.spawn_delay -= 1;
561 true
562 } else {
563 false
564 }
565 }
566}
567
568pub(super) fn choose_weighted<R: Rng + ?Sized>(
570 entries: &[WeightedSpawnData],
571 random: &mut R,
572) -> Option<usize> {
573 let total_weight = entries
574 .iter()
575 .map(|entry| i64::from(entry.weight))
576 .sum::<i64>();
577 if total_weight == 0 {
578 return None;
579 }
580
581 let mut selection = random.random_range(0..total_weight);
582 for (index, entry) in entries.iter().enumerate() {
583 let weight = i64::from(entry.weight);
584 if selection < weight {
585 return Some(index);
586 }
587 selection -= weight;
588 }
589 None
590}
591
592fn entity_type_from_owned_nbt(entity: &NbtCompound) -> Option<EntityTypeRef> {
593 let id = entity.string("id")?.to_str().parse::<Identifier>().ok()?;
594 REGISTRY.entity_types.by_key(&id)
595}
596
597fn configured_or_random_position<R: Rng + ?Sized>(
598 entity: &NbtCompound,
599 spawner_pos: BlockPos,
600 spawn_range: i32,
601 random: &mut R,
602) -> DVec3 {
603 entity
604 .get("Pos")
605 .and_then(nbt_dvec3)
606 .filter(|position| position.is_finite())
607 .unwrap_or_else(|| random_spawn_position(spawner_pos, spawn_range, random))
608}
609
610fn random_spawn_position<R: Rng + ?Sized>(
611 spawner_pos: BlockPos,
612 spawn_range: i32,
613 random: &mut R,
614) -> DVec3 {
615 DVec3::new(
616 f64::from(spawner_pos.x())
617 + (random.random::<f64>() - random.random::<f64>()) * f64::from(spawn_range)
618 + 0.5,
619 f64::from(spawner_pos.y() + random.random_range(-1..=1)),
620 f64::from(spawner_pos.z())
621 + (random.random::<f64>() - random.random::<f64>()) * f64::from(spawn_range)
622 + 0.5,
623 )
624}
625
626fn equip_from_table(mob: &dyn Mob, equipment: &EquipmentTable) {
627 let Some(table): Option<LootTableRef> = REGISTRY.loot_tables.by_key(&equipment.loot_table)
628 else {
629 return;
630 };
631
632 let mut random = rng();
633 let mob_equipment = living_loot_equipment(mob);
634 let entity_ref = entity_loot_ref(mob.as_entity_event_source(), Some(&mob_equipment));
635 let mut context = LootContext::new(&mut random)
636 .with_origin(mob.position().x, mob.position().y, mob.position().z)
637 .with_this_entity(entity_ref);
638 let mut inserted = [false; EquipmentSlot::ALL.len()];
639 for mut item in table.get_random_items(&mut context) {
640 if item.is_empty() {
641 continue;
642 }
643 let slot = item
644 .get_equippable_slot()
645 .unwrap_or(EquipmentSlot::MainHand);
646 if inserted[slot.index()] {
647 continue;
648 }
649
650 let equipped = slot.limit(&mut item);
651 if equipped.is_empty() {
652 continue;
653 }
654 mob.living_base().equipment().lock().set(slot, equipped);
655 if let Some(chance) = equipment.drop_chance(slot) {
656 mob.set_equipment_drop_chance(slot, chance);
657 }
658 inserted[slot.index()] = true;
659 }
660}
661
662fn replace_entity_id(entity: &mut NbtCompound, entity_type: EntityTypeRef) {
663 while entity.remove("id").is_some() {}
664 entity.insert("id", entity_type.key.to_string());
665}