Save level.dat
This commit is contained in:
parent
cc83e090f9
commit
fcf85df518
@ -7,7 +7,7 @@ pub use gamemode::GameMode;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(PartialEq, Serialize, Deserialize)]
|
#[derive(PartialEq, Serialize, Deserialize, Clone)]
|
||||||
pub enum Difficulty {
|
pub enum Difficulty {
|
||||||
Peaceful,
|
Peaceful,
|
||||||
Easy,
|
Easy,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
pumpkin-nbt = { path = "../pumpkin-nbt" }
|
||||||
pumpkin-core = { path = "../pumpkin-core" }
|
pumpkin-core = { path = "../pumpkin-core" }
|
||||||
pumpkin-config = { path = "../pumpkin-config" }
|
pumpkin-config = { path = "../pumpkin-config" }
|
||||||
pumpkin-macros = { path = "../pumpkin-macros" }
|
pumpkin-macros = { path = "../pumpkin-macros" }
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
||||||
|
|
||||||
use crate::level::SaveFile;
|
use crate::level::LevelFolder;
|
||||||
|
|
||||||
use super::{ChunkData, ChunkReader, ChunkReadingError, CompressionError};
|
use super::{ChunkData, ChunkReader, ChunkReadingError, CompressionError};
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ impl Compression {
|
|||||||
impl ChunkReader for AnvilChunkReader {
|
impl ChunkReader for AnvilChunkReader {
|
||||||
fn read_chunk(
|
fn read_chunk(
|
||||||
&self,
|
&self,
|
||||||
save_file: &SaveFile,
|
save_file: &LevelFolder,
|
||||||
at: &pumpkin_core::math::vector2::Vector2<i32>,
|
at: &pumpkin_core::math::vector2::Vector2<i32>,
|
||||||
) -> Result<super::ChunkData, ChunkReadingError> {
|
) -> Result<super::ChunkData, ChunkReadingError> {
|
||||||
let region = (at.x >> 5, at.z >> 5);
|
let region = (at.x >> 5, at.z >> 5);
|
||||||
@ -168,14 +168,14 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chunk::{anvil::AnvilChunkReader, ChunkReader, ChunkReadingError},
|
chunk::{anvil::AnvilChunkReader, ChunkReader, ChunkReadingError},
|
||||||
level::SaveFile,
|
level::LevelFolder,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn not_existing() {
|
fn not_existing() {
|
||||||
let region_path = PathBuf::from("not_existing");
|
let region_path = PathBuf::from("not_existing");
|
||||||
let result = AnvilChunkReader::new().read_chunk(
|
let result = AnvilChunkReader::new().read_chunk(
|
||||||
&SaveFile {
|
&LevelFolder {
|
||||||
root_folder: PathBuf::from(""),
|
root_folder: PathBuf::from(""),
|
||||||
region_folder: region_path,
|
region_folder: region_path,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use thiserror::Error;
|
|||||||
use crate::{
|
use crate::{
|
||||||
block::BlockState,
|
block::BlockState,
|
||||||
coordinates::{ChunkRelativeBlockCoordinates, Height},
|
coordinates::{ChunkRelativeBlockCoordinates, Height},
|
||||||
level::SaveFile,
|
level::LevelFolder,
|
||||||
WORLD_HEIGHT,
|
WORLD_HEIGHT,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT;
|
|||||||
pub trait ChunkReader: Sync + Send {
|
pub trait ChunkReader: Sync + Send {
|
||||||
fn read_chunk(
|
fn read_chunk(
|
||||||
&self,
|
&self,
|
||||||
save_file: &SaveFile,
|
save_file: &LevelFolder,
|
||||||
at: &Vector2<i32>,
|
at: &Vector2<i32>,
|
||||||
) -> Result<ChunkData, ChunkReadingError>;
|
) -> Result<ChunkData, ChunkReadingError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ use std::{path::PathBuf, sync::Arc};
|
|||||||
|
|
||||||
use dashmap::{DashMap, Entry};
|
use dashmap::{DashMap, Entry};
|
||||||
use num_traits::Zero;
|
use num_traits::Zero;
|
||||||
use pumpkin_config::BASIC_CONFIG;
|
|
||||||
use pumpkin_core::math::vector2::Vector2;
|
use pumpkin_core::math::vector2::Vector2;
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@ -15,7 +14,7 @@ use crate::{
|
|||||||
anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError,
|
anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError,
|
||||||
},
|
},
|
||||||
generation::{get_world_gen, Seed, WorldGenerator},
|
generation::{get_world_gen, Seed, WorldGenerator},
|
||||||
world_info::{anvil::AnvilInfoReader, WorldInfo, WorldInfoReader},
|
world_info::{anvil::AnvilLevelInfo, LevelData, WorldInfoReader, WorldInfoWriter},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
|
/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
|
||||||
@ -29,8 +28,9 @@ use crate::{
|
|||||||
/// For more details on world generation, refer to the `WorldGenerator` module.
|
/// For more details on world generation, refer to the `WorldGenerator` module.
|
||||||
pub struct Level {
|
pub struct Level {
|
||||||
pub seed: Seed,
|
pub seed: Seed,
|
||||||
pub world_info: Option<WorldInfo>,
|
pub level_info: LevelData,
|
||||||
save_file: Option<SaveFile>,
|
world_info_writer: Arc<dyn WorldInfoWriter>,
|
||||||
|
level_folder: LevelFolder,
|
||||||
loaded_chunks: Arc<DashMap<Vector2<i32>, Arc<RwLock<ChunkData>>>>,
|
loaded_chunks: Arc<DashMap<Vector2<i32>, Arc<RwLock<ChunkData>>>>,
|
||||||
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
|
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
|
||||||
chunk_reader: Arc<dyn ChunkReader>,
|
chunk_reader: Arc<dyn ChunkReader>,
|
||||||
@ -38,60 +38,53 @@ pub struct Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SaveFile {
|
pub struct LevelFolder {
|
||||||
pub root_folder: PathBuf,
|
pub root_folder: PathBuf,
|
||||||
pub region_folder: PathBuf,
|
pub region_folder: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_create_seed() -> Seed {
|
|
||||||
// TODO: if there is a seed in the config (!= 0) use it. Otherwise make a random one
|
|
||||||
Seed::from(BASIC_CONFIG.seed.as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Level {
|
impl Level {
|
||||||
pub fn from_root_folder(root_folder: PathBuf) -> Self {
|
pub fn from_root_folder(root_folder: PathBuf) -> Self {
|
||||||
// If we are using an already existing world we want to read the seed from the level.dat, If not we want to check if there is a seed in the config, if not lets create a random one
|
// If we are using an already existing world we want to read the seed from the level.dat, If not we want to check if there is a seed in the config, if not lets create a random one
|
||||||
if root_folder.exists() {
|
let region_folder = root_folder.join("region");
|
||||||
let region_folder = root_folder.join("region");
|
if !region_folder.exists() {
|
||||||
assert!(
|
std::fs::create_dir_all(®ion_folder).expect("Failed to create Region folder");
|
||||||
region_folder.exists(),
|
|
||||||
"World region folder does not exist, despite there being a root folder."
|
|
||||||
);
|
|
||||||
let save_file = SaveFile {
|
|
||||||
root_folder,
|
|
||||||
region_folder,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Load info correctly based on world format type
|
|
||||||
let world_info_reader = AnvilInfoReader::new();
|
|
||||||
let info = world_info_reader
|
|
||||||
.read_world_info(&save_file)
|
|
||||||
.expect("Unable to get world info!"); // TODO: Improve error handling
|
|
||||||
let seed = Seed(info.seed as u64);
|
|
||||||
let world_gen = get_world_gen(seed).into(); // TODO Read Seed from config.
|
|
||||||
|
|
||||||
Self {
|
|
||||||
seed,
|
|
||||||
world_gen,
|
|
||||||
save_file: Some(save_file),
|
|
||||||
chunk_reader: Arc::new(AnvilChunkReader::new()),
|
|
||||||
loaded_chunks: Arc::new(DashMap::new()),
|
|
||||||
chunk_watchers: Arc::new(DashMap::new()),
|
|
||||||
world_info: Some(info),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let seed = get_or_create_seed();
|
|
||||||
let world_gen = get_world_gen(seed).into();
|
|
||||||
Self {
|
|
||||||
seed,
|
|
||||||
world_gen,
|
|
||||||
save_file: None,
|
|
||||||
chunk_reader: Arc::new(AnvilChunkReader::new()),
|
|
||||||
loaded_chunks: Arc::new(DashMap::new()),
|
|
||||||
chunk_watchers: Arc::new(DashMap::new()),
|
|
||||||
world_info: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let level_folder = LevelFolder {
|
||||||
|
root_folder,
|
||||||
|
region_folder,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Load info correctly based on world format type
|
||||||
|
let level_info = AnvilLevelInfo
|
||||||
|
.read_world_info(&level_folder)
|
||||||
|
.unwrap_or_default(); // TODO: Improve error handling
|
||||||
|
let seed = Seed(level_info.world_gen_settings.seed as u64);
|
||||||
|
let world_gen = get_world_gen(seed).into();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
seed,
|
||||||
|
world_gen,
|
||||||
|
world_info_writer: Arc::new(AnvilLevelInfo),
|
||||||
|
level_folder,
|
||||||
|
chunk_reader: Arc::new(AnvilChunkReader::new()),
|
||||||
|
loaded_chunks: Arc::new(DashMap::new()),
|
||||||
|
chunk_watchers: Arc::new(DashMap::new()),
|
||||||
|
level_info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self) {
|
||||||
|
log::info!("Saving level...");
|
||||||
|
// lets first save all chunks
|
||||||
|
for chunk in self.loaded_chunks.iter() {
|
||||||
|
let chunk = chunk.read().await;
|
||||||
|
self.clean_chunk(&chunk.position);
|
||||||
|
}
|
||||||
|
// then lets save the world info
|
||||||
|
self.world_info_writer
|
||||||
|
.write_world_info(self.level_info.clone(), &self.level_folder)
|
||||||
|
.expect("Failed to save world info");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_block() {}
|
pub fn get_block() {}
|
||||||
@ -205,10 +198,10 @@ impl Level {
|
|||||||
|
|
||||||
fn load_chunk_from_save(
|
fn load_chunk_from_save(
|
||||||
chunk_reader: Arc<dyn ChunkReader>,
|
chunk_reader: Arc<dyn ChunkReader>,
|
||||||
save_file: SaveFile,
|
save_file: &LevelFolder,
|
||||||
chunk_pos: Vector2<i32>,
|
chunk_pos: Vector2<i32>,
|
||||||
) -> Result<Option<Arc<RwLock<ChunkData>>>, ChunkReadingError> {
|
) -> Result<Option<Arc<RwLock<ChunkData>>>, ChunkReadingError> {
|
||||||
match chunk_reader.read_chunk(&save_file, &chunk_pos) {
|
match chunk_reader.read_chunk(save_file, &chunk_pos) {
|
||||||
Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))),
|
Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))),
|
||||||
Err(
|
Err(
|
||||||
ChunkReadingError::ChunkNotExist
|
ChunkReadingError::ChunkNotExist
|
||||||
@ -233,7 +226,7 @@ impl Level {
|
|||||||
let channel = channel.clone();
|
let channel = channel.clone();
|
||||||
let loaded_chunks = self.loaded_chunks.clone();
|
let loaded_chunks = self.loaded_chunks.clone();
|
||||||
let chunk_reader = self.chunk_reader.clone();
|
let chunk_reader = self.chunk_reader.clone();
|
||||||
let save_file = self.save_file.clone();
|
let level_info = self.level_folder.clone();
|
||||||
let world_gen = self.world_gen.clone();
|
let world_gen = self.world_gen.clone();
|
||||||
let chunk_pos = *at;
|
let chunk_pos = *at;
|
||||||
|
|
||||||
@ -241,20 +234,18 @@ impl Level {
|
|||||||
.get(&chunk_pos)
|
.get(&chunk_pos)
|
||||||
.map(|entry| entry.value().clone())
|
.map(|entry| entry.value().clone())
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
let loaded_chunk = save_file
|
let loaded_chunk =
|
||||||
.and_then(|save_file| {
|
match Self::load_chunk_from_save(chunk_reader, &level_info, chunk_pos) {
|
||||||
match Self::load_chunk_from_save(chunk_reader, save_file, chunk_pos) {
|
Ok(chunk) => chunk,
|
||||||
Ok(chunk) => chunk,
|
Err(err) => {
|
||||||
Err(err) => {
|
log::error!(
|
||||||
log::error!(
|
"Failed to read chunk (regenerating) {:?}: {:?}",
|
||||||
"Failed to read chunk (regenerating) {:?}: {:?}",
|
chunk_pos,
|
||||||
chunk_pos,
|
err
|
||||||
err
|
);
|
||||||
);
|
None
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos)))
|
Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos)))
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,29 +1,30 @@
|
|||||||
use std::{fs::OpenOptions, io::Read};
|
use std::{
|
||||||
|
fs::OpenOptions,
|
||||||
|
io::{Read, Write},
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
use flate2::read::GzDecoder;
|
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::level::SaveFile;
|
use crate::level::LevelFolder;
|
||||||
|
|
||||||
use super::{WorldInfo, WorldInfoError, WorldInfoReader};
|
use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};
|
||||||
|
|
||||||
pub struct AnvilInfoReader {}
|
const LEVEL_DAT_FILE_NAME: &str = "level.dat";
|
||||||
|
|
||||||
impl AnvilInfoReader {
|
pub struct AnvilLevelInfo;
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorldInfoReader for AnvilInfoReader {
|
impl WorldInfoReader for AnvilLevelInfo {
|
||||||
fn read_world_info(&self, save_file: &SaveFile) -> Result<WorldInfo, WorldInfoError> {
|
fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError> {
|
||||||
let path = save_file.root_folder.join("level.dat");
|
let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
|
||||||
|
|
||||||
let mut world_info_file = OpenOptions::new().read(true).open(path)?;
|
let mut world_info_file = OpenOptions::new().read(true).open(path)?;
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
world_info_file.read_to_end(&mut buffer)?;
|
world_info_file.read_to_end(&mut buffer)?;
|
||||||
|
|
||||||
|
// try to decompress using GZip
|
||||||
let mut decoder = GzDecoder::new(&buffer[..]);
|
let mut decoder = GzDecoder::new(&buffer[..]);
|
||||||
let mut decompressed_data = Vec::new();
|
let mut decompressed_data = Vec::new();
|
||||||
decoder.read_to_end(&mut decompressed_data)?;
|
decoder.read_to_end(&mut decompressed_data)?;
|
||||||
@ -31,63 +32,57 @@ impl WorldInfoReader for AnvilInfoReader {
|
|||||||
let info = fastnbt::from_bytes::<LevelDat>(&decompressed_data)
|
let info = fastnbt::from_bytes::<LevelDat>(&decompressed_data)
|
||||||
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
|
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
|
||||||
|
|
||||||
Ok(WorldInfo {
|
// todo check version
|
||||||
seed: info.data.world_gen_settings.seed,
|
|
||||||
})
|
Ok(info.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AnvilInfoReader {
|
impl WorldInfoWriter for AnvilLevelInfo {
|
||||||
fn default() -> Self {
|
fn write_world_info(
|
||||||
Self::new()
|
&self,
|
||||||
|
info: LevelData,
|
||||||
|
level_folder: &LevelFolder,
|
||||||
|
) -> Result<(), WorldInfoError> {
|
||||||
|
let start = SystemTime::now();
|
||||||
|
let since_the_epoch = start
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards");
|
||||||
|
let level = LevelDat {
|
||||||
|
data: LevelData {
|
||||||
|
allow_commands: info.allow_commands,
|
||||||
|
data_version: info.data_version,
|
||||||
|
difficulty: info.difficulty,
|
||||||
|
world_gen_settings: info.world_gen_settings,
|
||||||
|
last_played: since_the_epoch.as_millis() as i64,
|
||||||
|
level_name: info.level_name,
|
||||||
|
spawn_x: info.spawn_x,
|
||||||
|
spawn_y: info.spawn_y,
|
||||||
|
spawn_z: info.spawn_z,
|
||||||
|
nbt_version: info.nbt_version,
|
||||||
|
version: info.version,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// convert it into nbt
|
||||||
|
let nbt = pumpkin_nbt::serializer::to_bytes_unnamed(&level).unwrap();
|
||||||
|
// now compress using GZip, TODO: im not sure about the to_vec, but writer is not implemented for BytesMut, see https://github.com/tokio-rs/bytes/pull/478
|
||||||
|
let mut encoder = GzEncoder::new(nbt.to_vec(), Compression::best());
|
||||||
|
let compressed_data = Vec::new();
|
||||||
|
encoder.write_all(&compressed_data)?;
|
||||||
|
|
||||||
|
// open file
|
||||||
|
let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
|
||||||
|
let mut world_info_file = OpenOptions::new().write(true).open(path)?;
|
||||||
|
// write compressed data into file
|
||||||
|
world_info_file.write_all(&compressed_data).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct LevelDat {
|
pub struct LevelDat {
|
||||||
// No idea why its formatted like this
|
// This tag contains all the level data.
|
||||||
#[serde(rename = "Data")]
|
#[serde(rename = "Data")]
|
||||||
pub data: WorldData,
|
pub data: LevelData,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub struct WorldData {
|
|
||||||
pub world_gen_settings: WorldGenSettings,
|
|
||||||
// TODO: Implement the rest of the fields
|
|
||||||
// Fields below this comment are being deserialized, but are not being used
|
|
||||||
pub spawn_x: i32,
|
|
||||||
pub spawn_y: i32,
|
|
||||||
pub spawn_z: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct WorldGenSettings {
|
|
||||||
pub seed: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
level::SaveFile,
|
|
||||||
world_info::{anvil::AnvilInfoReader, WorldInfo, WorldInfoReader},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_level_dat_reading() {
|
|
||||||
let world_info = AnvilInfoReader::new();
|
|
||||||
let root_folder = PathBuf::from("test-files").join("sample-1");
|
|
||||||
let save_file = SaveFile {
|
|
||||||
root_folder: root_folder.clone(),
|
|
||||||
region_folder: root_folder,
|
|
||||||
};
|
|
||||||
let expected = WorldInfo {
|
|
||||||
seed: -79717552349559436,
|
|
||||||
};
|
|
||||||
let info = world_info.read_world_info(&save_file).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(info, expected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,113 @@
|
|||||||
|
use pumpkin_config::BASIC_CONFIG;
|
||||||
|
use pumpkin_core::Difficulty;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::level::SaveFile;
|
use crate::{generation::Seed, level::LevelFolder};
|
||||||
|
|
||||||
pub mod anvil;
|
pub mod anvil;
|
||||||
|
|
||||||
pub(crate) trait WorldInfoReader {
|
pub(crate) trait WorldInfoReader {
|
||||||
fn read_world_info(&self, save_file: &SaveFile) -> Result<WorldInfo, WorldInfoError>;
|
fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
pub(crate) trait WorldInfoWriter: Sync + Send {
|
||||||
pub struct WorldInfo {
|
fn write_world_info(
|
||||||
|
&self,
|
||||||
|
info: LevelData,
|
||||||
|
level_folder: &LevelFolder,
|
||||||
|
) -> Result<(), WorldInfoError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct LevelData {
|
||||||
|
// true if cheats are enabled.
|
||||||
|
pub allow_commands: bool,
|
||||||
|
// An integer displaying the data version.
|
||||||
|
pub data_version: i32,
|
||||||
|
// The current difficulty setting.
|
||||||
|
pub difficulty: Difficulty,
|
||||||
|
// the generation settings for each dimension.
|
||||||
|
pub world_gen_settings: WorldGenSettings,
|
||||||
|
// The Unix time in milliseconds when the level was last loaded.
|
||||||
|
pub last_played: i64,
|
||||||
|
// The name of the level.
|
||||||
|
pub level_name: String,
|
||||||
|
// The X coordinate of the world spawn.
|
||||||
|
pub spawn_x: i32,
|
||||||
|
// The Y coordinate of the world spawn.
|
||||||
|
pub spawn_y: i32,
|
||||||
|
// The Z coordinate of the world spawn.
|
||||||
|
pub spawn_z: i32,
|
||||||
|
#[serde(rename = "version")]
|
||||||
|
// The NBT version of the level
|
||||||
|
pub nbt_version: i32,
|
||||||
|
#[serde(rename = "Version")]
|
||||||
|
pub version: WorldVersion,
|
||||||
|
// TODO: Implement the rest of the fields
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct WorldGenSettings {
|
||||||
|
// the numerical seed of the world
|
||||||
pub seed: i64,
|
pub seed: i64,
|
||||||
// TODO: Implement all fields
|
}
|
||||||
|
|
||||||
|
fn get_or_create_seed() -> Seed {
|
||||||
|
// TODO: if there is a seed in the config (!= 0) use it. Otherwise make a random one
|
||||||
|
Seed::from(BASIC_CONFIG.seed.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WorldGenSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
seed: get_or_create_seed().0 as i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct WorldVersion {
|
||||||
|
// The version name as a string, e.g. "15w32b".
|
||||||
|
pub name: String,
|
||||||
|
// An integer displaying the data version.
|
||||||
|
pub id: i32,
|
||||||
|
// Whether the version is a snapshot or not.
|
||||||
|
pub snapshot: bool,
|
||||||
|
// Developing series. In 1.18 experimental snapshots, it was set to "ccpreview". In others, set to "main".
|
||||||
|
pub series: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WorldVersion {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "1.24.4".to_string(),
|
||||||
|
id: -1,
|
||||||
|
snapshot: false,
|
||||||
|
series: "main".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LevelData {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
allow_commands: true,
|
||||||
|
// TODO
|
||||||
|
data_version: -1,
|
||||||
|
difficulty: Difficulty::Normal,
|
||||||
|
world_gen_settings: Default::default(),
|
||||||
|
last_played: -1,
|
||||||
|
level_name: "world".to_string(),
|
||||||
|
spawn_x: 0,
|
||||||
|
spawn_y: 200,
|
||||||
|
spawn_z: 0,
|
||||||
|
nbt_version: -1,
|
||||||
|
version: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|||||||
Binary file not shown.
@ -32,7 +32,7 @@ impl CommandExecutor for StopExecutor {
|
|||||||
for player in server.get_all_players().await {
|
for player in server.get_all_players().await {
|
||||||
player.kick(kick_message.clone()).await;
|
player.kick(kick_message.clone()).await;
|
||||||
}
|
}
|
||||||
|
server.save().await;
|
||||||
std::process::exit(0)
|
std::process::exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -220,18 +220,18 @@ impl Player {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Decrement value of watched chunks
|
// Decrement value of watched chunks
|
||||||
let chunks_to_clean = world.mark_chunks_as_not_watched(&radial_chunks);
|
let chunks_to_clean = world.level.mark_chunks_as_not_watched(&radial_chunks);
|
||||||
|
|
||||||
// Remove chunks with no watchers from the cache
|
// Remove chunks with no watchers from the cache
|
||||||
world.clean_chunks(&chunks_to_clean);
|
world.level.clean_chunks(&chunks_to_clean);
|
||||||
// Remove left over entries from all possiblily loaded chunks
|
// Remove left over entries from all possiblily loaded chunks
|
||||||
world.clean_memory(&radial_chunks);
|
world.level.clean_memory(&radial_chunks);
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Removed player id {} ({}) ({} chunks remain cached)",
|
"Removed player id {} ({}) ({} chunks remain cached)",
|
||||||
self.gameprofile.name,
|
self.gameprofile.name,
|
||||||
self.client.id,
|
self.client.id,
|
||||||
self.world().get_cached_chunk_len()
|
self.world().level.loaded_chunk_count()
|
||||||
);
|
);
|
||||||
|
|
||||||
//self.world().level.list_cached();
|
//self.world().level.list_cached();
|
||||||
|
|||||||
@ -174,6 +174,12 @@ impl Server {
|
|||||||
self.server_listing.lock().await.remove_player();
|
self.server_listing.lock().await.remove_player();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self) {
|
||||||
|
for world in &self.worlds {
|
||||||
|
world.save().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn try_get_container(
|
pub async fn try_get_container(
|
||||||
&self,
|
&self,
|
||||||
player_id: EntityId,
|
player_id: EntityId,
|
||||||
|
|||||||
@ -118,6 +118,10 @@ impl World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self) {
|
||||||
|
self.level.save().await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Broadcasts a packet to all connected players within the world.
|
/// Broadcasts a packet to all connected players within the world.
|
||||||
///
|
///
|
||||||
/// Sends the specified packet to every player currently logged in to the world.
|
/// Sends the specified packet to every player currently logged in to the world.
|
||||||
@ -523,26 +527,6 @@ impl World {
|
|||||||
player.set_health(20.0, 20, 20.0).await;
|
player.set_health(20.0, 20, 20.0).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_chunks_as_not_watched(&self, chunks: &[Vector2<i32>]) -> Vec<Vector2<i32>> {
|
|
||||||
self.level.mark_chunks_as_not_watched(chunks)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark_chunks_as_watched(&self, chunks: &[Vector2<i32>]) {
|
|
||||||
self.level.mark_chunks_as_newly_watched(chunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clean_chunks(&self, chunks: &[Vector2<i32>]) {
|
|
||||||
self.level.clean_chunks(chunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clean_memory(&self, chunks_to_check: &[Vector2<i32>]) {
|
|
||||||
self.level.clean_memory(chunks_to_check);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cached_chunk_len(&self) -> usize {
|
|
||||||
self.level.loaded_chunk_count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IMPORTANT: Chunks have to be non-empty
|
/// IMPORTANT: Chunks have to be non-empty
|
||||||
fn spawn_world_chunks(
|
fn spawn_world_chunks(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@ -79,12 +79,18 @@ pub async fn update_position(player: &Arc<Player>) {
|
|||||||
|
|
||||||
// Make sure the watched section and the chunk watcher updates are async atomic. We want to
|
// Make sure the watched section and the chunk watcher updates are async atomic. We want to
|
||||||
// ensure what we unload when the player disconnects is correct
|
// ensure what we unload when the player disconnects is correct
|
||||||
entity.world.mark_chunks_as_watched(&loading_chunks);
|
entity
|
||||||
let chunks_to_clean = entity.world.mark_chunks_as_not_watched(&unloading_chunks);
|
.world
|
||||||
|
.level
|
||||||
|
.mark_chunks_as_newly_watched(&loading_chunks);
|
||||||
|
let chunks_to_clean = entity
|
||||||
|
.world
|
||||||
|
.level
|
||||||
|
.mark_chunks_as_not_watched(&unloading_chunks);
|
||||||
player.watched_section.store(new_cylindrical);
|
player.watched_section.store(new_cylindrical);
|
||||||
|
|
||||||
if !chunks_to_clean.is_empty() {
|
if !chunks_to_clean.is_empty() {
|
||||||
entity.world.clean_chunks(&chunks_to_clean);
|
entity.world.level.clean_chunks(&chunks_to_clean);
|
||||||
|
|
||||||
// This can take a little if we are sending a bunch of packets, queue it up :p
|
// This can take a little if we are sending a bunch of packets, queue it up :p
|
||||||
let client = player.client.clone();
|
let client = player.client.clone();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user