alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates server src save mod.rs
//! World saves in the McRegion format of `SaveHandler`, `McRegionChunkLoader` and `WorldInfo`.
pub mod catalog;
mod chunk;
mod conversion;
pub mod maps;
mod region;
mod writer;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::{SystemTime, UNIX_EPOCH};
use common::nbt::{Compound, NbtError};
use common::world::ChunkPos;
use flate2::Compression;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use rustc_hash::FxHashMap;
pub use chunk::LoadedChunk;
use region::RegionFile;
pub use writer::ChunkStorage;
/// `SaveConverterMcRegion`'s format version.
pub const MCREGION_VERSION: i32 = 19132;
const MAX_OPEN_REGIONS: usize = 256;
#[derive(Debug, thiserror::Error)]
pub enum SaveError {
#[error("{path}: {source}")]
Io { path: PathBuf, source: io::Error },
#[error("{path}: {source}")]
Nbt { path: PathBuf, source: NbtError },
#[error("the save is being accessed from another location, aborting")]
SessionLock,
#[error("{0}")]
Invalid(String),
}
fn io_error(path: &Path) -> impl FnOnce(io::Error) -> SaveError + '_ {
move |source| SaveError::Io {
path: path.to_owned(),
source,
}
}
/// Locks a mutex, recovering from a panic in another thread.
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn sync(path: &Path) -> Result<(), SaveError> {
File::open(path)
.and_then(|file| file.sync_all())
.map_err(io_error(path))
}
pub fn now_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis() as i64)
}
/// The `level.dat` contents of `WorldInfo`.
#[derive(Clone, Debug, Default)]
pub struct WorldInfo {
extra: Compound,
pub seed: i64,
pub spawn: [i32; 3],
pub time: i64,
pub size_on_disk: i64,
pub name: String,
pub version: i32,
pub raining: bool,
pub rain_time: i32,
pub thundering: bool,
pub thunder_time: i32,
/// The singleplayer player, stored in `level.dat`.
pub player: Option<Compound>,
}
impl WorldInfo {
pub fn new(seed: i64, name: &str) -> Self {
Self {
seed,
name: name.to_owned(),
version: MCREGION_VERSION,
..Self::default()
}
}
fn from_nbt(nbt: &Compound) -> Self {
Self {
extra: nbt.clone(),
seed: nbt.long("RandomSeed"),
spawn: [nbt.int("SpawnX"), nbt.int("SpawnY"), nbt.int("SpawnZ")],
time: nbt.long("Time"),
size_on_disk: nbt.long("SizeOnDisk"),
name: nbt.string("LevelName").to_owned(),
version: nbt.int("version"),
raining: nbt.bool("raining"),
rain_time: nbt.int("rainTime"),
thundering: nbt.bool("thundering"),
thunder_time: nbt.int("thunderTime"),
player: nbt.compound("Player").cloned(),
}
}
fn to_nbt(&self) -> Compound {
let mut nbt = self.extra.clone();
nbt.set_long("RandomSeed", self.seed);
nbt.set_int("SpawnX", self.spawn[0]);
nbt.set_int("SpawnY", self.spawn[1]);
nbt.set_int("SpawnZ", self.spawn[2]);
nbt.set_long("Time", self.time);
nbt.set_long("SizeOnDisk", self.size_on_disk);
nbt.set_long("LastPlayed", now_millis());
nbt.set_string("LevelName", self.name.clone());
nbt.set_int("version", self.version);
nbt.set_int("rainTime", self.rain_time);
nbt.set_bool("raining", self.raining);
nbt.set_int("thunderTime", self.thunder_time);
nbt.set_bool("thundering", self.thundering);
if let Some(player) = &self.player {
nbt.set_compound("Player", player.clone());
}
nbt
}
/// The dimension the stored player is in.
pub fn dimension(&self) -> i32 {
self.player
.as_ref()
.map_or(0, |player| player.int("Dimension"))
}
}
pub fn read_gzip(path: &Path) -> Result<Compound, SaveError> {
let file = File::open(path).map_err(io_error(path))?;
Compound::read(&mut GzDecoder::new(BufReader::new(file))).map_err(|source| SaveError::Nbt {
path: path.to_owned(),
source,
})
}
/// Replaces `level.dat` through `level.dat_new`, keeping a readable previous one as `level.dat_old`.
fn replace_level_dat(dir: &Path, root: &Compound) -> Result<(), SaveError> {
let new = dir.join("level.dat_new");
let current = dir.join("level.dat");
write_gzip(&new, root)?;
if current.try_exists().map_err(io_error(¤t))?
&& read_gzip(¤t).is_ok_and(|root| root.compound("Data").is_some())
{
let backup = dir.join("level.dat_old_new");
let old = dir.join("level.dat_old");
std::fs::copy(¤t, &backup).map_err(io_error(&backup))?;
sync(&backup)?;
std::fs::rename(&backup, &old).map_err(io_error(&old))?;
}
std::fs::rename(&new, ¤t).map_err(io_error(¤t))?;
sync(dir)
}
pub fn write_gzip(path: &Path, nbt: &Compound) -> Result<(), SaveError> {
let file = File::create(path).map_err(io_error(path))?;
let mut encoder = GzEncoder::new(BufWriter::new(file), Compression::default());
nbt.write(&mut encoder).map_err(io_error(path))?;
encoder
.finish()
.and_then(|mut writer| {
writer.flush()?;
writer.get_ref().sync_all()
})
.map_err(io_error(path))
}
#[derive(Default)]
struct Regions {
dir: PathBuf,
open: FxHashMap<(i32, i32), RegionFile>,
/// Bytes the region files grew by since the last `level.dat` save.
growth: u64,
}
#[derive(Clone)]
struct SessionLock {
path: PathBuf,
stamp: i64,
}
impl SessionLock {
fn check(&self) -> io::Result<()> {
if std::fs::read(&self.path)? != self.stamp.to_be_bytes() {
return Err(io::Error::other(
"the save is being accessed from another location, aborting",
));
}
Ok(())
}
fn verify(&self) -> Result<(), SaveError> {
self.check().map_err(|_| SaveError::SessionLock)
}
}
/// Player names have 1 to 16 ASCII letters, digits or underscores.
pub(crate) fn valid_name(name: &str) -> bool {
(1..=16).contains(&name.len()) && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
impl Regions {
fn region(&mut self, pos: ChunkPos, create: bool) -> io::Result<Option<&mut RegionFile>> {
let key = (pos.x >> 5, pos.z >> 5);
if !self.open.contains_key(&key) {
if self.open.len() >= MAX_OPEN_REGIONS {
self.open.clear();
}
let path = self.dir.join(format!("r.{}.{}.mcr", key.0, key.1));
if create {
std::fs::create_dir_all(&self.dir)?;
} else if !path.try_exists()? {
return Ok(None);
}
let region = RegionFile::open(&path)?;
self.growth += region.created;
self.open.insert(key, region);
}
Ok(self.open.get_mut(&key))
}
}
/// Port of `SaveHandler`: one world directory with its session lock, `level.dat` and dimensions.
pub struct WorldSave {
dir: PathBuf,
session: SessionLock,
pub overworld: Arc<ChunkStorage>,
nether: Mutex<Option<Arc<ChunkStorage>>>,
}
impl WorldSave {
/// Opens or creates a world directory and takes its session lock.
pub fn open(dir: &Path) -> Result<Self, SaveError> {
Self::open_converting(dir, &|_| {})
}
/// Opens the save, converting a legacy one to McRegion with `progress` in percent.
pub fn open_converting(dir: &Path, progress: &dyn Fn(i32)) -> Result<Self, SaveError> {
let mut info = load_info(dir)?;
if let Some(info) = &info
&& !matches!(info.version, 0 | MCREGION_VERSION)
{
return Err(SaveError::Invalid(
"This world must be converted to McRegion before it can be played".into(),
));
}
std::fs::create_dir_all(dir.join("data")).map_err(io_error(dir))?;
static LAST_SESSION: AtomicI64 = AtomicI64::new(0);
LAST_SESSION.fetch_max(now_millis(), Ordering::Relaxed);
let stamp = LAST_SESSION.fetch_add(1, Ordering::Relaxed);
let path = dir.join("session.lock");
std::fs::write(&path, stamp.to_be_bytes()).map_err(io_error(&path))?;
let session = SessionLock { path, stamp };
let save = Self {
dir: dir.to_owned(),
overworld: Arc::new(ChunkStorage::with_session(dir.to_owned(), session.clone())?),
session,
nether: Default::default(),
};
if let Some(info) = &mut info
&& info.version == 0
{
conversion::convert_chunks(dir, &save.session, progress)?;
info.version = MCREGION_VERSION;
save.save_info(info)?;
}
Ok(save)
}
pub fn save_player(&self, name: &str, nbt: &Compound) -> Result<(), SaveError> {
if !valid_name(name) {
return Err(SaveError::Invalid("invalid player name".into()));
}
self.check_session_lock()?;
let directory = self.dir.join("players");
std::fs::create_dir_all(&directory).map_err(io_error(&directory))?;
let temporary = directory.join(format!("{name}.dat.tmp"));
let path = directory.join(format!("{name}.dat"));
write_gzip(&temporary, nbt)?;
self.check_session_lock()?;
std::fs::rename(&temporary, &path).map_err(io_error(&path))?;
sync(&directory)
}
pub fn dir(&self) -> &Path {
&self.dir
}
/// Storage of the Nether, in `DIM-1`.
pub fn nether(&self) -> Result<Arc<ChunkStorage>, SaveError> {
let mut nether = lock(&self.nether);
if let Some(storage) = &*nether {
return Ok(storage.clone());
}
let storage = Arc::new(ChunkStorage::with_session(
self.dir.join("DIM-1"),
self.session.clone(),
)?);
*nether = Some(storage.clone());
Ok(storage)
}
/// `SaveHandler.checkSessionLock`: fails once another program opened the world.
pub fn check_session_lock(&self) -> Result<(), SaveError> {
self.session.verify()
}
/// `SaveHandler.loadWorldInfo`, falling back to `level.dat_old`.
pub fn load_info(&self) -> Result<Option<WorldInfo>, SaveError> {
load_info(&self.dir)
}
/// `SaveHandler.saveWorldInfo`: writes `level.dat_new`, keeps the previous file as `level.dat_old`.
pub fn save_info(&self, info: &mut WorldInfo) -> Result<(), SaveError> {
self.check_session_lock()?;
info.size_on_disk += self.overworld.take_growth() as i64;
if let Some(nether) = &*lock(&self.nether) {
info.size_on_disk += nether.take_growth() as i64;
}
let mut root = Compound::default();
root.set_compound("Data", info.to_nbt());
replace_level_dat(&self.dir, &root)
}
}
pub fn load_info(dir: &Path) -> Result<Option<WorldInfo>, SaveError> {
let mut failure = None;
for name in ["level.dat", "level.dat_old"] {
let path = dir.join(name);
if !path.try_exists().map_err(io_error(&path))? {
continue;
}
match read_gzip(&path).map(|root| root.compound("Data").map(WorldInfo::from_nbt)) {
Ok(Some(info)) => return Ok(Some(info)),
Ok(None) => {
let error = format!("{} has no world data", path.display());
failure = Some(SaveError::Invalid(error));
}
Err(error) => failure = Some(error),
}
}
failure.map_or(Ok(None), Err)
}