feat: guess living entities

This commit is contained in:
Alexander Medvedev 2026-01-13 23:36:49 +01:00
parent 57db90ce1a
commit 0555f804e0
21 changed files with 135 additions and 84 deletions

View File

@ -279,8 +279,9 @@ impl ToTokens for BlockPropertyStruct {
}
#[inline]
#[allow(clippy::manual_range_patterns)]
fn handles_block_id(block_id: u16) -> bool where Self: Sized {
[#(#block_ids),*].contains(&block_id)
matches!(block_id, #(#block_ids)|*)
}
fn to_state_id(&self, block: &Block) -> u16 {
@ -291,14 +292,22 @@ impl ToTokens for BlockPropertyStruct {
}
fn from_state_id(state_id: u16, block: &Block) -> Self {
if !Self::handles_block_id(block.id) {
panic!("{} is not a valid block for {}", &block.name, #struct_name);
}
if state_id >= block.states[0].id && state_id <= block.states.last().unwrap().id {
let index = state_id - block.states[0].id;
Self::from_index(index)
debug_assert!(
Self::handles_block_id(block.id),
"{} is not a valid block for {}", &block.name, #struct_name
);
let min_id = block.states[0].id;
let max_id = block.states.last().map(|s| s.id).unwrap_or(min_id);
if (min_id..=max_id).contains(&state_id) {
Self::from_index(state_id - min_id)
} else {
#[cfg(debug_assertions)]
panic!("State ID {} does not exist for {}", state_id, &block.name);
#[cfg(not(debug_assertions))]
Self::from_index(0)
}
}
@ -313,8 +322,10 @@ impl ToTokens for BlockPropertyStruct {
vec![ #(#to_props_entries),* ]
}
#[allow(clippy::manual_range_patterns)]
fn from_props(props: &[(&str, &str)], block: &Block) -> Self {
if ![#(#block_ids),*].contains(&block.id) {
#[cfg(debug_assertions)]
if !matches!(block.id, #(#block_ids)|*) {
panic!("{} is not a valid block for {}", &block.name, #struct_name);
}
let mut block_props = Self::default(block);

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, fs};
use std::{collections::BTreeMap, fs};
use heck::ToPascalCase;
use proc_macro2::TokenStream;
@ -7,7 +7,7 @@ use quote::{format_ident, quote};
pub(crate) fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/meta_data_type.json");
let handlers_map: HashMap<String, i32> =
let handlers_map: BTreeMap<String, i32> =
serde_json::from_str(&fs::read_to_string("../assets/meta_data_type.json").unwrap())
.expect("Failed to parse meta_data_type.json");

View File

@ -218,13 +218,7 @@ impl ChunkHeightmaps {
data[packed_array_idx] = data[packed_array_idx].bitand(mask).bitor(height);
}
pub fn get_height(&self, _type: ChunkHeightmapType, x: i32, z: i32, min_y: i32) -> i32 {
let data = match _type {
ChunkHeightmapType::WorldSurface => &self.world_surface,
ChunkHeightmapType::MotionBlocking => &self.motion_blocking,
ChunkHeightmapType::MotionBlockingNoLeaves => &self.motion_blocking_no_leaves,
};
pub fn get_height(&self, heightmap: ChunkHeightmapType, x: i32, z: i32, min_y: i32) -> i32 {
let local_x = (x & 15) as usize;
let local_z = (z & 15) as usize;
@ -244,6 +238,11 @@ impl ChunkHeightmaps {
let packed_array_idx = column_idx / 7;
let data = match heightmap {
ChunkHeightmapType::WorldSurface => &self.world_surface,
ChunkHeightmapType::MotionBlocking => &self.motion_blocking,
ChunkHeightmapType::MotionBlockingNoLeaves => &self.motion_blocking_no_leaves,
};
let height_bit_bytes_i64 = data[packed_array_idx].bitand(mask).to_ne_bytes();
(u64::from_ne_bytes(height_bit_bytes_i64)

View File

@ -239,8 +239,10 @@ impl WouldSurviveBlockPredicate {
pos: &BlockPos,
) -> bool {
let block = self.state.get_block();
let state = self.state.get_state();
let pos = self.offset.get(pos);
block_registry.can_place_at(block, chunk, &pos, BlockDirection::Up)
block_registry.can_place_at(block, state, chunk, &pos)
}
}

View File

@ -1,5 +1,5 @@
use pumpkin_data::{
Block, BlockDirection, BlockState,
Block, BlockState,
block_properties::{BambooLeaves, BambooLikeProperties, BlockProperties, Integer0To1},
tag,
tag::Taggable,
@ -32,7 +32,8 @@ impl BambooFeature {
) -> bool {
let mut i = 0;
if chunk.is_air(&pos.0) {
if block_registry.can_place_at(&Block::BAMBOO, chunk, &pos, BlockDirection::Up) {
if block_registry.can_place_at(&Block::BAMBOO, Block::BAMBOO.default_state, chunk, &pos)
{
let height = random.next_bounded_i32(12) + 5;
if random.next_f32() < self.probability {
let rnd = random.next_bounded_i32(4) + 1;

View File

@ -1,4 +1,4 @@
use pumpkin_data::{Block, BlockDirection, tag, tag::Taggable};
use pumpkin_data::{Block, tag, tag::Taggable};
use pumpkin_util::{
math::position::BlockPos,
random::{RandomGenerator, RandomImpl},
@ -47,7 +47,7 @@ impl NetherForestVegetationFeature {
let nether_block = Block::from_state_id(nether_state.id);
if !chunk.is_air(&pos.0)
|| pos.0.y <= chunk.bottom_y() as i32
|| block_registry.can_place_at(nether_block, chunk, &pos, BlockDirection::Up)
|| block_registry.can_place_at(nether_block, nether_state, chunk, &pos)
{
continue;
}

View File

@ -1,4 +1,4 @@
use pumpkin_data::{Block, BlockDirection};
use pumpkin_data::Block;
use pumpkin_util::{math::position::BlockPos, random::RandomGenerator};
use serde::Deserialize;
@ -25,7 +25,7 @@ impl SimpleBlockFeature {
let state = self.to_place.get(random, pos);
let block = Block::from_state_id(state.id);
let block_accessor: &dyn BlockAccessor = chunk;
if !block_registry.can_place_at(block, block_accessor, &pos, BlockDirection::Up) {
if !block_registry.can_place_at(block, state, block_accessor, &pos) {
return false;
}

View File

@ -121,9 +121,9 @@ pub trait BlockRegistryExt: Send + Sync {
fn can_place_at(
&self,
block: &Block,
state: &BlockState,
block_accessor: &dyn BlockAccessor,
block_pos: &BlockPos,
face: BlockDirection,
) -> bool;
}

View File

@ -224,8 +224,8 @@ impl BlockBehaviour for FireBlock {
world: Some(args.world),
block_accessor: args.world,
block: &Block::FIRE,
state: Block::FIRE.default_state,
position: args.position,
direction: BlockDirection::Up,
player: None,
use_item_on: None,
})
@ -275,8 +275,8 @@ impl BlockBehaviour for FireBlock {
world: Some(world),
block_accessor: world.as_ref(),
block,
state: block.default_state,
position: pos,
direction: BlockDirection::Up,
player: None,
use_item_on: None,
})

View File

@ -56,8 +56,8 @@ impl FireBlockBase {
world: Some(world),
block_accessor: world.as_ref(),
block: &Block::SOUL_FIRE,
state: Block::SOUL_FIRE.default_state,
position: block_pos,
direction: BlockDirection::Up,
player: None,
use_item_on: None,
})
@ -69,8 +69,8 @@ impl FireBlockBase {
world: Some(world),
block_accessor: world.as_ref(),
block: &Block::FIRE,
state: Block::FIRE.default_state,
position: block_pos,
direction: BlockDirection::Up,
player: None,
use_item_on: None,
})

View File

@ -27,11 +27,8 @@ impl BlockBehaviour for GrindstoneBlock {
})
}
fn can_place_at<'a>(&'a self, args: CanPlaceAtArgs<'a>) -> BlockFuture<'a, bool> {
Box::pin(async move {
WallMountedBlock::can_place_at(self, args.block_accessor, args.position, args.direction)
.await
})
fn can_place_at<'a>(&'a self, _args: CanPlaceAtArgs<'a>) -> BlockFuture<'a, bool> {
Box::pin(async move { true })
}
fn get_state_for_neighbor_update<'a>(

View File

@ -3,9 +3,9 @@ use crate::block::{
OnScheduledTickArgs,
};
use crate::world::World;
use pumpkin_data::BlockDirection;
use pumpkin_data::block_properties::HorizontalFacing;
use pumpkin_data::block_properties::{BlockProperties, LadderLikeProperties};
use pumpkin_data::{BlockDirection, HorizontalFacingExt};
use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::BlockStateId;
@ -25,11 +25,12 @@ impl BlockBehaviour for LadderBlock {
}
fn can_place_at<'a>(&'a self, args: CanPlaceAtArgs<'a>) -> BlockFuture<'a, bool> {
Box::pin(async move {
let props = LadderLikeProperties::from_state_id(args.state.id, args.block);
args.block_accessor
.get_block_state(&args.use_item_on.unwrap().position)
.await
.is_side_solid(args.direction.opposite())
&& args.direction.is_horizontal()
.is_side_solid(props.facing.opposite().to_block_direction())
})
}

View File

@ -138,8 +138,13 @@ impl BlockBehaviour for ButtonBlock {
fn can_place_at<'a>(&'a self, args: CanPlaceAtArgs<'a>) -> BlockFuture<'a, bool> {
Box::pin(async move {
WallMountedBlock::can_place_at(self, args.block_accessor, args.position, args.direction)
.await
WallMountedBlock::can_place_at(
self,
args.block_accessor,
args.position,
self.get_direction(args.state.id, args.block),
)
.await
})
}

View File

@ -104,8 +104,13 @@ impl BlockBehaviour for LeverBlock {
fn can_place_at<'a>(&'a self, args: CanPlaceAtArgs<'a>) -> BlockFuture<'a, bool> {
Box::pin(async move {
WallMountedBlock::can_place_at(self, args.block_accessor, args.position, args.direction)
.await
WallMountedBlock::can_place_at(
self,
args.block_accessor,
args.position,
self.get_direction(args.state.id, args.block),
)
.await
})
}

View File

@ -1,7 +1,7 @@
use std::sync::Arc;
use pumpkin_data::{
Block, BlockDirection,
Block, BlockDirection, HorizontalFacingExt,
block_properties::BlockProperties,
sound::{Sound, SoundCategory},
};
@ -45,7 +45,14 @@ impl BlockBehaviour for TripwireHookBlock {
fn can_place_at<'a>(&'a self, args: CanPlaceAtArgs<'a>) -> BlockFuture<'a, bool> {
Box::pin(async move {
Self::can_place_at(args.block_accessor, args.position, args.direction).await
let props = TripwireHookProperties::from_state_id(args.state.id, args.block);
Self::can_place_at(
args.block_accessor,
args.position,
props.facing.to_block_direction(),
)
.await
})
}

View File

@ -230,8 +230,8 @@ pub struct CanPlaceAtArgs<'a> {
pub world: Option<&'a World>,
pub block_accessor: &'a dyn BlockAccessor,
pub block: &'a Block,
pub state: &'a BlockState,
pub position: &'a BlockPos,
pub direction: BlockDirection,
pub player: Option<&'a Player>,
pub use_item_on: Option<&'a SUseItemOn>,
}

View File

@ -306,9 +306,9 @@ impl BlockRegistryExt for BlockRegistry {
fn can_place_at(
&self,
block: &pumpkin_data::Block,
state: &BlockState,
block_accessor: &dyn BlockAccessor,
block_pos: &BlockPos,
face: BlockDirection,
) -> bool {
futures::executor::block_on(async move {
self.can_place_at(
@ -317,8 +317,8 @@ impl BlockRegistryExt for BlockRegistry {
block_accessor,
None,
block,
state,
block_pos,
face,
None,
)
.await
@ -493,8 +493,8 @@ impl BlockRegistry {
block_accessor: &dyn BlockAccessor,
player: Option<&Player>,
block: &Block,
state: &BlockState,
position: &BlockPos,
direction: BlockDirection,
use_item_on: Option<&SUseItemOn>,
) -> bool {
let pumpkin_block = self.get_pumpkin_block(block);
@ -505,8 +505,8 @@ impl BlockRegistry {
world,
block_accessor,
block,
state,
position,
direction,
player,
use_item_on,
})

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use pumpkin_data::entity::EntityType;
use pumpkin_data::entity::{EntityType, MobCategory};
use pumpkin_util::math::vector3::Vector3;
use uuid::Uuid;
@ -8,6 +8,7 @@ use crate::{
entity::{
Entity, EntityBase,
decoration::{end_crystal::EndCrystalEntity, painting::PaintingEntity},
living::LivingEntity,
mob::zombie::Zombie,
},
world::World,
@ -25,8 +26,14 @@ pub async fn from_type(
id if id == EntityType::ZOMBIE.id => Zombie::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)),
// TODO
_ => Arc::new(entity), // Fallback Entity
// Fallback Entity
_ => {
if entity_type.category == &MobCategory::MISC {
Arc::new(entity)
} else {
Arc::new(LivingEntity::new(entity))
}
}
};
mob

View File

@ -1999,8 +1999,8 @@ impl JavaClient {
world.as_ref(),
Some(player),
block,
block.default_state,
&final_block_pos,
final_face,
Some(&use_item_on),
)
.await

View File

@ -559,40 +559,41 @@ impl World {
pub async fn tick(self: &Arc<Self>, server: &Server) {
let start = tokio::time::Instant::now();
// 1. Block & Environment
self.flush_block_updates().await;
// tick block entities
self.flush_synced_block_events().await;
self.tick_environment().await;
let env_done = start.elapsed();
// 2. Chunks
let chunk_start = tokio::time::Instant::now();
// log::debug!("Ticking chunks");
self.tick_chunks().await;
let chunk_elapsed = chunk_start.elapsed();
// 3. Players
let player_start = tokio::time::Instant::now();
let players_to_tick: Vec<_> = self.players.read().await.values().cloned().collect();
// log::debug!("Ticking players");
// player ticks
for player in players_to_tick {
let player_count = players_to_tick.len();
for player in &players_to_tick {
player.tick(server).await;
}
let player_elapsed = player_start.elapsed();
// 4. Entities & Collision
let entity_start = tokio::time::Instant::now();
let entities_to_tick: Vec<_> = self.entities.read().await.values().cloned().collect();
let entity_count = entities_to_tick.len();
// log::debug!("Ticking entities");
// Entity ticks
for entity in entities_to_tick {
entity.get_entity().age.fetch_add(1, Relaxed);
entity.tick(entity.clone(), server).await;
for player in self.players.read().await.values() {
for player in &players_to_tick {
if player
.living_entity
.entity
.bounding_box
.load()
// This is vanilla, but TODO: change this when is in a vehicle
.expand(1.0, 0.5, 1.0)
.intersects(&entity.get_entity().bounding_box.load())
{
@ -601,14 +602,22 @@ impl World {
}
}
}
let entity_elapsed = entity_start.elapsed();
self.level.chunk_loading.lock().unwrap().send_change();
if start.elapsed().as_millis() > 50 {
// 5. Detailed Slow Tick Logging
let total_elapsed = start.elapsed();
if total_elapsed.as_millis() > 50 {
log::warn!(
"Slow Tick: Total {:?} | Chunks {:?}",
start.elapsed(),
"Slow Tick [{}ms]: Chunks: {:?} | Players({}): {:?} | Entities({}): {:?} | Env: {:?}",
total_elapsed.as_millis(),
chunk_elapsed,
player_count,
player_elapsed,
entity_count,
entity_elapsed,
env_done
);
}
}

View File

@ -14,6 +14,8 @@ 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::{ChunkData, ChunkHeightmapType};
use rand::seq::IndexedRandom;
use rand::{Rng, rng};
@ -131,7 +133,7 @@ impl LocalMobCapCalculator {
}
#[derive(Debug)]
struct PointCharge(Vector3<f64>, f64); // pos charge
struct PointCharge(Vector3<f64>, f64);
impl PointCharge {
fn get_potential_change(&self, pos: &BlockPos) -> f64 {
@ -296,13 +298,23 @@ pub fn get_filtered_spawning_categories(
spawn_enemies: bool,
spawn_passives: bool,
) -> Vec<&'static MobCategory> {
let mut ret = Vec::with_capacity(8);
let mut ret = Vec::with_capacity(MobCategory::SPAWNING_CATEGORIES.len());
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)
{
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);
}
}
@ -323,7 +335,6 @@ pub async fn spawn_for_chunk(
.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;
@ -336,8 +347,10 @@ pub async fn get_random_pos_within(
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 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
.read()
@ -345,7 +358,7 @@ pub async fn get_random_pos_within(
.heightmap
.get_height(ChunkHeightmapType::WorldSurface, x, z, min_y)
+ 1;
let y = rng().random_range(min_y..=temp_y);
let y = rng.next_inbetween_i32(min_y, temp_y);
BlockPos::new(x, y, z)
}
@ -365,8 +378,6 @@ pub async fn spawn_category_for_position(
let mut new_z = new_pos.0.z;
let mut random_group_size = (rng().random::<f32>() * 4.).ceil() as i32;
let mut inc = 0;
#[expect(unused_variables)]
let 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);
@ -379,12 +390,10 @@ pub async fn spawn_category_for_position(
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);
@ -399,12 +408,10 @@ pub async fn spawn_category_for_position(
)
.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;
}