Pumpkin/pumpkin-world/src/level.rs
Alexander Medvedev 222b40c8bd Add World struct
We also check now if world exits
2024-08-19 21:14:48 +01:00

262 lines
9.5 KiB
Rust

use std::{
fs::OpenOptions,
io::{Read, Seek},
path::PathBuf,
};
use flate2::{bufread::ZlibDecoder, read::GzDecoder};
use itertools::Itertools;
use rayon::prelude::*;
use thiserror::Error;
use tokio::sync::mpsc;
use crate::chunk::ChunkData;
#[allow(dead_code)]
/// The Level represents a
pub struct Level {
root_folder: PathBuf,
region_folder: PathBuf,
}
#[derive(Error, Debug)]
pub enum WorldError {
// using ErrorKind instead of Error, beacuse the function read_chunks and read_region_chunks is designed to return an error on a per-chunk basis, while std::io::Error does not implement Copy or Clone
#[error("Io error: {0}")]
IoError(std::io::ErrorKind),
#[error("Region not found")]
RegionNotFound,
#[error("Region is invalid")]
RegionIsInvalid,
#[error("Chunk not found")]
ChunkNotFound,
#[error("Compression Error")]
Compression(CompressionError),
#[error("Error deserializing chunk: {0}")]
ErrorDeserializingChunk(String),
#[error("The requested block state id does not exist")]
BlockStateIdNotFound,
}
#[derive(Error, Debug)]
pub enum CompressionError {
#[error("Compression scheme not recognised")]
UnknownCompression,
#[error("Error while working with zlib compression: {0}")]
ZlibError(std::io::Error),
#[error("Error while working with Gzip compression: {0}")]
GZipError(std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
Gzip,
Zlib,
None,
LZ4,
}
impl Compression {
pub fn from_byte(byte: u8) -> Option<Self> {
match byte {
1 => Some(Self::Gzip),
2 => Some(Self::Zlib),
3 => Some(Self::None),
4 => Some(Self::LZ4),
_ => None,
}
}
}
impl Level {
pub fn from_root_folder(root_folder: PathBuf) -> Self {
assert!(root_folder.exists(), "World root folder does not exist!");
let region_folder = root_folder.join("region");
assert!(
region_folder.exists(),
"World region folder does not exist!"
);
Level {
root_folder,
region_folder,
}
}
// /// Read one chunk in the world
// ///
// /// Do not use this function if reading many chunks is required, since in case those two chunks which are read separately using `.read_chunk` are in the same region file, it will need to be opened and closed separately for both of them, leading to a performance loss.
// pub async fn read_chunk(&self, chunk: (i32, i32)) -> Result<ChunkData, WorldError> {
// self.read_chunks(vec![chunk])
// .await
// .pop()
// .expect("Read chunks must return a chunk")
// .1
// }
/// Read many chunks in a world
/// MUST be called from a tokio runtime thread
///
/// Note: The order of the output chunks will almost never be in the same order as the order of input chunks
pub async fn read_chunks(
&self,
chunks: Vec<(i32, i32)>,
channel: mpsc::Sender<((i32, i32), Result<ChunkData, WorldError>)>,
) {
chunks
.into_par_iter()
.map(|chunk| {
let region = (
((chunk.0 as f32) / 32.0).floor() as i32,
((chunk.1 as f32) / 32.0).floor() as i32,
);
let channel = channel.clone();
// return different error when file is not found (because that means that the chunks have just not been generated yet)
let mut region_file = match OpenOptions::new().read(true).open(
self.region_folder
.join(format!("r.{}.{}.mca", region.0, region.1)),
) {
Ok(f) => f,
Err(err) => match err.kind() {
std::io::ErrorKind::NotFound => {
let _ = channel.blocking_send((chunk, Err(WorldError::RegionNotFound)));
return;
}
_ => {
let _ = channel
.blocking_send((chunk, Err(WorldError::IoError(err.kind()))));
return;
}
},
};
let mut location_table: [u8; 4096] = [0; 4096];
let mut timestamp_table: [u8; 4096] = [0; 4096];
// fill the location and timestamp tables
{
match region_file.read_exact(&mut location_table) {
Ok(_) => {}
Err(err) => {
let _ = channel
.blocking_send((chunk, Err(WorldError::IoError(err.kind()))));
return;
}
}
match region_file.read_exact(&mut timestamp_table) {
Ok(_) => {}
Err(err) => {
let _ = channel
.blocking_send((chunk, Err(WorldError::IoError(err.kind()))));
return;
}
}
}
let modulus = |a: i32, b: i32| ((a % b) + b) % b;
let chunk_x = modulus(chunk.0, 32) as u32;
let chunk_z = modulus(chunk.1, 32) as u32;
let channel = channel.clone();
let table_entry = (chunk_x + chunk_z * 32) * 4;
let mut offset = vec![0u8];
offset.extend_from_slice(
&location_table[table_entry as usize..table_entry as usize + 3],
);
let offset = u32::from_be_bytes(offset.try_into().unwrap()) as u64 * 4096;
let size = location_table[table_entry as usize + 3] as usize * 4096;
if offset == 0 && size == 0 {
let _ =
channel.blocking_send(((chunk.0, chunk.1), Err(WorldError::ChunkNotFound)));
return;
}
// Read the file using the offset and size
let mut file_buf = {
let seek_result = region_file.seek(std::io::SeekFrom::Start(offset));
if seek_result.is_err() {
let _ = channel
.blocking_send(((chunk.0, chunk.1), Err(WorldError::RegionIsInvalid)));
return;
}
let mut out = vec![0; size];
let read_result = region_file.read_exact(&mut out);
if read_result.is_err() {
let _ = channel
.blocking_send(((chunk.0, chunk.1), Err(WorldError::RegionIsInvalid)));
return;
}
out
};
// TODO: check checksum to make sure chunk is not corrupted
let header = file_buf.drain(0..5).collect_vec();
let compression = match Compression::from_byte(header[4]) {
Some(c) => c,
None => {
let _ = channel.blocking_send((
(chunk.0, chunk.1),
Err(WorldError::Compression(
CompressionError::UnknownCompression,
)),
));
return;
}
};
let size = u32::from_be_bytes(header[..4].try_into().unwrap());
// size includes the compression scheme byte, so we need to subtract 1
let chunk_data = file_buf.drain(0..size as usize - 1).collect_vec();
let decompressed_chunk = match Self::decompress_data(compression, chunk_data) {
Ok(data) => data,
Err(e) => {
channel
.blocking_send(((chunk.0, chunk.1), Err(WorldError::Compression(e))))
.expect("Failed to send Compression error");
return;
}
};
channel
.blocking_send((chunk, ChunkData::from_bytes(decompressed_chunk, chunk)))
.expect("Error sending decompressed chunk");
})
.collect::<Vec<()>>();
}
fn decompress_data(
compression: Compression,
compressed_data: Vec<u8>,
) -> Result<Vec<u8>, CompressionError> {
match compression {
Compression::Gzip => {
let mut z = GzDecoder::new(&compressed_data[..]);
let mut chunk_data = Vec::new();
match z.read_to_end(&mut chunk_data) {
Ok(_) => {}
Err(e) => {
return Err(CompressionError::GZipError(e));
}
}
Ok(chunk_data)
}
Compression::Zlib => {
let mut z = ZlibDecoder::new(&compressed_data[..]);
let mut chunk_data = Vec::new();
match z.read_to_end(&mut chunk_data) {
Ok(_) => {}
Err(e) => {
return Err(CompressionError::ZlibError(e));
}
}
Ok(chunk_data)
}
Compression::None => Ok(compressed_data),
Compression::LZ4 => todo!(),
}
}
}