chore: batch some things together

also added DrownedEntity
This commit is contained in:
Alexander Medvedev
2026-01-14 22:31:58 +01:00
parent 6889399e65
commit b8e0dc85e8
11 changed files with 237 additions and 116 deletions

View File

@@ -226,21 +226,28 @@ impl BlockPos {
}
pub fn chunk_and_chunk_relative_position(&self) -> (Vector2<i32>, Vector3<i32>) {
let (z_chunk, z_rem) = self.0.z.div_rem_euclid(&16);
let (x_chunk, x_rem) = self.0.x.div_rem_euclid(&16);
(
Vector2 {
x: x_chunk,
y: z_chunk,
},
// Since we divide by 16, remnant can never exceed u8
Vector3 {
x: x_rem,
z: z_rem,
y: self.0.y,
},
)
(self.chunk_position(), self.chunk_relative_position())
}
pub fn chunk_position(&self) -> Vector2<i32> {
let z_chunk = self.0.z.div_euclid(16);
let x_chunk = self.0.x.div_euclid(16);
Vector2 {
x: x_chunk,
y: z_chunk,
}
}
pub fn chunk_relative_position(&self) -> Vector3<i32> {
let z_chunk = self.0.z.rem_euclid(16);
let x_chunk = self.0.x.rem_euclid(16);
Vector3 {
x: x_chunk,
y: self.0.y,
z: z_chunk,
}
}
pub fn section_relative_position(&self) -> Vector3<i32> {
let (_z_chunk, z_rem) = self.0.z.div_rem_euclid(&16);
let (_x_chunk, x_rem) = self.0.x.div_rem_euclid(&16);

View File

@@ -844,9 +844,7 @@ impl Level {
delay: u8,
priority: TickPriority,
) {
let chunk = self
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let mut chunk = chunk.write().await;
chunk.block_ticks.schedule_tick(
&ScheduledTick {
@@ -867,9 +865,7 @@ impl Level {
delay: u8,
priority: TickPriority,
) {
let chunk = self
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let mut chunk = chunk.write().await;
chunk.fluid_ticks.schedule_tick(
&ScheduledTick {
@@ -888,9 +884,7 @@ impl Level {
block_pos: &BlockPos,
block: &Block,
) -> bool {
let chunk = self
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let chunk = chunk.read().await;
chunk.block_ticks.is_scheduled(*block_pos, block)
}
@@ -900,9 +894,7 @@ impl Level {
block_pos: &BlockPos,
fluid: &Fluid,
) -> bool {
let chunk = self
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let chunk = chunk.read().await;
chunk.fluid_ticks.is_scheduled(*block_pos, fluid)
}

View File

@@ -93,7 +93,6 @@ impl LivingEntity {
} else {
0.8
};
// TODO: Extract default MOVEMENT_SPEED Entity Attribute
let default_movement_speed = 0.25;
let health = entity.entity_type.max_health.unwrap_or(20.0);
@@ -361,8 +360,8 @@ impl LivingEntity {
.slipperiness,
);
let speed =
self.movement_speed.load() * 0.216 / (slipperiness * slipperiness * slipperiness);
let speed = self.movement_speed.load() * 0.216_000_02
/ (slipperiness * slipperiness * slipperiness);
(speed, slipperiness * 0.91)
} else {

View File

@@ -0,0 +1,26 @@
use std::sync::Arc;
use crate::entity::{
Entity, NBTStorage,
mob::{Mob, MobEntity, zombie::ZombieEntity},
};
pub struct DrownedEntity {
entity: Arc<ZombieEntity>,
}
impl DrownedEntity {
pub async fn make(entity: Entity) -> Arc<Self> {
Arc::new(Self {
entity: ZombieEntity::make(entity).await,
})
}
}
impl NBTStorage for DrownedEntity {}
impl Mob for DrownedEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.entity.mob_entity
}
}

View File

@@ -14,6 +14,8 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering::Relaxed;
use tokio::sync::Mutex;
pub mod drowned;
pub mod skeleton;
pub mod zombie;
pub struct MobEntity {

View File

@@ -0,0 +1,55 @@
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use crate::entity::{
Entity, NBTStorage,
ai::goal::{
active_target_goal::ActiveTargetGoal, look_around_goal::LookAroundGoal,
look_at_entity::LookAtEntityGoal,
},
mob::{Mob, MobEntity},
};
//pub mod skeleton;
pub struct SkeletonEntityBase {
pub mob_entity: MobEntity,
}
impl SkeletonEntityBase {
pub async fn make(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let mob = Self { mob_entity };
let mob_arc = Arc::new(mob);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
{
let mut goal_selector = mob_arc.mob_entity.goals_selector.lock().await;
let mut target_selector = mob_arc.mob_entity.target_selector.lock().await;
goal_selector.add_goal(
8,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(8, Box::new(LookAroundGoal::default()));
target_selector.add_goal(
2,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
);
};
mob_arc
}
}
impl NBTStorage for SkeletonEntityBase {}
impl Mob for SkeletonEntityBase {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
}

View File

@@ -19,11 +19,11 @@ use rand::{Rng, rng};
use std::pin::Pin;
use std::sync::{Arc, Weak};
pub struct Zombie {
mob_entity: MobEntity,
pub struct ZombieEntity {
pub mob_entity: MobEntity,
}
impl Zombie {
impl ZombieEntity {
pub async fn make(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let zombie = Self { mob_entity };
@@ -55,9 +55,9 @@ impl Zombie {
}
}
impl NBTStorage for Zombie {}
impl NBTStorage for ZombieEntity {}
impl Mob for Zombie {
impl Mob for ZombieEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}

View File

@@ -9,7 +9,7 @@ use crate::{
Entity, EntityBase,
decoration::{end_crystal::EndCrystalEntity, painting::PaintingEntity},
living::LivingEntity,
mob::zombie::Zombie,
mob::{drowned::DrownedEntity, zombie::ZombieEntity},
},
world::World,
};
@@ -23,7 +23,8 @@ pub async fn from_type(
let entity = Entity::new(uuid, world.clone(), position, entity_type, false);
let mob: Arc<dyn EntityBase> = match entity_type.id {
id if id == EntityType::ZOMBIE.id => Zombie::make(entity).await,
id if id == EntityType::ZOMBIE.id => ZombieEntity::make(entity).await,
id if id == EntityType::DROWNED.id => DrownedEntity::make(entity).await,
id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)),
id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),
// Fallback Entity

View File

@@ -236,10 +236,7 @@ impl World {
// First lets see if the entity was saved on an other chunk, and if the current chunk does not match we remove it
// Otherwise we just update the nbt data
let base_entity = entity.get_entity();
let (current_chunk_coordinate, _) = base_entity
.block_pos
.load()
.chunk_and_chunk_relative_position();
let current_chunk_coordinate = base_entity.block_pos.load().chunk_position();
let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await;
if let Some(old_chunk) = base_entity.first_loaded_chunk_position.load() {
@@ -263,8 +260,7 @@ impl World {
}
async fn remove_entity_data(&self, entity: &Entity) {
let (current_chunk_coordinate, _) =
entity.block_pos.load().chunk_and_chunk_relative_position();
let current_chunk_coordinate = entity.block_pos.load().chunk_position();
if let Some(old_chunk) = entity.first_loaded_chunk_position.load() {
let old_chunk = old_chunk.to_vec2_i32();
let chunk = self.level.get_entity_chunk(old_chunk).await;
@@ -351,7 +347,7 @@ impl World {
pub async fn broadcast_packet_all<P: ClientPacket>(&self, packet: &P) {
let current_players = self.players.read().await;
for (_, player) in current_players.iter() {
for player in current_players.values() {
player.client.enqueue_packet(packet).await;
}
}
@@ -378,7 +374,7 @@ impl World {
) {
let current_players = self.players.read().await;
for (_, player) in current_players.iter() {
for player in current_players.values() {
match &player.client {
ClientPlatform::Java(client) => client.enqueue_packet(je_packet).await,
ClientPlatform::Bedrock(client) => client.send_game_packet(be_packet).await,
@@ -563,7 +559,6 @@ impl World {
self.flush_block_updates().await;
self.flush_synced_block_events().await;
self.tick_environment().await;
let env_done = start.elapsed();
// 2. Chunks
let chunk_start = tokio::time::Instant::now();
@@ -610,14 +605,13 @@ impl World {
let total_elapsed = start.elapsed();
if total_elapsed.as_millis() > 50 {
log::warn!(
"Slow Tick [{}ms]: Chunks: {:?} | Players({}): {:?} | Entities({}): {:?} | Env: {:?}",
"Slow Tick [{}ms]: Chunks: {:?} | Players({}): {:?} | Entities({}): {:?}",
total_elapsed.as_millis(),
chunk_elapsed,
player_count,
player_elapsed,
entity_count,
entity_elapsed,
env_done
);
}
}
@@ -1843,6 +1837,7 @@ impl World {
// NOTE: This function doesn't actually await on anything, it just spawns two tokio tasks
/// IMPORTANT: Chunks have to be non-empty
#[expect(clippy::too_many_lines)]
fn spawn_world_entity_chunks(
self: &Arc<Self>,
player: Arc<Player>,
@@ -1896,6 +1891,7 @@ impl World {
let mut ids = Vec::new();
// Remove all the entities from the world
let mut entities = world.entities.write().await;
for (uuid, entity_nbt) in &chunk.read().await.data {
let Some(id) = entity_nbt.get_string("id") else {
log::warn!("Entity has no ID");
@@ -1917,8 +1913,30 @@ impl World {
entities.remove(&base_entity.entity_uuid);
ids.push(VarInt(base_entity.entity_id));
world.save_entity(uuid, &entity).await;
let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await;
if let Some(old_chunk) = base_entity.first_loaded_chunk_position.load() {
let old_chunk = old_chunk.to_vec2_i32();
let chunk = world.level.get_entity_chunk(old_chunk).await;
let mut chunk = chunk.write().await;
chunk.mark_dirty(true);
let base_entity = entity.get_entity();
let current_chunk_coordinate =
base_entity.block_pos.load().chunk_position();
if old_chunk == current_chunk_coordinate {
chunk.data.insert(*uuid, nbt);
return;
}
// The chunk has changed, lets remove the entity from the old chunk
chunk.data.remove(uuid);
}
let mut chunk = chunk.write().await;
chunk.data.insert(*uuid, nbt);
chunk.mark_dirty(true);
}
if !ids.is_empty() {
player
.client
@@ -2268,17 +2286,15 @@ impl World {
.await;
entity.init_data_tracker().await;
let (chunk_coordinate, _) = base_entity
.block_pos
.load()
.chunk_and_chunk_relative_position();
let chunk_coordinate = base_entity.block_pos.load().chunk_position();
let chunk = self.level.get_entity_chunk(chunk_coordinate).await;
let mut chunk = chunk.write().await;
let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await;
chunk.data.insert(base_entity.entity_uuid, nbt);
chunk.mark_dirty(true);
drop(chunk);
{
let mut chunk = chunk.write().await;
let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await;
chunk.data.insert(base_entity.entity_uuid, nbt);
chunk.mark_dirty(true);
};
let mut current_entities = self.entities.write().await;
current_entities.insert(base_entity.entity_uuid, entity);
@@ -3016,10 +3032,7 @@ impl World {
}
pub async fn get_block_entity(&self, block_pos: &BlockPos) -> Option<Arc<dyn BlockEntity>> {
let chunk = self
.level
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.level.get_chunk(block_pos.chunk_position()).await;
let chunk: tokio::sync::RwLockReadGuard<ChunkData> = chunk.read().await;
chunk.block_entities.get(block_pos).cloned()
@@ -3027,10 +3040,7 @@ impl World {
pub async fn add_block_entity(&self, block_entity: Arc<dyn BlockEntity>) {
let block_pos = block_entity.get_position();
let chunk = self
.level
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.level.get_chunk(block_pos.chunk_position()).await;
let mut chunk: tokio::sync::RwLockWriteGuard<ChunkData> = chunk.write().await;
let block_entity_nbt = block_entity.chunk_data_nbt();
@@ -3050,10 +3060,7 @@ impl World {
}
pub async fn remove_block_entity(&self, block_pos: &BlockPos) {
let chunk = self
.level
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.level.get_chunk(block_pos.chunk_position()).await;
let mut chunk: tokio::sync::RwLockWriteGuard<ChunkData> = chunk.write().await;
chunk.block_entities.remove(block_pos);
chunk.mark_dirty(true);
@@ -3061,10 +3068,7 @@ impl World {
pub async fn update_block_entity(&self, block_entity: &Arc<dyn BlockEntity>) {
let block_pos = block_entity.get_position();
let chunk = self
.level
.get_chunk(block_pos.chunk_and_chunk_relative_position().0)
.await;
let chunk = self.level.get_chunk(block_pos.chunk_position()).await;
let mut chunk: tokio::sync::RwLockWriteGuard<ChunkData> = chunk.write().await;
let block_entity_nbt = block_entity.chunk_data_nbt();

View File

@@ -8,6 +8,7 @@ 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;
@@ -16,6 +17,7 @@ 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 rand::seq::IndexedRandom;
use rand::{Rng, rng};
@@ -42,8 +44,8 @@ impl MobCounts {
}
}
#[derive(Default)]
pub struct LocalMobCapCalculator {
world: Arc<World>,
player_mob_counts: HashMap<i32, MobCounts>,
players_near_chunk: HashMap<Vector2<i32>, Vec<i32>>,
}
@@ -59,14 +61,6 @@ impl fmt::Debug for LocalMobCapCalculator {
}
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;
@@ -79,10 +73,7 @@ impl LocalMobCapCalculator {
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::Occupied(value) => value.into_mut(),
Entry::Vacant(entry) => {
let mut players = Vec::new();
for (_uuid, player) in world.players.read().await.iter() {
@@ -93,14 +84,17 @@ impl LocalMobCapCalculator {
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;
pub async fn add_mob(
&mut self,
chunk_pos: &Vector2<i32>,
world: &Arc<World>,
category: &'static MobCategory,
) {
let players = Self::get_players_near(&mut self.players_near_chunk, world, chunk_pos).await;
for player in players {
self.player_mob_counts
.entry(*player)
@@ -115,10 +109,10 @@ impl LocalMobCapCalculator {
pub async fn can_spawn(
&mut self,
category: &'static MobCategory,
world: &Arc<World>,
chunk_pos: &Vector2<i32>,
) -> bool {
let players =
Self::get_players_near(&mut self.players_near_chunk, &self.world, chunk_pos).await;
let players = Self::get_players_near(&mut self.players_near_chunk, world, chunk_pos).await;
for player in players {
if let Some(count) = self.player_mob_counts.get(player) {
if count.can_spawn(category) {
@@ -195,7 +189,7 @@ impl SpawnState {
world: &Arc<World>,
) -> Self {
let mut potential = PotentialCalculator::default();
let mut local_mob_cap = LocalMobCapCalculator::new(world);
let mut local_mob_cap = LocalMobCapCalculator::default();
let mut counter = MobCounts::default();
for entity in entities.read().await.values() {
let entity = entity.get_entity();
@@ -211,7 +205,7 @@ impl SpawnState {
}
if entity_type.mob {
local_mob_cap
.add_mob(&entity.chunk_pos.load(), entity_type.category)
.add_mob(&entity.chunk_pos.load(), world, entity_type.category)
.await;
}
counter.add(entity_type.category);
@@ -234,11 +228,12 @@ impl SpawnState {
}
async fn can_spawn_for_category_local(
&mut self,
world: &Arc<World>,
category: &'static MobCategory,
chunk_pos: &Vector2<i32>,
) -> bool {
self.local_mob_cap_calculator
.can_spawn(category, chunk_pos)
.can_spawn(category, world, chunk_pos)
.await
}
async fn can_spawn(
@@ -285,6 +280,7 @@ impl SpawnState {
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,
)
.await;
@@ -331,7 +327,7 @@ pub async fn spawn_for_chunk(
// debug!("spawn for chunk {:?}", chunk_pos);
for category in spawn_list {
if spawn_state
.can_spawn_for_category_local(category, chunk_pos)
.can_spawn_for_category_local(world, category, chunk_pos)
.await
{
let random_pos = get_random_pos_within(world.min_y, chunk_pos, chunk).await;
@@ -371,25 +367,30 @@ pub async fn spawn_category_for_position(
) {
// TODO StructureManager structureManager = level.structureManager();
// TODO blockState.isRedstoneConductor(chunk, pos) is true then return
let mut batch_buffer = vec![];
let mut spawn_cluster_size = 0;
let mut new_pos = pos;
let player_positions: Vec<_> = world
.players
.read()
.await
.values()
.map(|p| p.position())
.collect();
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;
'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 !is_right_distance_to_player_and_spawn_point(
&new_pos,
player_distance,
world,
chunk_pos,
) {
let player_distance = get_nearest_player(&new_pos_center, &player_positions);
if !is_right_distance_to_player_and_spawn_point(&new_pos, player_distance, chunk_pos) {
inc += 1;
continue;
}
@@ -423,7 +424,7 @@ pub async fn spawn_category_for_position(
// TODO spawnGroupData = mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.NATURAL, spawnGroupData);
spawn_cluster_size += 1;
//group_size += 1;
world.spawn_entity(entity).await;
batch_buffer.push(entity);
spawn_state.after_spawn(entity_type, &new_pos, world).await;
if spawn_cluster_size >= entity_type.limit_per_chunk {
return;
@@ -433,25 +434,59 @@ pub async fn spawn_category_for_position(
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;
// 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;
prepared_data.push((base_entity.entity_uuid, nbt, packet));
}
let cur_dst = player.position().squared_distance_to_vec(*pos);
if cur_dst < dst {
dst = cur_dst;
{
let chunk_handle = world.level.get_entity_chunk(*chunk_pos).await;
let mut chunk_lock = chunk_handle.write().await;
let mut entities_lock = world.entities.write().await;
for (uuid, nbt, _) in &prepared_data {
let entity_ref = batch_buffer
.iter()
.find(|e| e.get_entity().entity_uuid == *uuid)
.unwrap();
entities_lock.insert(*uuid, entity_ref.clone());
chunk_lock.data.insert(*uuid, nbt.clone());
}
chunk_lock.mark_dirty(true);
};
for (_, _, packet) in prepared_data {
world.broadcast_packet_all(&packet).await;
}
}
dst
}
#[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,
_world: &Arc<World>,
chunk_pos: &Vector2<i32>,
) -> bool {
if distance <= 24. * 24. {