Multi Dimension refactor

This commit is contained in:
Alexander Medvedev 2025-05-15 22:59:00 +02:00
parent 30f1a88126
commit 06c82c75f0
28 changed files with 338 additions and 176 deletions

20
Cargo.lock generated
View File

@ -194,9 +194,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.0" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@ -510,7 +510,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
"crossterm_winapi", "crossterm_winapi",
"derive_more", "derive_more",
"document-features", "document-features",
@ -1844,7 +1844,7 @@ version = "0.1.0-dev+1.21.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
"bitflags 2.9.0", "bitflags 2.9.1",
"bytes", "bytes",
"chrono", "chrono",
"crossbeam", "crossbeam",
@ -1968,7 +1968,7 @@ version = "0.1.0-dev+1.21.5"
dependencies = [ dependencies = [
"aes", "aes",
"async-compression", "async-compression",
"bitflags 2.9.0", "bitflags 2.9.1",
"bytes", "bytes",
"cfb8", "cfb8",
"flate2", "flate2",
@ -2022,7 +2022,7 @@ name = "pumpkin-world"
version = "0.1.0-dev+1.21.5" version = "0.1.0-dev+1.21.5"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bitflags 2.9.0", "bitflags 2.9.1",
"bytes", "bytes",
"criterion", "criterion",
"crossbeam", "crossbeam",
@ -2214,7 +2214,7 @@ version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]
@ -2368,7 +2368,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -2719,7 +2719,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys",
] ]
@ -3625,7 +3625,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]

View File

@ -32,7 +32,7 @@ cfb8 = "0.8"
async-compression = { version = "0.4.23", features = ["tokio", "zlib"] } async-compression = { version = "0.4.23", features = ["tokio", "zlib"] }
take_mut = "0.2.2" take_mut = "0.2.2"
bitflags = "2.9.0" bitflags = "2.9.1"
[dev-dependencies] [dev-dependencies]
# Validate correctness # Validate correctness

View File

