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 / region.rs

//! Port of `RegionFile`: 32x32 chunks per file, stored in 4 KiB sectors behind an offset
//! table and a timestamp table.

use std::fs::{File, OpenOptions};
use std::io::ErrorKind::{InvalidData, InvalidInput};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;

use common::world::ChunkPos;
use flate2::read::{GzDecoder, ZlibDecoder};

use super::now_millis;

const SECTOR: usize = 4096;
const VERSION_GZIP: u8 = 1;
const VERSION_DEFLATE: u8 = 2;

pub struct RegionFile {
    file: File,
    offsets: [u32; 1024],
    free: Vec<bool>,
    /// Slots whose header entry points outside the file or into another chunk's sectors.
    damaged: Box<[bool; 1024]>,
    /// The bytes opening added: a new header or the padding to whole sectors.
    pub created: u64,
}

fn slot(pos: ChunkPos) -> usize {
    (pos.x & 31) as usize + (pos.z & 31) as usize * 32
}

/// The first sector and the sector count of an offset table entry.
fn sectors(offset: u32) -> (usize, usize) {
    ((offset >> 8) as usize, (offset & 0xFF) as usize)
}

impl RegionFile {
    pub fn open(path: &Path) -> io::Result<Self> {
        let (mut file, created) = match OpenOptions::new()
            .read(true)
            .write(true)
            .create_new(true)
            .open(path)
        {
            Ok(file) => (file, true),
            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
                (OpenOptions::new().read(true).write(true).open(path)?, false)
            }
            Err(error) => return Err(error),
        };
        let mut len = file.metadata()?.len();
        let initial = len;
        if created {
            file.write_all(&[0; 2 * SECTOR])?;
            len = 2 * SECTOR as u64;
        } else if len < 2 * SECTOR as u64 {
            return Err(io::Error::new(InvalidData, "truncated region header"));
        }
        if len % SECTOR as u64 != 0 {
            let padding = SECTOR as u64 - len % SECTOR as u64;
            file.seek(SeekFrom::End(0))?;
            file.write_all(&vec![0; padding as usize])?;
            len += padding;
        }
        let mut free = vec![true; (len / SECTOR as u64) as usize];
        free[0] = false;
        free[1] = false;

        let mut header = [0; SECTOR];
        file.seek(SeekFrom::Start(0))?;
        file.read_exact(&mut header)?;
        let offsets: [u32; 1024] = std::array::from_fn(|i| {
            u32::from_be_bytes(header[i * 4..i * 4 + 4].try_into().unwrap_or_default())
        });
        // Like the original, a bad entry only makes its own chunk unreadable
        let mut damaged = Box::new([false; 1024]);
        for (slot, &offset) in offsets.iter().enumerate() {
            let (start, count) = sectors(offset);
            if offset == 0 {
                continue;
            }
            if start < 2
                || count == 0
                || start + count > free.len()
                || free[start..start + count].contains(&false)
            {
                damaged[slot] = true;
                continue;
            }
            free[start..start + count].fill(false);
        }
        Ok(Self {
            file,
            offsets,
            free,
            damaged,
            created: len - initial,
        })
    }

    /// Missing chunks are distinct from damaged chunks
    pub fn read(&mut self, pos: ChunkPos) -> io::Result<Option<Vec<u8>>> {
        let offset = self.offsets[slot(pos)];
        let (start, count) = sectors(offset);
        if offset == 0 {
            return Ok(None);
        }
        if self.damaged[slot(pos)] {
            return Err(io::Error::new(
                InvalidData,
                "invalid or overlapping region sectors",
            ));
        }
        self.file.seek(SeekFrom::Start((start * SECTOR) as u64))?;
        let mut prefix = [0; 5];
        self.file.read_exact(&mut prefix)?;
        let len = u32::from_be_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]) as usize;
        if len == 0 || len + 4 > SECTOR * count {
            return Err(io::Error::new(InvalidData, "invalid chunk length"));
        }
        let mut compressed = vec![0; len - 1];
        self.file.read_exact(&mut compressed)?;
        let mut data = Vec::new();
        match prefix[4] {
            VERSION_GZIP => GzDecoder::new(&compressed[..]).read_to_end(&mut data)?,
            VERSION_DEFLATE => ZlibDecoder::new(&compressed[..]).read_to_end(&mut data)?,
            _ => return Err(io::Error::new(InvalidData, "unknown chunk compression")),
        };
        Ok(Some(data))
    }

    /// Writes the payload to unused sectors before publishing its offset
    /// Returns how many bytes the file grew
    pub fn write(&mut self, pos: ChunkPos, compressed: &[u8]) -> io::Result<u64> {
        let slot = slot(pos);
        let offset = self.offsets[slot];
        let (old_start, count) = sectors(offset);
        let needed = compressed
            .len()
            .checked_add(5)
            .ok_or_else(|| io::Error::other("chunk is too large"))?
            .div_ceil(SECTOR);
        if needed >= 256 {
            return Err(io::Error::new(
                InvalidInput,
                "chunk exceeds the McRegion sector limit",
            ));
        }
        let mut run = 0;
        let start = self
            .free
            .iter()
            .enumerate()
            .find_map(|(i, &free)| {
                run = if free { run + 1 } else { 0 };
                (run == needed).then_some(i + 1 - run)
            })
            .unwrap_or(self.free.len());
        if start > 0xFF_FFFF {
            return Err(io::Error::other("region exceeds the McRegion offset limit"));
        }
        let grown = (start + needed).saturating_sub(self.free.len()) * SECTOR;
        if grown != 0 {
            self.file.set_len(((start + needed) * SECTOR) as u64)?;
            self.free.resize(start + needed, true);
        }
        self.file.seek(SeekFrom::Start((start * SECTOR) as u64))?;
        self.file
            .write_all(&(compressed.len() as u32 + 1).to_be_bytes())?;
        self.file.write_all(&[VERSION_DEFLATE])?;
        self.file.write_all(compressed)?;
        self.file.sync_data()?;
        let entry = (start << 8 | needed) as u32;
        self.file.seek(SeekFrom::Start((slot * 4) as u64))?;
        self.file.write_all(&entry.to_be_bytes())?;
        self.offsets[slot] = entry;
        self.free[start..start + needed].fill(false);
        if offset != 0 && !self.damaged[slot] {
            self.free[old_start..old_start + count].fill(true);
        }
        self.damaged[slot] = false;
        let timestamp = (now_millis() / 1000) as u32;
        self.file
            .seek(SeekFrom::Start((SECTOR + slot * 4) as u64))?;
        self.file.write_all(&timestamp.to_be_bytes())?;
        self.file.sync_data()?;
        Ok(grown as u64)
    }
}