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 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(×tamp.to_be_bytes())?;
self.file.sync_data()?;
Ok(grown as u64)
}
}