mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 15:21:31 +11:00
9c9f672a22
## Description Closes #291 - Update extractors to support Minecraft 1.19.4 - Update code generators. - Changed generated entity component names to avoid name collisions. - Update `glam` version. - Added `Encode` and `Decode` for `glam` types in `valence_protocol`. - Fixed inconsistent packet names and assign packet IDs automatically. - Remove `ident` and rename `ident_str` to `ident`. - Rework registry codec configuration. Biomes and dimensions exist as entities.`BiomeRegistry` and `DimensionTypeRegistry` resources have been added. The vanilla registry codec is loaded at startup. ### Issues - Creating new instances has become more tedious than it should be. This will be addressed later. ## Test Plan Steps: 1. Boot up a vanilla server with online mode disabled. 2. Run the `packet_inspector`. 3. Connect to the vanilla server through the packet inspector to ensure all packets are updated correctly. 4. Close the vanilla server and try some valence examples.
175 lines
5.3 KiB
Rust
175 lines
5.3 KiB
Rust
use std::collections::btree_map::Entry;
|
|
use std::collections::BTreeMap;
|
|
use std::fs::File;
|
|
use std::io;
|
|
use std::io::{ErrorKind, Read, Seek, SeekFrom};
|
|
use std::path::PathBuf;
|
|
|
|
use byteorder::{BigEndian, ReadBytesExt};
|
|
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
|
use thiserror::Error;
|
|
#[cfg(feature = "valence")]
|
|
pub use to_valence::*;
|
|
use valence_nbt::Compound;
|
|
|
|
#[cfg(feature = "valence")]
|
|
mod to_valence;
|
|
|
|
#[derive(Debug)]
|
|
pub struct AnvilWorld {
|
|
/// Path to the "region" subdirectory in the world root.
|
|
region_root: PathBuf,
|
|
// TODO: LRU cache for region file handles.
|
|
/// Maps region (x, z) positions to region files.
|
|
regions: BTreeMap<(i32, i32), Region>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Debug)]
|
|
pub struct AnvilChunk {
|
|
/// This chunk's NBT data.
|
|
pub data: Compound,
|
|
/// The time this chunk was last modified measured in seconds since the
|
|
/// epoch.
|
|
pub timestamp: u32,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
#[non_exhaustive]
|
|
pub enum ReadChunkError {
|
|
#[error(transparent)]
|
|
Io(#[from] io::Error),
|
|
#[error(transparent)]
|
|
Nbt(#[from] valence_nbt::Error),
|
|
#[error("invalid chunk sector offset")]
|
|
BadSectorOffset,
|
|
#[error("invalid chunk size")]
|
|
BadChunkSize,
|
|
#[error("unknown compression scheme number of {0}")]
|
|
UnknownCompressionScheme(u8),
|
|
#[error("not all chunk NBT data was read")]
|
|
IncompleteNbtRead,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Region {
|
|
file: File,
|
|
/// The first 8 KiB in the file.
|
|
header: [u8; SECTOR_SIZE * 2],
|
|
}
|
|
|
|
const SECTOR_SIZE: usize = 4096;
|
|
|
|
impl AnvilWorld {
|
|
pub fn new(world_root: impl Into<PathBuf>) -> Self {
|
|
let mut region_root = world_root.into();
|
|
region_root.push("region");
|
|
|
|
Self {
|
|
region_root,
|
|
regions: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Reads a chunk from the file system with the given chunk coordinates. If
|
|
/// no chunk exists at the position, then `None` is returned.
|
|
pub fn read_chunk(
|
|
&mut self,
|
|
chunk_x: i32,
|
|
chunk_z: i32,
|
|
) -> Result<Option<AnvilChunk>, ReadChunkError> {
|
|
let region_x = chunk_x.div_euclid(32);
|
|
let region_z = chunk_z.div_euclid(32);
|
|
|
|
let region = match self.regions.entry((region_x, region_z)) {
|
|
Entry::Vacant(ve) => {
|
|
// Load the region file if it exists. Otherwise, the chunk is considered absent.
|
|
|
|
// TODO: Add tombstone for missing region file in `regions`.
|
|
|
|
let path = self
|
|
.region_root
|
|
.join(format!("r.{region_x}.{region_z}.mca"));
|
|
|
|
let mut file = match File::options().read(true).write(true).open(path) {
|
|
Ok(file) => file,
|
|
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
|
|
Err(e) => return Err(e.into()),
|
|
};
|
|
|
|
let mut header = [0; SECTOR_SIZE * 2];
|
|
|
|
file.read_exact(&mut header)?;
|
|
|
|
ve.insert(Region { file, header })
|
|
}
|
|
Entry::Occupied(oe) => oe.into_mut(),
|
|
};
|
|
|
|
let chunk_idx = (chunk_x.rem_euclid(32) + chunk_z.rem_euclid(32) * 32) as usize;
|
|
|
|
let location_bytes = (®ion.header[chunk_idx * 4..]).read_u32::<BigEndian>()?;
|
|
let timestamp = (®ion.header[chunk_idx * 4 + SECTOR_SIZE..]).read_u32::<BigEndian>()?;
|
|
|
|
if location_bytes == 0 {
|
|
// No chunk exists at this position.
|
|
return Ok(None);
|
|
}
|
|
|
|
let sector_offset = (location_bytes >> 8) as u64;
|
|
let sector_count = (location_bytes & 0xff) as usize;
|
|
|
|
if sector_offset < 2 {
|
|
// If the sector offset was <2, then the chunk data would be inside the region
|
|
// header. That doesn't make any sense.
|
|
return Err(ReadChunkError::BadSectorOffset);
|
|
}
|
|
|
|
// Seek to the beginning of the chunk's data.
|
|
region
|
|
.file
|
|
.seek(SeekFrom::Start(sector_offset * SECTOR_SIZE as u64))?;
|
|
|
|
let exact_chunk_size = region.file.read_u32::<BigEndian>()? as usize;
|
|
|
|
if exact_chunk_size > sector_count * SECTOR_SIZE {
|
|
// Sector size of this chunk must always be >= the exact size.
|
|
return Err(ReadChunkError::BadChunkSize);
|
|
}
|
|
|
|
let mut data_buf = vec![0; exact_chunk_size].into_boxed_slice();
|
|
region.file.read_exact(&mut data_buf)?;
|
|
|
|
let mut r = data_buf.as_ref();
|
|
|
|
let mut decompress_buf = vec![];
|
|
|
|
// What compression does the chunk use?
|
|
let mut nbt_slice = match r.read_u8()? {
|
|
// GZip
|
|
1 => {
|
|
let mut z = GzDecoder::new(r);
|
|
z.read_to_end(&mut decompress_buf)?;
|
|
decompress_buf.as_slice()
|
|
}
|
|
// Zlib
|
|
2 => {
|
|
let mut z = ZlibDecoder::new(r);
|
|
z.read_to_end(&mut decompress_buf)?;
|
|
decompress_buf.as_slice()
|
|
}
|
|
// Uncompressed
|
|
3 => r,
|
|
// Unknown
|
|
b => return Err(ReadChunkError::UnknownCompressionScheme(b)),
|
|
};
|
|
|
|
let (data, _) = valence_nbt::from_binary_slice(&mut nbt_slice)?;
|
|
|
|
if !nbt_slice.is_empty() {
|
|
return Err(ReadChunkError::IncompleteNbtRead);
|
|
}
|
|
|
|
Ok(Some(AnvilChunk { data, timestamp }))
|
|
}
|
|
}
|