chore: this should speed things up

This commit is contained in:
Alexander Medvedev
2026-01-20 18:59:05 +01:00
parent d737b56cbc
commit afc4cb0cd3
21 changed files with 430 additions and 383 deletions

View File

@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::chunk::ChunkConfig;
#[derive(Deserialize, Serialize, Default)]
#[derive(Deserialize, Serialize, Default, Clone)]
pub struct LevelConfig {
pub chunk: ChunkConfig,
// TODO: More options

View File

@@ -428,12 +428,17 @@ impl PistonBehavior {
}
impl BlockState {
const IS_AIR: u16 = 1 << 0;
const HAS_RANDOM_TICKS: u16 = 1 << 9;
fn has_random_ticks(&self) -> bool {
self.state_flags & Self::HAS_RANDOM_TICKS != 0
}
pub const fn is_air(&self) -> bool {
self.state_flags & Self::IS_AIR != 0
}
fn to_tokens(&self) -> TokenStream {
let mut tokens = TokenStream::new();
let id = LitInt::new(&self.id.to_string(), Span::call_site());
@@ -659,6 +664,8 @@ pub(crate) fn build() -> TokenStream {
.collect();
let mut random_tick_states = Vec::new();
let mut air_states = Vec::new();
let mut constants_list = Vec::new();
let mut block_from_name_entries = Vec::new();
let mut block_from_item_id_arms = Vec::new();
@@ -679,6 +686,10 @@ pub(crate) fn build() -> TokenStream {
let state_id = LitInt::new(&state.id.to_string(), Span::call_site());
random_tick_states.push(state_id);
}
if state.is_air() {
let state_id = LitInt::new(&state.id.to_string(), Span::call_site());
air_states.push(state_id);
}
}
let mut property_collection = HashSet::new();
@@ -817,6 +828,8 @@ pub(crate) fn build() -> TokenStream {
.iter()
.map(|shape| shape.to_token_stream());
let air_state_ids = quote! { #(#air_states)|* };
let block_props = block_properties.iter().map(|prop| prop.to_token_stream());
let properties = property_enums.values().map(|prop| prop.to_token_stream());
@@ -908,6 +921,11 @@ pub(crate) fn build() -> TokenStream {
#(#block_entity_types),*
];
#[inline(always)]
pub fn is_air(state_id: u16) -> bool {
matches!(state_id, #air_state_ids)
}
#[inline(always)]
pub fn has_random_ticks(state_id: u16) -> bool {
#mod_ident::#contains_ident(state_id)

View File

@@ -77,6 +77,14 @@ pub(crate) fn build() -> TokenStream {
}
.to_token_stream();
let block_id_map: BTreeMap<String, u16> = blocks_assets
.blocks
.iter()
.map(|b| (b.name.clone(), b.id))
.collect();
let fluid_id_map: BTreeMap<String, u16> =
fluids.iter().map(|f| (f.name.clone(), f.id)).collect();
// Generate tag arrays for each registry key
let mut tag_dicts = Vec::new();
let mut match_arms_value = Vec::new();
@@ -84,80 +92,48 @@ pub(crate) fn build() -> TokenStream {
let mut match_arms_tags_all = Vec::new();
let mut tag_identifiers = Vec::new();
for (key, tag_map) in tags.into_iter() {
for (key, tag_map) in tags {
let key_pascal = format_ident!("{}", key.to_pascal_case());
let dict_name = format_ident!("{}_TAGS", key.to_pascal_case().to_uppercase());
// Create a BTreeMap to store tag name -> index mapping
let mut tag_values = Vec::new();
let mut tag_entries = Vec::new();
let mut tag_map_entries = Vec::new();
// Collect all unique tags
for (tag_name, values) in tag_map {
tag_values.push((tag_name, values));
let ids: Vec<u16> = values
.iter()
.filter_map(|v| match key.as_str() {
"worldgen/biome" => biomes.get(v).map(|b| b.id as u16),
"fluid" => fluid_id_map.get(v).copied(),
"item" => items.get(v).map(|i| i.id),
"block" => block_id_map.get(v).copied(),
"enchantment" => enchantments
.get(&format!("minecraft:{}", v))
.map(|e| e.id as u16),
"entity_type" => entities.get(v).map(|e| e.id),
_ => None,
})
.collect();
let tag_const_name =
format_ident!("{}", tag_name.replace([':', '/'], "_").to_uppercase());
tag_entries.push(quote! {
pub const #tag_const_name: Tag = (&[#(#values),*], &[#(#ids),*]);
});
tag_map_entries.push(quote! {
#tag_name => &#key_pascal::#tag_const_name
});
}
tag_values.sort();
// Generate the static array of tag values
let tag_array_entries = tag_values
.iter()
.map(|(tag_name, values)| {
let tag_values_array = values.iter().map(|v| quote! { #v }).collect::<Vec<_>>();
let tag_id_array = match &key {
t if t == "worldgen/biome" => values.iter().map(|v| {
let id = biomes.get(v).unwrap().id as u16;
quote! { #id }
}).collect::<Vec<_>>(),
t if t == "fluid" => values.iter().map(|v| {
let id = fluids.iter().find(|i| { &i.name == v }).unwrap().id;
quote! { #id }
}).collect::<Vec<_>>(),
t if t == "item" => values.iter().map(|v| {
let id = items.get(v).unwrap().id;
quote! { #id }
}).collect::<Vec<_>>(),
t if t == "block" => values.iter().map(|v| {
let id = blocks_assets.blocks.iter().find(|i| { &i.name == v }).unwrap().id;
quote! { #id }
}).collect::<Vec<_>>(),
t if t == "enchantment" => values.iter().map(|v| {
let id = enchantments.get(&("minecraft:".to_string() + v)).unwrap().id as u16;
quote! { #id }
}).collect::<Vec<_>>(),
t if t == "entity_type" => values.iter().map(|v| {
let id = entities.get(v).unwrap().id;
quote! { #id }
}).collect::<Vec<_>>(),
_ => Vec::new(),
};
let mapped_name = format_ident!("{}", tag_name.replace(":", "_").replace("/", "_").to_uppercase());
quote! {
pub const #mapped_name: Tag = (&[#(#tag_values_array),*], &[#(#tag_id_array),*]);
}
})
.collect::<Vec<_>>();
let tag_array_entries_map = tag_values
.iter()
.map(|(tag_name, _values)| {
let mapped_name = format_ident!(
"{}",
tag_name.replace(":", "_").replace("/", "_").to_uppercase()
);
quote! {
#tag_name => &#key_pascal::#mapped_name
}
})
.collect::<Vec<_>>();
// Add the static array declaration
tag_dicts.push(quote! {
#[allow(non_snake_case)]
pub mod #key_pascal {
use crate::tag::Tag;
#(#tag_array_entries)*
use super::Tag;
#(#tag_entries)*
}
static #dict_name: phf::Map<&str, &'static Tag> = phf::phf_map! {
#(#tag_array_entries_map),*
static #dict_name: phf::Map<&'static str, &'static Tag> = phf::phf_map! {
#(#tag_map_entries),*
};
});

View File

@@ -27,6 +27,18 @@ pub struct Block {
pub experience: Option<Experience>,
}
impl PartialEq<u16> for Block {
fn eq(&self, other: &u16) -> bool {
self.id == *other
}
}
impl PartialEq<Block> for u16 {
fn eq(&self, other: &Block) -> bool {
*self == other.id
}
}
impl PartialEq for Block {
fn eq(&self, other: &Self) -> bool {
self.id == other.id

View File

@@ -18,4 +18,9 @@ impl RawBlockState {
pub fn to_block(&self) -> &'static Block {
Block::from_state_id(self.0)
}
#[inline]
pub fn to_block_id(&self) -> u16 {
Block::get_raw_id_from_state_id(self.0)
}
}

View File

@@ -1,7 +1,6 @@
use std::{
collections::BTreeMap,
io::ErrorKind,
ops::{AddAssign, SubAssign},
path::{Path, PathBuf},
sync::Arc,
};
@@ -162,62 +161,60 @@ where
{
type Data = Arc<RwLock<S::Data>>;
// Changed: Return BoxFuture<()>
fn watch_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
chunks: &'a [Vector2<i32>],
) -> BoxFuture<'a, ()> {
let paths: Vec<_> = chunks
.iter()
.map(|chunk| P::file_path(folder, &S::get_chunk_key(chunk)))
.collect();
Box::pin(async move {
// It is intentional that regions are watched multiple times (once per chunk)
let mut watchers = self.watchers.write().await;
for chunk in chunks {
let key = S::get_chunk_key(chunk);
let map_key = P::file_path(folder, &key);
match watchers.entry(map_key) {
std::collections::btree_map::Entry::Vacant(vacant) => {
let _ = vacant.insert(1);
}
std::collections::btree_map::Entry::Occupied(mut occupied) => {
occupied.get_mut().add_assign(1);
}
}
for path in paths {
watchers
.entry(path)
.and_modify(|count| *count += 1)
.or_insert(1);
}
})
}
// Changed: Return BoxFuture<()>
fn unwatch_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
chunks: &'a [Vector2<i32>],
) -> BoxFuture<'a, ()> {
let paths: Vec<_> = chunks
.iter()
.map(|chunk| P::file_path(folder, &S::get_chunk_key(chunk)))
.collect();
Box::pin(async move {
let mut watchers = self.watchers.write().await;
for chunk in chunks {
let key = S::get_chunk_key(chunk);
let map_key = P::file_path(folder, &key);
match watchers.entry(map_key) {
std::collections::btree_map::Entry::Vacant(_vacant) => {}
std::collections::btree_map::Entry::Occupied(mut occupied) => {
occupied.get_mut().sub_assign(1);
if occupied.get().is_zero() {
occupied.remove_entry();
}
for path in paths {
if let std::collections::btree_map::Entry::Occupied(mut occupied) =
watchers.entry(path)
{
let count = occupied.get_mut();
*count = count.saturating_sub(1);
if *count == 0 {
occupied.remove();
}
}
}
})
}
// Changed: Return BoxFuture<()>
fn clear_watched_chunks(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
self.watchers.write().await.clear();
})
}
// Changed: Return BoxFuture<()>
fn fetch_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
@@ -284,7 +281,6 @@ where
})
}
// Changed: Return BoxFuture<Result<(), ChunkWritingError>>
fn save_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
@@ -374,8 +370,6 @@ where
// Decrement strong count
drop(chunk_serializer);
// If there are still no watchers, drop from the locks
let mut locks = self.file_locks.write().await;
if self
.watchers
@@ -384,6 +378,8 @@ where
.get(&path)
.is_none_or(|count| count.is_zero())
{
let mut locks = self.file_locks.write().await;
let can_remove = if let Some(loader) = locks.get(&path) {
loader.can_remove().await
} else {
@@ -410,7 +406,6 @@ where
})
}
// Changed: Return BoxFuture<()>
fn clean_up_log(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
let locks = self.file_locks.read().await;
@@ -418,11 +413,10 @@ where
})
}
// Changed: Return BoxFuture<()>
fn block_and_await_ongoing_tasks(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
//we need to block any other operation
let serializer_cache = self.file_locks.write().await;
let serializer_cache = self.file_locks.read().await;
// Acquire a write lock on all entries to verify they are complete
let tasks = serializer_cache

View File

@@ -3,7 +3,7 @@ use crate::block::entities::BlockEntity;
use crate::chunk::format::LightContainer;
use crate::tick::scheduler::ChunkTickScheduler;
use palette::{BiomePalette, BlockPalette};
use pumpkin_data::block_properties::blocks_movement;
use pumpkin_data::block_properties::{blocks_movement, is_air};
use pumpkin_data::chunk::ChunkStatus;
use pumpkin_data::fluid::Fluid;
use pumpkin_data::tag::Block::MINECRAFT_LEAVES;
@@ -438,7 +438,7 @@ impl ChunkSections {
let mut y = first_y;
while y >= self.min_y {
if let Some(block_state_id) = self.get_block_absolute_y(relative_x, y, relative_z)
&& !BlockState::from_id(block_state_id).is_air()
&& !is_air(block_state_id)
{
return Some(y);
}
@@ -568,12 +568,14 @@ impl ChunkData {
}
pub fn get_highest_non_empty_subchunk(&self) -> usize {
for (i, sub_chunk) in self.section.sections.iter().enumerate().rev() {
if sub_chunk.block_states.non_air_block_count() != 0 {
return i;
}
}
0
self.section
.sections
.iter()
.enumerate()
.rev()
.position(|(_, sub)| !sub.block_states.has_only_air())
.map(|p| self.section.sections.len() - 1 - p)
.unwrap_or(0)
}
}

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, hash::Hash};
use pumpkin_data::{Block, BlockState, chunk::Biome};
use pumpkin_data::{Block, BlockState, block_properties::is_air, chunk::Biome};
use pumpkin_util::encompassing_bits;
use crate::block::BlockStateCodec;
@@ -475,10 +475,18 @@ impl BlockPalette {
}
}
/// Check if the entire chunk is filled with only air
pub fn has_only_air(&self) -> bool {
match self {
Self::Homogeneous(id) => is_air(*id),
Self::Heterogeneous(data) => data.palette.iter().all(|&id| is_air(id)),
}
}
pub fn non_air_block_count(&self) -> u16 {
match self {
Self::Homogeneous(registry_id) => {
if !BlockState::from_id(*registry_id).is_air() {
if !is_air(*registry_id) {
Self::VOLUME as u16
} else {
0
@@ -489,7 +497,7 @@ impl BlockPalette {
.iter()
.zip(data.counts.iter())
.filter_map(|(registry_id, count)| {
if !BlockState::from_id(*registry_id).is_air() {
if !is_air(*registry_id) {
Some(*count)
} else {
None

View File

@@ -9,6 +9,7 @@ TODO
use crate::block::RawBlockState;
use crate::chunk::io::LoadedData::Loaded;
use crate::chunk::{ChunkData, ChunkHeightmapType, ChunkLight, ChunkSections, SubChunk};
use pumpkin_data::block_properties::is_air;
use pumpkin_data::dimension::Dimension;
use std::default::Default;
use std::pin::Pin;
@@ -825,6 +826,7 @@ impl Chunk {
}
}
}
let mut chunk = ChunkData {
light_engine: ChunkLight {
sky_light: (0..sections.sections.len())
@@ -1140,9 +1142,7 @@ impl GenerationCache for Cache {
}
fn is_air(&self, local_pos: &Vector3<i32>) -> bool {
GenerationCache::get_block_state(self, local_pos)
.to_state()
.is_air()
is_air(GenerationCache::get_block_state(self, local_pos).0)
}
}

View File

@@ -1,3 +1,4 @@
use pumpkin_data::block_properties::is_air;
use pumpkin_data::{Block, BlockDirection};
use pumpkin_util::{HeightMap, include_json_static};
use serde::Deserialize;
@@ -366,8 +367,8 @@ impl CountOnEveryLayerPlacementModifier {
}
fn blocks_spawn(state: &RawBlockState) -> bool {
let block = state.to_block();
state.to_state().is_air() || block == &Block::WATER || block == &Block::LAVA
let block = state.to_block_id();
is_air(state.0) || block == Block::WATER || block == Block::LAVA
}
}

View File

@@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::pin::Pin;
use pumpkin_data::block_properties::is_air;
use pumpkin_data::dimension::Dimension;
use pumpkin_data::fluid::{Fluid, FluidState};
use pumpkin_data::tag;
@@ -430,8 +431,7 @@ impl ProtoChunk {
#[inline]
pub fn is_air(&self, local_pos: &Vector3<i32>) -> bool {
let state = self.get_block_state(local_pos).to_state();
state.is_air()
is_air(self.get_block_state(local_pos).0)
}
#[inline]
@@ -815,12 +815,10 @@ impl ProtoChunk {
let state = self
.get_block_state(&Vector3::new(local_x, search_y, local_z))
.to_block();
.to_block_id();
// TODO: Is there a better way to check that its not a fluid?
if !(state != &AIR_BLOCK
&& state != &WATER_BLOCK
&& state != &LAVA_BLOCK)
if !(state != AIR_BLOCK && state != WATER_BLOCK && state != LAVA_BLOCK)
{
min = search_y + 1;
break;

View File

@@ -152,12 +152,12 @@ impl SurfaceTerrainBuilder {
if surface_y <= elevation_y {
for y in (chunk.bottom_y() as i32..=elevation_y).rev() {
let pos = Vector3::new(global_x, y, global_z);
let block_state = chunk.get_block_state(&pos).to_block();
if block_state == Block::from_state_id(chunk.default_block.id) {
let block_state = chunk.get_block_state(&pos).to_block_id();
if block_state == Block::get_raw_id_from_state_id(chunk.default_block.id) {
break;
}
if block_state == &WATER_BLOCK {
if block_state == WATER_BLOCK {
return;
}
}

View File

@@ -13,7 +13,7 @@ use crate::{
world::BlockRegistryExt,
};
use crossbeam::channel::Sender;
use dashmap::{DashMap, Entry};
use dashmap::DashMap;
use log::trace;
use num_traits::Zero;
use pumpkin_config::{chunk::ChunkConfig, world::LevelConfig};
@@ -35,7 +35,6 @@ use std::{
thread,
};
// use tokio::runtime::Handle;
use tokio::time::Instant;
use tokio::{
select,
sync::{
@@ -134,45 +133,40 @@ impl Level {
seed: i64,
dimension: Dimension,
) -> Arc<Self> {
// If we are using an already existing world we want to read the seed from the level.dat, If not we want to check if there is a seed in the config, if not lets create a random one
let region_folder = root_folder.join("region");
if !region_folder.exists() {
std::fs::create_dir_all(&region_folder).expect("Failed to create Region folder");
}
let entities_folder = root_folder.join("entities");
if !entities_folder.exists() {
std::fs::create_dir_all(&region_folder).expect("Failed to create Entities folder");
}
std::fs::create_dir_all(&region_folder).expect("Failed to create Region folder");
std::fs::create_dir_all(&entities_folder).expect("Failed to create Entities folder");
let level_folder = LevelFolder {
root_folder,
region_folder,
entities_folder,
};
// TODO: Load info correctly based on world format type
let seed = Seed(seed as u64);
let world_gen = get_world_gen(seed, dimension).into();
let chunk_saver: Arc<dyn FileIO<Data = SyncChunk>> = match &level_config.chunk {
ChunkConfig::Linear(chunk_config) => Arc::new(
ChunkFileManager::<LinearFile<ChunkData>>::new(chunk_config.clone()),
ChunkConfig::Linear(config) => Arc::new(
ChunkFileManager::<LinearFile<ChunkData>>::new(config.clone()),
),
ChunkConfig::Anvil(config) => Arc::new(
ChunkFileManager::<AnvilChunkFile<ChunkData>>::new(config.clone()),
),
ChunkConfig::Anvil(chunk_config) => Arc::new(ChunkFileManager::<
AnvilChunkFile<ChunkData>,
>::new(chunk_config.clone())),
};
let entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>> = match &level_config.chunk {
ChunkConfig::Linear(chunk_config) => Arc::new(ChunkFileManager::<
LinearFile<ChunkEntityData>,
>::new(chunk_config.clone())),
ChunkConfig::Anvil(chunk_config) => Arc::new(ChunkFileManager::<
ChunkConfig::Linear(config) => Arc::new(
ChunkFileManager::<LinearFile<ChunkEntityData>>::new(config.clone()),
),
ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::<
AnvilChunkFile<ChunkEntityData>,
>::new(chunk_config.clone())),
>::new(config.clone())),
};
let (gen_entity_request_tx, gen_entity_request_rx) = crossbeam::channel::unbounded();
let pending_entity_generations = Arc::new(DashMap::new());
let level_channel = Arc::new(LevelChannel::new());
let thread_tracker = Mutex::new(Vec::new());
let listener = Arc::new(ChunkListener::new());
@@ -214,8 +208,6 @@ impl Level {
level_ref.thread_tracker.lock().unwrap().as_mut(),
);
// let mut tracker = level_ref.thread_tracker.lock().unwrap();
// Entity Chunks
for thread_id in 0..(num_threads / 2).max(1) {
let level_clone = level_ref.clone();
let pending_clone = pending_entity_generations.clone();
@@ -223,7 +215,7 @@ impl Level {
let builder =
thread::Builder::new().name(format!("Entity Chunk Generation Thread {thread_id}"));
// tracker.push( TODO
builder
.spawn(move || {
while let Ok(pos) = rx.recv() {
@@ -231,11 +223,6 @@ impl Level {
break;
}
// log::debug!(
// "Generating entity chunk {pos:?}, worker thread {thread_id:?}, queue length {}",
// rx.len()
// );
let chunk = ChunkEntityData {
x: pos.x,
z: pos.y,
@@ -248,25 +235,15 @@ impl Level {
.loaded_entity_chunks
.insert(pos, arc_chunk.clone());
if let Some(waiters) = pending_clone.remove(&pos) {
for tx in waiters.1 {
if let Some((_, waiters)) = pending_clone.remove(&pos) {
for tx in waiters {
let _ = tx.send(arc_chunk.clone());
}
}
}
})
.unwrap();
// );
}
// drop(tracker);
// level_ref
// .chunk_loading
// .lock()
// .unwrap()
// .add_ticket(
// Vector2::<i32>::new(0, 0),
// ChunkLoading::FULL_CHUNK_LEVEL - 1,
// );
level_ref
}
@@ -385,21 +362,16 @@ impl Level {
/// before
pub async fn mark_chunks_as_newly_watched(&self, chunks: &[Vector2<i32>]) {
for chunk in chunks {
log::trace!("{chunk:?} marked as newly watched");
match self.chunk_watchers.entry(*chunk) {
Entry::Occupied(mut occupied) => {
let value = occupied.get_mut();
self.chunk_watchers
.entry(*chunk)
.and_modify(|value| {
if let Some(new_value) = value.checked_add(1) {
*value = new_value;
//log::debug!("Watch value for {:?}: {}", chunk, value);
} else {
log::error!("Watching overflow on chunk {chunk:?}");
log::error!("Watching overflow on chunk {:?}", chunk);
}
}
Entry::Vacant(vacant) => {
vacant.insert(1);
}
}
})
.or_insert(1);
}
// self.chunk_saver
@@ -416,22 +388,14 @@ impl Level {
let mut chunks_to_clean = Vec::new();
for chunk in chunks {
log::trace!("{chunk:?} marked as no longer watched");
match self.chunk_watchers.entry(*chunk) {
Entry::Occupied(mut occupied) => {
let value = occupied.get_mut();
*value = value.saturating_sub(1);
if let Some(mut count) = self.chunk_watchers.get_mut(chunk) {
let value = count.value_mut();
*value = value.saturating_sub(1);
if *value == 0 {
occupied.remove_entry();
chunks_to_clean.push(*chunk);
}
}
Entry::Vacant(_) => {
// This can be:
// - Player disconnecting before all packets have been sent
// - Player moving so fast that the chunk leaves the render distance before it
// is loaded into memory
if *value == 0 {
drop(count);
self.chunk_watchers.remove(chunk);
chunks_to_clean.push(*chunk);
}
}
}
@@ -506,13 +470,10 @@ impl Level {
.iter()
.map(|x| x.value().clone())
.collect::<Vec<_>>();
for chunk in chunks {
let mut chunk = chunk.write().await;
ticks.block_ticks.append(&mut chunk.block_ticks.step_tick());
ticks.fluid_ticks.append(&mut chunk.fluid_ticks.step_tick());
let chunk = chunk.downgrade();
for chunk_sync in chunks {
// Try to get a READ lock first.
// Most chunks won't have pending ticks, so this is very fast.
let chunk = chunk_sync.read().await;
let chunk_x_base = chunk.x * 16;
let chunk_z_base = chunk.z * 16;
@@ -544,21 +505,38 @@ impl Level {
}
for section_data in section_blocks {
for (random_pos, block_state_id) in section_data {
if has_random_ticks(block_state_id) {
ticks.random_ticks.push(ScheduledTick {
position: random_pos,
delay: 0,
priority: TickPriority::Normal,
value: (),
});
}
}
ticks
.random_ticks
.extend(
section_data
.into_iter()
.filter_map(|(random_pos, block_state_id)| {
if has_random_ticks(block_state_id) {
Some(ScheduledTick {
position: random_pos,
delay: 0,
priority: TickPriority::Normal,
value: (),
})
} else {
None
}
}),
);
}
ticks
.block_entities
.extend(chunk.block_entities.values().cloned());
drop(chunk);
let mut chunk_write = chunk_sync.write().await;
ticks
.block_ticks
.append(&mut chunk_write.block_ticks.step_tick());
ticks
.fluid_ticks
.append(&mut chunk_write.fluid_ticks.step_tick());
}
ticks.block_ticks.sort_unstable();
@@ -603,8 +581,6 @@ impl Level {
return chunk.clone();
}
log::debug!("Missing Chunk {pos:?}. Fetching.");
let clock = Instant::now();
let recv = self.chunk_listener.add_single_chunk_listener(pos);
{
@@ -613,16 +589,12 @@ impl Level {
lock.send_change();
}
let ret = if let Some(chunk) = self.loaded_chunks.get(&pos) {
if let Some(chunk) = self.loaded_chunks.get(&pos) {
chunk.clone()
} else {
recv.await
.expect("Chunk listener dropped without sending chunk")
};
log::debug!("Chunk {pos:?} received after {:?}.", Instant::now() - clock);
ret
}
}
async fn load_single_entity_chunk(
@@ -846,6 +818,7 @@ impl Level {
priority: TickPriority,
) {
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let tick_order = self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed);
let mut chunk = chunk.write().await;
chunk.block_ticks.schedule_tick(
&ScheduledTick {
@@ -854,9 +827,8 @@ impl Level {
priority,
value: unsafe { &*(block as *const Block) },
},
self.schedule_tick_counts.load(Ordering::Relaxed),
tick_order,
);
self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed);
}
pub async fn schedule_fluid_tick(
@@ -867,6 +839,7 @@ impl Level {
priority: TickPriority,
) {
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let tick_order = self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed);
let mut chunk = chunk.write().await;
chunk.fluid_ticks.schedule_tick(
&ScheduledTick {
@@ -875,9 +848,8 @@ impl Level {
priority,
value: unsafe { &*(fluid as *const Fluid) },
},
self.schedule_tick_counts.load(Ordering::Relaxed),
tick_order,
);
self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed);
}
pub async fn is_block_tick_scheduled(

View File

@@ -1,6 +1,7 @@
use crate::block::{
BlockBehaviour, BlockFuture, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnScheduledTickArgs,
};
use pumpkin_data::block_properties::is_air;
use pumpkin_data::tag::{RegistryKey, get_tag_values};
use pumpkin_macros::{pumpkin_block, pumpkin_block_from_tag};
use pumpkin_util::math::position::BlockPos;
@@ -108,8 +109,5 @@ impl BlockBehaviour for PaleMossCarpetBlock {
}
async fn can_place_at(block_accessor: &dyn BlockAccessor, block_pos: &BlockPos) -> bool {
!block_accessor
.get_block_state(&block_pos.down())
.await
.is_air()
!is_air(block_accessor.get_block_state_id(&block_pos.down()).await)
}

View File

@@ -206,23 +206,6 @@ impl ChunkManager {
let view_distance_i32 = i32::from(view_distance);
let mut chunks_to_watch = Vec::new();
let mut chunks_to_unwatch = Vec::new();
for pos in &self.chunk_sent {
if (pos.x - center.x).abs().max((pos.y - center.y).abs()) > view_distance_i32 {
chunks_to_unwatch.push(*pos);
}
}
let level_clone = level.clone();
let chunks_to_unwatch_clone = chunks_to_unwatch.clone();
tokio::spawn(async move {
level_clone
.mark_chunks_as_not_watched(&chunks_to_unwatch_clone)
.await;
});
self.chunk_sent.retain(|pos| {
(pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_distance_i32
});
@@ -242,20 +225,13 @@ impl ChunkManager {
for dx in (-view_distance_i32)..=view_distance_i32 {
for dy in (-view_distance_i32)..=view_distance_i32 {
let new_pos = center.add_raw(dx, dy);
if !self.chunk_sent.contains(&new_pos) {
chunks_to_watch.push(new_pos);
if let Some(chunk) = level.loaded_chunks.get(&new_pos) {
self.push_chunk(new_pos, chunk.value().clone());
}
if !self.chunk_sent.contains(&new_pos)
&& let Some(chunk) = level.loaded_chunks.get(&new_pos)
{
self.push_chunk(new_pos, chunk.value().clone());
}
}
}
let level_clone = level.clone();
tokio::spawn(async move {
level_clone
.mark_chunks_as_newly_watched(&chunks_to_watch)
.await;
});
}
pub fn clean_up(&mut self, level: &Arc<Level>) {

View File

@@ -20,7 +20,7 @@ use rustyline::Editor;
use rustyline::history::FileHistory;
use rustyline::{Config, error::ReadlineError};
use std::collections::HashMap;
use std::io::{Cursor, IsTerminal, stdin};
use std::io::{Cursor, ErrorKind, IsTerminal, stdin};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
@@ -222,10 +222,36 @@ impl PumpkinServer {
let mut tcp_listener = None;
if server.basic_config.java_edition {
let address = server.basic_config.java_edition_address;
// Setup the TCP server socket.
let listener = tokio::net::TcpListener::bind(server.basic_config.java_edition_address)
.await
.expect("Failed to start `TcpListener`");
let listener = match TcpListener::bind(address).await {
Ok(l) => l,
Err(e) => match e.kind() {
ErrorKind::AddrInUse => {
log::error!("Error: Address {} is already in use.", address);
log::error!(
"Make sure another instance of the server isn't already running"
);
std::process::exit(1);
}
ErrorKind::PermissionDenied => {
log::error!("Error: Permission denied when binding to {}.", address);
log::error!("You might need sudo/admin privileges to use ports below 1024");
std::process::exit(1);
}
ErrorKind::AddrNotAvailable => {
log::error!(
"Error: The address {} is not available on this machine",
address
);
std::process::exit(1);
}
_ => {
log::error!("Failed to start TcpListener on {}: {}", address, e);
std::process::exit(1);
}
},
};
// In the event the user puts 0 for their port, this will allow us to know what port it is running on
let addr = listener
.local_addr()

View File

@@ -90,7 +90,9 @@ impl JavaClient {
let verify_token: [u8; 4] = rand::random();
// Wait until we have sent the encryption packet to the client
self.send_packet_now(
&server.encryption_request(&verify_token, server.basic_config.online_mode),
&server
.encryption_request(&verify_token, server.basic_config.online_mode)
.await,
)
.await;
} else {
@@ -107,7 +109,10 @@ impl JavaClient {
encryption_response: SEncryptionResponse,
) {
log::debug!("Handling encryption");
let shared_secret = server.decrypt(&encryption_response.shared_secret).unwrap();
let shared_secret = server
.decrypt(&encryption_response.shared_secret)
.await
.unwrap();
if let Err(error) = self.set_encryption(&shared_secret).await {
self.kick(TextComponent::text(error.to_string())).await;
@@ -207,7 +212,7 @@ impl JavaClient {
shared_secret: &[u8],
username: &str,
) -> Result<GameProfile, AuthError> {
let hash = server.digest_secret(shared_secret);
let hash = server.digest_secret(shared_secret).await;
let ip = self.address.lock().await.ip();
let profile = authentication::authenticate(
username,

View File

@@ -1,3 +1,5 @@
use std::time::Instant;
use num_bigint::BigInt;
use pkcs8::EncodePublicKey;
use pumpkin_protocol::java::client::login::CEncryptionRequest;
@@ -15,6 +17,7 @@ pub struct KeyStore {
impl KeyStore {
#[must_use]
pub fn new() -> Self {
let instant = Instant::now();
log::debug!("Creating encryption keys...");
let private_key = Self::generate_private_key();
@@ -27,6 +30,8 @@ impl KeyStore {
.to_vec()
.into_boxed_slice();
log::debug!("Created RSA keys, took {}ms", instant.elapsed().as_millis());
Self {
private_key,
public_key_der,

View File

@@ -39,7 +39,7 @@ use std::net::IpAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32};
use std::{future::Future, sync::atomic::Ordering, time::Duration};
use tokio::sync::{Mutex, RwLock};
use tokio::sync::{Mutex, OnceCell, RwLock};
use tokio::task::JoinHandle;
use tokio_util::task::TaskTracker;
@@ -59,7 +59,7 @@ pub struct Server {
pub advanced_config: AdvancedConfiguration,
/// Handles cryptographic keys for secure communication.
key_store: KeyStore,
key_store: OnceCell<Arc<KeyStore>>,
/// Manages server status information.
listing: Mutex<CachedStatus>,
/// Saves server branding information.
@@ -190,7 +190,7 @@ impl Server {
command_dispatcher,
block_registry: block_registry.clone(),
item_registry: super::item::items::default_registry(),
key_store: KeyStore::new(),
key_store: OnceCell::new(),
listing,
branding: CachedBranding::new(),
bossbars: Mutex::new(CustomBossbars::new()),
@@ -208,59 +208,86 @@ impl Server {
level_info: level_info.clone(),
_locker: Arc::new(locker),
};
let server = Arc::new(server);
let weak = Arc::downgrade(&server);
let level_config = &server.advanced_config.world;
let level_config = Arc::new(server.advanced_config.world.clone());
let server_clone = server.clone();
tokio::spawn(async move {
server_clone
.key_store
.get_or_init(|| async { Arc::new(KeyStore::new()) })
.await;
});
let weak_server = Arc::downgrade(&server);
log::info!("Loading Overworld: {seed}");
let overworld = World::load(
into_level(
Dimension::OVERWORLD,
level_config,
world_path.clone(),
block_registry.clone(),
seed,
),
level_info.clone(),
Dimension::OVERWORLD,
block_registry.clone(),
weak.clone(),
);
log::info!("Loading Nether: {seed}");
let nether = World::load(
into_level(
Dimension::THE_NETHER,
level_config,
world_path.clone(),
block_registry.clone(),
seed,
),
level_info.clone(),
Dimension::THE_NETHER,
block_registry.clone(),
weak.clone(),
);
log::info!("Loading End: {seed}");
let end = World::load(
into_level(
Dimension::THE_END,
level_config,
world_path,
block_registry.clone(),
seed,
),
level_info,
Dimension::THE_END,
block_registry,
weak,
);
*server
.worlds
.try_write()
.expect("Nothing should hold a lock of worlds before server startup") =
// vec![overworld.into()];
vec![overworld.into(), nether.into(), end.into()];
let overworld_task = tokio::task::spawn_blocking({
let path = world_path.clone();
let registry = block_registry.clone();
let level_info = level_info.clone();
let weak = weak_server.clone();
let config = level_config.clone();
move || {
World::load(
into_level(Dimension::OVERWORLD, &config, path, registry.clone(), seed),
level_info,
Dimension::OVERWORLD,
registry,
weak,
)
}
});
let nether_task = tokio::task::spawn_blocking({
let path = world_path.clone();
let registry = block_registry.clone();
let level_info = level_info.clone();
let weak = weak_server.clone();
let config = level_config.clone();
move || {
World::load(
into_level(Dimension::THE_NETHER, &config, path, registry.clone(), seed),
level_info,
Dimension::THE_NETHER,
registry,
weak,
)
}
});
let end_task = tokio::task::spawn_blocking({
let path = world_path.clone();
let registry = block_registry.clone();
let level_info = level_info.clone();
let weak = weak_server.clone();
let config = level_config.clone();
move || {
World::load(
into_level(Dimension::THE_END, &config, path, registry.clone(), seed),
level_info,
Dimension::THE_END,
registry,
weak,
)
}
});
let (overworld_res, nether_res, end_res) =
tokio::join!(overworld_task, nether_task, end_task);
let overworld = overworld_res.expect("Overworld load panicked");
let nether = nether_res.expect("Nether load panicked");
let end = end_res.expect("End load panicked");
{
let mut worlds = server.worlds.write().await;
worlds.push(overworld.into());
worlds.push(nether.into());
worlds.push(end.into());
};
log::info!("All worlds loaded successfully.");
server
}
@@ -623,21 +650,29 @@ impl Server {
&self.listing
}
pub fn encryption_request<'a>(
pub async fn encryption_request<'a>(
&'a self,
verification_token: &'a [u8; 4],
should_authenticate: bool,
) -> CEncryptionRequest<'a> {
self.key_store
.get_or_init(|| async { Arc::new(KeyStore::new()) })
.await
.encryption_request("", verification_token, should_authenticate)
}
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
self.key_store.decrypt(data)
pub async fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
self.key_store
.get_or_init(|| async { Arc::new(KeyStore::new()) })
.await
.decrypt(data)
}
pub fn digest_secret(&self, secret: &[u8]) -> String {
self.key_store.get_digest(secret)
pub async fn digest_secret(&self, secret: &[u8]) -> String {
self.key_store
.get_or_init(|| async { Arc::new(KeyStore::new()) })
.await
.get_digest(secret)
}
/// Main server tick method. This now handles both player/network ticking (which always runs)
@@ -711,6 +746,22 @@ impl Server {
self.aggregated_tick_times_nanos.load(Ordering::Relaxed) / sample_size as i64
}
/// Returns the average Milliseconds Per Tick (MSPT).
pub fn get_mspt(&self) -> f64 {
let avg_nanos = self.get_average_tick_time_nanos();
// Convert nanoseconds to decimal milliseconds
avg_nanos as f64 / 1_000_000.0
}
/// Returns the Ticks Per Second (TPS).
pub fn get_tps(&self) -> f64 {
let mspt = self.get_mspt();
if mspt <= 0.0 {
return 0.0;
}
1000.0 / mspt
}
/// Returns a copy of the last 100 tick times.
pub async fn get_tick_times_nanos_copy(&self) -> [i64; 100] {
*self.tick_times_nanos.lock().await

View File

@@ -22,74 +22,77 @@ pub async fn get_view_distance(player: &Player) -> NonZeroU8 {
pub async fn update_position(player: &Arc<Player>) {
let entity = &player.living_entity.entity;
let new_chunk_center = entity.chunk_pos.load();
let old_cylindrical = player.watched_section.load();
if old_cylindrical.center == new_chunk_center {
return;
}
let view_distance = get_view_distance(player).await;
let new_chunk_center = entity.chunk_pos.load();
let old_cylindrical = player.watched_section.load();
let new_cylindrical = Cylindrical::new(new_chunk_center, view_distance);
if old_cylindrical != new_cylindrical {
match &player.client {
ClientPlatform::Java(java_client) => {
java_client
.send_packet_now(&CCenterChunk {
chunk_x: new_chunk_center.x.into(),
chunk_z: new_chunk_center.y.into(),
})
.await;
}
ClientPlatform::Bedrock(bedrock_client) => {
bedrock_client
.send_game_packet(&CNetworkChunkPublisherUpdate::new(
player.get_entity().block_pos.load(),
u32::from(view_distance.get()) * 16,
))
.await;
}
if old_cylindrical == new_cylindrical {
return;
}
match &player.client {
ClientPlatform::Java(java_client) => {
java_client
.send_packet_now(&CCenterChunk {
chunk_x: new_chunk_center.x.into(),
chunk_z: new_chunk_center.y.into(),
})
.await;
}
let mut loading_chunks = Vec::new();
let mut unloading_chunks = Vec::new();
Cylindrical::for_each_changed_chunk(
old_cylindrical,
new_cylindrical,
&mut loading_chunks,
&mut unloading_chunks,
);
// Make sure the watched section and the chunk watcher updates are async atomic. We want to
// ensure what we unload when the player disconnects is correct.
let level = &entity.world.level;
level.mark_chunks_as_newly_watched(&loading_chunks).await;
let chunks_to_clean = level.mark_chunks_as_not_watched(&unloading_chunks).await;
{
let mut chunk_manager = player.chunk_manager.lock().await;
chunk_manager.update_center_and_view_distance(
new_chunk_center,
view_distance.into(),
level,
);
};
player.watched_section.store(new_cylindrical);
if !chunks_to_clean.is_empty() {
// level.clean_chunks(&chunks_to_clean).await;
for chunk in unloading_chunks {
player
.client
.enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y))
.await;
}
}
if !loading_chunks.is_empty() {
entity.world.spawn_world_entity_chunks(
player.clone(),
loading_chunks,
new_chunk_center,
);
ClientPlatform::Bedrock(bedrock_client) => {
bedrock_client
.send_game_packet(&CNetworkChunkPublisherUpdate::new(
player.get_entity().block_pos.load(),
u32::from(view_distance.get()) * 16,
))
.await;
}
}
let mut loading_chunks = Vec::new();
let mut unloading_chunks = Vec::new();
Cylindrical::for_each_changed_chunk(
old_cylindrical,
new_cylindrical,
&mut loading_chunks,
&mut unloading_chunks,
);
let level = &entity.world.level;
{
let mut chunk_manager = player.chunk_manager.lock().await;
chunk_manager.update_center_and_view_distance(
new_chunk_center,
view_distance.into(),
level,
);
};
player.watched_section.store(new_cylindrical);
// Make sure the watched section and the chunk watcher updates are async atomic. We want to
// ensure what we unload when the player disconnects is correct.
level.mark_chunks_as_newly_watched(&loading_chunks).await;
level.mark_chunks_as_not_watched(&unloading_chunks).await;
if let ClientPlatform::Java(_) = &player.client {
for chunk in &unloading_chunks {
player
.client
.enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y))
.await;
}
}
if !loading_chunks.is_empty() {
entity
.world
.spawn_world_entity_chunks(player.clone(), loading_chunks, new_chunk_center);
}
}

View File

@@ -38,6 +38,7 @@ use bytes::BufMut;
use crossbeam::queue::SegQueue;
use explosion::Explosion;
use pumpkin_config::BasicConfiguration;
use pumpkin_data::block_properties::is_air;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::dimension::Dimension;
use pumpkin_data::entity::MobCategory;
@@ -1874,10 +1875,6 @@ impl World {
chunks: Vec<Vector2<i32>>,
center_chunk: Vector2<i32>,
) {
if player.client.closed() {
log::info!("The connection has closed before world chunks were spawned");
return;
}
#[cfg(debug_assertions)]
let inst = std::time::Instant::now();
@@ -3054,7 +3051,7 @@ impl World {
if new_state_id != block_state_id {
let flags = flags & !BlockFlags::SKIP_DROPS;
if BlockState::from_id(new_state_id).is_air() {
if is_air(new_state_id) {
self.break_block(block_pos, None, flags).await;
} else {
self.set_block_state(block_pos, new_state_id, flags).await;