Multi Dimension refactor
This commit is contained in:
parent
30f1a88126
commit
06c82c75f0
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -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]]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
},
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -18,7 +18,7 @@ pub enum MaterialRule {
|
||||
#[serde(rename = "minecraft:sequence")]
|
||||
Sequence(SequenceMaterialRule),
|
||||
#[serde(rename = "minecraft:condition")]
|
||||
Condition(ConditionMaterialRule),
|
||||
Condition(Box<ConditionMaterialRule>),
|
||||
}
|
||||
|
||||
impl MaterialRule {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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(_)) => {}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
45
pumpkin/src/world/portal/mod.rs
Normal file
45
pumpkin/src/world/portal/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
Loading…
Reference in New Issue
Block a user