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 common src protocol packet.rs
//! The packets of protocol 14 and their wire format, from the `Packet` subclasses.
use std::io::{self, Read, Write};
use std::sync::Arc;
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
use crate::item::{Item, ItemStack};
use crate::nbt;
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("bad packet id {0}")]
BadId(u8),
#[error("string length {length} outside 0..={max}")]
StringLength { length: i16, max: usize },
#[error("invalid string")]
BadString,
#[error("bad compressed chunk data")]
BadChunkData,
}
/// A value of `DataWatcher`, sent as entity metadata.
#[derive(Clone, Debug, PartialEq)]
pub enum MetadataValue {
Byte(i8),
Short(i16),
Int(i32),
Float(f32),
String(String),
Item(ItemStack),
Position([i32; 3]),
}
/// Entity metadata entries: index (0 to 31) and value.
pub type Metadata = Vec<(u8, MetadataValue)>;
/// A value with a fixed big-endian wire format.
trait Field: Sized {
fn read(r: &mut impl Read) -> Result<Self, ProtocolError>;
fn write(&self, w: &mut impl Write) -> io::Result<()>;
}
fn read<T: Field>(r: &mut impl Read) -> Result<T, ProtocolError> {
T::read(r)
}
fn invalid(message: &'static str) -> ProtocolError {
io::Error::new(io::ErrorKind::InvalidData, message).into()
}
macro_rules! numbers {
($($t:ty),*) => {$(
impl Field for $t {
fn read(r: &mut impl Read) -> Result<Self, ProtocolError> {
let mut bytes = [0; size_of::<$t>()];
r.read_exact(&mut bytes)?;
Ok(Self::from_be_bytes(bytes))
}
fn write(&self, w: &mut impl Write) -> io::Result<()> {
w.write_all(&self.to_be_bytes())
}
}
)*};
}
numbers!(u8, i8, u16, i16, i32, i64, f32, f64);
impl Field for bool {
fn read(r: &mut impl Read) -> Result<Self, ProtocolError> {
Ok(read::<u8>(r)? != 0)
}
fn write(&self, w: &mut impl Write) -> io::Result<()> {
u8::from(*self).write(w)
}
}
impl<T: Field + Default + Copy, const N: usize> Field for [T; N] {
fn read(r: &mut impl Read) -> Result<Self, ProtocolError> {
let mut values = [T::default(); N];
for value in &mut values {
*value = read(r)?;
}
Ok(values)
}
fn write(&self, w: &mut impl Write) -> io::Result<()> {
self.iter().try_for_each(|value| value.write(w))
}
}
fn read_stack(id: i16, r: &mut impl Read) -> Result<ItemStack, ProtocolError> {
let count = i32::from(read::<i8>(r)?);
Ok(ItemStack::new(Item(id), count, i32::from(read::<i16>(r)?)))
}
impl Field for ItemStack {
fn read(r: &mut impl Read) -> Result<Self, ProtocolError> {
read_stack(read(r)?, r)
}
fn write(&self, w: &mut impl Write) -> io::Result<()> {
self.item.0.write(w)?;
(self.count as i8).write(w)?;
(self.damage as i16).write(w)
}
}
impl Field for Option<ItemStack> {
fn read(r: &mut impl Read) -> Result<Self, ProtocolError> {
let id: i16 = read(r)?;
if id < 0 {
return Ok(None);
}
read_stack(id, r).map(Some)
}
fn write(&self, w: &mut impl Write) -> io::Result<()> {
match self {
Some(stack) => stack.write(w),
None => (-1i16).write(w),
}
}
}
impl Field for Metadata {
/// `DataWatcher.readWatchableObjects`.
fn read(r: &mut impl Read) -> Result<Self, ProtocolError> {
let mut entries = Vec::new();
loop {
let header: u8 = read(r)?;
if header == 127 {
return Ok(entries);
}
if entries.len() == 32 {
return Err(invalid("Too many entity metadata entries"));
}
let value = match header >> 5 {
0 => MetadataValue::Byte(read(r)?),
1 => MetadataValue::Short(read(r)?),
2 => MetadataValue::Int(read(r)?),
3 => MetadataValue::Float(read(r)?),
4 => MetadataValue::String(read_string(r, 64)?),
5 => MetadataValue::Item(read(r)?),
_ => MetadataValue::Position(read(r)?),
};
entries.push((header & 31, value));
}
}
fn write(&self, w: &mut impl Write) -> io::Result<()> {
for (index, value) in self {
let kind: u8 = match value {
MetadataValue::Byte(_) => 0,
MetadataValue::Short(_) => 1,
MetadataValue::Int(_) => 2,
MetadataValue::Float(_) => 3,
MetadataValue::String(_) => 4,
MetadataValue::Item(_) => 5,
MetadataValue::Position(_) => 6,
};
(kind << 5 | index & 31).write(w)?;
match value {
MetadataValue::Byte(v) => v.write(w)?,
MetadataValue::Short(v) => v.write(w)?,
MetadataValue::Int(v) => v.write(w)?,
MetadataValue::Float(v) => v.write(w)?,
MetadataValue::String(v) => write_string(w, v)?,
MetadataValue::Item(stack) => stack.write(w)?,
MetadataValue::Position(p) => p.write(w)?,
}
}
127u8.write(w)
}
}
/// `Packet.readString`: UTF-16 code units with a 16-bit length.
fn read_string(r: &mut impl Read, max: usize) -> Result<String, ProtocolError> {
let length: i16 = read(r)?;
if length < 0 || length as usize > max {
return Err(ProtocolError::StringLength { length, max });
}
let units = (0..length)
.map(|_| read(r))
.collect::<Result<Vec<u16>, _>>()?;
Ok(String::from_utf16_lossy(&units))
}
/// `Packet.writeString`.
fn write_string(w: &mut impl Write, text: &str) -> io::Result<()> {
let units: Vec<u16> = text.encode_utf16().collect();
i16::try_from(units.len())
.map_err(|_| io::Error::other("String too big"))?
.write(w)?;
units.iter().try_for_each(|unit| unit.write(w))
}
fn read_bytes(r: &mut impl Read, len: usize) -> Result<Vec<u8>, ProtocolError> {
if len > 64 << 20 {
return Err(invalid("Packet data is too large"));
}
let mut bytes = vec![0; len];
r.read_exact(&mut bytes)?;
Ok(bytes)
}
macro_rules! put {
($w:ident, $($value:expr),*) => {
$(Field::write($value, $w)?;)*
};
}
macro_rules! read_field {
($r:ident) => {
read($r)?
};
($r:ident, $max:literal) => {
read_string($r, $max)?
};
}
macro_rules! write_field {
($w:ident, $value:ident) => {
$value.write($w)?
};
($w:ident, $value:ident, $max:literal) => {
write_string($w, $value)?
};
}
/// Declares `Packet` with its ids. Plain packets are read and written field by field, a
/// `String[max]` field with the length limit of `Packet.readString`; `custom` packets have
/// hand-written formats and `local` packets never leave the process.
macro_rules! packets {
(
$($(#[$doc:meta])* $id:literal $name:ident
$({ $($field:ident: $ty:ty $([$max:literal])?),* })?)*
custom:
$($(#[$cdoc:meta])* $cid:literal $cname:ident
{ $($(#[$cfdoc:meta])* $cfield:ident: $cty:ty),* })*
local:
$($(#[$ldoc:meta])* $lname:ident { $($lfield:ident: $lty:ty),* })*
) => {
#[derive(Clone, Debug, PartialEq)]
pub enum Packet {
$($(#[$doc])* $name $({ $($field: $ty),* })?,)*
$($(#[$cdoc])* $cname { $($(#[$cfdoc])* $cfield: $cty),* },)*
$($(#[$ldoc])* $lname { $($lfield: $lty),* },)*
}
impl Packet {
/// The id of `Packet.addIdClassMapping`, or `None` for packets that never leave
/// the process.
pub fn id(&self) -> Option<u8> {
match self {
$(Self::$name { .. } => Some($id),)*
$(Self::$cname { .. } => Some($cid),)*
$(Self::$lname { .. })|* => None,
}
}
fn read_body(id: u8, r: &mut impl Read) -> Result<Self, ProtocolError> {
Ok(match id {
$($id => Self::$name $({ $($field: read_field!(r $(, $max)?)),* })?,)*
_ => Self::read_custom(id, r)?,
})
}
/// `Packet.writePacket`. Chunk data is compressed here; local-only packets write
/// nothing.
pub fn write(&self, w: &mut impl Write) -> io::Result<()> {
let Some(id) = self.id() else {
return Ok(());
};
id.write(w)?;
match self {
$(Self::$name $({ $($field),* })? => {
$($(write_field!(w, $field $(, $max)?);)*)?
})*
_ => self.write_custom(w)?,
}
Ok(())
}
}
};
}
packets! {
0 KeepAlive
/// Client to server: protocol version and username. Server to client: entity id in place
/// of the version, with the world seed and dimension.
1 Login { version_or_entity: i32, username: String[16], seed: i64, dimension: i8 }
2 Handshake { username: String[32] }
3 Chat { message: String[119] }
4 UpdateTime { time: i64 }
5 PlayerInventory { entity: i32, slot: i16, item: i16, damage: i16 }
6 SpawnPosition { x: i32, y: i32, z: i32 }
7 UseEntity { player: i32, target: i32, left_click: bool }
8 UpdateHealth { health: i16 }
9 Respawn { dimension: i8 }
10 Flying { on_ground: bool }
11 PlayerPosition { x: f64, y: f64, stance: f64, z: f64, on_ground: bool }
12 PlayerLook { yaw: f32, pitch: f32, on_ground: bool }
/// Client to server `y` is the feet and `stance` the eye height; server to client the reverse.
13 PlayerPositionLook {
x: f64, y: f64, stance: f64, z: f64, yaw: f32, pitch: f32, on_ground: bool
}
14 BlockDig { status: u8, x: i32, y: u8, z: i32, face: u8 }
15 Place { x: i32, y: u8, z: i32, direction: u8, item: Option<ItemStack> }
16 BlockItemSwitch { slot: i16 }
17 Sleep { entity: i32, status: i8, x: i32, y: i8, z: i32 }
18 Animation { entity: i32, animation: i8 }
19 EntityAction { entity: i32, action: i8 }
20 NamedEntitySpawn {
entity: i32, name: String[16], x: i32, y: i32, z: i32, yaw: i8, pitch: i8, item: i16
}
21 PickupSpawn {
entity: i32, item: ItemStack, x: i32, y: i32, z: i32, rotation: i8, pitch: i8, roll: i8
}
22 Collect { collected: i32, collector: i32 }
24 MobSpawn {
entity: i32, kind: i8, x: i32, y: i32, z: i32, yaw: i8, pitch: i8, metadata: Metadata
}
25 Painting { entity: i32, title: String[13], x: i32, y: i32, z: i32, direction: i32 }
/// `Packet27Position`, unused by the original client.
27 VehicleInput {
strafe: f32, forward: f32, pitch: f32, yaw: f32, sneaking: bool, jumping: bool
}
28 EntityVelocity { entity: i32, velocity: [i16; 3] }
29 DestroyEntity { entity: i32 }
30 Entity { entity: i32 }
31 RelEntityMove { entity: i32, delta: [i8; 3] }
32 EntityLook { entity: i32, yaw: i8, pitch: i8 }
33 RelEntityMoveLook { entity: i32, delta: [i8; 3], yaw: i8, pitch: i8 }
34 EntityTeleport { entity: i32, x: i32, y: i32, z: i32, yaw: i8, pitch: i8 }
38 EntityStatus { entity: i32, status: i8 }
39 AttachEntity { entity: i32, vehicle: i32 }
40 EntityMetadata { entity: i32, metadata: Metadata }
50 PreChunk { x: i32, z: i32, load: bool }
53 BlockChange { x: i32, y: u8, z: i32, block: u8, metadata: u8 }
54 PlayNoteBlock { x: i32, y: i16, z: i32, instrument: u8, pitch: u8 }
/// `Packet61DoorChange`: an auxiliary effect such as a sound or particles.
61 AuxEffect { effect: i32, x: i32, y: i8, z: i32, data: i32 }
/// `Packet70Bed`: 0 invalid bed, 1 rain starts, 2 rain stops.
70 GameState { state: i8 }
71 Weather { entity: i32, lightning: bool, x: i32, y: i32, z: i32 }
101 CloseWindow { window: i8 }
102 WindowClick {
window: i8, slot: i16, button: i8, action: i16, shift: bool, item: Option<ItemStack>
}
103 SetSlot { window: i8, slot: i16, item: Option<ItemStack> }
105 UpdateProgressBar { window: i8, bar: i16, value: i16 }
106 Transaction { window: i8, action: i16, accepted: bool }
200 Statistic { id: i32, amount: i8 }
255 Disconnect { reason: String[100] }
custom:
23 VehicleSpawn {
entity: i32, kind: i8, x: i32, y: i32, z: i32,
/// Thrower entity id, with the velocity sent only when positive.
thrower: i32,
velocity: [i16; 3]
}
/// A box of blocks, uncompressed in the layout of `World.getChunkData`: per chunk column
/// run, ids, then metadata, block light and sky light nibbles.
51 MapChunk { x: i32, y: i16, z: i32, size: [u8; 3], data: Arc<[u8]> }
52 MultiBlockChange {
chunk_x: i32,
chunk_z: i32,
/// `x << 12 | z << 8 | y` within the chunk, block id, metadata.
changes: Vec<(i16, u8, u8)>
}
60 Explosion {
x: f64, y: f64, z: f64, size: f32,
/// Destroyed blocks relative to the truncated explosion position.
blocks: Vec<[i8; 3]>
}
100 OpenWindow { window: i8, kind: i8, title: String, slots: i8 }
104 WindowItems { window: i8, items: Vec<Option<ItemStack>> }
130 UpdateSign { x: i32, y: i16, z: i32, lines: [String; 4] }
131 MapData { item: i16, map: i16, data: Vec<u8> }
local:
/// Local only: tile entity state the original only had in singleplayer, such as the mob
/// shown in a spawner.
TileEntity { x: i32, y: i32, z: i32, data: Box<crate::world::TileEntity> }
/// Local only: a world sound, which the original server never sends.
Sound { name: String, pos: [f64; 3], volume: f32, pitch: f32 }
/// Local only: a particle effect.
Particle { name: String, pos: [f64; 3], velocity: [f64; 3] }
/// Local only: the weather flags singleplayer clients fade towards, and the strengths
/// to start from.
WeatherState { raining: bool, thundering: bool, rain: f32, thunder: f32 }
/// Local only: `EntityLiving.attackedAtYaw` of a player, for its hurt camera.
HurtDirection { entity: i32, yaw: f32 }
/// Local simulation state without multiplayer position rounding
EntityState {
entity: i32,
pos: [f64; 3],
rotation: [f32; 3],
walk: [f32; 2],
hurt: i32,
death: i32,
fuse: i32,
special: crate::entity::mob::SpecialPose,
vehicle: crate::entity::vehicle::VehiclePose
}
}
impl Packet {
/// `Packet3Chat(String)`: cut to the 119 UTF-16 units the reader accepts.
pub fn chat(message: impl Into<String>) -> Self {
let mut message = message.into();
let mut units = 0;
if let Some((end, _)) = message.char_indices().find(|&(_, ch)| {
units += ch.len_utf16();
units > 119
}) {
message.truncate(end);
}
Self::Chat { message }
}
/// `Packet.readPacket` on the server: client-bound ids are refused before their payload.
pub fn read_from_client(input: &mut impl Read) -> Result<Self, ProtocolError> {
const SERVER_BOUND: &[u8] = &[
0, 1, 2, 3, 7, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 27, 101, 102, 106, 130, 255,
];
Self::read_if(input, |id| SERVER_BOUND.contains(&id))
}
/// `Packet.readPacket` on the client: server-bound ids are refused before their payload.
pub fn read_from_server(input: &mut impl Read) -> Result<Self, ProtocolError> {
Self::read_if(input, |id| ![7, 14, 15, 16, 19, 27, 102].contains(&id))
}
fn read_if(input: &mut impl Read, accept: impl Fn(u8) -> bool) -> Result<Self, ProtocolError> {
let id = read(input)?;
if !accept(id) {
return Err(ProtocolError::BadId(id));
}
Self::read_body(id, input)
}
fn read_custom(id: u8, r: &mut impl Read) -> Result<Self, ProtocolError> {
Ok(match id {
23 => {
let (entity, kind, [x, y, z]) = (read(r)?, read(r)?, read(r)?);
let thrower: i32 = read(r)?;
let velocity = if thrower > 0 { read(r)? } else { [0; 3] };
Self::VehicleSpawn {
entity,
kind,
x,
y,
z,
thrower,
velocity,
}
}
51 => {
let (x, y, z, size) = (read(r)?, read(r)?, read(r)?, read::<[u8; 3]>(r)?);
let length =
usize::try_from(read::<i32>(r)?).map_err(|_| ProtocolError::BadChunkData)?;
let volume = size.iter().map(|&s| usize::from(s) + 1).product::<usize>();
let mut data = vec![0; volume * 5 / 2];
ZlibDecoder::new(&read_bytes(r, length)?[..])
.read_exact(&mut data)
.map_err(|_| ProtocolError::BadChunkData)?;
Self::MapChunk {
x,
y,
z,
size,
data: data.into(),
}
}
52 => {
let (chunk_x, chunk_z) = (read(r)?, read(r)?);
let count = usize::from(read::<u16>(r)?);
let coordinates = (0..count)
.map(|_| read(r))
.collect::<Result<Vec<i16>, _>>()?;
let (blocks, metadata) = (read_bytes(r, count)?, read_bytes(r, count)?);
let changes = (0..count)
.map(|i| (coordinates[i], blocks[i], metadata[i]))
.collect();
Self::MultiBlockChange {
chunk_x,
chunk_z,
changes,
}
}
60 => {
let (x, y, z, size) = (read(r)?, read(r)?, read(r)?, read(r)?);
let count: i32 = read(r)?;
if !(0..=1 << 20).contains(&count) {
return Err(invalid("Invalid explosion size"));
}
let blocks = (0..count).map(|_| read(r)).collect::<Result<_, _>>()?;
Self::Explosion {
x,
y,
z,
size,
blocks,
}
}
100 => Self::OpenWindow {
window: read(r)?,
kind: read(r)?,
title: nbt::read_utf(r).map_err(|_| ProtocolError::BadString)?,
slots: read(r)?,
},
104 => {
let window = read(r)?;
let count = read::<i16>(r)?.max(0);
let items = (0..count).map(|_| read(r)).collect::<Result<_, _>>()?;
Self::WindowItems { window, items }
}
130 => Self::UpdateSign {
x: read(r)?,
y: read(r)?,
z: read(r)?,
lines: [
read_string(r, 15)?,
read_string(r, 15)?,
read_string(r, 15)?,
read_string(r, 15)?,
],
},
131 => {
let (item, map, length) = (read(r)?, read(r)?, read::<u8>(r)?);
let data = read_bytes(r, length.into())?;
Self::MapData { item, map, data }
}
id => return Err(ProtocolError::BadId(id)),
})
}
fn write_custom(&self, w: &mut impl Write) -> io::Result<()> {
match self {
Self::VehicleSpawn {
entity,
kind,
x,
y,
z,
thrower,
velocity,
} => {
put!(w, entity, kind, x, y, z, thrower);
if *thrower > 0 {
velocity.write(w)?;
}
}
Self::MapChunk {
x,
y,
z,
size,
data,
} => {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data)?;
let compressed = encoder.finish()?;
put!(w, x, y, z, size, &(compressed.len() as i32));
w.write_all(&compressed)?;
}
Self::MultiBlockChange {
chunk_x,
chunk_z,
changes,
} => {
put!(w, chunk_x, chunk_z, &(changes.len() as i16));
changes.iter().try_for_each(|c| c.0.write(w))?;
changes.iter().try_for_each(|c| c.1.write(w))?;
changes.iter().try_for_each(|c| c.2.write(w))?;
}
Self::Explosion {
x,
y,
z,
size,
blocks,
} => {
put!(w, x, y, z, size, &(blocks.len() as i32));
blocks.iter().try_for_each(|block| block.write(w))?;
}
Self::OpenWindow {
window,
kind,
title,
slots,
} => {
put!(w, window, kind);
nbt::write_utf(w, title)?;
slots.write(w)?;
}
Self::WindowItems { window, items } => {
put!(w, window, &(items.len() as i16));
items.iter().try_for_each(|item| item.write(w))?;
}
Self::UpdateSign { x, y, z, lines } => {
put!(w, x, y, z);
lines.iter().try_for_each(|line| write_string(w, line))?;
}
Self::MapData { item, map, data } => {
put!(w, item, map, &(data.len() as u8));
w.write_all(data)?;
}
_ => unreachable!("plain and local packets are written by `write`"),
}
Ok(())
}
}