fix: mega pine trees

This commit is contained in:
Alexander Medvedev
2026-08-06 16:16:12 +02:00
parent 0b7e093c58
commit caf954d170
12 changed files with 277 additions and 119 deletions

View File

@@ -69,6 +69,7 @@ impl<R: Read> Read for CompressionRead<R> {
}
}
#[derive(Clone)]
pub struct AnvilChunkData {
compression: Option<Compression>,
// Length is always the length of this + compression byte (1) so we dont need to save a length
@@ -321,11 +322,14 @@ impl AnvilChunkData {
.map_err(|err| ChunkWritingError::ChunkSerializingError(err.to_string()))?;
let compression = compression.unwrap_or_else(|| chunk_config.compression.algorithm.into());
let level = chunk_config.compression.level;
// We need to buffer here anyway so there's no use in making an impl Write for this
let compressed_data = compression
.compress_data(&raw_bytes, chunk_config.compression.level)
.map_err(ChunkWritingError::Compression)?;
// Offload CPU-heavy compression to blocking thread pool
let compressed_data =
tokio::task::spawn_blocking(move || compression.compress_data(&raw_bytes, level))
.await
.map_err(|err| ChunkWritingError::IoError(std::io::Error::other(err)))?
.map_err(ChunkWritingError::Compression)?;
Ok(Self {
compression: Some(compression),
@@ -504,7 +508,7 @@ impl<S: SingleChunkDataSerializer> Default for AnvilChunkFile<S> {
}
}
pub trait SingleChunkDataSerializer: Send + Sync + Sized + Dirtiable {
pub trait SingleChunkDataSerializer: Send + Sync + Sized + Dirtiable + 'static {
fn to_bytes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Bytes, ChunkSerializingError>> + Send + '_>>;
@@ -512,7 +516,7 @@ pub trait SingleChunkDataSerializer: Send + Sync + Sized + Dirtiable {
fn position(&self) -> (i32, i32);
}
impl<S: SingleChunkDataSerializer> ChunkSerializer for AnvilChunkFile<S> {
impl<S: SingleChunkDataSerializer + 'static> ChunkSerializer for AnvilChunkFile<S> {
type Data = S;
type WriteBackend = PathBuf;
@@ -796,11 +800,17 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for AnvilChunkFile<S> {
let is_ok = match &self.chunks_data[index] {
None => stream.send(LoadedData::Missing(chunk)).await.is_ok(),
Some(chunk_metadata) => {
let chunk_data = &chunk_metadata.serialized_data;
let result = match chunk_data.to_chunk(chunk) {
Ok(chunk) => LoadedData::Loaded(chunk),
Err(err) => LoadedData::Error((chunk, err)),
};
let chunk_data = chunk_metadata.serialized_data.clone();
let result =
match tokio::task::spawn_blocking(move || chunk_data.to_chunk(chunk)).await
{
Ok(Ok(chunk_res)) => LoadedData::Loaded(chunk_res),
Ok(Err(err)) => LoadedData::Error((chunk, err)),
Err(err) => LoadedData::Error((
chunk,
ChunkReadingError::IoError(std::io::Error::other(err)),
)),
};
stream.send(result).await.is_ok()
}

View File

@@ -339,8 +339,12 @@ impl<S: SingleChunkDataSerializer> LinearV2File<S> {
}
/// Build the decompressed byte stream for bucket `bucket_idx`.
fn serialise_bucket(&self, bucket_idx: usize) -> Vec<u8> {
let grid_size = self.grid_size;
fn serialise_bucket(
chunks_data: &[Option<Bytes>; CHUNK_COUNT],
timestamps: &[u64; CHUNK_COUNT],
bucket_idx: usize,
grid_size: u8,
) -> Vec<u8> {
let cpb = Self::chunks_per_bucket(grid_size);
let mut buf = Vec::new();
@@ -348,8 +352,8 @@ impl<S: SingleChunkDataSerializer> LinearV2File<S> {
// Recover the global chunk index from bucket + local.
let chunk_index = Self::global_chunk_index(bucket_idx, local, grid_size);
let entry = BucketChunkEntry {
timestamp: self.timestamps[chunk_index],
data: self.chunks_data[chunk_index].clone(),
timestamp: timestamps[chunk_index],
data: chunks_data[chunk_index].clone(),
};
entry.write_into(&mut buf);
}
@@ -371,13 +375,15 @@ impl<S: SingleChunkDataSerializer> LinearV2File<S> {
fn build_bitmap(&self) -> ChunkBitmap {
let mut bitmap = ChunkBitmap::new();
for (i, chunk) in self.chunks_data.iter().enumerate() {
bitmap.set(i, chunk.is_some());
if chunk.is_some() {
bitmap.set(i, true);
}
}
bitmap
}
}
impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
impl<S: SingleChunkDataSerializer + 'static> ChunkSerializer for LinearV2File<S> {
type Data = S;
type WriteBackend = PathBuf;
type ChunkConfig = ();
@@ -398,23 +404,29 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
let grid_size = self.grid_size;
let bucket_count = Self::bucket_count(grid_size);
let chunks_data = self.chunks_data.clone();
let timestamps = self.timestamps;
let mut compressed_buckets: Vec<Box<[u8]>> = Vec::with_capacity(bucket_count);
let mut bucket_entries: Vec<BucketSizeEntry> = Vec::with_capacity(bucket_count);
let (bucket_entries, compressed_buckets) = tokio::task::spawn_blocking(move || {
let mut compressed_buckets: Vec<Box<[u8]>> = Vec::with_capacity(bucket_count);
let mut bucket_entries: Vec<BucketSizeEntry> = Vec::with_capacity(bucket_count);
for bucket_idx in 0..bucket_count {
let raw = self.serialise_bucket(bucket_idx);
// TODO: ruzstd currently only supports Fastest level.
let compressed =
compress_to_vec(raw.as_slice(), CompressionLevel::Fastest).into_boxed_slice();
let hash = xxh64(&compressed, 0);
bucket_entries.push(BucketSizeEntry {
size: compressed.len() as u32,
compression_level: 1,
xxhash: hash,
});
compressed_buckets.push(compressed);
}
for bucket_idx in 0..bucket_count {
let raw = Self::serialise_bucket(&chunks_data, &timestamps, bucket_idx, grid_size);
let compressed =
compress_to_vec(raw.as_slice(), CompressionLevel::Fastest).into_boxed_slice();
let hash = xxh64(&compressed, 0);
bucket_entries.push(BucketSizeEntry {
size: compressed.len() as u32,
compression_level: 1,
xxhash: hash,
});
compressed_buckets.push(compressed);
}
(bucket_entries, compressed_buckets)
})
.await
.map_err(std::io::Error::other)?;
let newest_timestamp = self.timestamps.iter().copied().max().unwrap_or(0);
@@ -583,15 +595,27 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
for chunk in chunks {
let index = Self::get_chunk_index(chunk.x, chunk.y);
let result = self.chunks_data[index].as_ref().map_or_else(
|| LoadedData::Missing(chunk),
|data| match S::from_bytes(data, chunk) {
Ok(c) => LoadedData::Loaded(c),
Err(err) => LoadedData::Error((chunk, err)),
},
);
let is_ok = match &self.chunks_data[index] {
None => stream.send(LoadedData::Missing(chunk)).await.is_ok(),
Some(data) => {
let data = data.clone();
let result = match tokio::task::spawn_blocking(move || {
S::from_bytes(&data, chunk)
})
.await
{
Ok(Ok(c)) => LoadedData::Loaded(c),
Ok(Err(err)) => LoadedData::Error((chunk, err)),
Err(err) => LoadedData::Error((
chunk,
ChunkReadingError::IoError(std::io::Error::other(err)),
)),
};
stream.send(result).await.is_ok()
}
};
if stream.send(result).await.is_err() {
if !is_ok {
// Receiver dropped — stop early to avoid unnecessary work.
return;
}

View File

@@ -34,7 +34,7 @@ impl<D> Default for PumpFile<D> {
impl<D> ChunkSerializer for PumpFile<D>
where
D: SingleChunkDataSerializer + Send + Sync + Sized,
D: SingleChunkDataSerializer + Send + Sync + Sized + 'static,
{
type Data = D;
type WriteBackend = PathBuf;
@@ -129,34 +129,30 @@ where
let index = (rel_x + rel_z * 32) as usize;
if let Some(chunk_bytes) = self.data.chunks.get(&index.to_string()) {
let mut decoder = match StreamingDecoder::new(&chunk_bytes[..]) {
Ok(d) => d,
Err(e) => {
let _ = stream
.send(LoadedData::Error((
pos,
ChunkReadingError::IoError(std::io::Error::other(e.to_string())),
)))
.await;
continue;
}
};
let mut decompressed = Vec::new();
if let Err(e) = std::io::Read::read_to_end(&mut decoder, &mut decompressed) {
let _ = stream
.send(LoadedData::Error((pos, ChunkReadingError::IoError(e))))
.await;
continue;
}
let chunk_bytes = chunk_bytes.clone();
let res = tokio::task::spawn_blocking(move || {
let mut decoder = StreamingDecoder::new(&chunk_bytes[..]).map_err(|e| {
ChunkReadingError::IoError(std::io::Error::other(e.to_string()))
})?;
let mut decompressed = Vec::new();
std::io::Read::read_to_end(&mut decoder, &mut decompressed)
.map_err(ChunkReadingError::IoError)?;
let bytes = Bytes::from(decompressed);
D::from_bytes(&bytes, pos)
})
.await;
let bytes = Bytes::from(decompressed);
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;
}
let data_res = match res {
Ok(Ok(data)) => LoadedData::Loaded(data),
Ok(Err(e)) => LoadedData::Error((pos, e)),
Err(e) => LoadedData::Error((
pos,
ChunkReadingError::IoError(std::io::Error::other(e)),
)),
};
if stream.send(data_res).await.is_err() {
return;
}
} else {
let _ = stream.send(LoadedData::Missing(pos)).await;

View File

@@ -58,7 +58,7 @@ struct ChunkSerializerLazyLoader<S: ChunkSerializer<WriteBackend = PathBuf>> {
internal: OnceCell<Arc<RwLock<S>>>,
}
impl<S: ChunkSerializer<WriteBackend = PathBuf>> ChunkSerializerLazyLoader<S> {
impl<S: ChunkSerializer<WriteBackend = PathBuf> + 'static> ChunkSerializerLazyLoader<S> {
fn new(path: PathBuf) -> Self {
Self {
path,
@@ -101,7 +101,9 @@ impl<S: ChunkSerializer<WriteBackend = PathBuf>> ChunkSerializerLazyLoader<S> {
match tokio::fs::read(&self.path).await {
Ok(bytes) => {
let value = S::read(bytes.into())?;
let value = tokio::task::spawn_blocking(move || S::read(bytes.into()))
.await
.map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e)))??;
trace!("Successfully read file from disk: {}", self.path.display());
Ok(value)
}
@@ -428,3 +430,92 @@ where
})
}
}
pub enum LevelFileIO<Linear, Anvil, Pump>
where
Linear: ChunkSerializer<WriteBackend = PathBuf>,
Anvil: ChunkSerializer<WriteBackend = PathBuf>,
Pump: ChunkSerializer<WriteBackend = PathBuf>,
{
Linear(ChunkFileManager<Linear>),
Anvil(ChunkFileManager<Anvil>),
Pump(ChunkFileManager<Pump>),
}
impl<P, Linear, Anvil, Pump> FileIO for LevelFileIO<Linear, Anvil, Pump>
where
P: PathFromLevelFolder + Send + Sync + Sized + Dirtiable + 'static,
Linear: ChunkSerializer<Data = P, WriteBackend = PathBuf>,
Anvil: ChunkSerializer<Data = P, WriteBackend = PathBuf>,
Pump: ChunkSerializer<Data = P, WriteBackend = PathBuf>,
Linear::ChunkConfig: Send + Sync,
Anvil::ChunkConfig: Send + Sync,
Pump::ChunkConfig: Send + Sync,
{
type Data = Arc<P>;
fn fetch_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
chunk_coords: &'a [Vector2<i32>],
stream: tokio::sync::mpsc::Sender<LoadedData<Self::Data, ChunkReadingError>>,
) -> BoxFuture<'a, ()> {
match self {
Self::Linear(io) => io.fetch_chunks(folder, chunk_coords, stream),
Self::Anvil(io) => io.fetch_chunks(folder, chunk_coords, stream),
Self::Pump(io) => io.fetch_chunks(folder, chunk_coords, stream),
}
}
fn save_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
chunks_data: Vec<(Vector2<i32>, Self::Data)>,
) -> BoxFuture<'a, Result<(), ChunkWritingError>> {
match self {
Self::Linear(io) => io.save_chunks(folder, chunks_data),
Self::Anvil(io) => io.save_chunks(folder, chunks_data),
Self::Pump(io) => io.save_chunks(folder, chunks_data),
}
}
fn watch_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
chunks: &'a [Vector2<i32>],
) -> BoxFuture<'a, ()> {
match self {
Self::Linear(io) => io.watch_chunks(folder, chunks),
Self::Anvil(io) => io.watch_chunks(folder, chunks),
Self::Pump(io) => io.watch_chunks(folder, chunks),
}
}
fn unwatch_chunks<'a>(
&'a self,
folder: &'a LevelFolder,
chunks: &'a [Vector2<i32>],
) -> BoxFuture<'a, ()> {
match self {
Self::Linear(io) => io.unwatch_chunks(folder, chunks),
Self::Anvil(io) => io.unwatch_chunks(folder, chunks),
Self::Pump(io) => io.unwatch_chunks(folder, chunks),
}
}
fn clear_watched_chunks(&self) -> BoxFuture<'_, ()> {
match self {
Self::Linear(io) => io.clear_watched_chunks(),
Self::Anvil(io) => io.clear_watched_chunks(),
Self::Pump(io) => io.clear_watched_chunks(),
}
}
fn block_and_await_ongoing_tasks(&self) -> BoxFuture<'_, ()> {
match self {
Self::Linear(io) => io.block_and_await_ongoing_tasks(),
Self::Anvil(io) => io.block_and_await_ongoing_tasks(),
Self::Pump(io) => io.block_and_await_ongoing_tasks(),
}
}
}

View File

@@ -93,7 +93,7 @@ where
///
/// The `Data` type is the type of the data that will be updated or serialized/deserialized
/// like `ChunkData` or `EntityData`
pub trait ChunkSerializer: Send + Sync + Default {
pub trait ChunkSerializer: Send + Sync + Default + 'static {
type Data: Send + Sync + Sized + Dirtiable;
type WriteBackend;

View File

@@ -211,19 +211,24 @@ impl GenerationSchedule {
pos: ChunkPos,
stage: StagedChunkEnum,
) -> i8 {
if last_high_priority.is_empty() {
return *last_level.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL) + (stage as i8);
let base_level = *last_level.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL);
if base_level == ChunkLoading::MAX_LEVEL {
return 127;
}
if last_high_priority.is_empty() {
return base_level + (stage as i8);
}
let mut min_dst = i32::MAX;
for i in last_high_priority {
let dst = max((i.x - pos.x).abs(), (i.y - pos.y).abs());
min_dst = min_dst.min(dst);
if dst <= StagedChunkEnum::FULL_RADIUS
&& stage <= StagedChunkEnum::FULL_DEPENDENCIES[dst as usize]
{
return *last_level.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL) + (stage as i8)
- 100;
return base_level + (stage as i8) - 100 + (dst as i8);
}
}
*last_level.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL) + (stage as i8)
base_level + (stage as i8) + (min_dst.min(60) as i8)
}
fn sort_queue(&mut self) {
@@ -1143,6 +1148,20 @@ impl GenerationSchedule {
continue;
}
// Cancel/drop task if chunk is out of range or no longer needed by any target/dependency
let effective_target = self
.chunk_map
.get(&node.pos)
.map_or(StagedChunkEnum::None, |h| {
h.target_stage.max(h.dependency_stage)
});
if node.stage > effective_target {
self.waiting_for_chunks.remove(&task.1);
self.drop_node(task.1);
continue;
}
if node.stage == StagedChunkEnum::Empty {
self.running_task_count += 1;
let holder = self.chunk_map.get_mut(&node.pos).unwrap();

View File

@@ -3,8 +3,8 @@ use super::generation_cache::Cache;
use super::{ChunkPos, IOLock};
use crate::ProtoChunk;
use crate::chunk::format::LightContainer;
use crate::chunk::io::LoadedData;
use crate::chunk::io::LoadedData::Loaded;
use crate::chunk::io::{FileIO, LoadedData};
use crate::level::Level;
use crossfire::compat::AsyncRx;
use pumpkin_config::lighting::LightingEngineConfig;

View File

@@ -26,16 +26,16 @@ impl MegaPineFoliagePlacer {
) -> Vec<BlockPos> {
let mut foliage_positions = Vec::new();
let pos = node.center;
let mut current = 0;
for y in pos.0.y - foliage_height + offset..pos.0.y + offset {
let mut current_radius = 0;
for y in (pos.0.y - foliage_height + offset)..=(pos.0.y + offset) {
let delta = pos.0.y - y;
let rad = radius
let computed_radius = radius
+ node.foliage_radius
+ (delta as f32 / foliage_height as f32 * 3.5).floor() as i32;
let radius = if delta > 0 && rad == current && (y & 1) == 0 {
radius + 1
+ ((delta as f32 / foliage_height as f32) * 3.5).floor() as i32;
let r = if delta > 0 && computed_radius == current_radius && (y & 1) == 0 {
computed_radius + 1
} else {
radius
computed_radius
};
FoliagePlacer::generate_square(
&mut foliage_positions,
@@ -43,12 +43,12 @@ impl MegaPineFoliagePlacer {
chunk,
random,
BlockPos::new(pos.0.x, y, pos.0.z),
radius,
r,
0,
node.giant_trunk,
foliage_provider,
);
current = rad;
current_radius = computed_radius;
}
foliage_positions
}

View File

@@ -26,7 +26,7 @@ impl PineFoliagePlacer {
) -> Vec<BlockPos> {
let mut foliage_positions = Vec::new();
let mut radius = 0;
for y in (offset - foliage_height)..offset {
for y in (offset - foliage_height..=offset).rev() {
FoliagePlacer::generate_square(
&mut foliage_positions,
self,

View File

@@ -24,47 +24,51 @@ impl GiantTrunkPlacer {
below_trunk_provider: &BlockStateProvider,
trunk_block: &BlockState,
) -> (Vec<TreeNode>, Vec<BlockPos>) {
let pos = start_pos.down();
TrunkPlacer::set_dirt(block_registry, chunk, random, &pos, below_trunk_provider);
let below = start_pos.down();
TrunkPlacer::set_dirt(block_registry, chunk, random, &below, below_trunk_provider);
TrunkPlacer::set_dirt(
block_registry,
chunk,
random,
&pos.east(),
&below.east(),
below_trunk_provider,
);
TrunkPlacer::set_dirt(
block_registry,
chunk,
random,
&pos.south(),
&below.south(),
below_trunk_provider,
);
TrunkPlacer::set_dirt(
block_registry,
chunk,
random,
&pos.south().east(),
&below.south().east(),
below_trunk_provider,
);
let mut trunk_poses = Vec::new();
for y in 0..height {
if TrunkPlacer::try_place(chunk, &pos.up_height(y as i32), trunk_block) {
trunk_poses.push(pos.up_height(y as i32));
let log_pos = start_pos.up_height(y as i32);
if TrunkPlacer::try_place(chunk, &log_pos, trunk_block) {
trunk_poses.push(log_pos);
}
if y >= height - 1 {
continue;
}
if TrunkPlacer::try_place(chunk, &pos.east().up_height(y as i32), trunk_block) {
trunk_poses.push(pos.east().up_height(y as i32));
let log_pos_east = start_pos.east().up_height(y as i32);
if TrunkPlacer::try_place(chunk, &log_pos_east, trunk_block) {
trunk_poses.push(log_pos_east);
}
if TrunkPlacer::try_place(chunk, &pos.east().south().up_height(y as i32), trunk_block) {
trunk_poses.push(pos.east().south().up_height(y as i32));
let log_pos_se = start_pos.east().south().up_height(y as i32);
if TrunkPlacer::try_place(chunk, &log_pos_se, trunk_block) {
trunk_poses.push(log_pos_se);
}
if TrunkPlacer::try_place(chunk, &pos.south().up_height(y as i32), trunk_block) {
trunk_poses.push(pos.south().up_height(y as i32));
let log_pos_south = start_pos.south().up_height(y as i32);
if TrunkPlacer::try_place(chunk, &log_pos_south, trunk_block) {
trunk_poses.push(log_pos_south);
}
}
(

View File

@@ -7,7 +7,10 @@ use crate::{
chunk::{
ChunkData, ChunkEntityData, ChunkReadingError,
format::anvil::AnvilChunkFile,
io::{Dirtiable, FileIO, LoadedData, file_manager::ChunkFileManager},
io::{
Dirtiable, FileIO, LoadedData,
file_manager::{ChunkFileManager, LevelFileIO},
},
palette::has_random_ticking_fluid,
},
generation::get_world_gen,
@@ -47,6 +50,15 @@ use tokio_util::task::TaskTracker;
pub type SyncChunk = Arc<ChunkData>;
pub type SyncEntityChunk = Arc<ChunkEntityData>;
pub type ChunkSaver =
LevelFileIO<LinearV2File<ChunkData>, AnvilChunkFile<ChunkData>, PumpFile<ChunkData>>;
pub type EntitySaver = LevelFileIO<
LinearV2File<ChunkEntityData>,
AnvilChunkFile<ChunkEntityData>,
PumpFile<ChunkEntityData>,
>;
/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
///
/// Key features include:
@@ -74,8 +86,8 @@ pub struct Level {
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
pub chunk_saver: Arc<dyn FileIO<Data = SyncChunk>>,
entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>>,
pub chunk_saver: Arc<ChunkSaver>,
entity_saver: Arc<EntitySaver>,
pub world_gen: Arc<WorldGenerator>,
@@ -216,21 +228,19 @@ impl Level {
flat_biome,
));
let chunk_saver: Arc<dyn FileIO<Data = SyncChunk>> = match &level_config.chunk {
ChunkConfig::Linear => Arc::new(ChunkFileManager::<LinearV2File<ChunkData>>::new(())),
ChunkConfig::Anvil(config) => Arc::new(
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 {
ChunkConfig::Linear => {
Arc::new(ChunkFileManager::<LinearV2File<ChunkEntityData>>::new(()))
let chunk_saver = match &level_config.chunk {
ChunkConfig::Linear => Arc::new(ChunkSaver::Linear(ChunkFileManager::new(()))),
ChunkConfig::Anvil(config) => {
Arc::new(ChunkSaver::Anvil(ChunkFileManager::new(config.clone())))
}
ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::<
AnvilChunkFile<ChunkEntityData>,
>::new(config.clone())),
ChunkConfig::Pump => Arc::new(ChunkFileManager::<PumpFile<ChunkEntityData>>::new(())),
ChunkConfig::Pump => Arc::new(ChunkSaver::Pump(ChunkFileManager::new(()))),
};
let entity_saver = match &level_config.chunk {
ChunkConfig::Linear => Arc::new(EntitySaver::Linear(ChunkFileManager::new(()))),
ChunkConfig::Anvil(config) => {
Arc::new(EntitySaver::Anvil(ChunkFileManager::new(config.clone())))
}
ChunkConfig::Pump => Arc::new(EntitySaver::Pump(ChunkFileManager::new(()))),
};
let pending_entity_generations = Arc::new(DashMap::new());

View File

@@ -182,7 +182,9 @@ impl CommandExecutor for Executor {
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let contributors = fetch_all_contributors();
let contributors = tokio::task::spawn_blocking(fetch_all_contributors)
.await
.unwrap_or_default();
let contributor_names = contributors
.iter()
.map(|c| c.login.as_str())
@@ -282,7 +284,9 @@ impl CommandExecutor for Executor {
msg = msg.add_child(TextComponent::text(" "));
let donators_hover = fetch_donators_hover();
let donators_hover = tokio::task::spawn_blocking(fetch_donators_hover)
.await
.unwrap_or_else(|_| TextComponent::text("Unable to load donators"));
msg = msg.add_child(
TextComponent::text("[Donate]")
.click_event(ClickEvent::OpenUrl {