Add Entity spawning on Chunk generation (#1054)

* Support Spawn

* Update natural_spawner.rs

* fix distance

* fix error

* fix

* use heigh map

* Update living.rs

* impl get_rough_biome and merge

* fix dropper and entity disappear

* merge

---------

Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
spr-equinox
2025-08-01 00:38:06 +08:00
committed by GitHub
parent 7468f2cf76
commit 5084ccd43f
31 changed files with 1830 additions and 255 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ pub struct Biome {
features: Vec<Vec<String>>,
creature_spawn_probability: Option<f32>,
spawners: SpawnGroups,
spawn_costs: HashMap<String, SpawnCosts>,
pub id: u8,
}
@@ -31,17 +32,43 @@ struct SpawnGroups {
water_creature: Vec<Spawner>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Hash, PartialEq, Eq)]
struct Spawner {
r#type: String,
minCount: i32,
maxCount: i32,
}
impl Spawner {
pub fn to_tokens(&self) -> TokenStream {
let r#type = &self.r#type;
let min_count = &self.minCount;
let max_count = &self.maxCount;
quote! {
Spawner {
r#type: #r#type,
min_count: #min_count,
max_count: #max_count,
}
}
}
}
#[derive(Deserialize, PartialEq)]
struct SpawnCosts {
energy_budget: f64,
charge: f64,
}
impl SpawnCosts {
pub fn to_tokens(&self) -> TokenStream {
let energy_budget = &self.energy_budget;
let charge = &self.charge;
quote! {
SpawnCosts {
energy_budget: #energy_budget,
charge: #charge,
}
}
}
@@ -217,6 +244,18 @@ pub(crate) fn build() -> TokenStream {
}
};
let spawn_costs: Vec<_> = biome
.spawn_costs
.iter()
.map(|(name, cost)| {
let cost_token = cost.to_tokens();
let entity_type = name.strip_prefix("minecraft:").unwrap();
quote! {
#entity_type => #cost_token
}
})
.collect();
let temperature_modifier = match temperature_modifier {
TemperatureModifier::Frozen => quote! { TemperatureModifier::Frozen },
TemperatureModifier::None => quote! { TemperatureModifier::None },
@@ -225,17 +264,20 @@ pub(crate) fn build() -> TokenStream {
variants.extend([quote! {
pub const #format_name: Biome = Biome {
id: #index,
registry_id: #name,
weather: Weather::new(
#has_precipitation,
#temperature,
#temperature_modifier,
#downfall
),
features: &[#(&[#(#features),*]),*],
creature_spawn_probability: #creature_spawn_probability,
spawners: #spawners,
id: #index,
registry_id: #name,
weather: Weather::new(
#has_precipitation,
#temperature,
#temperature_modifier,
#downfall
),
features: &[#(&[#(#features),*]),*],
creature_spawn_probability: #creature_spawn_probability,
spawners: #spawners,
spawn_costs: phf::phf_map! {
#(#spawn_costs),*
},
};
}]);
@@ -246,197 +288,223 @@ pub(crate) fn build() -> TokenStream {
let overworld_tree = biome_trees.overworld.into_token_stream();
let nether_tree = biome_trees.nether.into_token_stream();
quote! {
use crate::biome::de::Deserialize;
use crate::entity_type::EntityType;
use crate::tag::Taggable;
use crate::tag::RegistryKey;
use pumpkin_util::biome::{TemperatureModifier, Weather};
use serde::{Deserializer, de};
use std::{fmt, hash::{Hasher, Hash}};
use pumpkin_util::biome::{TemperatureModifier, Weather};
use serde::{Deserializer, de};
use crate::biome::de::Deserialize;
use std::{fmt, hash::{Hasher, Hash}};
#[derive(Debug)]
pub struct Biome {
pub id: u8,
pub registry_id: &'static str,
pub weather: Weather,
// carvers: &'static [&str],
pub features: &'static [&'static [&'static str]],
pub creature_spawn_probability: f32,
pub spawners: SpawnGroups,
pub spawn_costs: phf::Map<&'static str, SpawnCosts>,
}
#[derive(Debug)]
pub struct Biome {
pub id: u8,
pub registry_id: &'static str,
pub weather: Weather,
// carvers: &'static [&str],
pub features: &'static [&'static [&'static str]],
pub creature_spawn_probability: f32,
pub spawners: SpawnGroups,
#[derive(Debug)]
pub struct SpawnGroups {
pub monster: &'static [Spawner],
pub ambient: &'static [Spawner],
pub axolotls: &'static [Spawner],
pub creature: &'static [Spawner],
pub misc: &'static [Spawner],
pub underground_water_creature: &'static [Spawner],
pub water_ambient: &'static [Spawner],
pub water_creature: &'static [Spawner],
}
#[derive(Debug)]
pub struct Spawner {
pub r#type: &'static str,
pub min_count: i32,
pub max_count: i32,
}
impl PartialEq for Biome {
fn eq(&self, other: &Biome) -> bool {
self.id == other.id
}
}
#[derive(Debug)]
pub struct SpawnGroups {
pub monster: &'static [Spawner],
pub ambient: &'static [Spawner],
pub axolotls: &'static [Spawner],
pub creature: &'static [Spawner],
pub misc: &'static [Spawner],
pub underground_water_creature: &'static [Spawner],
pub water_ambient: &'static [Spawner],
pub water_creature: &'static [Spawner],
impl Eq for Biome {}
impl Hash for Biome {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Debug)]
pub struct Spawner {
pub r#type: &'static str,
}
#[derive(Debug)]
pub struct SpawnCosts {
pub energy_budget: f64,
pub charge: f64,
}
impl PartialEq for Biome {
fn eq(&self, other: &Biome) -> bool {
self.id == other.id
}
}
impl<'de> Deserialize<'de> for &'static Biome {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct BiomeVisitor;
impl Eq for Biome {}
impl de::Visitor<'_> for BiomeVisitor {
type Value = &'static Biome;
impl Hash for Biome {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl<'de> Deserialize<'de> for &'static Biome {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct BiomeVisitor;
impl de::Visitor<'_> for BiomeVisitor {
type Value = &'static Biome;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a biome name as a string")
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
self.visit_str(&v)
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
let biome = Biome::from_name(value.strip_prefix("minecraft:").unwrap_or(value));
biome.ok_or_else(|| E::unknown_variant(value, &["unknown biome"]))
}
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a biome name as a string")
}
deserializer.deserialize_str(BiomeVisitor)
}
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
self.visit_str(&v)
}
impl Biome {
#variants
pub fn from_name(name: &str) -> Option<&'static Self> {
match name {
#name_to_type
_ => None
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
let biome = Biome::from_name(value.strip_prefix("minecraft:").unwrap_or(value));
biome.ok_or_else(|| E::unknown_variant(value, &["unknown biome"]))
}
}
pub const fn from_id(id: u8) -> Option<&'static Self> {
match id {
#id_to_type
_ => None
}
deserializer.deserialize_str(BiomeVisitor)
}
}
impl Biome {
#variants
pub fn from_name(name: &str) -> Option<&'static Self> {
match name {
#name_to_type
_ => None
}
}
#[derive(PartialEq)]
pub struct ParameterRange {
min: i64,
max: i64,
pub const fn from_id(id: u8) -> Option<&'static Self> {
match id {
#id_to_type
_ => None
}
}
}
impl ParameterRange {
fn calc_distance(&self, noise: i64) -> i64 {
if noise > self.max {
noise - self.max
} else if noise < self.min {
self.min - noise
} else {
0
impl Taggable for Biome {
#[inline]
fn registry_id(&self) -> u16 {
self.id as u16
}
#[inline]
fn tag_key() -> RegistryKey {
RegistryKey::WorldgenBiome
}
#[inline]
fn registry_key(&self) -> &str {
self.registry_id
}
}
#[derive(PartialEq)]
pub struct ParameterRange {
min: i64,
max: i64,
}
impl ParameterRange {
fn calc_distance(&self, noise: i64) -> i64 {
if noise > self.max {
noise - self.max
} else if noise < self.min {
self.min - noise
} else {
0
}
}
}
#[derive(PartialEq)]
pub enum BiomeTree {
Leaf {
parameters: [ParameterRange; 7],
biome: &'static Biome,
},
Branch {
parameters: [ParameterRange; 7],
nodes: &'static [BiomeTree],
},
}
impl BiomeTree {
pub fn get(
&'static self,
point_list: &[i64; 7],
previous_result_node: &mut Option<&'static BiomeTree>,
) -> &'static Biome {
let result_node = self.get_resulting_node(point_list, *previous_result_node);
match result_node {
BiomeTree::Leaf { biome, .. } => {
*previous_result_node = Some(result_node);
biome
}
_ => unreachable!(),
}
}
#[derive(PartialEq)]
pub enum BiomeTree {
Leaf {
parameters: [ParameterRange; 7],
biome: &'static Biome,
},
Branch {
parameters: [ParameterRange; 7],
nodes: &'static [BiomeTree],
},
}
fn get_resulting_node(
&'static self,
point_list: &[i64; 7],
previous_result_node: Option<&'static BiomeTree>,
) -> &'static BiomeTree {
match self {
Self::Leaf { .. } => self,
Self::Branch { nodes, .. } => {
let mut distance = previous_result_node
.map(|node| node.get_squared_distance(point_list))
.unwrap_or(i64::MAX);
let mut best_node = previous_result_node;
for node in *nodes {
let node_distance = node.get_squared_distance(point_list);
if distance > node_distance {
let node2 = node.get_resulting_node(point_list, best_node);
let node2_distance = if node == node2 {
node_distance
} else {
node2.get_squared_distance(point_list)
};
impl BiomeTree {
pub fn get(
&'static self,
point_list: &[i64; 7],
previous_result_node: &mut Option<&'static BiomeTree>,
) -> &'static Biome {
let result_node = self.get_resulting_node(point_list, *previous_result_node);
match result_node {
BiomeTree::Leaf { biome, .. } => {
*previous_result_node = Some(result_node);
biome
}
_ => unreachable!(),
}
}
fn get_resulting_node(
&'static self,
point_list: &[i64; 7],
previous_result_node: Option<&'static BiomeTree>,
) -> &'static BiomeTree {
match self {
Self::Leaf { .. } => self,
Self::Branch { nodes, .. } => {
let mut distance = previous_result_node
.map(|node| node.get_squared_distance(point_list))
.unwrap_or(i64::MAX);
let mut best_node = previous_result_node;
for node in *nodes {
let node_distance = node.get_squared_distance(point_list);
if distance > node_distance {
let node2 = node.get_resulting_node(point_list, best_node);
let node2_distance = if node == node2 {
node_distance
} else {
node2.get_squared_distance(point_list)
};
if distance > node2_distance {
distance = node2_distance;
best_node = Some(node2);
}
if distance > node2_distance {
distance = node2_distance;
best_node = Some(node2);
}
}
best_node.expect("This should be populated after traversing the tree")
}
best_node.expect("This should be populated after traversing the tree")
}
}
fn get_squared_distance(&self, point_list: &[i64; 7]) -> i64 {
let parameters = match self {
Self::Leaf { parameters, .. } => parameters,
Self::Branch { parameters, .. } => parameters,
};
parameters
.iter()
.zip(point_list)
.map(|(bound, value)| {
let distance = bound.calc_distance(*value);
distance * distance
})
.sum()
}
}
pub const OVERWORLD_BIOME_SOURCE: BiomeTree = #overworld_tree;
pub const NETHER_BIOME_SOURCE: BiomeTree = #nether_tree;
fn get_squared_distance(&self, point_list: &[i64; 7]) -> i64 {
let parameters = match self {
Self::Leaf { parameters, .. } => parameters,
Self::Branch { parameters, .. } => parameters,
};
parameters
.iter()
.zip(point_list)
.map(|(bound, value)| {
let distance = bound.calc_distance(*value);
distance * distance
})
.sum()
}
}
pub const OVERWORLD_BIOME_SOURCE: BiomeTree = #overworld_tree;
pub const NETHER_BIOME_SOURCE: BiomeTree = #nether_tree;
}
}

View File

@@ -13,9 +13,13 @@ pub struct EntityType {
pub id: u16,
pub max_health: Option<f32>,
pub attackable: Option<bool>,
pub mob: Option<bool>,
pub limit_per_chunk: Option<i32>,
pub loot_table: Option<LootTableStruct>,
pub summonable: bool,
pub fire_immune: bool,
pub category: MobCategory,
pub can_spawn_far_from_player: bool,
pub dimension: [f32; 2],
pub eye_height: f32,
pub spawn_restriction: SpawnRestriction,
@@ -36,6 +40,20 @@ pub enum SpawnLocation {
Unrestricted,
}
#[derive(Deserialize)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
pub enum MobCategory {
MONSTER,
CREATURE,
AMBIENT,
AXOLOTLS,
UNDERGROUND_WATER_CREATURE,
WATER_CREATURE,
WATER_AMBIENT,
MISC,
}
pub struct NamedEntityType<'a>(&'a str, &'a EntityType);
impl ToTokens for NamedEntityType<'_> {
@@ -75,9 +93,31 @@ impl ToTokens for NamedEntityType<'_> {
heightmap: #spawn_restriction_heightmap,
}};
let spawn_category = match entity.category {
MobCategory::MONSTER => quote! { MobCategory::MONSTER },
MobCategory::CREATURE => quote! { MobCategory::CREATURE },
MobCategory::AMBIENT => quote! { MobCategory::AMBIENT },
MobCategory::AXOLOTLS => quote! { MobCategory::AXOLOTLS },
MobCategory::UNDERGROUND_WATER_CREATURE => {
quote! { MobCategory::UNDERGROUND_WATER_CREATURE }
}
MobCategory::WATER_CREATURE => quote! { MobCategory::WATER_CREATURE },
MobCategory::WATER_AMBIENT => quote! { MobCategory::WATER_AMBIENT },
MobCategory::MISC => quote! { MobCategory::MISC },
};
let summonable = entity.summonable;
let fire_immune = entity.fire_immune;
let eye_height = entity.eye_height;
if entity.mob.is_none() && name != "player" {
panic!("missing field 'mob', entity name {name}");
}
if entity.limit_per_chunk.is_none() && name != "player" {
panic!("missing field 'mob', entity name {name}");
}
let mob = entity.mob.unwrap_or(false);
let limit_per_chunk = entity.limit_per_chunk.unwrap_or(0);
let can_spawn_far_from_player = entity.can_spawn_far_from_player;
let dimension0 = entity.dimension[0];
let dimension1 = entity.dimension[1];
@@ -95,8 +135,12 @@ impl ToTokens for NamedEntityType<'_> {
id: #id,
max_health: #max_health,
attackable: #attackable,
mob: #mob,
limit_per_chunk: #limit_per_chunk,
summonable: #summonable,
fire_immune: #fire_immune,
category: &#spawn_category,
can_spawn_far_from_player: #can_spawn_far_from_player,
loot_table: #loot_table,
dimension: [#dimension0, #dimension1], // Correctly construct the array
eye_height: #eye_height,
@@ -130,24 +174,28 @@ pub(crate) fn build() -> TokenStream {
});
type_from_raw_id_arms.extend(quote! {
#id_lit => Some(Self::#upper_name),
#id_lit => Some(&Self::#upper_name),
});
type_from_name.extend(quote! {
#name => Some(Self::#upper_name),
#name => Some(&Self::#upper_name),
});
}
quote! {
use pumpkin_util::loot_table::*;
use pumpkin_util::HeightMap;
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Debug)]
pub struct EntityType {
pub id: u16,
pub max_health: Option<f32>,
pub attackable: Option<bool>,
pub mob: bool,
pub limit_per_chunk: i32,
pub summonable: bool,
pub fire_immune: bool,
pub category: &'static MobCategory,
pub can_spawn_far_from_player: bool,
pub loot_table: Option<LootTable>,
pub dimension: [f32; 2],
pub eye_height: f32,
@@ -155,31 +203,122 @@ pub(crate) fn build() -> TokenStream {
pub resource_name: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpawnRestriction {
location: SpawnLocation,
heightmap: HeightMap,
impl PartialEq for EntityType {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SpawnLocation {
InLava,
InWater,
OnGround,
Unrestricted
}
#[derive(Debug)]
pub struct SpawnRestriction {
pub location: SpawnLocation,
pub heightmap: HeightMap,
}
#[derive(Debug)]
pub enum SpawnLocation {
InLava,
InWater,
OnGround,
Unrestricted,
}
#[derive(Debug)]
pub struct MobCategory {
pub id: usize, // mojang don't have this field
pub max: i32,
pub is_friendly: bool,
pub is_persistent: bool,
pub despawn_distance: i32,
}
impl PartialEq for MobCategory {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl MobCategory {
pub const NO_DESPAWN_DISTANCE: i32 = 32;
pub const MONSTER: MobCategory = MobCategory {
id: 0,
max: 70,
is_friendly: false,
is_persistent: false,
despawn_distance: 128,
};
pub const CREATURE: MobCategory = MobCategory {
id: 1,
max: 10,
is_friendly: true,
is_persistent: true,
despawn_distance: 128,
};
pub const AMBIENT: MobCategory = MobCategory {
id: 2,
max: 15,
is_friendly: true,
is_persistent: false,
despawn_distance: 128,
};
pub const AXOLOTLS: MobCategory = MobCategory {
id: 3,
max: 5,
is_friendly: true,
is_persistent: false,
despawn_distance: 128,
};
pub const UNDERGROUND_WATER_CREATURE: MobCategory = MobCategory {
id: 4,
max: 5,
is_friendly: true,
is_persistent: false,
despawn_distance: 128,
};
pub const WATER_CREATURE: MobCategory = MobCategory {
id: 5,
max: 5,
is_friendly: true,
is_persistent: true,
despawn_distance: 128,
};
pub const WATER_AMBIENT: MobCategory = MobCategory {
id: 6,
max: 20,
is_friendly: true,
is_persistent: false,
despawn_distance: 64,
};
pub const MISC: MobCategory = MobCategory {
id: 7,
max: -1,
is_friendly: true,
is_persistent: true,
despawn_distance: 128,
};
pub const SPAWNING_CATEGORIES: [&'static Self; 8] = [
&Self::MONSTER,
&Self::CREATURE,
&Self::AMBIENT,
&Self::AXOLOTLS,
&Self::UNDERGROUND_WATER_CREATURE,
&Self::WATER_CREATURE,
&Self::WATER_AMBIENT,
&Self::MISC,
];
}
impl EntityType {
#consts
pub const fn from_raw(id: u16) -> Option<Self> {
pub const fn from_raw(id: u16) -> Option<&'static Self> {
match id {
#type_from_raw_id_arms
_ => None
}
}
pub fn from_name(name: &str) -> Option<Self> {
pub fn from_name(name: &str) -> Option<&'static Self> {
match name {
#type_from_name
_ => None

View File

@@ -15,12 +15,12 @@ pub(crate) fn build() -> TokenStream {
for (egg, entity) in &eggs {
let entity = entity.to_shouty_snake_case();
let entity = format_ident!("{}", entity);
names.extend(quote! { #egg => Some(EntityType::#entity), });
names.extend(quote! { #egg => Some(&EntityType::#entity), });
}
quote! {
use crate::entity_type::EntityType;
pub fn entity_from_egg(id: u16) -> Option<EntityType> {
pub fn entity_from_egg(id: u16) -> Option<&'static EntityType> {
match id {
#names
_ => None

View File

@@ -27,6 +27,13 @@ impl<T: Math + Copy> Vector2<T> {
}
}
pub fn add_raw(&self, x: T, y: T) -> Self {
Vector2 {
x: self.x + x,
y: self.y + y,
}
}
pub fn sub(&self, other: &Vector2<T>) -> Self {
Vector2 {
x: self.x - other.x,

View File

@@ -73,11 +73,11 @@ impl DropperBlockEntity {
pub async fn get_random_slot(&self) -> Option<MutexGuard<ItemStack>> {
// this.unpackLootTable(null);
let mut ret = None;
let mut j = 0;
let mut j = 1;
for i in &self.items {
let item = i.lock().await;
if !item.is_empty() {
if rng().random_range(0..=j) == 0 {
if rng().random_range(0..j) == 0 {
ret = Some(item);
}
j += 1;

View File

@@ -303,6 +303,26 @@ impl ChunkSections {
}
}
pub fn get_rough_biome_absolute_y(
&self,
relative_x: usize,
y: i32,
relative_z: usize,
) -> Option<u8> {
let y = y - self.min_y;
if y < 0 {
None
} else {
let relative_y = y as usize;
self.get_noise_biome(
relative_y / BlockPalette::SIZE,
relative_x >> 2 & 3,
relative_y >> 2 & 3,
relative_z >> 2 & 3,
)
}
}
/// Returns the replaced block state ID
pub fn set_block_absolute_y(
&mut self,
@@ -391,6 +411,20 @@ impl ChunkSections {
.set(relative_x, relative_y, relative_z, biome_id);
}
}
pub fn get_noise_biome(
&self,
index: usize,
scale_x: usize,
scale_y: usize,
scale_z: usize,
) -> Option<u8> {
debug_assert!(scale_x < BiomePalette::SIZE);
debug_assert!(scale_z < BiomePalette::SIZE);
self.sections
.get(index)
.map(|section| section.biomes.get(scale_x, scale_y, scale_z))
}
}
impl ChunkData {
@@ -438,7 +472,7 @@ impl ChunkData {
}
//TODO: Tracking heightmaps update.
pub async fn calculate_heightmap(&mut self) -> ChunkHeightmaps {
pub fn calculate_heightmap(&mut self) -> ChunkHeightmaps {
let highest_non_empty_subchunk = self.get_highest_non_empty_subchunk();
let mut heightmaps = ChunkHeightmaps::default();

View File

@@ -1,7 +1,21 @@
use crate::{
BlockStateId,
block::{RawBlockState, entities::BlockEntity},
chunk::{
ChunkData, ChunkEntityData, ChunkParsingError, ChunkReadingError,
format::{anvil::AnvilChunkFile, linear::LinearFile},
io::{Dirtiable, FileIO, LoadedData, file_manager::ChunkFileManager},
},
dimension::Dimension,
generation::{Seed, get_world_gen, implementation::WorldGenerator},
tick::{OrderedTick, ScheduledTick, TickPriority},
world::BlockRegistryExt,
};
use dashmap::{DashMap, Entry};
use log::trace;
use num_traits::Zero;
use pumpkin_config::{advanced_config, chunk::ChunkFormat};
use pumpkin_data::biome::Biome;
use pumpkin_data::{Block, block_properties::has_random_ticks, fluid::Fluid};
use pumpkin_util::math::{position::BlockPos, vector2::Vector2};
use rand::{Rng, SeedableRng, rngs::SmallRng};
@@ -24,20 +38,6 @@ use tokio::{
};
use tokio_util::task::TaskTracker;
use crate::{
BlockStateId,
block::{RawBlockState, entities::BlockEntity},
chunk::{
ChunkData, ChunkEntityData, ChunkParsingError, ChunkReadingError,
format::{anvil::AnvilChunkFile, linear::LinearFile},
io::{Dirtiable, FileIO, LoadedData, file_manager::ChunkFileManager},
},
dimension::Dimension,
generation::{Seed, get_world_gen, implementation::WorldGenerator},
tick::{OrderedTick, ScheduledTick, TickPriority},
world::BlockRegistryExt,
};
pub type SyncChunk = Arc<RwLock<ChunkData>>;
pub type SyncEntityChunk = Arc<RwLock<ChunkEntityData>>;
@@ -607,6 +607,21 @@ impl Level {
RawBlockState(id)
}
pub async fn get_rough_biome(self: &Arc<Self>, position: &BlockPos) -> &'static Biome {
let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position();
let chunk = self.get_chunk(chunk_coordinate).await;
let chunk = chunk.read().await;
let Some(id) = chunk.section.get_rough_biome_absolute_y(
relative.x as usize,
relative.y,
relative.z as usize,
) else {
return &Biome::THE_VOID;
};
Biome::from_id(id).unwrap()
}
pub async fn set_block_state(
self: &Arc<Self>,
@@ -815,11 +830,12 @@ impl Level {
// Deduplicate chunk generation using chunk_generation_locks
// We are responsible for generating the chunk
let generated_chunk = world_gen.generate_chunk(
let mut generated_chunk = world_gen.generate_chunk(
&self_clone,
block_registry.as_ref(),
&pos,
);
generated_chunk.heightmap = generated_chunk.calculate_heightmap();
let arc_chunk = Arc::new(RwLock::new(generated_chunk));
loaded_chunks.insert(pos, arc_chunk.clone());

View File

@@ -133,7 +133,7 @@ impl ComposterBlock {
Uuid::new_v4(),
world.clone(),
item_position,
EntityType::ITEM,
&EntityType::ITEM,
false,
),
ItemStack::new(1, &Item::BONE_MEAL),

View File

@@ -202,7 +202,7 @@ impl BlockBehaviour for FireBlock {
let ticks = base_entity.fire_ticks.load(Ordering::Relaxed);
if ticks < 0 {
base_entity.fire_ticks.store(ticks + 1, Ordering::Relaxed);
} else if base_entity.entity_type == EntityType::PLAYER {
} else if base_entity.entity_type == &EntityType::PLAYER {
let rnd_ticks = rand::rng().random_range(1..3);
base_entity
.fire_ticks

View File

@@ -21,8 +21,10 @@ impl NetherPortalBlock {
async fn get_portal_time(world: &Arc<World>, entity: &dyn EntityBase) -> u32 {
let entity_type = entity.get_entity().entity_type;
match entity_type {
EntityType::PLAYER => (world.get_player_by_id(entity.get_entity().entity_id).await)
match entity_type.id {
id if id == EntityType::PLAYER.id => (world
.get_player_by_id(entity.get_entity().entity_id)
.await)
.map_or(80, |player| match player.gamemode.load() {
GameMode::Creative => 0,
_ => 80,

View File

@@ -33,7 +33,7 @@ impl crate::block::BlockBehaviour for PumpkinBlock {
Uuid::new_v4(),
args.world.clone(),
args.position.to_f64(),
EntityType::ITEM,
&EntityType::ITEM,
false,
);
let item_entity =

View File

@@ -319,7 +319,7 @@ impl ComparatorBlock {
.await
.into_iter()
.filter(|entity| {
entity.get_entity().entity_type == EntityType::ITEM_FRAME
entity.get_entity().entity_type == &EntityType::ITEM_FRAME
&& entity.get_entity().get_horizontal_facing() == facing
});
if let Some(_itemframe) = itemframes.next() {

View File

@@ -195,7 +195,7 @@ impl BlockBehaviour for DropperBlock {
Uuid::new_v4(),
args.world.clone(),
position,
EntityType::ITEM,
&EntityType::ITEM,
false,
);
let rd = rng().random::<f64>() * 0.1 + 0.2;

View File

@@ -29,7 +29,7 @@ impl TNTBlock {
Uuid::new_v4(),
world.clone(),
location.to_f64(),
EntityType::TNT,
&EntityType::TNT,
false,
);
let pos = entity.pos.load();
@@ -81,7 +81,7 @@ impl BlockBehaviour for TNTBlock {
Uuid::new_v4(),
args.world.clone(),
args.position.to_f64(),
EntityType::TNT,
&EntityType::TNT,
false,
);
let angle = rand::random::<f64>() * std::f64::consts::TAU;

View File

@@ -54,7 +54,7 @@ impl DefaultNameArgConsumer for SummonableEntitiesArgumentConsumer {
}
impl<'a> FindArg<'a> for SummonableEntitiesArgumentConsumer {
type Data = EntityType;
type Data = &'static EntityType;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {

View File

@@ -19,14 +19,14 @@ pub struct ActiveTargetGoal {
track_target_goal: TrackTargetGoal,
target: Mutex<Option<Arc<dyn EntityBase>>>,
reciprocal_chance: i32,
target_type: EntityType,
target_type: &'static EntityType,
target_predicate: TargetPredicate,
}
impl ActiveTargetGoal {
pub async fn new<F, Fut>(
mob: &MobEntity,
target_type: EntityType,
target_type: &'static EntityType,
reciprocal_chance: i32,
check_visibility: bool,
check_can_navigate: bool,
@@ -55,7 +55,7 @@ impl ActiveTargetGoal {
#[must_use]
pub async fn with_default(
mob: &MobEntity,
target_type: EntityType,
target_type: &'static EntityType,
check_visibility: bool,
) -> Self {
let track_target_goal = TrackTargetGoal::with_default(check_visibility);
@@ -74,7 +74,7 @@ impl ActiveTargetGoal {
async fn find_closest_target(&self, mob: &MobEntity) {
let mut target = self.target.lock().await;
let world = mob.living_entity.entity.world.read().await;
if self.target_type == EntityType::PLAYER {
if self.target_type == &EntityType::PLAYER {
*target = world
.get_closest_player(
mob.living_entity.entity.pos.load(),

View File

@@ -19,7 +19,7 @@ pub struct LookAtEntityGoal {
look_time: AtomicI32,
chance: f64,
look_forward: bool,
target_type: EntityType,
target_type: &'static EntityType,
target_predicate: TargetPredicate,
}
@@ -27,7 +27,7 @@ impl LookAtEntityGoal {
#[must_use]
pub fn new(
mob_weak: Weak<dyn Mob>,
target_type: EntityType,
target_type: &'static EntityType,
range: f64,
chance: f64,
look_forward: bool,
@@ -46,18 +46,22 @@ impl LookAtEntityGoal {
}
#[must_use]
pub fn with_default(mob_weak: Weak<dyn Mob>, target_type: EntityType, range: f64) -> Self {
pub fn with_default(
mob_weak: Weak<dyn Mob>,
target_type: &'static EntityType,
range: f64,
) -> Self {
Self::new(mob_weak, target_type, range, 0.02, false)
}
fn create_target_predicate(
mob_weak: Weak<dyn Mob>,
target_type: EntityType,
target_type: &'static EntityType,
range: f64,
) -> TargetPredicate {
let mut target_predicate = TargetPredicate::non_attackable();
target_predicate.base_max_distance = range;
if target_type == EntityType::PLAYER {
if target_type == &EntityType::PLAYER {
target_predicate.set_predicate(move |living_entity, _world| {
let mob_weak = mob_weak.clone();
async move {
@@ -92,7 +96,7 @@ impl Goal for LookAtEntityGoal {
drop(mob_target);
let world = mob_entity.living_entity.entity.world.read().await;
if self.target_type == EntityType::PLAYER {
if self.target_type == &EntityType::PLAYER {
*target = world
.get_closest_player(mob_entity.living_entity.entity.pos.load(), self.range)
.await

View File

@@ -38,7 +38,7 @@ impl ExperienceOrbEntity {
Uuid::new_v4(),
world.clone(),
position,
EntityType::EXPERIENCE_ORB,
&EntityType::EXPERIENCE_ORB,
false,
);
let orb = Arc::new(Self::new(entity, i));

View File

@@ -258,7 +258,7 @@ impl LivingEntity {
)
.await;
let params = LootContextParameters {
killed_by_player: cause.map(|c| c.get_entity().entity_type == EntityType::PLAYER),
killed_by_player: cause.map(|c| c.get_entity().entity_type == &EntityType::PLAYER),
..Default::default()
};
@@ -267,7 +267,7 @@ impl LivingEntity {
let level_info = world.level_info.read().await;
let game_rules = &level_info.game_rules;
if self.entity.entity_type == EntityType::PLAYER && game_rules.show_death_messages {
if self.entity.entity_type == &EntityType::PLAYER && game_rules.show_death_messages {
//TODO: KillCredit
let death_message = if let Some(death_message_type) = damage_type.death_message_type
{

View File

@@ -42,7 +42,7 @@ impl Zombie {
8,
Arc::new(LookAtEntityGoal::with_default(
mob_weak,
EntityType::PLAYER,
&EntityType::PLAYER,
8.0,
)),
)
@@ -58,7 +58,7 @@ impl Zombie {
.add_goal(
2,
Arc::new(
ActiveTargetGoal::with_default(&mob_arc.mob_entity, EntityType::PLAYER, true)
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true)
.await,
),
)

View File

@@ -219,7 +219,7 @@ pub struct Entity {
/// A persistent, unique identifier for the entity
pub entity_uuid: uuid::Uuid,
/// The type of entity (e.g., player, zombie, item)
pub entity_type: EntityType,
pub entity_type: &'static EntityType,
/// The world in which the entity exists.
pub world: Arc<RwLock<Arc<World>>>,
/// The entity's current position in the world
@@ -286,7 +286,7 @@ impl Entity {
entity_uuid: uuid::Uuid,
world: Arc<World>,
position: Vector3<f64>,
entity_type: EntityType,
entity_type: &'static EntityType,
invulnerable: bool,
) -> Self {
let floor_x = position.x.floor() as i32;
@@ -305,7 +305,10 @@ impl Entity {
on_ground: AtomicBool::new(false),
pos: AtomicCell::new(position),
block_pos: AtomicCell::new(BlockPos(Vector3::new(floor_x, floor_y, floor_z))),
chunk_pos: AtomicCell::new(Vector2::new(floor_x, floor_z)),
chunk_pos: AtomicCell::new(Vector2::new(
get_section_cord(floor_x),
get_section_cord(floor_z),
)),
sneaking: AtomicBool::new(false),
world: Arc::new(RwLock::new(world)),
sprinting: AtomicBool::new(false),
@@ -436,7 +439,7 @@ impl Entity {
}
fn default_portal_cooldown(&self) -> u32 {
if self.entity_type == EntityType::PLAYER {
if self.entity_type == &EntityType::PLAYER {
10
} else {
300

View File

@@ -302,7 +302,7 @@ impl Player {
player_uuid,
world,
Vector3::new(0.0, 0.0, 0.0),
EntityType::PLAYER,
&EntityType::PLAYER,
matches!(gamemode, GameMode::Creative | GameMode::Spectator),
));
@@ -1452,7 +1452,7 @@ impl Player {
Uuid::new_v4(),
self.world().await,
item_pos,
EntityType::ITEM,
&EntityType::ITEM,
false,
);

View File

@@ -10,7 +10,7 @@ use crate::{
};
pub async fn from_type(
entity_type: EntityType,
entity_type: &'static EntityType,
position: Vector3<f64>,
world: &Arc<World>,
uuid: Uuid,
@@ -18,9 +18,9 @@ pub async fn from_type(
let entity = Entity::new(uuid, world.clone(), position, entity_type, false);
#[allow(clippy::single_match)]
let mob: Arc<dyn EntityBase> = match entity_type {
EntityType::ZOMBIE => Zombie::make(entity).await,
EntityType::PAINTING => Arc::new(PaintingEntity::new(entity)),
let mob: Arc<dyn EntityBase> = match entity_type.id {
id if id == EntityType::ZOMBIE.id => Zombie::make(entity).await,
id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)),
// TODO
_ => Arc::new(entity), // Fallback Entity
};

View File

@@ -37,7 +37,7 @@ impl ItemBehaviour for EggItem {
Uuid::new_v4(),
world.clone(),
position,
EntityType::EGG,
&EntityType::EGG,
false,
);
let egg = ThrownItemEntity::new(entity, &player.living_entity.entity);

View File

@@ -85,7 +85,7 @@ impl ItemBehaviour for HoeItem {
Uuid::new_v4(),
world.clone(),
location,
EntityType::SNOWBALL,
&EntityType::SNOWBALL,
false,
);
// TODO: Merge stacks together

View File

@@ -20,14 +20,14 @@ use uuid::Uuid;
pub struct MinecartItem;
impl MinecartItem {
fn item_to_entity(item: &Item) -> EntityType {
fn item_to_entity(item: &Item) -> &'static EntityType {
match item.id {
val if val == Item::MINECART.id => EntityType::MINECART,
val if val == Item::TNT_MINECART.id => EntityType::TNT_MINECART,
val if val == Item::CHEST_MINECART.id => EntityType::CHEST_MINECART,
val if val == Item::HOPPER_MINECART.id => EntityType::HOPPER_MINECART,
val if val == Item::FURNACE_MINECART.id => EntityType::FURNACE_MINECART,
val if val == Item::COMMAND_BLOCK_MINECART.id => EntityType::COMMAND_BLOCK_MINECART,
val if val == Item::MINECART.id => &EntityType::MINECART,
val if val == Item::TNT_MINECART.id => &EntityType::TNT_MINECART,
val if val == Item::CHEST_MINECART.id => &EntityType::CHEST_MINECART,
val if val == Item::HOPPER_MINECART.id => &EntityType::HOPPER_MINECART,
val if val == Item::FURNACE_MINECART.id => &EntityType::FURNACE_MINECART,
val if val == Item::COMMAND_BLOCK_MINECART.id => &EntityType::COMMAND_BLOCK_MINECART,
_ => unreachable!(),
}
}

View File

@@ -36,7 +36,7 @@ impl ItemBehaviour for SnowBallItem {
Uuid::new_v4(),
world.clone(),
position,
EntityType::SNOWBALL,
&EntityType::SNOWBALL,
false,
);
let snowball = ThrownItemEntity::new(entity, &player.living_entity.entity);

View File

@@ -1709,7 +1709,7 @@ impl JavaClient {
async fn spawn_entity_from_egg(
&self,
player: &Player,
entity_type: EntityType,
entity_type: &'static EntityType,
location: BlockPos,
face: BlockDirection,
) {

View File

@@ -1,5 +1,6 @@
use std::sync::Weak;
use std::sync::atomic::Ordering::Relaxed;
use std::time::Duration;
use std::{
collections::HashMap,
sync::{Arc, atomic::Ordering},
@@ -37,6 +38,7 @@ use bytes::BufMut;
use explosion::Explosion;
use pumpkin_config::BasicConfiguration;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::entity::MobCategory;
use pumpkin_data::fluid::{Falling, FluidProperties};
use pumpkin_data::{
Block,
@@ -115,6 +117,7 @@ use pumpkin_world::{
};
use pumpkin_world::{level::Level, tick::TickPriority};
use pumpkin_world::{world::BlockFlags, world_info::LevelData};
use rand::seq::SliceRandom;
use rand::{Rng, rng};
use scoreboard::Scoreboard;
use serde::Serialize;
@@ -125,10 +128,13 @@ use tokio::sync::RwLock;
pub mod border;
pub mod bossbar;
pub mod custom_bossbar;
pub mod natural_spawner;
pub mod scoreboard;
pub mod weather;
use crate::world::natural_spawner::{SpawnState, spawn_for_chunk};
use pumpkin_data::effect::StatusEffect;
use pumpkin_world::chunk::ChunkHeightmapType::MotionBlocking;
use pumpkin_world::generation::settings::GenerationSettings;
use uuid::Uuid;
use weather::Weather;
@@ -176,6 +182,7 @@ pub struct World {
/// The type of dimension the world is in.
pub dimension_type: VanillaDimensionType,
pub sea_level: i32,
pub min_y: i32,
/// The world's weather, including rain and thunder levels.
pub weather: Mutex<Weather>,
/// Block Behaviour
@@ -221,6 +228,7 @@ impl World {
weather: Mutex::new(Weather::new()),
block_registry,
sea_level: generation_settings.sea_level,
min_y: i32::from(generation_settings.shape.min_y),
synced_block_event_queue: Mutex::new(Vec::new()),
unsent_block_changes: Mutex::new(HashMap::new()),
server,
@@ -683,12 +691,141 @@ impl World {
}
} */
let spawn_entity_clock_start = tokio::time::Instant::now();
let mut spawning_chunks_map = HashMap::new();
// TODO use FixedPlayerDistanceChunkTracker
for i in self.players.read().await.values() {
let center = i.living_entity.entity.chunk_pos.load();
for dx in -8i32..=8 {
for dy in -8i32..=8 {
// if dx.abs() <= 2 || dy.abs() <= 2 || dx.abs() >= 6 || dy.abs() >= 6 { // this is only for debug, spawning runs too slow
// continue;
// }
let chunk_pos = center.add_raw(dx, dy);
if let Some(chunk) = self.level.try_get_chunk(&chunk_pos) {
spawning_chunks_map
.entry(chunk_pos)
.or_insert(chunk.value().clone());
}
}
}
}
let mut spawning_chunks = Vec::with_capacity(spawning_chunks_map.len());
for i in spawning_chunks_map {
spawning_chunks.push(i);
}
let get_chunks_clock = spawn_entity_clock_start.elapsed();
// log::debug!("spawning chunks size {}", spawning_chunks.len());
let mut spawn_state =
SpawnState::new(spawning_chunks.len() as i32, &self.entities, self).await; // TODO store it
// TODO gamerule this.spawnEnemies || this.spawnFriendlies
let spawn_passives = self.level_time.lock().await.time_of_day % 400 == 0;
let spawn_list: Vec<&'static MobCategory> =
natural_spawner::get_filtered_spawning_categories(
&spawn_state,
true,
true,
spawn_passives,
);
// log::debug!("spawning list size {}", spawn_list.len());
log::debug!("spawning counter {:?}", spawn_state.mob_category_counts);
spawning_chunks.shuffle(&mut rng());
// TODO i think it can be multithread
for (pos, chunk) in &spawning_chunks {
self.tick_spawning_chunk(pos, chunk, &spawn_list, &mut spawn_state)
.await;
}
log::debug!(
"Spawning entity took {:?}, getting chunks {:?}, spawning chunks: {}, avg {:?} per chunk",
spawn_entity_clock_start.elapsed(),
get_chunks_clock,
spawning_chunks.len(),
spawn_entity_clock_start
.elapsed()
.checked_div(spawning_chunks.len() as u32)
.unwrap_or(Duration::new(0, 0))
);
for block_entity in tick_data.block_entities {
let world: Arc<dyn SimpleWorld> = self.clone();
block_entity.tick(world).await;
}
}
pub async fn tick_spawning_chunk(
self: &Arc<Self>,
chunk_pos: &Vector2<i32>,
chunk: &Arc<RwLock<ChunkData>>,
spawn_list: &Vec<&'static MobCategory>,
spawn_state: &mut SpawnState,
) {
// this.level.tickThunder(chunk);
//TODO check in simulation distance
if self.weather.lock().await.raining
&& self.weather.lock().await.thundering
&& rng().random_range(0..100_000) == 0
{
let rand_value = rng().random::<i32>() >> 2;
let delta = Vector3::new(rand_value & 15, rand_value >> 16 & 15, rand_value >> 8 & 15);
let random_pos = Vector3::new(
chunk_pos.x << 4,
chunk.read().await.heightmap.get_height(
MotionBlocking,
chunk_pos.x << 4,
chunk_pos.y << 4,
self.min_y,
),
chunk_pos.y << 4,
)
.add(&delta);
// TODO this.getBrightness(LightLayer.SKY, blockPos) >= 15;
// TODO heightmap
// TODO findLightningRod(blockPos)
// TODO encapsulatingFullBlocks
if true {
// TODO biome.getPrecipitationAt(pos, this.getSeaLevel()) == Biome.Precipitation.RAIN
// TODO this.getCurrentDifficultyAt(blockPos);
if rng().random::<f32>() < 0.0675
&& self.get_block(&random_pos.to_block_pos().down()).await
!= &Block::LIGHTNING_ROD
{
let entity = Entity::new(
Uuid::new_v4(),
self.clone(),
random_pos.to_f64(),
&EntityType::SKELETON_HORSE,
false,
);
self.spawn_entity(Arc::new(entity)).await;
}
let entity = Entity::new(
Uuid::new_v4(),
self.clone(),
random_pos.to_f64().add_raw(0.5, 0., 0.5),
&EntityType::LIGHTNING_BOLT,
false,
);
self.spawn_entity(Arc::new(entity)).await;
}
}
if spawn_list.is_empty() {
return;
}
// TODO this.level.canSpawnEntitiesInChunk(chunkPos)
spawn_for_chunk(self, chunk_pos, chunk, spawn_state, spawn_list).await;
}
pub fn generation_settings(&self) -> &GenerationSettings {
// TODO: this is bad
match self.dimension_type {
@@ -1768,7 +1905,7 @@ impl World {
&self,
pos: Vector3<f64>,
radius: f64,
entity_types: Option<&[EntityType]>,
entity_types: Option<&[&'static EntityType]>,
) -> Option<Arc<dyn EntityBase>> {
// Get regular entities
let entities = self.get_nearby_entities(pos, radius).await;
@@ -2158,7 +2295,7 @@ impl World {
f64::from(pos.0.z) + 0.5 + rand::rng().random_range(-0.25..0.25),
);
let entity = Entity::new(Uuid::new_v4(), self.clone(), pos, EntityType::ITEM, false);
let entity = Entity::new(Uuid::new_v4(), self.clone(), pos, &EntityType::ITEM, false);
let item_entity = Arc::new(ItemEntity::new(entity, stack).await);
self.spawn_entity(item_entity).await;
}
@@ -2208,7 +2345,7 @@ impl World {
Uuid::new_v4(),
self.clone(),
Vector3::new(x, y, z),
EntityType::ITEM,
&EntityType::ITEM,
false,
);
let entity = Arc::new(ItemEntity::new_with_velocity(entity, item, velocity, 10).await);

View File

@@ -0,0 +1,563 @@
use crate::entity::EntityBase;
use crate::entity::r#type::from_type;
use crate::world::World;
use pumpkin_data::biome::Spawner;
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_util::GameMode;
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_world::chunk::{ChunkData, ChunkHeightmapType};
use rand::seq::IndexedRandom;
use rand::{Rng, rng};
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fmt;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
const MAGIC_NUMBER: i32 = 17 * 17;
#[derive(Default, Debug)]
pub struct MobCounts([i32; 8]);
impl MobCounts {
#[inline]
const fn add(&mut self, category: &'static MobCategory) {
self.0[category.id] += 1;
}
#[inline]
const fn can_spawn(&self, category: &'static MobCategory) -> bool {
self.0[category.id] < category.max
}
}
pub struct LocalMobCapCalculator {
world: Arc<World>,
player_mob_counts: HashMap<i32, MobCounts>,
players_near_chunk: HashMap<Vector2<i32>, Vec<i32>>,
}
impl fmt::Debug for LocalMobCapCalculator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("LocalMobCapCalculator")
.field("world", &"<skipped>")
.field("player_mob_counts", &self.player_mob_counts)
.field("players_near_chunk", &self.players_near_chunk)
.finish()
}
}
impl LocalMobCapCalculator {
pub fn new(world: &Arc<World>) -> Self {
Self {
world: world.clone(), // can anybody get rid of this clone?
player_mob_counts: HashMap::new(),
players_near_chunk: HashMap::new(),
}
}
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
}
async fn get_players_near<'b>(
players_near_chunk: &'b mut HashMap<Vector2<i32>, Vec<i32>>,
world: &Arc<World>,
chunk_pos: &Vector2<i32>,
) -> &'b Vec<i32> {
match players_near_chunk.entry(*chunk_pos) {
Entry::Occupied(value) => {
// debug!("chunk {chunk_pos:?} near player {:?}", value.get());
value.into_mut()
}
Entry::Vacant(entry) => {
let mut players = Vec::new();
for (_uuid, player) in world.players.read().await.iter() {
if player.gamemode.load() == GameMode::Spectator {
continue;
}
if Self::calc_distance(*chunk_pos, &player.position()) < 16384. {
players.push(player.entity_id());
}
}
// debug!("chunk {chunk_pos:?} near player {:?}", players);
entry.insert(players)
}
}
}
pub async fn add_mob(&mut self, chunk_pos: &Vector2<i32>, category: &'static MobCategory) {
let players =
Self::get_players_near(&mut self.players_near_chunk, &self.world, chunk_pos).await;
for player in players {
self.player_mob_counts
.entry(*player)
.or_default()
.add(category);
}
// debug!("player_mob_counts {:?}", self.player_mob_counts);
// debug!("players_near_chunk {:?}", self.players_near_chunk);
// debug!("chunk_pos {:?}", chunk_pos);
// debug!("players {:?}", players);
}
pub async fn can_spawn(
&mut self,
category: &'static MobCategory,
chunk_pos: &Vector2<i32>,
) -> bool {
let players =
Self::get_players_near(&mut self.players_near_chunk, &self.world, chunk_pos).await;
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)]
struct PointCharge(Vector3<f64>, f64); // pos charge
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(Vec<PointCharge>);
impl PotentialCalculator {
pub fn add_charge(&mut self, pos: &BlockPos, charge: f64) {
if charge != 0. {
self.0.push(PointCharge(pos.to_f64(), charge));
}
}
pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 {
if charge == 0. {
return 0.;
}
let mut sum: f64 = 0.;
for i in &self.0 {
sum += i.get_potential_change(pos);
}
sum * charge
}
}
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_pos: BlockPos,
last_checked_type: &'static EntityType,
last_charge: f64,
}
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_pos", &self.last_checked_pos)
.field("last_checked_type", &self.last_checked_type.resource_name)
.field("last_charge", &self.last_charge)
.finish()
}
}
impl SpawnState {
pub async fn new(
chunk_count: i32,
entities: &Arc<RwLock<HashMap<Uuid, Arc<dyn EntityBase>>>>,
world: &Arc<World>,
) -> Self {
let mut potential = PotentialCalculator::default();
let mut local_mob_cap = LocalMobCapCalculator::new(world);
let mut counter = MobCounts::default();
for entity in entities.read().await.values() {
let entity_type = &entity.get_entity().entity_type;
#[allow(clippy::overly_complex_bool_expr)]
if entity_type.mob && false || entity_type.category == &MobCategory::MISC {
// TODO (mob.isPersistenceRequired() || mob.requiresCustomPersistence())
continue;
}
let entity_pos = &entity.get_entity().block_pos.load();
let biome = world.level.get_rough_biome(entity_pos).await;
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(&entity.get_entity().chunk_pos.load(), entity_type.category)
.await;
}
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,
// unmodifiable_mob_category_counts: counter,
last_checked_pos: BlockPos::new(i32::MAX, i32::MAX, i32::MAX),
last_checked_type: &EntityType::PLAYER,
last_charge: 0.,
}
}
#[inline]
fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool {
self.mob_category_counts.0[category.id]
< category.max * self.spawnable_chunk_count / MAGIC_NUMBER
}
async fn can_spawn_for_category_local(
&mut self,
category: &'static MobCategory,
chunk_pos: &Vector2<i32>,
) -> bool {
self.local_mob_cap_calculator
.can_spawn(category, chunk_pos)
.await
}
async fn can_spawn(
&mut self,
entity_type: &'static EntityType,
pos: &BlockPos,
world: &Arc<World>,
) -> bool {
self.last_checked_pos = *pos;
self.last_checked_type = entity_type;
// TODO get biome
let biome = world.level.get_rough_biome(pos).await;
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
self.last_charge = cost.charge;
self.spawn_potential
.get_potential_energy_change(pos, cost.charge)
<= cost.energy_budget
} else {
self.last_charge = 0.;
true
}
}
async fn after_spawn(
&mut self,
entity_type: &'static EntityType,
pos: &BlockPos,
world: &Arc<World>,
) {
let charge;
if self.last_checked_pos.eq(pos) && self.last_checked_type == entity_type {
charge = self.last_charge;
} else {
// TODO get biome
let biome = world.level.get_rough_biome(pos).await;
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
charge = cost.charge;
} else {
charge = 0.;
}
}
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)),
entity_type.category,
)
.await;
}
}
#[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(8);
for category in MobCategory::SPAWNING_CATEGORIES {
if (spawn_friendlies || !category.is_friendly)
&& (spawn_enemies || category.is_friendly)
&& (spawn_passives || !category.is_persistent)
&& 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<RwLock<ChunkData>>,
spawn_state: &mut 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(category, chunk_pos)
.await
{
let random_pos = get_random_pos_within(world.min_y, chunk_pos, chunk).await;
// debug!("try random pos: {:?}", random_pos);
if random_pos.0.y > world.min_y {
spawn_category_for_position(category, world, random_pos, chunk_pos, spawn_state)
.await;
}
}
}
}
pub async fn get_random_pos_within(
min_y: i32,
chunk_pos: &Vector2<i32>,
chunk: &Arc<RwLock<ChunkData>>,
) -> BlockPos {
let x = (chunk_pos.x << 4) + rng().random_range(0..16);
let z = (chunk_pos.y << 4) + rng().random_range(0..16);
let temp_y =
chunk
.read()
.await
.heightmap
.get_height(ChunkHeightmapType::WorldSurface, x, z, min_y)
+ 1;
let y = rng().random_range(min_y..=temp_y);
BlockPos::new(x, y, z)
}
pub async fn spawn_category_for_position(
category: &'static MobCategory,
world: &Arc<World>,
pos: BlockPos,
chunk_pos: &Vector2<i32>,
spawn_state: &mut SpawnState,
) {
// TODO StructureManager structureManager = level.structureManager();
// TODO blockState.isRedstoneConductor(chunk, pos) is true then return
let mut spawn_cluster_size = 0;
let mut new_pos = pos;
for _ in 0..3 {
let mut new_x = new_pos.0.x;
let mut new_z = new_pos.0.z;
let mut random_group_size = (rng().random::<f32>() * 4.).ceil() as i32;
let mut inc = 0;
#[allow(unused_variables)]
let mut group_size = 0;
'outer: 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);
new_pos = BlockPos::new(new_x, new_pos.0.y, new_z);
let new_pos_center = new_pos.to_centered_f64();
let player_distance = get_nearest_player(&new_pos_center, world).await;
if player_distance == f64::MAX {
// debug!("player_distance infinity");
return;
}
if !is_right_distance_to_player_and_spawn_point(
&new_pos,
player_distance,
world,
chunk_pos,
) {
// debug!("{new_pos:?} failed, too near to player or spawn point dst: {player_distance}");
inc += 1;
continue;
}
let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else {
// debug!("{new_pos:?} failed, no random spawn mob at category: {category:?}");
break 'outer;
};
random_group_size = rng().random_range(spawner.min_count..=spawner.max_count);
let entity_type =
&EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap();
if !is_valid_spawn_position_for_type(
world,
&new_pos,
category,
entity_type,
player_distance,
)
.await
{
// debug!("{new_pos:?} failed, not valid spawn position");
inc += 1;
continue;
}
if !spawn_state.can_spawn(entity_type, &new_pos, world).await {
// debug!("{new_pos:?} failed, can't spawn at");
inc += 1;
continue;
}
let entity = from_type(entity_type, new_pos_center, world, Uuid::new_v4()).await;
entity
.get_entity()
.set_rotation(rng().random::<f32>() * 360., 0.);
// TODO isValidPositionForMob(level, mob, f)
// TODO spawnGroupData = mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.NATURAL, spawnGroupData);
spawn_cluster_size += 1;
group_size += 1;
world.spawn_entity(entity).await;
spawn_state.after_spawn(entity_type, &new_pos, world).await;
if spawn_cluster_size >= entity_type.limit_per_chunk {
return;
}
//TODO mob.isMaxGroupSizeReached(p)
inc += 1;
}
}
}
pub async fn get_nearest_player(pos: &Vector3<f64>, world: &Arc<World>) -> f64 {
let mut dst = f64::MAX;
for (_uuid, player) in world.players.read().await.iter() {
if player.gamemode.load() == GameMode::Spectator {
continue;
}
let cur_dst = player.position().squared_distance_to_vec(*pos);
if cur_dst < dst {
dst = cur_dst;
}
}
dst
}
pub fn is_right_distance_to_player_and_spawn_point(
pos: &BlockPos,
distance: f64,
_world: &Arc<World>,
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;
}
#[allow(clippy::overly_complex_bool_expr)]
#[allow(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.is_tagged_with_by_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)
// TODO level.noCollision(entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5))
!(category == &MobCategory::MISC
|| (!entity_type.can_spawn_far_from_player
&& distance
> f64::from(entity_type.category.despawn_distance)
* f64::from(entity_type.category.despawn_distance))
|| !entity_type.summonable
|| !is_spawn_position_ok(world, block_pos, entity_type).await)
}
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
.is_tagged_with_by_tag(&MINECRAFT_LAVA),
SpawnLocation::InWater => {
// TODO !level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)
world
.get_fluid(block_pos)
.await
.is_tagged_with_by_tag(&MINECRAFT_WATER)
}
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;
if down.is_side_solid(BlockDirection::Up) {
is_valid_empty_spawn_block(cur) && is_valid_empty_spawn_block(up)
} else {
false
}
}
SpawnLocation::Unrestricted => true,
}
}
#[must_use]
pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool {
if state.is_solid() {
false
} else if false {
// TODO isSignalSource
false
} else if state.is_liquid() {
false
} else {
// TODO !entityType.isBlockDangerous(blockState);
!Block::from_state_id(state.id)
.is_tagged_with_by_tag(&MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE)
}
}