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]]
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]]

View File

@ -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

View File

@ -95,6 +95,16 @@ impl DimensionType {
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 {

View File

@ -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"

View File

@ -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
})
},

View File

@ -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)
}
}

View File

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

View File

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

View File

@ -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<RwLock<ChunkData>>;
@ -46,8 +40,6 @@ pub type SyncChunk = Arc<RwLock<ChunkData>>;
/// 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<dyn WorldInfoWriter>,
level_folder: LevelFolder,
// Holds this level's spawn chunks, which are always loaded
@ -60,9 +52,7 @@ pub struct Level {
chunk_saver: Arc<dyn ChunkIO<Data = SyncChunk>>,
world_gen: Arc<dyn WorldGenerator>,
// Gets unlocked when dropped
// TODO: Make this a trait
_locker: Arc<AnvilLevelLocker>,
block_ticks: Arc<Mutex<Vec<ScheduledTick>>>,
fluid_ticks: Arc<Mutex<Vec<ScheduledTick>>>,
/// 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<dyn ChunkIO<Data = SyncChunk>> = 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 {

View File

@ -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;

View File

@ -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<FileGuard<Arc<File>>>,
@ -13,8 +13,8 @@ const SESSION_LOCK_FILE_NAME: &str = "session.lock";
const SNOWMAN: &[u8] = "☃".as_bytes();
impl LevelLocker<Self> for AnvilLevelLocker {
fn look(folder: &crate::level::LevelFolder) -> Result<Self, LockError> {
let file_path = folder.root_folder.join(SESSION_LOCK_FILE_NAME);
fn look(folder: &Path) -> Result<Self, LockError> {
let file_path = folder.join(SESSION_LOCK_FILE_NAME);
let mut file = File::options()
.create(true)
.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;
// Gets unlocked when dropped
pub trait LevelLocker<T>: Send + Sync {
fn look(folder: &LevelFolder) -> Result<T, LockError>;
fn look(folder: &Path) -> Result<T, LockError>;
}
#[derive(Error, Debug)]

View File

@ -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<LevelData, WorldInfoError> {
let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError> {
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(_)) => {}

View File

@ -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<LevelData, WorldInfoError>;
pub trait WorldInfoReader {
fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError>;
}
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)]

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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<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 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()
);
}
}
}

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.
///
/// 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<Option<NbtCompound>, 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)
}
}
}

View File

@ -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);

View File

@ -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

View File

@ -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<DamageType>,
pub portal_cooldown: AtomicU32,
// :c
pub portal_manager: Mutex<Option<Mutex<PortalManager>>>,
}
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<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
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
}

View File

@ -886,7 +886,7 @@ impl Player {
pitch: Option<f32>,
) {
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 {

View File

@ -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<ItemRegistry>,
/// Manages multiple worlds within the server.
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>,
/// Caches game registries for efficient access.
pub cached_registry: Vec<Registry>,
@ -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<LevelData>,
world_info_writer: Arc<dyn WorldInfoWriter>,
// Gets unlocked when dropped
// TODO: Make this a trait
_locker: Arc<AnvilLevelLocker>,
}
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<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)]
/// 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<Player>, Arc<World>)> {
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");
}

View File

@ -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<Level>,
pub level_info: Arc<LevelData>,
/// A map of active players within the world, keyed by their unique UUID.
pub players: Arc<RwLock<HashMap<uuid::Uuid, Arc<Player>>>>,
/// 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>>,
/// A map of unsent block changes, keyed by block position.
unsent_block_changes: Mutex<HashMap<BlockPos, u16>>,
// TODO: entities
}
impl World {
#[must_use]
pub fn load(
level: Level,
level_info: Arc<LevelData>,
dimension_type: DimensionType,
block_registry: Arc<BlockRegistry>,
) -> 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),

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,
};
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,