@ -95,6 +95,16 @@ impl DimensionType {
Self::TheNether => Identifier::vanilla("the_nether"), Self::TheNether => Identifier::vanilla("the_nether"),
} }
} }
pub fn from_name(name: &str) -> Option<Self> {
match name {
"minecraft:overworld" => Some(Self::Overworld),
"minecraft:overworld_caves" => Some(Self::OverworldCaves),
"minecraft:the_end" => Some(Self::TheEnd),
"minecraft:the_nether" => Some(Self::TheNether),
_ => None,
}
}
} }
impl Registry { impl Registry {

View File

@ -26,7 +26,7 @@ crossbeam.workspace = true
num-derive = "0.4" num-derive = "0.4"
sha2 = "0.10" sha2 = "0.10"
bitflags = "2.9.0" bitflags = "2.9.1"
dashmap = "6.1" dashmap = "6.1"

View File

@ -80,7 +80,7 @@ fn initialize_level(
let (send, mut recv) = tokio::sync::mpsc::unbounded_channel(); let (send, mut recv) = tokio::sync::mpsc::unbounded_channel();
// Our data dir is empty, so we're generating new chunks here // Our data dir is empty, so we're generating new chunks here
let level_to_save = Arc::new(Level::from_root_folder(root_dir.clone())); let level_to_save = Arc::new(Level::from_root_folder(root_dir.clone(), 123));
println!("Level Seed is: {}", level_to_save.seed.0); println!("Level Seed is: {}", level_to_save.seed.0);
let level_to_fetch = level_to_save.clone(); let level_to_fetch = level_to_save.clone();
@ -194,7 +194,7 @@ fn bench_chunk_io(c: &mut Criterion) {
&chunks, &chunks,
|b, chunks| { |b, chunks| {
b.to_async(&async_handler).iter(async || { b.to_async(&async_handler).iter(async || {
let level = Arc::new(Level::from_root_folder(root_dir.clone())); let level = Arc::new(Level::from_root_folder(root_dir.clone(), 123));
test_writes(&level, chunks.to_vec()).await test_writes(&level, chunks.to_vec()).await
}) })
}, },
@ -220,7 +220,7 @@ fn bench_chunk_io(c: &mut Criterion) {
&positions, &positions,
|b, positions| { |b, positions| {
b.to_async(&async_handler).iter(async || { b.to_async(&async_handler).iter(async || {
let level = Arc::new(Level::from_root_folder(root_dir.clone())); let level = Arc::new(Level::from_root_folder(root_dir.clone(), 123));
test_reads(&level, positions.to_vec()).await test_reads(&level, positions.to_vec()).await
}) })
}, },

View File

@ -13,12 +13,12 @@ pub enum Dimension {
} }
impl Dimension { impl Dimension {
pub fn into_level(&self, mut base_directory: PathBuf) -> Level { pub fn into_level(&self, mut base_directory: PathBuf, seed: i64) -> Level {
match self { match self {
Dimension::Overworld => {} Dimension::Overworld => {}
Dimension::Nether => base_directory.push("DIM-1"), Dimension::Nether => base_directory.push("DIM-1"),
Dimension::End => base_directory.push("DIM1"), Dimension::End => base_directory.push("DIM1"),
} }
Level::from_root_folder(base_directory) Level::from_root_folder(base_directory, seed)
} }
} }

View File

@ -137,7 +137,7 @@ pub enum MaterialCondition {
#[serde(rename = "minecraft:noise_threshold")] #[serde(rename = "minecraft:noise_threshold")]
NoiseThreshold(NoiseThresholdMaterialCondition), NoiseThreshold(NoiseThresholdMaterialCondition),
#[serde(rename = "minecraft:vertical_gradient")] #[serde(rename = "minecraft:vertical_gradient")]
VerticalGradient(VerticalGradientMaterialCondition), VerticalGradient(Box<VerticalGradientMaterialCondition>),
#[serde(rename = "minecraft:y_above")] #[serde(rename = "minecraft:y_above")]
YAbove(AboveYMaterialCondition), YAbove(AboveYMaterialCondition),
#[serde(rename = "minecraft:water")] #[serde(rename = "minecraft:water")]

View File

@ -18,7 +18,7 @@ pub enum MaterialRule {
#[serde(rename = "minecraft:sequence")] #[serde(rename = "minecraft:sequence")]
Sequence(SequenceMaterialRule), Sequence(SequenceMaterialRule),
#[serde(rename = "minecraft:condition")] #[serde(rename = "minecraft:condition")]
Condition(ConditionMaterialRule), Condition(Box<ConditionMaterialRule>),
} }
impl MaterialRule { impl MaterialRule {

View File

@ -1,5 +1,4 @@
use std::{ use std::{
fs,
path::PathBuf, path::PathBuf,
sync::{ sync::{
Arc, Arc,
@ -25,12 +24,7 @@ use crate::{
io::{ChunkIO, LoadedData, chunk_file_manager::ChunkFileManager}, io::{ChunkIO, LoadedData, chunk_file_manager::ChunkFileManager},
}, },
generation::{Seed, WorldGenerator, get_world_gen}, generation::{Seed, WorldGenerator, get_world_gen},
lock::{LevelLocker, anvil::AnvilLevelLocker},
world::SimpleWorld, world::SimpleWorld,
world_info::{
LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter,
anvil::{AnvilLevelInfo, LEVEL_DAT_BACKUP_FILE_NAME, LEVEL_DAT_FILE_NAME},
},
}; };
pub type SyncChunk = Arc<RwLock<ChunkData>>; pub type SyncChunk = Arc<RwLock<ChunkData>>;
@ -46,8 +40,6 @@ pub type SyncChunk = Arc<RwLock<ChunkData>>;
/// For more details on world generation, refer to the `WorldGenerator` module. /// For more details on world generation, refer to the `WorldGenerator` module.
pub struct Level { pub struct Level {
pub seed: Seed, pub seed: Seed,
pub level_info: LevelData,
world_info_writer: Arc<dyn WorldInfoWriter>,
level_folder: LevelFolder, level_folder: LevelFolder,
// Holds this level's spawn chunks, which are always loaded // Holds this level's spawn chunks, which are always loaded
@ -60,9 +52,7 @@ pub struct Level {
chunk_saver: Arc<dyn ChunkIO<Data = SyncChunk>>, chunk_saver: Arc<dyn ChunkIO<Data = SyncChunk>>,
world_gen: Arc<dyn WorldGenerator>, world_gen: Arc<dyn WorldGenerator>,
// Gets unlocked when dropped
// TODO: Make this a trait
_locker: Arc<AnvilLevelLocker>,
block_ticks: Arc<Mutex<Vec<ScheduledTick>>>, block_ticks: Arc<Mutex<Vec<ScheduledTick>>>,
fluid_ticks: Arc<Mutex<Vec<ScheduledTick>>>, fluid_ticks: Arc<Mutex<Vec<ScheduledTick>>>,
/// Tracks tasks associated with this world instance /// Tracks tasks associated with this world instance
@ -78,7 +68,7 @@ pub struct LevelFolder {
} }
impl Level { impl Level {
pub fn from_root_folder(root_folder: PathBuf) -> Self { pub fn from_root_folder(root_folder: PathBuf, seed: i64) -> 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 // 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"); let region_folder = root_folder.join("region");
if !region_folder.exists() { if !region_folder.exists() {
@ -89,40 +79,11 @@ impl Level {
region_folder, region_folder,
}; };
// if we fail to lock, lets crash ???. maybe not the best solution when we have a large server with many worlds and one is locked.
// So TODO
let locker = AnvilLevelLocker::look(&level_folder).expect("Failed to lock level");
// TODO: Load info correctly based on world format type // TODO: Load info correctly based on world format type
let level_info = AnvilLevelInfo.read_world_info(&level_folder);
if let Err(error) = &level_info {
match error {
// If it doesn't exist, just make a new one
WorldInfoError::InfoNotFound => (),
WorldInfoError::UnsupportedVersion(version) => {
log::error!("Failed to load world info!, {version}");
log::error!("{error}");
panic!("Unsupported world data! See the logs for more info.");
}
e => {
panic!("World Error {e}");
}
}
} else {
let dat_path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
if dat_path.exists() {
let backup_path = level_folder.root_folder.join(LEVEL_DAT_BACKUP_FILE_NAME);
fs::copy(dat_path, backup_path).unwrap();
}
}
let level_info = level_info.unwrap_or_default(); // TODO: Improve error handling log::info!("Loading world with seed: {}", seed);
log::info!(
"Loading world with seed: {}",
level_info.world_gen_settings.seed
);
let seed = Seed(level_info.world_gen_settings.seed as u64); let seed = Seed(seed as u64);
let world_gen = get_world_gen(seed).into(); let world_gen = get_world_gen(seed).into();
let chunk_saver: Arc<dyn ChunkIO<Data = SyncChunk>> = match advanced_config().chunk.format { let chunk_saver: Arc<dyn ChunkIO<Data = SyncChunk>> = match advanced_config().chunk.format {
@ -134,14 +95,11 @@ impl Level {
Self { Self {
seed, seed,
world_gen, world_gen,
world_info_writer: Arc::new(AnvilLevelInfo),
level_folder, level_folder,
chunk_saver, chunk_saver,
spawn_chunks: Arc::new(DashMap::new()), spawn_chunks: Arc::new(DashMap::new()),
loaded_chunks: Arc::new(DashMap::new()), loaded_chunks: Arc::new(DashMap::new()),
chunk_watchers: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()),
level_info,
_locker: Arc::new(locker),
tasks: TaskTracker::new(), tasks: TaskTracker::new(),
shutdown_notifier: Notify::new(), shutdown_notifier: Notify::new(),
block_ticks: Arc::new(Mutex::new(Vec::new())), block_ticks: Arc::new(Mutex::new(Vec::new())),
@ -182,16 +140,6 @@ impl Level {
// TODO: I think the chunk_saver should be at the server level // TODO: I think the chunk_saver should be at the server level
self.chunk_saver.clear_watched_chunks().await; self.chunk_saver.clear_watched_chunks().await;
self.write_chunks(chunks_to_write).await; self.write_chunks(chunks_to_write).await;
// then lets save the world info
let result = self
.world_info_writer
.write_world_info(self.level_info.clone(), &self.level_folder);
// Lets not stop the overall save for this
if let Err(err) = result {
log::error!("Failed to save level.dat: {err}");
}
} }
pub fn loaded_chunk_count(&self) -> usize { pub fn loaded_chunk_count(&self) -> usize {

View File

@ -12,7 +12,7 @@ mod generation;
pub mod inventory; pub mod inventory;
pub mod item; pub mod item;
pub mod level; pub mod level;
mod lock; pub mod lock;
pub mod world; pub mod world;
pub mod world_info; pub mod world_info;

View File

@ -2,7 +2,7 @@ use file_guard::{FileGuard, Lock};
use super::{LevelLocker, LockError}; use super::{LevelLocker, LockError};
use std::{fs::File, io::Write, sync::Arc}; use std::{fs::File, io::Write, path::Path, sync::Arc};
pub struct AnvilLevelLocker { pub struct AnvilLevelLocker {
_lock: Option<FileGuard<Arc<File>>>, _lock: Option<FileGuard<Arc<File>>>,
@ -13,8 +13,8 @@ const SESSION_LOCK_FILE_NAME: &str = "session.lock";
const SNOWMAN: &[u8] = "☃".as_bytes(); const SNOWMAN: &[u8] = "☃".as_bytes();
impl LevelLocker<Self> for AnvilLevelLocker { impl LevelLocker<Self> for AnvilLevelLocker {
fn look(folder: &crate::level::LevelFolder) -> Result<Self, LockError> { fn look(folder: &Path) -> Result<Self, LockError> {
let file_path = folder.root_folder.join(SESSION_LOCK_FILE_NAME); let file_path = folder.join(SESSION_LOCK_FILE_NAME);
let mut file = File::options() let mut file = File::options()
.create(true) .create(true)
.truncate(false) .truncate(false)

View File

@ -1,12 +1,12 @@
use thiserror::Error; use std::path::Path;
use crate::level::LevelFolder; use thiserror::Error;
pub mod anvil; pub mod anvil;
// Gets unlocked when dropped // Gets unlocked when dropped
pub trait LevelLocker<T>: Send + Sync { pub trait LevelLocker<T>: Send + Sync {
fn look(folder: &LevelFolder) -> Result<T, LockError>; fn look(folder: &Path) -> Result<T, LockError>;
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]

View File

@ -1,15 +1,15 @@
use std::{ use std::{
fs::OpenOptions, fs::OpenOptions,
io::Read, io::Read,
path::Path,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use flate2::{Compression, read::GzDecoder, write::GzEncoder}; use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::world_info::{
level::LevelFolder, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION,
world_info::{MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION},
}; };
use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter}; use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};
@ -54,8 +54,8 @@ fn check_file_data_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> {
} }
impl WorldInfoReader for AnvilLevelInfo { impl WorldInfoReader for AnvilLevelInfo {
fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError> { fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError> {
let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME); let path = level_folder.join(LEVEL_DAT_FILE_NAME);
let world_info_file = OpenOptions::new().read(true).open(path)?; let world_info_file = OpenOptions::new().read(true).open(path)?;
let mut compression_reader = GzDecoder::new(world_info_file); let mut compression_reader = GzDecoder::new(world_info_file);
@ -75,19 +75,21 @@ impl WorldInfoReader for AnvilLevelInfo {
impl WorldInfoWriter for AnvilLevelInfo { impl WorldInfoWriter for AnvilLevelInfo {
fn write_world_info( fn write_world_info(
&self, &self,
info: LevelData, info: &LevelData,
level_folder: &LevelFolder, level_folder: &Path,
) -> Result<(), WorldInfoError> { ) -> Result<(), WorldInfoError> {
let start = SystemTime::now(); let start = SystemTime::now();
let since_the_epoch = start let since_the_epoch = start
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Time went backwards"); .expect("Time went backwards");
let mut level_data = info; let mut level_data = info.clone();
level_data.last_played = since_the_epoch.as_millis() as i64; level_data.last_played = since_the_epoch.as_millis() as i64;
let level = LevelDat { data: level_data }; let level = LevelDat {
data: level_data.clone(),
};
// open file // open file
let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME); let path = level_folder.join(LEVEL_DAT_FILE_NAME);
let world_info_file = OpenOptions::new() let world_info_file = OpenOptions::new()
.truncate(true) .truncate(true)
.create(true) .create(true)
@ -122,7 +124,6 @@ mod test {
use crate::{ use crate::{
global_path, global_path,
level::LevelFolder,
world_info::{DataPacks, LevelData, WorldGenSettings, WorldInfoError, WorldVersion}, world_info::{DataPacks, LevelData, WorldGenSettings, WorldInfoError, WorldVersion},
}; };
@ -136,16 +137,12 @@ mod test {
data.world_gen_settings.seed = seed; data.world_gen_settings.seed = seed;
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {
root_folder: temp_dir.path().to_path_buf(),
region_folder: temp_dir.path().join("region"),
};
AnvilLevelInfo AnvilLevelInfo
.write_world_info(data, &level_folder) .write_world_info(&data, temp_dir.path())
.unwrap(); .unwrap();
let data = AnvilLevelInfo.read_world_info(&level_folder).unwrap(); let data = AnvilLevelInfo.read_world_info(temp_dir.path()).unwrap();
assert_eq!(data.world_gen_settings.seed, seed); assert_eq!(data.world_gen_settings.seed, seed);
} }
@ -219,15 +216,15 @@ mod test {
#[test] #[test]
fn failed_deserialize_old_level_dat() { fn failed_deserialize_old_level_dat() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {
root_folder: temp_dir.path().to_path_buf(),
region_folder: temp_dir.path().join("region"),
};
let test_dat = global_path!("../../assets/level_1_20.dat"); let test_dat = global_path!("../../assets/level_1_20.dat");
fs::copy(test_dat, level_folder.root_folder.join(LEVEL_DAT_FILE_NAME)).unwrap(); fs::copy(
test_dat,
temp_dir.path().to_path_buf().join(LEVEL_DAT_FILE_NAME),
)
.unwrap();
let result = AnvilLevelInfo.read_world_info(&level_folder); let result = AnvilLevelInfo.read_world_info(temp_dir.path());
match result { match result {
Ok(_) => panic!("This should fail!"), Ok(_) => panic!("This should fail!"),
Err(WorldInfoError::UnsupportedVersion(_)) => {} Err(WorldInfoError::UnsupportedVersion(_)) => {}

View File

@ -1,9 +1,11 @@
use std::path::Path;
use pumpkin_config::BASIC_CONFIG; use pumpkin_config::BASIC_CONFIG;
use pumpkin_util::{Difficulty, serde_enum_as_integer}; use pumpkin_util::{Difficulty, serde_enum_as_integer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{generation::Seed, level::LevelFolder}; use crate::generation::Seed;
pub mod anvil; pub mod anvil;
@ -11,16 +13,13 @@ pub mod anvil;
pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4325; // 1.21.5 pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4325; // 1.21.5
pub const MAXIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4325; // 1.21.5 pub const MAXIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4325; // 1.21.5
pub(crate) trait WorldInfoReader { pub trait WorldInfoReader {
fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError>; fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError>;
} }
pub(crate) trait WorldInfoWriter: Sync + Send { pub trait WorldInfoWriter: Sync + Send {
fn write_world_info( fn write_world_info(&self, info: &LevelData, level_folder: &Path)
&self, -> Result<(), WorldInfoError>;
info: LevelData,
level_folder: &LevelFolder,
) -> Result<(), WorldInfoError>;
} }
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]

View File

@ -82,7 +82,7 @@ chrono = { version = "0.4", features = ["serde"] }
# plugins # plugins
libloading = "0.8" libloading = "0.8"
bitflags = "2.9.0" bitflags = "2.9.1"
rustc-hash = "2.1.1" rustc-hash = "2.1.1"
# Task handling # Task handling

View File

@ -16,7 +16,7 @@ use crate::block::pumpkin_block::PumpkinBlock;
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::server::Server; use crate::server::Server;
use crate::world::World; use crate::world::World;
use crate::world::portal::NetherPortal; use crate::world::portal::nether::NetherPortal;
use super::FireBlockBase; use super::FireBlockBase;

View File

@ -21,7 +21,7 @@ impl PumpkinBlock for LilyPadBlock {
_block: Block, _block: Block,
_state: BlockState, _state: BlockState,
) { ) {
// Proberbly not the best soultion, but works // Proberbly not the best solution, but works
if entity if entity
.get_entity() .get_entity()
.entity_type .entity_type

View File

@ -1,9 +1,12 @@
use std::sync::Arc;
use crate::block::pumpkin_block::PumpkinBlock; use crate::block::pumpkin_block::PumpkinBlock;
use crate::entity::EntityBase;
use crate::world::World; use crate::world::World;
use crate::world::portal::NetherPortal; use crate::world::portal::nether::NetherPortal;
use async_trait::async_trait; use async_trait::async_trait;
use pumpkin_data::Block;
use pumpkin_data::block_properties::{Axis, BlockProperties, NetherPortalLikeProperties}; use pumpkin_data::block_properties::{Axis, BlockProperties, NetherPortalLikeProperties};
use pumpkin_data::{Block, BlockState};
use pumpkin_macros::pumpkin_block; use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_world::BlockStateId; use pumpkin_world::BlockStateId;
@ -38,4 +41,14 @@ impl PumpkinBlock for NetherPortalBlock {
} }
Block::AIR.default_state_id Block::AIR.default_state_id
} }
async fn on_entity_collision(
&self,
_world: &Arc<World>,
_entity: &dyn EntityBase,
_pos: BlockPos,
_block: Block,
_state: BlockState,
) {
}
} }

View File

@ -27,12 +27,13 @@ pub trait LoadJSONConfiguration {
let path = data_dir.join(Self::get_path()); let path = data_dir.join(Self::get_path());
let config = if path.exists() { let config = if path.exists() {
let file_content = fs::read_to_string(&path) let file_content = fs::read_to_string(&path).unwrap_or_else(|_| {
.unwrap_or_else(|_| panic!("Couldn't read configuration file at {path:?}")); panic!("Couldn't read configuration file at {}", path.display())
});
serde_json::from_str(&file_content).unwrap_or_else(|err| { serde_json::from_str(&file_content).unwrap_or_else(|err| {
panic!( panic!(
"Couldn't parse data config at {path:?}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.", "Couldn't parse data config at {}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.", path.display(),
) )
}) })
} else { } else {
@ -40,7 +41,8 @@ pub trait LoadJSONConfiguration {
if let Err(err) = fs::write(&path, serde_json::to_string_pretty(&content).unwrap()) { if let Err(err) = fs::write(&path, serde_json::to_string_pretty(&content).unwrap()) {
log::error!( log::error!(
"Couldn't write default data config to {path:?}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.", "Couldn't write default data config to {}. Reason: {err}. This is probably caused by a config update. Just delete the old data config and restart.",
path.display(),
); );
} }
@ -72,13 +74,19 @@ pub trait SaveJSONConfiguration: LoadJSONConfiguration {
let content = match serde_json::to_string_pretty(self) { let content = match serde_json::to_string_pretty(self) {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
log::warn!("Couldn't serialize operator data config to {path:?}. Reason: {err}",); log::warn!(
"Couldn't serialize operator data config to {}. Reason: {err}",
path.display()
);
return; return;
} }
}; };
if let Err(err) = std::fs::write(&path, content) { if let Err(err) = std::fs::write(&path, content) {
log::warn!("Couldn't write operator config to {path:?}. Reason: {err}",); log::warn!(
"Couldn't write operator config to {}. Reason: {err}",
path.display()
);
} }
} }
} }

View File

@ -31,21 +31,6 @@ impl ServerPlayerData {
} }
} }
/// Handles a player joining the server.
///
/// This function loads player data and applies it to a newly joined player.
///
/// # Arguments
///
/// * `player` - The player who joined.
///
/// # Returns
///
/// A Result indicating success or the error that occurred.
pub async fn handle_player_join(&self, player: &mut Player) -> Result<(), PlayerDataError> {
self.load_and_apply_data_to_player(player).await
}
/// Handles a player leaving the server. /// Handles a player leaving the server.
/// ///
/// This function saves player data when they disconnect. /// This function saves player data when they disconnect.
@ -141,19 +126,14 @@ impl ServerPlayerData {
/// # Returns /// # Returns
/// ///
/// A Result indicating success or the error that occurred. /// A Result indicating success or the error that occurred.
pub async fn load_and_apply_data_to_player( pub fn load_data(&self, uuid: &uuid::Uuid) -> Result<Option<NbtCompound>, PlayerDataError> {
&self,
player: &mut Player,
) -> Result<(), PlayerDataError> {
let uuid = &player.gameprofile.id;
match self.storage.load_player_data(uuid) { match self.storage.load_player_data(uuid) {
Ok((should_load, mut data)) => { Ok((should_load, data)) => {
if !should_load { if !should_load {
// No data to load, continue with default data // No data to load, continue with default data
return Ok(()); return Ok(None);
} }
player.read_nbt(&mut data).await; Ok(Some(data))
Ok(())
} }
Err(e) => { Err(e) => {
if self.storage.is_save_enabled() { if self.storage.is_save_enabled() {
@ -164,7 +144,7 @@ impl ServerPlayerData {
log::debug!("Not loading player data for {uuid} (saving disabled)"); log::debug!("Not loading player data for {uuid} (saving disabled)");
} }
// Continue with default data even if there's an error // Continue with default data even if there's an error
Ok(()) Ok(None)
} }
} }
} }

View File

@ -32,7 +32,7 @@ impl HungerManager {
let level = self.level.load(); let level = self.level.load();
let exhaustion = self.exhaustion.load(); let exhaustion = self.exhaustion.load();
let health = player.living_entity.health.load(); let health = player.living_entity.health.load();
let difficulty = player.world().await.level.level_info.difficulty.clone(); let difficulty = player.world().await.level_info.difficulty.clone();
// Decrease hunger level on exhaustion // Decrease hunger level on exhaustion
if level != 0 && exhaustion > 4.0 { if level != 0 && exhaustion > 4.0 {
self.exhaustion.store(exhaustion - 4.0); self.exhaustion.store(exhaustion - 4.0);

View File

@ -62,6 +62,7 @@ pub async fn from_type(
goals: Mutex::new(vec![]), goals: Mutex::new(vec![]),
navigator: Mutex::new(Navigator::default()), navigator: Mutex::new(Navigator::default()),
}; };
#[expect(clippy::single_match)]
match entity_type { match entity_type {
EntityType::ZOMBIE => Zombie::make(&mob).await, EntityType::ZOMBIE => Zombie::make(&mob).await,
// TODO // TODO

View File

@ -1,4 +1,7 @@
use crate::server::Server; use crate::{
server::Server,
world::portal::{Portal, PortalManager},
};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::BufMut; use bytes::BufMut;
use core::f32; use core::f32;
@ -32,11 +35,11 @@ use serde::Serialize;
use std::sync::{ use std::sync::{
Arc, Arc,
atomic::{ atomic::{
AtomicBool, AtomicI32, AtomicBool, AtomicI32, AtomicU32,
Ordering::{Relaxed, SeqCst}, Ordering::{self, Relaxed, SeqCst},
}, },
}; };
use tokio::sync::RwLock; use tokio::sync::{Mutex, RwLock};
use crate::world::World; use crate::world::World;
@ -135,6 +138,11 @@ pub struct Entity {
pub invulnerable: AtomicBool, pub invulnerable: AtomicBool,
/// List of damage types this entity is immune to /// List of damage types this entity is immune to
pub damage_immunities: Vec<DamageType>, pub damage_immunities: Vec<DamageType>,
pub portal_cooldown: AtomicU32,
// :c
pub portal_manager: Mutex<Option<Mutex<PortalManager>>>,
} }
impl Entity { impl Entity {
@ -181,6 +189,8 @@ impl Entity {
bounding_box_size: AtomicCell::new(bounding_box_size), bounding_box_size: AtomicCell::new(bounding_box_size),
invulnerable: AtomicBool::new(invulnerable), invulnerable: AtomicBool::new(invulnerable),
damage_immunities: Vec::new(), damage_immunities: Vec::new(),
portal_cooldown: AtomicU32::new(0),
portal_manager: Mutex::new(None),
} }
} }
@ -296,6 +306,53 @@ impl Entity {
self.set_rotation(yaw, pitch); self.set_rotation(yaw, pitch);
} }
fn default_portal_cooldown(&self) -> u32 {
if self.entity_type == EntityType::PLAYER {
10
} else {
300
}
}
async fn tick_portal(&self) {
if self.portal_cooldown.load(Ordering::Relaxed) > 0 {
self.portal_cooldown.fetch_sub(1, Ordering::Relaxed);
}
let mut manager_guard = self.portal_manager.lock().await;
// I know this is ugly, but a quick fix because i can't modify the thing while using it
let mut should_remove = false;
if let Some(pmanager_mutex) = manager_guard.as_ref() {
let mut portal_manager = pmanager_mutex.lock().await;
if portal_manager.tick() {
// reset cooldown
self.portal_cooldown
.store(self.default_portal_cooldown(), Ordering::Relaxed);
//let world = portal_manager.portal.get_world(self);
} else {
should_remove = true;
}
}
if should_remove {
*manager_guard = None;
}
}
pub async fn try_use_portal(&self, portal: Arc<dyn Portal>, pos: BlockPos) {
if self.portal_cooldown.load(Ordering::Relaxed) > 0 {
self.portal_cooldown
.store(self.default_portal_cooldown(), Ordering::Relaxed);
return;
}
let mut manager = self.portal_manager.lock().await;
if manager.is_none() {
*manager = Some(Mutex::new(PortalManager::new(portal, pos)));
} else if let Some(manager) = manager.as_ref() {
let mut manager = manager.lock().await;
manager.pos = pos;
manager.in_portal = true;
}
}
/// Sets the `Entity` yaw & pitch rotation /// Sets the `Entity` yaw & pitch rotation
pub fn set_rotation(&self, yaw: f32, pitch: f32) { pub fn set_rotation(&self, yaw: f32, pitch: f32) {
// TODO // TODO
@ -527,6 +584,7 @@ impl EntityBase for Entity {
} }
async fn tick(&self, _caller: &dyn EntityBase, _: &Server) { async fn tick(&self, _caller: &dyn EntityBase, _: &Server) {
self.tick_portal().await;
// TODO: Tick // TODO: Tick
} }

View File

@ -886,7 +886,7 @@ impl Player {
pitch: Option<f32>, pitch: Option<f32>,
) { ) {
let current_world = self.living_entity.entity.world.read().await.clone(); let current_world = self.living_entity.entity.world.read().await.clone();
let info = &new_world.level.level_info; let info = &new_world.level_info;
let position = if let Some(pos) = position { let position = if let Some(pos) = position {
pos pos
} else { } else {

View File

@ -2,6 +2,7 @@ use crate::block::registry::BlockRegistry;
use crate::command::commands::default_dispatcher; use crate::command::commands::default_dispatcher;
use crate::command::commands::defaultgamemode::DefaultGamemode; use crate::command::commands::defaultgamemode::DefaultGamemode;
use crate::data::player_server_data::ServerPlayerData; use crate::data::player_server_data::ServerPlayerData;
use crate::entity::NBTStorage;
use crate::item::registry::ItemRegistry; use crate::item::registry::ItemRegistry;
use crate::net::EncryptionError; use crate::net::EncryptionError;
use crate::plugin::player::player_login::PlayerLoginEvent; use crate::plugin::player::player_login::PlayerLoginEvent;
@ -22,8 +23,15 @@ use pumpkin_registry::{DimensionType, Registry};
use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use pumpkin_world::dimension::Dimension; use pumpkin_world::dimension::Dimension;
use pumpkin_world::lock::LevelLocker;
use pumpkin_world::lock::anvil::AnvilLevelLocker;
use pumpkin_world::world_info::anvil::{
AnvilLevelInfo, LEVEL_DAT_BACKUP_FILE_NAME, LEVEL_DAT_FILE_NAME,
};
use pumpkin_world::world_info::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rsa::RsaPublicKey; use rsa::RsaPublicKey;
use std::fs;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, AtomicU32}; use std::sync::atomic::{AtomicBool, AtomicU32};
use std::{ use std::{
@ -57,7 +65,7 @@ pub struct Server {
pub item_registry: Arc<ItemRegistry>, pub item_registry: Arc<ItemRegistry>,
/// Manages multiple worlds within the server. /// Manages multiple worlds within the server.
pub worlds: RwLock<Vec<Arc<World>>>, pub worlds: RwLock<Vec<Arc<World>>>,
// All the dimensions that exist on the server. /// All the dimensions that exist on the server.
pub dimensions: Vec<DimensionType>, pub dimensions: Vec<DimensionType>,
/// Caches game registries for efficient access. /// Caches game registries for efficient access.
pub cached_registry: Vec<Registry>, pub cached_registry: Vec<Registry>,
@ -77,6 +85,13 @@ pub struct Server {
// Whether the server whitelist is on or off // Whether the server whitelist is on or off
pub white_list: AtomicBool, pub white_list: AtomicBool,
tasks: TaskTracker, tasks: TaskTracker,
// world stuff which maybe should be put into a struct
pub level_info: Arc<LevelData>,
world_info_writer: Arc<dyn WorldInfoWriter>,
// Gets unlocked when dropped
// TODO: Make this a trait
_locker: Arc<AnvilLevelLocker>,
} }
impl Server { impl Server {
@ -101,18 +116,58 @@ impl Server {
let block_registry = super::block::default_registry(); let block_registry = super::block::default_registry();
let world = World::load( let level_info = AnvilLevelInfo.read_world_info(&world_path);
Dimension::Overworld.into_level(world_path.clone()), if let Err(error) = &level_info {
match error {
// If it doesn't exist, just make a new one
WorldInfoError::InfoNotFound => (),
WorldInfoError::UnsupportedVersion(version) => {
log::error!("Failed to load world info!, {version}");
log::error!("{error}");
panic!("Unsupported world data! See the logs for more info.");
}
e => {
panic!("World Error {e}");
}
}
} else {
let dat_path = world_path.join(LEVEL_DAT_FILE_NAME);
if dat_path.exists() {
let backup_path = world_path.join(LEVEL_DAT_BACKUP_FILE_NAME);
fs::copy(dat_path, backup_path).unwrap();
}
}
let level_info = Arc::new(level_info.unwrap_or_default()); // TODO: Improve error handling
let overworld = World::load(
Dimension::Overworld.into_level(world_path.clone(), level_info.world_gen_settings.seed),
level_info.clone(),
DimensionType::Overworld, DimensionType::Overworld,
block_registry.clone(), block_registry.clone(),
); );
let nether = World::load(
Dimension::Nether.into_level(world_path.clone(), level_info.world_gen_settings.seed),
level_info.clone(),
DimensionType::TheNether,
block_registry.clone(),
);
let end = World::load(
Dimension::End.into_level(world_path.clone(), level_info.world_gen_settings.seed),
level_info.clone(),
DimensionType::TheEnd,
block_registry.clone(),
);
// if we fail to lock, lets crash ???. maybe not the best solution when we have a large server with many worlds and one is locked.
// So TODO
let locker = AnvilLevelLocker::look(&world_path).expect("Failed to lock level");
let world_name = world_path.to_str().unwrap(); let world_name = world_path.to_str().unwrap();
Self { Self {
cached_registry: Registry::get_synced(), cached_registry: Registry::get_synced(),
container_id: 0.into(), container_id: 0.into(),
worlds: RwLock::new(vec![Arc::new(world)]), worlds: RwLock::new(vec![Arc::new(overworld), Arc::new(nether), Arc::new(end)]),
dimensions: vec![ dimensions: vec![
DimensionType::Overworld, DimensionType::Overworld,
DimensionType::OverworldCaves, DimensionType::OverworldCaves,
@ -137,6 +192,9 @@ impl Server {
white_list: AtomicBool::new(BASIC_CONFIG.white_list), white_list: AtomicBool::new(BASIC_CONFIG.white_list),
tasks: TaskTracker::new(), tasks: TaskTracker::new(),
mojang_public_keys: Mutex::new(Vec::new()), mojang_public_keys: Mutex::new(Vec::new()),
world_info_writer: Arc::new(AnvilLevelInfo),
level_info,
_locker: Arc::new(locker),
} }
} }
@ -162,6 +220,18 @@ impl Server {
self.tasks.spawn(task) self.tasks.spawn(task)
} }
pub async fn get_world_from_dimension(&self, dimension: DimensionType) -> Arc<World> {
// TODO: this is really bad
let world_guard = self.worlds.read().await;
let world = match dimension {
DimensionType::Overworld => world_guard.first(),
DimensionType::OverworldCaves => todo!(),
DimensionType::TheEnd => world_guard.get(2),
DimensionType::TheNether => world_guard.get(1),
};
world.cloned().unwrap()
}
#[allow(clippy::if_then_some_else_none)] #[allow(clippy::if_then_some_else_none)]
/// Adds a new player to the server. /// Adds a new player to the server.
/// ///
@ -190,19 +260,43 @@ impl Server {
/// You still have to spawn the `Player` in a `World` to let them join and make them visible. /// You still have to spawn the `Player` in a `World` to let them join and make them visible.
pub async fn add_player(&self, client: Client) -> Option<(Arc<Player>, Arc<World>)> { pub async fn add_player(&self, client: Client) -> Option<(Arc<Player>, Arc<World>)> {
let gamemode = self.defaultgamemode.lock().await.gamemode; let gamemode = self.defaultgamemode.lock().await.gamemode;
// Basically the default world let uuid = client.gameprofile.lock().await.as_ref().unwrap().id;
// TODO: select default from config
let world = &self.worlds.read().await[0]; let (world, nbt) = if let Ok(Some(data)) = self.player_data_storage.load_data(&uuid) {
if let Some(dimension_key) = data.get_string("Dimension") {
if let Some(dimension) = DimensionType::from_name(dimension_key) {
let world = self.get_world_from_dimension(dimension).await;
(world, Some(data))
} else {
log::warn!("Invalid dimension key in player data: {dimension_key}");
let default_world_guard = self.worlds.read().await;
let default_world = default_world_guard
.first()
.expect("Default world should exist");
(default_world.clone(), Some(data))
}
} else {
// Player data exists but doesn't have a "Dimension" key.
log::warn!("Player data missing 'Dimension' key, defaulting to Overworld.");
let default_world_guard = self.worlds.read().await;
let default_world = default_world_guard
.first()
.expect("Default world should exist");
(default_world.clone(), Some(data))
}
} else {
// No player data found or an error occurred, default to the Overworld.
let default_world_guard = self.worlds.read().await;
let default_world = default_world_guard
.first()
.expect("Default world should exist");
(default_world.clone(), None)
};
let mut player = Player::new(client, world.clone(), gamemode).await; let mut player = Player::new(client, world.clone(), gamemode).await;
// Load player data if let Some(mut nbt_data) = nbt {
if let Err(e) = self player.read_nbt(&mut nbt_data).await;
.player_data_storage
.handle_player_join(&mut player)
.await
{
log::error!("Unexpected error loading player data: {e}");
} }
// Wrap in Arc after data is loaded // Wrap in Arc after data is loaded
@ -251,6 +345,13 @@ impl Server {
for world in self.worlds.read().await.iter() { for world in self.worlds.read().await.iter() {
world.shutdown().await; world.shutdown().await;
} }
// then lets save the world info
if let Err(err) = self
.world_info_writer
.write_world_info(&self.level_info, &BASIC_CONFIG.get_world_path())
{
log::error!("Failed to save level.dat: {err}");
}
log::info!("Completed worlds"); log::info!("Completed worlds");
} }

View File

@ -67,7 +67,6 @@ use pumpkin_registry::DimensionType;
use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3};
use pumpkin_util::math::{position::chunk_section_from_pos, vector2::Vector2}; use pumpkin_util::math::{position::chunk_section_from_pos, vector2::Vector2};
use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::text::{TextComponent, color::NamedColor};
use pumpkin_world::world::BlockFlags;
use pumpkin_world::{ use pumpkin_world::{
BlockStateId, GENERATION_SETTINGS, GeneratorSetting, biome, block::entities::BlockEntity, BlockStateId, GENERATION_SETTINGS, GeneratorSetting, biome, block::entities::BlockEntity,
level::SyncChunk, level::SyncChunk,
@ -78,6 +77,7 @@ use pumpkin_world::{
entity::entity_data_flags::{DATA_PLAYER_MAIN_HAND, DATA_PLAYER_MODE_CUSTOMISATION}, entity::entity_data_flags::{DATA_PLAYER_MAIN_HAND, DATA_PLAYER_MODE_CUSTOMISATION},
world::GetBlockError, world::GetBlockError,
}; };
use pumpkin_world::{world::BlockFlags, world_info::LevelData};
use rand::{Rng, thread_rng}; use rand::{Rng, thread_rng};
use scoreboard::Scoreboard; use scoreboard::Scoreboard;
use serde::Serialize; use serde::Serialize;
@ -122,6 +122,7 @@ impl PumpkinError for GetBlockError {
pub struct World { pub struct World {
/// The underlying level, responsible for chunk management and terrain generation. /// The underlying level, responsible for chunk management and terrain generation.
pub level: Arc<Level>, pub level: Arc<Level>,
pub level_info: Arc<LevelData>,
/// A map of active players within the world, keyed by their unique UUID. /// A map of active players within the world, keyed by their unique UUID.
pub players: Arc<RwLock<HashMap<uuid::Uuid, Arc<Player>>>>, pub players: Arc<RwLock<HashMap<uuid::Uuid, Arc<Player>>>>,
/// A map of active entities within the world, keyed by their unique UUID. /// A map of active entities within the world, keyed by their unique UUID.
@ -143,13 +144,13 @@ pub struct World {
synced_block_event_queue: Mutex<Vec<BlockEvent>>, synced_block_event_queue: Mutex<Vec<BlockEvent>>,
/// A map of unsent block changes, keyed by block position. /// A map of unsent block changes, keyed by block position.
unsent_block_changes: Mutex<HashMap<BlockPos, u16>>, unsent_block_changes: Mutex<HashMap<BlockPos, u16>>,
// TODO: entities
} }
impl World { impl World {
#[must_use] #[must_use]
pub fn load( pub fn load(
level: Level, level: Level,
level_info: Arc<LevelData>,
dimension_type: DimensionType, dimension_type: DimensionType,
block_registry: Arc<BlockRegistry>, block_registry: Arc<BlockRegistry>,
) -> Self { ) -> Self {
@ -159,6 +160,7 @@ impl World {
.unwrap(); .unwrap();
Self { Self {
level: Arc::new(level), level: Arc::new(level),
level_info,
players: Arc::new(RwLock::new(HashMap::new())), players: Arc::new(RwLock::new(HashMap::new())),
entities: Arc::new(RwLock::new(HashMap::new())), entities: Arc::new(RwLock::new(HashMap::new())),
scoreboard: Mutex::new(Scoreboard::new()), scoreboard: Mutex::new(Scoreboard::new()),
@ -559,7 +561,7 @@ impl World {
(position, yaw, pitch) (position, yaw, pitch)
} else { } else {
let info = &self.level.level_info; let info = &self.level_info;
let position = Vector3::new( let position = Vector3::new(
f64::from(info.spawn_x), f64::from(info.spawn_x),
f64::from(info.spawn_y) + 1.0, f64::from(info.spawn_y) + 1.0,
@ -890,7 +892,7 @@ impl World {
player.send_permission_lvl_update().await; player.send_permission_lvl_update().await;
// Teleport // Teleport
let info = &self.level.level_info; let info = &self.level_info;
let mut position = Vector3::new( let mut position = Vector3::new(
f64::from(info.spawn_x), f64::from(info.spawn_x),
f64::from(info.spawn_y), f64::from(info.spawn_y),

View File

@ -0,0 +1,45 @@
use std::sync::Arc;
use pumpkin_util::math::position::BlockPos;
use crate::entity::Entity;
use super::World;
pub mod nether;
pub trait Portal: Send + Sync {
fn get_delay(&self) -> u32 {
0
}
fn get_world(&self, entity: &Entity) -> Arc<World>;
}
pub struct PortalManager {
pub portal: Arc<dyn Portal>,
pub pos: BlockPos,
ticks_in_portal: u32,
pub in_portal: bool,
}
impl PortalManager {
pub fn new(portal: Arc<dyn Portal>, pos: BlockPos) -> Self {
Self {
portal,
pos,
ticks_in_portal: 0,
in_portal: true,
}
}
pub fn tick(&mut self) -> bool {
if self.in_portal {
self.in_portal = false;
self.ticks_in_portal += 1;
self.ticks_in_portal >= self.portal.get_delay()
} else {
self.ticks_in_portal -= 4;
false
}
}
}

View File

@ -6,9 +6,9 @@ use pumpkin_data::{
tag::Tagable, tag::Tagable,
}; };
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::BlockDirection; use pumpkin_world::{block::BlockDirection, world::BlockFlags};
use super::{BlockFlags, World}; use crate::world::World;
pub struct NetherPortal { pub struct NetherPortal {
axis: HorizontalAxis, axis: HorizontalAxis,