From 06c82c75f0cfb6607acc06e384cf4d892004a00b Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 15 May 2025 22:59:00 +0200 Subject: [PATCH] Multi Dimension refactor --- Cargo.lock | 20 +-- pumpkin-protocol/Cargo.toml | 2 +- pumpkin-registry/src/lib.rs | 10 ++ pumpkin-world/Cargo.toml | 2 +- pumpkin-world/benches/chunk_io.rs | 6 +- pumpkin-world/src/dimension.rs | 4 +- pumpkin-world/src/generation/surface/mod.rs | 2 +- pumpkin-world/src/generation/surface/rule.rs | 2 +- pumpkin-world/src/level.rs | 60 +------- pumpkin-world/src/lib.rs | 2 +- pumpkin-world/src/lock/anvil.rs | 6 +- pumpkin-world/src/lock/mod.rs | 6 +- pumpkin-world/src/world_info/anvil.rs | 43 +++--- pumpkin-world/src/world_info/mod.rs | 17 ++- pumpkin/Cargo.toml | 2 +- pumpkin/src/block/blocks/fire/fire.rs | 2 +- pumpkin/src/block/blocks/lily_pad.rs | 2 +- pumpkin/src/block/blocks/nether_portal.rs | 17 ++- pumpkin/src/data/mod.rs | 20 ++- pumpkin/src/data/player_server_data.rs | 30 +--- pumpkin/src/entity/hunger.rs | 2 +- pumpkin/src/entity/mob/mod.rs | 1 + pumpkin/src/entity/mod.rs | 66 ++++++++- pumpkin/src/entity/player.rs | 2 +- pumpkin/src/server/mod.rs | 129 ++++++++++++++++-- pumpkin/src/world/mod.rs | 10 +- pumpkin/src/world/portal/mod.rs | 45 ++++++ .../src/world/{portal.rs => portal/nether.rs} | 4 +- 28 files changed, 338 insertions(+), 176 deletions(-) create mode 100644 pumpkin/src/world/portal/mod.rs rename pumpkin/src/world/{portal.rs => portal/nether.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 42a6aa4dd..9c1edefa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,9 +194,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -510,7 +510,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "crossterm_winapi", "derive_more", "document-features", @@ -1844,7 +1844,7 @@ version = "0.1.0-dev+1.21.5" dependencies = [ "async-trait", "base64", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "chrono", "crossbeam", @@ -1968,7 +1968,7 @@ version = "0.1.0-dev+1.21.5" dependencies = [ "aes", "async-compression", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "cfb8", "flate2", @@ -2022,7 +2022,7 @@ name = "pumpkin-world" version = "0.1.0-dev+1.21.5" dependencies = [ "async-trait", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "criterion", "crossbeam", @@ -2214,7 +2214,7 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2368,7 +2368,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -2719,7 +2719,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -3625,7 +3625,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index e7d0b0c3e..124b9c0b7 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -32,7 +32,7 @@ cfb8 = "0.8" async-compression = { version = "0.4.23", features = ["tokio", "zlib"] } take_mut = "0.2.2" -bitflags = "2.9.0" +bitflags = "2.9.1" [dev-dependencies] # Validate correctness diff --git a/pumpkin-registry/src/lib.rs b/pumpkin-registry/src/lib.rs index 8b8fbc845..86f3bd420 100644 --- a/pumpkin-registry/src/lib.rs +++ b/pumpkin-registry/src/lib.rs @@ -95,6 +95,16 @@ impl DimensionType { Self::TheNether => Identifier::vanilla("the_nether"), } } + + pub fn from_name(name: &str) -> Option { + 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 { diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index c62885e37..1b02f7f9d 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -26,7 +26,7 @@ crossbeam.workspace = true num-derive = "0.4" sha2 = "0.10" -bitflags = "2.9.0" +bitflags = "2.9.1" dashmap = "6.1" diff --git a/pumpkin-world/benches/chunk_io.rs b/pumpkin-world/benches/chunk_io.rs index cd80bd87e..8ef1b2ec0 100644 --- a/pumpkin-world/benches/chunk_io.rs +++ b/pumpkin-world/benches/chunk_io.rs @@ -80,7 +80,7 @@ fn initialize_level( let (send, mut recv) = tokio::sync::mpsc::unbounded_channel(); // 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); let level_to_fetch = level_to_save.clone(); @@ -194,7 +194,7 @@ fn bench_chunk_io(c: &mut Criterion) { &chunks, |b, chunks| { 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 }) }, @@ -220,7 +220,7 @@ fn bench_chunk_io(c: &mut Criterion) { &positions, |b, positions| { 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 }) }, diff --git a/pumpkin-world/src/dimension.rs b/pumpkin-world/src/dimension.rs index 427b64119..51c60e619 100644 --- a/pumpkin-world/src/dimension.rs +++ b/pumpkin-world/src/dimension.rs @@ -13,12 +13,12 @@ pub enum 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 { Dimension::Overworld => {} Dimension::Nether => base_directory.push("DIM-1"), Dimension::End => base_directory.push("DIM1"), } - Level::from_root_folder(base_directory) + Level::from_root_folder(base_directory, seed) } } diff --git a/pumpkin-world/src/generation/surface/mod.rs b/pumpkin-world/src/generation/surface/mod.rs index 717208321..e22f8f5a2 100644 --- a/pumpkin-world/src/generation/surface/mod.rs +++ b/pumpkin-world/src/generation/surface/mod.rs @@ -137,7 +137,7 @@ pub enum MaterialCondition { #[serde(rename = "minecraft:noise_threshold")] NoiseThreshold(NoiseThresholdMaterialCondition), #[serde(rename = "minecraft:vertical_gradient")] - VerticalGradient(VerticalGradientMaterialCondition), + VerticalGradient(Box), #[serde(rename = "minecraft:y_above")] YAbove(AboveYMaterialCondition), #[serde(rename = "minecraft:water")] diff --git a/pumpkin-world/src/generation/surface/rule.rs b/pumpkin-world/src/generation/surface/rule.rs index 4d9812ec7..f563dc033 100644 --- a/pumpkin-world/src/generation/surface/rule.rs +++ b/pumpkin-world/src/generation/surface/rule.rs @@ -18,7 +18,7 @@ pub enum MaterialRule { #[serde(rename = "minecraft:sequence")] Sequence(SequenceMaterialRule), #[serde(rename = "minecraft:condition")] - Condition(ConditionMaterialRule), + Condition(Box), } impl MaterialRule { diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 8329dd82a..8a9f32418 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,5 +1,4 @@ use std::{ - fs, path::PathBuf, sync::{ Arc, @@ -25,12 +24,7 @@ use crate::{ io::{ChunkIO, LoadedData, chunk_file_manager::ChunkFileManager}, }, generation::{Seed, WorldGenerator, get_world_gen}, - lock::{LevelLocker, anvil::AnvilLevelLocker}, world::SimpleWorld, - world_info::{ - LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter, - anvil::{AnvilLevelInfo, LEVEL_DAT_BACKUP_FILE_NAME, LEVEL_DAT_FILE_NAME}, - }, }; pub type SyncChunk = Arc>; @@ -46,8 +40,6 @@ pub type SyncChunk = Arc>; /// For more details on world generation, refer to the `WorldGenerator` module. pub struct Level { pub seed: Seed, - pub level_info: LevelData, - world_info_writer: Arc, level_folder: LevelFolder, // Holds this level's spawn chunks, which are always loaded @@ -60,9 +52,7 @@ pub struct Level { chunk_saver: Arc>, world_gen: Arc, - // Gets unlocked when dropped - // TODO: Make this a trait - _locker: Arc, + block_ticks: Arc>>, fluid_ticks: Arc>>, /// Tracks tasks associated with this world instance @@ -78,7 +68,7 @@ pub struct LevelFolder { } 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 let region_folder = root_folder.join("region"); if !region_folder.exists() { @@ -89,40 +79,11 @@ impl Level { 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 - 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: {}", - level_info.world_gen_settings.seed - ); + log::info!("Loading world with seed: {}", 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 chunk_saver: Arc> = match advanced_config().chunk.format { @@ -134,14 +95,11 @@ impl Level { Self { seed, world_gen, - world_info_writer: Arc::new(AnvilLevelInfo), level_folder, chunk_saver, spawn_chunks: Arc::new(DashMap::new()), loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), - level_info, - _locker: Arc::new(locker), tasks: TaskTracker::new(), shutdown_notifier: Notify::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 self.chunk_saver.clear_watched_chunks().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 { diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index c3f259693..d5b1a8e42 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -12,7 +12,7 @@ mod generation; pub mod inventory; pub mod item; pub mod level; -mod lock; +pub mod lock; pub mod world; pub mod world_info; diff --git a/pumpkin-world/src/lock/anvil.rs b/pumpkin-world/src/lock/anvil.rs index 7864cf711..fe046f546 100644 --- a/pumpkin-world/src/lock/anvil.rs +++ b/pumpkin-world/src/lock/anvil.rs @@ -2,7 +2,7 @@ use file_guard::{FileGuard, Lock}; 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 { _lock: Option>>, @@ -13,8 +13,8 @@ const SESSION_LOCK_FILE_NAME: &str = "session.lock"; const SNOWMAN: &[u8] = "☃".as_bytes(); impl LevelLocker for AnvilLevelLocker { - fn look(folder: &crate::level::LevelFolder) -> Result { - let file_path = folder.root_folder.join(SESSION_LOCK_FILE_NAME); + fn look(folder: &Path) -> Result { + let file_path = folder.join(SESSION_LOCK_FILE_NAME); let mut file = File::options() .create(true) .truncate(false) diff --git a/pumpkin-world/src/lock/mod.rs b/pumpkin-world/src/lock/mod.rs index bf4b262f2..d6bb715af 100644 --- a/pumpkin-world/src/lock/mod.rs +++ b/pumpkin-world/src/lock/mod.rs @@ -1,12 +1,12 @@ -use thiserror::Error; +use std::path::Path; -use crate::level::LevelFolder; +use thiserror::Error; pub mod anvil; // Gets unlocked when dropped pub trait LevelLocker: Send + Sync { - fn look(folder: &LevelFolder) -> Result; + fn look(folder: &Path) -> Result; } #[derive(Error, Debug)] diff --git a/pumpkin-world/src/world_info/anvil.rs b/pumpkin-world/src/world_info/anvil.rs index 61830343a..ce541499c 100644 --- a/pumpkin-world/src/world_info/anvil.rs +++ b/pumpkin-world/src/world_info/anvil.rs @@ -1,15 +1,15 @@ use std::{ fs::OpenOptions, io::Read, + path::Path, time::{SystemTime, UNIX_EPOCH}, }; use flate2::{Compression, read::GzDecoder, write::GzEncoder}; use serde::{Deserialize, Serialize}; -use crate::{ - level::LevelFolder, - world_info::{MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION}, +use crate::world_info::{ + MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION, }; use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter}; @@ -54,8 +54,8 @@ fn check_file_data_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> { } impl WorldInfoReader for AnvilLevelInfo { - fn read_world_info(&self, level_folder: &LevelFolder) -> Result { - let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME); + fn read_world_info(&self, level_folder: &Path) -> Result { + let path = level_folder.join(LEVEL_DAT_FILE_NAME); let world_info_file = OpenOptions::new().read(true).open(path)?; let mut compression_reader = GzDecoder::new(world_info_file); @@ -75,19 +75,21 @@ impl WorldInfoReader for AnvilLevelInfo { impl WorldInfoWriter for AnvilLevelInfo { fn write_world_info( &self, - info: LevelData, - level_folder: &LevelFolder, + info: &LevelData, + level_folder: &Path, ) -> Result<(), WorldInfoError> { let start = SystemTime::now(); let since_the_epoch = start .duration_since(UNIX_EPOCH) .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; - let level = LevelDat { data: level_data }; + let level = LevelDat { + data: level_data.clone(), + }; // 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() .truncate(true) .create(true) @@ -122,7 +124,6 @@ mod test { use crate::{ global_path, - level::LevelFolder, world_info::{DataPacks, LevelData, WorldGenSettings, WorldInfoError, WorldVersion}, }; @@ -136,16 +137,12 @@ mod test { data.world_gen_settings.seed = seed; 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 - .write_world_info(data, &level_folder) + .write_world_info(&data, temp_dir.path()) .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); } @@ -219,15 +216,15 @@ mod test { #[test] fn failed_deserialize_old_level_dat() { 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"); - 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 { Ok(_) => panic!("This should fail!"), Err(WorldInfoError::UnsupportedVersion(_)) => {} diff --git a/pumpkin-world/src/world_info/mod.rs b/pumpkin-world/src/world_info/mod.rs index 4c7df5b37..4641ca69d 100644 --- a/pumpkin-world/src/world_info/mod.rs +++ b/pumpkin-world/src/world_info/mod.rs @@ -1,9 +1,11 @@ +use std::path::Path; + use pumpkin_config::BASIC_CONFIG; use pumpkin_util::{Difficulty, serde_enum_as_integer}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{generation::Seed, level::LevelFolder}; +use crate::generation::Seed; pub mod anvil; @@ -11,16 +13,13 @@ pub mod anvil; 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(crate) trait WorldInfoReader { - fn read_world_info(&self, level_folder: &LevelFolder) -> Result; +pub trait WorldInfoReader { + fn read_world_info(&self, level_folder: &Path) -> Result; } -pub(crate) trait WorldInfoWriter: Sync + Send { - fn write_world_info( - &self, - info: LevelData, - level_folder: &LevelFolder, - ) -> Result<(), WorldInfoError>; +pub trait WorldInfoWriter: Sync + Send { + fn write_world_info(&self, info: &LevelData, level_folder: &Path) + -> Result<(), WorldInfoError>; } #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 226b84130..b3a1a794f 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -82,7 +82,7 @@ chrono = { version = "0.4", features = ["serde"] } # plugins libloading = "0.8" -bitflags = "2.9.0" +bitflags = "2.9.1" rustc-hash = "2.1.1" # Task handling diff --git a/pumpkin/src/block/blocks/fire/fire.rs b/pumpkin/src/block/blocks/fire/fire.rs index eb213797b..302810342 100644 --- a/pumpkin/src/block/blocks/fire/fire.rs +++ b/pumpkin/src/block/blocks/fire/fire.rs @@ -16,7 +16,7 @@ use crate::block::pumpkin_block::PumpkinBlock; use crate::entity::player::Player; use crate::server::Server; use crate::world::World; -use crate::world::portal::NetherPortal; +use crate::world::portal::nether::NetherPortal; use super::FireBlockBase; diff --git a/pumpkin/src/block/blocks/lily_pad.rs b/pumpkin/src/block/blocks/lily_pad.rs index 045912f6b..9070a6886 100644 --- a/pumpkin/src/block/blocks/lily_pad.rs +++ b/pumpkin/src/block/blocks/lily_pad.rs @@ -21,7 +21,7 @@ impl PumpkinBlock for LilyPadBlock { _block: Block, _state: BlockState, ) { - // Proberbly not the best soultion, but works + // Proberbly not the best solution, but works if entity .get_entity() .entity_type diff --git a/pumpkin/src/block/blocks/nether_portal.rs b/pumpkin/src/block/blocks/nether_portal.rs index 3d08f53f4..6904943c1 100644 --- a/pumpkin/src/block/blocks/nether_portal.rs +++ b/pumpkin/src/block/blocks/nether_portal.rs @@ -1,9 +1,12 @@ +use std::sync::Arc; + use crate::block::pumpkin_block::PumpkinBlock; +use crate::entity::EntityBase; use crate::world::World; -use crate::world::portal::NetherPortal; +use crate::world::portal::nether::NetherPortal; use async_trait::async_trait; -use pumpkin_data::Block; use pumpkin_data::block_properties::{Axis, BlockProperties, NetherPortalLikeProperties}; +use pumpkin_data::{Block, BlockState}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::BlockStateId; @@ -38,4 +41,14 @@ impl PumpkinBlock for NetherPortalBlock { } Block::AIR.default_state_id } + + async fn on_entity_collision( + &self, + _world: &Arc, + _entity: &dyn EntityBase, + _pos: BlockPos, + _block: Block, + _state: BlockState, + ) { + } } diff --git a/pumpkin/src/data/mod.rs b/pumpkin/src/data/mod.rs index 745cb9eaf..0e22de06b 100644 --- a/pumpkin/src/data/mod.rs +++ b/pumpkin/src/data/mod.rs @@ -27,12 +27,13 @@ pub trait LoadJSONConfiguration { let path = data_dir.join(Self::get_path()); let config = if path.exists() { - let file_content = fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Couldn't read configuration file at {path:?}")); + let file_content = fs::read_to_string(&path).unwrap_or_else(|_| { + panic!("Couldn't read configuration file at {}", path.display()) + }); serde_json::from_str(&file_content).unwrap_or_else(|err| { 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 { @@ -40,7 +41,8 @@ pub trait LoadJSONConfiguration { if let Err(err) = fs::write(&path, serde_json::to_string_pretty(&content).unwrap()) { 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) { Ok(content) => content, 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; } }; 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() + ); } } } diff --git a/pumpkin/src/data/player_server_data.rs b/pumpkin/src/data/player_server_data.rs index b543af52a..e7f137fae 100644 --- a/pumpkin/src/data/player_server_data.rs +++ b/pumpkin/src/data/player_server_data.rs @@ -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. /// /// This function saves player data when they disconnect. @@ -141,19 +126,14 @@ impl ServerPlayerData { /// # Returns /// /// A Result indicating success or the error that occurred. - pub async fn load_and_apply_data_to_player( - &self, - player: &mut Player, - ) -> Result<(), PlayerDataError> { - let uuid = &player.gameprofile.id; + pub fn load_data(&self, uuid: &uuid::Uuid) -> Result, PlayerDataError> { match self.storage.load_player_data(uuid) { - Ok((should_load, mut data)) => { + Ok((should_load, data)) => { if !should_load { // No data to load, continue with default data - return Ok(()); + return Ok(None); } - player.read_nbt(&mut data).await; - Ok(()) + Ok(Some(data)) } Err(e) => { if self.storage.is_save_enabled() { @@ -164,7 +144,7 @@ impl ServerPlayerData { log::debug!("Not loading player data for {uuid} (saving disabled)"); } // Continue with default data even if there's an error - Ok(()) + Ok(None) } } } diff --git a/pumpkin/src/entity/hunger.rs b/pumpkin/src/entity/hunger.rs index 02ca41815..9d9b75e07 100644 --- a/pumpkin/src/entity/hunger.rs +++ b/pumpkin/src/entity/hunger.rs @@ -32,7 +32,7 @@ impl HungerManager { let level = self.level.load(); let exhaustion = self.exhaustion.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 if level != 0 && exhaustion > 4.0 { self.exhaustion.store(exhaustion - 4.0); diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 5ed3026f9..b6b1ddf40 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -62,6 +62,7 @@ pub async fn from_type( goals: Mutex::new(vec![]), navigator: Mutex::new(Navigator::default()), }; + #[expect(clippy::single_match)] match entity_type { EntityType::ZOMBIE => Zombie::make(&mob).await, // TODO diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 4ab3f29af..06c28a828 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -1,4 +1,7 @@ -use crate::server::Server; +use crate::{ + server::Server, + world::portal::{Portal, PortalManager}, +}; use async_trait::async_trait; use bytes::BufMut; use core::f32; @@ -32,11 +35,11 @@ use serde::Serialize; use std::sync::{ Arc, atomic::{ - AtomicBool, AtomicI32, - Ordering::{Relaxed, SeqCst}, + AtomicBool, AtomicI32, AtomicU32, + Ordering::{self, Relaxed, SeqCst}, }, }; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use crate::world::World; @@ -135,6 +138,11 @@ pub struct Entity { pub invulnerable: AtomicBool, /// List of damage types this entity is immune to pub damage_immunities: Vec, + + pub portal_cooldown: AtomicU32, + + // :c + pub portal_manager: Mutex>>, } impl Entity { @@ -181,6 +189,8 @@ impl Entity { bounding_box_size: AtomicCell::new(bounding_box_size), invulnerable: AtomicBool::new(invulnerable), 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); } + 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, 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 pub fn set_rotation(&self, yaw: f32, pitch: f32) { // TODO @@ -527,6 +584,7 @@ impl EntityBase for Entity { } async fn tick(&self, _caller: &dyn EntityBase, _: &Server) { + self.tick_portal().await; // TODO: Tick } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index ce0e71304..92fbc1d0c 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -886,7 +886,7 @@ impl Player { pitch: Option, ) { 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 { pos } else { diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 99484cc0d..6e35f03e8 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -2,6 +2,7 @@ use crate::block::registry::BlockRegistry; use crate::command::commands::default_dispatcher; use crate::command::commands::defaultgamemode::DefaultGamemode; use crate::data::player_server_data::ServerPlayerData; +use crate::entity::NBTStorage; use crate::item::registry::ItemRegistry; use crate::net::EncryptionError; 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::text::TextComponent; 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 rsa::RsaPublicKey; +use std::fs; use std::net::IpAddr; use std::sync::atomic::{AtomicBool, AtomicU32}; use std::{ @@ -57,7 +65,7 @@ pub struct Server { pub item_registry: Arc, /// Manages multiple worlds within the server. pub worlds: RwLock>>, - // All the dimensions that exist on the server. + /// All the dimensions that exist on the server. pub dimensions: Vec, /// Caches game registries for efficient access. pub cached_registry: Vec, @@ -77,6 +85,13 @@ pub struct Server { // Whether the server whitelist is on or off pub white_list: AtomicBool, tasks: TaskTracker, + + // world stuff which maybe should be put into a struct + pub level_info: Arc, + world_info_writer: Arc, + // Gets unlocked when dropped + // TODO: Make this a trait + _locker: Arc, } impl Server { @@ -101,18 +116,58 @@ impl Server { let block_registry = super::block::default_registry(); - let world = World::load( - Dimension::Overworld.into_level(world_path.clone()), + let level_info = AnvilLevelInfo.read_world_info(&world_path); + 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, 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(); Self { cached_registry: Registry::get_synced(), 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![ DimensionType::Overworld, DimensionType::OverworldCaves, @@ -137,6 +192,9 @@ impl Server { white_list: AtomicBool::new(BASIC_CONFIG.white_list), tasks: TaskTracker::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) } + pub async fn get_world_from_dimension(&self, dimension: DimensionType) -> Arc { + // 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)] /// 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. pub async fn add_player(&self, client: Client) -> Option<(Arc, Arc)> { let gamemode = self.defaultgamemode.lock().await.gamemode; - // Basically the default world - // TODO: select default from config - let world = &self.worlds.read().await[0]; + let uuid = client.gameprofile.lock().await.as_ref().unwrap().id; + + 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; - // Load player data - if let Err(e) = self - .player_data_storage - .handle_player_join(&mut player) - .await - { - log::error!("Unexpected error loading player data: {e}"); + if let Some(mut nbt_data) = nbt { + player.read_nbt(&mut nbt_data).await; } // Wrap in Arc after data is loaded @@ -251,6 +345,13 @@ impl Server { for world in self.worlds.read().await.iter() { 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"); } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index f4040f532..1c2159eb0 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -67,7 +67,6 @@ use pumpkin_registry::DimensionType; use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use pumpkin_util::math::{position::chunk_section_from_pos, vector2::Vector2}; use pumpkin_util::text::{TextComponent, color::NamedColor}; -use pumpkin_world::world::BlockFlags; use pumpkin_world::{ BlockStateId, GENERATION_SETTINGS, GeneratorSetting, biome, block::entities::BlockEntity, level::SyncChunk, @@ -78,6 +77,7 @@ use pumpkin_world::{ entity::entity_data_flags::{DATA_PLAYER_MAIN_HAND, DATA_PLAYER_MODE_CUSTOMISATION}, world::GetBlockError, }; +use pumpkin_world::{world::BlockFlags, world_info::LevelData}; use rand::{Rng, thread_rng}; use scoreboard::Scoreboard; use serde::Serialize; @@ -122,6 +122,7 @@ impl PumpkinError for GetBlockError { pub struct World { /// The underlying level, responsible for chunk management and terrain generation. pub level: Arc, + pub level_info: Arc, /// A map of active players within the world, keyed by their unique UUID. pub players: Arc>>>, /// 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>, /// A map of unsent block changes, keyed by block position. unsent_block_changes: Mutex>, - // TODO: entities } impl World { #[must_use] pub fn load( level: Level, + level_info: Arc, dimension_type: DimensionType, block_registry: Arc, ) -> Self { @@ -159,6 +160,7 @@ impl World { .unwrap(); Self { level: Arc::new(level), + level_info, players: Arc::new(RwLock::new(HashMap::new())), entities: Arc::new(RwLock::new(HashMap::new())), scoreboard: Mutex::new(Scoreboard::new()), @@ -559,7 +561,7 @@ impl World { (position, yaw, pitch) } else { - let info = &self.level.level_info; + let info = &self.level_info; let position = Vector3::new( f64::from(info.spawn_x), f64::from(info.spawn_y) + 1.0, @@ -890,7 +892,7 @@ impl World { player.send_permission_lvl_update().await; // Teleport - let info = &self.level.level_info; + let info = &self.level_info; let mut position = Vector3::new( f64::from(info.spawn_x), f64::from(info.spawn_y), diff --git a/pumpkin/src/world/portal/mod.rs b/pumpkin/src/world/portal/mod.rs new file mode 100644 index 000000000..13e98538c --- /dev/null +++ b/pumpkin/src/world/portal/mod.rs @@ -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; +} + +pub struct PortalManager { + pub portal: Arc, + pub pos: BlockPos, + ticks_in_portal: u32, + pub in_portal: bool, +} + +impl PortalManager { + pub fn new(portal: Arc, 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 + } + } +} diff --git a/pumpkin/src/world/portal.rs b/pumpkin/src/world/portal/nether.rs similarity index 98% rename from pumpkin/src/world/portal.rs rename to pumpkin/src/world/portal/nether.rs index 349c9d409..fdd3e4ab6 100644 --- a/pumpkin/src/world/portal.rs +++ b/pumpkin/src/world/portal/nether.rs @@ -6,9 +6,9 @@ use pumpkin_data::{ tag::Tagable, }; 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 { axis: HorizontalAxis,