Pumpkin/pumpkin/src/world/natural_spawner.rs
Alexander Medvedev 60bfdf62db Revert a55c42c83
2026-05-10 12:13:33 +02:00

997 lines
32 KiB
Rust

use crate::entity::EntityBase;
use crate::entity::r#type::{check_spawn_rules, from_type};
use crate::world::World;
use arc_swap::ArcSwap;
use pumpkin_data::biome::Spawner;
use pumpkin_data::chunk::Biome;
use pumpkin_data::entity::{EntityType, MobCategory, SpawnLocation};
use pumpkin_data::tag::Block::MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE;
use pumpkin_data::tag::Fluid::{MINECRAFT_LAVA, MINECRAFT_WATER};
use pumpkin_data::tag::Taggable;
use pumpkin_data::tag::WorldgenBiome::MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS;
use pumpkin_data::{Block, BlockDirection, BlockState};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::GameMode;
use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions};
use pumpkin_util::math::get_section_cord;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::random::xoroshiro128::Xoroshiro;
use pumpkin_util::random::{RandomImpl, get_seed};
use pumpkin_world::chunk::io::Dirtiable;
use pumpkin_world::chunk::{ChunkData, ChunkHeightmapType};
use pumpkin_world::generation::proto_chunk::GenerationCache;
use rand::seq::IndexedRandom;
use rand::{RngExt, rng};
use std::fmt;
use std::sync::Arc;
use uuid::Uuid;
const MAGIC_NUMBER: i32 = 17 * 17;
use dashmap::DashMap;
use std::sync::atomic::{AtomicI32, Ordering::Relaxed};
pub struct MobCounts([AtomicI32; 8]);
impl Default for MobCounts {
fn default() -> Self {
Self(std::array::from_fn(|_| AtomicI32::new(0)))
}
}
impl fmt::Debug for MobCounts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|a| a.load(Relaxed)))
.finish()
}
}
impl Clone for MobCounts {
fn clone(&self) -> Self {
Self(std::array::from_fn(|i| {
AtomicI32::new(self.0[i].load(Relaxed))
}))
}
}
impl MobCounts {
#[inline]
pub fn add(&self, category: &'static MobCategory) {
self.0[category.id].fetch_add(1, Relaxed);
}
#[inline]
pub fn remove(&self, category: &'static MobCategory) {
self.0[category.id].fetch_sub(1, Relaxed);
}
#[inline]
pub fn can_spawn(&self, category: &'static MobCategory) -> bool {
self.0[category.id].load(Relaxed) < category.max
}
}
pub struct LocalMobCapCalculator {
player_mob_counts: DashMap<i32, MobCounts>,
players_near_chunk: DashMap<Vector2<i32>, Vec<i32>>,
}
impl Clone for LocalMobCapCalculator {
fn clone(&self) -> Self {
let player_mob_counts = DashMap::new();
for r in &self.player_mob_counts {
player_mob_counts.insert(*r.key(), r.value().clone());
}
let players_near_chunk = DashMap::new();
for r in &self.players_near_chunk {
players_near_chunk.insert(*r.key(), r.value().clone());
}
Self {
player_mob_counts,
players_near_chunk,
}
}
}
impl Default for LocalMobCapCalculator {
fn default() -> Self {
Self {
player_mob_counts: DashMap::new(),
players_near_chunk: DashMap::new(),
}
}
}
impl fmt::Debug for LocalMobCapCalculator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("LocalMobCapCalculator")
.field("world", &"<skipped>")
.finish()
}
}
impl LocalMobCapCalculator {
const fn calc_distance(chunk_pos: Vector2<i32>, player_pos: &Vector3<f64>) -> f64 {
let dx = ((chunk_pos.x << 4) + 8) as f64 - player_pos.x;
let dy = ((chunk_pos.y << 4) + 8) as f64 - player_pos.z;
dx * dx + dy * dy
}
fn get_players_near(&self, world: &World, chunk_pos: Vector2<i32>) -> Vec<i32> {
if let Some(players) = self.players_near_chunk.get(&chunk_pos) {
return players.value().clone();
}
let mut players = Vec::new();
for player in world.players.load().iter() {
if player.gamemode.load() == GameMode::Spectator {
continue;
}
if Self::calc_distance(chunk_pos, &player.position()) < 16384. {
players.push(player.entity_id());
}
}
self.players_near_chunk.insert(chunk_pos, players.clone());
players
}
pub fn add_mob(&self, chunk_pos: Vector2<i32>, world: &World, category: &'static MobCategory) {
let players = self.get_players_near(world, chunk_pos);
for player in players {
self.player_mob_counts
.entry(player)
.or_default()
.add(category);
}
}
pub fn remove_mob(
&self,
chunk_pos: Vector2<i32>,
world: &World,
category: &'static MobCategory,
) {
let players = self.get_players_near(world, chunk_pos);
for player in players {
if let Some(count) = self.player_mob_counts.get(&player) {
count.remove(category);
}
}
}
pub fn can_spawn(
&self,
category: &'static MobCategory,
world: &World,
chunk_pos: Vector2<i32>,
) -> bool {
let players = self.get_players_near(world, chunk_pos);
for player in players {
if let Some(count) = self.player_mob_counts.get(&player) {
if count.can_spawn(category) {
return true;
}
} else {
return true;
}
}
false
}
}
#[derive(Debug, Clone)]
struct PointCharge(Vector3<f64>, f64);
impl PointCharge {
fn get_potential_change(&self, pos: &BlockPos) -> f64 {
let dst = self.0.sub(&pos.to_f64()).length();
self.1 / dst
}
}
#[derive(Default, Debug)]
struct PotentialCalculator(std::sync::Mutex<Vec<PointCharge>>);
impl Clone for PotentialCalculator {
fn clone(&self) -> Self {
Self(std::sync::Mutex::new(self.0.lock().unwrap().clone()))
}
}
impl PotentialCalculator {
pub fn add_charge(&self, pos: &BlockPos, charge: f64) {
if charge != 0. {
self.0
.lock()
.unwrap()
.push(PointCharge(pos.to_f64(), charge));
}
}
pub fn remove_charge(&self, pos: &BlockPos, charge: f64) {
if charge != 0. {
let mut charges = self.0.lock().unwrap();
let pos_f64 = pos.to_f64();
if let Some(idx) = charges.iter().position(|c| c.0 == pos_f64 && c.1 == charge) {
charges.swap_remove(idx);
}
}
}
pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 {
if charge == 0. {
return 0.;
}
let mut sum: f64 = 0.;
let charges = self.0.lock().unwrap();
for i in charges.iter() {
sum += i.get_potential_change(pos);
}
sum * charge
}
}
use crossbeam::atomic::AtomicCell;
pub struct SpawnState {
spawnable_chunk_count: i32,
pub mob_category_counts: MobCounts,
spawn_potential: PotentialCalculator,
local_mob_cap_calculator: LocalMobCapCalculator,
// unmodifiable_mob_category_counts: MobCounts, seems only for debug
last_checked: AtomicCell<Option<(BlockPos, &'static EntityType, f64)>>,
}
impl Clone for SpawnState {
fn clone(&self) -> Self {
Self {
spawnable_chunk_count: self.spawnable_chunk_count,
mob_category_counts: self.mob_category_counts.clone(),
spawn_potential: self.spawn_potential.clone(),
local_mob_cap_calculator: self.local_mob_cap_calculator.clone(),
last_checked: AtomicCell::new(self.last_checked.load()),
}
}
}
impl fmt::Debug for SpawnState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("SpawnState")
.field("spawnable_chunk_count", &self.spawnable_chunk_count)
.field("mob_category_counts", &self.mob_category_counts)
.field("spawn_potential", &self.spawn_potential)
.field("local_mob_cap_calculator", &self.local_mob_cap_calculator)
.field("last_checked", &self.last_checked)
.finish()
}
}
impl SpawnState {
#[must_use]
pub fn empty() -> Self {
Self {
spawnable_chunk_count: 0,
mob_category_counts: MobCounts::default(),
spawn_potential: PotentialCalculator::default(),
local_mob_cap_calculator: LocalMobCapCalculator::default(),
last_checked: AtomicCell::new(None),
}
}
pub const fn set_spawnable_chunk_count(&mut self, count: i32) {
self.spawnable_chunk_count = count;
}
pub fn add_entity(&self, world: &World, entity: &dyn EntityBase) {
let base_entity = entity.get_entity();
let entity_type = base_entity.entity_type;
if !entity_type.mob || entity_type.category == &MobCategory::MISC {
return;
}
let entity_pos = base_entity.block_pos.load();
let biome = base_entity.current_biome.load();
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
self.spawn_potential.add_charge(&entity_pos, cost.charge);
}
if entity_type.mob {
self.local_mob_cap_calculator.add_mob(
base_entity.chunk_pos.load(),
world,
entity_type.category,
);
self.mob_category_counts.add(entity_type.category);
}
}
pub fn remove_entity(&self, world: &World, entity: &dyn EntityBase) {
let base_entity = entity.get_entity();
let entity_type = base_entity.entity_type;
if !entity_type.mob || entity_type.category == &MobCategory::MISC {
return;
}
let entity_pos = base_entity.block_pos.load();
let biome = base_entity.current_biome.load();
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
self.spawn_potential.remove_charge(&entity_pos, cost.charge);
}
if entity_type.mob {
self.local_mob_cap_calculator.remove_mob(
base_entity.chunk_pos.load(),
world,
entity_type.category,
);
self.mob_category_counts.remove(entity_type.category);
}
}
pub fn new(
chunk_count: i32,
entities: &ArcSwap<Vec<Arc<dyn EntityBase>>>,
world: &Arc<World>,
) -> Self {
let potential = PotentialCalculator::default();
let local_mob_cap = LocalMobCapCalculator::default();
let counter = MobCounts::default();
let active_chunks = world.active_chunks.load();
for entity in entities.load().iter() {
let entity = entity.get_entity();
let entity_type = entity.entity_type;
if !entity_type.mob || entity_type.category == &MobCategory::MISC {
// TODO (mob.isPersistenceRequired() || mob.requiresCustomPersistence())
continue;
}
let chunk_pos = entity.chunk_pos.load();
if !active_chunks.contains(&chunk_pos) {
continue;
}
let entity_pos = entity.block_pos.load();
let biome = entity.current_biome.load();
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
potential.add_charge(&entity_pos, cost.charge);
}
if entity_type.mob {
local_mob_cap.add_mob(chunk_pos, world, entity_type.category);
}
counter.add(entity_type.category);
}
Self {
spawnable_chunk_count: chunk_count,
mob_category_counts: counter,
spawn_potential: potential,
local_mob_cap_calculator: local_mob_cap,
last_checked: AtomicCell::new(None),
}
}
#[inline]
pub fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool {
self.mob_category_counts.0[category.id].load(Relaxed)
< category.max * self.spawnable_chunk_count / MAGIC_NUMBER
}
pub fn can_spawn_for_category_local(
&self,
world: &Arc<World>,
category: &'static MobCategory,
chunk_pos: Vector2<i32>,
) -> bool {
self.local_mob_cap_calculator
.can_spawn(category, world, chunk_pos)
}
pub async fn can_spawn(
&self,
entity_type: &'static EntityType,
pos: &BlockPos,
world: &Arc<World>,
) -> bool {
// TODO get biome
let biome = world.level.get_rough_biome(pos).await;
biome
.spawn_costs
.get(entity_type.resource_name)
.map_or_else(
|| {
self.last_checked.store(Some((*pos, entity_type, 0.)));
true
},
|cost| {
self.last_checked
.store(Some((*pos, entity_type, cost.charge)));
self.spawn_potential
.get_potential_energy_change(pos, cost.charge)
<= cost.energy_budget
},
)
}
pub async fn after_spawn(
&self,
entity_type: &'static EntityType,
pos: &BlockPos,
world: &Arc<World>,
) {
let charge = if let Some((l_pos, l_type, l_charge)) = self.last_checked.load()
&& l_pos.eq(pos)
&& l_type == entity_type
{
Some(l_charge)
} else {
None
};
let charge = if let Some(charge) = charge {
charge
} else {
// TODO get biome
let biome = world.level.get_rough_biome(pos).await;
biome
.spawn_costs
.get(entity_type.resource_name)
.map_or(0., |cost| cost.charge)
};
self.spawn_potential.add_charge(pos, charge);
self.mob_category_counts.add(entity_type.category);
self.local_mob_cap_calculator.add_mob(
Vector2::<i32>::new(get_section_cord(pos.0.x), get_section_cord(pos.0.z)),
world,
entity_type.category,
);
}
}
#[must_use]
pub fn get_filtered_spawning_categories(
state: &SpawnState,
spawn_friendlies: bool,
spawn_enemies: bool,
spawn_passives: bool,
) -> Vec<&'static MobCategory> {
let mut ret = Vec::with_capacity(MobCategory::SPAWNING_CATEGORIES.len());
for category in MobCategory::SPAWNING_CATEGORIES {
let is_type_allowed = if category.is_friendly {
spawn_friendlies
} else {
spawn_enemies
};
if !is_type_allowed {
continue;
}
if category.is_persistent && !spawn_passives {
continue;
}
if state.can_spawn_for_category_global(category) {
ret.push(category);
}
}
ret
}
pub async fn spawn_for_chunk(
world: &Arc<World>,
chunk_pos: Vector2<i32>,
chunk: &Arc<ChunkData>,
spawn_state: &SpawnState,
spawn_list: &Vec<&'static MobCategory>,
) {
// debug!("spawn for chunk {:?}", chunk_pos);
for category in spawn_list {
if spawn_state.can_spawn_for_category_local(world, category, chunk_pos) {
let random_pos = get_random_pos_within(world.min_y, &chunk_pos, chunk);
if random_pos.0.y > world.min_y {
spawn_category_for_position(category, world, random_pos, &chunk_pos, spawn_state)
.await;
}
}
}
}
pub fn get_random_pos_within(
min_y: i32,
chunk_pos: &Vector2<i32>,
chunk: &Arc<ChunkData>,
) -> BlockPos {
let mut rng = Xoroshiro::from_seed(get_seed());
let x = (chunk_pos.x << 4) + rng.next_bounded_i32(16);
let z = (chunk_pos.y << 4) + rng.next_bounded_i32(16);
let temp_y = chunk.heightmap.lock().unwrap().get(
ChunkHeightmapType::WorldSurface,
x,
z,
chunk.section.min_y,
) + 1;
let y = rng.next_inbetween_i32(min_y, temp_y);
BlockPos::new(x, y, z)
}
pub async fn spawn_mobs_for_chunk_generation(
world: &Arc<World>,
cache: &mut dyn GenerationCache,
biome: &'static Biome,
chunk_x: i32,
chunk_z: i32,
) {
let mob_settings = &biome.spawners;
let creatures = &mob_settings.creature;
if creatures.is_empty() {
return;
}
let xo = chunk_x << 4;
let zo = chunk_z << 4;
while rand::random::<f32>() < biome.creature_spawn_probability {
let Some(spawner_data) = creatures.choose(&mut rand::rng()) else {
continue;
};
let count = spawner_data.min_count
+ rand::random_range(0..(1 + spawner_data.max_count - spawner_data.min_count).max(1));
let entity_type = EntityType::from_name(
spawner_data
.r#type
.strip_prefix("minecraft:")
.unwrap_or(spawner_data.r#type),
)
.unwrap();
let mut x = xo + rand::random_range(0..16);
let mut z = zo + rand::random_range(0..16);
let start_x = x;
let start_z = z;
for _ in 0..count {
let mut success = false;
// Try 4 times to find a valid spot in the immediate area
for _ in 0..4 {
if success {
break;
}
let pos = get_top_non_colliding_pos(world, cache, entity_type, x, z);
if is_spawn_position_ok_cache(cache, &pos, entity_type) {
let spawn_pos_f64 = Vector3::new(
f64::from(pos.0.x) + 0.5,
f64::from(pos.0.y),
f64::from(pos.0.z) + 0.5,
);
let entity = from_type(entity_type, spawn_pos_f64, world, Uuid::new_v4()).await;
entity
.get_entity()
.set_rotation(rand::random::<f32>() * 360., 0.);
world.spawn_entity(entity).await;
success = true;
}
// Random jitter for the next mob in the group
x += rand::random_range(0..5) - rand::random_range(0..5);
z += rand::random_range(0..5) - rand::random_range(0..5);
// Keep group within the chunk bounds
if x < xo || x >= xo + 16 || z < zo || z >= zo + 16 {
x = start_x;
z = start_z;
}
}
}
}
}
pub fn get_top_non_colliding_pos(
world: &World,
cache: &dyn GenerationCache,
entity_type: &'static EntityType,
x: i32,
z: i32,
) -> BlockPos {
let mut y = cache.get_top_y(&entity_type.spawn_restriction.heightmap, x, z);
let mut pos_vec = Vector3::new(x, y, z);
let min_y = world.min_y;
if world.dimension.has_ceiling {
loop {
y -= 1;
pos_vec.y = y;
// Use UFCS to avoid the ambiguity error from earlier
if GenerationCache::get_block_state(cache, &pos_vec)
.to_state()
.is_air()
|| y <= min_y
{
break;
}
}
loop {
y -= 1;
pos_vec.y = y;
if !GenerationCache::get_block_state(cache, &pos_vec)
.to_state()
.is_air()
|| y <= min_y
{
break;
}
}
}
let pos = BlockPos::new(x, y, z);
adjust_spawn_position_cache(cache, pos, entity_type)
}
pub async fn spawn_category_for_position(
category: &'static MobCategory,
world: &Arc<World>,
pos: BlockPos,
chunk_pos: &Vector2<i32>,
spawn_state: &SpawnState,
) {
let mut batch_buffer = vec![];
let mut spawn_cluster_size = 0;
let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect();
'group_loop: for _ in 0..3 {
let mut new_x = pos.0.x;
let mut new_z = pos.0.z;
let mut random_group_size = (rng().random::<f32>() * 4.).ceil() as i32;
let mut inc = 0;
let mut current_spawner = None;
'spawn_loop: while inc < random_group_size {
new_x += rng().random_range(0..6) - rng().random_range(0..6);
new_z += rng().random_range(0..6) - rng().random_range(0..6);
let mut new_pos = BlockPos::new(new_x, pos.0.y, new_z);
if current_spawner.is_none() {
let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else {
break 'spawn_loop;
};
current_spawner = Some(spawner);
random_group_size = rng().random_range(spawner.min_count..=spawner.max_count);
}
let spawner = current_spawner.unwrap();
let entity_type =
&EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap();
new_pos = adjust_spawn_position(world, new_pos, entity_type).await;
let spawn_pos_f64 = Vector3::new(
f64::from(new_pos.0.x) + 0.5,
f64::from(new_pos.0.y),
f64::from(new_pos.0.z) + 0.5,
);
let player_distance = get_nearest_player(&spawn_pos_f64, &player_positions);
if !is_right_distance_to_player_and_spawn_point(&new_pos, player_distance, chunk_pos) {
inc += 1;
continue;
}
if !is_valid_spawn_position_for_type(
world,
&new_pos,
category,
entity_type,
player_distance,
)
.await
{
inc += 1;
continue;
}
if !spawn_state.can_spawn(entity_type, &new_pos, world).await {
inc += 1;
continue;
}
let entity = from_type(entity_type, spawn_pos_f64, world, Uuid::new_v4()).await;
entity
.get_entity()
.set_rotation(rng().random::<f32>() * 360., 0.);
spawn_cluster_size += 1;
batch_buffer.push(entity);
spawn_state.after_spawn(entity_type, &new_pos, world).await;
if spawn_cluster_size >= entity_type.limit_per_chunk {
break 'group_loop;
}
inc += 1;
}
}
// Spawn in batch
if !batch_buffer.is_empty() {
let mut prepared_data = Vec::with_capacity(batch_buffer.len());
for entity in &batch_buffer {
entity.init_data_tracker().await;
let base_entity = entity.get_entity();
let packet = base_entity.create_spawn_packet();
let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await;
// Keep the entity reference here so we don't have to "find" it later
prepared_data.push((base_entity.entity_uuid, nbt, packet, entity.clone()));
}
let chunk_handle = world.level.get_entity_chunk(*chunk_pos).await;
let mut data = chunk_handle.data.lock().await;
for (uuid, nbt, _, _) in &prepared_data {
data.insert(*uuid, nbt.clone());
}
drop(data);
chunk_handle.mark_dirty(true);
world.entities.rcu(|current_entities| {
let mut new_entities = (**current_entities).clone();
for (uuid, _, _, entity_ref) in &prepared_data {
if !new_entities
.iter()
.any(|e| e.get_entity().entity_uuid == *uuid)
{
new_entities.push(entity_ref.clone());
}
}
new_entities
});
for (_, _, packet, _) in prepared_data {
world.broadcast_packet_all(&packet).await;
}
}
}
#[must_use]
pub fn get_nearest_player(pos: &Vector3<f64>, player_positions: &[Vector3<f64>]) -> f64 {
let mut min_dst_sq = f64::MAX;
for player_pos in player_positions {
let cur_dst_sq = player_pos.squared_distance_to_vec(pos);
if cur_dst_sq < min_dst_sq {
min_dst_sq = cur_dst_sq;
}
}
min_dst_sq
}
#[must_use]
pub fn is_right_distance_to_player_and_spawn_point(
pos: &BlockPos,
distance: f64,
chunk_pos: &Vector2<i32>,
) -> bool {
if distance <= 24. * 24. {
return false;
}
// TODO getSharedSpawnPos/WorldSpawnPoint
if pos.to_centered_f64().squared_distance_to(0., 0., 0.) <= 24. * 24. {
return false;
}
#[expect(clippy::nonminimal_bool)]
{
chunk_pos == &Vector2::new(get_section_cord(pos.0.x), get_section_cord(pos.0.z)) || false // TODO canSpawnEntitiesInChunk(ChunkPos chunkPos)
}
}
#[must_use]
pub async fn get_random_spawn_mob_at(
world: &Arc<World>,
category: &'static MobCategory,
block_pos: &BlockPos,
) -> Option<&'static Spawner> {
// TODO Holder<Biome> holder = level.getBiome(pos);
let biome = world.level.get_rough_biome(block_pos).await;
if category == &MobCategory::WATER_AMBIENT
&& biome.has_tag(&MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS)
&& rng().random::<f32>() < 0.98f32
{
None
} else {
// TODO isInNetherFortressBounds(pos, level, cetagory, structureManager) then NetherFortressStructure.FORTRESS_ENEMIES
// TODO structureManager.getAllStructuresAt(pos); ChunkGenerator::getMobsAt
match category.id {
id if id == MobCategory::MONSTER.id => biome.spawners.monster,
id if id == MobCategory::CREATURE.id => biome.spawners.creature,
id if id == MobCategory::AMBIENT.id => biome.spawners.ambient,
id if id == MobCategory::AXOLOTLS.id => biome.spawners.axolotls,
id if id == MobCategory::UNDERGROUND_WATER_CREATURE.id => {
biome.spawners.underground_water_creature
}
id if id == MobCategory::WATER_CREATURE.id => biome.spawners.water_creature,
id if id == MobCategory::WATER_AMBIENT.id => biome.spawners.water_ambient,
id if id == MobCategory::MISC.id => biome.spawners.misc,
_ => panic!(),
}
.choose(&mut rng())
}
}
pub async fn is_valid_spawn_position_for_type(
world: &Arc<World>,
block_pos: &BlockPos,
category: &'static MobCategory,
entity_type: &'static EntityType,
distance: f64,
) -> bool {
// TODO !SpawnPlacements.checkSpawnRules(entityType, level, EntitySpawnReason.NATURAL, pos, level.random)
if category == &MobCategory::MISC {
return false;
}
if !entity_type.can_spawn_far_from_player
&& distance
> f64::from(entity_type.category.despawn_distance)
* f64::from(entity_type.category.despawn_distance)
{
return false;
}
if !entity_type.summonable {
return false;
}
if !is_spawn_position_ok(world, block_pos, entity_type).await {
return false;
}
if !check_spawn_rules(entity_type, world, block_pos).await {
return false;
}
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes
if !world
.is_space_empty(BoundingBox::new_from_pos(
f64::from(block_pos.0.x) + 0.5,
f64::from(block_pos.0.y),
f64::from(block_pos.0.z) + 0.5,
&EntityDimensions {
width: entity_type.dimension[0],
height: entity_type.dimension[1],
eye_height: entity_type.eye_height,
},
))
.await
{
return false;
}
true
}
pub async fn is_spawn_position_ok(
world: &Arc<World>,
block_pos: &BlockPos,
entity_type: &'static EntityType,
) -> bool {
match entity_type.spawn_restriction.location {
SpawnLocation::InLava => world.get_fluid(block_pos).await.has_tag(&MINECRAFT_LAVA),
SpawnLocation::InWater => {
// TODO !level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)
let above_state = world.get_block_state(&block_pos.up()).await;
world.get_fluid(block_pos).await.has_tag(&MINECRAFT_WATER)
&& !above_state.is_full_cube()
}
SpawnLocation::OnGround => {
let down = world.get_block_state(&block_pos.down()).await;
let up = world.get_block_state(&block_pos.up()).await;
let cur = world.get_block_state(block_pos).await;
// TODO: blockState.allowsSpawning
let is_valid_spawn_below =
down.is_side_solid(BlockDirection::Up) && down.luminance < 14;
if is_valid_spawn_below {
is_valid_empty_spawn_block(cur) && is_valid_empty_spawn_block(up)
} else {
false
}
}
SpawnLocation::Unrestricted => true,
}
}
/// Cache-based version of `is_spawn_position_ok` used during world generation.
pub fn is_spawn_position_ok_cache(
cache: &dyn GenerationCache,
block_pos: &BlockPos,
entity_type: &'static EntityType,
) -> bool {
let pos_vec = block_pos.0;
let state = GenerationCache::get_block_state(cache, &pos_vec).to_state();
match entity_type.spawn_restriction.location {
SpawnLocation::InLava => {
// During generation, we check the block state's liquid property and tag
state.is_liquid() && Block::from_state_id(state.id).has_tag(&MINECRAFT_LAVA)
}
SpawnLocation::InWater => {
let above_pos = block_pos.up().0;
let above_state = GenerationCache::get_block_state(cache, &above_pos).to_state();
state.is_liquid()
&& Block::from_state_id(state.id).has_tag(&MINECRAFT_WATER)
&& !above_state.is_full_cube()
}
SpawnLocation::OnGround => {
let down_pos = block_pos.down().0;
let up_pos = block_pos.up().0;
let down = GenerationCache::get_block_state(cache, &down_pos).to_state();
let up = GenerationCache::get_block_state(cache, &up_pos).to_state();
// Logic: solid surface below and low enough light level (if applicable in generation)
let is_valid_spawn_below =
down.is_side_solid(BlockDirection::Up) && down.luminance < 14;
if is_valid_spawn_below {
is_valid_empty_spawn_block(state) && is_valid_empty_spawn_block(up)
} else {
false
}
}
SpawnLocation::Unrestricted => true,
}
}
/// Cache-based version of `adjust_spawn_position` used during world generation.
pub fn adjust_spawn_position_cache(
cache: &dyn GenerationCache,
pos: BlockPos,
entity_type: &'static EntityType,
) -> BlockPos {
if matches!(
entity_type.spawn_restriction.location,
SpawnLocation::OnGround
) {
let below = pos.down();
let state = GenerationCache::get_block_state(cache, &below.0).to_state();
if !state.is_full_cube() && !state.is_liquid() {
return below;
}
}
pos
}
pub async fn adjust_spawn_position(
world: &World,
pos: BlockPos,
entity_type: &'static EntityType,
) -> BlockPos {
if matches!(
entity_type.spawn_restriction.location,
SpawnLocation::OnGround
) {
let below = pos.down();
let state = world.get_block_state(&below).await;
// Approximation of isPathfindable(LAND)
if !state.is_full_cube() && !state.is_liquid() {
return below;
}
}
pos
}
#[must_use]
pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool {
if state.is_full_cube() {
return false;
}
// if state.is_signal_source() {
// return false;
// }
if state.is_liquid() {
return false;
}
if Block::from_state_id(state.id).has_tag(&MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE) {
return false;
}
// TODO: !entityType.isBlockDangerous(blockState)
// (e.g., preventing spawns inside Sweet Berry Bushes, Wither Roses, or Fire)
true
}