From 8c4fa994709f844cd67b9bac0f83438221cbb95d Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 30 Apr 2026 09:05:40 +0200 Subject: [PATCH] feat: add rudimentary .pump format also this should fix a dead lock --- README.md | 4 +- pumpkin-config/src/chunk.rs | 12 +- pumpkin-world/src/chunk/format/mod.rs | 1 + pumpkin-world/src/chunk/format/pump.rs | 149 +++++++++++++++++++++++++ pumpkin-world/src/level.rs | 3 + pumpkin/src/entity/item.rs | 42 +++++-- pumpkin/src/entity/living.rs | 10 +- 7 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 pumpkin-world/src/chunk/format/pump.rs diff --git a/README.md b/README.md index 70e1ee8a2..4d2e3f470 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [x] Lighting - [x] Entity Spawning - [x] Bossbar - - [x] Chunk Loading (Vanilla, Linear) + - [x] Chunk Loading (Vanilla, Linear, Pump) - [Chunk Generation](https://github.com/Pumpkin-MC/Pumpkin/issues/36) - - [x] Chunk Saving (Vanilla, Linear) + - [x] Chunk Saving (Vanilla, Linear, Pump) - [Redstone](https://github.com/Pumpkin-MC/Pumpkin/issues/1402) - [x] Liquid Physics - ... diff --git a/pumpkin-config/src/chunk.rs b/pumpkin-config/src/chunk.rs index 2682539c2..8463ac0bd 100644 --- a/pumpkin-config/src/chunk.rs +++ b/pumpkin-config/src/chunk.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// Configuration for chunk storage format. /// /// Supports multiple chunk formats, currently `Anvil` and `Linear`. -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Default, Serialize, Clone)] #[serde(tag = "type")] pub enum ChunkConfig { /// Standard Anvil chunk storage. @@ -14,12 +14,10 @@ pub enum ChunkConfig { /// Linear chunk storage format. #[serde(rename = "linear")] Linear, -} - -impl Default for ChunkConfig { - fn default() -> Self { - Self::Anvil(AnvilChunkConfig::default()) - } + /// Pumpkin's own optimized world format. + #[serde(rename = "pump")] + #[default] + Pump, } /// Configuration for Anvil chunk storage. diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index c8303be42..32a159874 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -35,6 +35,7 @@ use super::{ }; pub mod anvil; pub mod linear; +pub mod pump; impl SingleChunkDataSerializer for ChunkData { #[inline] diff --git a/pumpkin-world/src/chunk/format/pump.rs b/pumpkin-world/src/chunk/format/pump.rs new file mode 100644 index 000000000..01b1bc53a --- /dev/null +++ b/pumpkin-world/src/chunk/format/pump.rs @@ -0,0 +1,149 @@ +use std::marker::PhantomData; +use std::path::PathBuf; + +use crate::chunk::format::anvil::{CHUNK_COUNT, SingleChunkDataSerializer}; +use crate::chunk::io::{ChunkSerializer, LoadedData}; +use crate::chunk::{ChunkReadingError, ChunkWritingError}; +use bytes::Bytes; +use pumpkin_util::math::vector2::Vector2; +use ruzstd::decoding::StreamingDecoder; +use ruzstd::encoding::{CompressionLevel, compress_to_vec}; +use serde::{Deserialize, Serialize}; +use tracing::warn; + +pub struct PumpFile { + pub data: PumpData, + _phantom: PhantomData, +} + +#[derive(Serialize, Deserialize, Default)] +pub struct PumpData { + pub x: i32, + pub z: i32, + pub chunks: Vec>>, +} + +impl Default for PumpFile { + fn default() -> Self { + Self { + data: PumpData { + x: 0, + z: 0, + chunks: vec![None; CHUNK_COUNT], + }, + _phantom: PhantomData, + } + } +} + +impl ChunkSerializer for PumpFile +where + D: SingleChunkDataSerializer + Send + Sync + Sized, +{ + type Data = D; + type WriteBackend = PathBuf; + type ChunkConfig = (); + + fn get_chunk_key(chunk: &Vector2) -> String { + let region_x = chunk.x >> 5; + let region_z = chunk.y >> 5; + format!("r.{region_x}.{region_z}.pump") + } + + fn should_write(&self, _is_watched: bool) -> bool { + true + } + + async fn write(&self, backend: &Self::WriteBackend) -> Result<(), std::io::Error> { + let bytes = + pumpkin_nbt::to_pnbt(&self.data).map_err(|e| std::io::Error::other(e.to_string()))?; + + let compressed = compress_to_vec(&bytes[..], CompressionLevel::Fastest); + + tokio::fs::write(backend, compressed).await + } + + fn read(r: Bytes) -> Result { + let mut decoder = StreamingDecoder::new(&r[..]) + .map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e.to_string()).kind()))?; + let mut decompressed = Vec::new(); + std::io::Read::read_to_end(&mut decoder, &mut decompressed) + .map_err(|e| ChunkReadingError::IoError(e.kind()))?; + + let data: PumpData = pumpkin_nbt::from_pnbt(&decompressed).map_err(|e| { + ChunkReadingError::ParsingError( + crate::chunk::ChunkParsingError::ErrorDeserializingChunk(e.to_string()), + ) + })?; + + Ok(Self { + data, + _phantom: PhantomData, + }) + } + + async fn update_chunk( + &mut self, + chunk_data: &Self::Data, + _chunk_config: &Self::ChunkConfig, + ) -> Result<(), ChunkWritingError> { + let (x, z) = chunk_data.position(); + self.data.x = x >> 5; + self.data.z = z >> 5; + let rel_x = x.rem_euclid(32); + let rel_z = z.rem_euclid(32); + let index = (rel_x + rel_z * 32) as usize; + + let bytes = chunk_data + .to_bytes() + .await + .map_err(|e| ChunkWritingError::ChunkSerializingError(e.to_string()))?; + + if index < self.data.chunks.len() { + self.data.chunks[index] = Some(bytes.to_vec()); + } else { + warn!("Chunk position {}, {} out of range for Pump region", x, z); + } + + Ok(()) + } + + async fn get_chunks( + &self, + chunks: Vec>, + stream: tokio::sync::mpsc::Sender>, + ) { + for pos in chunks { + let rel_x = pos.x.rem_euclid(32); + let rel_z = pos.y.rem_euclid(32); + let index = (rel_x + rel_z * 32) as usize; + + if index < self.data.chunks.len() { + if let Some(chunk_bytes) = &self.data.chunks[index] { + let bytes = Bytes::copy_from_slice(chunk_bytes); + match D::from_bytes(&bytes, pos) { + Ok(data) => { + let _ = stream.send(LoadedData::Loaded(data)).await; + } + Err(e) => { + let _ = stream.send(LoadedData::Error((pos, e))).await; + } + } + } else { + let _ = stream.send(LoadedData::Missing(pos)).await; + } + } else { + let _ = stream + .send(LoadedData::Error(( + pos, + ChunkReadingError::ParsingError( + crate::chunk::ChunkParsingError::ErrorDeserializingChunk( + "Chunk out of range".to_string(), + ), + ), + ))) + .await; + } + } + } +} diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 24e4b194b..436df4b14 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,4 +1,5 @@ use crate::chunk::format::linear::LinearV2File; +use crate::chunk::format::pump::PumpFile; use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel}; use crate::generation::generator::VanillaGenerator; use crate::lighting::DynamicLightEngine; @@ -168,6 +169,7 @@ impl Level { ChunkConfig::Anvil(config) => Arc::new( ChunkFileManager::>::new(config.clone()), ), + ChunkConfig::Pump => Arc::new(ChunkFileManager::>::new(())), }; let entity_saver: Arc> = match &level_config.chunk { ChunkConfig::Linear => { @@ -176,6 +178,7 @@ impl Level { ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::< AnvilChunkFile, >::new(config.clone())), + ChunkConfig::Pump => Arc::new(ChunkFileManager::>::new(())), }; let pending_entity_generations = Arc::new(DashMap::new()); diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index 5377cf8e9..3274e0ffe 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -127,9 +127,22 @@ impl ItemEntity { } async fn try_merge_with(&self, other: &Self) { - let self_stack = self.item_stack.lock().await; + // Always lock in entity_id order to prevent deadlock when two + // items try to merge with each other concurrently. + let (low, high) = if self.entity.entity_id < other.entity.entity_id { + (self, other) + } else { + (other, self) + }; - let other_stack = other.item_stack.lock().await; + let low_stack = low.item_stack.lock().await; + let high_stack = high.item_stack.lock().await; + + let (self_stack, other_stack) = if self.entity.entity_id < other.entity.entity_id { + (low_stack, high_stack) + } else { + (high_stack, low_stack) + }; if !self_stack.are_equal(&other_stack) || self_stack.item_count + other_stack.item_count > self_stack.get_max_stack_size() @@ -419,7 +432,6 @@ impl EntityBase for ItemEntity { return false; } - // Thread safe damage application loop { let current = self.health.load(Relaxed); let new = current - amount; @@ -446,20 +458,26 @@ impl EntityBase for ItemEntity { return; } - if player - .inventory - .insert_stack_anywhere(&mut *self.item_stack.lock().await) - .await - || player.is_creative() - { + let inserted = { + let mut stack = self.item_stack.lock().await; + player.inventory.insert_stack_anywhere(&mut stack).await + }; + + if inserted || player.is_creative() { + let (item_count, is_empty) = { + let stack = self.item_stack.lock().await; + (stack.item_count, stack.is_empty()) + }; + player .client .enqueue_packet(&CTakeItemEntity::new( self.entity.entity_id.into(), player.entity_id().into(), - self.item_stack.lock().await.item_count.into(), + item_count.into(), )) .await; + player .current_screen_handler .lock() @@ -468,10 +486,10 @@ impl EntityBase for ItemEntity { .await .send_content_updates() .await; - if self.item_stack.lock().await.is_empty() { + + if is_empty { self.entity.remove().await; } else { - // Update entity self.init_data_tracker().await; } } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index eacd792b7..2bb190bad 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -1135,8 +1135,7 @@ impl LivingEntity { let yaw = f64::from(self.entity.yaw.load()).to_radians(); velo.x -= yaw.sin() * 0.2; - - velo.y += yaw.cos() * 0.2; + velo.z += yaw.cos() * 0.2; } self.entity.velocity.store(velo); @@ -1147,7 +1146,9 @@ impl LivingEntity { async fn get_jump_velocity(&self, mut strength: f64) -> f64 { strength *= self.get_attribute_value(&Attributes::JUMP_STRENGTH); strength *= f64::from(self.entity.get_jump_velocity_multiplier().await); - + if let Some(effect) = self.get_effect(&StatusEffect::JUMP_BOOST).await { + strength += 0.1 * f64::from(effect.amplifier + 1); + } strength } @@ -2031,8 +2032,7 @@ impl EntityBase for LivingEntity { } fn get_gravity(&self) -> f64 { - const GRAVITY: f64 = 0.08; - GRAVITY + self.get_attribute_value(&Attributes::GRAVITY) } #[allow(clippy::too_many_lines)]