feat: add rudimentary .pump format

also this should fix a dead lock
This commit is contained in:
Alexander Medvedev 2026-04-30 09:05:40 +02:00
parent 1d671504d1
commit 8c4fa99470
7 changed files with 195 additions and 26 deletions

View File

@ -48,9 +48,9 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- [x] Lighting - [x] Lighting
- [x] Entity Spawning - [x] Entity Spawning
- [x] Bossbar - [x] Bossbar
- [x] Chunk Loading (Vanilla, Linear) - [x] Chunk Loading (Vanilla, Linear, Pump)
- [Chunk Generation](https://github.com/Pumpkin-MC/Pumpkin/issues/36) - [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) - [Redstone](https://github.com/Pumpkin-MC/Pumpkin/issues/1402)
- [x] Liquid Physics - [x] Liquid Physics
- ... - ...

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
/// Configuration for chunk storage format. /// Configuration for chunk storage format.
/// ///
/// Supports multiple chunk formats, currently `Anvil` and `Linear`. /// Supports multiple chunk formats, currently `Anvil` and `Linear`.
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Default, Serialize, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ChunkConfig { pub enum ChunkConfig {
/// Standard Anvil chunk storage. /// Standard Anvil chunk storage.
@ -14,12 +14,10 @@ pub enum ChunkConfig {
/// Linear chunk storage format. /// Linear chunk storage format.
#[serde(rename = "linear")] #[serde(rename = "linear")]
Linear, Linear,
} /// Pumpkin's own optimized world format.
#[serde(rename = "pump")]
impl Default for ChunkConfig { #[default]
fn default() -> Self { Pump,
Self::Anvil(AnvilChunkConfig::default())
}
} }
/// Configuration for Anvil chunk storage. /// Configuration for Anvil chunk storage.

View File

@ -35,6 +35,7 @@ use super::{
}; };
pub mod anvil; pub mod anvil;
pub mod linear; pub mod linear;
pub mod pump;
impl SingleChunkDataSerializer for ChunkData { impl SingleChunkDataSerializer for ChunkData {
#[inline] #[inline]

View File

@ -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<D> {
pub data: PumpData,
_phantom: PhantomData<D>,
}
#[derive(Serialize, Deserialize, Default)]
pub struct PumpData {
pub x: i32,
pub z: i32,
pub chunks: Vec<Option<Vec<u8>>>,
}
impl<D> Default for PumpFile<D> {
fn default() -> Self {
Self {
data: PumpData {
x: 0,
z: 0,
chunks: vec![None; CHUNK_COUNT],
},
_phantom: PhantomData,
}
}
}
impl<D> ChunkSerializer for PumpFile<D>
where
D: SingleChunkDataSerializer + Send + Sync + Sized,
{
type Data = D;
type WriteBackend = PathBuf;
type ChunkConfig = ();
fn get_chunk_key(chunk: &Vector2<i32>) -> 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<Self, ChunkReadingError> {
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<Vector2<i32>>,
stream: tokio::sync::mpsc::Sender<LoadedData<Self::Data, ChunkReadingError>>,
) {
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;
}
}
}
}

View File

@ -1,4 +1,5 @@
use crate::chunk::format::linear::LinearV2File; use crate::chunk::format::linear::LinearV2File;
use crate::chunk::format::pump::PumpFile;
use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel}; use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel};
use crate::generation::generator::VanillaGenerator; use crate::generation::generator::VanillaGenerator;
use crate::lighting::DynamicLightEngine; use crate::lighting::DynamicLightEngine;
@ -168,6 +169,7 @@ impl Level {
ChunkConfig::Anvil(config) => Arc::new( ChunkConfig::Anvil(config) => Arc::new(
ChunkFileManager::<AnvilChunkFile<ChunkData>>::new(config.clone()), ChunkFileManager::<AnvilChunkFile<ChunkData>>::new(config.clone()),
), ),
ChunkConfig::Pump => Arc::new(ChunkFileManager::<PumpFile<ChunkData>>::new(())),
}; };
let entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>> = match &level_config.chunk { let entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>> = match &level_config.chunk {
ChunkConfig::Linear => { ChunkConfig::Linear => {
@ -176,6 +178,7 @@ impl Level {
ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::< ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::<
AnvilChunkFile<ChunkEntityData>, AnvilChunkFile<ChunkEntityData>,
>::new(config.clone())), >::new(config.clone())),
ChunkConfig::Pump => Arc::new(ChunkFileManager::<PumpFile<ChunkEntityData>>::new(())),
}; };
let pending_entity_generations = Arc::new(DashMap::new()); let pending_entity_generations = Arc::new(DashMap::new());

View File

@ -127,9 +127,22 @@ impl ItemEntity {
} }
async fn try_merge_with(&self, other: &Self) { 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) if !self_stack.are_equal(&other_stack)
|| self_stack.item_count + other_stack.item_count > self_stack.get_max_stack_size() || self_stack.item_count + other_stack.item_count > self_stack.get_max_stack_size()
@ -419,7 +432,6 @@ impl EntityBase for ItemEntity {
return false; return false;
} }
// Thread safe damage application
loop { loop {
let current = self.health.load(Relaxed); let current = self.health.load(Relaxed);
let new = current - amount; let new = current - amount;
@ -446,20 +458,26 @@ impl EntityBase for ItemEntity {
return; return;
} }
if player let inserted = {
.inventory let mut stack = self.item_stack.lock().await;
.insert_stack_anywhere(&mut *self.item_stack.lock().await) player.inventory.insert_stack_anywhere(&mut stack).await
.await };
|| player.is_creative()
{ if inserted || player.is_creative() {
let (item_count, is_empty) = {
let stack = self.item_stack.lock().await;
(stack.item_count, stack.is_empty())
};
player player
.client .client
.enqueue_packet(&CTakeItemEntity::new( .enqueue_packet(&CTakeItemEntity::new(
self.entity.entity_id.into(), self.entity.entity_id.into(),
player.entity_id().into(), player.entity_id().into(),
self.item_stack.lock().await.item_count.into(), item_count.into(),
)) ))
.await; .await;
player player
.current_screen_handler .current_screen_handler
.lock() .lock()
@ -468,10 +486,10 @@ impl EntityBase for ItemEntity {
.await .await
.send_content_updates() .send_content_updates()
.await; .await;
if self.item_stack.lock().await.is_empty() {
if is_empty {
self.entity.remove().await; self.entity.remove().await;
} else { } else {
// Update entity
self.init_data_tracker().await; self.init_data_tracker().await;
} }
} }

View File

@ -1135,8 +1135,7 @@ impl LivingEntity {
let yaw = f64::from(self.entity.yaw.load()).to_radians(); let yaw = f64::from(self.entity.yaw.load()).to_radians();
velo.x -= yaw.sin() * 0.2; velo.x -= yaw.sin() * 0.2;
velo.z += yaw.cos() * 0.2;
velo.y += yaw.cos() * 0.2;
} }
self.entity.velocity.store(velo); self.entity.velocity.store(velo);
@ -1147,7 +1146,9 @@ impl LivingEntity {
async fn get_jump_velocity(&self, mut strength: f64) -> f64 { async fn get_jump_velocity(&self, mut strength: f64) -> f64 {
strength *= self.get_attribute_value(&Attributes::JUMP_STRENGTH); strength *= self.get_attribute_value(&Attributes::JUMP_STRENGTH);
strength *= f64::from(self.entity.get_jump_velocity_multiplier().await); 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 strength
} }
@ -2031,8 +2032,7 @@ impl EntityBase for LivingEntity {
} }
fn get_gravity(&self) -> f64 { fn get_gravity(&self) -> f64 {
const GRAVITY: f64 = 0.08; self.get_attribute_value(&Attributes::GRAVITY)
GRAVITY
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]