Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

root / crates / server / src / save / maps.rs

use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::path::PathBuf;

use common::map::Map;
use common::nbt::Compound;
use rustc_hash::FxHashMap;

use super::{SaveError, io_error, read_gzip, sync, write_gzip};

pub struct StoredMap {
    pub map: Map,
    pub revisions: [u64; 128],
    pub ticks: i32,
    dirty: bool,
    extra: Compound,
}
impl StoredMap {
    pub fn new(map: Map) -> Self {
        Self {
            map,
            revisions: [0; 128],
            ticks: 0,
            dirty: true,
            extra: Default::default(),
        }
    }
    pub fn changed(&mut self, x: usize) {
        self.revisions[x] = self.revisions[x].wrapping_add(1);
        self.dirty = true;
    }
}

#[derive(Clone)]
pub struct Holder {
    pub maps: Vec<i16>,
    pub position: glam::DVec3,
    pub yaw: f32,
    pub dimension: i8,
}

#[derive(Default)]
pub struct Maps {
    directory: Option<PathBuf>,
    ids: Compound,
    ids_dirty: bool,
    pub loaded: FxHashMap<i16, StoredMap>,
    pub holders: FxHashMap<i32, Holder>,
}

impl Maps {
    pub fn open(world: &std::path::Path) -> Result<Self, SaveError> {
        let directory = world.join("data");
        let path = directory.join("idcounts.dat");
        // `MapStorage.loadIdCounts` starts over from unreadable counters
        let ids = match File::open(&path) {
            Ok(file) => Compound::read(&mut BufReader::new(file)).unwrap_or_else(|error| {
                tracing::warn!("cannot read {}: {error}", path.display());
                Compound::default()
            }),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Compound::default(),
            Err(error) => {
                tracing::warn!("cannot read {}: {error}", path.display());
                Compound::default()
            }
        };
        Ok(Self {
            directory: Some(directory),
            ids,
            ..Default::default()
        })
    }
    pub fn load(&mut self, id: i16) -> Result<bool, SaveError> {
        if self.loaded.contains_key(&id) {
            return Ok(true);
        }
        let Some(directory) = &self.directory else {
            return Ok(false);
        };
        let path = directory.join(format!("map_{id}.dat"));
        if !path.exists() {
            return Ok(false);
        }
        let root = read_gzip(&path)?;
        let extra = root.compound("data").cloned().unwrap_or_default();
        let mut stored = StoredMap::new(Map::read(&extra));
        stored.extra = extra;
        stored.dirty = false;
        self.loaded.insert(id, stored);
        Ok(true)
    }
    pub fn create(&mut self, center: [i32; 2], dimension: i8) -> Result<i16, SaveError> {
        let start = if self.ids.contains("map") {
            self.ids.short("map").wrapping_add(1)
        } else {
            0
        };
        let mut id = start;
        while self.load(id)? {
            id = id.wrapping_add(1);
            if id == start {
                return Err(SaveError::Invalid("all map identifiers are in use".into()));
            }
        }
        self.ids.set_short("map", id);
        self.ids_dirty = true;
        self.loaded
            .insert(id, StoredMap::new(Map::new(center, dimension)));
        Ok(id)
    }
    pub fn save(&mut self) -> Result<(), SaveError> {
        if !self.ids_dirty && !self.loaded.values().any(|map| map.dirty) {
            return Ok(());
        }
        let Some(directory) = &self.directory else {
            return Ok(());
        };
        std::fs::create_dir_all(directory).map_err(io_error(directory))?;
        // Publish map data before its allocated identifier
        for (&id, map) in &mut self.loaded {
            if !map.dirty {
                continue;
            }
            let mut data = map.extra.clone();
            map.map.write(&mut data);
            let mut root = Compound::default();
            root.set_compound("data", data);
            atomic_nbt(directory.join(format!("map_{id}.dat")), &root, true)?;
            map.dirty = false;
        }
        if self.ids_dirty {
            atomic_nbt(directory.join("idcounts.dat"), &self.ids, false)?;
            self.ids_dirty = false;
        }
        sync(directory)
    }
}

fn atomic_nbt(path: PathBuf, nbt: &Compound, gzip: bool) -> Result<(), SaveError> {
    let temporary = path.with_extension("dat.tmp");
    if gzip {
        write_gzip(&temporary, nbt)?;
    } else {
        let mut writer = BufWriter::new(File::create(&temporary).map_err(io_error(&temporary))?);
        nbt.write(&mut writer)
            .and_then(|()| writer.flush())
            .and_then(|()| writer.get_ref().sync_all())
            .map_err(io_error(&temporary))?;
    }
    std::fs::rename(&temporary, &path).map_err(io_error(&path))
}